Project

General

Profile

Bug #10427 ยป ga510.py

deac8597 - Dan Smith, 03/08/2023 11:36 PM

 
1
# Copyright 2011 Dan Smith <dsmith@danplanet.com>
2
# Portions copyright 2023 Dave Liske <dave@micuisine.com>
3
# Portions copyright 2020 by Jiauxn Yang <jiaxun.yang@flygoat.com>
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

    
18
import logging
19
import struct
20

    
21
from chirp import bandplan_na
22
from chirp import bitwise
23
from chirp import chirp_common
24
from chirp import directory
25
from chirp import errors
26
from chirp import memmap
27
from chirp.settings import RadioSetting, RadioSettingGroup, RadioSettings
28
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
29
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
30

    
31
LOG = logging.getLogger(__name__)
32

    
33
try:
34
    from builtins import bytes
35
    has_future = True
36
except ImportError:
37
    has_future = False
38
    LOG.debug('python-future package is not available; '
39
              '%s requires it' % __name__)
40

    
41
# GA510 and SHX8800 also have DTCS code 645
42
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
43

    
44
DTMFCHARS = '0123456789ABCD*#'
45

    
46

    
47
def reset(radio):
48
    radio.pipe.write(b'E')
49

    
50

    
51
def start_program(radio):
52
    reset(radio)
53
    radio.pipe.read(256)
54
    radio.pipe.write(radio._magic)
55
    ack = radio.pipe.read(256)
56
    if not ack.endswith(b'\x06'):
57
        LOG.debug('Ack was %r' % ack)
58
        raise errors.RadioError('Radio did not respond to clone request')
59

    
60
    radio.pipe.write(b'F')
61

    
62
    ident = radio.pipe.read(8)
63
    LOG.debug('Radio ident string is %r' % ident)
64

    
65
    return ident
66

    
67

    
68
def do_download(radio):
69
    ident = start_program(radio)
70

    
71
    s = chirp_common.Status()
72
    s.msg = 'Downloading'
73
    s.max = 0x1C00
74

    
75
    data = bytes()
76
    for addr in range(0, 0x1C40, 0x40):
77
        cmd = struct.pack('>cHB', b'R', addr, 0x40)
78
        LOG.debug('Reading block at %04x: %r' % (addr, cmd))
79
        radio.pipe.write(cmd)
80

    
81
        block = radio.pipe.read(0x44)
82
        header = block[:4]
83
        rcmd, raddr, rlen = struct.unpack('>BHB', header)
84
        block = block[4:]
85
        if raddr != addr:
86
            raise errors.RadioError('Radio send address %04x, expected %04x' %
87
                                    (raddr, addr))
88
        if rlen != 0x40 or len(block) != 0x40:
89
            raise errors.RadioError('Radio sent %02x (%02x) bytes, '
90
                                    'expected %02x' % (rlen, len(block), 0x40))
91

    
92
        data += block
93

    
94
        s.cur = addr
95
        radio.status_fn(s)
96

    
97
    reset(radio)
98

    
99
    return data
100

    
101

    
102
def do_upload(radio):
103
    ident = start_program(radio)
104

    
105
    s = chirp_common.Status()
106
    s.msg = 'Uploading'
107
    s.max = 0x1C00
108

    
109
    # The factory software downloads 0x40 for the block
110
    # at 0x1C00, but only uploads 0x20 there. Mimic that
111
    # here.
112
    for addr in range(0, 0x1C20, 0x20):
113
        cmd = struct.pack('>cHB', b'W', addr, 0x20)
114
        LOG.debug('Writing block at %04x: %r' % (addr, cmd))
115
        block = radio._mmap[addr:addr + 0x20]
116
        radio.pipe.write(cmd)
117
        radio.pipe.write(block)
118

    
119
        ack = radio.pipe.read(1)
120
        if ack != b'\x06':
121
            raise errors.RadioError('Radio refused block at addr %04x' % addr)
122

    
123
        s.cur = addr
124
        radio.status_fn(s)
125

    
126

    
127
BASE_FORMAT = """
128
struct {
129
  lbcd rxfreq[4];
130
  lbcd txfreq[4];
131
  ul16 rxtone;
132
  ul16 txtone;
133
  u8 signal;
134
  u8 unknown1:6,
135
     pttid:2;
136
  u8 unknown2:6,
137
     power:2;
138
  u8 unknown3_0:1,
139
     narrow:1,
140
     unknown3_1:2,
141
     bcl:1,
142
     scan:1,
143
     allow_tx:1,	// SHX8800 FAMILY ONLY (unknown3_2 for GA-510)
144
     fhss:1;
145
} memories[128];
146

    
147
#seekto 0x0C00;
148
struct {
149
  char name[10];
150
  u8 pad[6];
151
} names[128];
152

    
153
"""
154

    
155
MODEL_GA510_FORMAT = """
156
#seekto 0x1A00;
157
struct {
158
  // 0x1A00
159
  u8 squelch;
160
  u8 savemode; // [off, mode1, mode2, mode3]
161
  u8 vox; // off=0
162
  u8 backlight;
163
  u8 tdr; // bool
164
  u8 timeout; // n*15 = seconds
165
  u8 beep; // bool
166
  u8 voice;
167

    
168
  // 0x1A08
169
  u8 language; // [eng, chin]
170
  u8 dtmfst;
171
  u8 scanmode; // [TO, CO, SE]
172
  u8 pttid; // [off, BOT, EOT, Both]
173
  u8 pttdelay; // 0-30
174
  u8 cha_disp; // [ch-name, ch-freq]
175
               // [ch, ch-name]; retevis
176
  u8 chb_disp;
177
  u8 bcl; // bool
178

    
179
  // 0x1A10
180
  u8 autolock; // bool
181
  u8 alarm_mode; // [site, tone, code]
182
  u8 alarmsound; // bool
183
  u8 txundertdr; // [off, bandA, bandB]
184
  u8 tailnoiseclear; // [off, on]
185
  u8 rptnoiseclr; // 10*ms, 0-1000
186
  u8 rptnoisedet;
187
  u8 roger; // bool
188

    
189
  // 0x1A18
190
  u8 unknown1a10;
191
  u8 fmradio; // boolean, inverted
192
  u8 workmode; // [vfo, chan]; 1A30-1A31 related?
193
  u8 kblock; // boolean
194
} settings;
195

    
196
#seekto 0x1A80;
197
struct {
198
  u8 skey1sp; // [off, lamp, sos, fm, noaa, moni, search]
199
  u8 skey1lp; // [off, lamp, sos, fm, noaa, moni, search]
200
  u8 skey2sp; // [off, lamp, sos, fm, noaa, moni, search]
201
  u8 skey2lp; // [off, lamp, sos, fm, noaa, moni, search]
202
} skey;
203

    
204
struct dtmfcode {
205
  u8 code[5];
206
  u8 ffpad[11]; // always 0xFF
207
};
208
#seekto 0x1B00;
209
struct dtmfcode dtmfgroup[15];
210
struct {
211
  u8 code[5];
212
  u8 groupcode; // 0->D, *, #
213
  u8 nothing:6,
214
     releasetosend:1,
215
     presstosend:1;
216
  u8 dtmfspeedon; // 80 + n*10, up to [194]
217
  u8 dtmfspeedoff;
218
} anicode;
219

    
220
//dtmf on -> 90ms
221
//dtmf off-> 120ms
222
//group code *->0
223
//press 0->1
224
//release 1->0
225

    
226
"""
227

    
228
MODEL_SHX8800_FORMAT = """
229
#seekto 0x1A00;
230
struct {
231
  // 0x1A00
232
  u8 squelch;
233
  u8 savemode; // [off, mode1, mode2, mode3]
234
  u8 vox; // off=0
235
  u8 backlight;
236
  u8 tdr; // bool
237
  u8 timeout; // n*15 = seconds
238
  u8 beep; // bool
239
  u8 voice;
240

    
241
  // 0x1A08
242
  u8 language; // [eng, chin]
243
  u8 dtmfst;
244
  u8 scanmode; // [TO, CO, SE]
245
  u8 pttid; // [off, BOT, EOT, Both]
246
  u8 pttdelay; // 0-30
247
  u8 cha_disp; // [ch-name, ch-freq]
248
               // [ch, ch-name]; retevis
249
  u8 chb_disp;
250
  u8 bcl; // bool
251

    
252
  // 0x1A10
253
  u8 autolock; // bool
254
  u8 alarm_mode; // [site, tone, code]
255
  u8 alarmsound; // bool
256
  u8 txundertdr; // [off, bandA, bandB]
257
  u8 tailnoiseclear; // [off, on]
258
  u8 rptnoiseclr; // 10*ms, 0-1000
259
  u8 rptnoisedet;
260
  u8 roger; // bool
261

    
262
  // 0x1A18
263
  u8 unknown1a10;
264
  u8 fmradio; // boolean, inverted
265
  u8 workmodeb:4,
266
     workmodea:4;
267
  u8 kblock;
268
  u8 unknown2[4];
269
  u8 voxdelay;
270
  u8 menu_timeout;
271
  u8 micgain;
272
} settings;
273

    
274
#seekto 0x1a40;
275
struct {
276
  u8 freq[8];
277
  ul16 rxtone;
278
  ul16 txtone;
279
  u8 unknown[2];
280
  u8 unused2:2,
281
     sftd:2,
282
     scode:4;
283
  u8 unknown1;
284
  u8 txpower;
285
  u8 widenarr:1,
286
     unknown2:4,
287
     fhss:1,
288
     unknown3:2;
289
  u8 band;
290
  u8 unknown4:5,
291
     step:3;
292
  u8 unknown5;
293
  u8 offset[6];
294
} vfoa;			// displays in Browser tab
295

    
296
#seekto 0x1a60;
297
struct {
298
  u8 freq[8];
299
  ul16 rxtone;
300
  ul16 txtone;
301
  u8 unknown[2];
302
  u8 unused2:2,
303
     sftd:2,
304
     scode:4;
305
  u8 unknown1;
306
  u8 txpower;
307
  u8 widenarr:1,
308
     unknown2:4,
309
     fhss:1,
310
     unknown3:2;
311
  u8 band;
312
  u8 unknown4:5,
313
     step:3;
314
  u8 unknown5;
315
  u8 offset[6];
316
} vfob;			// displays in Browser tab
317

    
318
#seekto 0x1a80;
319
struct {
320
    u8 sidekey;
321
    u8 sidekeyl;
322
} keymaps;
323

    
324
#seekto 0x1b00;
325
struct {
326
  u8 code[5];
327
  u8 unused[11];
328
} dtmfgroup[15];
329

    
330
struct {
331
  u8 code[5];
332
  u8 groupcode;
333
  u8 aniid;
334
  u8 dtmfspeedon;
335
  u8 dtmfspeedoff;
336
} anicode;
337

    
338
"""
339

    
340
PTTID = ['Off', 'BOT', 'EOT', 'Both']
341
SIGNAL = [str(i) for i in range(1, 16)]
342
WORKMODE_LIST = ["VFO", "CH"]
343
SHIFTD_LIST = ["Off", "+", "-"]
344
PTTIDCODE_LIST = ["%s" % x for x in range(1, 128)]
345
STEPS = [6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
346
STEP_LIST = [str(x) for x in STEPS]
347
TXPOWER_LIST = ["High (5W)", "Low (1W)"]
348
BANDWIDTH_LIST = ["Wide", "Narrow"]
349

    
350

    
351
@directory.register
352
class RadioddityGA510Radio(chirp_common.CloneModeRadio):
353
    VENDOR = 'Radioddity'
354
    MODEL = 'GA-510'
355
    BAUD_RATE = 9600
356
    NEEDS_COMPAT_SERIAL = False
357
    POWER_LEVELS = [
358
        chirp_common.PowerLevel('H', watts=10),
359
        chirp_common.PowerLevel('L', watts=1),
360
        chirp_common.PowerLevel('M', watts=5)]
361

    
362
    _mem_format = MODEL_GA510_FORMAT
363
    _magic = (b'PROGROMBFHU')
364

    
365
    _gmrs = False
366

    
367
    def sync_in(self):
368
        try:
369
            data = do_download(self)
370
            self._mmap = memmap.MemoryMapBytes(data)
371
        except errors.RadioError:
372
            raise
373
        except Exception as e:
374
            LOG.exception('General failure')
375
            raise errors.RadioError('Failed to download from radio: %s' % e)
376
        self.process_mmap()
377

    
378
    def sync_out(self):
379
        try:
380
            do_upload(self)
381
        except errors.RadioError:
382
            raise
383
        except Exception as e:
384
            LOG.exception('General failure')
385
            raise errors.RadioError('Failed to upload to radio: %s' % e)
386

    
387
    def process_mmap(self):
388
        self._memobj = bitwise.parse(BASE_FORMAT + self._mem_format,
389
                                     self._mmap)
390

    
391
    def get_features(self):
392
        rf = chirp_common.RadioFeatures()
393
        rf.memory_bounds = (0, 127)
394
        rf.has_ctone = True
395
        rf.has_cross = True
396
        rf.has_tuning_step = False
397
        rf.has_settings = True
398
        rf.has_bank = False
399
        rf.has_sub_devices = False
400
        rf.has_dtcs_polarity = True
401
        rf.has_rx_dtcs = True
402
        rf.can_odd_split = True
403
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
404
        rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
405
                                'DTCS->Tone', '->Tone', 'DTCS->DTCS']
406
        rf.valid_modes = ['FM', 'NFM']
407
        rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0,
408
                                 25.0, 50.0, 100.0]
409
        rf.valid_dtcs_codes = DTCS_CODES
410
        rf.valid_duplexes = ['', '-', '+', 'split', 'off']
411
        rf.valid_power_levels = self.POWER_LEVELS
412
        rf.valid_name_length = 10
413
        rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
414
                               'abcdefghijklmnopqrstuvwxyz'
415
                               '0123456789'
416
                               '!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ')
417
        rf.valid_bands = [(136000000, 174000000),
418
                          (400000000, 480000000)]
419
        return rf
420

    
421
    def get_raw_memory(self, num):
422
        return repr(self._memobj.memories[num]) + repr(self._memobj.names[num])
423

    
424
    @staticmethod
425
    def _decode_tone(toneval):
426
        if toneval in (0, 0xFFFF):
427
            LOG.debug('no tone value: %s' % toneval)
428
            return '', None, None
429
        elif toneval < 670:
430
            toneval = toneval - 1
431
            index = toneval % len(DTCS_CODES)
432
            if index != int(toneval):
433
                pol = 'R'
434
                # index -= 1
435
            else:
436
                pol = 'N'
437
            return 'DTCS', DTCS_CODES[index], pol
438
        else:
439
            return 'Tone', toneval / 10.0, 'N'
440

    
441
    @staticmethod
442
    def _encode_tone(mode, val, pol):
443
        if not mode:
444
            return 0x0000
445
        elif mode == 'Tone':
446
            return int(val * 10)
447
        elif mode == 'DTCS':
448
            index = DTCS_CODES.index(val)
449
            if pol == 'R':
450
                index += len(DTCS_CODES)
451
            index += 1
452
            LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index))
453
            return index
454
        else:
455
            raise errors.RadioError('Unsupported tone mode %r' % mode)
456

    
457
    def _get_extra(self, _mem):
458
        group = RadioSettingGroup('extra', 'Extra')
459

    
460
        s = RadioSetting('bcl', 'Busy Channel Lockout',
461
                         RadioSettingValueBoolean(_mem.bcl))
462
        group.append(s)
463

    
464
        s = RadioSetting('fhss', 'FHSS',
465
                         RadioSettingValueBoolean(_mem.fhss))
466
        group.append(s)
467

    
468
        # pttid, signal
469

    
470
        cur = PTTID[int(_mem.pttid)]
471
        s = RadioSetting('pttid', 'PTTID',
472
                         RadioSettingValueList(PTTID, cur))
473
        group.append(s)
474

    
475
        cur = SIGNAL[int(_mem.signal)]
476
        s = RadioSetting('signal', 'Signal',
477
                         RadioSettingValueList(SIGNAL, cur))
478
        group.append(s)
479

    
480
        return group
481

    
482
    def _set_extra(self, _mem, mem):
483
        _mem.bcl = int(mem.extra['bcl'].value)
484
        _mem.fhss = int(mem.extra['fhss'].value)
485
        _mem.pttid = int(mem.extra['pttid'].value)
486
        _mem.signal = int(mem.extra['signal'].value)
487

    
488
    def _is_txinh(self, _mem):
489
        raw_tx = ""
490
        for i in range(0, 4):
491
            raw_tx += _mem.txfreq[i].get_raw()
492
        return raw_tx == "\xFF\xFF\xFF\xFF"
493

    
494
    def _get_mem(self, num):
495
        return self._memobj.memories[num]
496

    
497
    def _get_nam(self, num):
498
        return self._memobj.names[num]
499

    
500
    def get_memory(self, num):
501
        _mem = self._get_mem(num)
502
        _nam = self._get_nam(num)
503
        mem = chirp_common.Memory()
504
        mem.number = num
505
        if int(_mem.rxfreq) == 166666665:
506
            mem.empty = True
507
            return mem
508

    
509
        mem.name = ''.join([str(c) for c in _nam.name
510
                            if ord(str(c)) < 127]).rstrip()
511
        mem.freq = int(_mem.rxfreq) * 10
512
        offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
513
        if self._is_txinh(_mem):
514
            mem.duplex = 'off'
515
            mem.offset = 0
516
        elif offset == 0:
517
            mem.duplex = ''
518
        elif abs(offset) < 100000000:
519
            mem.duplex = offset < 0 and '-' or '+'
520
            mem.offset = abs(offset)
521
        else:
522
            mem.duplex = 'split'
523
            mem.offset = int(_mem.txfreq) * 10
524

    
525
        mem.power = self.POWER_LEVELS[_mem.power]
526
        mem.mode = 'NFM' if _mem.narrow else 'FM'
527
        mem.skip = '' if _mem.scan else 'S'
528

    
529
        LOG.debug('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
530
        LOG.debug('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
531
        chirp_common.split_tone_decode(mem,
532
                                       self._decode_tone(_mem.txtone),
533
                                       self._decode_tone(_mem.rxtone))
534
        try:
535
            mem.extra = self._get_extra(_mem)
536
        except:
537
            LOG.exception('Failed to get extra for %i' % num)
538

    
539
        immutable = []
540

    
541
        if self._gmrs:
542
            if mem.freq in bandplan_na.ALL_GMRS_FREQS:
543
                if mem.freq in bandplan_na.GMRS_LOW:
544
                    mem.duplex = ''
545
                    mem.offset = 0
546
                    immutable = ["duplex", "offset"]
547
                if mem.freq in bandplan_na.GMRS_HHONLY:
548
                    mem.duplex = 'off'
549
                    mem.offset = 0
550
                    immutable = ["duplex", "offset"]
551
                if mem.freq in bandplan_na.GMRS_HIRPT:
552
                    immutable = ["freq"]
553
                    if mem.duplex == '+':
554
                        mem.offset = 5000000
555
                    else:
556
                        mem.duplex = ''
557
                        mem.offset = 0
558
            else:
559
                mem.duplex = 'off'
560
                mem.offset = 0
561
                immutable = ["duplex", "offset"]
562

    
563
        if self.MODEL == "RA685":
564
            if not ((mem.freq >= self.vhftx[0] and mem.freq < self.vhftx[1]) or
565
                    (mem.freq >= self.uhftx[0] and mem.freq < self.uhftx[1])):
566
                mem.duplex = 'off'
567
                mem.offset = 0
568
                immutable = ["duplex", "offset"]
569

    
570
        mem.immutable = immutable
571

    
572
        return mem
573

    
574
    def _set_mem(self, number):
575
        return self._memobj.memories[number]
576

    
577
    def _set_nam(self, number):
578
        return self._memobj.names[number]
579

    
580
    def set_memory(self, mem):
581
        _mem = self._set_mem(mem.number)
582
        _nam = self._set_nam(mem.number)
583

    
584
        if mem.empty:
585
            _mem.set_raw(b'\xff' * 16)
586
            _nam.set_raw(b'\xff' * 16)
587
            return
588

    
589
        if int(_mem.rxfreq) == 166666665:
590
            LOG.debug('Initializing new memory %i' % mem.number)
591
            _mem.set_raw(b'\x00' * 16)
592

    
593
        _nam.name = mem.name.ljust(10)
594

    
595
        if isinstance(self, Senhaix8800Radio):
596
            _mem.allow_tx = True
597

    
598
        _mem.rxfreq = mem.freq // 10
599
        if mem.duplex == '':
600
            if isinstance(self, Senhaix8800Radio):
601
                _mem.allow_tx = False
602
            _mem.txfreq = mem.freq // 10
603
        elif mem.duplex == 'split':
604
            _mem.txfreq = mem.offset // 10
605
        elif mem.duplex == 'off':
606
            for i in range(0, 4):
607
                _mem.txfreq[i].set_raw(b'\xFF')
608
        elif mem.duplex == '-':
609
            _mem.txfreq = (mem.freq - mem.offset) // 10
610
        elif mem.duplex == '+':
611
            _mem.txfreq = (mem.freq + mem.offset) // 10
612
        else:
613
            raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
614

    
615
        txtone, rxtone = chirp_common.split_tone_encode(mem)
616
        LOG.debug('tx tone is %s' % repr(txtone))
617
        LOG.debug('rx tone is %s' % repr(rxtone))
618
        _mem.txtone = self._encode_tone(*txtone)
619
        _mem.rxtone = self._encode_tone(*rxtone)
620

    
621
        try:
622
            _mem.power = self.POWER_LEVELS.index(mem.power)
623
        except ValueError:
624
            _mem.power = 0
625
        _mem.narrow = mem.mode == 'NFM'
626
        _mem.scan = mem.skip != 'S'
627
        if mem.extra:
628
            self._set_extra(_mem, mem)
629

    
630
    def get_settings(self):
631
        _set = self._memobj.settings
632

    
633
        basic = RadioSettingGroup('basic', 'Basic')
634
        adv = RadioSettingGroup('advanced', 'Advanced')
635
        dtmf = RadioSettingGroup('dtmf', 'DTMF')
636

    
637
        radioddity_settings = {
638
            'language': ['English', 'Chinese'],
639
            'savemode': ['Off', 'Mode 1', 'Mode 2', 'Mode 3'],
640
            'cha_disp': ['CH+Name', 'CH+Freq'],
641
            'chb_disp': ['CH+Name', 'CH+Freq'],
642
            'txundertdr': ['Off', 'Band A', 'Band B'],
643
            'rptnoiseclr': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
644
            'rptnoisedet': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
645
        }
646

    
647
        retevis_settings = {
648
            'language': ['English', 'Chinese'],
649
            'savemode': ['Off', 'On'],
650
            'cha_disp': ['CH', 'CH+Name'],
651
            'chb_disp': ['CH', 'CH+Name'],
652
        }
653

    
654
        ga_workmode = {
655
            'language': ['English', 'Chinese'],
656
            'workmode': ['VFO', 'Chan'],
657
        }
658

    
659
        shx_workmode = {
660
            'workmodea': ['VFO', 'Chan'],
661
            'workmodeb': ['VFO', 'Chan'],
662
        }
663

    
664
        choice_settings = {
665
            'vox': ['Off'] + ['%i' % i for i in range(1, 11)],
666
            'backlight': ['Off'] + ['%i' % i for i in range(1, 11)],
667
            'timeout': ['Off'] + ['%i' % i for i in range(15, 615, 15)],
668
            'dtmfst': ['OFF', 'KB Side Tone', 'ANI Side Tone',
669
                       'KB ST+ANI ST', 'Both'],
670
            'scanmode': ['TO', 'CO', 'SE'],
671
            'pttid': ['Off', 'BOT', 'EOT', 'Both'],
672
            'alarm_mode': ['Site', 'Tone', 'Code'],
673
        }
674

    
675
        if isinstance(self, Senhaix8800Radio):
676
            choice_settings.update(shx_workmode)
677
        else:
678
            choice_settings.update(ga_workmode)
679

    
680
        if self.VENDOR == "Retevis":
681
            choice_settings.update(retevis_settings)
682
        else:
683
            choice_settings.update(radioddity_settings)
684

    
685
        if isinstance(self, Senhaix8800Radio):
686
            basic_settings = ['timeout', 'vox', 'backlight',
687
                              'cha_disp', 'chb_disp', 'workmodea',
688
                              'workmodeb']
689
        else:
690
            basic_settings = ['timeout', 'vox', 'backlight', 'language',
691
                              'cha_disp', 'chb_disp', 'workmode']
692
        titles = {
693
            'savemode': 'Save Mode',
694
            'vox': 'VOX',
695
            'backlight': 'Auto Backlight',
696
            'timeout': 'Time Out Timer (s)',
697
            'language': 'Language',
698
            'dtmfst': 'DTMF-ST',
699
            'scanmode': 'Scan Mode',
700
            'pttid': 'PTT-ID',
701
            'cha_disp': 'Channel A Display',
702
            'chb_disp': 'Channel B Display',
703
            'alarm_mode': 'Alarm Mode',
704
            'txundertdr': 'TX Under TDR',
705
            'rptnoiseclr': 'RPT Noise Clear (ms)',
706
            'rptnoisedet': 'RPT Noise Detect (ms)',
707
            'workmode': 'Work Mode',
708
            'workmodea': 'Work Mode A',
709
            'workmodeb': 'Work Mode B',
710
        }
711

    
712
        basic.append(
713
            RadioSetting('squelch', 'Squelch Level',
714
                         RadioSettingValueInteger(0, 9, int(_set.squelch))))
715
        adv.append(
716
            RadioSetting('pttdelay', 'PTT Delay',
717
                         RadioSettingValueInteger(0, 30, int(_set.pttdelay))))
718
        adv.append(
719
            RadioSetting('tdr', 'TDR',
720
                         RadioSettingValueBoolean(
721
                             int(_set.tdr))))
722
        adv.append(
723
            RadioSetting('beep', 'Beep',
724
                         RadioSettingValueBoolean(
725
                             int(_set.beep))))
726
        basic.append(
727
            RadioSetting('voice', 'Voice Enable',
728
                         RadioSettingValueBoolean(
729
                             int(_set.voice))))
730
        adv.append(
731
            RadioSetting('bcl', 'BCL',
732
                         RadioSettingValueBoolean(
733
                             int(_set.bcl))))
734
        adv.append(
735
            RadioSetting('autolock', 'Auto Lock',
736
                         RadioSettingValueBoolean(
737
                             int(_set.autolock))))
738
        adv.append(
739
            RadioSetting('alarmsound', 'Alarm Sound',
740
                         RadioSettingValueBoolean(
741
                             int(_set.alarmsound))))
742
        adv.append(
743
            RadioSetting('tailnoiseclear', 'Tail Noise Clear',
744
                         RadioSettingValueBoolean(
745
                             int(_set.tailnoiseclear))))
746
        adv.append(
747
            RadioSetting('roger', 'Roger',
748
                         RadioSettingValueBoolean(
749
                             int(_set.roger))))
750
        adv.append(
751
            RadioSetting('fmradio', 'FM Radio Disabled',
752
                         RadioSettingValueBoolean(
753
                             int(_set.fmradio))))
754
        adv.append(
755
            RadioSetting('kblock', 'KB Lock',
756
                         RadioSettingValueBoolean(
757
                             int(_set.kblock))))
758

    
759
        for key in sorted(choice_settings):
760
            choices = choice_settings[key]
761
            title = titles[key]
762
            if key in basic_settings:
763
                group = basic
764
            else:
765
                group = adv
766

    
767
            val = int(getattr(_set, key))
768
            try:
769
                cur = choices[val]
770
            except IndexError:
771
                LOG.error('Value %i for %s out of range for list (%i): %s' % (
772
                    val, key, len(choices), choices))
773
                raise
774
            group.append(
775
                RadioSetting(key, title,
776
                             RadioSettingValueList(
777
                                 choices,
778
                                 choices[val])))
779

    
780
        if self.VENDOR == "Retevis":
781
            # Side Keys
782
            _skey = self._memobj.skey
783
            SK_CHOICES = ['OFF', 'LAMP', 'SOS', 'FM', 'NOAA', 'MONI', 'SEARCH']
784
            SK_VALUES = [0xFF, 0x08, 0x03, 0x07, 0x0C, 0x05, 0x1D]
785

    
786
            def apply_sk_listvalue(setting, obj):
787
                LOG.debug("Setting value: " + str(setting.value) +
788
                          " from list")
789
                val = str(setting.value)
790
                index = SK_CHOICES.index(val)
791
                val = SK_VALUES[index]
792
                obj.set_value(val)
793

    
794
            # Side Key 1 - Short Press
795
            if _skey.skey1sp in SK_VALUES:
796
                idx = SK_VALUES.index(_skey.skey1sp)
797
            else:
798
                idx = SK_VALUES.index(0xFF)
799
            rs = RadioSetting('skey.skey1sp', 'Side Key 1 - Short Press',
800
                              RadioSettingValueList(SK_CHOICES,
801
                                                    SK_CHOICES[idx]))
802
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey1sp)
803
            adv.append(rs)
804

    
805
            # Side Key 1 - Long Press
806
            if _skey.skey1lp in SK_VALUES:
807
                idx = SK_VALUES.index(_skey.skey1lp)
808
            else:
809
                idx = SK_VALUES.index(0xFF)
810
            rs = RadioSetting('skey.skey1lp', 'Side Key 1 - Long Press',
811
                              RadioSettingValueList(SK_CHOICES,
812
                                                    SK_CHOICES[idx]))
813
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey1lp)
814
            adv.append(rs)
815

    
816
            # Side Key 2 - Short Press
817
            if _skey.skey2sp in SK_VALUES:
818
                idx = SK_VALUES.index(_skey.skey2sp)
819
            else:
820
                idx = SK_VALUES.index(0xFF)
821
            rs = RadioSetting('skey.skey2sp', 'Side Key 2 - Short Press',
822
                              RadioSettingValueList(SK_CHOICES,
823
                                                    SK_CHOICES[idx]))
824
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey2sp)
825
            adv.append(rs)
826

    
827
            # Side Key 1 - Long Press
828
            if _skey.skey2lp in SK_VALUES:
829
                idx = SK_VALUES.index(_skey.skey2lp)
830
            else:
831
                idx = SK_VALUES.index(0xFF)
832
            rs = RadioSetting('skey.skey2lp', 'Side Key 2 - Long Press',
833
                              RadioSettingValueList(SK_CHOICES,
834
                                                    SK_CHOICES[idx]))
835
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey2lp)
836
            adv.append(rs)
837

    
838
        for i in range(1, 16):
839
            cur = ''.join(
840
                DTMFCHARS[i]
841
                for i in self._memobj.dtmfgroup[i - 1].code if int(i) < 0xF)
842
            dtmf.append(
843
                RadioSetting(
844
                    'dtmf.code@%i' % i, 'DTMF Group %i' % i,
845
                    RadioSettingValueString(0, 5, cur,
846
                                            autopad=False,
847
                                            charset=DTMFCHARS)))
848
        cur = ''.join(
849
            '%X' % i
850
            for i in self._memobj.anicode.code if int(i) < 0xE)
851

    
852
        anicode = self._memobj.anicode
853

    
854
        if isinstance(self, Senhaix8800Radio):
855
            _codeobj = self._memobj.anicode.code
856
            _code = "".join([DTMFCHARS[x] for x in _codeobj if int(x) < 0x1F])
857
            val = RadioSettingValueString(0, 5, _code, False)
858
            val.set_charset(DTMFCHARS)
859
            rs = RadioSetting("anicode.code", "ANI Code", val)
860

    
861
            def apply_code(setting, obj):
862
                code = []
863
                for j in range(0, 5):
864
                    try:
865
                        code.append(DTMFCHARS.index(str(setting.value)[j]))
866
                    except IndexError:
867
                        code.append(0xFF)
868
                obj.code = code
869

    
870
            rs.set_apply_callback(apply_code, anicode)
871

    
872
            dtmf.append(rs)
873

    
874
            dtmf.append(
875
                RadioSetting(
876
                    "anicode.groupcode", "Group Code",
877
                    RadioSettingValueList(list(DTMFCHARS),
878
                                          DTMFCHARS[int(anicode.groupcode)])))
879

    
880
        else:
881
            dtmf.append(
882
                RadioSetting(
883
                    'anicode.code', 'ANI Code',
884
                    RadioSettingValueString(0, 5, cur,
885
                                            autopad=False,
886
                                            charset=DTMFCHARS)))
887
            dtmf.append(
888
                RadioSetting(
889
                    'anicode.groupcode', 'Group Code',
890
                    RadioSettingValueList(
891
                        list(DTMFCHARS),
892
                        DTMFCHARS[int(anicode.groupcode)])))
893
            dtmf.append(
894
                RadioSetting(
895
                    'anicode.releasetosend', 'Release To Send',
896
                    RadioSettingValueBoolean(
897
                        int(anicode.releasetosend))))
898
            dtmf.append(
899
                RadioSetting(
900
                    'anicode.presstosend', 'Press To Send',
901
                    RadioSettingValueBoolean(
902
                        int(anicode.presstosend))))
903

    
904
        cur = int(anicode.dtmfspeedon) * 10 + 80
905
        dtmf.append(
906
            RadioSetting(
907
                'anicode.dtmfspeedon', 'DTMF Speed (on time in ms)',
908
                RadioSettingValueInteger(60, 2000, cur, 10)))
909
        cur = int(anicode.dtmfspeedoff) * 10 + 80
910
        dtmf.append(
911
            RadioSetting(
912
                'anicode.dtmfspeedoff', 'DTMF Speed (off time in ms)',
913
                RadioSettingValueInteger(60, 2000, cur, 10)))
914

    
915
        top = RadioSettings(basic, adv, dtmf)
916
        return top
917

    
918
    def set_settings(self, settings):
919
        for element in settings:
920
            if element.get_name().startswith('anicode.'):
921
                self._set_anicode(element)
922
            elif element.get_name().startswith('dtmf.code'):
923
                self._set_dtmfcode(element)
924
            elif element.get_name().startswith('skey.'):
925
                self._set_skey(element)
926
            elif not isinstance(element, RadioSetting):
927
                self.set_settings(element)
928
                continue
929
            else:
930
                self._set_setting(element)
931

    
932
    def _set_setting(self, setting):
933
        key = setting.get_name()
934
        val = setting.value
935

    
936
        setattr(self._memobj.settings, key, int(val))
937

    
938
    def _set_anicode(self, setting):
939
        name = setting.get_name().split('.', 1)[1]
940
        if name == 'code':
941
            val = [DTMFCHARS.index(c) for c in str(setting.value)]
942
            for i in range(0, 5):
943
                try:
944
                    value = val[i]
945
                except IndexError:
946
                    value = 0xFF
947
                self._memobj.anicode.code[i] = value
948
        elif name.startswith('dtmfspeed'):
949
            setattr(self._memobj.anicode, name,
950
                    (int(setting.value) - 80) // 10)
951
        else:
952
            setattr(self._memobj.anicode, name, int(setting.value))
953

    
954
    def _set_dtmfcode(self, setting):
955
        index = int(setting.get_name().split('@', 1)[1]) - 1
956
        val = [DTMFCHARS.index(c) for c in str(setting.value)]
957
        for i in range(0, 5):
958
            try:
959
                value = val[i]
960
            except IndexError:
961
                value = 0xFF
962
            self._memobj.dtmfgroup[index].code[i] = value
963

    
964
    def _set_skey(self, setting):
965
        if setting.has_apply_callback():
966
            LOG.debug("Using apply callback")
967
            setting.run_apply_callback()
968

    
969

    
970
@directory.register
971
class RetevisRA685Radio(RadioddityGA510Radio):
972
    VENDOR = 'Retevis'
973
    MODEL = 'RA685'
974
    POWER_LEVELS = [
975
        chirp_common.PowerLevel('H', watts=5),
976
        chirp_common.PowerLevel('L', watts=1),
977
        chirp_common.PowerLevel('M', watts=3)]
978

    
979
    _magic = b'PROGROMWLTU'
980

    
981
    def check_set_memory_immutable_policy(self, existing, new):
982
        existing.immutable = []
983
        super().check_set_memory_immutable_policy(existing, new)
984

    
985
    def get_features(self):
986
        rf = RadioddityGA510Radio.get_features(self)
987
        rf.memory_bounds = (1, 128)
988
        rf.valid_bands = [(136000000, 174000000),
989
                          (400000000, 520000000)]
990
        return rf
991

    
992
    def validate_memory(self, mem):
993
        msgs = super().validate_memory(mem)
994

    
995
        _msg_duplex = 'Duplex must be "off" for this frequency'
996
        _msg_offset = 'Only simplex or +5MHz offset allowed on GMRS'
997

    
998
        if not ((mem.freq >= self.vhftx[0] and mem.freq < self.vhftx[1]) or
999
                (mem.freq >= self.uhftx[0] and mem.freq < self.uhftx[1])):
1000
            if mem.duplex != "off":
1001
                msgs.append(chirp_common.ValidationWarning(_msg_duplex))
1002

    
1003
        return msgs
1004

    
1005
    def _get_mem(self, num):
1006
        return self._memobj.memories[num - 1]
1007

    
1008
    def _get_nam(self, number):
1009
        return self._memobj.names[number - 1]
1010

    
1011
    def _set_mem(self, num):
1012
        return self._memobj.memories[num - 1]
1013

    
1014
    def _set_nam(self, number):
1015
        return self._memobj.names[number - 1]
1016

    
1017
    vhftx = [144000000, 146000000]
1018
    uhftx = [430000000, 440000000]
1019

    
1020

    
1021
@directory.register
1022
class RetevisRA85Radio(RadioddityGA510Radio):
1023
    VENDOR = 'Retevis'
1024
    MODEL = 'RA85'
1025
    POWER_LEVELS = [
1026
        chirp_common.PowerLevel('H', watts=5),
1027
        chirp_common.PowerLevel('L', watts=0.5),
1028
        chirp_common.PowerLevel('M', watts=0.6)]
1029

    
1030
    _magic = b'PROGROMWLTU'
1031
    _gmrs = True
1032

    
1033
    def validate_memory(self, mem):
1034
        msgs = super().validate_memory(mem)
1035

    
1036
        _msg_duplex = 'Duplex must be "off" for this frequency'
1037
        _msg_offset = 'Only simplex or +5MHz offset allowed on GMRS'
1038

    
1039
        if mem.freq not in bandplan_na.ALL_GMRS_FREQS:
1040
            if mem.duplex != "off":
1041
                msgs.append(chirp_common.ValidationWarning(_msg_duplex))
1042
        if mem.freq in bandplan_na.GMRS_HIRPT:
1043
            if mem.duplex and mem.offset != 5000000:
1044
                msgs.append(chirp_common.ValidationWarning(_msg_offset))
1045

    
1046
        return msgs
1047

    
1048
    def check_set_memory_immutable_policy(self, existing, new):
1049
        existing.immutable = []
1050
        super().check_set_memory_immutable_policy(existing, new)
1051

    
1052
    def get_features(self):
1053
        rf = RadioddityGA510Radio.get_features(self)
1054
        rf.memory_bounds = (1, 128)
1055
        rf.valid_bands = [(136000000, 174000000),
1056
                          (400000000, 520000000)]
1057
        return rf
1058

    
1059
    def _get_mem(self, num):
1060
        return self._memobj.memories[num - 1]
1061

    
1062
    def _get_nam(self, number):
1063
        return self._memobj.names[number - 1]
1064

    
1065
    def _set_mem(self, num):
1066
        return self._memobj.memories[num - 1]
1067

    
1068
    def _set_nam(self, number):
1069
        return self._memobj.names[number - 1]
1070

    
1071

    
1072
@directory.register
1073
class TDH6Radio(RadioddityGA510Radio):
1074
    VENDOR = "TIDRADIO"
1075
    MODEL = "TD-H6"
1076

    
1077
    def get_features(self):
1078
        rf = super().get_features()
1079
        rf.valid_bands = [(136000000, 174000000),
1080
                          (400000000, 520000000)]
1081
        return rf
1082

    
1083

    
1084
@directory.register
1085
class Senhaix8800Radio(RadioddityGA510Radio):
1086
    """Senhaix 8800"""
1087
    VENDOR = "SenhaiX"
1088
    MODEL = "8800"
1089

    
1090
    POWER_LEVELS = [
1091
        chirp_common.PowerLevel('H', watts=5),
1092
        chirp_common.PowerLevel('L', watts=1)]
1093
    _mem_format = MODEL_SHX8800_FORMAT
1094
    _magic = b'PROGROMSHXU'
1095

    
1096

    
1097
@directory.register
1098
class RadioddityGS5BRadio(Senhaix8800Radio):
1099
    """Radioddity GS-5B"""
1100
    VENDOR = "Radioddity"
1101
    MODEL = "GS-5B"
1102

    
1103

    
1104
@directory.register
1105
class SignusXTR5Radio(Senhaix8800Radio):
1106
    """Signus XTR-5"""
1107
    VENDOR = "Signus"
1108
    MODEL = "XTR-5"
1109

    
1110

    
1111
@directory.register
1112
class AnysecuAC580Radio(Senhaix8800Radio):
1113
    """Anysecu AC-580"""
1114
    VENDOR = "Anysecu"
1115
    MODEL = "AC-580"
    (1-1/1)