1
|
# Copyright 2014 Tom Hayward <tom@tomh.us>
|
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 logging
|
18
|
|
19
|
from chirp import chirp_common, directory, memmap, errors, util
|
20
|
from chirp import bitwise
|
21
|
from chirp.settings import RadioSetting, RadioSettingGroup, \
|
22
|
RadioSettingValueInteger, RadioSettingValueList, \
|
23
|
RadioSettingValueBoolean, RadioSettingValueString, \
|
24
|
RadioSettings
|
25
|
|
26
|
LOG = logging.getLogger(__name__)
|
27
|
|
28
|
MEM_FORMAT = """
|
29
|
#seekto 0x0184;
|
30
|
struct {
|
31
|
u8 unknown:4,
|
32
|
sql:4; // squelch level
|
33
|
u8 unknown0x0185;
|
34
|
u8 obeep:1, // open beep
|
35
|
dw_off:1, // dual watch (inverted)
|
36
|
kbeep:1, // key beep
|
37
|
rbeep:1, // roger beep
|
38
|
unknown:2,
|
39
|
ctdcsb:1, // ct/dcs busy lock
|
40
|
unknown:1;
|
41
|
u8 alarm:1, // alarm key
|
42
|
unknown1:1,
|
43
|
aliasen_off:1, // alias enable (inverted)
|
44
|
save:1, // battery save
|
45
|
unknown2:2,
|
46
|
mrcha:1, // mr/cha
|
47
|
vfomr:1; // vfo/mr
|
48
|
u8 keylock_off:1, // key lock (inverted)
|
49
|
txstop_off:1, // tx stop (inverted)
|
50
|
scanm:1, // scan key/mode
|
51
|
vir:1, // vox inhibit on receive
|
52
|
keylockm:2, // key lock mode
|
53
|
lamp:2; // backlight
|
54
|
u8 opendis:2, // open display
|
55
|
fmen_off:1, // fm enable (inverted)
|
56
|
unknown1:1,
|
57
|
fmscan_off:1, // fm scan (inverted)
|
58
|
fmdw:1, // fm dual watch
|
59
|
unknown2:2;
|
60
|
u8 step:4, // step
|
61
|
vol:4; // volume
|
62
|
u8 apo:4, // auto power off
|
63
|
tot:4; // time out timer
|
64
|
u8 unknown0x018C;
|
65
|
u8 voxdt:4, // vox delay time
|
66
|
voxgain:4; // vox gain
|
67
|
u8 unknown0x018E;
|
68
|
u8 unknown0x018F;
|
69
|
u8 unknown:3,
|
70
|
lptime:5; // long press time
|
71
|
u8 keyp2long:4, // p2 key long press
|
72
|
keyp2short:4; // p2 key short press
|
73
|
u8 keyp1long:4, // p1 key long press
|
74
|
keyp1short:4; // p1 key short press
|
75
|
u8 keyp3long:4, // p3 key long press
|
76
|
keyp3short:4; // p3 key short press
|
77
|
u8 unknown0x0194;
|
78
|
u8 menuen:1, // menu enable
|
79
|
absel:1, // a/b select
|
80
|
unknown:2,
|
81
|
keymshort:4; // m key short press
|
82
|
u8 unknown:4,
|
83
|
dtmfst:1, // dtmf sidetone
|
84
|
ackdecode:1, // ack decode
|
85
|
monitor:2; // monitor
|
86
|
u8 unknown1:3,
|
87
|
reset:1, // reset enable
|
88
|
unknown2:1,
|
89
|
keypadmic_off:1, // keypad mic (inverted)
|
90
|
unknown3:2;
|
91
|
u8 unknown0x0198;
|
92
|
u8 unknown1:3,
|
93
|
dtmftime:5; // dtmf digit time
|
94
|
u8 unknown1:3,
|
95
|
dtmfspace:5; // dtmf digit space time
|
96
|
u8 unknown1:2,
|
97
|
dtmfdelay:6; // dtmf first digit delay
|
98
|
u8 unknown1:1,
|
99
|
dtmfpretime:7; // dtmf pretime
|
100
|
u8 unknown1:2,
|
101
|
dtmfdelay2:6; // dtmf * and # digit delay
|
102
|
u8 unknown1:3,
|
103
|
smfont_off:1, // small font (inverted)
|
104
|
unknown:4;
|
105
|
} settings;
|
106
|
|
107
|
#seekto 0x01cd;
|
108
|
struct {
|
109
|
u8 rssi136; // squelch base level (vhf)
|
110
|
u8 unknown0x01ce;
|
111
|
u8 rssi400; // squelch base level (uhf)
|
112
|
} service;
|
113
|
|
114
|
#seekto 0x0900;
|
115
|
struct {
|
116
|
char user1[7]; // user message 1
|
117
|
char unknown0x0907;
|
118
|
char unknown0x0908[8];
|
119
|
char unknown0x0910[8];
|
120
|
char system[7]; // system message
|
121
|
char unknown0x091F;
|
122
|
char user2[7]; // user message 2
|
123
|
char unknown0x0927;
|
124
|
} messages;
|
125
|
|
126
|
struct channel {
|
127
|
bbcd rx_freq[4];
|
128
|
bbcd tx_freq[4];
|
129
|
u8 rx_tone;
|
130
|
u8 rx_tmode_extra:6,
|
131
|
rx_tmode:2;
|
132
|
u8 tx_tone;
|
133
|
u8 tx_tmode_extra:6,
|
134
|
tx_tmode:2;
|
135
|
u8 unknown5;
|
136
|
u8 pttidoff:1,
|
137
|
dtmfoff:1,
|
138
|
%(unknownormode)s,
|
139
|
tailcut:1,
|
140
|
aliasop:1,
|
141
|
talkaroundoff:1,
|
142
|
voxoff:1,
|
143
|
skip:1;
|
144
|
u8 %(modeorpower)s,
|
145
|
reverseoff:1,
|
146
|
blckoff:1,
|
147
|
unknown7:1,
|
148
|
apro:3;
|
149
|
u8 unknown8;
|
150
|
};
|
151
|
|
152
|
struct name {
|
153
|
char name[7];
|
154
|
u8 pad;
|
155
|
};
|
156
|
|
157
|
#seekto 0x%(chanstart)x;
|
158
|
struct channel default[%(defaults)i];
|
159
|
struct channel memory[199];
|
160
|
|
161
|
#seekto 0x%(namestart)x;
|
162
|
struct name defaultname[%(defaults)i];
|
163
|
struct name name[199];
|
164
|
"""
|
165
|
|
166
|
|
167
|
APO_LIST = ["OFF", "10M", "20M", "30M", "40M", "50M", "60M", "90M",
|
168
|
"2H", "4H", "6H", "8H", "10H", "12H", "14H", "16H"]
|
169
|
SQL_LIST = ["%s" % x for x in range(0, 10)]
|
170
|
SCANM_LIST = ["CO", "TO"]
|
171
|
TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(10, 130, 10)]
|
172
|
_STEP_LIST = [2.5, 5., 6.25, 10., 12.5, 25.]
|
173
|
STEP_LIST = ["{} kHz".format(x) for x in _STEP_LIST]
|
174
|
MONITOR_LIST = ["CTC/DCS", "DTMF", "CTC/DCS and DTMF", "CTC/DCS or DTMF"]
|
175
|
VFOMR_LIST = ["MR", "VFO"]
|
176
|
MRCHA_LIST = ["MR CHA", "Freq. MR"]
|
177
|
VOL_LIST = ["OFF"] + ["%s" % x for x in range(1, 16)]
|
178
|
OPENDIS_LIST = ["All", "Lease Time", "User-defined", "Leixen"]
|
179
|
LAMP_LIST = ["OFF", "KEY", "CONT"]
|
180
|
KEYLOCKM_LIST = ["K+S", "PTT", "KEY", "ALL"]
|
181
|
ABSEL_LIST = ["B Channel", "A Channel"]
|
182
|
VOXGAIN_LIST = ["%s" % x for x in range(1, 9)]
|
183
|
VOXDT_LIST = ["%s seconds" % x for x in range(1, 5)]
|
184
|
DTMFTIME_LIST = ["%i milliseconds" % x for x in range(50, 210, 10)]
|
185
|
DTMFDELAY_LIST = ["%i milliseconds" % x for x in range(0, 550, 50)]
|
186
|
DTMFPRETIME_LIST = ["%i milliseconds" % x for x in range(100, 1100, 100)]
|
187
|
DTMFDELAY2_LIST = ["%i milliseconds" % x for x in range(0, 450, 50)]
|
188
|
|
189
|
LPTIME_LIST = ["%i milliseconds" % x for x in range(500, 2600, 100)]
|
190
|
PFKEYLONG_LIST = ["OFF",
|
191
|
"FM",
|
192
|
"Monitor Momentary",
|
193
|
"Monitor Lock",
|
194
|
"SQ Off Momentary",
|
195
|
"Mute",
|
196
|
"SCAN",
|
197
|
"TX Power",
|
198
|
"EMG",
|
199
|
"VFO/MR",
|
200
|
"DTMF",
|
201
|
"CALL",
|
202
|
"Transmit 1750 Hz",
|
203
|
"A/B",
|
204
|
"Talk Around",
|
205
|
"Reverse"
|
206
|
]
|
207
|
|
208
|
PFKEYSHORT_LIST = ["OFF",
|
209
|
"FM",
|
210
|
"BandChange",
|
211
|
"Time",
|
212
|
"Monitor Lock",
|
213
|
"Mute",
|
214
|
"SCAN",
|
215
|
"TX Power",
|
216
|
"EMG",
|
217
|
"VFO/MR",
|
218
|
"DTMF",
|
219
|
"CALL",
|
220
|
"Transmit 1750 Hz",
|
221
|
"A/B",
|
222
|
"Talk Around",
|
223
|
"Reverse"
|
224
|
]
|
225
|
|
226
|
MODES = ["NFM", "FM"]
|
227
|
WTFTONES = tuple(float(x) for x in range(56, 64))
|
228
|
TONES = tuple(sorted(WTFTONES + chirp_common.TONES))
|
229
|
DTCS_CODES = tuple(sorted((17, 50, 645) + chirp_common.DTCS_CODES))
|
230
|
TMODES = ["", "Tone", "DTCS", "DTCS"]
|
231
|
|
232
|
|
233
|
def _image_ident_from_data(data):
|
234
|
return data[0x168:0x178]
|
235
|
|
236
|
|
237
|
def _image_ident_from_image(radio):
|
238
|
return _image_ident_from_data(radio.get_mmap())
|
239
|
|
240
|
|
241
|
def checksum(frame):
|
242
|
x = 0
|
243
|
for b in frame:
|
244
|
x ^= b
|
245
|
return x & 0xFF
|
246
|
|
247
|
|
248
|
def make_frame(cmd, addr, data=b""):
|
249
|
payload = struct.pack(">H", addr) + data
|
250
|
header = struct.pack(">cB", cmd, len(payload))
|
251
|
frame = header + payload
|
252
|
return frame + bytes([checksum(frame)])
|
253
|
|
254
|
|
255
|
def send(radio, frame):
|
256
|
# LOG.debug("%04i P>R: %s" %
|
257
|
# (len(frame),
|
258
|
# util.hexprint(frame).replace("\n", "\n ")))
|
259
|
try:
|
260
|
radio.pipe.write(frame)
|
261
|
except Exception as e:
|
262
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
263
|
|
264
|
|
265
|
def recv(radio, readdata=True):
|
266
|
hdr = radio.pipe.read(4)
|
267
|
# LOG.debug("%04i P<R: %s" %
|
268
|
# (len(hdr), util.hexprint(hdr).replace("\n", "\n ")))
|
269
|
if hdr == b"\x09\x00\x09":
|
270
|
raise errors.RadioError("Radio rejected command.")
|
271
|
cmd, length, addr = struct.unpack(">BBH", hdr)
|
272
|
length -= 2
|
273
|
if readdata:
|
274
|
data = radio.pipe.read(length)
|
275
|
# LOG.debug(" P<R: %s" %
|
276
|
# util.hexprint(hdr + data).replace("\n", "\n "))
|
277
|
if len(data) != length:
|
278
|
raise errors.RadioError("Radio sent %i bytes (expected %i)" % (
|
279
|
len(data), length))
|
280
|
chk = radio.pipe.read(1)
|
281
|
else:
|
282
|
data = b""
|
283
|
return addr, data
|
284
|
|
285
|
|
286
|
def do_ident(radio):
|
287
|
send(radio, b"\x02\x06LEIXEN\x17")
|
288
|
ident = radio.pipe.read(9)
|
289
|
LOG.debug(" P<R: %s" %
|
290
|
util.hexprint(ident).replace("\n", "\n "))
|
291
|
if ident != b"\x06\x06leixen\x13":
|
292
|
raise errors.RadioError("Radio refused program mode")
|
293
|
radio.pipe.write(b"\x06\x00\x06")
|
294
|
ack = radio.pipe.read(3)
|
295
|
if ack != b"\x06\x00\x06":
|
296
|
raise errors.RadioError("Radio did not ack.")
|
297
|
|
298
|
|
299
|
def do_download(radio):
|
300
|
# Ident should have already been done by the detect_from_serial()
|
301
|
|
302
|
data = b""
|
303
|
data += b"\xFF" * (0 - len(data))
|
304
|
for addr in range(0, radio._memsize, 0x10):
|
305
|
send(radio, make_frame(b"R", addr, b'\x10'))
|
306
|
_addr, _data = recv(radio)
|
307
|
if _addr != addr:
|
308
|
raise errors.RadioError("Radio sent unexpected address")
|
309
|
data += _data
|
310
|
|
311
|
status = chirp_common.Status()
|
312
|
status.cur = addr
|
313
|
status.max = radio._memsize
|
314
|
status.msg = "Cloning from radio"
|
315
|
radio.status_fn(status)
|
316
|
|
317
|
finish(radio)
|
318
|
|
319
|
return memmap.MemoryMapBytes(data)
|
320
|
|
321
|
|
322
|
def do_upload(radio):
|
323
|
_ranges = [(0x0d00, 0x2000)]
|
324
|
|
325
|
image_ident = _image_ident_from_image(radio)
|
326
|
if image_ident.startswith(radio._file_ident) and \
|
327
|
radio._model_ident in image_ident:
|
328
|
_ranges = radio._ranges
|
329
|
|
330
|
do_ident(radio)
|
331
|
|
332
|
for start, end in _ranges:
|
333
|
LOG.debug('Uploading range 0x%04X - 0x%04X' % (start, end))
|
334
|
for addr in range(start, end, 0x10):
|
335
|
frame = make_frame(b"W", addr, radio._mmap[addr:addr + 0x10])
|
336
|
send(radio, frame)
|
337
|
# LOG.debug(" P<R: %s" %
|
338
|
# util.hexprint(frame).replace("\n", "\n "))
|
339
|
radio.pipe.write(b"\x06\x00\x06")
|
340
|
ack = radio.pipe.read(3)
|
341
|
if ack != b"\x06\x00\x06":
|
342
|
raise errors.RadioError("Radio refused block at %04x" % addr)
|
343
|
|
344
|
status = chirp_common.Status()
|
345
|
status.cur = addr
|
346
|
status.max = radio._memsize
|
347
|
status.msg = "Cloning to radio"
|
348
|
radio.status_fn(status)
|
349
|
|
350
|
finish(radio)
|
351
|
|
352
|
|
353
|
def finish(radio):
|
354
|
send(radio, b"\x64\x01\x6F\x0A")
|
355
|
ack = radio.pipe.read(8)
|
356
|
|
357
|
|
358
|
@directory.register
|
359
|
class LeixenVV898Radio(chirp_common.CloneModeRadio):
|
360
|
|
361
|
"""Leixen VV-898"""
|
362
|
VENDOR = "Leixen"
|
363
|
MODEL = "VV-898"
|
364
|
BAUD_RATE = 9600
|
365
|
NEEDS_COMPAT_SERIAL = False
|
366
|
|
367
|
_file_ident = b"Leixen"
|
368
|
_model_ident = b'LX-\x89\x85\x63'
|
369
|
|
370
|
_memsize = 0x2000
|
371
|
_ranges = [
|
372
|
(0x0000, 0x013f),
|
373
|
(0x0148, 0x0167),
|
374
|
(0x0184, 0x018f),
|
375
|
(0x0190, 0x01cf),
|
376
|
(0x0900, 0x090f),
|
377
|
(0x0920, 0x0927),
|
378
|
(0x0d00, 0x2000),
|
379
|
]
|
380
|
|
381
|
_mem_formatter = {'unknownormode': 'unknown6:1',
|
382
|
'modeorpower': 'mode:1, power:1',
|
383
|
'chanstart': 0x0D00,
|
384
|
'namestart': 0x19B0,
|
385
|
'defaults': 3}
|
386
|
_power_levels = [chirp_common.PowerLevel("Low", watts=4),
|
387
|
chirp_common.PowerLevel("High", watts=10)]
|
388
|
|
389
|
@classmethod
|
390
|
def detect_from_serial(cls, pipe):
|
391
|
radio = cls(pipe)
|
392
|
do_ident(radio)
|
393
|
send(radio, make_frame(b"R", 0x0168, b'\x10'))
|
394
|
_addr, _data = recv(radio)
|
395
|
ident = _data[8:14]
|
396
|
LOG.debug('Got ident from radio:\n%s' % util.hexprint(ident))
|
397
|
for rclass in [cls] + (cls.DETECTED_MODELS or []):
|
398
|
if ident == rclass._model_ident:
|
399
|
return rclass
|
400
|
# Reset the radio if we didn't find a match
|
401
|
finish(radio)
|
402
|
raise errors.RadioError('Unable to detect a supported model')
|
403
|
|
404
|
def get_features(self):
|
405
|
rf = chirp_common.RadioFeatures()
|
406
|
rf.has_settings = True
|
407
|
rf.has_cross = True
|
408
|
rf.has_bank = False
|
409
|
rf.has_tuning_step = False
|
410
|
rf.can_odd_split = True
|
411
|
rf.has_rx_dtcs = True
|
412
|
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
|
413
|
rf.valid_modes = MODES
|
414
|
rf.valid_cross_modes = [
|
415
|
"Tone->Tone",
|
416
|
"DTCS->",
|
417
|
"->DTCS",
|
418
|
"Tone->DTCS",
|
419
|
"DTCS->Tone",
|
420
|
"->Tone",
|
421
|
"DTCS->DTCS"]
|
422
|
rf.valid_characters = chirp_common.CHARSET_ASCII
|
423
|
rf.valid_name_length = 7
|
424
|
rf.valid_power_levels = self._power_levels
|
425
|
rf.valid_duplexes = ["", "-", "+", "split", "off"]
|
426
|
rf.valid_skips = ["", "S"]
|
427
|
rf.valid_tuning_steps = _STEP_LIST
|
428
|
rf.valid_bands = [(136000000, 174000000),
|
429
|
(400000000, 480000000)]
|
430
|
rf.valid_tones = TONES
|
431
|
rf.valid_dtcs_codes = DTCS_CODES
|
432
|
rf.memory_bounds = (1, 199)
|
433
|
return rf
|
434
|
|
435
|
def sync_in(self):
|
436
|
try:
|
437
|
self._mmap = do_download(self)
|
438
|
except Exception as e:
|
439
|
finish(self)
|
440
|
raise errors.RadioError("Failed to download from radio: %s" % e)
|
441
|
self.process_mmap()
|
442
|
|
443
|
def process_mmap(self):
|
444
|
self._memobj = bitwise.parse(
|
445
|
MEM_FORMAT % self._mem_formatter, self._mmap)
|
446
|
|
447
|
def sync_out(self):
|
448
|
try:
|
449
|
do_upload(self)
|
450
|
except errors.RadioError:
|
451
|
finish(self)
|
452
|
raise
|
453
|
except Exception as e:
|
454
|
raise errors.RadioError("Failed to upload to radio: %s" % e)
|
455
|
|
456
|
def get_raw_memory(self, number):
|
457
|
name, mem = self._get_memobjs(number)
|
458
|
return repr(name) + repr(mem)
|
459
|
|
460
|
def _get_tone(self, mem, _mem):
|
461
|
rx_tone = tx_tone = None
|
462
|
|
463
|
tx_tmode = TMODES[_mem.tx_tmode]
|
464
|
rx_tmode = TMODES[_mem.rx_tmode]
|
465
|
|
466
|
if tx_tmode == "Tone":
|
467
|
tx_tone = TONES[_mem.tx_tone - 1]
|
468
|
elif tx_tmode == "DTCS":
|
469
|
tx_tone = DTCS_CODES[_mem.tx_tone - 1]
|
470
|
|
471
|
if rx_tmode == "Tone":
|
472
|
rx_tone = TONES[_mem.rx_tone - 1]
|
473
|
elif rx_tmode == "DTCS":
|
474
|
rx_tone = DTCS_CODES[_mem.rx_tone - 1]
|
475
|
|
476
|
tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
|
477
|
rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
|
478
|
|
479
|
chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
|
480
|
(rx_tmode, rx_tone, rx_pol))
|
481
|
|
482
|
def _is_txinh(self, _mem):
|
483
|
raw_tx = b""
|
484
|
for i in range(0, 4):
|
485
|
raw_tx += _mem.tx_freq[i].get_raw()
|
486
|
return raw_tx == b"\xFF\xFF\xFF\xFF"
|
487
|
|
488
|
def _get_memobjs(self, number):
|
489
|
_mem = self._memobj.memory[number - 1]
|
490
|
_name = self._memobj.name[number - 1]
|
491
|
return _mem, _name
|
492
|
|
493
|
def get_memory(self, number):
|
494
|
_mem, _name = self._get_memobjs(number)
|
495
|
|
496
|
mem = chirp_common.Memory()
|
497
|
mem.number = number
|
498
|
|
499
|
if _mem.get_raw()[:4] == b"\xFF\xFF\xFF\xFF":
|
500
|
mem.empty = True
|
501
|
return mem
|
502
|
|
503
|
mem.freq = int(_mem.rx_freq) * 10
|
504
|
|
505
|
if self._is_txinh(_mem):
|
506
|
mem.duplex = "off"
|
507
|
mem.offset = 0
|
508
|
elif int(_mem.rx_freq) == int(_mem.tx_freq):
|
509
|
mem.duplex = ""
|
510
|
mem.offset = 0
|
511
|
elif abs(int(_mem.rx_freq) * 10 - int(_mem.tx_freq) * 10) > 70000000:
|
512
|
mem.duplex = "split"
|
513
|
mem.offset = int(_mem.tx_freq) * 10
|
514
|
else:
|
515
|
mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+"
|
516
|
mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
|
517
|
|
518
|
mem.name = str(_name.name).rstrip()
|
519
|
|
520
|
self._get_tone(mem, _mem)
|
521
|
mem.mode = MODES[_mem.mode]
|
522
|
powerindex = _mem.power if _mem.power < len(self._power_levels) else -1
|
523
|
mem.power = self._power_levels[powerindex]
|
524
|
mem.skip = _mem.skip and "S" or ""
|
525
|
|
526
|
mem.extra = RadioSettingGroup("Extra", "extra")
|
527
|
|
528
|
opts = ["On", "Off"]
|
529
|
rs = RadioSetting("blckoff", "Busy Channel Lockout",
|
530
|
RadioSettingValueList(
|
531
|
opts, opts[_mem.blckoff]))
|
532
|
mem.extra.append(rs)
|
533
|
opts = ["Off", "On"]
|
534
|
rs = RadioSetting("tailcut", "Squelch Tail Elimination",
|
535
|
RadioSettingValueList(
|
536
|
opts, opts[_mem.tailcut]))
|
537
|
mem.extra.append(rs)
|
538
|
apro = _mem.apro if _mem.apro < 0x5 else 0
|
539
|
opts = ["Off", "Compander", "Scrambler", "TX Scrambler",
|
540
|
"RX Scrambler"]
|
541
|
rs = RadioSetting("apro", "Audio Processing",
|
542
|
RadioSettingValueList(
|
543
|
opts, opts[apro]))
|
544
|
mem.extra.append(rs)
|
545
|
opts = ["On", "Off"]
|
546
|
rs = RadioSetting("voxoff", "VOX",
|
547
|
RadioSettingValueList(
|
548
|
opts, opts[_mem.voxoff]))
|
549
|
mem.extra.append(rs)
|
550
|
opts = ["On", "Off"]
|
551
|
rs = RadioSetting("pttidoff", "PTT ID",
|
552
|
RadioSettingValueList(
|
553
|
opts, opts[_mem.pttidoff]))
|
554
|
mem.extra.append(rs)
|
555
|
opts = ["On", "Off"]
|
556
|
rs = RadioSetting("dtmfoff", "DTMF",
|
557
|
RadioSettingValueList(
|
558
|
opts, opts[_mem.dtmfoff]))
|
559
|
mem.extra.append(rs)
|
560
|
opts = ["Name", "Frequency"]
|
561
|
aliasop = RadioSetting("aliasop", "Display",
|
562
|
RadioSettingValueList(
|
563
|
opts, opts[_mem.aliasop]))
|
564
|
mem.extra.append(aliasop)
|
565
|
opts = ["On", "Off"]
|
566
|
rs = RadioSetting("reverseoff", "Reverse Frequency",
|
567
|
RadioSettingValueList(
|
568
|
opts, opts[_mem.reverseoff]))
|
569
|
mem.extra.append(rs)
|
570
|
opts = ["On", "Off"]
|
571
|
rs = RadioSetting("talkaroundoff", "Talk Around",
|
572
|
RadioSettingValueList(
|
573
|
opts, opts[_mem.talkaroundoff]))
|
574
|
mem.extra.append(rs)
|
575
|
|
576
|
return mem
|
577
|
|
578
|
def _set_tone(self, mem, _mem):
|
579
|
((txmode, txtone, txpol),
|
580
|
(rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
|
581
|
|
582
|
_mem.tx_tmode = TMODES.index(txmode)
|
583
|
_mem.rx_tmode = TMODES.index(rxmode)
|
584
|
if txmode == "Tone":
|
585
|
_mem.tx_tone = TONES.index(txtone) + 1
|
586
|
elif txmode == "DTCS":
|
587
|
_mem.tx_tmode = txpol == "R" and 0x03 or 0x02
|
588
|
_mem.tx_tone = DTCS_CODES.index(txtone) + 1
|
589
|
if rxmode == "Tone":
|
590
|
_mem.rx_tone = TONES.index(rxtone) + 1
|
591
|
elif rxmode == "DTCS":
|
592
|
_mem.rx_tmode = rxpol == "R" and 0x03 or 0x02
|
593
|
_mem.rx_tone = DTCS_CODES.index(rxtone) + 1
|
594
|
|
595
|
def set_memory(self, mem):
|
596
|
_mem, _name = self._get_memobjs(mem.number)
|
597
|
|
598
|
if mem.empty:
|
599
|
_mem.set_raw(b"\xFF" * 16)
|
600
|
return
|
601
|
elif _mem.get_raw() == (b"\xFF" * 16):
|
602
|
_mem.set_raw(b"\xFF" * 8 + b"\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC")
|
603
|
|
604
|
_mem.rx_freq = mem.freq / 10
|
605
|
|
606
|
if mem.duplex == "off":
|
607
|
for i in range(0, 4):
|
608
|
_mem.tx_freq[i].set_raw(b"\xFF")
|
609
|
elif mem.duplex == "split":
|
610
|
_mem.tx_freq = mem.offset / 10
|
611
|
elif mem.duplex == "+":
|
612
|
_mem.tx_freq = (mem.freq + mem.offset) / 10
|
613
|
elif mem.duplex == "-":
|
614
|
_mem.tx_freq = (mem.freq - mem.offset) / 10
|
615
|
else:
|
616
|
_mem.tx_freq = mem.freq / 10
|
617
|
|
618
|
self._set_tone(mem, _mem)
|
619
|
|
620
|
_mem.power = mem.power and self._power_levels.index(mem.power) or 0
|
621
|
_mem.mode = MODES.index(mem.mode)
|
622
|
_mem.skip = mem.skip == "S"
|
623
|
_name.name = mem.name.ljust(7)
|
624
|
|
625
|
# autoset display to name if filled, else show frequency
|
626
|
if mem.extra:
|
627
|
# mem.extra only seems to be populated when called from edit panel
|
628
|
aliasop = mem.extra["aliasop"]
|
629
|
else:
|
630
|
aliasop = None
|
631
|
if mem.name:
|
632
|
_mem.aliasop = False
|
633
|
else:
|
634
|
_mem.aliasop = True
|
635
|
|
636
|
for setting in mem.extra:
|
637
|
setattr(_mem, setting.get_name(), setting.value)
|
638
|
|
639
|
def _get_settings(self):
|
640
|
_settings = self._memobj.settings
|
641
|
_service = self._memobj.service
|
642
|
_msg = self._memobj.messages
|
643
|
cfg_grp = RadioSettingGroup("cfg_grp", "Basic Settings")
|
644
|
adv_grp = RadioSettingGroup("adv_grp", "Advanced Settings")
|
645
|
key_grp = RadioSettingGroup("key_grp", "Key Assignment")
|
646
|
group = RadioSettings(cfg_grp, adv_grp, key_grp)
|
647
|
|
648
|
#
|
649
|
# Basic Settings
|
650
|
#
|
651
|
rs = RadioSetting("apo", "Auto Power Off",
|
652
|
RadioSettingValueList(
|
653
|
APO_LIST, APO_LIST[_settings.apo]))
|
654
|
cfg_grp.append(rs)
|
655
|
rs = RadioSetting("sql", "Squelch Level",
|
656
|
RadioSettingValueList(
|
657
|
SQL_LIST, SQL_LIST[_settings.sql]))
|
658
|
cfg_grp.append(rs)
|
659
|
rs = RadioSetting("scanm", "Scan Mode",
|
660
|
RadioSettingValueList(
|
661
|
SCANM_LIST, SCANM_LIST[_settings.scanm]))
|
662
|
cfg_grp.append(rs)
|
663
|
rs = RadioSetting("tot", "Time Out Timer",
|
664
|
RadioSettingValueList(
|
665
|
TOT_LIST, TOT_LIST[_settings.tot]))
|
666
|
cfg_grp.append(rs)
|
667
|
rs = RadioSetting("step", "Step",
|
668
|
RadioSettingValueList(
|
669
|
STEP_LIST, STEP_LIST[_settings.step]))
|
670
|
cfg_grp.append(rs)
|
671
|
rs = RadioSetting("monitor", "Monitor",
|
672
|
RadioSettingValueList(
|
673
|
MONITOR_LIST, MONITOR_LIST[_settings.monitor]))
|
674
|
cfg_grp.append(rs)
|
675
|
rs = RadioSetting("vfomr", "VFO/MR",
|
676
|
RadioSettingValueList(
|
677
|
VFOMR_LIST, VFOMR_LIST[_settings.vfomr]))
|
678
|
cfg_grp.append(rs)
|
679
|
rs = RadioSetting("mrcha", "MR/CHA",
|
680
|
RadioSettingValueList(
|
681
|
MRCHA_LIST, MRCHA_LIST[_settings.mrcha]))
|
682
|
cfg_grp.append(rs)
|
683
|
rs = RadioSetting("vol", "Volume",
|
684
|
RadioSettingValueList(
|
685
|
VOL_LIST, VOL_LIST[_settings.vol]))
|
686
|
cfg_grp.append(rs)
|
687
|
rs = RadioSetting("opendis", "Open Display",
|
688
|
RadioSettingValueList(
|
689
|
OPENDIS_LIST, OPENDIS_LIST[_settings.opendis]))
|
690
|
cfg_grp.append(rs)
|
691
|
|
692
|
def _filter(name):
|
693
|
filtered = ""
|
694
|
for char in str(name):
|
695
|
if char in chirp_common.CHARSET_ASCII:
|
696
|
filtered += char
|
697
|
else:
|
698
|
filtered += " "
|
699
|
LOG.debug("Filtered: %s" % filtered)
|
700
|
return filtered
|
701
|
|
702
|
rs = RadioSetting("messages.user1", "User-defined Message 1",
|
703
|
RadioSettingValueString(0, 7, _filter(_msg.user1)))
|
704
|
cfg_grp.append(rs)
|
705
|
rs = RadioSetting("messages.user2", "User-defined Message 2",
|
706
|
RadioSettingValueString(0, 7, _filter(_msg.user2)))
|
707
|
cfg_grp.append(rs)
|
708
|
|
709
|
val = RadioSettingValueString(0, 7, _filter(_msg.system))
|
710
|
val.set_mutable(False)
|
711
|
rs = RadioSetting("messages.system", "System Message", val)
|
712
|
cfg_grp.append(rs)
|
713
|
|
714
|
rs = RadioSetting("lamp", "Backlight",
|
715
|
RadioSettingValueList(
|
716
|
LAMP_LIST, LAMP_LIST[_settings.lamp]))
|
717
|
cfg_grp.append(rs)
|
718
|
rs = RadioSetting("keylockm", "Key Lock Mode",
|
719
|
RadioSettingValueList(
|
720
|
KEYLOCKM_LIST,
|
721
|
KEYLOCKM_LIST[_settings.keylockm]))
|
722
|
cfg_grp.append(rs)
|
723
|
rs = RadioSetting("absel", "A/B Select",
|
724
|
RadioSettingValueList(ABSEL_LIST,
|
725
|
ABSEL_LIST[_settings.absel]))
|
726
|
cfg_grp.append(rs)
|
727
|
|
728
|
rs = RadioSetting("obeep", "Open Beep",
|
729
|
RadioSettingValueBoolean(_settings.obeep))
|
730
|
cfg_grp.append(rs)
|
731
|
rs = RadioSetting("rbeep", "Roger Beep",
|
732
|
RadioSettingValueBoolean(_settings.rbeep))
|
733
|
cfg_grp.append(rs)
|
734
|
rs = RadioSetting("keylock_off", "Key Lock",
|
735
|
RadioSettingValueBoolean(not _settings.keylock_off))
|
736
|
cfg_grp.append(rs)
|
737
|
rs = RadioSetting("ctdcsb", "CT/DCS Busy Lock",
|
738
|
RadioSettingValueBoolean(_settings.ctdcsb))
|
739
|
cfg_grp.append(rs)
|
740
|
rs = RadioSetting("alarm", "Alarm Key",
|
741
|
RadioSettingValueBoolean(_settings.alarm))
|
742
|
cfg_grp.append(rs)
|
743
|
rs = RadioSetting("save", "Battery Save",
|
744
|
RadioSettingValueBoolean(_settings.save))
|
745
|
cfg_grp.append(rs)
|
746
|
rs = RadioSetting("kbeep", "Key Beep",
|
747
|
RadioSettingValueBoolean(_settings.kbeep))
|
748
|
cfg_grp.append(rs)
|
749
|
rs = RadioSetting("reset", "Reset Enable",
|
750
|
RadioSettingValueBoolean(_settings.reset))
|
751
|
cfg_grp.append(rs)
|
752
|
rs = RadioSetting("smfont_off", "Small Font",
|
753
|
RadioSettingValueBoolean(not _settings.smfont_off))
|
754
|
cfg_grp.append(rs)
|
755
|
rs = RadioSetting("aliasen_off", "Alias Enable",
|
756
|
RadioSettingValueBoolean(not _settings.aliasen_off))
|
757
|
cfg_grp.append(rs)
|
758
|
rs = RadioSetting("txstop_off", "TX Stop",
|
759
|
RadioSettingValueBoolean(not _settings.txstop_off))
|
760
|
cfg_grp.append(rs)
|
761
|
rs = RadioSetting("dw_off", "Dual Watch",
|
762
|
RadioSettingValueBoolean(not _settings.dw_off))
|
763
|
cfg_grp.append(rs)
|
764
|
rs = RadioSetting("fmen_off", "FM Enable",
|
765
|
RadioSettingValueBoolean(not _settings.fmen_off))
|
766
|
cfg_grp.append(rs)
|
767
|
rs = RadioSetting("fmdw", "FM Dual Watch",
|
768
|
RadioSettingValueBoolean(_settings.fmdw))
|
769
|
cfg_grp.append(rs)
|
770
|
rs = RadioSetting("fmscan_off", "FM Scan",
|
771
|
RadioSettingValueBoolean(
|
772
|
not _settings.fmscan_off))
|
773
|
cfg_grp.append(rs)
|
774
|
rs = RadioSetting("keypadmic_off", "Keypad MIC",
|
775
|
RadioSettingValueBoolean(
|
776
|
not _settings.keypadmic_off))
|
777
|
cfg_grp.append(rs)
|
778
|
rs = RadioSetting("voxgain", "VOX Gain",
|
779
|
RadioSettingValueList(
|
780
|
VOXGAIN_LIST, VOXGAIN_LIST[_settings.voxgain]))
|
781
|
cfg_grp.append(rs)
|
782
|
rs = RadioSetting("voxdt", "VOX Delay Time",
|
783
|
RadioSettingValueList(
|
784
|
VOXDT_LIST, VOXDT_LIST[_settings.voxdt]))
|
785
|
cfg_grp.append(rs)
|
786
|
rs = RadioSetting("vir", "VOX Inhibit on Receive",
|
787
|
RadioSettingValueBoolean(_settings.vir))
|
788
|
cfg_grp.append(rs)
|
789
|
|
790
|
#
|
791
|
# Advanced Settings
|
792
|
#
|
793
|
val = (_settings.dtmftime) - 5
|
794
|
rs = RadioSetting("dtmftime", "DTMF Digit Time",
|
795
|
RadioSettingValueList(
|
796
|
DTMFTIME_LIST, DTMFTIME_LIST[val]))
|
797
|
adv_grp.append(rs)
|
798
|
val = (_settings.dtmfspace) - 5
|
799
|
rs = RadioSetting("dtmfspace", "DTMF Digit Space Time",
|
800
|
RadioSettingValueList(
|
801
|
DTMFTIME_LIST, DTMFTIME_LIST[val]))
|
802
|
adv_grp.append(rs)
|
803
|
val = (_settings.dtmfdelay) // 5
|
804
|
rs = RadioSetting("dtmfdelay", "DTMF 1st Digit Delay",
|
805
|
RadioSettingValueList(
|
806
|
DTMFDELAY_LIST, DTMFDELAY_LIST[val]))
|
807
|
adv_grp.append(rs)
|
808
|
val = (_settings.dtmfpretime) // 10 - 1
|
809
|
rs = RadioSetting("dtmfpretime", "DTMF Pretime",
|
810
|
RadioSettingValueList(
|
811
|
DTMFPRETIME_LIST, DTMFPRETIME_LIST[val]))
|
812
|
adv_grp.append(rs)
|
813
|
val = (_settings.dtmfdelay2) // 5
|
814
|
rs = RadioSetting("dtmfdelay2", "DTMF * and # Digit Delay",
|
815
|
RadioSettingValueList(
|
816
|
DTMFDELAY2_LIST, DTMFDELAY2_LIST[val]))
|
817
|
adv_grp.append(rs)
|
818
|
rs = RadioSetting("ackdecode", "ACK Decode",
|
819
|
RadioSettingValueBoolean(_settings.ackdecode))
|
820
|
adv_grp.append(rs)
|
821
|
rs = RadioSetting("dtmfst", "DTMF Sidetone",
|
822
|
RadioSettingValueBoolean(_settings.dtmfst))
|
823
|
adv_grp.append(rs)
|
824
|
|
825
|
rs = RadioSetting("service.rssi400", "Squelch Base Level (UHF)",
|
826
|
RadioSettingValueInteger(0, 255, _service.rssi400))
|
827
|
adv_grp.append(rs)
|
828
|
rs = RadioSetting("service.rssi136", "Squelch Base Level (VHF)",
|
829
|
RadioSettingValueInteger(0, 255, _service.rssi136))
|
830
|
adv_grp.append(rs)
|
831
|
|
832
|
#
|
833
|
# Key Settings
|
834
|
#
|
835
|
val = (_settings.lptime) - 5
|
836
|
rs = RadioSetting("lptime", "Long Press Time",
|
837
|
RadioSettingValueList(
|
838
|
LPTIME_LIST, LPTIME_LIST[val]))
|
839
|
key_grp.append(rs)
|
840
|
rs = RadioSetting("keyp1long", "P1 Long Key",
|
841
|
RadioSettingValueList(
|
842
|
PFKEYLONG_LIST,
|
843
|
PFKEYLONG_LIST[_settings.keyp1long]))
|
844
|
key_grp.append(rs)
|
845
|
rs = RadioSetting("keyp1short", "P1 Short Key",
|
846
|
RadioSettingValueList(
|
847
|
PFKEYSHORT_LIST,
|
848
|
PFKEYSHORT_LIST[_settings.keyp1short]))
|
849
|
key_grp.append(rs)
|
850
|
rs = RadioSetting("keyp2long", "P2 Long Key",
|
851
|
RadioSettingValueList(
|
852
|
PFKEYLONG_LIST,
|
853
|
PFKEYLONG_LIST[_settings.keyp2long]))
|
854
|
key_grp.append(rs)
|
855
|
rs = RadioSetting("keyp2short", "P2 Short Key",
|
856
|
RadioSettingValueList(
|
857
|
PFKEYSHORT_LIST,
|
858
|
PFKEYSHORT_LIST[_settings.keyp2short]))
|
859
|
key_grp.append(rs)
|
860
|
rs = RadioSetting("keyp3long", "P3 Long Key",
|
861
|
RadioSettingValueList(
|
862
|
PFKEYLONG_LIST,
|
863
|
PFKEYLONG_LIST[_settings.keyp3long]))
|
864
|
key_grp.append(rs)
|
865
|
rs = RadioSetting("keyp3short", "P3 Short Key",
|
866
|
RadioSettingValueList(
|
867
|
PFKEYSHORT_LIST,
|
868
|
PFKEYSHORT_LIST[_settings.keyp3short]))
|
869
|
key_grp.append(rs)
|
870
|
|
871
|
val = RadioSettingValueList(PFKEYSHORT_LIST,
|
872
|
PFKEYSHORT_LIST[_settings.keymshort])
|
873
|
val.set_mutable(_settings.menuen == 0)
|
874
|
rs = RadioSetting("keymshort", "M Short Key", val)
|
875
|
key_grp.append(rs)
|
876
|
val = RadioSettingValueBoolean(_settings.menuen)
|
877
|
rs = RadioSetting("menuen", "Menu Enable", val)
|
878
|
key_grp.append(rs)
|
879
|
|
880
|
return group
|
881
|
|
882
|
def get_settings(self):
|
883
|
try:
|
884
|
return self._get_settings()
|
885
|
except:
|
886
|
import traceback
|
887
|
LOG.error("Failed to parse settings: %s", traceback.format_exc())
|
888
|
return None
|
889
|
|
890
|
def set_settings(self, settings):
|
891
|
_settings = self._memobj.settings
|
892
|
for element in settings:
|
893
|
if not isinstance(element, RadioSetting):
|
894
|
self.set_settings(element)
|
895
|
continue
|
896
|
else:
|
897
|
try:
|
898
|
name = element.get_name()
|
899
|
if "." in name:
|
900
|
bits = name.split(".")
|
901
|
obj = self._memobj
|
902
|
for bit in bits[:-1]:
|
903
|
if "/" in bit:
|
904
|
bit, index = bit.split("/", 1)
|
905
|
index = int(index)
|
906
|
obj = getattr(obj, bit)[index]
|
907
|
else:
|
908
|
obj = getattr(obj, bit)
|
909
|
setting = bits[-1]
|
910
|
else:
|
911
|
obj = _settings
|
912
|
setting = element.get_name()
|
913
|
|
914
|
if element.has_apply_callback():
|
915
|
LOG.debug("Using apply callback")
|
916
|
element.run_apply_callback()
|
917
|
elif setting == "keylock_off":
|
918
|
setattr(obj, setting, not int(element.value))
|
919
|
elif setting == "smfont_off":
|
920
|
setattr(obj, setting, not int(element.value))
|
921
|
elif setting == "aliasen_off":
|
922
|
setattr(obj, setting, not int(element.value))
|
923
|
elif setting == "txstop_off":
|
924
|
setattr(obj, setting, not int(element.value))
|
925
|
elif setting == "dw_off":
|
926
|
setattr(obj, setting, not int(element.value))
|
927
|
elif setting == "fmen_off":
|
928
|
setattr(obj, setting, not int(element.value))
|
929
|
elif setting == "fmscan_off":
|
930
|
setattr(obj, setting, not int(element.value))
|
931
|
elif setting == "keypadmic_off":
|
932
|
setattr(obj, setting, not int(element.value))
|
933
|
elif setting == "dtmftime":
|
934
|
setattr(obj, setting, int(element.value) + 5)
|
935
|
elif setting == "dtmfspace":
|
936
|
setattr(obj, setting, int(element.value) + 5)
|
937
|
elif setting == "dtmfdelay":
|
938
|
setattr(obj, setting, int(element.value) * 5)
|
939
|
elif setting == "dtmfpretime":
|
940
|
setattr(obj, setting, (int(element.value) + 1) * 10)
|
941
|
elif setting == "dtmfdelay2":
|
942
|
setattr(obj, setting, int(element.value) * 5)
|
943
|
elif setting == "lptime":
|
944
|
setattr(obj, setting, int(element.value) + 5)
|
945
|
else:
|
946
|
LOG.debug("Setting %s = %s" % (setting, element.value))
|
947
|
setattr(obj, setting, element.value)
|
948
|
except Exception:
|
949
|
LOG.debug(element.get_name())
|
950
|
raise
|
951
|
|
952
|
@classmethod
|
953
|
def match_model(cls, filedata, filename):
|
954
|
if filedata[0x168:0x170].startswith(cls._file_ident) and \
|
955
|
filedata[0x170:0x178].startswith(cls._model_ident):
|
956
|
return True
|
957
|
else:
|
958
|
return False
|
959
|
|
960
|
|
961
|
@directory.register
|
962
|
class JetstreamJT270MRadio(LeixenVV898Radio):
|
963
|
|
964
|
"""Jetstream JT270M"""
|
965
|
VENDOR = "Jetstream"
|
966
|
MODEL = "JT270M"
|
967
|
ALIASES = []
|
968
|
|
969
|
_file_ident = b"JET"
|
970
|
_model_ident = b'LX-\x89\x85\x53'
|
971
|
|
972
|
|
973
|
class LT898UV(LeixenVV898Radio):
|
974
|
VENDOR = "LUITON"
|
975
|
MODEL = "LT-898UV"
|
976
|
|
977
|
@classmethod
|
978
|
def match_model(cls, filedata, filename):
|
979
|
return False
|
980
|
|
981
|
|
982
|
@directory.register
|
983
|
class JetstreamJT270MHRadio(LeixenVV898Radio):
|
984
|
|
985
|
"""Jetstream JT270MH"""
|
986
|
VENDOR = "Jetstream"
|
987
|
MODEL = "JT270MH"
|
988
|
|
989
|
_file_ident = b"Leixen"
|
990
|
_model_ident = b'LX-\x89\x85\x85'
|
991
|
_ranges = [(0x0C00, 0x2000)]
|
992
|
_mem_formatter = {'unknownormode': 'mode:1',
|
993
|
'modeorpower': 'power:2',
|
994
|
'chanstart': 0x0C00,
|
995
|
'namestart': 0x1900,
|
996
|
'defaults': 6}
|
997
|
_power_levels = [chirp_common.PowerLevel("Low", watts=5),
|
998
|
chirp_common.PowerLevel("Mid", watts=10),
|
999
|
chirp_common.PowerLevel("High", watts=25)]
|
1000
|
# Base radio has offset zero to distinguish from sub devices
|
1001
|
_offset = 0
|
1002
|
|
1003
|
def get_features(self):
|
1004
|
rf = super(JetstreamJT270MHRadio, self).get_features()
|
1005
|
rf.has_sub_devices = self._offset == 0
|
1006
|
rf.memory_bounds = (1, 99)
|
1007
|
return rf
|
1008
|
|
1009
|
def get_sub_devices(self):
|
1010
|
return [JetstreamJT270MHRadioA(self._mmap),
|
1011
|
JetstreamJT270MHRadioB(self._mmap)]
|
1012
|
|
1013
|
def _get_memobjs(self, number):
|
1014
|
number = number * 2 - self._offset
|
1015
|
_mem = self._memobj.memory[number]
|
1016
|
_name = self._memobj.name[number]
|
1017
|
return _mem, _name
|
1018
|
|
1019
|
|
1020
|
class JetstreamJT270MHRadioA(JetstreamJT270MHRadio):
|
1021
|
VARIANT = 'A Band'
|
1022
|
_offset = 1
|
1023
|
|
1024
|
|
1025
|
class JetstreamJT270MHRadioB(JetstreamJT270MHRadio):
|
1026
|
VARIANT = 'B Band'
|
1027
|
_offset = 2
|
1028
|
|
1029
|
|
1030
|
@directory.register
|
1031
|
class LeixenVV898SRadio(LeixenVV898Radio):
|
1032
|
|
1033
|
"""Leixen VV-898S, also VV-898E which is identical"""
|
1034
|
VENDOR = "Leixen"
|
1035
|
MODEL = "VV-898S"
|
1036
|
|
1037
|
_model_ident = b'LX-\x89\x85\x75'
|
1038
|
_mem_formatter = {'unknownormode': 'mode:1',
|
1039
|
'modeorpower': 'power:2',
|
1040
|
'chanstart': 0x0D00,
|
1041
|
'namestart': 0x19B0,
|
1042
|
'defaults': 3}
|
1043
|
_power_levels = [chirp_common.PowerLevel("Low", watts=5),
|
1044
|
chirp_common.PowerLevel("Med", watts=10),
|
1045
|
chirp_common.PowerLevel("High", watts=25)]
|
1046
|
|
1047
|
|
1048
|
@directory.register
|
1049
|
class VV898E(LeixenVV898SRadio):
|
1050
|
'''Leixen has called this radio both 898E and S historically, ident is
|
1051
|
identical'''
|
1052
|
VENDOR = "Leixen"
|
1053
|
MODEL = "VV-898E"
|
1054
|
|
1055
|
@classmethod
|
1056
|
def match_model(cls, filedata, filename):
|
1057
|
return False
|
1058
|
|
1059
|
|
1060
|
@directory.register
|
1061
|
@directory.detected_by(LeixenVV898SRadio)
|
1062
|
class VV898SDualBank(JetstreamJT270MHRadio):
|
1063
|
'''Newer VV898S 1.06+ firmware that features dual memory banks'''
|
1064
|
VENDOR = "Leixen"
|
1065
|
MODEL = "VV-898S"
|
1066
|
VARIANT = "Dual Bank"
|
1067
|
|
1068
|
@classmethod
|
1069
|
def match_model(cls, filedata, filename):
|
1070
|
return False
|
1071
|
|
1072
|
|
1073
|
@directory.register
|
1074
|
@directory.detected_by(VV898E)
|
1075
|
class VV898EDualBank(JetstreamJT270MHRadio):
|
1076
|
'''Newer VV898E 1.06+ firmware that features dual memory banks'''
|
1077
|
VENDOR = "Leixen"
|
1078
|
MODEL = "VV-898E"
|
1079
|
VARIANT = "Dual Bank"
|
1080
|
|
1081
|
@classmethod
|
1082
|
def match_model(cls, filedata, filename):
|
1083
|
return False
|