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):
|
1014
|
val = int(setting.value)
|
1015
|
if val == 0:
|
1016
|
val = 0xFFFF
|
1017
|
setattr(_zone, key, val)
|
1018
|
|
1019
|
def collapse(val):
|
1020
|
val = int(val)
|
1021
|
if val == 0xFFFF:
|
1022
|
val = 0
|
1023
|
return val
|
1024
|
|
1025
|
timer = RadioSetting(
|
1026
|
'timeout', 'Time-out Timer',
|
1027
|
RadioSettingValueInteger(15, 1200, collapse(_zone.timeout)))
|
1028
|
timer.set_apply_callback(apply_timer, 'timeout')
|
1029
|
zone.append(timer)
|
1030
|
|
1031
|
timer = RadioSetting(
|
1032
|
'tot_alert', 'TOT Pre-Alert',
|
1033
|
RadioSettingValueInteger(0, 10, collapse(_zone.tot_alert)))
|
1034
|
timer.set_apply_callback(apply_timer, 'tot_alert')
|
1035
|
zone.append(timer)
|
1036
|
|
1037
|
timer = RadioSetting(
|
1038
|
'tot_rekey', 'TOT Re-Key Time',
|
1039
|
RadioSettingValueInteger(0, 60, collapse(_zone.tot_rekey)))
|
1040
|
timer.set_apply_callback(apply_timer, 'tot_rekey')
|
1041
|
zone.append(timer)
|
1042
|
|
1043
|
timer = RadioSetting(
|
1044
|
'tot_reset', 'TOT Reset Time',
|
1045
|
RadioSettingValueInteger(0, 15, collapse(_zone.tot_reset)))
|
1046
|
timer.set_apply_callback(apply_timer, 'tot_reset')
|
1047
|
zone.append(timer)
|
1048
|
|
1049
|
zone.append(self._inverted_flag_setting(
|
1050
|
'bcl_override', 'BCL Override',
|
1051
|
_zone))
|
1052
|
|
1053
|
zones.append(zone)
|
1054
|
|
1055
|
return zones
|
1056
|
|
1057
|
def _get_ost(self, parent):
|
1058
|
tones = chirp_common.TONES[:]
|
1059
|
|
1060
|
def apply_tone(setting, index, which):
|
1061
|
if str(setting.value) == 'Off':
|
1062
|
val = 0xFFFF
|
1063
|
else:
|
1064
|
val = int(float(str(setting.value)) * 10)
|
1065
|
setattr(self._memobj.ost_tones[index], '%stone' % which, val)
|
1066
|
|
1067
|
def _tones():
|
1068
|
return ['Off'] + [str(x) for x in tones]
|
1069
|
|
1070
|
for i in range(0, 40):
|
1071
|
_ost = self._memobj.ost_tones[i]
|
1072
|
ost = RadioSettingGroup('ost%i' % i,
|
1073
|
'OST %i' % (i + 1))
|
1074
|
|
1075
|
cur = str(_ost.name).rstrip('\x00')
|
1076
|
name = RadioSetting('name%i' % i, 'Name',
|
1077
|
RadioSettingValueString(0, 12, cur))
|
1078
|
ost.append(name)
|
1079
|
|
1080
|
if _ost.rxtone == 0xFFFF:
|
1081
|
cur = 'Off'
|
1082
|
else:
|
1083
|
cur = round(int(_ost.rxtone) / 10.0, 1)
|
1084
|
if cur not in tones:
|
1085
|
LOG.debug('Non-standard OST rx tone %i %s' % (i, cur))
|
1086
|
tones.append(cur)
|
1087
|
tones.sort()
|
1088
|
rx = RadioSetting('rxtone%i' % i, 'RX Tone',
|
1089
|
RadioSettingValueList(_tones(),
|
1090
|
str(cur)))
|
1091
|
rx.set_apply_callback(apply_tone, i, 'rx')
|
1092
|
ost.append(rx)
|
1093
|
|
1094
|
if _ost.txtone == 0xFFFF:
|
1095
|
cur = 'Off'
|
1096
|
else:
|
1097
|
cur = round(int(_ost.txtone) / 10.0, 1)
|
1098
|
if cur not in tones:
|
1099
|
LOG.debug('Non-standard OST tx tone %i %s' % (i, cur))
|
1100
|
tones.append(cur)
|
1101
|
tones.sort()
|
1102
|
tx = RadioSetting('txtone%i' % i, 'TX Tone',
|
1103
|
RadioSettingValueList(_tones(),
|
1104
|
str(cur)))
|
1105
|
tx.set_apply_callback(apply_tone, i, 'tx')
|
1106
|
ost.append(tx)
|
1107
|
|
1108
|
parent.append(ost)
|
1109
|
|
1110
|
def get_settings(self):
|
1111
|
settings = self._memobj.settings
|
1112
|
|
1113
|
zones = self._get_zones()
|
1114
|
common1 = self._get_common1()
|
1115
|
common2 = self._get_common2()
|
1116
|
conv = self._get_conventional()
|
1117
|
top = RadioSettings(zones, common1, common2, conv)
|
1118
|
return top
|
1119
|
|
1120
|
def set_settings(self, settings):
|
1121
|
for element in settings:
|
1122
|
if not isinstance(element, RadioSetting):
|
1123
|
self.set_settings(element)
|
1124
|
continue
|
1125
|
elif element.get_name() == '_zonecount':
|
1126
|
new_zone_count = int(element.value)
|
1127
|
zone_sizes = [x[1] for x in self._zones[:new_zone_count]]
|
1128
|
if len(self._zones) > new_zone_count:
|
1129
|
self.expand_mmap(zone_sizes[:new_zone_count])
|
1130
|
elif len(self._zones) < new_zone_count:
|
1131
|
self.expand_mmap(zone_sizes + (
|
1132
|
[0] * (new_zone_count - len(self._zones))))
|
1133
|
elif element.has_apply_callback():
|
1134
|
element.run_apply_callback()
|
1135
|
|
1136
|
def get_sub_devices(self):
|
1137
|
zones = []
|
1138
|
for i, _ in enumerate(self._zones):
|
1139
|
zone = getattr(self._memobj, 'zone%i' % i)
|
1140
|
|
1141
|
class _Zone(KenwoodTKx180RadioZone):
|
1142
|
VENDOR = self.VENDOR
|
1143
|
MODEL = self.MODEL
|
1144
|
VALID_BANDS = self.VALID_BANDS
|
1145
|
VARIANT = 'Zone %s' % (
|
1146
|
str(zone.zoneinfo.name).rstrip('\x00').rstrip())
|
1147
|
_model = self._model
|
1148
|
|
1149
|
zones.append(_Zone(self, i))
|
1150
|
return zones
|
1151
|
|
1152
|
|
1153
|
class KenwoodTKx180RadioZone(KenwoodTKx180Radio):
|
1154
|
_zone = None
|
1155
|
|
1156
|
def __init__(self, parent, zone=0):
|
1157
|
if isinstance(parent, KenwoodTKx180Radio):
|
1158
|
self._parent = parent
|
1159
|
else:
|
1160
|
LOG.warning('Parent was not actually our parent, expect failure')
|
1161
|
self._zone = zone
|
1162
|
|
1163
|
@property
|
1164
|
def _zones(self):
|
1165
|
return self._parent._zones
|
1166
|
|
1167
|
@property
|
1168
|
def _memobj(self):
|
1169
|
return self._parent._memobj
|
1170
|
|
1171
|
def load_mmap(self, filename):
|
1172
|
self._parent.load_mmap(filename)
|
1173
|
|
1174
|
def get_features(self):
|
1175
|
rf = KenwoodTKx180Radio.get_features(self)
|
1176
|
rf.has_sub_devices = False
|
1177
|
rf.memory_bounds = (1, 250)
|
1178
|
return rf
|
1179
|
|
1180
|
def get_sub_devices(self):
|
1181
|
return []
|
1182
|
|
1183
|
|
1184
|
if has_future:
|
1185
|
@directory.register
|
1186
|
class KenwoodTK7180Radio(KenwoodTKx180Radio):
|
1187
|
MODEL = 'TK-7180'
|
1188
|
VALID_BANDS = [(136000000, 174000000)]
|
1189
|
_model = b'M7180\x04'
|
1190
|
|
1191
|
@directory.register
|
1192
|
class KenwoodTK8180Radio(KenwoodTKx180Radio):
|
1193
|
MODEL = 'TK-8180'
|
1194
|
VALID_BANDS = [(400000000, 520000000)]
|
1195
|
_model = b'M8180\x06'
|
1196
|
|
1197
|
@directory.register
|
1198
|
class KenwoodTK2180Radio(KenwoodTKx180Radio):
|
1199
|
MODEL = 'TK-2180'
|
1200
|
VALID_BANDS = [(136000000, 174000000)]
|
1201
|
_model = b'P2180\x04'
|
1202
|
|
1203
|
# K1,K3 are technically 450-470 (K3 == keypad)
|
1204
|
@directory.register
|
1205
|
class KenwoodTK3180K1Radio(KenwoodTKx180Radio):
|
1206
|
MODEL = 'TK-3180K'
|
1207
|
VALID_BANDS = [(400000000, 520000000)]
|
1208
|
_model = b'P3180\x06'
|
1209
|
|
1210
|
# K2,K4 are technically 400-470 (K4 == keypad)
|
1211
|
@directory.register
|
1212
|
class KenwoodTK3180K2Radio(KenwoodTKx180Radio):
|
1213
|
MODEL = 'TK-3180K2'
|
1214
|
VALID_BANDS = [(400000000, 520000000)]
|
1215
|
_model = b'P3180\x07'
|
1216
|
|
1217
|
@directory.register
|
1218
|
class KenwoodTK8180ERadio(KenwoodTKx180Radio):
|
1219
|
MODEL = 'TK-8180E'
|
1220
|
VALID_BANDS = [(400000000, 520000000)]
|
1221
|
_model = b'M8189\''
|