Project

General

Profile

Bug #7939 » ga510.py

Dan Smith, 09/08/2020 11:54 AM

 
1
import logging
2
import struct
3

    
4
from chirp import bitwise
5
from chirp import chirp_common
6
from chirp import directory
7
from chirp import errors
8
from chirp import memmap
9
from chirp.settings import RadioSetting, RadioSettingGroup, RadioSettings
10
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
11
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
12

    
13
LOG = logging.getLogger(__name__)
14

    
15
try:
16
    from builtins import bytes
17
    has_future = True
18
except ImportError:
19
    has_future = False
20
    LOG.warning('python-future package is not available; '
21
                '%s requires it' % __name__)
22

    
23

    
24
POWER_LEVELS = [
25
    chirp_common.PowerLevel('H', watts=10),
26
    chirp_common.PowerLevel('L', watts=1),
27
    chirp_common.PowerLevel('M', watts=5)]
28
DTMFCHARS = '0123456789ABCD*#'
29

    
30

    
31
def reset(radio):
32
    radio.pipe.write(b'E')
33

    
34

    
35
def start_program(radio):
36
    reset(radio)
37
    radio.pipe.read(256)
38
    radio.pipe.write(b'PROGROMBFHU')
39
    ack = radio.pipe.read(256)
40
    if not ack.endswith(b'\x06'):
41
        LOG.debug('Ack was %r' % ack)
42
        raise errors.RadioError('Radio did not respond to clone request')
43

    
44
    radio.pipe.write(b'F')
45

    
46
    ident = radio.pipe.read(8)
47
    LOG.debug('Radio ident string is %r' % ident)
48

    
49
    return ident
50

    
51

    
52
def do_download(radio):
53
    ident = start_program(radio)
54

    
55
    s = chirp_common.Status()
56
    s.msg = 'Downloading'
57
    s.max = 0x1C00
58

    
59
    data = bytes()
60
    for addr in range(0, 0x1C40, 0x40):
61
        cmd = struct.pack('>cHB', b'R', addr, 0x40)
62
        LOG.debug('Reading block at %04x: %r' % (addr, cmd))
63
        radio.pipe.write(cmd)
64

    
65
        block = radio.pipe.read(0x44)
66
        header = block[:4]
67
        rcmd, raddr, rlen = struct.unpack('>BHB', header)
68
        block = block[4:]
69
        if raddr != addr:
70
            raise errors.RadioError('Radio send address %04x, expected %04x' %
71
                                    (raddr, addr))
72
        if rlen != 0x40 or len(block) != 0x40:
73
            raise errors.RadioError('Radio sent %02x (%02x) bytes, '
74
                                    'expected %02x' % (rlen, len(block), 0x40))
75

    
76
        data += block
77

    
78
        s.cur = addr
79
        radio.status_fn(s)
80

    
81
    reset(radio)
82

    
83
    return data
84

    
85

    
86
def do_upload(radio):
87
    ident = start_program(radio)
88

    
89
    s = chirp_common.Status()
90
    s.msg = 'Uploading'
91
    s.max = 0x1C00
92

    
93
    # The factory software downloads 0x40 for the block
94
    # at 0x1C00, but only uploads 0x20 there. Mimic that
95
    # here.
96
    for addr in range(0, 0x1C20, 0x20):
97
        cmd = struct.pack('>cHB', b'W', addr, 0x20)
98
        LOG.debug('Writing block at %04x: %r' % (addr, cmd))
99
        block = radio._mmap[addr:addr + 0x20]
100
        radio.pipe.write(cmd)
101
        radio.pipe.write(block)
102

    
103
        ack = radio.pipe.read(1)
104
        if ack != b'\x06':
105
            raise errors.RadioError('Radio refused block at addr %04x' % addr)
106

    
107
        s.cur = addr
108
        radio.status_fn(s)
109

    
110

    
111
MEM_FORMAT = """
112
struct {
113
  lbcd rxfreq[4];
114
  lbcd txfreq[4];
115
  ul16 rxtone;
116
  ul16 txtone;
117
  u8 signal;
118
  u8 unknown1:6,
119
     pttid:2;
120
  u8 unknown2:6,
121
     power:2;
122
  u8 unknown3_0:1,
123
     narrow:1,
124
     unknown3_1:2,
125
     bcl:1,
126
     scan:1,
127
     unknown3_2:1,
128
     fhss:1;
129
} memories[128];
130

    
131
#seekto 0x0C00;
132
struct {
133
  char name[10];
134
  u8 pad[6];
135
} names[128];
136

    
137
#seekto 0x1A00;
138
struct {
139
  // 0x1A00
140
  u8 squelch;
141
  u8 savemode; // [off, mode1, mode2, mode3]
142
  u8 vox; // off=0
143
  u8 backlight;
144
  u8 tdr; // bool
145
  u8 timeout; // n*15 = seconds
146
  u8 beep; // bool
147
  u8 voice;
148

    
149
  // 0x1A08
150
  u8 language; // [eng, chin]
151
  u8 dtmfst;
152
  u8 scanmode; // [TO, CO, SE]
153
  u8 pttid; // [off, BOT, EOT, Both]
154
  u8 pttdelay; // 0-30
155
  u8 cha_disp; // [ch-name, ch-freq]
156
  u8 chb_disp;
157
  u8 bcl; // bool
158

    
159
  // 0x1A10
160
  u8 autolock; // bool
161
  u8 alarm_mode; // [site, tone, code]
162
  u8 alarmsound; // bool
163
  u8 txundertdr; // [off, bandA, bandB]
164
  u8 tailnoiseclear; // [off, on]
165
  u8 rptnoiseclr; // 10*ms, 0-1000
166
  u8 rptnoisedet;
167
  u8 roger; // bool
168

    
169
  // 0x1A18
170
  u8 unknown1a10;
171
  u8 fmradio; // boolean, inverted
172
  u8 workmode; // [vfo, chan]; 1A30-1A31 related?
173
  u8 kblock; // boolean
174
} settings;
175

    
176
struct dtmfcode {
177
  u8 code[5];
178
  u8 ffpad[11]; // always 0xFF
179
};
180
#seekto 0x1B00;
181
struct dtmfcode dtmfgroup[15];
182
struct {
183
  u8 code[5];
184
  u8 groupcode; // 0->D, *, #
185
  u8 nothing:6,
186
     releasetosend:1,
187
     presstosend:1;
188
  u8 dtmfspeedon; // 80 + n*10, up to [194]
189
  u8 dtmfspeedoff;
190
} anicode;
191

    
192
//dtmf on -> 90ms
193
//dtmf off-> 120ms
194
//group code *->0
195
//press 0->1
196
//release 1->0
197

    
198
"""
199

    
200

    
201
PTTID = ['Off', 'BOT', 'EOT', 'Both']
202
SIGNAL = [str(i) for i in range(1, 16)]
203

    
204

    
205
class TDH6Radio(chirp_common.Alias):
206
    VENDOR = "TIDRADIO"
207
    MODEL = "TD-H6"
208

    
209

    
210
@directory.register
211
class RadioddityGA510Radio(chirp_common.CloneModeRadio):
212
    VENDOR = 'Radioddity'
213
    MODEL = 'GA-510'
214
    BAUD_RATE = 9600
215
    NEEDS_COMPAT_SERIAL = False
216
    ALIASES = [TDH6Radio]
217

    
218
    def sync_in(self):
219
        try:
220
            data = do_download(self)
221
            self._mmap = memmap.MemoryMapBytes(data)
222
        except errors.RadioError:
223
            raise
224
        except Exception as e:
225
            LOG.exception('General failure')
226
            raise errors.RadioError('Failed to download from radio: %s' % e)
227
        self.process_mmap()
228

    
229
    def sync_out(self):
230
        try:
231
            do_upload(self)
232
        except errors.RadioError:
233
            raise
234
        except Exception as e:
235
            LOG.exception('General failure')
236
            raise errors.RadioError('Failed to upload to radio: %s' % e)
237

    
238
    def process_mmap(self):
239
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
240

    
241
    def get_features(self):
242
        rf = chirp_common.RadioFeatures()
243
        rf.memory_bounds = (0, 127)
244
        rf.has_ctone = True
245
        rf.has_cross = True
246
        rf.has_tuning_step = False
247
        rf.has_settings = True
248
        rf.has_bank = False
249
        rf.has_sub_devices = False
250
        rf.has_dtcs_polarity = True
251
        rf.has_rx_dtcs = True
252
        rf.can_odd_split = True
253
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
254
        rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
255
                                'DTCS->Tone', '->Tone', 'DTCS->DTCS']
256
        rf.valid_modes = ['FM', 'NFM']
257
        rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0,
258
                                 25.0, 50.0, 100.0]
259
        rf.valid_duplexes = ['', '-', '+', 'split', 'off']
260
        rf.valid_power_levels = POWER_LEVELS
261
        rf.valid_name_length = 10
262
        rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
263
                               'abcdefghijklmnopqrstuvwxyz'
264
                               '0123456789'
265
                               '!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ')
266
        rf.valid_bands = [(136000000, 174000000),
267
                          (400000000, 480000000)]
268
        return rf
269

    
270
    def get_raw_memory(self, num):
271
        return repr(self._memobj.memories[num]) + repr(self._memobj.names[num])
272

    
273
    @staticmethod
274
    def _decode_tone(toneval):
275
        if toneval in (0, 0xFFFF):
276
            LOG.debug('no tone value: %s' % toneval)
277
            return '', None, None
278
        elif toneval < 670:
279
            toneval = toneval - 1
280
            index = toneval % len(chirp_common.DTCS_CODES)
281
            if index != int(toneval):
282
                pol = 'R'
283
                #index -= 1
284
            else:
285
                pol = 'N'
286
            return 'DTCS', chirp_common.DTCS_CODES[index], pol
287
        else:
288
            return 'Tone', toneval / 10.0, 'N'
289

    
290
    @staticmethod
291
    def _encode_tone(mode, val, pol):
292
        if not mode:
293
            return 0x0000
294
        elif mode == 'Tone':
295
            return int(val * 10)
296
        elif mode == 'DTCS':
297
            index = chirp_common.DTCS_CODES.index(val)
298
            if pol == 'R':
299
                index += len(chirp_common.DTCS_CODES)
300
            index += 1
301
            LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index))
302
            return index
303
        else:
304
            raise errors.RadioError('Unsupported tone mode %r' % mode)
305

    
306
    def _get_extra(self, _mem):
307
        group = RadioSettingGroup('extra', 'Extra')
308

    
309
        s = RadioSetting('bcl', 'Busy Channel Lockout',
310
                         RadioSettingValueBoolean(_mem.bcl))
311
        group.append(s)
312

    
313
        s = RadioSetting('fhss', 'FHSS',
314
                         RadioSettingValueBoolean(_mem.fhss))
315
        group.append(s)
316

    
317
        # pttid, signal
318

    
319
        cur = PTTID[int(_mem.pttid)]
320
        s = RadioSetting('pttid', 'PTTID',
321
                         RadioSettingValueList(PTTID, cur))
322
        group.append(s)
323

    
324
        cur = SIGNAL[int(_mem.signal)]
325
        s = RadioSetting('signal', 'Signal',
326
                         RadioSettingValueList(SIGNAL, cur))
327
        group.append(s)
328

    
329
        return group
330

    
331
    def _set_extra(self, _mem, mem):
332
        _mem.bcl = int(mem.extra['bcl'].value)
333
        _mem.fhss = int(mem.extra['fhss'].value)
334
        _mem.pttid = int(mem.extra['pttid'].value)
335
        _mem.signal = int(mem.extra['signal'].value)
336

    
337
    def get_memory(self, num):
338
        _mem = self._memobj.memories[num]
339
        mem = chirp_common.Memory()
340
        mem.number = num
341
        if int(_mem.rxfreq) == 166666665:
342
            mem.empty = True
343
            return mem
344

    
345
        mem.name = ''.join([str(c) for c in self._memobj.names[num].name
346
                            if ord(str(c)) < 127]).rstrip()
347
        mem.freq = int(_mem.rxfreq) * 10
348
        offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
349
        if offset == 0:
350
            mem.duplex = ''
351
        elif abs(offset) < 100000000:
352
            mem.duplex = offset < 0 and '-' or '+'
353
            mem.offset = abs(offset)
354
        else:
355
            mem.duplex = 'split'
356
            mem.offset = int(_mem.txfreq) * 10
357

    
358
        mem.power = POWER_LEVELS[_mem.power]
359
        mem.mode = 'NFM' if _mem.narrow else 'FM'
360
        mem.skip = '' if _mem.scan else 'S'
361

    
362
        LOG.debug('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
363
        LOG.debug('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
364
        chirp_common.split_tone_decode(mem,
365
                                       self._decode_tone(_mem.txtone),
366
                                       self._decode_tone(_mem.rxtone))
367
        try:
368
            mem.extra = self._get_extra(_mem)
369
        except:
370
            LOG.exception('Failed to get extra for %i' % num)
371
        return mem
372

    
373
    def set_memory(self, mem):
374
        _mem = self._memobj.memories[mem.number]
375
        _nam = self._memobj.names[mem.number]
376

    
377
        if mem.empty:
378
            _mem.set_raw(b'\xff' * 16)
379
            _nam.set_raw(b'\xff' * 16)
380
            return
381

    
382
        if int(_mem.rxfreq) == 166666665:
383
            LOG.debug('Initializing new memory %i' % mem.number)
384
            _mem.set_raw(b'\x00' * 16)
385

    
386
        _nam.name = mem.name.ljust(10)
387

    
388
        _mem.rxfreq = mem.freq // 10
389
        if mem.duplex == '':
390
            _mem.txfreq = mem.freq // 10
391
        elif mem.duplex == 'split':
392
            _mem.txfreq = mem.offset // 10
393
        elif mem.duplex == 'off':
394
            _mem.txfreq.set_raw(b'\xff\xff\xff\xff')
395
        elif mem.duplex == '-':
396
            _mem.txfreq = (mem.freq - mem.offset) // 10
397
        elif mem.duplex == '+':
398
            _mem.txfreq = (mem.freq + mem.offset) // 10
399
        else:
400
            raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
401

    
402
        txtone, rxtone = chirp_common.split_tone_encode(mem)
403
        LOG.debug('tx tone is %s' % repr(txtone))
404
        LOG.debug('rx tone is %s' % repr(rxtone))
405
        _mem.txtone = self._encode_tone(*txtone)
406
        _mem.rxtone = self._encode_tone(*rxtone)
407

    
408
        try:
409
            _mem.power = POWER_LEVELS.index(mem.power)
410
        except ValueError:
411
            _mem.power = 0
412
        _mem.narrow = mem.mode == 'NFM'
413
        _mem.scan = mem.skip != 'S'
414
        if mem.extra:
415
            self._set_extra(_mem, mem)
416

    
417
    def get_settings(self):
418
        _set = self._memobj.settings
419

    
420
        basic = RadioSettingGroup('basic', 'Basic')
421
        adv = RadioSettingGroup('advanced', 'Advanced')
422
        dtmf = RadioSettingGroup('dtmf', 'DTMF')
423

    
424
        choice_settings = {
425
            'savemode': ['Off', 'Mode 1', 'Mode 2', 'Mode 3'],
426
            'vox': ['Off'] + ['%i' % i for i in range(1, 11)],
427
            'backlight': ['Off'] + ['%i' % i for i in range(1, 11)],
428
            'timeout': ['Off'] + ['%i' % i for i in range(15, 615, 15)],
429
            'language': ['English', 'Chinese'],
430
            'dtmfst': ['OFF', 'KB Side Tone', 'ANI Side Tone',
431
                       'KB ST+ANI ST', 'Both'],
432
            'scanmode': ['TO', 'CO', 'SE'],
433
            'pttid': ['Off', 'BOT', 'EOT', 'Both'],
434
            'cha_disp': ['CH+Name', 'CH+Freq'],
435
            'chb_disp': ['CH+Name', 'CH+Freq'],
436
            'alarm_mode': ['Site', 'Tone', 'Code'],
437
            'txundertdr': ['Off', 'Band A', 'Band B'],
438
            'rptnoiseclr': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
439
            'rptnoisedet': ['Off'] + ['%i' % i for i in range(100, 1001, 100)],
440
            'workmode': ['VFO', 'Chan'],
441
        }
442

    
443
        basic_settings = ['timeout', 'vox', 'backlight', 'language',
444
                          'cha_disp', 'chb_disp', 'workmode']
445
        titles = {
446
            'savemode': 'Save Mode',
447
            'vox': 'VOX',
448
            'backlight': 'Auto Backlight',
449
            'timeout': 'Time Out Timer (s)',
450
            'language': 'Language',
451
            'dtmfst': 'DTMF-ST',
452
            'scanmode': 'Scan Mode',
453
            'pttid': 'PTT-ID',
454
            'cha_disp': 'Channel A Display',
455
            'chb_disp': 'Channel B Display',
456
            'alarm_mode': 'Alarm Mode',
457
            'txundertdr': 'TX Under TDR',
458
            'rptnoiseclr': 'RPT Noise Clear (ms)',
459
            'rptnoisedet': 'RPT Noise Detect (ms)',
460
            'workmode': 'Work Mode',
461
        }
462

    
463
        basic.append(
464
            RadioSetting('squelch', 'Squelch Level',
465
                         RadioSettingValueInteger(0, 9, int(_set.squelch))))
466
        adv.append(
467
            RadioSetting('pttdelay', 'PTT Delay',
468
                         RadioSettingValueInteger(0, 30, int(_set.pttdelay))))
469
        adv.append(
470
            RadioSetting('tdr', 'TDR',
471
                         RadioSettingValueBoolean(
472
                             int(_set.tdr))))
473
        adv.append(
474
            RadioSetting('beep', 'Beep',
475
                         RadioSettingValueBoolean(
476
                             int(_set.beep))))
477
        basic.append(
478
            RadioSetting('voice', 'Voice Enable',
479
                         RadioSettingValueBoolean(
480
                             int(_set.voice))))
481
        adv.append(
482
            RadioSetting('bcl', 'BCL',
483
                         RadioSettingValueBoolean(
484
                             int(_set.bcl))))
485
        adv.append(
486
            RadioSetting('autolock', 'Auto Lock',
487
                         RadioSettingValueBoolean(
488
                             int(_set.autolock))))
489
        adv.append(
490
            RadioSetting('alarmsound', 'Alarm Sound',
491
                         RadioSettingValueBoolean(
492
                             int(_set.alarmsound))))
493
        adv.append(
494
            RadioSetting('tailnoiseclear', 'Tail Noise Clear',
495
                         RadioSettingValueBoolean(
496
                             int(_set.tailnoiseclear))))
497
        adv.append(
498
            RadioSetting('roger', 'Roger',
499
                         RadioSettingValueBoolean(
500
                             int(_set.roger))))
501
        adv.append(
502
            RadioSetting('fmradio', 'FM Radio Disabled',
503
                         RadioSettingValueBoolean(
504
                             int(_set.fmradio))))
505
        adv.append(
506
            RadioSetting('kblock', 'KB Lock',
507
                         RadioSettingValueBoolean(
508
                             int(_set.kblock))))
509

    
510
        for key in sorted(choice_settings):
511
            choices = choice_settings[key]
512
            title = titles[key]
513
            if key in basic_settings:
514
                group = basic
515
            else:
516
                group = adv
517

    
518
            val = int(getattr(_set, key))
519
            try:
520
                cur = choices[val]
521
            except IndexError:
522
                LOG.error('Value %i for %s out of range for list (%i): %s' % (
523
                    val, key, len(choices), choices))
524
                raise
525
            group.append(
526
                RadioSetting(key, title,
527
                             RadioSettingValueList(
528
                                 choices,
529
                                 choices[val])))
530

    
531
        for i in range(1, 16):
532
            cur = ''.join(
533
                DTMFCHARS[i]
534
                for i in self._memobj.dtmfgroup[i - 1].code if int(i) < 0xF)
535
            dtmf.append(
536
                RadioSetting(
537
                    'dtmf.code@%i' % i, 'DTMF Group %i' % i,
538
                    RadioSettingValueString(0, 5, cur,
539
                                            autopad=False,
540
                                            charset=DTMFCHARS)))
541
        cur = ''.join(
542
            '%X' % i
543
            for i in self._memobj.anicode.code if int(i) < 0xE)
544
        dtmf.append(
545
            RadioSetting(
546
                'anicode.code', 'ANI Code',
547
                RadioSettingValueString(0, 5, cur,
548
                                        autopad=False,
549
                                        charset=DTMFCHARS)))
550

    
551
        anicode = self._memobj.anicode
552

    
553
        dtmf.append(
554
            RadioSetting(
555
                'anicode.groupcode', 'Group Code',
556
                RadioSettingValueList(
557
                    list(DTMFCHARS),
558
                    DTMFCHARS[int(anicode.groupcode)])))
559

    
560
        dtmf.append(
561
            RadioSetting(
562
                'anicode.releasetosend', 'Release To Send',
563
                RadioSettingValueBoolean(
564
                    int(anicode.releasetosend))))
565
        dtmf.append(
566
            RadioSetting(
567
                'anicode.presstosend', 'Press To Send',
568
                RadioSettingValueBoolean(
569
                    int(anicode.presstosend))))
570
        cur = int(anicode.dtmfspeedon) * 10 + 80
571
        dtmf.append(
572
            RadioSetting(
573
                'anicode.dtmfspeedon', 'DTMF Speed (on time in ms)',
574
                RadioSettingValueInteger(60, 2000, cur, 10)))
575
        cur = int(anicode.dtmfspeedoff) * 10 + 80
576
        dtmf.append(
577
            RadioSetting(
578
                'anicode.dtmfspeedoff', 'DTMF Speed (off time in ms)',
579
                RadioSettingValueInteger(60, 2000, cur, 10)))
580

    
581
        top = RadioSettings(basic, adv, dtmf)
582
        return top
583

    
584
    def set_settings(self, settings):
585
        for element in settings:
586
            if element.get_name().startswith('anicode.'):
587
                self._set_anicode(element)
588
            elif element.get_name().startswith('dtmf.code'):
589
                self._set_dtmfcode(element)
590
            elif not isinstance(element, RadioSetting):
591
                self.set_settings(element)
592
                continue
593
            else:
594
                self._set_setting(element)
595

    
596
    def _set_setting(self, setting):
597
        key = setting.get_name()
598
        val = setting.value
599

    
600
        setattr(self._memobj.settings, key, int(val))
601

    
602
    def _set_anicode(self, setting):
603
        name = setting.get_name().split('.', 1)[1]
604
        if name == 'code':
605
            val = [DTMFCHARS.index(c) for c in str(setting.value)]
606
            for i in range(0, 5):
607
                try:
608
                    value = val[i]
609
                except IndexError:
610
                    value = 0xFF
611
                self._memobj.anicode.code[i] = value
612
        elif name.startswith('dtmfspeed'):
613
            setattr(self._memobj.anicode, name,
614
                    (int(setting.value) - 80) // 10)
615
        else:
616
            setattr(self._memobj.anicode, name, int(setting.value))
617

    
618

    
619
    def _set_dtmfcode(self, setting):
620
        index = int(setting.get_name().split('@', 1)[1]) - 1
621
        val = [DTMFCHARS.index(c) for c in str(setting.value)]
622
        for i in range(0, 5):
623
            try:
624
                value = val[i]
625
            except IndexError:
626
                value = 0xFF
627
            self._memobj.dtmfgroup[index].code[i] = value
628

    
(3-3/3)