1
|
# Copyright 2016 Pavel Milanes, CO7WT, <pavelmc@gmail.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 logging
|
17
|
import struct
|
18
|
import time
|
19
|
import sys
|
20
|
|
21
|
from chirp import chirp_common, directory, memmap, errors, util, bitwise
|
22
|
from textwrap import dedent
|
23
|
from chirp.settings import RadioSettingGroup, RadioSetting, \
|
24
|
RadioSettingValueBoolean, RadioSettingValueList, \
|
25
|
RadioSettingValueString, RadioSettingValueInteger, \
|
26
|
RadioSettings
|
27
|
|
28
|
LOG = logging.getLogger(__name__)
|
29
|
|
30
|
##### IMPORTANT DATA ##########################################
|
31
|
# This radios have a span of
|
32
|
# 0x00000 - 0x08000 => Radio Memory / Settings data
|
33
|
# 0x08000 - 0x10000 => FIRMWARE... hum...
|
34
|
###############################################################
|
35
|
|
36
|
MEM_FORMAT = """
|
37
|
#seekto 0x0000;
|
38
|
struct {
|
39
|
u8 unknown0[14]; // x00-x0d unknown
|
40
|
u8 banks; // x0e how many banks are programmed
|
41
|
u8 channels; // x0f how many total channels are programmed
|
42
|
// --
|
43
|
ul16 tot; // x10 TOT value: range(15, 600, 15); x04b0 = off
|
44
|
u8 tot_rekey; // x12 TOT Re-key value range(0, 60); off= 0
|
45
|
u8 unknown1; // x13 unknown
|
46
|
u8 tot_reset; // x14 TOT Re-key value range(0, 60); off= 0
|
47
|
u8 unknown2; // x15 unknows
|
48
|
u8 tot_alert; // x16 TOT pre alert: range(0,10); 0 = off
|
49
|
u8 unknown3[7]; // x17-x1d unknown
|
50
|
u8 sql_level; // x1e SQ reference level
|
51
|
u8 battery_save; // Only for portable: FF = off, x32 = on
|
52
|
// --
|
53
|
u8 unknown4[10]; // x20
|
54
|
u8 unknown5:3, // x2d
|
55
|
c2t:1, // 1 bit clear to transpond: 1-off
|
56
|
// This is relative to DTMF / 2-Tone settings
|
57
|
unknown6:4;
|
58
|
u8 unknown7[5]; // x2b-x2f
|
59
|
// --
|
60
|
u8 unknown8[16]; // x30 ?
|
61
|
u8 unknown9[16]; // x40 ?
|
62
|
u8 unknown10[16]; // x50 ?
|
63
|
u8 unknown11[16]; // x60 ?
|
64
|
// --
|
65
|
u8 add[16]; // x70-x7f 128 bits corresponding add/skip values
|
66
|
// --
|
67
|
u8 unknown12:4, // x80
|
68
|
off_hook_decode:1, // 1 bit off hook decode enabled: 1-off
|
69
|
off_hook_horn_alert:1, // 1 bit off hook horn alert: 1-off
|
70
|
unknown13:2;
|
71
|
u8 unknown14; // x81
|
72
|
u8 unknown15:3, // x82
|
73
|
self_prog:1, // 1 bit Self programming enabled: 1-on
|
74
|
clone:1, // 1 bit clone enabled: 1-on
|
75
|
firmware_prog:1, // 1 bit firmware programming enabled: 1-on
|
76
|
unknown16:1,
|
77
|
panel_test:1; // 1 bit panel test enabled
|
78
|
u8 unknown17; // x83
|
79
|
u8 unknown18:5, // x84
|
80
|
warn_tone:1, // 1 bit warning tone, enabled: 1-on
|
81
|
control_tone:1, // 1 bit control tone (key tone), enabled: 1-on
|
82
|
poweron_tone:1; // 1 bit power on tone, enabled: 1-on
|
83
|
u8 unknown19[5]; // x85-x89
|
84
|
u8 min_vol; // minimum volume posible: range(0,32); 0 = off
|
85
|
u8 tone_vol; // minimum tone volume posible:
|
86
|
// xff = continous, range(0, 31)
|
87
|
u8 unknown20[4]; // x8c-x8f
|
88
|
// --
|
89
|
u8 unknown21[4]; // x90-x93
|
90
|
char poweronmesg[8]; // x94-x9b power on mesg 8 bytes, off is "\FF" * 8
|
91
|
u8 unknown22[4]; // x9c-x9f
|
92
|
// --
|
93
|
u8 unknown23[7]; // xa0-xa6
|
94
|
char ident[8]; // xa7-xae radio identification string
|
95
|
u8 unknown24; // xaf
|
96
|
// --
|
97
|
u8 unknown26[11]; // xaf-xba
|
98
|
char lastsoftversion[5]; // software version employed to program the radio
|
99
|
} settings;
|
100
|
|
101
|
#seekto 0xd0;
|
102
|
struct {
|
103
|
u8 unknown[4];
|
104
|
char radio[6];
|
105
|
char data[6];
|
106
|
} passwords;
|
107
|
|
108
|
#seekto 0x0110;
|
109
|
struct {
|
110
|
u8 kA; // Portable > Closed circle
|
111
|
u8 kDA; // Protable > Triangle to Left
|
112
|
u8 kGROUP_DOWN; // Protable > Triangle to Right
|
113
|
u8 kGROUP_UP; // Protable > Side 1
|
114
|
u8 kSCN; // Portable > Open Circle
|
115
|
u8 kMON; // Protable > Side 2
|
116
|
u8 kFOOT;
|
117
|
u8 kCH_UP;
|
118
|
u8 kCH_DOWN;
|
119
|
u8 kVOL_UP;
|
120
|
u8 kVOL_DOWN;
|
121
|
u8 unknown30[5];
|
122
|
// --
|
123
|
u8 unknown31[4];
|
124
|
u8 kP_KNOB; // Just portable: channel knob
|
125
|
u8 unknown32[11];
|
126
|
} keys;
|
127
|
|
128
|
#seekto 0x0140;
|
129
|
struct {
|
130
|
lbcd tf01_rx[4];
|
131
|
lbcd tf01_tx[4];
|
132
|
u8 tf01_u_rx;
|
133
|
u8 tf01_u_tx;
|
134
|
lbcd tf02_rx[4];
|
135
|
lbcd tf02_tx[4];
|
136
|
u8 tf02_u_rx;
|
137
|
u8 tf02_u_tx;
|
138
|
lbcd tf03_rx[4];
|
139
|
lbcd tf03_tx[4];
|
140
|
u8 tf03_u_rx;
|
141
|
u8 tf03_u_tx;
|
142
|
lbcd tf04_rx[4];
|
143
|
lbcd tf04_tx[4];
|
144
|
u8 tf04_u_rx;
|
145
|
u8 tf04_u_tx;
|
146
|
lbcd tf05_rx[4];
|
147
|
lbcd tf05_tx[4];
|
148
|
u8 tf05_u_rx;
|
149
|
u8 tf05_u_tx;
|
150
|
lbcd tf06_rx[4];
|
151
|
lbcd tf06_tx[4];
|
152
|
u8 tf06_u_rx;
|
153
|
u8 tf06_u_tx;
|
154
|
lbcd tf07_rx[4];
|
155
|
lbcd tf07_tx[4];
|
156
|
u8 tf07_u_rx;
|
157
|
u8 tf07_u_tx;
|
158
|
lbcd tf08_rx[4];
|
159
|
lbcd tf08_tx[4];
|
160
|
u8 tf08_u_rx;
|
161
|
u8 tf08_u_tx;
|
162
|
lbcd tf09_rx[4];
|
163
|
lbcd tf09_tx[4];
|
164
|
u8 tf09_u_rx;
|
165
|
u8 tf09_u_tx;
|
166
|
lbcd tf10_rx[4];
|
167
|
lbcd tf10_tx[4];
|
168
|
u8 tf10_u_rx;
|
169
|
u8 tf10_u_tx;
|
170
|
lbcd tf11_rx[4];
|
171
|
lbcd tf11_tx[4];
|
172
|
u8 tf11_u_rx;
|
173
|
u8 tf11_u_tx;
|
174
|
lbcd tf12_rx[4];
|
175
|
lbcd tf12_tx[4];
|
176
|
u8 tf12_u_rx;
|
177
|
u8 tf12_u_tx;
|
178
|
lbcd tf13_rx[4];
|
179
|
lbcd tf13_tx[4];
|
180
|
u8 tf13_u_rx;
|
181
|
u8 tf13_u_tx;
|
182
|
lbcd tf14_rx[4];
|
183
|
lbcd tf14_tx[4];
|
184
|
u8 tf14_u_rx;
|
185
|
u8 tf14_u_tx;
|
186
|
lbcd tf15_rx[4];
|
187
|
lbcd tf15_tx[4];
|
188
|
u8 tf15_u_rx;
|
189
|
u8 tf15_u_tx;
|
190
|
lbcd tf16_rx[4];
|
191
|
lbcd tf16_tx[4];
|
192
|
u8 tf16_u_rx;
|
193
|
u8 tf16_u_tx;
|
194
|
} test_freq;
|
195
|
|
196
|
#seekto 0x200;
|
197
|
struct {
|
198
|
char line1[32];
|
199
|
char line2[32];
|
200
|
} message;
|
201
|
|
202
|
#seekto 0x2000;
|
203
|
struct {
|
204
|
u8 bnumb; // mem number
|
205
|
u8 bank; // to which bank it belongs
|
206
|
char name[8]; // name 8 chars
|
207
|
u8 unknown20[2]; // unknown yet
|
208
|
lbcd rxfreq[4]; // rx freq
|
209
|
// --
|
210
|
lbcd txfreq[4]; // tx freq
|
211
|
u8 rx_unkw; // unknown yet
|
212
|
u8 tx_unkw; // unknown yet
|
213
|
ul16 rx_tone; // rx tone
|
214
|
ul16 tx_tone; // tx tone
|
215
|
u8 unknown23[5]; // unknown yet
|
216
|
u8 signaling; // xFF = off, x30 DTMF, x31 2-Tone
|
217
|
// See the zone on x7000
|
218
|
// --
|
219
|
u8 ptt_id:2, // ??? BOT = 0, EOT = 1, Both = 2, NONE = 3
|
220
|
beat_shift:1, // 1 = off
|
221
|
unknown26:2 // ???
|
222
|
power:1, // power: 0 low / 1 high
|
223
|
compander:1, // 1 = off
|
224
|
wide:1; // wide 1 / 0 narrow
|
225
|
u8 unknown27:6, // ???
|
226
|
busy_lock:1, // 1 = off
|
227
|
unknown28:1; // ???
|
228
|
u8 unknown29[14]; // unknown yet
|
229
|
} memory[128];
|
230
|
|
231
|
#seekto 0x5900;
|
232
|
struct {
|
233
|
char model[8];
|
234
|
u8 unknown50[4];
|
235
|
char type[2];
|
236
|
u8 unknown51[2];
|
237
|
// --
|
238
|
char serial[8];
|
239
|
u8 unknown52[8];
|
240
|
} id;
|
241
|
|
242
|
#seekto 0x6000;
|
243
|
struct {
|
244
|
u8 code[8];
|
245
|
u8 unknown60[7];
|
246
|
u8 count;
|
247
|
} bot[128];
|
248
|
|
249
|
#seekto 0x6800;
|
250
|
struct {
|
251
|
u8 code[8];
|
252
|
u8 unknown61[7];
|
253
|
u8 count;
|
254
|
} eot[128];
|
255
|
|
256
|
#seekto 0x7000;
|
257
|
struct {
|
258
|
lbcd dt2_id[5]; // DTMF lbcd ID (000-9999999999)
|
259
|
// 2-Tone = "11 f1 ff ff ff" ???
|
260
|
// None = "00 f0 ff ff ff"
|
261
|
} dtmf;
|
262
|
"""
|
263
|
|
264
|
MEM_SIZE = 0x8000 # 32,768 bytes
|
265
|
BLOCK_SIZE = 256
|
266
|
BLOCKS = MEM_SIZE / BLOCK_SIZE
|
267
|
MEM_BLOCKS = range(0, BLOCKS)
|
268
|
|
269
|
# define and empty block of data, as it will be used a lot in this code
|
270
|
EMPTY_BLOCK = "\xFF" * 256
|
271
|
|
272
|
RO_BLOCKS = range(0x10, 0x1F) + range(0x59, 0x5f)
|
273
|
ACK_CMD = "\x06"
|
274
|
|
275
|
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1),
|
276
|
chirp_common.PowerLevel("High", watts=5)]
|
277
|
|
278
|
MODES = ["NFM", "FM"] # 12.5 / 25 Khz
|
279
|
VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "_-*()/\-+=)"
|
280
|
SKIP_VALUES = ["", "S"]
|
281
|
|
282
|
TONES = chirp_common.TONES
|
283
|
# TONES.remove(254.1)
|
284
|
DTCS_CODES = chirp_common.DTCS_CODES
|
285
|
|
286
|
TOT = ["off"] + ["%s" % x for x in range(15, 615, 15)]
|
287
|
TOT_PRE = ["off"] + ["%s" % x for x in range(1, 11)]
|
288
|
TOT_REKEY = ["off"] + ["%s" % x for x in range(1, 61)]
|
289
|
TOT_RESET = ["off"] + ["%s" % x for x in range(1, 16)]
|
290
|
VOL = ["off"] + ["%s" % x for x in range(1, 32)]
|
291
|
TVOL = ["%s" % x for x in range(0, 33)]
|
292
|
TVOL[32] = "Continous"
|
293
|
SQL = ["off"] + ["%s" % x for x in range(1, 10)]
|
294
|
|
295
|
## BOT = 0, EOT = 1, Both = 2, NONE = 3
|
296
|
#PTTID = ["BOT", "EOT", "Both", "none"]
|
297
|
|
298
|
# For debugging purposes
|
299
|
debug = False
|
300
|
|
301
|
KEYS = {
|
302
|
0x33: "Display character",
|
303
|
0x35: "Home Channel", # Posible portable only, chek it
|
304
|
0x37: "CH down",
|
305
|
0x38: "CH up",
|
306
|
0x39: "Key lock",
|
307
|
0x3a: "Lamp", # Portable only
|
308
|
0x3b: "Public address",
|
309
|
0x3c: "Reverse", # Just in updated firmwares (768G)
|
310
|
0x3d: "Horn alert",
|
311
|
0x3e: "Selectable QT", # Just in updated firmwares (768G)
|
312
|
0x3f: "2-tone encode",
|
313
|
0x40: "Monitor A: open mommentary",
|
314
|
0x41: "Monitor B: Open Toggle",
|
315
|
0x42: "Monitor C: Carrier mommentary",
|
316
|
0x43: "Monitor D: Carrier toogle",
|
317
|
0x44: "Operator selectable tone",
|
318
|
0x45: "Redial",
|
319
|
0x46: "RF Power Low", # portable only ?
|
320
|
0x47: "Scan",
|
321
|
0x48: "Scan del/add",
|
322
|
0x4a: "GROUP down",
|
323
|
0x4b: "GROUP up",
|
324
|
#0x4e: "Tone off (Experimental)", # undocumented !!!!
|
325
|
0x4f: "None",
|
326
|
0x50: "VOL down",
|
327
|
0x51: "VOL up",
|
328
|
0x52: "Talk around",
|
329
|
0x5d: "AUX",
|
330
|
0xa1: "Channel Up/Down" # Knob for portables only
|
331
|
}
|
332
|
|
333
|
|
334
|
def _raw_recv(radio, amount):
|
335
|
"""Raw read from the radio device"""
|
336
|
data = ""
|
337
|
try:
|
338
|
data = radio.pipe.read(amount)
|
339
|
except:
|
340
|
raise errors.RadioError("Error reading data from radio")
|
341
|
|
342
|
# DEBUG
|
343
|
if debug is True:
|
344
|
LOG.debug("<== (%d) bytes:\n\n%s" % (len(data), util.hexprint(data)))
|
345
|
|
346
|
return data
|
347
|
|
348
|
|
349
|
def _raw_send(radio, data):
|
350
|
"""Raw send to the radio device"""
|
351
|
try:
|
352
|
radio.pipe.write(data)
|
353
|
except:
|
354
|
raise errors.RadioError("Error sending data to radio")
|
355
|
|
356
|
# DEBUG
|
357
|
if debug is True:
|
358
|
LOG.debug("==> (%d) bytes:\n\n%s" % (len(data), util.hexprint(data)))
|
359
|
|
360
|
|
361
|
def _close_radio(radio):
|
362
|
"""Get the radio out of program mode"""
|
363
|
_raw_send(radio, "\x45")
|
364
|
|
365
|
|
366
|
def _checksum(data):
|
367
|
"""the radio block checksum algorithm"""
|
368
|
cs = 0
|
369
|
for byte in data:
|
370
|
cs += ord(byte)
|
371
|
return cs % 256
|
372
|
|
373
|
|
374
|
def _send(radio, frame):
|
375
|
"""Generic send data to the radio"""
|
376
|
_raw_send(radio, frame)
|
377
|
|
378
|
|
379
|
def _make_frame(cmd, addr):
|
380
|
"""Pack the info in the format it likes"""
|
381
|
return struct.pack(">BH", ord(cmd), addr)
|
382
|
|
383
|
|
384
|
def _handshake(radio, msg=""):
|
385
|
"""Make a full handshake"""
|
386
|
# send ACK
|
387
|
_raw_send(radio, ACK_CMD)
|
388
|
# receive ACK
|
389
|
ack = _raw_recv(radio, 1)
|
390
|
# check ACK
|
391
|
if ack != ACK_CMD:
|
392
|
_close_radio(radio)
|
393
|
mesg = "Handshake failed " + msg
|
394
|
# DEBUG
|
395
|
LOG.debug(mesg)
|
396
|
raise Exception(mesg)
|
397
|
|
398
|
|
399
|
def _check_write_ack(r, ack, addr):
|
400
|
"""Process the ack from the write process
|
401
|
this is half handshake needed in tx data block"""
|
402
|
# all ok
|
403
|
if ack == ACK_CMD:
|
404
|
return True
|
405
|
|
406
|
# Explicit BAD checksum
|
407
|
if ack == "\x15":
|
408
|
_close_radio(r)
|
409
|
raise errors.RadioError(
|
410
|
"Bad checksum in block %02x write" % addr)
|
411
|
|
412
|
# everything else
|
413
|
_close_radio(r)
|
414
|
raise errors.RadioError(
|
415
|
"Problem with the ack to block %02x write, ack %03i" %
|
416
|
(addr, int(ack)))
|
417
|
|
418
|
|
419
|
def _recv(radio):
|
420
|
"""Receive data from the radio, 258 bytes split in (cmd, data, checksum)
|
421
|
checking the checksum to be correct, and returning just
|
422
|
256 bytes of data or false if short empty block"""
|
423
|
rxdata = _raw_recv(radio, BLOCK_SIZE + 2)
|
424
|
# when the RX block has two bytes and the first is \x5A
|
425
|
# then the block is all \xFF
|
426
|
if len(rxdata) == 2 and rxdata[0] == "\x5A":
|
427
|
# fast work in linux has to make the handshake, slow windows don't
|
428
|
if not sys.platform in ["win32", "cygwin"]:
|
429
|
_handshake(radio, "short block")
|
430
|
return False
|
431
|
elif len(rxdata) != 258:
|
432
|
# not the amount of data we want
|
433
|
msg = "The radio send %d bytes, we need 258" % len(rxdata)
|
434
|
# DEBUG
|
435
|
LOG.error(msg)
|
436
|
raise errors.RadioError(msg)
|
437
|
else:
|
438
|
rcs = ord(rxdata[-1])
|
439
|
data = rxdata[1:-1]
|
440
|
ccs = _checksum(data)
|
441
|
|
442
|
if rcs != ccs:
|
443
|
_close_radio(radio)
|
444
|
raise errors.RadioError(
|
445
|
"Block Checksum Error! real %02x, calculated %02x" %
|
446
|
(rcs, ccs))
|
447
|
|
448
|
_handshake(radio, "after checksum")
|
449
|
return data
|
450
|
|
451
|
|
452
|
def _open_radio(radio, status):
|
453
|
"""Open the radio into program mode and check if it's the correct model"""
|
454
|
# linux min is 0.13, win min is 0.25; set to bigger to be safe
|
455
|
radio.pipe.timeout = 0.4
|
456
|
radio.pipe.parity = "E"
|
457
|
|
458
|
# DEBUG
|
459
|
LOG.debug("Entering program mode.")
|
460
|
# max tries
|
461
|
tries = 10
|
462
|
|
463
|
# UI
|
464
|
status.cur = 0
|
465
|
status.max = tries
|
466
|
status.msg = "Entering program mode..."
|
467
|
|
468
|
# try a few times to get the radio into program mode
|
469
|
exito = False
|
470
|
for i in range(0, tries):
|
471
|
_raw_send(radio, "PROGRAM")
|
472
|
ack = _raw_recv(radio, 1)
|
473
|
|
474
|
if ack != ACK_CMD:
|
475
|
# DEBUG
|
476
|
LOG.debug("Try %s failed, traying again..." % i)
|
477
|
time.sleep(0.25)
|
478
|
else:
|
479
|
exito = True
|
480
|
break
|
481
|
|
482
|
status.cur += 1
|
483
|
radio.status_fn(status)
|
484
|
|
485
|
|
486
|
if exito is False:
|
487
|
_close_radio(radio)
|
488
|
LOG.debug("Radio did not accepted PROGRAM command in %s atempts" % tries)
|
489
|
raise errors.RadioError("The radio doesn't accept program mode")
|
490
|
|
491
|
# DEBUG
|
492
|
LOG.debug("Received ACK to the PROGRAM command, send ID query.")
|
493
|
|
494
|
_raw_send(radio, "\x02")
|
495
|
rid = _raw_recv(radio, 8)
|
496
|
|
497
|
if not (radio.TYPE in rid):
|
498
|
# bad response, properly close the radio before exception
|
499
|
_close_radio(radio)
|
500
|
|
501
|
# DEBUG
|
502
|
LOG.debug("Incorrect model ID:")
|
503
|
LOG.debug(util.hexprint(rid))
|
504
|
|
505
|
raise errors.RadioError(
|
506
|
"Incorrect model ID, got %s, it not contains %s" %
|
507
|
(rid.strip("\xff"), radio.TYPE))
|
508
|
|
509
|
# DEBUG
|
510
|
LOG.debug("Full ident string is:")
|
511
|
LOG.debug(util.hexprint(rid))
|
512
|
_handshake(radio)
|
513
|
|
514
|
status.msg = "Radio ident success!"
|
515
|
radio.status_fn(status)
|
516
|
# a pause
|
517
|
time.sleep(1)
|
518
|
|
519
|
|
520
|
def do_download(radio):
|
521
|
""" The download function """
|
522
|
# UI progress
|
523
|
status = chirp_common.Status()
|
524
|
data = ""
|
525
|
count = 0
|
526
|
|
527
|
# open the radio
|
528
|
_open_radio(radio, status)
|
529
|
|
530
|
# reset UI data
|
531
|
status.cur = 0
|
532
|
status.max = MEM_SIZE / 256
|
533
|
status.msg = "Cloning from radio..."
|
534
|
radio.status_fn(status)
|
535
|
|
536
|
# set the timeout and if windows keep it bigger
|
537
|
if sys.platform in ["win32", "cygwin"]:
|
538
|
# bigger timeout
|
539
|
radio.pipe.timeout = 0.55
|
540
|
else:
|
541
|
# Linux can keep up, MAC?
|
542
|
radio.pipe.timeout = 0.05
|
543
|
|
544
|
# DEBUG
|
545
|
LOG.debug("Starting the download from radio")
|
546
|
|
547
|
for addr in MEM_BLOCKS:
|
548
|
# send request, but before flush the rx buffer
|
549
|
radio.pipe.flush()
|
550
|
_send(radio, _make_frame("R", addr))
|
551
|
|
552
|
# now we get the data
|
553
|
d = _recv(radio)
|
554
|
# if empty block, it return false
|
555
|
# aka we asume a empty 256 xFF block
|
556
|
if d is False:
|
557
|
d = EMPTY_BLOCK
|
558
|
|
559
|
data += d
|
560
|
|
561
|
# UI Update
|
562
|
status.cur = count
|
563
|
radio.status_fn(status)
|
564
|
|
565
|
count += 1
|
566
|
|
567
|
_close_radio(radio)
|
568
|
return memmap.MemoryMap(data)
|
569
|
|
570
|
|
571
|
def do_upload(radio):
|
572
|
""" The upload function """
|
573
|
# UI progress
|
574
|
status = chirp_common.Status()
|
575
|
data = ""
|
576
|
count = 0
|
577
|
|
578
|
# open the radio
|
579
|
_open_radio(radio, status)
|
580
|
|
581
|
# update UI
|
582
|
status.cur = 0
|
583
|
status.max = MEM_SIZE / 256
|
584
|
status.msg = "Cloning to radio..."
|
585
|
radio.status_fn(status)
|
586
|
|
587
|
# the default for the original soft as measured
|
588
|
radio.pipe.timeout = 0.5
|
589
|
|
590
|
# DEBUG
|
591
|
LOG.debug("Starting the upload to the radio")
|
592
|
|
593
|
count = 0
|
594
|
raddr = 0
|
595
|
for addr in MEM_BLOCKS:
|
596
|
# this is the data block to write
|
597
|
data = radio.get_mmap()[raddr:raddr+BLOCK_SIZE]
|
598
|
|
599
|
# The blocks from x59-x5F are NOT programmable
|
600
|
# The blocks from x11-x1F are writed only if not empty
|
601
|
if addr in RO_BLOCKS:
|
602
|
# checking if in the range of optional blocks
|
603
|
if addr >= 0x10 and addr <= 0x1F:
|
604
|
# block is empty ?
|
605
|
if data == EMPTY_BLOCK:
|
606
|
# no write of this block
|
607
|
# but we have to continue updating the counters
|
608
|
count += 1
|
609
|
raddr = count * 256
|
610
|
continue
|
611
|
else:
|
612
|
count += 1
|
613
|
raddr = count * 256
|
614
|
continue
|
615
|
|
616
|
if data == EMPTY_BLOCK:
|
617
|
frame = _make_frame("Z", addr) + "\xFF"
|
618
|
else:
|
619
|
cs = _checksum(data)
|
620
|
frame = _make_frame("W", addr) + data + chr(cs)
|
621
|
|
622
|
_send(radio, frame)
|
623
|
|
624
|
# get the ACK
|
625
|
ack = _raw_recv(radio, 1)
|
626
|
_check_write_ack(radio, ack, addr)
|
627
|
|
628
|
# DEBUG
|
629
|
LOG.debug("Sending block %02x" % addr)
|
630
|
|
631
|
# UI Update
|
632
|
status.cur = count
|
633
|
radio.status_fn(status)
|
634
|
|
635
|
count += 1
|
636
|
raddr = count * 256
|
637
|
|
638
|
_close_radio(radio)
|
639
|
|
640
|
|
641
|
def model_match(cls, data):
|
642
|
"""Match the opened/downloaded image to the correct version"""
|
643
|
rid = data[0xA7:0xAE]
|
644
|
if (rid in cls.VARIANTS):
|
645
|
# correct model
|
646
|
return True
|
647
|
else:
|
648
|
return False
|
649
|
|
650
|
|
651
|
class Kenwood60GBankModel(chirp_common.BankModel):
|
652
|
"""Testing the bank model on kennwood"""
|
653
|
channelAlwaysHasBank = True
|
654
|
|
655
|
def get_num_mappings(self):
|
656
|
return self._radio._num_banks
|
657
|
|
658
|
def get_mappings(self):
|
659
|
banks = []
|
660
|
for i in range(0, self._radio._num_banks):
|
661
|
bindex = i + 1
|
662
|
bank = self._radio._bclass(self, i, "%03i" % bindex)
|
663
|
bank.index = i
|
664
|
banks.append(bank)
|
665
|
return banks
|
666
|
|
667
|
def add_memory_to_mapping(self, memory, bank):
|
668
|
self._radio._set_bank(memory.number, bank.index)
|
669
|
|
670
|
def remove_memory_from_mapping(self, memory, bank):
|
671
|
if self._radio._get_bank(memory.number) != bank.index:
|
672
|
raise Exception("Memory %i not in bank %s. Cannot remove." %
|
673
|
(memory.number, bank))
|
674
|
|
675
|
# We can't "Remove" it for good
|
676
|
# the kenwood paradigm don't allow it
|
677
|
# instead we move it to bank 0
|
678
|
self._radio._set_bank(memory.number, 0)
|
679
|
|
680
|
def get_mapping_memories(self, bank):
|
681
|
memories = []
|
682
|
for i in range(0, self._radio._upper):
|
683
|
if self._radio._get_bank(i) == bank.index:
|
684
|
memories.append(self._radio.get_memory(i))
|
685
|
return memories
|
686
|
|
687
|
def get_memory_mappings(self, memory):
|
688
|
index = self._radio._get_bank(memory.number)
|
689
|
return [self.get_mappings()[index]]
|
690
|
|
691
|
|
692
|
class memBank(chirp_common.Bank):
|
693
|
"""A bank model for kenwood"""
|
694
|
# Integral index of the bank (not to be confused with per-memory
|
695
|
# bank indexes
|
696
|
index = 0
|
697
|
|
698
|
|
699
|
class Kenwood_Serie_60G(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
|
700
|
"""Kenwood Serie 60G Radios base class"""
|
701
|
VENDOR = "Kenwood"
|
702
|
BAUD_RATE = 9600
|
703
|
_memsize = MEM_SIZE
|
704
|
NAME_LENGTH = 8
|
705
|
_range = [136000000, 162000000]
|
706
|
_upper = 128
|
707
|
_chs_progs = 0
|
708
|
_num_banks = 128
|
709
|
_bclass = memBank
|
710
|
_kind = ""
|
711
|
VARIANT = ""
|
712
|
MODEL = ""
|
713
|
|
714
|
@classmethod
|
715
|
def get_prompts(cls):
|
716
|
rp = chirp_common.RadioPrompts()
|
717
|
rp.experimental = \
|
718
|
('This driver is experimental; not all features have been '
|
719
|
'implemented, but it has those features most used by hams.\n'
|
720
|
'\n'
|
721
|
'This radios are able to work slightly outside the OEM '
|
722
|
'frequency limits. After testing, the limit in Chirp has '
|
723
|
'been set 4% outside the OEM limit. This allows you to use '
|
724
|
'some models on the ham bands.\n'
|
725
|
'\n'
|
726
|
'Nevertheless, each radio has its own hardware limits and '
|
727
|
'your mileage may vary.\n'
|
728
|
)
|
729
|
rp.pre_download = _(dedent("""\
|
730
|
Follow this instructions to download your info:
|
731
|
1 - Turn off your radio
|
732
|
2 - Connect your interface cable
|
733
|
3 - Turn on your radio (unblock it if password protected)
|
734
|
4 - Do the download of your radio data
|
735
|
"""))
|
736
|
rp.pre_upload = _(dedent("""\
|
737
|
Follow this instructions to upload your info:
|
738
|
1 - Turn off your radio
|
739
|
2 - Connect your interface cable
|
740
|
3 - Turn on your radio (unblock it if password protected)
|
741
|
4 - Do the upload of your radio data
|
742
|
"""))
|
743
|
return rp
|
744
|
|
745
|
def get_features(self):
|
746
|
"""Return information about this radio's features"""
|
747
|
rf = chirp_common.RadioFeatures()
|
748
|
rf.has_settings = True
|
749
|
rf.has_bank = True
|
750
|
rf.has_tuning_step = False
|
751
|
rf.has_name = True
|
752
|
rf.has_offset = True
|
753
|
rf.has_mode = True
|
754
|
rf.has_dtcs = True
|
755
|
rf.has_rx_dtcs = True
|
756
|
rf.has_dtcs_polarity = True
|
757
|
rf.has_ctone = True
|
758
|
rf.has_cross = True
|
759
|
rf.valid_modes = MODES
|
760
|
rf.valid_duplexes = ["", "-", "+", "off"]
|
761
|
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
|
762
|
rf.valid_cross_modes = [
|
763
|
"Tone->Tone",
|
764
|
"DTCS->",
|
765
|
"->DTCS",
|
766
|
"Tone->DTCS",
|
767
|
"DTCS->Tone",
|
768
|
"->Tone",
|
769
|
"DTCS->DTCS"]
|
770
|
rf.valid_power_levels = POWER_LEVELS
|
771
|
rf.valid_characters = VALID_CHARS
|
772
|
rf.valid_skips = SKIP_VALUES
|
773
|
rf.valid_dtcs_codes = DTCS_CODES
|
774
|
rf.valid_bands = [self._range]
|
775
|
rf.valid_name_length = 8
|
776
|
rf.memory_bounds = (1, self._upper)
|
777
|
rf.valid_tuning_steps = [1., 2.5, 5., 6.25, 12.5]
|
778
|
return rf
|
779
|
|
780
|
def _fill(self, offset, data):
|
781
|
"""Fill an specified area of the memmap with the passed data"""
|
782
|
for addr in range(0, len(data)):
|
783
|
self._mmap[offset + addr] = data[addr]
|
784
|
|
785
|
def _prep_data(self):
|
786
|
"""Prepare the areas in the memmap to do a consistend write
|
787
|
it has to make an update on the x300 area with banks and channel
|
788
|
info; other in the x1000 with banks and channel counts
|
789
|
and a last one in x7000 with flag data"""
|
790
|
rchs = 0
|
791
|
data = dict()
|
792
|
|
793
|
# sorting the data
|
794
|
for ch in range(0, self._upper):
|
795
|
mem = self._memobj.memory[ch]
|
796
|
bnumb = int(mem.bnumb)
|
797
|
bank = int(mem.bank)
|
798
|
if bnumb != 255 and (bank != 255 and bank != 0):
|
799
|
try:
|
800
|
data[bank].append(ch)
|
801
|
except:
|
802
|
data[bank] = list()
|
803
|
data[bank].append(ch)
|
804
|
data[bank].sort()
|
805
|
# counting the real channels
|
806
|
rchs = rchs + 1
|
807
|
|
808
|
# updating the channel/bank count
|
809
|
self._memobj.settings.channels = rchs
|
810
|
self._chs_progs = rchs
|
811
|
self._memobj.settings.banks = len(data)
|
812
|
|
813
|
# building the data for the memmap
|
814
|
fdata = ""
|
815
|
|
816
|
for k, v in data.iteritems():
|
817
|
# posible bad data
|
818
|
if k == 0:
|
819
|
k = 1
|
820
|
raise errors.InvalidValueError(
|
821
|
"Invalid bank value '%k', bad data in the image? \
|
822
|
Trying to fix this, review your bank data!" % k)
|
823
|
c = 1
|
824
|
for i in v:
|
825
|
fdata += chr(k) + chr(c) + chr(k - 1) + chr(i)
|
826
|
c = c + 1
|
827
|
|
828
|
# fill to match a full 256 bytes block
|
829
|
fdata += (len(fdata) % 256) * "\xFF"
|
830
|
|
831
|
# updating the data in the memmap [x300]
|
832
|
self._fill(0x300, fdata)
|
833
|
|
834
|
# update the info in x1000; it has 2 bytes with
|
835
|
# x00 = bank , x01 = bank's channel count
|
836
|
# the rest of the 14 bytes are \xff
|
837
|
bdata = ""
|
838
|
for i in range(1, len(data) + 1):
|
839
|
line = chr(i) + chr(len(data[i]))
|
840
|
line += "\xff" * 14
|
841
|
bdata += line
|
842
|
|
843
|
# fill to match a full 256 bytes block
|
844
|
bdata += (256 - (len(bdata)) % 256) * "\xFF"
|
845
|
|
846
|
# fill to match the whole area
|
847
|
bdata += (16 - len(bdata) / 256) * EMPTY_BLOCK
|
848
|
|
849
|
# updating the data in the memmap [x1000]
|
850
|
self._fill(0x1000, bdata)
|
851
|
|
852
|
# DTMF id for each channel, 5 bytes lbcd at x7000
|
853
|
# ############## TODO ###################
|
854
|
fldata = "\x00\xf0\xff\xff\xff" * self._chs_progs + \
|
855
|
"\xff" * (5 * (self._upper - self._chs_progs))
|
856
|
|
857
|
# write it
|
858
|
# updating the data in the memmap [x7000]
|
859
|
self._fill(0x7000, fldata)
|
860
|
|
861
|
def _set_variant(self):
|
862
|
"""Select and set the correct variables for the class acording
|
863
|
to the correct variant of the radio"""
|
864
|
rid = self._mmap[0xA7:0xAE]
|
865
|
|
866
|
# indentify the radio variant and set the enviroment to it's values
|
867
|
try:
|
868
|
self._upper, low, high, self._kind = self.VARIANTS[rid]
|
869
|
|
870
|
# Frequency ranges: some model/variants are able to work the near
|
871
|
# ham bands, even if they are outside the OEM ranges.
|
872
|
# By experimentation we found that 4% at the edges is in most
|
873
|
# cases safe and will cover the near ham bands in full
|
874
|
self._range = [low * 1000000 * 0.96, high * 1000000 * 1.04]
|
875
|
|
876
|
# setting the bank data in the features, 8 & 16 CH dont have banks
|
877
|
if self._upper < 32:
|
878
|
rf = chirp_common.RadioFeatures()
|
879
|
rf.has_bank = False
|
880
|
|
881
|
# put the VARIANT in the class, clean the model / CHs / Type
|
882
|
# in the same layout as the KPG program
|
883
|
self._VARIANT = self.MODEL + " [" + str(self._upper) + "CH]: "
|
884
|
# In the OEM string we show the real OEM ranges
|
885
|
self._VARIANT += self._kind + ", %d - %d MHz" % (low, high)
|
886
|
|
887
|
except KeyError:
|
888
|
LOG.debug("Wrong Kenwood radio, ID or unknown variant")
|
889
|
LOG.debug(util.hexprint(rid))
|
890
|
raise errors.RadioError(
|
891
|
"Wrong Kenwood radio, ID or unknown variant, see LOG output.")
|
892
|
return False
|
893
|
|
894
|
def sync_in(self):
|
895
|
"""Do a download of the radio eeprom"""
|
896
|
self._mmap = do_download(self)
|
897
|
self.process_mmap()
|
898
|
|
899
|
def sync_out(self):
|
900
|
"""Do an upload to the radio eeprom"""
|
901
|
|
902
|
# chirp signature on the eprom ;-)
|
903
|
sign = "Chirp"
|
904
|
self._fill(0xbb, sign)
|
905
|
|
906
|
try:
|
907
|
self._prep_data()
|
908
|
do_upload(self)
|
909
|
except errors.RadioError:
|
910
|
raise
|
911
|
except Exception, e:
|
912
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
913
|
|
914
|
def process_mmap(self):
|
915
|
"""Process the memory object"""
|
916
|
# how many channels are programed
|
917
|
self._chs_progs = ord(self._mmap[15])
|
918
|
|
919
|
# load the memobj
|
920
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
921
|
|
922
|
# to set the vars on the class to the correct ones
|
923
|
self._set_variant()
|
924
|
|
925
|
def get_raw_memory(self, number):
|
926
|
"""Return a raw representation of the memory object, which
|
927
|
is very helpful for development"""
|
928
|
return repr(self._memobj.memory[number])
|
929
|
|
930
|
def _decode_tone(self, val):
|
931
|
"""Parse the tone data to decode from mem, it returns:
|
932
|
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
|
933
|
val = int(val)
|
934
|
if val == 65535:
|
935
|
return '', None, None
|
936
|
elif val >= 0x2800:
|
937
|
code = int("%03o" % (val & 0x07FF))
|
938
|
pol = (val & 0x8000) and "R" or "N"
|
939
|
return 'DTCS', code, pol
|
940
|
else:
|
941
|
a = val / 10.0
|
942
|
return 'Tone', a, None
|
943
|
|
944
|
def _encode_tone(self, memval, mode, value, pol):
|
945
|
"""Parse the tone data to encode from UI to mem"""
|
946
|
if mode == '':
|
947
|
memval.set_raw("\xff\xff")
|
948
|
elif mode == 'Tone':
|
949
|
memval.set_value(int(value * 10))
|
950
|
elif mode == 'DTCS':
|
951
|
val = int("%i" % value, 8) + 0x2800
|
952
|
if pol == "R":
|
953
|
val += 0xA000
|
954
|
memval.set_value(val)
|
955
|
else:
|
956
|
raise Exception("Internal error: invalid mode `%s'" % mode)
|
957
|
|
958
|
def _get_scan(self, chan):
|
959
|
"""Get the channel scan status from the 16 bytes array on the eeprom
|
960
|
then from the bits on the byte, return '' or 'S' as needed"""
|
961
|
result = "S"
|
962
|
byte = int(chan/8)
|
963
|
bit = chan % 8
|
964
|
res = self._memobj.settings.add[byte] & (pow(2, bit))
|
965
|
if res > 0:
|
966
|
result = ""
|
967
|
|
968
|
return result
|
969
|
|
970
|
def _set_scan(self, chan, value):
|
971
|
"""Set the channel scan status from UI to the mem_map"""
|
972
|
byte = int(chan/8)
|
973
|
bit = chan % 8
|
974
|
|
975
|
# get the actual value to see if I need to change anything
|
976
|
actual = self._get_scan(chan)
|
977
|
if actual != value:
|
978
|
# I have to flip the value
|
979
|
rbyte = self._memobj.settings.add[byte]
|
980
|
rbyte = rbyte ^ pow(2, bit)
|
981
|
self._memobj.settings.add[byte] = rbyte
|
982
|
|
983
|
def get_memory(self, number):
|
984
|
# Get a low-level memory object mapped to the image
|
985
|
_mem = self._memobj.memory[number - 1]
|
986
|
|
987
|
# Create a high-level memory object to return to the UI
|
988
|
mem = chirp_common.Memory()
|
989
|
|
990
|
# Memory number
|
991
|
mem.number = number
|
992
|
|
993
|
# this radio has a setting about the amount of real chans of the 128
|
994
|
# olso in the channel has xff on the Rx freq it's empty
|
995
|
if (number > (self._chs_progs + 1)) or (_mem.get_raw()[0] == "\xFF"):
|
996
|
mem.empty = True
|
997
|
# but is not enough, you have to crear the memory in the mmap
|
998
|
# to get it ready for the sync_out process
|
999
|
_mem.set_raw("\xFF" * 48)
|
1000
|
return mem
|
1001
|
|
1002
|
# Freq and offset
|
1003
|
mem.freq = int(_mem.rxfreq) * 10
|
1004
|
# tx freq can be blank
|
1005
|
if _mem.get_raw()[16] == "\xFF":
|
1006
|
# TX freq not set
|
1007
|
mem.offset = 0
|
1008
|
mem.duplex = "off"
|
1009
|
else:
|
1010
|
# TX feq set
|
1011
|
offset = (int(_mem.txfreq) * 10) - mem.freq
|
1012
|
if offset < 0:
|
1013
|
mem.offset = abs(offset)
|
1014
|
mem.duplex = "-"
|
1015
|
elif offset > 0:
|
1016
|
mem.offset = offset
|
1017
|
mem.duplex = "+"
|
1018
|
else:
|
1019
|
mem.offset = 0
|
1020
|
|
1021
|
# name TAG of the channel
|
1022
|
mem.name = str(_mem.name).rstrip()
|
1023
|
|
1024
|
# power
|
1025
|
mem.power = POWER_LEVELS[_mem.power]
|
1026
|
|
1027
|
# wide/marrow
|
1028
|
mem.mode = MODES[_mem.wide]
|
1029
|
|
1030
|
# skip
|
1031
|
mem.skip = self._get_scan(number - 1)
|
1032
|
|
1033
|
# tone data
|
1034
|
rxtone = txtone = None
|
1035
|
txtone = self._decode_tone(_mem.tx_tone)
|
1036
|
rxtone = self._decode_tone(_mem.rx_tone)
|
1037
|
chirp_common.split_tone_decode(mem, txtone, rxtone)
|
1038
|
|
1039
|
# Extra
|
1040
|
# bank and number in the channel
|
1041
|
mem.extra = RadioSettingGroup("extra", "Extra")
|
1042
|
|
1043
|
# validate bank
|
1044
|
b = int(_mem.bank)
|
1045
|
if b > 127 or b == 0:
|
1046
|
_mem.bank = b = 1
|
1047
|
|
1048
|
bank = RadioSetting("bank", "Bank it belongs",
|
1049
|
RadioSettingValueInteger(1, 128, b))
|
1050
|
mem.extra.append(bank)
|
1051
|
|
1052
|
# validate bnumb
|
1053
|
if int(_mem.bnumb) > 127:
|
1054
|
_mem.bank = mem.number
|
1055
|
|
1056
|
bnumb = RadioSetting("bnumb", "Ch number in the bank",
|
1057
|
RadioSettingValueInteger(0, 127, _mem.bnumb))
|
1058
|
mem.extra.append(bnumb)
|
1059
|
|
1060
|
bs = RadioSetting("beat_shift", "Beat shift",
|
1061
|
RadioSettingValueBoolean(
|
1062
|
not bool(_mem.beat_shift)))
|
1063
|
mem.extra.append(bs)
|
1064
|
|
1065
|
cp = RadioSetting("compander", "Compander",
|
1066
|
RadioSettingValueBoolean(
|
1067
|
not bool(_mem.compander)))
|
1068
|
mem.extra.append(cp)
|
1069
|
|
1070
|
bl = RadioSetting("busy_lock", "Busy Channel lock",
|
1071
|
RadioSettingValueBoolean(
|
1072
|
not bool(_mem.busy_lock)))
|
1073
|
mem.extra.append(bl)
|
1074
|
|
1075
|
return mem
|
1076
|
|
1077
|
def set_memory(self, mem):
|
1078
|
"""Set the memory data in the eeprom img from the UI
|
1079
|
not ready yet, so it will return as is"""
|
1080
|
|
1081
|
# get the eprom representation of this channel
|
1082
|
_mem = self._memobj.memory[mem.number - 1]
|
1083
|
|
1084
|
# if empty memmory
|
1085
|
if mem.empty:
|
1086
|
_mem.set_raw("\xFF" * 48)
|
1087
|
return
|
1088
|
|
1089
|
# frequency
|
1090
|
_mem.rxfreq = mem.freq / 10
|
1091
|
|
1092
|
# this are a mistery yet, but so falr there is no impact
|
1093
|
# whit this default values for new channels
|
1094
|
if int(_mem.rx_unkw) == 0xff:
|
1095
|
_mem.rx_unkw = 0x35
|
1096
|
_mem.tx_unkw = 0x32
|
1097
|
|
1098
|
# duplex
|
1099
|
if mem.duplex == "+":
|
1100
|
_mem.txfreq = (mem.freq + mem.offset) / 10
|
1101
|
elif mem.duplex == "-":
|
1102
|
_mem.txfreq = (mem.freq - mem.offset) / 10
|
1103
|
elif mem.duplex == "off":
|
1104
|
for byte in _mem.txfreq:
|
1105
|
byte.set_raw("\xFF")
|
1106
|
else:
|
1107
|
_mem.txfreq = mem.freq / 10
|
1108
|
|
1109
|
# tone data
|
1110
|
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
|
1111
|
chirp_common.split_tone_encode(mem)
|
1112
|
self._encode_tone(_mem.tx_tone, txmode, txtone, txpol)
|
1113
|
self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol)
|
1114
|
|
1115
|
# name TAG of the channel
|
1116
|
_namelength = self.get_features().valid_name_length
|
1117
|
for i in range(_namelength):
|
1118
|
try:
|
1119
|
_mem.name[i] = mem.name[i]
|
1120
|
except IndexError:
|
1121
|
_mem.name[i] = "\x20"
|
1122
|
|
1123
|
# power
|
1124
|
# default power is low
|
1125
|
if mem.power is None:
|
1126
|
mem.power = POWER_LEVELS[0]
|
1127
|
|
1128
|
_mem.power = POWER_LEVELS.index(mem.power)
|
1129
|
|
1130
|
# wide/marrow
|
1131
|
_mem.wide = MODES.index(mem.mode)
|
1132
|
|
1133
|
# scan add property
|
1134
|
self._set_scan(mem.number - 1, mem.skip)
|
1135
|
|
1136
|
# bank and number in the channel
|
1137
|
if int(_mem.bnumb) == 0xff:
|
1138
|
_mem.bnumb = mem.number - 1
|
1139
|
_mem.bank = 1
|
1140
|
|
1141
|
# extra settings
|
1142
|
for setting in mem.extra:
|
1143
|
if setting != "bank" or setting != "bnumb":
|
1144
|
setattr(_mem, setting.get_name(), not bool(setting.value))
|
1145
|
|
1146
|
# all data get sync after channel mod
|
1147
|
self._prep_data()
|
1148
|
|
1149
|
return mem
|
1150
|
|
1151
|
@classmethod
|
1152
|
def match_model(cls, filedata, filename):
|
1153
|
match_size = False
|
1154
|
match_model = False
|
1155
|
|
1156
|
# testing the file data size
|
1157
|
if len(filedata) == MEM_SIZE:
|
1158
|
match_size = True
|
1159
|
|
1160
|
# testing the firmware model fingerprint
|
1161
|
match_model = model_match(cls, filedata)
|
1162
|
|
1163
|
if match_size and match_model:
|
1164
|
return True
|
1165
|
else:
|
1166
|
return False
|
1167
|
|
1168
|
def get_settings(self):
|
1169
|
"""Translate the bit in the mem_struct into settings in the UI"""
|
1170
|
sett = self._memobj.settings
|
1171
|
mess = self._memobj.message
|
1172
|
keys = self._memobj.keys
|
1173
|
idm = self._memobj.id
|
1174
|
passwd = self._memobj.passwords
|
1175
|
|
1176
|
# basic features of the radio
|
1177
|
basic = RadioSettingGroup("basic", "Basic Settings")
|
1178
|
# dealer settings
|
1179
|
dealer = RadioSettingGroup("dealer", "Dealer Settings")
|
1180
|
# buttons
|
1181
|
fkeys = RadioSettingGroup("keys", "Front keys config")
|
1182
|
|
1183
|
# TODO / PLANED
|
1184
|
# adjust feqs
|
1185
|
#freqs = RadioSettingGroup("freqs", "Adjust Frequencies")
|
1186
|
|
1187
|
top = RadioSettings(basic, dealer, fkeys)
|
1188
|
|
1189
|
# Basic
|
1190
|
tot = RadioSetting("settings.tot", "Time Out Timer (TOT)",
|
1191
|
RadioSettingValueList(TOT, TOT[
|
1192
|
TOT.index(str(int(sett.tot)))]))
|
1193
|
basic.append(tot)
|
1194
|
|
1195
|
totalert = RadioSetting("settings.tot_alert", "TOT pre alert",
|
1196
|
RadioSettingValueList(TOT_PRE,
|
1197
|
TOT_PRE[int(sett.tot_alert)]))
|
1198
|
basic.append(totalert)
|
1199
|
|
1200
|
totrekey = RadioSetting("settings.tot_rekey", "TOT re-key time",
|
1201
|
RadioSettingValueList(TOT_REKEY,
|
1202
|
TOT_REKEY[int(sett.tot_rekey)]))
|
1203
|
basic.append(totrekey)
|
1204
|
|
1205
|
totreset = RadioSetting("settings.tot_reset", "TOT reset time",
|
1206
|
RadioSettingValueList(TOT_RESET,
|
1207
|
TOT_RESET[int(sett.tot_reset)]))
|
1208
|
basic.append(totreset)
|
1209
|
|
1210
|
# this feature is for mobile only
|
1211
|
if self.TYPE[0] == "M":
|
1212
|
minvol = RadioSetting("settings.min_vol", "Minimum volume",
|
1213
|
RadioSettingValueList(VOL,
|
1214
|
VOL[int(sett.min_vol)]))
|
1215
|
basic.append(minvol)
|
1216
|
|
1217
|
tv = int(sett.tone_vol)
|
1218
|
if tv == 255:
|
1219
|
tv = 32
|
1220
|
tvol = RadioSetting("settings.tone_vol", "Minimum tone volume",
|
1221
|
RadioSettingValueList(TVOL, TVOL[tv]))
|
1222
|
basic.append(tvol)
|
1223
|
|
1224
|
sql = RadioSetting("settings.sql_level", "SQL Ref Level",
|
1225
|
RadioSettingValueList(
|
1226
|
SQL, SQL[int(sett.sql_level)]))
|
1227
|
basic.append(sql)
|
1228
|
|
1229
|
#c2t = RadioSetting("settings.c2t", "Clear to Transpond",
|
1230
|
#RadioSettingValueBoolean(not sett.c2t))
|
1231
|
#basic.append(c2t)
|
1232
|
|
1233
|
ptone = RadioSetting("settings.poweron_tone", "Power On tone",
|
1234
|
RadioSettingValueBoolean(sett.poweron_tone))
|
1235
|
basic.append(ptone)
|
1236
|
|
1237
|
ctone = RadioSetting("settings.control_tone", "Control (key) tone",
|
1238
|
RadioSettingValueBoolean(sett.control_tone))
|
1239
|
basic.append(ctone)
|
1240
|
|
1241
|
wtone = RadioSetting("settings.warn_tone", "Warning tone",
|
1242
|
RadioSettingValueBoolean(sett.warn_tone))
|
1243
|
basic.append(wtone)
|
1244
|
|
1245
|
# Save Battery only for portables?
|
1246
|
if self.TYPE[0] == "P":
|
1247
|
bs = int(sett.battery_save) == 0x32 and True or False
|
1248
|
bsave = RadioSetting("settings.battery_save", "Battery Saver",
|
1249
|
RadioSettingValueBoolean(bs))
|
1250
|
basic.append(bsave)
|
1251
|
|
1252
|
ponm = str(sett.poweronmesg).strip("\xff")
|
1253
|
pom = RadioSetting("settings.poweronmesg", "Power on message",
|
1254
|
RadioSettingValueString(0, 8, ponm, False))
|
1255
|
basic.append(pom)
|
1256
|
|
1257
|
# dealer
|
1258
|
valid_chars = ",-/:[]" + chirp_common.CHARSET_ALPHANUMERIC
|
1259
|
mstr = "".join([c for c in self._VARIANT if c in valid_chars])
|
1260
|
|
1261
|
val = RadioSettingValueString(0, 35, mstr)
|
1262
|
val.set_mutable(False)
|
1263
|
mod = RadioSetting("not.mod", "Radio Version", val)
|
1264
|
dealer.append(mod)
|
1265
|
|
1266
|
sn = str(idm.serial).strip(" \xff")
|
1267
|
val = RadioSettingValueString(0, 8, sn)
|
1268
|
val.set_mutable(False)
|
1269
|
serial = RadioSetting("not.serial", "Serial number", val)
|
1270
|
dealer.append(serial)
|
1271
|
|
1272
|
svp = str(sett.lastsoftversion).strip(" \xff")
|
1273
|
val = RadioSettingValueString(0, 5, svp)
|
1274
|
val.set_mutable(False)
|
1275
|
sver = RadioSetting("not.softver", "Software Version", val)
|
1276
|
dealer.append(sver)
|
1277
|
|
1278
|
l1 = str(mess.line1).strip(" \xff")
|
1279
|
line1 = RadioSetting("message.line1", "Comment 1",
|
1280
|
RadioSettingValueString(0, 32, l1))
|
1281
|
dealer.append(line1)
|
1282
|
|
1283
|
l2 = str(mess.line2).strip(" \xff")
|
1284
|
line2 = RadioSetting("message.line2", "Comment 2",
|
1285
|
RadioSettingValueString(0, 32, l2))
|
1286
|
dealer.append(line2)
|
1287
|
|
1288
|
sprog = RadioSetting("settings.self_prog", "Self program",
|
1289
|
RadioSettingValueBoolean(sett.self_prog))
|
1290
|
dealer.append(sprog)
|
1291
|
|
1292
|
clone = RadioSetting("settings.clone", "Allow clone",
|
1293
|
RadioSettingValueBoolean(sett.clone))
|
1294
|
dealer.append(clone)
|
1295
|
|
1296
|
panel = RadioSetting("settings.panel_test", "Panel Test",
|
1297
|
RadioSettingValueBoolean(sett.panel_test))
|
1298
|
dealer.append(panel)
|
1299
|
|
1300
|
fmw = RadioSetting("settings.firmware_prog", "Firmware program",
|
1301
|
RadioSettingValueBoolean(sett.firmware_prog))
|
1302
|
dealer.append(fmw)
|
1303
|
|
1304
|
# front keys
|
1305
|
# The Mobile only parameters are wraped here
|
1306
|
if self.TYPE[0] == "M":
|
1307
|
vu = RadioSetting("keys.kVOL_UP", "VOL UP",
|
1308
|
RadioSettingValueList(KEYS.values(),
|
1309
|
KEYS.values()[KEYS.keys().index(
|
1310
|
int(keys.kVOL_UP))]))
|
1311
|
fkeys.append(vu)
|
1312
|
|
1313
|
vd = RadioSetting("keys.kVOL_DOWN", "VOL DOWN",
|
1314
|
RadioSettingValueList(KEYS.values(),
|
1315
|
KEYS.values()[KEYS.keys().index(
|
1316
|
int(keys.kVOL_DOWN))]))
|
1317
|
fkeys.append(vd)
|
1318
|
|
1319
|
chu = RadioSetting("keys.kCH_UP", "CH UP",
|
1320
|
RadioSettingValueList(KEYS.values(),
|
1321
|
KEYS.values()[KEYS.keys().index(
|
1322
|
int(keys.kCH_UP))]))
|
1323
|
fkeys.append(chu)
|
1324
|
|
1325
|
chd = RadioSetting("keys.kCH_DOWN", "CH DOWN",
|
1326
|
RadioSettingValueList(KEYS.values(),
|
1327
|
KEYS.values()[KEYS.keys().index(
|
1328
|
int(keys.kCH_DOWN))]))
|
1329
|
fkeys.append(chd)
|
1330
|
|
1331
|
foot = RadioSetting("keys.kFOOT", "Foot switch",
|
1332
|
RadioSettingValueList(KEYS.values(),
|
1333
|
KEYS.values()[KEYS.keys().index(
|
1334
|
int(keys.kCH_DOWN))]))
|
1335
|
fkeys.append(foot)
|
1336
|
|
1337
|
# this is the common buttons for all
|
1338
|
|
1339
|
# 260G model don't have the front keys
|
1340
|
if not "P2600" in self.TYPE:
|
1341
|
scn_name = "SCN"
|
1342
|
if self.TYPE[0] == "P":
|
1343
|
scn_name = "Open Circle"
|
1344
|
|
1345
|
scn = RadioSetting("keys.kSCN", scn_name,
|
1346
|
RadioSettingValueList(KEYS.values(),
|
1347
|
KEYS.values()[KEYS.keys().index(
|
1348
|
int(keys.kSCN))]))
|
1349
|
fkeys.append(scn)
|
1350
|
|
1351
|
a_name = "A"
|
1352
|
if self.TYPE[0] == "P":
|
1353
|
a_name = "Closed circle"
|
1354
|
|
1355
|
a = RadioSetting("keys.kA", a_name,
|
1356
|
RadioSettingValueList(KEYS.values(),
|
1357
|
KEYS.values()[KEYS.keys().index(
|
1358
|
int(keys.kA))]))
|
1359
|
fkeys.append(a)
|
1360
|
|
1361
|
da_name = "D/A"
|
1362
|
if self.TYPE[0] == "P":
|
1363
|
da_name = "< key"
|
1364
|
|
1365
|
da = RadioSetting("keys.kDA", da_name,
|
1366
|
RadioSettingValueList(KEYS.values(),
|
1367
|
KEYS.values()[KEYS.keys().index(
|
1368
|
int(keys.kDA))]))
|
1369
|
fkeys.append(da)
|
1370
|
|
1371
|
gu_name = "Triangle up"
|
1372
|
if self.TYPE[0] == "P":
|
1373
|
gu_name = "Side 1"
|
1374
|
|
1375
|
gu = RadioSetting("keys.kGROUP_UP", gu_name,
|
1376
|
RadioSettingValueList(KEYS.values(),
|
1377
|
KEYS.values()[KEYS.keys().index(
|
1378
|
int(keys.kGROUP_UP))]))
|
1379
|
fkeys.append(gu)
|
1380
|
|
1381
|
# Side keys on portables
|
1382
|
gd_name = "Triangle Down"
|
1383
|
if self.TYPE[0] == "P":
|
1384
|
gd_name = "> key"
|
1385
|
|
1386
|
gd = RadioSetting("keys.kGROUP_DOWN", gd_name,
|
1387
|
RadioSettingValueList(KEYS.values(),
|
1388
|
KEYS.values()[KEYS.keys().index(
|
1389
|
int(keys.kGROUP_DOWN))]))
|
1390
|
fkeys.append(gd)
|
1391
|
|
1392
|
mon_name = "MON"
|
1393
|
if self.TYPE[0] == "P":
|
1394
|
mon_name = "Side 2"
|
1395
|
|
1396
|
mon = RadioSetting("keys.kMON", mon_name,
|
1397
|
RadioSettingValueList(KEYS.values(),
|
1398
|
KEYS.values()[KEYS.keys().index(
|
1399
|
int(keys.kMON))]))
|
1400
|
fkeys.append(mon)
|
1401
|
|
1402
|
return top
|
1403
|
|
1404
|
def set_settings(self, settings):
|
1405
|
"""Translate the settings in the UI into bit in the mem_struct
|
1406
|
I don't understand well the method used in many drivers
|
1407
|
so, I used mine, ugly but works ok"""
|
1408
|
|
1409
|
mobj = self._memobj
|
1410
|
|
1411
|
for element in settings:
|
1412
|
if not isinstance(element, RadioSetting):
|
1413
|
self.set_settings(element)
|
1414
|
continue
|
1415
|
|
1416
|
# Let's roll the ball
|
1417
|
if "." in element.get_name():
|
1418
|
inter, setting = element.get_name().split(".")
|
1419
|
# you must ignore the settings with "not"
|
1420
|
# this are READ ONLY attributes
|
1421
|
if inter == "not":
|
1422
|
continue
|
1423
|
|
1424
|
obj = getattr(mobj, inter)
|
1425
|
value = element.value
|
1426
|
|
1427
|
# integers case + special case
|
1428
|
if setting in ["tot", "tot_alert", "min_vol", "tone_vol",
|
1429
|
"sql_level", "tot_rekey", "tot_reset"]:
|
1430
|
# catching the "off" values as zero
|
1431
|
try:
|
1432
|
value = int(value)
|
1433
|
except:
|
1434
|
value = 0
|
1435
|
|
1436
|
# tot case step 15
|
1437
|
if setting == "tot":
|
1438
|
value = value * 15
|
1439
|
# off is special
|
1440
|
if value == 0:
|
1441
|
value = 0x4b0
|
1442
|
|
1443
|
# Caso tone_vol
|
1444
|
if setting == "tone_vol":
|
1445
|
# off is special
|
1446
|
if value == 32:
|
1447
|
value = 0xff
|
1448
|
|
1449
|
# Bool types + inverted
|
1450
|
if setting in ["c2t", "poweron_tone", "control_tone",
|
1451
|
"warn_tone", "battery_save", "self_prog",
|
1452
|
"clone", "panel_test"]:
|
1453
|
value = bool(value)
|
1454
|
|
1455
|
# this cases are inverted
|
1456
|
if setting == "c2t":
|
1457
|
value = not value
|
1458
|
|
1459
|
# case battery save is special
|
1460
|
if setting == "battery_save":
|
1461
|
if bool(value) is True:
|
1462
|
value = 0x32
|
1463
|
else:
|
1464
|
value = 0xff
|
1465
|
|
1466
|
# String cases
|
1467
|
if setting in ["poweronmesg", "line1", "line2"]:
|
1468
|
# some vars
|
1469
|
value = str(value)
|
1470
|
just = 8
|
1471
|
# lines with 32
|
1472
|
if "line" in setting:
|
1473
|
just = 32
|
1474
|
|
1475
|
# empty case
|
1476
|
if len(value) == 0:
|
1477
|
value = "\xff" * just
|
1478
|
else:
|
1479
|
value = value.ljust(just)
|
1480
|
|
1481
|
# case keys, with special config
|
1482
|
if inter == "keys":
|
1483
|
value = KEYS.keys()[KEYS.values().index(str(value))]
|
1484
|
|
1485
|
# Apply al configs done
|
1486
|
setattr(obj, setting, value)
|
1487
|
|
1488
|
def get_bank_model(self):
|
1489
|
"""Pass the bank model to the UI part"""
|
1490
|
rf = self.get_features()
|
1491
|
if rf.has_bank is True:
|
1492
|
return Kenwood60GBankModel(self)
|
1493
|
else:
|
1494
|
return None
|
1495
|
|
1496
|
def _get_bank(self, loc):
|
1497
|
"""Get the bank data for a specific channel"""
|
1498
|
mem = self._memobj.memory[loc - 1]
|
1499
|
bank = int(mem.bank) - 1
|
1500
|
|
1501
|
if bank > self._num_banks or bank < 1:
|
1502
|
# all channels must belong to a bank, even with just 1 bank
|
1503
|
return 0
|
1504
|
else:
|
1505
|
return bank
|
1506
|
|
1507
|
def _set_bank(self, loc, bank):
|
1508
|
"""Set the bank data for a specific channel"""
|
1509
|
try:
|
1510
|
b = int(bank)
|
1511
|
if b > 127:
|
1512
|
b = 0
|
1513
|
mem = self._memobj.memory[loc - 1]
|
1514
|
mem.bank = b + 1
|
1515
|
except:
|
1516
|
msg = "You can't have a channel without a bank, click another bank"
|
1517
|
raise errors.InvalidDataError(msg)
|
1518
|
|
1519
|
|
1520
|
# This kenwwood family is known as "60-G Serie"
|
1521
|
# all this radios ending in G are compatible:
|
1522
|
#
|
1523
|
# Portables VHF TK-260G/270G/272G/278G
|
1524
|
# Portables UHF TK-360G/370G/372G/378G/388G
|
1525
|
#
|
1526
|
# Mobiles VHF TK-760G/762G/768G
|
1527
|
# Mobiles VHF TK-860G/862G/868G
|
1528
|
#
|
1529
|
# WARNING !!!! Radios With Password in the data section ###############
|
1530
|
#
|
1531
|
# When a radio has a data password (aka to program it) the last byte (#8)
|
1532
|
# in the id code change from \xf1 to \xb1; so we remove this last byte
|
1533
|
# from the identification procedures and variants.
|
1534
|
#
|
1535
|
# This effectively render the data password USELESS even if set.
|
1536
|
# Translation: Chirps will read and write password protected radios
|
1537
|
# with no problem.
|
1538
|
|
1539
|
|
1540
|
@directory.register
|
1541
|
class TK868G_Radios(Kenwood_Serie_60G):
|
1542
|
"""Kenwood TK-868G Radio M/C"""
|
1543
|
MODEL = "TK-868G"
|
1544
|
TYPE = "M8680"
|
1545
|
VARIANTS = {
|
1546
|
"M8680\x18\xff": (8, 400, 490, "M"),
|
1547
|
"M8680;\xff": (128, 350, 390, "C1"),
|
1548
|
"M86808\xff": (128, 400, 430, "C2"),
|
1549
|
"M86806\xff": (128, 450, 490, "C3"),
|
1550
|
}
|
1551
|
|
1552
|
|
1553
|
@directory.register
|
1554
|
class TK862G_Radios(Kenwood_Serie_60G):
|
1555
|
"""Kenwood TK-862G Radio K/E/(N)E"""
|
1556
|
MODEL = "TK-862G"
|
1557
|
TYPE = "M8620"
|
1558
|
VARIANTS = {
|
1559
|
"M8620\x06\xff": (8, 450, 490, "K"),
|
1560
|
"M8620\x07\xff": (8, 485, 512, "K2"),
|
1561
|
"M8620&\xff": (8, 440, 470, "E"),
|
1562
|
"M8620V\xff": (8, 440, 470, "(N)E"),
|
1563
|
}
|
1564
|
|
1565
|
|
1566
|
@directory.register
|
1567
|
class TK860G_Radios(Kenwood_Serie_60G):
|
1568
|
"""Kenwood TK-860G Radio K"""
|
1569
|
MODEL = "TK-860G"
|
1570
|
TYPE = "M8600"
|
1571
|
VARIANTS = {
|
1572
|
"M8600\x08\xff": (128, 400, 430, "K"),
|
1573
|
"M8600\x06\xff": (128, 450, 490, "K1"),
|
1574
|
"M8600\x07\xff": (128, 485, 512, "K2"),
|
1575
|
"M8600\x18\xff": (128, 400, 430, "M"),
|
1576
|
"M8600\x16\xff": (128, 450, 490, "M1"),
|
1577
|
"M8600\x17\xff": (128, 485, 520, "M2"),
|
1578
|
}
|
1579
|
|
1580
|
|
1581
|
@directory.register
|
1582
|
class TK768G_Radios(Kenwood_Serie_60G):
|
1583
|
"""Kenwood TK-768G Radios [M/C]"""
|
1584
|
MODEL = "TK-768G"
|
1585
|
TYPE = "M7680"
|
1586
|
# Note that 8 CH don't have banks
|
1587
|
VARIANTS = {
|
1588
|
"M7680\x15\xff": (8, 136, 162, "M2"),
|
1589
|
"M7680\x14\xff": (8, 148, 174, "M"),
|
1590
|
"M76805\xff": (128, 136, 162, "C2"),
|
1591
|
"M76804\xff": (128, 148, 174, "C"),
|
1592
|
}
|
1593
|
|
1594
|
|
1595
|
@directory.register
|
1596
|
class TK762G_Radios(Kenwood_Serie_60G):
|
1597
|
"""Kenwood TK-762G Radios [K/E/NE]"""
|
1598
|
MODEL = "TK-762G"
|
1599
|
TYPE = "M7620"
|
1600
|
# Note that 8 CH don't have banks
|
1601
|
VARIANTS = {
|
1602
|
"M7620\x05\xff": (8, 136, 162, "K2"),
|
1603
|
"M7620\x04\xff": (8, 148, 172, "K"),
|
1604
|
"M7620$\xff": (8, 148, 172, "E"),
|
1605
|
"M7620T\xff": (8, 148, 172, "NE"),
|
1606
|
}
|
1607
|
|
1608
|
|
1609
|
@directory.register
|
1610
|
class TK760G_Radios(Kenwood_Serie_60G):
|
1611
|
"""Kenwood TK-760G Radios [K/M/(N)E]"""
|
1612
|
MODEL = "TK-760G"
|
1613
|
TYPE = "M7600"
|
1614
|
VARIANTS = {
|
1615
|
"M7600\x05\xff": (128, 136, 162, "K2"),
|
1616
|
"M7600\x04\xff": (128, 148, 174, "K"),
|
1617
|
"M7600\x14\xff": (128, 148, 174, "M"),
|
1618
|
"M7600T\xff": (128, 148, 174, "NE")
|
1619
|
}
|
1620
|
|
1621
|
|
1622
|
@directory.register
|
1623
|
class TK388G_Radios(Kenwood_Serie_60G):
|
1624
|
"""Kenwood TK-388 Radio [K/E/M/NE]"""
|
1625
|
MODEL = "TK-388G"
|
1626
|
TYPE = "P3880"
|
1627
|
VARIANTS = {
|
1628
|
"P3880\x1b\xff": (128, 350, 370, "M")
|
1629
|
}
|
1630
|
|
1631
|
|
1632
|
@directory.register
|
1633
|
class TK378G_Radios(Kenwood_Serie_60G):
|
1634
|
"""Kenwood TK-378 Radio [K/E/M/NE]"""
|
1635
|
MODEL = "TK-378G"
|
1636
|
TYPE = "P3780"
|
1637
|
VARIANTS = {
|
1638
|
"P3780\x16\xff": (16, 450, 470, "M"),
|
1639
|
"P3780\x17\xff": (16, 400, 420, "M1"),
|
1640
|
"P3780\x36\xff": (128, 490, 512, "C"),
|
1641
|
"P3780\x39\xff": (128, 403, 430, "C1")
|
1642
|
}
|
1643
|
|
1644
|
|
1645
|
@directory.register
|
1646
|
class TK372G_Radios(Kenwood_Serie_60G):
|
1647
|
"""Kenwood TK-372 Radio [K/E/M/NE]"""
|
1648
|
MODEL = "TK-372G"
|
1649
|
TYPE = "P3720"
|
1650
|
VARIANTS = {
|
1651
|
"P3720\x06\xff": (32, 450, 470, "K"),
|
1652
|
"P3720\x07\xff": (32, 470, 490, "K1"),
|
1653
|
"P3720\x08\xff": (32, 490, 512, "K2"),
|
1654
|
"P3720\x09\xff": (32, 403, 430, "K3")
|
1655
|
}
|
1656
|
|
1657
|
|
1658
|
@directory.register
|
1659
|
class TK370G_Radios(Kenwood_Serie_60G):
|
1660
|
"""Kenwood TK-370 Radio [K/E/M/NE]"""
|
1661
|
MODEL = "TK-370G"
|
1662
|
TYPE = "P3700"
|
1663
|
VARIANTS = {
|
1664
|
"P3700\x06\xff": (128, 450, 470, "K"),
|
1665
|
"P3700\x07\xff": (128, 470, 490, "K1"),
|
1666
|
"P3700\x08\xff": (128, 490, 512, "K2"),
|
1667
|
"P3700\x09\xff": (128, 403, 430, "K3"),
|
1668
|
"P3700\x16\xff": (128, 450, 470, "M"),
|
1669
|
"P3700\x17\xff": (128, 470, 490, "M1"),
|
1670
|
"P3700\x18\xff": (128, 490, 520, "M2"),
|
1671
|
"P3700\x19\xff": (128, 403, 430, "M3"),
|
1672
|
"P3700&\xff": (128, 440, 470, "E"),
|
1673
|
"P3700V\xff": (128, 440, 470, "NE")
|
1674
|
}
|
1675
|
|
1676
|
|
1677
|
@directory.register
|
1678
|
class TK360G_Radios(Kenwood_Serie_60G):
|
1679
|
"""Kenwood TK-360 Radio [K/E/M/NE]"""
|
1680
|
MODEL = "TK-360G"
|
1681
|
TYPE = "P3600"
|
1682
|
VARIANTS = {
|
1683
|
"P3600\x06\xff": (8, 450, 470, "K"),
|
1684
|
"P3600\x07\xff": (8, 470, 490, "K1"),
|
1685
|
"P3600\x08\xff": (8, 490, 512, "K2"),
|
1686
|
"P3600\x09\xff": (8, 403, 430, "K3"),
|
1687
|
"P3600&\xff": (8, 440, 470, "E"),
|
1688
|
"P3600)\xff": (8, 406, 430, "E1"),
|
1689
|
"P3600\x16\xff": (8, 450, 470, "M"),
|
1690
|
"P3600\x17\xff": (8, 470, 490, "M1"),
|
1691
|
"P3600\x19\xff": (8, 403, 430, "M2"),
|
1692
|
"P3600V\xff": (8, 440, 470, "NE"),
|
1693
|
"P3600Y\xff": (8, 403, 430, "NE1")
|
1694
|
}
|
1695
|
|
1696
|
|
1697
|
@directory.register
|
1698
|
class TK278G_Radios(Kenwood_Serie_60G):
|
1699
|
"""Kenwood TK-278G Radio C/C1/M/M1"""
|
1700
|
MODEL = "TK-278G"
|
1701
|
TYPE = "P2780"
|
1702
|
# Note that 16 CH don't have banks
|
1703
|
VARIANTS = {
|
1704
|
"P27805\xff": (128, 136, 150, "C1"),
|
1705
|
"P27804\xff": (128, 150, 174, "C"),
|
1706
|
"P2780\x15\xff": (16, 136, 150, "M1"),
|
1707
|
"P2780\x14\xff": (16, 150, 174, "M")
|
1708
|
}
|
1709
|
|
1710
|
|
1711
|
@directory.register
|
1712
|
class TK272G_Radios(Kenwood_Serie_60G):
|
1713
|
"""Kenwood TK-272G Radio K/K1"""
|
1714
|
MODEL = "TK-272G"
|
1715
|
TYPE = "P2720"
|
1716
|
VARIANTS = {
|
1717
|
"P2720\x05\xfb": (32, 136, 150, "K1"),
|
1718
|
"P2720\x04\xfb": (32, 150, 174, "K")
|
1719
|
}
|
1720
|
|
1721
|
|
1722
|
@directory.register
|
1723
|
class TK270G_Radios(Kenwood_Serie_60G):
|
1724
|
"""Kenwood TK-270G Radio K/K1/M/E/NE/NT"""
|
1725
|
MODEL = "TK-270G"
|
1726
|
TYPE = "P2700"
|
1727
|
VARIANTS = {
|
1728
|
"P2700T\xff": (128, 146, 174, "NE/NT"),
|
1729
|
"P2700$\xff": (128, 146, 174, "E"),
|
1730
|
"P2700\x14\xff": (128, 150, 174, "M"),
|
1731
|
"P2700\x05\xff": (128, 136, 150, "K1"),
|
1732
|
"P2700\x04\xff": (128, 150, 174, "K")
|
1733
|
}
|
1734
|
|
1735
|
|
1736
|
@directory.register
|
1737
|
class TK260G_Radios(Kenwood_Serie_60G):
|
1738
|
"""Kenwood TK-260G Radio K/K1/M/E/NE/NT"""
|
1739
|
MODEL = "TK-260G"
|
1740
|
_hasbanks = False
|
1741
|
TYPE = "P2600"
|
1742
|
VARIANTS = {
|
1743
|
"P2600U\xff": (8, 136, 150, "N1"),
|
1744
|
"P2600T\xff": (8, 146, 174, "N"),
|
1745
|
"P2600$\xff": (8, 150, 174, "E"),
|
1746
|
"P2600\x14\xff": (8, 150, 174, "M"),
|
1747
|
"P2600\x05\xff": (8, 136, 150, "K1"),
|
1748
|
"P2600\x04\xff": (8, 150, 174, "K")
|
1749
|
}
|