Project

General

Profile

Bug #10093 » tk8180.py

Dan Smith, 10/23/2022 02:08 PM

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

    
16
import struct
17
import os
18
import time
19
import logging
20
from collections import OrderedDict
21

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

    
29
LOG = logging.getLogger(__name__)
30

    
31
# Gross hack to handle missing future module on un-updatable
32
# platforms like MacOS. Just avoid registering these radio
33
# classes for now.
34
try:
35
    from builtins import bytes
36
    has_future = True
37
except ImportError:
38
    has_future = False
39
    LOG.debug('python-future package is not '
40
              'available; %s requires it' % __name__)
41

    
42

    
43
HEADER_FORMAT = """
44
#seekto 0x0100;
45
struct {
46
  char sw_name[7];
47
  char sw_ver[5];
48
  u8 unknown1[4];
49
  char sw_key[12];
50
  u8 unknown2[4];
51
  char model[5];
52
  u8 variant;
53
  u8 unknown3[10];
54
} header;
55

    
56
#seekto 0x0140;
57
struct {
58
  // 0x0140
59
  u8 unknown1;
60
  u8 sublcd;
61
  u8 unknown2[30];
62

    
63
  // 0x0160
64
  char pon_msgtext[12];
65
  u8 min_volume;
66
  u8 max_volume;
67
  u8 lo_volume;
68
  u8 hi_volume;
69

    
70
  // 0x0170
71
  u8 tone_volume_offset;
72
  u8 poweron_tone;
73
  u8 control_tone;
74
  u8 warning_tone;
75
  u8 alert_tone;
76
  u8 sidetone;
77
  u8 locator_tone;
78
  u8 unknown3[2];
79
  u8 ignition_mode;
80
  u8 ignition_time;  // In tens of minutes (6 = 1h)
81
  u8 micsense;
82
  ul16 modereset;
83
  u8 min_vol_preset;
84
  u8 unknown4;
85

    
86
  // 0x0180
87
  u8 unknown5[16];
88

    
89
  // 0x0190
90
  u8 unknown6[3];
91
  u8 pon_msgtype;
92
  u8 unknown7[8];
93
  u8 unknown8_1:2,
94
     ssi:1,
95
     busy_led:1,
96
     power_switch_memory:1,
97
     scrambler_memory:1,
98
     unknown8_2:1,
99
     off_hook_decode:1;
100
  u8 unknown9_1:5,
101
     clockfmt:1,
102
     datefmt:1,
103
     ignition_sense:1;
104
  u8 unknownA[2];
105

    
106
  // 0x01A0
107
  u8 unknownB[8];
108
  u8 ptt_timer;
109
  u8 unknownB2[3];
110
  u8 ptt_proceed:1,
111
     unknownC_1:3,
112
     tone_off:1,
113
     ost_memory:1,
114
     unknownC_2:1,
115
     ptt_release:1;
116
  u8 unknownD[3];
117
} settings;
118

    
119
#seekto 0x01E0;
120
struct {
121
  char name[12];
122
  ul16 rxtone;
123
  ul16 txtone;
124
} ost_tones[40];
125

    
126
#seekto 0x0A00;
127
ul16 zone_starts[128];
128

    
129
struct zoneinfo {
130
  u8 number;
131
  u8 zonetype;
132
  u8 unknown1[2];
133
  u8 count;
134
  char name[12];
135
  u8 unknown2[2];
136
  ul16 timeout;    // 15-1200
137
  ul16 tot_alert;  // 10
138
  ul16 tot_rekey;  // 60
139
  ul16 tot_reset;  // 15
140
  u8 unknown3[3];
141
  u8 unknown21:2,
142
     bcl_override:1,
143
     unknown22:5;
144
  u8 unknown5;
145
};
146

    
147
struct memory {
148
  u8 number;
149
  lbcd rx_freq[4];
150
  lbcd tx_freq[4];
151
  u8 unknown1[2];
152
  ul16 rx_tone;
153
  ul16 tx_tone;
154
  char name[12];
155
  u8 unknown2[19];
156
  u8 unknown3_1:4,
157
     highpower:1,
158
     unknown3_2:1,
159
     wide:1,
160
     unknown3_3:1;
161
  u8 unknown4;
162
};
163

    
164
#seekto 0xC570;  // Fixme
165
u8 skipflags[64];
166
"""
167

    
168

    
169
SYSTEM_MEM_FORMAT = """
170
#seekto 0x%(addr)x;
171
struct {
172
  struct zoneinfo zoneinfo;
173
  struct memory memories[%(count)i];
174
} zone%(index)i;
175
"""
176

    
177
STARTUP_MODES = ['Text', 'Clock']
178

    
179
VOLUMES = OrderedDict([(str(x), x) for x in range(0, 30)])
180
VOLUMES.update({'Selectable': 0x30,
181
                'Current': 0xFF})
182
VOLUMES_REV = {v: k for k, v in VOLUMES.items()}
183

    
184
MIN_VOL_PRESET = {'Preset': 0x30,
185
                  'Lowest Limit': 0x31}
186
MIN_VOL_PRESET_REV = {v: k for k, v in MIN_VOL_PRESET.items()}
187

    
188
SUBLCD = ['Zone Number', 'CH/GID Number', 'OSD List Number']
189
CLOCKFMT = ['12H', '24H']
190
DATEFMT = ['Day/Month', 'Month/Day']
191
MICSENSE = ['On']
192
ONLY_MOBILE_SETTINGS = ['power_switch_memory', 'off_hook_decode',
193
                        'ignition_sense', 'mvp', 'it', 'ignition_mode']
194

    
195

    
196
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
197
                chirp_common.PowerLevel("High", watts=50)]
198

    
199

    
200
def set_choice(setting, obj, key, choices, default='Off'):
201
    settingstr = str(setting.value)
202
    if settingstr == default:
203
        val = 0xFF
204
    else:
205
        val = choices.index(settingstr) + 0x30
206
    setattr(obj, key, val)
207

    
208

    
209
def get_choice(obj, key, choices, default='Off'):
210
    val = getattr(obj, key)
211
    if val == 0xFF:
212
        return default
213
    else:
214
        return choices[val - 0x30]
215

    
216

    
217
def make_frame(cmd, addr, data=b""):
218
    return struct.pack(">BH", ord(cmd), addr) + data
219

    
220

    
221
def send(radio, frame):
222
    # LOG.debug("%04i P>R:\n%s" % (len(frame), util.hexprint(frame)))
223
    radio.pipe.write(frame)
224

    
225

    
226
def do_ident(radio):
227
    radio.pipe.baudrate = 9600
228
    radio.pipe.stopbits = 2
229
    radio.pipe.timeout = 1
230
    send(radio, b'PROGRAM')
231
    ack = radio.pipe.read(1)
232
    LOG.debug('Read %r from radio' % ack)
233
    if ack != b'\x16':
234
        raise errors.RadioError('Radio refused hi-speed program mode')
235
    radio.pipe.baudrate = 19200
236
    ack = radio.pipe.read(1)
237
    if ack != b'\x06':
238
        raise errors.RadioError('Radio refused program mode')
239
    radio.pipe.write(b'\x02')
240
    ident = radio.pipe.read(8)
241
    LOG.debug('Radio ident is %r' % ident)
242
    radio.pipe.write(b'\x06')
243
    ack = radio.pipe.read(1)
244
    if ack != b'\x06':
245
        raise errors.RadioError('Radio refused program mode')
246
    if ident[:6] not in (radio._model,):
247
        model = ident[:5].decode()
248
        variants = {b'\x06': 'K, K1, K3 (450-520MHz)',
249
                    b'\x07': 'K2, K4 (400-470MHz)'}
250
        if model == 'P3180':
251
            model += ' ' + variants.get(ident[5], '(Unknown)')
252
        raise errors.RadioError('Unsupported radio model %s' % model)
253

    
254

    
255
def checksum_data(data):
256
    _chksum = 0
257
    for byte in data:
258
        _chksum = (_chksum + byte) & 0xFF
259
    return _chksum
260

    
261

    
262
def do_download(radio):
263
    do_ident(radio)
264

    
265
    data = bytes()
266

    
267
    def status():
268
        status = chirp_common.Status()
269
        status.cur = len(data)
270
        status.max = radio._memsize
271
        status.msg = "Cloning from radio"
272
        radio.status_fn(status)
273
        LOG.debug('Radio address 0x%04x' % len(data))
274

    
275
    # Addresses 0x0000-0xBF00 pulled by block number (divide by 0x100)
276
    for block in range(0, 0xBF + 1):
277
        send(radio, make_frame('R', block))
278
        cmd = radio.pipe.read(1)
279
        chunk = b''
280
        if cmd == b'Z':
281
            data += bytes(b'\xff' * 256)
282
            LOG.debug('Radio reports empty block %02x' % block)
283
        elif cmd == b'W':
284
            chunk = bytes(radio.pipe.read(256))
285
            if len(chunk) != 256:
286
                LOG.error('Received %i for block %02x' % (len(chunk), block))
287
                raise errors.RadioError('Radio did not send block')
288
            data += chunk
289
        else:
290
            LOG.error('Radio sent %r (%02x), expected W(0x57)' % (cmd,
291
                                                                  chr(cmd)))
292
            raise errors.RadioError('Radio sent unexpected response')
293

    
294
        LOG.debug('Read block index %02x' % block)
295
        status()
296

    
297
        chksum = radio.pipe.read(1)
298
        if len(chksum) != 1:
299
            LOG.error('Checksum was %r' % chksum)
300
            raise errors.RadioError('Radio sent invalid checksum')
301
        _chksum = checksum_data(chunk)
302

    
303
        if chunk and _chksum != ord(chksum):
304
            LOG.error(
305
                'Checksum failed for %i byte block 0x%02x: %02x != %02x' % (
306
                    len(chunk), block, _chksum, ord(chksum)))
307
            raise errors.RadioError('Checksum failure while reading block. '
308
                                    'Check serial cable.')
309

    
310
        radio.pipe.write(b'\x06')
311
        if radio.pipe.read(1) != b'\x06':
312
            raise errors.RadioError('Post-block exchange failed')
313

    
314
    # Addresses 0xC000 - 0xD1F0 pulled by address
315
    for block in range(0x0100, 0x1200, 0x40):
316
        send(radio, make_frame('S', block, b'\x40'))
317
        x = radio.pipe.read(1)
318
        if x != b'X':
319
            raise errors.RadioError('Radio did not send block')
320
        chunk = radio.pipe.read(0x40)
321
        data += chunk
322

    
323
        LOG.debug('Read memory address %04x' % block)
324
        status()
325

    
326
        radio.pipe.write(b'\x06')
327
        if radio.pipe.read(1) != b'\x06':
328
            raise errors.RadioError('Post-block exchange failed')
329

    
330
    radio.pipe.write(b'E')
331
    if radio.pipe.read(1) != b'\x06':
332
        raise errors.RadioError('Radio failed to acknowledge completion')
333

    
334
    LOG.debug('Read %i bytes total' % len(data))
335
    return data
336

    
337

    
338
def do_upload(radio):
339
    do_ident(radio)
340

    
341
    def status(addr):
342
        status = chirp_common.Status()
343
        status.cur = addr
344
        status.max = radio._memsize
345
        status.msg = "Cloning to radio"
346
        radio.status_fn(status)
347

    
348
    for block in range(0, 0xBF + 1):
349
        addr = block * 0x100
350
        chunk = bytes(radio._mmap[addr:addr + 0x100])
351
        if all(byte == b'\xff' for byte in chunk):
352
            LOG.debug('Sending zero block %i, range 0x%04x' % (block, addr))
353
            send(radio, make_frame('Z', block, b'\xFF'))
354
        else:
355
            checksum = checksum_data(chunk)
356
            send(radio, make_frame('W', block, chunk + chr(checksum)))
357

    
358
        ack = radio.pipe.read(1)
359
        if ack != b'\x06':
360
            LOG.error('Radio refused block 0x%02x with %r' % (block, ack))
361
            raise errors.RadioError('Radio refused data block')
362

    
363
        status(addr)
364

    
365
    addr_base = 0xC000
366
    for addr in range(addr_base, radio._memsize, 0x40):
367
        block_addr = addr - addr_base + 0x0100
368
        chunk = radio._mmap[addr:addr + 0x40]
369
        send(radio, make_frame('X', block_addr, b'\x40' + chunk))
370

    
371
        ack = radio.pipe.read(1)
372
        if ack != b'\x06':
373
            LOG.error('Radio refused address 0x%02x with %r' % (block_addr,
374
                                                                ack))
375
            raise errors.RadioError('Radio refused data block')
376

    
377
        status(addr)
378

    
379
    radio.pipe.write(b'E')
380
    if radio.pipe.read(1) != b'\x06':
381
        raise errors.RadioError('Radio failed to acknowledge completion')
382

    
383

    
384
def reset(self):
385
    try:
386
        self.pipe.baudrate = 9600
387
        self.pipe.write(b'E')
388
        time.sleep(0.5)
389
        self.pipe.baudrate = 19200
390
        self.pipe.write(b'E')
391
    except Exception:
392
        LOG.error('Unable to send reset sequence')
393

    
394

    
395
class KenwoodTKx180Radio(chirp_common.CloneModeRadio):
396
    """Kenwood TK-x180"""
397
    VENDOR = 'Kenwood'
398
    MODEL = 'TK-x180'
399
    BAUD_RATE = 9600
400
    NEEDS_COMPAT_SERIAL = False
401

    
402
    _system_start = 0x0B00
403
    _memsize = 0xD100
404

    
405
    def __init__(self, *a, **k):
406
        self._zones = []
407
        chirp_common.CloneModeRadio.__init__(self, *a, **k)
408

    
409
    def sync_in(self):
410
        try:
411
            data = do_download(self)
412
            self._mmap = memmap.MemoryMapBytes(data)
413
        except errors.RadioError:
414
            reset(self)
415
            raise
416
        except Exception as e:
417
            reset(self)
418
            LOG.exception('General failure')
419
            raise errors.RadioError('Failed to download from radio: %s' % e)
420
        self.process_mmap()
421

    
422
    def sync_out(self):
423
        try:
424
            do_upload(self)
425
        except Exception as e:
426
            reset(self)
427
            LOG.exception('General failure')
428
            raise errors.RadioError('Failed to upload to radio: %s' % e)
429

    
430
    @property
431
    def is_portable(self):
432
        return self._model.startswith(b'P')
433

    
434
    def probe_layout(self):
435
        start_addrs = []
436
        tmp_format = '#seekto 0x0A00; ul16 zone_starts[128];'
437
        mem = bitwise.parse(tmp_format, self._mmap)
438
        zone_format = """struct zoneinfo {
439
        u8 number;
440
        u8 zonetype;
441
        u8 unknown1[2];
442
        u8 count;
443
        char name[12];
444
        u8 unknown2[15];
445
        };"""
446

    
447
        zone_addresses = []
448
        for i in range(0, 128):
449
            if mem.zone_starts[i] == 0xFFFF:
450
                break
451
            zone_addresses.append(mem.zone_starts[i])
452
            zone_format += '#seekto 0x%x; struct zoneinfo zone%i;' % (
453
                mem.zone_starts[i], i)
454

    
455
        zoneinfo = bitwise.parse(zone_format, self._mmap)
456
        zones = []
457
        for i, addr in enumerate(zone_addresses):
458
            zone = getattr(zoneinfo, 'zone%i' % i)
459
            if zone.zonetype != 0x31:
460
                LOG.error('Zone %i is type 0x%02x; '
461
                          'I only support 0x31 (conventional)')
462
                raise errors.RadioError(
463
                    'Unsupported non-conventional zone found in radio; '
464
                    'Refusing to load to safeguard your data!')
465
            zones.append((addr, zone.count))
466

    
467
        LOG.debug('Zones: %s' % zones)
468
        return zones
469

    
470
    def process_mmap(self):
471
        self._zones = self.probe_layout()
472

    
473
        mem_format = HEADER_FORMAT
474
        for index, (addr, count) in enumerate(self._zones):
475
            mem_format += '\n\n' + (
476
                SYSTEM_MEM_FORMAT % {
477
                    'addr': addr,
478
                    'count': max(count, 2),   # bitwise bug, one-element array
479
                    'index': index})
480

    
481
        self._memobj = bitwise.parse(mem_format, self._mmap)
482

    
483
    def expand_mmap(self, zone_sizes):
484
        """Remap memory into zones of the specified sizes, copying things
485
        around to keep the contents, as appropriate."""
486
        old_zones = self._zones
487
        old_memobj = self._memobj
488

    
489
        self._mmap = memmap.MemoryMapBytes(bytes(self._mmap.get_packed()))
490

    
491
        new_format = HEADER_FORMAT
492
        addr = self._system_start
493
        self._zones = []
494
        for index, count in enumerate(zone_sizes):
495
            new_format += SYSTEM_MEM_FORMAT % {
496
                'addr': addr,
497
                'count': max(count, 2),  # bitwise bug
498
                'index': index}
499
            self._zones.append((addr, count))
500
            addr += 0x20 + (count * 0x30)
501

    
502
        self._memobj = bitwise.parse(new_format, self._mmap)
503

    
504
        # Set all known zone addresses and clear the rest
505
        for index in range(0, 128):
506
            try:
507
                self._memobj.zone_starts[index] = self._zones[index][0]
508
            except IndexError:
509
                self._memobj.zone_starts[index] = 0xFFFF
510

    
511
        for zone_number, count in enumerate(zone_sizes):
512
            dest_zone = getattr(self._memobj, 'zone%i' % zone_number)
513
            dest = dest_zone.memories
514
            dest_zoneinfo = dest_zone.zoneinfo
515

    
516
            if zone_number < len(old_zones):
517
                LOG.debug('Copying existing zone %i' % zone_number)
518
                _, old_count = old_zones[zone_number]
519
                source_zone = getattr(old_memobj, 'zone%i' % zone_number)
520
                source = source_zone.memories
521
                source_zoneinfo = source_zone.zoneinfo
522

    
523
                if old_count != count:
524
                    LOG.debug('Zone %i going from %i to %i' % (zone_number,
525
                                                               old_count,
526
                                                               count))
527

    
528
                # Copy the zone record from the source, but then update
529
                # the count
530
                dest_zoneinfo.set_raw(source_zoneinfo.get_raw())
531
                dest_zoneinfo.count = count
532

    
533
                source_i = 0
534
                for dest_i in range(0, min(count, old_count)):
535
                    dest[dest_i].set_raw(source[dest_i].get_raw())
536
            else:
537
                LOG.debug('New zone %i' % zone_number)
538
                dest_zone.zoneinfo.number = zone_number + 1
539
                dest_zone.zoneinfo.zonetype = 0x31
540
                dest_zone.zoneinfo.count = count
541
                dest_zone.zoneinfo.name = (
542
                    'Zone %i' % (zone_number + 1)).ljust(12)
543

    
544
    def shuffle_zone(self):
545
        """Sort the memories in the zone according to logical channel number"""
546
        # FIXME: Move this to the zone
547
        raw_memories = self.raw_memories
548
        memories = [(i, raw_memories[i].number)
549
                    for i in range(0, self.raw_zoneinfo.count)]
550
        current = memories[:]
551
        memories.sort(key=lambda t: t[1])
552
        if current == memories:
553
            LOG.debug('Shuffle not required')
554
            return
555
        raw_data = [raw_memories[i].get_raw() for i, n in memories]
556
        for i, raw_mem in enumerate(raw_data):
557
            raw_memories[i].set_raw(raw_mem)
558

    
559
    @classmethod
560
    def get_prompts(cls):
561
        rp = chirp_common.RadioPrompts()
562
        rp.info = ('This radio is zone-based, which is different from how '
563
                   'most radios work (that CHIRP supports). The zone count '
564
                   'can be adjusted in the Settings tab, but you must save '
565
                   'and re-load the file after changing that value in order '
566
                   'to be able to add/edit memories there.')
567
        rp.experimental = ('This driver is very experimental. Every attempt '
568
                           'has been made to be overly pedantic to avoid '
569
                           'destroying data. However, you should use caution, '
570
                           'maintain backups, and proceed at your own risk.')
571
        return rp
572

    
573
    def get_features(self):
574
        rf = chirp_common.RadioFeatures()
575
        rf.has_ctone = True
576
        rf.has_cross = True
577
        rf.has_tuning_step = False
578
        rf.has_settings = True
579
        rf.has_bank = False
580
        rf.has_sub_devices = True
581
        rf.has_rx_dtcs = True
582
        rf.can_odd_split = True
583
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
584
        rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
585
                                'DTCS->Tone', '->Tone', 'DTCS->DTCS']
586
        rf.valid_bands = self.VALID_BANDS
587
        rf.valid_modes = ['FM', 'NFM']
588
        rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0,
589
                                 25.0, 50.0, 100.0]
590
        rf.valid_duplexes = ['', '-', '+', 'split', 'off']
591
        rf.valid_power_levels = POWER_LEVELS
592
        rf.valid_name_length = 12
593
        rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
594
                               'abcdefghijklmnopqrstuvwxyz'
595
                               '0123456789'
596
                               '!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ')
597
        rf.memory_bounds = (1, 512)
598
        return rf
599

    
600
    @property
601
    def raw_zone(self):
602
        return getattr(self._memobj, 'zone%i' % self._zone)
603

    
604
    @property
605
    def raw_zoneinfo(self):
606
        return self.raw_zone.zoneinfo
607

    
608
    @property
609
    def raw_memories(self):
610
        return self.raw_zone.memories
611

    
612
    @property
613
    def max_mem(self):
614
        return self.raw_memories[self.raw_zoneinfo.count].number
615

    
616
    def _get_raw_memory(self, number):
617
        for i in range(0, self.raw_zoneinfo.count):
618
            if self.raw_memories[i].number == number:
619
                return self.raw_memories[i]
620
        return None
621

    
622
    def get_raw_memory(self, number):
623
        return repr(self._get_raw_memory(number))
624

    
625
    @staticmethod
626
    def _decode_tone(toneval):
627
        # DCS examples:
628
        # D024N - 2814 - 0010 1000 0001 0100
629
        #                  ^--DCS
630
        # D024I - A814 - 1010 1000 0001 0100
631
        #                ^----inverted
632
        # D754I - A9EC - 1010 1001 1110 1100
633
        #    code in octal-------^^^^^^^^^^^
634

    
635
        pol = toneval & 0x8000 and 'R' or 'N'
636
        if toneval == 0xFFFF:
637
            return '', None, None
638
        elif toneval & 0x2000:
639
            # DTCS
640
            code = int('%o' % (toneval & 0x1FF))
641
            return 'DTCS', code, pol
642
        else:
643
            return 'Tone', toneval / 10.0, None
644

    
645
    @staticmethod
646
    def _encode_tone(mode, val, pol):
647
        if not mode:
648
            return 0xFFFF
649
        elif mode == 'Tone':
650
            return int(val * 10)
651
        elif mode == 'DTCS':
652
            code = int('%i' % val, 8)
653
            code |= 0x2800
654
            if pol == 'R':
655
                code |= 0x8000
656
            return code
657
        else:
658
            raise errors.RadioError('Unsupported tone mode %r' % mode)
659

    
660
    def get_memory(self, number):
661
        mem = chirp_common.Memory()
662
        mem.number = number
663
        _mem = self._get_raw_memory(number)
664
        if _mem is None:
665
            mem.empty = True
666
            return mem
667

    
668
        mem.name = str(_mem.name).rstrip('\x00')
669
        mem.freq = int(_mem.rx_freq) * 10
670
        chirp_common.split_tone_decode(mem,
671
                                       self._decode_tone(_mem.tx_tone),
672
                                       self._decode_tone(_mem.rx_tone))
673
        if _mem.wide:
674
            mem.mode = 'FM'
675
        else:
676
            mem.mode = 'NFM'
677

    
678
        mem.power = POWER_LEVELS[_mem.highpower]
679

    
680
        offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
681
        if offset == 0:
682
            mem.duplex = ''
683
        elif abs(offset) < 10000000:
684
            mem.duplex = offset < 0 and '-' or '+'
685
            mem.offset = abs(offset)
686
        else:
687
            mem.duplex = 'split'
688
            mem.offset = int(_mem.tx_freq) * 10
689

    
690
        skipbyte = self._memobj.skipflags[(mem.number - 1) // 8]
691
        skipbit = skipbyte & (1 << (mem.number - 1) % 8)
692
        mem.skip = skipbit and 'S' or ''
693

    
694
        return mem
695

    
696
    def set_memory(self, mem):
697
        _mem = self._get_raw_memory(mem.number)
698
        if _mem is None:
699
            LOG.debug('Need to expand zone %i' % self._zone)
700

    
701
            # Calculate the new zone sizes and remap memory
702
            new_zones = [x[1] for x in self._parent._zones]
703
            new_zones[self._zone] = new_zones[self._zone] + 1
704
            self._parent.expand_mmap(new_zones)
705

    
706
            # Assign the new memory (at the end) to the desired
707
            # number
708
            _mem = self.raw_memories[self.raw_zoneinfo.count - 1]
709
            _mem.number = mem.number
710

    
711
            # Sort the memory into place
712
            self.shuffle_zone()
713

    
714
            # Now find it in the right spot
715
            _mem = self._get_raw_memory(mem.number)
716
            if _mem is None:
717
                raise errors.RadioError('Internal error after '
718
                                        'memory allocation')
719

    
720
            # Default values for unknown things
721
            _mem.unknown1[0] = 0x36
722
            _mem.unknown1[1] = 0x36
723
            _mem.unknown2 = [0xFF for i in range(0, 19)]
724
            _mem.unknown3_1 = 0xF
725
            _mem.unknown3_2 = 0x1
726
            _mem.unknown3_3 = 0x0
727
            _mem.unknown4 = 0xFF
728

    
729
        if mem.empty:
730
            LOG.debug('Need to shrink zone %i' % self._zone)
731
            # Make the memory sort to the end, and sort the zone
732
            _mem.number = 0xFF
733
            self.shuffle_zone()
734

    
735
            # Calculate the new zone sizes and remap memory
736
            new_zones = [x[1] for x in self._parent._zones]
737
            new_zones[self._zone] = new_zones[self._zone] - 1
738
            self._parent.expand_mmap(new_zones)
739
            return
740

    
741
        _mem.name = mem.name[:12].encode().rstrip().ljust(12, b'\x00')
742
        _mem.rx_freq = mem.freq // 10
743

    
744
        txtone, rxtone = chirp_common.split_tone_encode(mem)
745
        _mem.tx_tone = self._encode_tone(*txtone)
746
        _mem.rx_tone = self._encode_tone(*rxtone)
747

    
748
        _mem.wide = mem.mode == 'FM'
749
        _mem.highpower = mem.power == POWER_LEVELS[1]
750

    
751
        if mem.duplex == '':
752
            _mem.tx_freq = mem.freq // 10
753
        elif mem.duplex == 'split':
754
            _mem.tx_freq = mem.offset // 10
755
        elif mem.duplex == 'off':
756
            _mem.tx_freq.set_raw(b'\xff\xff\xff\xff')
757
        elif mem.duplex == '-':
758
            _mem.tx_freq = (mem.freq - mem.offset) // 10
759
        elif mem.duplex == '+':
760
            _mem.tx_freq = (mem.freq + mem.offset) // 10
761
        else:
762
            raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
763

    
764
        skipbyte = self._memobj.skipflags[(mem.number - 1) // 8]
765
        if mem.skip == 'S':
766
            skipbyte |= (1 << (mem.number - 1) % 8)
767
        else:
768
            skipbyte &= ~(1 << (mem.number - 1) % 8)
769

    
770
    def _pure_choice_setting(self, settings_key, name, choices, default='Off'):
771
        if default is not None:
772
            ui_choices = [default] + choices
773
        else:
774
            ui_choices = choices
775
        s = RadioSetting(
776
            settings_key, name,
777
            RadioSettingValueList(
778
                ui_choices,
779
                get_choice(self._memobj.settings, settings_key,
780
                           choices, default)))
781
        s.set_apply_callback(set_choice, self._memobj.settings,
782
                             settings_key, choices, default)
783
        return s
784

    
785
    def _inverted_flag_setting(self, key, name, obj=None):
786
        if obj is None:
787
            obj = self._memobj.settings
788

    
789
        def apply_inverted(setting, key):
790
            setattr(obj, key, not int(setting.value))
791

    
792
        v = not getattr(obj, key)
793
        s = RadioSetting(
794
                key, name,
795
                RadioSettingValueBoolean(v))
796
        s.set_apply_callback(apply_inverted, key)
797
        return s
798

    
799
    def _get_common1(self):
800
        settings = self._memobj.settings
801
        common1 = RadioSettingGroup('common1', 'Common 1')
802

    
803
        common1.append(self._pure_choice_setting('sublcd',
804
                                                 'Sub LCD Display',
805
                                                 SUBLCD,
806
                                                 default='None'))
807

    
808
        def apply_clockfmt(setting):
809
            settings.clockfmt = CLOCKFMT.index(str(setting.value))
810

    
811
        clockfmt = RadioSetting(
812
            'clockfmt', 'Clock Format',
813
            RadioSettingValueList(CLOCKFMT,
814
                                  CLOCKFMT[settings.clockfmt]))
815
        clockfmt.set_apply_callback(apply_clockfmt)
816
        common1.append(clockfmt)
817

    
818
        def apply_datefmt(setting):
819
            settings.datefmt = DATEFMT.index(str(setting.value))
820

    
821
        datefmt = RadioSetting(
822
            'datefmt', 'Date Format',
823
            RadioSettingValueList(DATEFMT,
824
                                  DATEFMT[settings.datefmt]))
825
        datefmt.set_apply_callback(apply_datefmt)
826
        common1.append(datefmt)
827

    
828
        common1.append(self._pure_choice_setting('micsense',
829
                                                 'Mic Sense High',
830
                                                 MICSENSE))
831

    
832
        def apply_modereset(setting):
833
            val = int(setting.value)
834
            if val == 0:
835
                val = 0xFFFF
836
            settings.modereset = val
837

    
838
        _modereset = int(settings.modereset)
839
        if _modereset == 0xFFFF:
840
            _modereset = 0
841
        modereset = RadioSetting(
842
            'modereset', 'Mode Reset Timer',
843
            RadioSettingValueInteger(0, 300, _modereset))
844
        modereset.set_apply_callback(apply_modereset)
845
        common1.append(modereset)
846

    
847
        inverted_flags = [('power_switch_memory', 'Power Switch Memory'),
848
                          ('scrambler_memory', 'Scrambler Memory'),
849
                          ('off_hook_decode', 'Off-Hook Decode'),
850
                          ('ssi', 'Signal Strength Indicator'),
851
                          ('ignition_sense', 'Ingnition Sense')]
852
        for key, name in inverted_flags:
853
            if self.is_portable and key in ONLY_MOBILE_SETTINGS:
854
                # Skip settings that are not valid for portables
855
                continue
856
            common1.append(self._inverted_flag_setting(key, name))
857

    
858
        if not self.is_portable and 'ignition_mode' in ONLY_MOBILE_SETTINGS:
859
            common1.append(self._pure_choice_setting('ignition_mode',
860
                                                     'Ignition Mode',
861
                                                     ['Ignition & SW',
862
                                                      'Ignition Only'],
863
                                                     None))
864

    
865
        def apply_it(setting):
866
            settings.ignition_time = int(setting.value) / 600
867

    
868
        _it = int(settings.ignition_time) * 600
869
        it = RadioSetting(
870
            'it', 'Ignition Timer (s)',
871
            RadioSettingValueInteger(10, 28800, _it))
872
        it.set_apply_callback(apply_it)
873
        if not self.is_portable and 'it' in ONLY_MOBILE_SETTINGS:
874
            common1.append(it)
875

    
876
        return common1
877

    
878
    def _get_common2(self):
879
        settings = self._memobj.settings
880
        common2 = RadioSettingGroup('common2', 'Common 2')
881

    
882
        def apply_ponmsgtext(setting):
883
            settings.pon_msgtext = (
884
                str(setting.value)[:12].strip().ljust(12, '\x00'))
885

    
886
        common2.append(
887
            self._pure_choice_setting('pon_msgtype', 'Power On Message Type',
888
                                      STARTUP_MODES))
889

    
890
        _text = str(settings.pon_msgtext).rstrip('\x00')
891
        text = RadioSetting('settings.pon_msgtext',
892
                            'Power On Text',
893
                            RadioSettingValueString(
894
                                0, 12, _text))
895
        text.set_apply_callback(apply_ponmsgtext)
896
        common2.append(text)
897

    
898
        def apply_volume(setting, key):
899
            setattr(settings, key, VOLUMES[str(setting.value)])
900

    
901
        volumes = {'poweron_tone': 'Power-on Tone',
902
                   'control_tone': 'Control Tone',
903
                   'warning_tone': 'Warning Tone',
904
                   'alert_tone': 'Alert Tone',
905
                   'sidetone': 'Sidetone',
906
                   'locator_tone': 'Locator Tone'}
907
        for value, name in volumes.items():
908
            setting = getattr(settings, value)
909
            volume = RadioSetting('settings.%s' % value, name,
910
                                  RadioSettingValueList(
911
                                      VOLUMES.keys(),
912
                                      VOLUMES_REV.get(int(setting), 0)))
913
            volume.set_apply_callback(apply_volume, value)
914
            common2.append(volume)
915

    
916
        def apply_vol_level(setting, key):
917
            setattr(settings, key, int(setting.value))
918

    
919
        levels = {'lo_volume': 'Low Volume Level (Fixed Volume)',
920
                  'hi_volume': 'High Volume Level (Fixed Volume)',
921
                  'min_volume': 'Minimum Audio Volume',
922
                  'max_volume': 'Maximum Audio Volume'}
923
        for value, name in levels.items():
924
            setting = getattr(settings, value)
925
            if 'Audio' in name:
926
                minimum = 0
927
            else:
928
                minimum = 1
929
            volume = RadioSetting(
930
                'settings.%s' % value, name,
931
                RadioSettingValueInteger(minimum, 31, int(setting)))
932
            volume.set_apply_callback(apply_vol_level, value)
933
            common2.append(volume)
934

    
935
        def apply_vo(setting):
936
            val = int(setting.value)
937
            if val < 0:
938
                val = abs(val) | 0x80
939
            settings.tone_volume_offset = val
940

    
941
        _voloffset = int(settings.tone_volume_offset)
942
        if _voloffset & 0x80:
943
            _voloffset = abs(_voloffset & 0x7F) * -1
944
        voloffset = RadioSetting(
945
            'tvo', 'Tone Volume Offset',
946
            RadioSettingValueInteger(
947
                -5, 5,
948
                _voloffset))
949
        voloffset.set_apply_callback(apply_vo)
950
        common2.append(voloffset)
951

    
952
        def apply_mvp(setting):
953
            settings.min_vol_preset = MIN_VOL_PRESET[str(setting.value)]
954

    
955
        _volpreset = int(settings.min_vol_preset)
956
        volpreset = RadioSetting(
957
            'mvp', 'Minimum Volume Type',
958
            RadioSettingValueList(MIN_VOL_PRESET.keys(),
959
                                  MIN_VOL_PRESET_REV[_volpreset]))
960
        volpreset.set_apply_callback(apply_mvp)
961
        if not self.is_portable and 'mvp' in ONLY_MOBILE_SETTINGS:
962
            common2.append(volpreset)
963

    
964
        return common2
965

    
966
    def _get_conventional(self):
967
        settings = self._memobj.settings
968

    
969
        conv = RadioSettingGroup('conv', 'Conventional')
970
        inverted_flags = [('busy_led', 'Busy LED'),
971
                          ('ost_memory', 'OST Status Memory'),
972
                          ('tone_off', 'Tone Off'),
973
                          ('ptt_release', 'PTT Release tone'),
974
                          ('ptt_proceed', 'PTT Proceed Tone')]
975
        for key, name in inverted_flags:
976
            conv.append(self._inverted_flag_setting(key, name))
977

    
978
        def apply_pttt(setting):
979
            settings.ptt_timer = int(setting.value)
980

    
981
        pttt = RadioSetting(
982
            'pttt', 'PTT Proceed Tone Timer (ms)',
983
            RadioSettingValueInteger(0, 6000, int(settings.ptt_timer)))
984
        pttt.set_apply_callback(apply_pttt)
985
        conv.append(pttt)
986

    
987
        self._get_ost(conv)
988

    
989
        return conv
990

    
991
    def _get_zones(self):
992
        zones = RadioSettingGroup('zones', 'Zones')
993

    
994
        zone_count = RadioSetting('_zonecount',
995
                                  'Number of Zones',
996
                                  RadioSettingValueInteger(
997
                                      1, 128, len(self._zones)))
998
        zone_count.set_doc('Number of zones in the radio. '
999
                           'Requires a save and re-load of the '
1000
                           'file to take effect. Reducing this number '
1001
                           'will DELETE memories in affected zones!')
1002
        zones.append(zone_count)
1003

    
1004
        for i in range(len(self._zones)):
1005
            zone = RadioSettingGroup('zone%i' % i, 'Zone %i' % (i + 1))
1006

    
1007
            _zone = getattr(self._memobj, 'zone%i' % i).zoneinfo
1008
            _name = str(_zone.name).rstrip('\x00')
1009
            name = RadioSetting('name%i' % i, 'Name',
1010
                                RadioSettingValueString(0, 12, _name))
1011
            zone.append(name)
1012

    
1013
            def apply_timer(setting, key, zone_number):
1014
                val = int(setting.value)
1015
                if val == 0:
1016
                    val = 0xFFFF
1017
                _zone = getattr(self._memobj, 'zone%i' % zone_number).zoneinfo
1018
                setattr(_zone, key, val)
1019

    
1020
            def collapse(val):
1021
                val = int(val)
1022
                if val == 0xFFFF:
1023
                    val = 0
1024
                return val
1025

    
1026
            timer = RadioSetting(
1027
                'timeout', 'Time-out Timer',
1028
                RadioSettingValueInteger(15, 1200, collapse(_zone.timeout)))
1029
            timer.set_apply_callback(apply_timer, 'timeout', i)
1030
            zone.append(timer)
1031

    
1032
            timer = RadioSetting(
1033
                'tot_alert', 'TOT Pre-Alert',
1034
                RadioSettingValueInteger(0, 10, collapse(_zone.tot_alert)))
1035
            timer.set_apply_callback(apply_timer, 'tot_alert', i)
1036
            zone.append(timer)
1037

    
1038
            timer = RadioSetting(
1039
                'tot_rekey', 'TOT Re-Key Time',
1040
                RadioSettingValueInteger(0, 60, collapse(_zone.tot_rekey)))
1041
            timer.set_apply_callback(apply_timer, 'tot_rekey', i)
1042
            zone.append(timer)
1043

    
1044
            timer = RadioSetting(
1045
                'tot_reset', 'TOT Reset Time',
1046
                RadioSettingValueInteger(0, 15, collapse(_zone.tot_reset)))
1047
            timer.set_apply_callback(apply_timer, 'tot_reset', i)
1048
            zone.append(timer)
1049

    
1050
            zone.append(self._inverted_flag_setting(
1051
                'bcl_override', 'BCL Override',
1052
                _zone))
1053

    
1054
            zones.append(zone)
1055

    
1056
        return zones
1057

    
1058
    def _get_ost(self, parent):
1059
        tones = chirp_common.TONES[:]
1060

    
1061
        def apply_tone(setting, index, which):
1062
            if str(setting.value) == 'Off':
1063
                val = 0xFFFF
1064
            else:
1065
                val = int(float(str(setting.value)) * 10)
1066
            setattr(self._memobj.ost_tones[index], '%stone' % which, val)
1067

    
1068
        def _tones():
1069
            return ['Off'] + [str(x) for x in tones]
1070

    
1071
        for i in range(0, 40):
1072
            _ost = self._memobj.ost_tones[i]
1073
            ost = RadioSettingGroup('ost%i' % i,
1074
                                    'OST %i' % (i + 1))
1075

    
1076
            cur = str(_ost.name).rstrip('\x00')
1077
            name = RadioSetting('name%i' % i, 'Name',
1078
                                RadioSettingValueString(0, 12, cur))
1079
            ost.append(name)
1080

    
1081
            if _ost.rxtone == 0xFFFF:
1082
                cur = 'Off'
1083
            else:
1084
                cur = round(int(_ost.rxtone) / 10.0, 1)
1085
                if cur not in tones:
1086
                    LOG.debug('Non-standard OST rx tone %i %s' % (i, cur))
1087
                    tones.append(cur)
1088
                    tones.sort()
1089
            rx = RadioSetting('rxtone%i' % i, 'RX Tone',
1090
                              RadioSettingValueList(_tones(),
1091
                                                    str(cur)))
1092
            rx.set_apply_callback(apply_tone, i, 'rx')
1093
            ost.append(rx)
1094

    
1095
            if _ost.txtone == 0xFFFF:
1096
                cur = 'Off'
1097
            else:
1098
                cur = round(int(_ost.txtone) / 10.0, 1)
1099
                if cur not in tones:
1100
                    LOG.debug('Non-standard OST tx tone %i %s' % (i, cur))
1101
                    tones.append(cur)
1102
                    tones.sort()
1103
            tx = RadioSetting('txtone%i' % i, 'TX Tone',
1104
                              RadioSettingValueList(_tones(),
1105
                                                    str(cur)))
1106
            tx.set_apply_callback(apply_tone, i, 'tx')
1107
            ost.append(tx)
1108

    
1109
            parent.append(ost)
1110

    
1111
    def get_settings(self):
1112
        settings = self._memobj.settings
1113

    
1114
        zones = self._get_zones()
1115
        common1 = self._get_common1()
1116
        common2 = self._get_common2()
1117
        conv = self._get_conventional()
1118
        top = RadioSettings(zones, common1, common2, conv)
1119
        return top
1120

    
1121
    def set_settings(self, settings):
1122
        for element in settings:
1123
            if not isinstance(element, RadioSetting):
1124
                self.set_settings(element)
1125
                continue
1126
            elif element.get_name() == '_zonecount':
1127
                new_zone_count = int(element.value)
1128
                zone_sizes = [x[1] for x in self._zones[:new_zone_count]]
1129
                if len(self._zones) > new_zone_count:
1130
                    self.expand_mmap(zone_sizes[:new_zone_count])
1131
                elif len(self._zones) < new_zone_count:
1132
                    self.expand_mmap(zone_sizes + (
1133
                        [0] * (new_zone_count - len(self._zones))))
1134
            elif element.has_apply_callback():
1135
                element.run_apply_callback()
1136

    
1137
    def get_sub_devices(self):
1138
        zones = []
1139
        for i, _ in enumerate(self._zones):
1140
            zone = getattr(self._memobj, 'zone%i' % i)
1141

    
1142
            class _Zone(KenwoodTKx180RadioZone):
1143
                VENDOR = self.VENDOR
1144
                MODEL = self.MODEL
1145
                VALID_BANDS = self.VALID_BANDS
1146
                VARIANT = 'Zone %s' % (
1147
                    str(zone.zoneinfo.name).rstrip('\x00').rstrip())
1148
                _model = self._model
1149

    
1150
            zones.append(_Zone(self, i))
1151
        return zones
1152

    
1153

    
1154
class KenwoodTKx180RadioZone(KenwoodTKx180Radio):
1155
    _zone = None
1156

    
1157
    def __init__(self, parent, zone=0):
1158
        if isinstance(parent, KenwoodTKx180Radio):
1159
            self._parent = parent
1160
        else:
1161
            LOG.warning('Parent was not actually our parent, expect failure')
1162
        self._zone = zone
1163

    
1164
    @property
1165
    def _zones(self):
1166
        return self._parent._zones
1167

    
1168
    @property
1169
    def _memobj(self):
1170
        return self._parent._memobj
1171

    
1172
    def load_mmap(self, filename):
1173
        self._parent.load_mmap(filename)
1174

    
1175
    def get_features(self):
1176
        rf = KenwoodTKx180Radio.get_features(self)
1177
        rf.has_sub_devices = False
1178
        rf.memory_bounds = (1, 250)
1179
        return rf
1180

    
1181
    def get_sub_devices(self):
1182
        return []
1183

    
1184

    
1185
if has_future:
1186
    @directory.register
1187
    class KenwoodTK7180Radio(KenwoodTKx180Radio):
1188
        MODEL = 'TK-7180'
1189
        VALID_BANDS = [(136000000, 174000000)]
1190
        _model = b'M7180\x04'
1191

    
1192
    @directory.register
1193
    class KenwoodTK8180Radio(KenwoodTKx180Radio):
1194
        MODEL = 'TK-8180'
1195
        VALID_BANDS = [(400000000, 520000000)]
1196
        _model = b'M8180\x06'
1197

    
1198
    @directory.register
1199
    class KenwoodTK2180Radio(KenwoodTKx180Radio):
1200
        MODEL = 'TK-2180'
1201
        VALID_BANDS = [(136000000, 174000000)]
1202
        _model = b'P2180\x04'
1203

    
1204
    # K1,K3 are technically 450-470 (K3 == keypad)
1205
    @directory.register
1206
    class KenwoodTK3180K1Radio(KenwoodTKx180Radio):
1207
        MODEL = 'TK-3180K'
1208
        VALID_BANDS = [(400000000, 520000000)]
1209
        _model = b'P3180\x06'
1210

    
1211
    # K2,K4 are technically 400-470 (K4 == keypad)
1212
    @directory.register
1213
    class KenwoodTK3180K2Radio(KenwoodTKx180Radio):
1214
        MODEL = 'TK-3180K2'
1215
        VALID_BANDS = [(400000000, 520000000)]
1216
        _model = b'P3180\x07'
1217

    
1218
    @directory.register
1219
    class KenwoodTK8180E(KenwoodTKx180Radio):
1220
        MODEL = 'TK-8180E'
1221
        VALID_BANDS = [(400000000, 520000000)]
1222
        _model = b'P8189\''
1223

    
1224
    @directory.register
1225
    class KenwoodTK7180ERadio(KenwoodTKx180Radio):
1226
        MODEL = 'TK-7180E'
1227
        VALID_BANDS = [(136000000, 174000000)]
1228
        _model = b'7189$'
(8-8/13)