Project

General

Profile

Bug #10651 ยป ga510 - allow_tx.py

Jim Unroe, 11/25/2023 06:13 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
    try:
49
        radio.pipe.write(b'E')
50
    except Exception as e:
51
        LOG.error('Failed to reset radio: %s' % e)
52

    
53

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

    
63
    radio.pipe.write(b'F')
64

    
65
    ident = radio.pipe.read(8)
66
    LOG.debug('Radio ident string is %r' % ident)
67

    
68
    return ident
69

    
70

    
71
def do_download(radio):
72
    ident = start_program(radio)
73

    
74
    s = chirp_common.Status()
75
    s.msg = 'Downloading'
76
    s.max = 0x1C00
77

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

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

    
95
        data += block
96

    
97
        s.cur = addr
98
        radio.status_fn(s)
99

    
100
    return data
101

    
102

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

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

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

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

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

    
127

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

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

    
154
"""
155

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

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

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

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

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

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

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

    
227
"""
228

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

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

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

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

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

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

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

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

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

    
339
"""
340

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

    
351

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

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

    
366
    _gmrs = False
367

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

    
381
    def sync_out(self):
382
        try:
383
            do_upload(self)
384
        except errors.RadioError:
385
            raise
386
        except Exception as e:
387
            LOG.exception('General failure')
388
            raise errors.RadioError('Failed to upload to radio: %s' % e)
389
        finally:
390
            reset(self)
391

    
392
    def process_mmap(self):
393
        self._memobj = bitwise.parse(BASE_FORMAT + self._mem_format,
394
                                     self._mmap)
395

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

    
426
    def get_raw_memory(self, num):
427
        return repr(self._memobj.memories[num]) + repr(self._memobj.names[num])
428

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

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

    
462
    def _get_extra(self, _mem):
463
        group = RadioSettingGroup('extra', 'Extra')
464

    
465
        s = RadioSetting('bcl', 'Busy Channel Lockout',
466
                         RadioSettingValueBoolean(_mem.bcl))
467
        group.append(s)
468

    
469
        s = RadioSetting('fhss', 'FHSS',
470
                         RadioSettingValueBoolean(_mem.fhss))
471
        group.append(s)
472

    
473
        # pttid, signal
474

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

    
480
        cur = SIGNAL[int(_mem.signal)]
481
        s = RadioSetting('signal', 'Signal',
482
                         RadioSettingValueList(SIGNAL, cur))
483
        group.append(s)
484

    
485
        return group
486

    
487
    def _set_extra(self, _mem, mem):
488
        _mem.bcl = int(mem.extra['bcl'].value)
489
        _mem.fhss = int(mem.extra['fhss'].value)
490
        _mem.pttid = int(mem.extra['pttid'].value)
491
        _mem.signal = int(mem.extra['signal'].value)
492

    
493
    def _is_txinh(self, _mem):
494
        raw_tx = ""
495
        for i in range(0, 4):
496
            raw_tx += _mem.txfreq[i].get_raw(asbytes=False)
497
        return raw_tx == "\xFF\xFF\xFF\xFF"
498

    
499
    def _get_mem(self, num):
500
        return self._memobj.memories[num]
501

    
502
    def _get_nam(self, num):
503
        return self._memobj.names[num]
504

    
505
    def get_memory(self, num):
506
        _mem = self._get_mem(num)
507
        _nam = self._get_nam(num)
508
        mem = chirp_common.Memory()
509
        mem.number = num
510
        if int(_mem.rxfreq) == 166666665:
511
            mem.empty = True
512
            return mem
513

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

    
530
        mem.power = self.POWER_LEVELS[_mem.power]
531
        mem.mode = 'NFM' if _mem.narrow else 'FM'
532
        mem.skip = '' if _mem.scan else 'S'
533

    
534
        LOG.debug('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
535
        LOG.debug('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
536
        chirp_common.split_tone_decode(mem,
537
                                       self._decode_tone(_mem.txtone),
538
                                       self._decode_tone(_mem.rxtone))
539
        try:
540
            mem.extra = self._get_extra(_mem)
541
        except:
542
            LOG.exception('Failed to get extra for %i' % num)
543

    
544
        return mem
545

    
546
    def _set_mem(self, number):
547
        return self._memobj.memories[number]
548

    
549
    def _set_nam(self, number):
550
        return self._memobj.names[number]
551

    
552
    def set_memory(self, mem):
553
        _mem = self._set_mem(mem.number)
554
        _nam = self._set_nam(mem.number)
555

    
556
        if mem.empty:
557
            _mem.set_raw(b'\xff' * 16)
558
            _nam.set_raw(b'\xff' * 16)
559
            return
560

    
561
        if int(_mem.rxfreq) == 166666665:
562
            LOG.debug('Initializing new memory %i' % mem.number)
563
            _mem.set_raw(b'\x00' * 16)
564

    
565
        _nam.name = mem.name.ljust(10)
566

    
567
        #_mem.allow_tx = True
568
        if isinstance(self, Senhaix8800Radio):
569
            _mem.allow_tx = True
570

    
571
        _mem.rxfreq = mem.freq // 10
572
        if mem.duplex == '':
573
            #if isinstance(self, Senhaix8800Radio):
574
            #    _mem.allow_tx = False
575
            _mem.txfreq = mem.freq // 10
576
        elif mem.duplex == 'split':
577
            _mem.txfreq = mem.offset // 10
578
        elif mem.duplex == 'off':
579
            if isinstance(self, Senhaix8800Radio):
580
                #_mem.allow_tx = True
581
                _mem.allow_tx = False
582
            for i in range(0, 4):
583
                _mem.txfreq[i].set_raw(b'\xFF')
584
        elif mem.duplex == '-':
585
            _mem.txfreq = (mem.freq - mem.offset) // 10
586
        elif mem.duplex == '+':
587
            _mem.txfreq = (mem.freq + mem.offset) // 10
588
        else:
589
            raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
590

    
591
        txtone, rxtone = chirp_common.split_tone_encode(mem)
592
        LOG.debug('tx tone is %s' % repr(txtone))
593
        LOG.debug('rx tone is %s' % repr(rxtone))
594
        _mem.txtone = self._encode_tone(*txtone)
595
        _mem.rxtone = self._encode_tone(*rxtone)
596

    
597
        try:
598
            _mem.power = self.POWER_LEVELS.index(mem.power)
599
        except ValueError:
600
            _mem.power = 0
601
        _mem.narrow = mem.mode == 'NFM'
602
        _mem.scan = mem.skip != 'S'
603
        if mem.extra:
604
            self._set_extra(_mem, mem)
605

    
606
    def get_settings(self):
607
        _set = self._memobj.settings
608

    
609
        basic = RadioSettingGroup('basic', 'Basic')
610
        adv = RadioSettingGroup('advanced', 'Advanced')
611
        dtmf = RadioSettingGroup('dtmf', 'DTMF')
612

    
613
        radioddity_settings = {
614
            'savemode': ['Off', 'Mode 1', 'Mode 2', 'Mode 3'],
615
            'cha_disp': ['CH+Name', 'CH+Freq'],
616
            'chb_disp': ['CH+Name', 'CH+Freq'],
617
            'txundertdr': ['Off', 'Band A', 'Band B'],
618
            'rptnoiseclr': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
619
            'rptnoisedet': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
620
        }
621

    
622
        retevis_settings = {
623
            'savemode': ['Off', 'On'],
624
            'cha_disp': ['CH', 'CH+Name'],
625
            'chb_disp': ['CH', 'CH+Name'],
626
        }
627

    
628
        language_setting = {
629
            'language': ['English', 'Chinese'],
630
        }
631

    
632
        ga_workmode = {
633
            'workmode': ['VFO', 'Chan'],
634
        }
635

    
636
        shx_workmode = {
637
            'workmodea': ['VFO', 'Chan'],
638
            'workmodeb': ['VFO', 'Chan'],
639
        }
640

    
641
        choice_settings = {
642
            'vox': ['Off'] + ['%i' % i for i in range(1, 11)],
643
            'backlight': ['Off'] + ['%i' % i for i in range(1, 11)],
644
            'timeout': ['Off'] + ['%i' % i for i in range(15, 615, 15)],
645
            'dtmfst': ['OFF', 'KB Side Tone', 'ANI Side Tone',
646
                       'KB ST+ANI ST', 'Both'],
647
            'scanmode': ['TO', 'CO', 'SE'],
648
            'pttid': ['Off', 'BOT', 'EOT', 'Both'],
649
            'alarm_mode': ['Site', 'Tone', 'Code'],
650
        }
651

    
652
        if isinstance(self, Senhaix8800Radio):
653
            choice_settings.update(shx_workmode)
654
        else:
655
            choice_settings.update(language_setting)
656
            choice_settings.update(ga_workmode)
657

    
658
        if self.VENDOR == "Retevis":
659
            choice_settings.update(retevis_settings)
660
        else:
661
            choice_settings.update(radioddity_settings)
662

    
663
        if isinstance(self, Senhaix8800Radio):
664
            basic_settings = ['timeout', 'vox', 'backlight',
665
                              'cha_disp', 'chb_disp', 'workmodea',
666
                              'workmodeb']
667
        else:
668
            basic_settings = ['timeout', 'vox', 'backlight', 'language',
669
                              'cha_disp', 'chb_disp', 'workmode']
670
        titles = {
671
            'savemode': 'Save Mode',
672
            'vox': 'VOX',
673
            'backlight': 'Auto Backlight',
674
            'timeout': 'Time Out Timer (s)',
675
            'language': 'Language',
676
            'dtmfst': 'DTMF-ST',
677
            'scanmode': 'Scan Mode',
678
            'pttid': 'PTT-ID',
679
            'cha_disp': 'Channel A Display',
680
            'chb_disp': 'Channel B Display',
681
            'alarm_mode': 'Alarm Mode',
682
            'txundertdr': 'TX Under TDR',
683
            'rptnoiseclr': 'RPT Noise Clear (ms)',
684
            'rptnoisedet': 'RPT Noise Detect (ms)',
685
            'workmode': 'Work Mode',
686
            'workmodea': 'Work Mode A',
687
            'workmodeb': 'Work Mode B',
688
        }
689

    
690
        basic.append(
691
            RadioSetting('squelch', 'Squelch Level',
692
                         RadioSettingValueInteger(0, 9, int(_set.squelch))))
693
        adv.append(
694
            RadioSetting('pttdelay', 'PTT Delay',
695
                         RadioSettingValueInteger(0, 30, int(_set.pttdelay))))
696
        adv.append(
697
            RadioSetting('tdr', 'TDR',
698
                         RadioSettingValueBoolean(
699
                             int(_set.tdr))))
700
        adv.append(
701
            RadioSetting('beep', 'Beep',
702
                         RadioSettingValueBoolean(
703
                             int(_set.beep))))
704
        basic.append(
705
            RadioSetting('voice', 'Voice Enable',
706
                         RadioSettingValueBoolean(
707
                             int(_set.voice))))
708
        adv.append(
709
            RadioSetting('bcl', 'BCL',
710
                         RadioSettingValueBoolean(
711
                             int(_set.bcl))))
712
        adv.append(
713
            RadioSetting('autolock', 'Auto Lock',
714
                         RadioSettingValueBoolean(
715
                             int(_set.autolock))))
716
        adv.append(
717
            RadioSetting('alarmsound', 'Alarm Sound',
718
                         RadioSettingValueBoolean(
719
                             int(_set.alarmsound))))
720
        adv.append(
721
            RadioSetting('tailnoiseclear', 'Tail Noise Clear',
722
                         RadioSettingValueBoolean(
723
                             int(_set.tailnoiseclear))))
724
        adv.append(
725
            RadioSetting('roger', 'Roger',
726
                         RadioSettingValueBoolean(
727
                             int(_set.roger))))
728
        adv.append(
729
            RadioSetting('fmradio', 'FM Radio Disabled',
730
                         RadioSettingValueBoolean(
731
                             int(_set.fmradio))))
732
        adv.append(
733
            RadioSetting('kblock', 'KB Lock',
734
                         RadioSettingValueBoolean(
735
                             int(_set.kblock))))
736

    
737
        for key in sorted(choice_settings):
738
            choices = choice_settings[key]
739
            title = titles[key]
740
            if key in basic_settings:
741
                group = basic
742
            else:
743
                group = adv
744

    
745
            val = int(getattr(_set, key))
746
            try:
747
                cur = choices[val]
748
            except IndexError:
749
                LOG.error('Value %i for %s out of range for list (%i): %s' % (
750
                    val, key, len(choices), choices))
751
                raise
752
            group.append(
753
                RadioSetting(key, title,
754
                             RadioSettingValueList(
755
                                 choices,
756
                                 choices[val])))
757

    
758
        if self.VENDOR == "Retevis":
759
            # Side Keys
760
            _skey = self._memobj.skey
761
            SK_CHOICES = ['OFF', 'LAMP', 'SOS', 'FM', 'NOAA', 'MONI', 'SEARCH']
762
            SK_VALUES = [0xFF, 0x08, 0x03, 0x07, 0x0C, 0x05, 0x1D]
763

    
764
            def apply_sk_listvalue(setting, obj):
765
                LOG.debug("Setting value: " + str(setting.value) +
766
                          " from list")
767
                val = str(setting.value)
768
                index = SK_CHOICES.index(val)
769
                val = SK_VALUES[index]
770
                obj.set_value(val)
771

    
772
            # Side Key 1 - Short Press
773
            if _skey.skey1sp in SK_VALUES:
774
                idx = SK_VALUES.index(_skey.skey1sp)
775
            else:
776
                idx = SK_VALUES.index(0xFF)
777
            rs = RadioSetting('skey.skey1sp', 'Side Key 1 - Short Press',
778
                              RadioSettingValueList(SK_CHOICES,
779
                                                    SK_CHOICES[idx]))
780
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey1sp)
781
            adv.append(rs)
782

    
783
            # Side Key 1 - Long Press
784
            if _skey.skey1lp in SK_VALUES:
785
                idx = SK_VALUES.index(_skey.skey1lp)
786
            else:
787
                idx = SK_VALUES.index(0xFF)
788
            rs = RadioSetting('skey.skey1lp', 'Side Key 1 - Long Press',
789
                              RadioSettingValueList(SK_CHOICES,
790
                                                    SK_CHOICES[idx]))
791
            rs.set_apply_callback(apply_sk_listvalue, _skey.skey1lp)
792
            adv.append(rs)
793

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

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

    
816
        for i in range(1, 16):
817
            cur = ''.join(
818
                DTMFCHARS[i]
819
                for i in self._memobj.dtmfgroup[i - 1].code if int(i) < 0xF)
820
            dtmf.append(
821
                RadioSetting(
822
                    'dtmf.code@%i' % i, 'DTMF Group %i' % i,
823
                    RadioSettingValueString(0, 5, cur,
824
                                            autopad=False,
825
                                            charset=DTMFCHARS)))
826
        cur = ''.join(
827
            '%X' % i
828
            for i in self._memobj.anicode.code if int(i) < 0xE)
829

    
830
        anicode = self._memobj.anicode
831

    
832
        if isinstance(self, Senhaix8800Radio):
833
            _codeobj = self._memobj.anicode.code
834
            _code = "".join([DTMFCHARS[x] for x in _codeobj if int(x) < 0x1F])
835
            val = RadioSettingValueString(0, 5, _code, False)
836
            val.set_charset(DTMFCHARS)
837
            rs = RadioSetting("anicode.code", "ANI Code", val)
838

    
839
            def apply_code(setting, obj):
840
                code = []
841
                for j in range(0, 5):
842
                    try:
843
                        code.append(DTMFCHARS.index(str(setting.value)[j]))
844
                    except IndexError:
845
                        code.append(0xFF)
846
                obj.code = code
847

    
848
            rs.set_apply_callback(apply_code, anicode)
849

    
850
            dtmf.append(rs)
851

    
852
            dtmf.append(
853
                RadioSetting(
854
                    "anicode.groupcode", "Group Code",
855
                    RadioSettingValueList(list(DTMFCHARS),
856
                                          DTMFCHARS[int(anicode.groupcode)])))
857

    
858
        else:
859
            dtmf.append(
860
                RadioSetting(
861
                    'anicode.code', 'ANI Code',
862
                    RadioSettingValueString(0, 5, cur,
863
                                            autopad=False,
864
                                            charset=DTMFCHARS)))
865
            dtmf.append(
866
                RadioSetting(
867
                    'anicode.groupcode', 'Group Code',
868
                    RadioSettingValueList(
869
                        list(DTMFCHARS),
870
                        DTMFCHARS[int(anicode.groupcode)])))
871
            dtmf.append(
872
                RadioSetting(
873
                    'anicode.releasetosend', 'Release To Send',
874
                    RadioSettingValueBoolean(
875
                        int(anicode.releasetosend))))
876
            dtmf.append(
877
                RadioSetting(
878
                    'anicode.presstosend', 'Press To Send',
879
                    RadioSettingValueBoolean(
880
                        int(anicode.presstosend))))
881

    
882
        cur = int(anicode.dtmfspeedon) * 10 + 80
883
        dtmf.append(
884
            RadioSetting(
885
                'anicode.dtmfspeedon', 'DTMF Speed (on time in ms)',
886
                RadioSettingValueInteger(60, 2000, cur, 10)))
887
        cur = int(anicode.dtmfspeedoff) * 10 + 80
888
        dtmf.append(
889
            RadioSetting(
890
                'anicode.dtmfspeedoff', 'DTMF Speed (off time in ms)',
891
                RadioSettingValueInteger(60, 2000, cur, 10)))
892

    
893
        top = RadioSettings(basic, adv, dtmf)
894
        return top
895

    
896
    def set_settings(self, settings):
897
        for element in settings:
898
            if element.get_name().startswith('anicode.'):
899
                self._set_anicode(element)
900
            elif element.get_name().startswith('dtmf.code'):
901
                self._set_dtmfcode(element)
902
            elif element.get_name().startswith('skey.'):
903
                self._set_skey(element)
904
            elif not isinstance(element, RadioSetting):
905
                self.set_settings(element)
906
                continue
907
            else:
908
                self._set_setting(element)
909

    
910
    def _set_setting(self, setting):
911
        key = setting.get_name()
912
        val = setting.value
913

    
914
        setattr(self._memobj.settings, key, int(val))
915

    
916
    def _set_anicode(self, setting):
917
        name = setting.get_name().split('.', 1)[1]
918
        if name == 'code':
919
            val = [DTMFCHARS.index(c) for c in str(setting.value)]
920
            for i in range(0, 5):
921
                try:
922
                    value = val[i]
923
                except IndexError:
924
                    value = 0xFF
925
                self._memobj.anicode.code[i] = value
926
        elif name.startswith('dtmfspeed'):
927
            setattr(self._memobj.anicode, name,
928
                    (int(setting.value) - 80) // 10)
929
        else:
930
            setattr(self._memobj.anicode, name, int(setting.value))
931

    
932
    def _set_dtmfcode(self, setting):
933
        index = int(setting.get_name().split('@', 1)[1]) - 1
934
        val = [DTMFCHARS.index(c) for c in str(setting.value)]
935
        for i in range(0, 5):
936
            try:
937
                value = val[i]
938
            except IndexError:
939
                value = 0xFF
940
            self._memobj.dtmfgroup[index].code[i] = value
941

    
942
    def _set_skey(self, setting):
943
        if setting.has_apply_callback():
944
            LOG.debug("Using apply callback")
945
            setting.run_apply_callback()
946

    
947

    
948
@directory.register
949
class RetevisRA685Radio(RadioddityGA510Radio):
950
    VENDOR = 'Retevis'
951
    MODEL = 'RA685'
952
    POWER_LEVELS = [
953
        chirp_common.PowerLevel('H', watts=5),
954
        chirp_common.PowerLevel('L', watts=1),
955
        chirp_common.PowerLevel('M', watts=3)]
956

    
957
    _magic = b'PROGROMWLTU'
958

    
959
    def get_features(self):
960
        rf = RadioddityGA510Radio.get_features(self)
961
        rf.memory_bounds = (1, 128)
962
        rf.valid_bands = [(136000000, 174000000),
963
                          (400000000, 520000000)]
964
        return rf
965

    
966
    def _get_mem(self, num):
967
        return self._memobj.memories[num - 1]
968

    
969
    def _get_nam(self, number):
970
        return self._memobj.names[number - 1]
971

    
972
    def _set_mem(self, num):
973
        return self._memobj.memories[num - 1]
974

    
975
    def _set_nam(self, number):
976
        return self._memobj.names[number - 1]
977

    
978
    vhftx = [144000000, 146000000]
979
    uhftx = [430000000, 440000000]
980

    
981

    
982
@directory.register
983
class RetevisRA85Radio(RadioddityGA510Radio):
984
    VENDOR = 'Retevis'
985
    MODEL = 'RA85'
986
    POWER_LEVELS = [
987
        chirp_common.PowerLevel('H', watts=5),
988
        chirp_common.PowerLevel('L', watts=0.5),
989
        chirp_common.PowerLevel('M', watts=0.6)]
990

    
991
    _magic = b'PROGROMWLTU'
992

    
993
    def get_features(self):
994
        rf = RadioddityGA510Radio.get_features(self)
995
        rf.memory_bounds = (1, 128)
996
        rf.valid_bands = [(136000000, 174000000),
997
                          (400000000, 520000000)]
998
        return rf
999

    
1000
    def _get_mem(self, num):
1001
        return self._memobj.memories[num - 1]
1002

    
1003
    def _get_nam(self, number):
1004
        return self._memobj.names[number - 1]
1005

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

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

    
1012

    
1013
@directory.register
1014
class TDH6Radio(RadioddityGA510Radio):
1015
    VENDOR = "TIDRADIO"
1016
    MODEL = "TD-H6"
1017

    
1018
    def get_features(self):
1019
        rf = super().get_features()
1020
        rf.valid_bands = [(136000000, 174000000),
1021
                          (400000000, 520000000)]
1022
        return rf
1023

    
1024

    
1025
@directory.register
1026
class Senhaix8800Radio(RadioddityGA510Radio):
1027
    """Senhaix 8800"""
1028
    VENDOR = "SenhaiX"
1029
    MODEL = "8800"
1030

    
1031
    POWER_LEVELS = [
1032
        chirp_common.PowerLevel('H', watts=5),
1033
        chirp_common.PowerLevel('L', watts=1)]
1034
    _mem_format = MODEL_SHX8800_FORMAT
1035
    _magic = b'PROGROMSHXU'
1036

    
1037

    
1038
@directory.register
1039
class RadioddityGS5BRadio(Senhaix8800Radio):
1040
    """Radioddity GS-5B"""
1041
    VENDOR = "Radioddity"
1042
    MODEL = "GS-5B"
1043

    
1044

    
1045
# NOTE: This was added as Signus originally in 18295675
1046
@directory.register
1047
class CignusXTR5Radio(Senhaix8800Radio):
1048
    """Cignus XTR-5"""
1049
    VENDOR = "Cignus"
1050
    MODEL = "XTR-5"
1051

    
1052

    
1053
@directory.register
1054
class AnysecuAC580Radio(Senhaix8800Radio):
1055
    """Anysecu AC-580"""
1056
    VENDOR = "Anysecu"
1057
    MODEL = "AC-580"
1058

    
1059

    
1060
@directory.register
1061
class AbbreeARF5Radio(RadioddityGA510Radio):
1062
    VENDOR = 'Abbree'
1063
    MODEL = 'AR-F5'
1064
    POWER_LEVELS = [
1065
        chirp_common.PowerLevel('H', watts=5),
1066
        chirp_common.PowerLevel('L', watts=1),
1067
        chirp_common.PowerLevel('M', watts=2)]
1068

    
1069
    _magic = b'PROGROMWLTU'
1070

    
1071
    def get_features(self):
1072
        rf = RadioddityGA510Radio.get_features(self)
1073
        rf.memory_bounds = (1, 128)
1074
        rf.valid_bands = [(136000000, 174000000),
1075
                          (200000000, 300000000),
1076
                          (300000000, 400000000),
1077
                          (400000000, 520000000)]
1078
        return rf
1079

    
1080
    def _get_mem(self, num):
1081
        return self._memobj.memories[num - 1]
1082

    
1083
    def _get_nam(self, number):
1084
        return self._memobj.names[number - 1]
1085

    
1086
    def _set_mem(self, num):
1087
        return self._memobj.memories[num - 1]
1088

    
1089
    def _set_nam(self, number):
1090
        return self._memobj.names[number - 1]
    (1-1/1)