1
|
# Copyright (c) 2022 <angelof9@protonmail.com>
|
2
|
#
|
3
|
# BSD 2-Clause "Simplified" License
|
4
|
# https://opensource.org/licenses/BSD-2-Clause
|
5
|
#
|
6
|
# Redistribution and use in source and binary forms,
|
7
|
# with or without modification, are permitted provided
|
8
|
# that the following conditions are met:
|
9
|
#
|
10
|
# 1. Redistributions of source code must retain the above
|
11
|
# copyright notice, this list of conditions and
|
12
|
# the following disclaimer.
|
13
|
# 2. Redistributions in binary form must reproduce the
|
14
|
# above copyright notice, this list of conditions and
|
15
|
# the following disclaimer in the documentation
|
16
|
# and/or other materials provided with the distribution.
|
17
|
#
|
18
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
20
|
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
21
|
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
22
|
# CONTRIBUTORS
|
23
|
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
24
|
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
25
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
27
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
28
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
29
|
# THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
|
31
|
# Python3 byte clean by Darryl Pogue
|
32
|
# Pep8 style copliant by Jim Unroe <rock.unroe@gmail.com>
|
33
|
|
34
|
import logging
|
35
|
import struct
|
36
|
|
37
|
from chirp import (
|
38
|
bitwise,
|
39
|
chirp_common,
|
40
|
directory,
|
41
|
errors,
|
42
|
memmap,
|
43
|
util,
|
44
|
)
|
45
|
|
46
|
from chirp.settings import (
|
47
|
RadioSetting,
|
48
|
RadioSettingGroup,
|
49
|
RadioSettings,
|
50
|
RadioSettingValueBoolean,
|
51
|
RadioSettingValueInteger,
|
52
|
RadioSettingValueList,
|
53
|
RadioSettingValueString,
|
54
|
RadioSettingValueMap,
|
55
|
RadioSettingValueFloat,
|
56
|
InvalidValueError
|
57
|
)
|
58
|
|
59
|
from textwrap import dedent
|
60
|
|
61
|
LOG = logging.getLogger(__name__)
|
62
|
|
63
|
# 'True' or 'False'
|
64
|
# True unlocks:
|
65
|
# - FM preset
|
66
|
# - Channel memory # preset
|
67
|
# - Killswitch (with revive killed radios)
|
68
|
# - Bands settings
|
69
|
# - Disable radio identification string verification
|
70
|
# (good to recover from a bad state)
|
71
|
RT490_EXPERIMENTAL = True
|
72
|
|
73
|
MEM_FORMAT_RT490 = """
|
74
|
struct { // Memory settings
|
75
|
lbcd rxfreq[4];
|
76
|
lbcd txfreq[4];
|
77
|
ul16 rxtone;
|
78
|
ul16 txtone;
|
79
|
u8 signal; // int 0->14, Signal 1->15
|
80
|
u8 pttid; // ['OFF', 'BOT', 'EOT', 'Both']
|
81
|
u8 dcp:4, // What is DCP ? FHSS ? DC-FHSS ??? TODO
|
82
|
power:4; // POWER_LEVELS
|
83
|
u8 unknown3_0:1, // Used by the driver to store AM/NAM flag
|
84
|
// (thank you Radtel for free space)
|
85
|
narrow:1, // bool true=NFM false=FM (col[7] a)
|
86
|
unknown3_1:1,
|
87
|
unknown3_2:1,
|
88
|
bcl:1, // bool (col[9] a2)
|
89
|
scan:1, // bool (col[10] a3)
|
90
|
tx_enable:1, // bool (col[1] a4)
|
91
|
learn:1; // bool ??? TODO (col[14] a5)
|
92
|
} memory[%(memsize)d];
|
93
|
#seekto 0x1000; // Memory names (@4096)
|
94
|
struct {
|
95
|
char name[12];
|
96
|
u8 ffpad[4];
|
97
|
} memname[%(memsize)d];
|
98
|
#seekto 0x2000; // GOOD DCP keys ??? TODO ?? (@8192)
|
99
|
struct {
|
100
|
u8 code[4];
|
101
|
} memcode[%(memsize)d]; // up to @x2400
|
102
|
// Filled with xFF during download (=> no need to fill
|
103
|
// with xFF), ready to upload
|
104
|
#seekto 0x3400; // Custom ANI Names (@13312)
|
105
|
struct {
|
106
|
char name[10];
|
107
|
u8 ffpad[6];
|
108
|
} custom_ani_names[10];
|
109
|
// Filled with xFF during download (=> no need to fill
|
110
|
// with xFF), ready to upload
|
111
|
#seekto 0x3500; // ANI Codes (@13568)
|
112
|
struct {
|
113
|
u8 anicode[6];
|
114
|
u8 ffpad[10];
|
115
|
} anicodes[60];
|
116
|
// Filled with xFF during download (=> no need to fill
|
117
|
// with xFF), ready to upload
|
118
|
#seekto 0x3900; // Custom channel names (@14592)
|
119
|
struct {
|
120
|
char name[10];
|
121
|
u8 ffpad[6];
|
122
|
} custom_channel_names[10];
|
123
|
// Filled with xFF during download (=> no need to fill
|
124
|
// with xFF), ready to upload
|
125
|
#seekto 0x3A00; // Settings (@14848)
|
126
|
struct {
|
127
|
u8 squelch; // 0: int 0 -> 9
|
128
|
u8 savemode; // 1: ['OFF', 'Normal', 'Super', 'Deep']
|
129
|
u8 vox; // 2: off=0, 1 -> 9
|
130
|
u8 backlight; // 3: ['OFF', '5s', '15s', '20s', '30s', '1m', '2m',
|
131
|
// '3m']
|
132
|
u8 tdr; // 4: bool
|
133
|
u8 timeout; // 5: n*30seconds, 0-240s
|
134
|
u8 beep; // 6: bool
|
135
|
u8 voice; // 7: bool
|
136
|
u8 byte_not_used_10; // 8: Allways 1
|
137
|
u8 dtmfst; // 9: ['OFF', 'KB Side Tone', 'ANI Side Tone',
|
138
|
// 'KB ST + ANI ST']
|
139
|
u8 scanmode; // 10: ['TO', 'CO', 'SE']
|
140
|
u8 pttid; // 11: ['OFF', 'BOT', 'EOT', 'Both']
|
141
|
u8 pttiddelay; // 12: ['0', '100ms', '200ms', '400ms', '600ms',
|
142
|
// '800ms', '1000ms']
|
143
|
u8 cha_disp; // 13: ['Name', 'Freq', 'Channel ID']
|
144
|
u8 chb_disp; // 14: ['Name', 'Freq', 'Channel ID']
|
145
|
u8 bcl; // 15: bool
|
146
|
u8 autolock; // 0: ['OFF', '5s', '10s', 15s']
|
147
|
u8 alarm_mode; // 1: ['Site', 'Tone', 'Code']
|
148
|
u8 alarmsound; // 2: bool
|
149
|
u8 txundertdr; // 3: ['OFF', 'A', 'B']
|
150
|
u8 tailnoiseclear; // 4: [off, on]
|
151
|
u8 rptnoiseclear; // 5: n*100ms, 0-1000
|
152
|
u8 rptnoisedelay; // 6: n*100ms, 0-1000
|
153
|
u8 roger; // 7: bool
|
154
|
u8 active_channel; // 8: 0 or 1
|
155
|
u8 fmradio; // 9: boolean, inverted
|
156
|
u8 workmodeb:4, // 10:up ['VFO', 'CH Mode']
|
157
|
workmodea:4; // 10:down ['VFO', 'CH Mode']
|
158
|
u8 kblock; // 11: bool // TODO TEST WITH autolock
|
159
|
u8 powermsg; // 12: 0=Image / 1=Voltage
|
160
|
u8 byte_not_used_21; // 13: Allways 0
|
161
|
u8 rpttone; // 14: ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
|
162
|
u8 byte_not_used_22; // 15: pad with xFF
|
163
|
u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)]
|
164
|
// '0.5s' to '2.0s'
|
165
|
u8 timer_menu_quit; // 1: ['5s', '10s', '15s', '20s', '25s', '30s', '35s',
|
166
|
// '40s', '45s', '50s', '60s']
|
167
|
u8 byte_not_used_30; // 2: pad with xFF
|
168
|
u8 byte_not_used_31; // 3: pad with xFF
|
169
|
u8 enable_killsw; // 4: bool
|
170
|
u8 display_ani; // 5: bool
|
171
|
u8 byte_not_used_32; // 6: pad with xFF
|
172
|
u8 enable_gps; // 7: bool
|
173
|
u8 scan_dcs; // 8: ['All', 'Receive', 'Transmit']
|
174
|
u8 ani_id; // 9: int 0-59 (ANI 1-60)
|
175
|
u8 rx_time; // 10: bool
|
176
|
u8 ffpad0[5]; // 11: Pad xFF
|
177
|
u8 cha_memidx; // 0: Memory index when channel A use memories
|
178
|
u8 byte_not_used_40;
|
179
|
u8 chb_memidx; // 2: Memory index when channel B use memories
|
180
|
u8 byte_not_used_41;
|
181
|
u8 ffpad1[10];
|
182
|
ul16 fmpreset;
|
183
|
} settings;
|
184
|
// Filled with xFF during download (=> no need to fill
|
185
|
// with xFF), ready to upload
|
186
|
struct settings_vfo_chan {
|
187
|
u8 rxfreq[8]; // 0
|
188
|
ul16 rxtone; // 8
|
189
|
ul16 txtone; // 10
|
190
|
ul16 byte_not_used0; // 12 Pad xFF
|
191
|
u8 sftd:4, // 14 Shift dir ['OFF', '+', '-']
|
192
|
signal:4; // 14 int 0->14, Signal 1->15
|
193
|
u8 byte_not_used1; // 15 Pad xFF
|
194
|
u8 power; // 16:0 POWER_LEVELS
|
195
|
u8 fhss:4, // 17 ['OFF', 'FHSS 1', 'FHSS 2', 'FHSS 3', 'FHSS 4']
|
196
|
narrow:4; // 17 bool true=NFM false=FM
|
197
|
u8 byte_not_used2; // 18 Pad xFF but received 0x00 ???
|
198
|
u8 freqstep; // 19:3 ['2.5 KHz', '5.0 KHz', '6.25 KHz',
|
199
|
// '10.0 KHz', '12.5 KHz', '20.0 KHz',
|
200
|
// '25.0 KHz', '50.0 KHz']
|
201
|
u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO
|
202
|
u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST
|
203
|
u8 byte_not_used4; // 27:11 Pad xFF
|
204
|
u8 byte_not_used5; // 28 Pad xFF
|
205
|
u8 byte_not_used6; // 29 Pad xFF
|
206
|
u8 byte_not_used7; // 30 Pad xFF
|
207
|
u8 byte_not_used8; // 31:15 Pad xFF
|
208
|
};
|
209
|
#seekto 0x3A40; // VFO A/B (@14912)
|
210
|
struct {
|
211
|
struct settings_vfo_chan vfo_a;
|
212
|
struct settings_vfo_chan vfo_b;
|
213
|
} settings_vfo;
|
214
|
#seekto 0x3A80; // Side keys settings (@14976)
|
215
|
struct { // Values from Radio
|
216
|
u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
217
|
// '29': 'Search, '1': 'PPT B' }
|
218
|
u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
219
|
// '29': 'Search' }
|
220
|
u8 pf3_short; // {'7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
221
|
// '29': 'Search'}
|
222
|
u8 ffpad; // Pad xFF
|
223
|
} settings_sidekeys;
|
224
|
struct dtmfcode {
|
225
|
u8 code[5]; // 5 digits DTMF
|
226
|
u8 ffpad[11]; // Pad xFF
|
227
|
};
|
228
|
// Filled with xFF during download (=> no need to fill
|
229
|
// with xFF), ready to upload
|
230
|
#seekto 0x3B00; // DTMF (@15104)
|
231
|
struct dtmfcode settings_dtmfgroup[15];
|
232
|
struct { // @15296+3x16
|
233
|
u8 byte_not_used1; // 0: Pad xFF something here
|
234
|
u8 byte_not_used2; // 1: Pad xFF something here
|
235
|
u8 byte_not_used3; // 2: Pad xFF something here
|
236
|
u8 byte_not_used4; // 3: Pad xFF
|
237
|
u8 byte_not_used5; // 4: Pad xFF
|
238
|
u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO
|
239
|
u8 pttid; // 6: [off, BOT, EOT, Both]
|
240
|
u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms']
|
241
|
u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms']
|
242
|
} settings_dtmf;
|
243
|
// Filled with xFF during download (=> no need to fill
|
244
|
// with xFF), ready to upload
|
245
|
#seekto 0x3C00; // DTMF Kill/ReLive Codes (@15360)
|
246
|
struct {
|
247
|
u8 kill_dtmf[6]; // 0: Kill DTMF
|
248
|
u8 ffpad1[2]; // Pad xFF
|
249
|
u8 revive_dtmf[6]; // 8: Revive DTMF
|
250
|
u8 ffpad2[2]; // Pad xFF
|
251
|
} settings_killswitch;
|
252
|
// Some unknown data between 0x3E00 and 0x3F00
|
253
|
#seekto 0x3F80; // Hmm hmm
|
254
|
struct {
|
255
|
u8 unknown_data_0[16];
|
256
|
u8 unknown_data_1;
|
257
|
u8 active; // Bool radio killed (killed=0, active=1)
|
258
|
u8 unknown_data_2[46];
|
259
|
} management_settings;
|
260
|
struct band {
|
261
|
u8 enable; // 0 bool / enable-disable Tx on band
|
262
|
bbcd freq_low[2]; // 1 lowest band frequency
|
263
|
bbcd freq_high[2]; // 3 highest band frequency
|
264
|
};
|
265
|
#seekto 0x3FC0; // Bands settings (@16320)
|
266
|
struct {
|
267
|
struct band band136; // 0 Settings for 136MHz band
|
268
|
struct band band400; // 5 Settings for 400MHz band
|
269
|
struct band band200; // 10 Settings for 200MHz band
|
270
|
u8 byte_not_used1; // 15
|
271
|
struct band band350; // 0 Settings for 350MHz band
|
272
|
u8 byte_not_used2[43];// 5
|
273
|
} settings_bands;
|
274
|
"""
|
275
|
|
276
|
CMD_ACK = b"\x06"
|
277
|
|
278
|
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
|
279
|
|
280
|
DTMFCHARS = '0123456789ABCD*#'
|
281
|
|
282
|
|
283
|
def _enter_programming_mode(radio):
|
284
|
serial = radio.pipe
|
285
|
|
286
|
exito = False
|
287
|
for i in range(0, 5):
|
288
|
serial.write(radio._magic)
|
289
|
ack = serial.read(1)
|
290
|
|
291
|
try:
|
292
|
if ack == CMD_ACK:
|
293
|
exito = True
|
294
|
break
|
295
|
except Exception:
|
296
|
LOG.debug("Attempt #%s, failed, trying again" % i)
|
297
|
pass
|
298
|
|
299
|
# check if we had EXITO
|
300
|
if exito is False:
|
301
|
msg = "The radio did not accept program mode after five tries.\n"
|
302
|
msg += "Check you interface cable and power cycle your radio."
|
303
|
raise errors.RadioError(msg)
|
304
|
|
305
|
try:
|
306
|
serial.write(b"F")
|
307
|
ident = serial.read(8)
|
308
|
except Exception:
|
309
|
raise errors.RadioError("Error communicating with radio")
|
310
|
|
311
|
if not ident.startswith(radio._fingerprint) and not RT490_EXPERIMENTAL:
|
312
|
LOG.debug(util.hexprint(ident))
|
313
|
raise errors.RadioError("Radio returned unknown identification string")
|
314
|
|
315
|
|
316
|
def _exit_programming_mode(radio):
|
317
|
serial = radio.pipe
|
318
|
try:
|
319
|
serial.write(b"E")
|
320
|
except Exception:
|
321
|
raise errors.RadioError("Radio refused to exit programming mode")
|
322
|
|
323
|
|
324
|
def _read_block(radio, block_addr, block_size):
|
325
|
serial = radio.pipe
|
326
|
|
327
|
cmd = struct.pack(">cHb", b'R', block_addr, block_size)
|
328
|
expectedresponse = b"R" + cmd[1:]
|
329
|
LOG.debug("Reading block %04x..." % (block_addr))
|
330
|
|
331
|
try:
|
332
|
serial.write(cmd)
|
333
|
response = serial.read(4 + block_size)
|
334
|
if response[:4] != expectedresponse:
|
335
|
raise Exception("Error reading block %04x." % (block_addr))
|
336
|
|
337
|
block_data = response[4:]
|
338
|
except Exception:
|
339
|
raise errors.RadioError("Failed to read block at %04x" % block_addr)
|
340
|
|
341
|
return block_data
|
342
|
|
343
|
|
344
|
def _write_block(radio, block_addr, block_size):
|
345
|
serial = radio.pipe
|
346
|
|
347
|
cmd = struct.pack(">cHb", b'W', block_addr, block_size)
|
348
|
data = radio.get_mmap()[block_addr:block_addr + block_size]
|
349
|
|
350
|
LOG.debug("Writing Data:")
|
351
|
LOG.debug(util.hexprint(cmd + data))
|
352
|
|
353
|
try:
|
354
|
serial.write(cmd + data)
|
355
|
if serial.read(1) != CMD_ACK:
|
356
|
raise Exception("No ACK")
|
357
|
except Exception:
|
358
|
raise errors.RadioError("Failed to send block "
|
359
|
"to radio at %04x" % block_addr)
|
360
|
|
361
|
|
362
|
def do_download(radio):
|
363
|
LOG.debug("download")
|
364
|
_enter_programming_mode(radio)
|
365
|
|
366
|
data = b""
|
367
|
|
368
|
status = chirp_common.Status()
|
369
|
status.msg = "Cloning from radio"
|
370
|
|
371
|
status.cur = 0
|
372
|
status.max = radio._memsize
|
373
|
|
374
|
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
|
375
|
status.cur = addr + radio.BLOCK_SIZE
|
376
|
radio.status_fn(status)
|
377
|
|
378
|
block = _read_block(radio, addr, radio.BLOCK_SIZE)
|
379
|
data += block
|
380
|
|
381
|
LOG.debug("Address: %04x" % addr)
|
382
|
LOG.debug(util.hexprint(block))
|
383
|
|
384
|
_exit_programming_mode(radio)
|
385
|
|
386
|
return memmap.MemoryMapBytes(data)
|
387
|
|
388
|
|
389
|
def do_upload(radio):
|
390
|
status = chirp_common.Status()
|
391
|
status.msg = "Uploading to radio"
|
392
|
|
393
|
_enter_programming_mode(radio)
|
394
|
|
395
|
status.cur = 0
|
396
|
status.max = radio._memsize
|
397
|
|
398
|
for start_addr, end_addr in radio._ranges:
|
399
|
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
|
400
|
status.cur = addr + radio.BLOCK_SIZE
|
401
|
radio.status_fn(status)
|
402
|
_write_block(radio, addr, radio.BLOCK_SIZE)
|
403
|
|
404
|
_exit_programming_mode(radio)
|
405
|
|
406
|
|
407
|
class RT490Radio(chirp_common.CloneModeRadio):
|
408
|
"""RADTEL RT-490"""
|
409
|
VENDOR = "Radtel"
|
410
|
MODEL = "RT-490"
|
411
|
BLOCK_SIZE = 0x40 # 64 bytes
|
412
|
BAUD_RATE = 9600
|
413
|
NEEDS_COMPAT_SERIAL = False
|
414
|
|
415
|
POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
|
416
|
chirp_common.PowerLevel("L", watts=3.00)]
|
417
|
|
418
|
# magic = progmode + modelType + garbage (works with any last char)
|
419
|
_magic = b"PROGROMJJCCU"
|
420
|
|
421
|
# fingerprint is default band ranges of the radio
|
422
|
# the driver can change band ranges and fingerprint will
|
423
|
# change accordingly, so it is not used to verify radio id.
|
424
|
_fingerprint = b"\x01\x36\x01\x80\x04\x00\x05\x20"
|
425
|
|
426
|
# Ranges of memory used when uploading data to radio
|
427
|
# same as official software
|
428
|
_ranges = [
|
429
|
(0x0000, 0x2400),
|
430
|
(0x3400, 0x3C40),
|
431
|
(0x3FC0, 0x4000)
|
432
|
]
|
433
|
|
434
|
if RT490_EXPERIMENTAL:
|
435
|
# Experimental driver (already heavily tested)
|
436
|
_ranges = [(0x0000, 0x2400), (0x3400, 0x3C40), (0x3F80, 0x4000)]
|
437
|
|
438
|
# Danger zone
|
439
|
# _ranges = [(0x0000, 0x2500), (0x3400, 0x3C40), (0x3E00, 0x4000)]
|
440
|
|
441
|
# 16KB of memory, download read everything
|
442
|
# same as official software (remark: loops if overread :))
|
443
|
_memsize = 16384
|
444
|
|
445
|
POWER_LEVELS_LIST = [str(i) for i in POWER_LEVELS]
|
446
|
FHSS_LIST = ['OFF', 'ENCRYPT 1', 'ENCRYPT 2', 'ENCRYPT 3', 'ENCRYPT 4']
|
447
|
DCP_LIST = ['OFF', 'DCP1', 'DCP2', 'DCP3', 'DCP4'] # Same as FHSS ?
|
448
|
# # Seems yes
|
449
|
TUNING_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
|
450
|
TUNING_STEPS_LIST = [str(i)+' KHz' for i in TUNING_STEPS]
|
451
|
SIGNAL = [str(i) for i in range(1, 16)]
|
452
|
PTTID = ['OFF', 'BOT', 'EOT', 'Both']
|
453
|
PTTIDDELAYS = ['0', '100ms', '200ms', '400ms', '600ms', '800ms', '1000ms']
|
454
|
DTMF_SPEEDS = ['50ms', '100ms', '200ms', '300ms', '500ms']
|
455
|
SIDEKEY_VALUEMAP = [('FM', 7), ('Tx Power', 10), ('Scan', 28),
|
456
|
('Search', 29), ('PTT B', 1)]
|
457
|
KEY_CHARS = '0123456789ABCDEF'
|
458
|
FULL_CHARSET_ASCII = ("".join([chr(x) for x in range(ord(" "),
|
459
|
ord("~") + 1)] + [chr(x) for x in range(128, 255)] +
|
460
|
[chr(0)]))
|
461
|
VFO_SFTD = ['OFF', '+', '-']
|
462
|
WORKMODES = ['VFO', 'Memory Mode']
|
463
|
SAVEMODES = ['OFF', 'Normal', 'Super', 'Deep']
|
464
|
DISPLAYMODES = ['Name', 'Freq', 'Memory ID']
|
465
|
SCANMODES = ['TO', 'CO', 'SE']
|
466
|
ALARMMODES = ['On Site', 'Send Sound', 'Send Code']
|
467
|
TDRTXMODES = ['OFF', 'A', 'B']
|
468
|
SCANDCSMODES = ['All', 'Receive', 'Transmit']
|
469
|
POWERMESSAGES = ['Image', 'Voltage']
|
470
|
FMRADIO = ['ON', 'OFF']
|
471
|
ENABLERADIO = ['Killed', 'Active']
|
472
|
CHANNELS = ['A', 'B']
|
473
|
TOT_LIST = ['OFF'] + [str(i*30) + "s" for i in range(1, 9)]
|
474
|
VOX_LIST = ['OFF'] + [str(i) for i in range(1, 9)]
|
475
|
BACKLIGHT_TO = ['OFF', '5s', '10s', '15s', '20s', '30s', '1m', '2m', '3m']
|
476
|
AUTOLOCK_TO = ['OFF', '5s', '10s', '15s']
|
477
|
MENUEXIT_TO = ['5s', '10s', '15s', '20s', '25s', '30s', '35s', '40s',
|
478
|
'45s', '50s', '60s']
|
479
|
SQUELCHLVLS = [str(i) for i in range(10)]
|
480
|
ANI_IDS = [str(i+1) for i in range(60)]
|
481
|
VOXDELAYLIST = [str(float(a)/10)+'s' for a in range(5, 21)]
|
482
|
DTMFSTLIST = ['OFF', 'DT Side Tone', 'ANI Side Tone', 'DT ST + ANI ST']
|
483
|
RPTTONES = ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
|
484
|
RPTNOISE = [str(a)+'s' for a in range(11)]
|
485
|
_memory_size = _upper = 256 # Number of memory slots
|
486
|
_mem_params = (_upper-1)
|
487
|
_frs = _murs = _pmr = _gmrs = True
|
488
|
|
489
|
def set_settings(self, settings):
|
490
|
for element in settings:
|
491
|
if not isinstance(element, RadioSetting):
|
492
|
self.set_settings(element)
|
493
|
continue
|
494
|
else:
|
495
|
self._set_setting(element)
|
496
|
|
497
|
def _set_setting(self, setting): # WIP
|
498
|
_mem = self._memobj
|
499
|
key = setting.get_name()
|
500
|
val = setting.value
|
501
|
if key.startswith('dummy'):
|
502
|
return
|
503
|
elif key.startswith('settings_dtmfgroup.'):
|
504
|
if str(val) == "":
|
505
|
setattr(_mem.settings_dtmfgroup[int(key.split('@')[1])-1],
|
506
|
'code', [0xFF]*5)
|
507
|
else:
|
508
|
tmp = [DTMFCHARS.index(c) for c in str(val)]
|
509
|
tmp += [0xFF] * (5 - len(tmp))
|
510
|
setattr(_mem.settings_dtmfgroup[int(key.split('@')[1])-1],
|
511
|
'code', tmp)
|
512
|
elif key.startswith('settings.'):
|
513
|
if key.endswith('_memidx'):
|
514
|
val = int(val) - 1
|
515
|
if key.endswith('fmpreset'):
|
516
|
tmp = val.get_value() * 10
|
517
|
setattr(_mem.settings, key.split('.')[1], tmp)
|
518
|
else:
|
519
|
setattr(_mem.settings, key.split('.')[1], int(val))
|
520
|
elif key.startswith('settings_dtmf.'):
|
521
|
attr = key.split('.')[1]
|
522
|
setattr(_mem.settings_dtmf, attr, int(val))
|
523
|
if attr.startswith('pttid'):
|
524
|
setattr(_mem.settings, attr, int(val))
|
525
|
elif key.startswith('settings_sidekeys.'):
|
526
|
setattr(_mem.settings_sidekeys, key.split('.')[1], int(val))
|
527
|
elif key.startswith('settings_vfo.'): # TODO rx/tx tones
|
528
|
tmp = key.split('.')
|
529
|
attr = tmp[2]
|
530
|
vfo = tmp[1]
|
531
|
# LOG.debug(">>> PRE key '%s'" % key)
|
532
|
# LOG.debug(">>> PRE val '%s'" % val)
|
533
|
if attr.startswith('rxfreq'):
|
534
|
value = chirp_common.parse_freq(str(val)) / 10
|
535
|
for i in range(7, -1, -1):
|
536
|
_mem.settings_vfo[vfo].rxfreq[i] = value % 10
|
537
|
value /= 10
|
538
|
elif attr.startswith('offset'):
|
539
|
value = int(float(str(val)) * 10000)
|
540
|
for i in range(5, -1, -1):
|
541
|
_mem.settings_vfo[vfo].offset[i] = value % 10
|
542
|
value /= 10
|
543
|
else:
|
544
|
setattr(_mem.settings_vfo[vfo], attr, int(val))
|
545
|
elif key.startswith('settings_bands.'):
|
546
|
tmp = key.split('.')
|
547
|
attr = tmp[2]
|
548
|
band = tmp[1]
|
549
|
setattr(_mem.settings_bands[band], attr, int(val))
|
550
|
elif key.startswith('settings_killswitch.'):
|
551
|
attr = key.split('.')[1]
|
552
|
if attr.endswith('dtmf'):
|
553
|
if str(val) == "":
|
554
|
setattr(_mem.settings_killswitch, attr,
|
555
|
[0x01, 0x02, 0x01, 0x03, 0x01, 0x04])
|
556
|
else:
|
557
|
setattr(_mem.settings_killswitch, attr,
|
558
|
[DTMFCHARS.index(c) for c in str(val)])
|
559
|
elif attr.startswith('enable'):
|
560
|
setattr(_mem.settings_killswitch, attr, int(val))
|
561
|
else:
|
562
|
LOG.debug(">>> TODO key '%s'" % key)
|
563
|
LOG.debug(">>> TODO val '%s'" % val)
|
564
|
elif key.startswith('custom_') or key.startswith('anicode') \
|
565
|
or key.startswith('memcode'):
|
566
|
tmp = key.split('@')
|
567
|
if key.startswith('anicode'):
|
568
|
val = [DTMFCHARS.index(c) for c in str(val)]
|
569
|
val += [0xFF] * (6 - len(val))
|
570
|
for i in range(10):
|
571
|
_mem[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF
|
572
|
elif key.startswith('memcode'):
|
573
|
if len(str(val)) > 0:
|
574
|
tmpp = str(val).zfill(6)
|
575
|
val = self._encode_key(tmpp)
|
576
|
else:
|
577
|
val = (0xFF, 0xFF, 0xFF, 0xFF)
|
578
|
if key.startswith('custom_'):
|
579
|
for i in range(6):
|
580
|
_mem[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF
|
581
|
setattr(_mem[str(tmp[0])][int(tmp[2])], str(tmp[1]), val)
|
582
|
elif key.startswith('management_settings'):
|
583
|
setattr(_mem.management_settings, key.split('.')[1], int(val))
|
584
|
else:
|
585
|
LOG.debug(">>> TODO _set_setting key '%s'" % key)
|
586
|
LOG.debug(">>> TODO _set_setting val '%s'" % val)
|
587
|
|
588
|
def _get_settings_bands(self):
|
589
|
_mem = self._memobj
|
590
|
ret = RadioSettingGroup('bands', 'Bands')
|
591
|
bands = [('136', _mem.settings_bands.band136),
|
592
|
('200', _mem.settings_bands.band200),
|
593
|
('350', _mem.settings_bands.band350),
|
594
|
('400', _mem.settings_bands.band400)]
|
595
|
for label, band in bands:
|
596
|
rs = RadioSetting('settings_bands.band%s.enable' % label,
|
597
|
'Enable Band %s' % label,
|
598
|
RadioSettingValueBoolean(band.enable))
|
599
|
ret.append(rs)
|
600
|
rsi = RadioSettingValueInteger(1, 1000, band.freq_low)
|
601
|
# if label == '136' or label == '400':
|
602
|
# rsi.set_mutable(False)
|
603
|
rs = RadioSetting("settings_bands.band%s.freq_low" % label,
|
604
|
"Band %s Lower Limit (MHz) (EXPERIMENTAL)"
|
605
|
% label, rsi)
|
606
|
ret.append(rs)
|
607
|
rsi = RadioSettingValueInteger(1, 1000, band.freq_high)
|
608
|
# if label == '350':
|
609
|
# rsi.set_mutable(False)
|
610
|
rs = RadioSetting("settings_bands.band%s.freq_high" % label,
|
611
|
"Band %s Upper Limit (MHz) (EXPERIMENTAL)"
|
612
|
% label, rsi)
|
613
|
ret.append(rs)
|
614
|
return ret
|
615
|
|
616
|
def _get_settings_ks(self):
|
617
|
_mem = self._memobj
|
618
|
ret = RadioSettingGroup('killswitch', 'Killswitch')
|
619
|
# Kill Enable/Disable enable_killsw
|
620
|
rsvb = RadioSettingValueBoolean(_mem.settings.enable_killsw)
|
621
|
ret.append(RadioSetting('settings.enable_killsw',
|
622
|
'Enable Killswitch', rsvb))
|
623
|
# Kill DTMF
|
624
|
cur = ''.join(
|
625
|
DTMFCHARS[i]
|
626
|
for i in _mem.settings_killswitch.kill_dtmf if int(i) < 0xF)
|
627
|
ret.append(
|
628
|
RadioSetting('settings_killswitch.kill_dtmf', 'DTMF Kill',
|
629
|
RadioSettingValueString(6, 6, cur,
|
630
|
autopad=False,
|
631
|
charset=DTMFCHARS)))
|
632
|
# Revive DTMF
|
633
|
cur = ''.join(
|
634
|
DTMFCHARS[i]
|
635
|
for i in _mem.settings_killswitch.revive_dtmf if int(i) < 0xF)
|
636
|
ret.append(
|
637
|
RadioSetting('settings_killswitch.revive_dtmf', 'DTMF Revive',
|
638
|
RadioSettingValueString(6, 6, cur,
|
639
|
autopad=False,
|
640
|
charset=DTMFCHARS)))
|
641
|
# Enable/Disable entire radio
|
642
|
rs = RadioSettingValueString(0, 255, "Can be used to revive radio")
|
643
|
rs.set_mutable(False)
|
644
|
ret.append(RadioSetting('dummy', 'Factory reserved', rs))
|
645
|
tmp = 1 if int(_mem.management_settings.active) > 0 else 0
|
646
|
ret.append(RadioSetting('management_settings.active', 'Radio Status',
|
647
|
RadioSettingValueList(self.ENABLERADIO,
|
648
|
self.ENABLERADIO[tmp])))
|
649
|
return ret
|
650
|
|
651
|
def _get_settings_dtmf(self):
|
652
|
_mem = self._memobj
|
653
|
dtmf = RadioSettingGroup('dtmf', 'DTMF')
|
654
|
# DTMF Group
|
655
|
msgs = ["Allowed chars (%s)" % DTMFCHARS,
|
656
|
"Input from 0 to 5 characters."
|
657
|
]
|
658
|
for msg in msgs:
|
659
|
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
|
660
|
rsvs.set_mutable(False)
|
661
|
rs = RadioSetting('dummy_dtmf_msg_%i' % msgs.index(msg),
|
662
|
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
|
663
|
dtmf.append(rs)
|
664
|
for i in range(1, 16):
|
665
|
cur = ''.join(
|
666
|
DTMFCHARS[i]
|
667
|
for i in _mem.settings_dtmfgroup[i - 1].code if int(i) < 0xF)
|
668
|
dtmf.append(
|
669
|
RadioSetting(
|
670
|
'settings_dtmfgroup.code@%i' % i, 'PTT ID Code %i' % i,
|
671
|
RadioSettingValueString(0, 5, cur,
|
672
|
autopad=False,
|
673
|
charset=DTMFCHARS)))
|
674
|
# DTMF Speed (on time in ms)
|
675
|
dtmf_speed_on = int(_mem.settings_dtmf.dtmf_speed_on)
|
676
|
if dtmf_speed_on > len(self.DTMF_SPEEDS)-1:
|
677
|
_mem.settings_dtmf.dtmf_speed_on = 0
|
678
|
LOG.debug('DTMF Speed On overflow')
|
679
|
cur = self.DTMF_SPEEDS[dtmf_speed_on]
|
680
|
dtmf.append(
|
681
|
RadioSetting(
|
682
|
'settings_dtmf.dtmf_speed_on', 'DTMF Speed (on time in ms)',
|
683
|
RadioSettingValueList(self.DTMF_SPEEDS, cur)))
|
684
|
# DTMF Speed (on time in ms)
|
685
|
dtmf_speed_off = int(_mem.settings_dtmf.dtmf_speed_off)
|
686
|
if dtmf_speed_off > len(self.DTMF_SPEEDS)-1:
|
687
|
_mem.settings_dtmf.dtmf_speed_off = 0
|
688
|
LOG.debug('DTMF Speed Off overflow')
|
689
|
cur = self.DTMF_SPEEDS[dtmf_speed_off]
|
690
|
dtmf.append(
|
691
|
RadioSetting(
|
692
|
'settings_dtmf.dtmf_speed_off', 'DTMF Speed (off time in ms)',
|
693
|
RadioSettingValueList(self.DTMF_SPEEDS, cur)))
|
694
|
# PTT ID
|
695
|
pttid = int(_mem.settings_dtmf.pttid)
|
696
|
if pttid > len(self.PTTID)-1:
|
697
|
_mem.settings_dtmf.pttid = 0
|
698
|
LOG.debug('PTT ID overflow')
|
699
|
cur = self.PTTID[pttid]
|
700
|
dtmf.append(
|
701
|
RadioSetting(
|
702
|
'settings_dtmf.pttid', 'Send DTMF Code (PTT ID)',
|
703
|
RadioSettingValueList(self.PTTID, cur)))
|
704
|
# PTT ID Delay
|
705
|
pttiddelay = int(_mem.settings.pttiddelay)
|
706
|
if pttiddelay > len(self.PTTIDDELAYS)-1:
|
707
|
_mem.settings.pttiddelay = 0
|
708
|
LOG.debug('PTT ID Delay overflow')
|
709
|
cur = self.PTTIDDELAYS[pttiddelay]
|
710
|
rsvl = RadioSettingValueList(self.PTTIDDELAYS, cur)
|
711
|
dtmf.append(RadioSetting('settings.pttiddelay',
|
712
|
'PTT ID Delay', rsvl))
|
713
|
rsvl = RadioSettingValueList(self.DTMFSTLIST,
|
714
|
self.DTMFSTLIST[_mem.settings.dtmfst])
|
715
|
dtmf.append(RadioSetting('settings.dtmfst',
|
716
|
'DTMF Side Tone (Required for GPS ID)', rsvl))
|
717
|
return dtmf
|
718
|
|
719
|
def _get_settings_sidekeys(self):
|
720
|
_mem = self._memobj
|
721
|
ret = RadioSettingGroup('sidekeys', 'Side Keys')
|
722
|
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP,
|
723
|
_mem.settings_sidekeys.pf2_short)
|
724
|
ret.append(RadioSetting('settings_sidekeys.pf2_short',
|
725
|
'Side key 1 (PTT2) Short press', rsvm))
|
726
|
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP[:-1],
|
727
|
_mem.settings_sidekeys.pf2_long)
|
728
|
ret.append(RadioSetting('settings_sidekeys.pf2_long',
|
729
|
'Side key 1 (PTT2) Long press', rsvm))
|
730
|
rsvm = RadioSettingValueMap(self.SIDEKEY_VALUEMAP[:-1],
|
731
|
_mem.settings_sidekeys.pf3_short)
|
732
|
ret.append(RadioSetting('settings_sidekeys.pf3_short',
|
733
|
'Side key 2 (PTT3) Short press', rsvm))
|
734
|
rs = RadioSettingValueString(0, 255, "MONI")
|
735
|
rs.set_mutable(False)
|
736
|
ret.append(RadioSetting('dummy', 'Side key 2 (PTT3) Long press', rs))
|
737
|
return ret
|
738
|
|
739
|
def _get_settings_vfo(self, vfo, chan): # WIP TODO Rx/Tx tones
|
740
|
_mem = self._memobj
|
741
|
ret = RadioSettingGroup('settings_vfo@%s' % chan.lower(),
|
742
|
'VFO %s Settings' % chan)
|
743
|
rsvl = RadioSettingValueList(self.WORKMODES,
|
744
|
self.WORKMODES[_mem.settings[
|
745
|
'workmode'+chan.lower()]])
|
746
|
ret.append(RadioSetting('settings.workmode%s' % chan.lower(),
|
747
|
'VFO %s Workmode' % chan, rsvl))
|
748
|
tmp = ''.join(DTMFCHARS[i] for i in _mem.settings_vfo[
|
749
|
'vfo_'+chan.lower()].rxfreq if i < 0xFF)
|
750
|
rsvf = RadioSettingValueFloat(66, 550, chirp_common.format_freq(
|
751
|
int(tmp) * 10), resolution=0.00001,
|
752
|
precision=5)
|
753
|
ret.append(RadioSetting('settings_vfo.vfo_%s.rxfreq' % chan.lower(),
|
754
|
'Rx Frequency', rsvf))
|
755
|
# TODO Rx/Tx tones
|
756
|
rsvl = RadioSettingValueList(self.VFO_SFTD,
|
757
|
self.VFO_SFTD[_mem.settings_vfo[
|
758
|
'vfo_'+chan.lower()].sftd])
|
759
|
ret.append(RadioSetting('settings_vfo.vfo_%s.sftd' % chan.lower(),
|
760
|
'Freq offset direction', rsvl))
|
761
|
tmp = ''.join(DTMFCHARS[i] for i in _mem.settings_vfo[
|
762
|
'vfo_'+chan.lower()].offset if i < 0xFF)
|
763
|
rsvf = RadioSettingValueFloat(0, 99.9999, float(tmp) / 10000)
|
764
|
ret.append(RadioSetting('settings_vfo.vfo_%s.offset' % chan.lower(),
|
765
|
'Tx Offset', rsvf))
|
766
|
rsvl = RadioSettingValueList(self.SIGNAL,
|
767
|
self.SIGNAL[
|
768
|
_mem.settings_vfo[
|
769
|
'vfo_'+chan.lower()].signal])
|
770
|
ret.append(RadioSetting('settings_vfo.vfo_%s.signal' % chan.lower(),
|
771
|
'PTT ID Code (S-Code)', rsvl))
|
772
|
rsvl = RadioSettingValueList(self.POWER_LEVELS_LIST,
|
773
|
self.POWER_LEVELS_LIST[
|
774
|
_mem.settings_vfo[
|
775
|
'vfo_'+chan.lower()].power])
|
776
|
ret.append(RadioSetting('settings_vfo.vfo_%s.power' % chan.lower(),
|
777
|
'Tx Power', rsvl))
|
778
|
rsvl = RadioSettingValueList(self.FHSS_LIST,
|
779
|
self.FHSS_LIST[
|
780
|
_mem.settings_vfo[
|
781
|
'vfo_'+chan.lower()].fhss])
|
782
|
ret.append(RadioSetting('settings_vfo.vfo_%s.fhss' % chan.lower(),
|
783
|
'FHSS (Encryption)', rsvl))
|
784
|
rsvl = RadioSettingValueList(['Wide', 'Narrow'],
|
785
|
['Wide', 'Narrow'][
|
786
|
_mem.settings_vfo[
|
787
|
'vfo_'+chan.lower()].narrow])
|
788
|
ret.append(RadioSetting('settings_vfo.vfo_%s.narrow' % chan.lower(),
|
789
|
'Wide / Narrow', rsvl))
|
790
|
rsvl = RadioSettingValueList(self.TUNING_STEPS_LIST,
|
791
|
self.TUNING_STEPS_LIST[
|
792
|
_mem.settings_vfo[
|
793
|
'vfo_'+chan.lower()].freqstep])
|
794
|
ret.append(RadioSetting('settings_vfo.vfo_%s.freqstep' % chan.lower(),
|
795
|
'Tuning Step', rsvl))
|
796
|
return ret
|
797
|
|
798
|
def _get_custom_channel_names(self):
|
799
|
_mem = self._memobj
|
800
|
ret = RadioSettingGroup('ccn', 'Custom Channel Names')
|
801
|
msgs = ["Add custom chan names to radio",
|
802
|
"-> Menu 09 CH-NAME",
|
803
|
"Allowed chars (ASCII only)",
|
804
|
"Input from 0 to 10 characters."
|
805
|
]
|
806
|
for msg in msgs:
|
807
|
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
|
808
|
rsvs.set_mutable(False)
|
809
|
rs = RadioSetting('dummy_cnames_msg_%i' % msgs.index(msg),
|
810
|
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
|
811
|
ret.append(rs)
|
812
|
for i in range(0, len(_mem.custom_channel_names)):
|
813
|
tmp = ''.join([str(j) for j in _mem.custom_channel_names[i].name
|
814
|
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
|
815
|
rsvs = RadioSettingValueString(0, 10, tmp, autopad=True,
|
816
|
charset=self.FULL_CHARSET_ASCII)
|
817
|
ret.append(RadioSetting('custom_channel_names@name@%i' % i,
|
818
|
'Custom Channel Name (%i)' % i, rsvs))
|
819
|
return ret
|
820
|
|
821
|
def _get_custom_ani_names(self):
|
822
|
_mem = self._memobj
|
823
|
ret = RadioSettingGroup('can', 'Custom ANI Names')
|
824
|
msgs = ["Can be used as radio id in GPS.",
|
825
|
"Allowed chars (ASCII only)",
|
826
|
"Input from 0 to 10 characters."
|
827
|
]
|
828
|
for msg in msgs:
|
829
|
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
|
830
|
rsvs.set_mutable(False)
|
831
|
rs = RadioSetting('dummy_caninames_msg_%i' % msgs.index(msg),
|
832
|
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
|
833
|
ret.append(rs)
|
834
|
for i in range(0, len(_mem.custom_ani_names)):
|
835
|
tmp = ''.join([str(j) for j in
|
836
|
_mem.custom_ani_names[i].name
|
837
|
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
|
838
|
rsvs = RadioSettingValueString(0, 10, tmp, autopad=True,
|
839
|
charset=self.FULL_CHARSET_ASCII)
|
840
|
ret.append(RadioSetting('custom_ani_names@name@%i' % i,
|
841
|
'Custom ANI Name (%i)' % (i+51), rsvs))
|
842
|
return ret
|
843
|
|
844
|
def _get_anicodes(self):
|
845
|
_mem = self._memobj
|
846
|
ret = RadioSettingGroup('ani', 'ANI Codes')
|
847
|
split = len(_mem.anicodes) - len(_mem.custom_ani_names)
|
848
|
msgs = ["Allowed chars (%s)" % DTMFCHARS,
|
849
|
"Input from 0 to 6 characters."
|
850
|
]
|
851
|
for msg in msgs:
|
852
|
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
|
853
|
rsvs.set_mutable(False)
|
854
|
rs = RadioSetting('dummy_canic_msg_%i' % msgs.index(msg),
|
855
|
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
|
856
|
ret.append(rs)
|
857
|
|
858
|
for i in range(0, split):
|
859
|
tmp = ''.join([DTMFCHARS[int(j)] for j in
|
860
|
_mem.anicodes[i].anicode if int(j) < 0xFF])
|
861
|
# LOG.debug("ANI Code (%i) '%s'" % (i, tmp))
|
862
|
rsvs = RadioSettingValueString(0, 6, tmp, autopad=False,
|
863
|
charset=DTMFCHARS)
|
864
|
ret.append(RadioSetting('anicodes@anicode@%i' % i,
|
865
|
'ANI-ID (%i) Code' % (i+1), rsvs))
|
866
|
for i in range(split, len(_mem.anicodes)):
|
867
|
tmp = ''.join([DTMFCHARS[int(j)] for j in
|
868
|
_mem.anicodes[i].anicode if int(j) < 0xFF])
|
869
|
tmp2 = ''.join([str(j) for j in
|
870
|
_mem.custom_ani_names[i-split].name
|
871
|
if ord(str(j)) < 0xFF and ord(str(j)) > 0x00])
|
872
|
# LOG.debug("ANI Code (%s) (%i) '%s'" % (tmp2, i, tmp))
|
873
|
rsvs = RadioSettingValueString(0, 6, tmp, autopad=False,
|
874
|
charset=DTMFCHARS)
|
875
|
ret.append(RadioSetting('anicodes@anicode@%i' % i,
|
876
|
'ANI-ID (%s) (%i) Code' % (tmp2, i+1), rsvs))
|
877
|
return ret
|
878
|
|
879
|
def _get_settings_adv(self):
|
880
|
_mem = self._memobj
|
881
|
ret = RadioSettingGroup('advanced', 'Advanced')
|
882
|
if RT490_EXPERIMENTAL:
|
883
|
rsvi = RadioSettingValueInteger(
|
884
|
1, self._memory_size, int(_mem.settings.cha_memidx)+1)
|
885
|
ret.append(RadioSetting("settings.cha_memidx",
|
886
|
"Channel A Memory index", rsvi))
|
887
|
rsvi = RadioSettingValueInteger(1, self._memory_size,
|
888
|
int(_mem.settings.chb_memidx)+1)
|
889
|
ret.append(RadioSetting("settings.chb_memidx",
|
890
|
"Channel B Memory index", rsvi))
|
891
|
ret.append(RadioSetting('settings.vox', 'VOX Sensitivity',
|
892
|
RadioSettingValueList(self.VOX_LIST,
|
893
|
self.VOX_LIST[_mem.settings.vox])))
|
894
|
ret.append(RadioSetting('settings.vox_delay', 'VOX Delay',
|
895
|
RadioSettingValueList(self.VOXDELAYLIST,
|
896
|
self.VOXDELAYLIST[
|
897
|
_mem.settings.vox_delay])))
|
898
|
ret.append(RadioSetting('settings.tdr', 'Dual Receive (TDR)',
|
899
|
RadioSettingValueBoolean(_mem.settings.tdr)))
|
900
|
ret.append(RadioSetting('settings.txundertdr', 'Tx under TDR',
|
901
|
RadioSettingValueList(self.TDRTXMODES,
|
902
|
self.TDRTXMODES[
|
903
|
_mem.settings.txundertdr])))
|
904
|
ret.append(RadioSetting('settings.voice', 'Menu Voice Prompts',
|
905
|
RadioSettingValueBoolean(_mem.settings.voice)))
|
906
|
ret.append(RadioSetting('settings.scanmode', 'Scan Mode',
|
907
|
RadioSettingValueList(self.SCANMODES,
|
908
|
self.SCANMODES[
|
909
|
_mem.settings.scanmode])))
|
910
|
ret.append(RadioSetting('settings.bcl', 'Busy Channel Lockout',
|
911
|
RadioSettingValueBoolean(_mem.settings.bcl)))
|
912
|
ret.append(RadioSetting('settings.display_ani', 'Display ANI ID',
|
913
|
RadioSettingValueBoolean(_mem.settings.display_ani)))
|
914
|
ret.append(RadioSetting('settings.ani_id', 'ANI ID',
|
915
|
RadioSettingValueList(self.ANI_IDS,
|
916
|
self.ANI_IDS[_mem.settings.ani_id])))
|
917
|
ret.append(RadioSetting('settings.alarm_mode', 'Alarm Mode',
|
918
|
RadioSettingValueList(self.ALARMMODES,
|
919
|
self.ALARMMODES[
|
920
|
_mem.settings.alarm_mode])))
|
921
|
ret.append(RadioSetting('settings.alarmsound', 'Alarm Sound',
|
922
|
RadioSettingValueBoolean(_mem.settings.alarmsound)))
|
923
|
ret.append(RadioSetting('settings.fmradio', 'Enable FM Radio',
|
924
|
RadioSettingValueList(self.FMRADIO,
|
925
|
self.FMRADIO[_mem.settings.fmradio])))
|
926
|
if RT490_EXPERIMENTAL:
|
927
|
tmp = _mem.settings.fmpreset / 10.0
|
928
|
if tmp < 65.0 or tmp > 108.0:
|
929
|
tmp = 80.0
|
930
|
ret.append(RadioSetting("settings.fmpreset", "FM Radio Freq",
|
931
|
RadioSettingValueFloat(65, 108, tmp, resolution=0.1,
|
932
|
precision=1)))
|
933
|
ret.append(RadioSetting('settings.kblock', 'Enable Keyboard Lock',
|
934
|
RadioSettingValueBoolean(_mem.settings.kblock)))
|
935
|
ret.append(RadioSetting('settings.autolock', 'Autolock Keyboard',
|
936
|
RadioSettingValueList(self.AUTOLOCK_TO,
|
937
|
self.AUTOLOCK_TO[
|
938
|
_mem.settings.autolock])))
|
939
|
ret.append(RadioSetting('settings.timer_menu_quit', 'Menu Exit Time',
|
940
|
RadioSettingValueList(self.MENUEXIT_TO,
|
941
|
self.MENUEXIT_TO[
|
942
|
_mem.settings.timer_menu_quit])))
|
943
|
ret.append(RadioSetting('settings.enable_gps', 'Enable GPS',
|
944
|
RadioSettingValueBoolean(_mem.settings.enable_gps)))
|
945
|
ret.append(RadioSetting('settings.scan_dcs', 'CDCSS Save Modes',
|
946
|
RadioSettingValueList(self.SCANDCSMODES,
|
947
|
self.SCANDCSMODES[
|
948
|
_mem.settings.scan_dcs])))
|
949
|
ret.append(RadioSetting('settings.tailnoiseclear', 'Tail Noise Clear',
|
950
|
RadioSettingValueBoolean(_mem.settings.tailnoiseclear)))
|
951
|
ret.append(RadioSetting('settings.rptnoiseclear', 'Rpt Noise Clear',
|
952
|
RadioSettingValueList(self.RPTNOISE,
|
953
|
self.RPTNOISE[
|
954
|
_mem.settings.rptnoiseclear])))
|
955
|
ret.append(RadioSetting('settings.rptnoisedelay', 'Rpt Noise Delay',
|
956
|
RadioSettingValueList(self.RPTNOISE,
|
957
|
self.RPTNOISE[
|
958
|
_mem.settings.rptnoisedelay])))
|
959
|
ret.append(RadioSetting('settings.rpttone', 'Rpt Tone',
|
960
|
RadioSettingValueList(self.RPTTONES,
|
961
|
self.RPTTONES[
|
962
|
_mem.settings.rpttone])))
|
963
|
return ret
|
964
|
|
965
|
def _get_settings_basic(self):
|
966
|
_mem = self._memobj
|
967
|
ret = RadioSettingGroup('basic', 'Basic')
|
968
|
ret.append(RadioSetting('settings.squelch', 'Carrier Squelch Level',
|
969
|
RadioSettingValueList(self.SQUELCHLVLS,
|
970
|
self.SQUELCHLVLS[
|
971
|
_mem.settings.squelch])))
|
972
|
ret.append(RadioSetting('settings.savemode', 'Battery Savemode',
|
973
|
RadioSettingValueList(self.SAVEMODES,
|
974
|
self.SAVEMODES[
|
975
|
_mem.settings.savemode])))
|
976
|
ret.append(RadioSetting('settings.backlight', 'Backlight Timeout',
|
977
|
RadioSettingValueList(self.BACKLIGHT_TO,
|
978
|
self.BACKLIGHT_TO[
|
979
|
_mem.settings.backlight])))
|
980
|
ret.append(RadioSetting('settings.timeout', 'Timeout Timer (TOT)',
|
981
|
RadioSettingValueList(self.TOT_LIST,
|
982
|
self.TOT_LIST[
|
983
|
_mem.settings.timeout])))
|
984
|
ret.append(RadioSetting('settings.beep', 'Beep',
|
985
|
RadioSettingValueBoolean(_mem.settings.beep)))
|
986
|
ret.append(RadioSetting('settings.active_channel', 'Active Channel',
|
987
|
RadioSettingValueList(self.CHANNELS,
|
988
|
self.CHANNELS[
|
989
|
_mem.settings.active_channel])))
|
990
|
ret.append(RadioSetting('settings.cha_disp', 'Channel A Display Mode',
|
991
|
RadioSettingValueList(self.DISPLAYMODES,
|
992
|
self.DISPLAYMODES[
|
993
|
_mem.settings.cha_disp])))
|
994
|
ret.append(RadioSetting('settings.chb_disp', 'Channel B Display Mode',
|
995
|
RadioSettingValueList(self.DISPLAYMODES,
|
996
|
self.DISPLAYMODES[
|
997
|
_mem.settings.chb_disp])))
|
998
|
ret.append(RadioSetting('settings.roger', 'Roger Beep',
|
999
|
RadioSettingValueBoolean(_mem.settings.roger)))
|
1000
|
ret.append(RadioSetting('settings.powermsg', 'Power Message',
|
1001
|
RadioSettingValueList(self.POWERMESSAGES,
|
1002
|
self.POWERMESSAGES[
|
1003
|
_mem.settings.powermsg])))
|
1004
|
ret.append(RadioSetting('settings.rx_time', 'Show RX Time',
|
1005
|
RadioSettingValueBoolean(_mem.settings.rx_time)))
|
1006
|
return ret
|
1007
|
|
1008
|
def _get_memcodes(self):
|
1009
|
ret = RadioSettingGroup('mc', 'Memory Channel Privacy Codes')
|
1010
|
msgs = ["Only hexadecimal chars accepted.",
|
1011
|
"Allowed chars (%s)" % self.KEY_CHARS,
|
1012
|
"Input from 0 to 6 characters. If code",
|
1013
|
"length is less than 6 chars it will be",
|
1014
|
"padded with leading zeros.",
|
1015
|
"Ex: 1D32EB or 0F12 or AB521, etc...",
|
1016
|
"Enable Code for the Location on the",
|
1017
|
"'Other' tab in 'Memory Properties'."
|
1018
|
]
|
1019
|
for msg in msgs:
|
1020
|
rsvs = RadioSettingValueString(0, 255, msg, autopad=False)
|
1021
|
rsvs.set_mutable(False)
|
1022
|
rs = RadioSetting('dummy_memcodes_msg_%i' % msgs.index(msg),
|
1023
|
'Input rule %i' % int(msgs.index(msg)+1), rsvs)
|
1024
|
ret.append(rs)
|
1025
|
for i in range(self._memory_size):
|
1026
|
code = ""
|
1027
|
if self._memobj.memcode[i].code[3] < 0xFF:
|
1028
|
code = self._decode_key(self._memobj.memcode[i].code)
|
1029
|
code = code.zfill(6)
|
1030
|
rsvs = RadioSettingValueString(0, 6, code, autopad=False,
|
1031
|
charset=self.KEY_CHARS)
|
1032
|
rs = RadioSetting('memcode@code@%i' % i,
|
1033
|
'Memory Location (%i) Privacy Code' %
|
1034
|
int(i+1), rsvs)
|
1035
|
ret.append(rs)
|
1036
|
return ret
|
1037
|
|
1038
|
def get_settings(self):
|
1039
|
radio_settings = []
|
1040
|
basic = self._get_settings_basic()
|
1041
|
radio_settings.append(basic)
|
1042
|
adv = self._get_settings_adv()
|
1043
|
radio_settings.append(adv)
|
1044
|
vfoa = self._get_settings_vfo(self._memobj.settings_vfo.vfo_a, 'A')
|
1045
|
radio_settings.append(vfoa)
|
1046
|
vfob = self._get_settings_vfo(self._memobj.settings_vfo.vfo_b, 'B')
|
1047
|
radio_settings.append(vfob)
|
1048
|
sk = self._get_settings_sidekeys()
|
1049
|
radio_settings.append(sk)
|
1050
|
dtmf = self._get_settings_dtmf()
|
1051
|
radio_settings.append(dtmf)
|
1052
|
ccn = self._get_custom_channel_names()
|
1053
|
radio_settings.append(ccn)
|
1054
|
can = self._get_custom_ani_names()
|
1055
|
radio_settings.append(can)
|
1056
|
ani = self._get_anicodes()
|
1057
|
radio_settings.append(ani)
|
1058
|
mcodes = self._get_memcodes()
|
1059
|
radio_settings.append(mcodes)
|
1060
|
if RT490_EXPERIMENTAL:
|
1061
|
ks = self._get_settings_ks()
|
1062
|
radio_settings.append(ks)
|
1063
|
bands = self._get_settings_bands()
|
1064
|
radio_settings.append(bands)
|
1065
|
top = RadioSettings(*radio_settings)
|
1066
|
return top
|
1067
|
|
1068
|
def get_raw_memory(self, number):
|
1069
|
return repr(self._memobj.memory[number])
|
1070
|
|
1071
|
# TODO Add Code when RadioSettingValueString is fixed
|
1072
|
def _get_extra(self, _mem, num):
|
1073
|
group = RadioSettingGroup('extra', 'Extra')
|
1074
|
# LOG.debug("Get extra %i" % num)
|
1075
|
|
1076
|
s = RadioSetting('bcl', 'Busy Channel Lockout',
|
1077
|
RadioSettingValueBoolean(_mem.bcl))
|
1078
|
group.append(s)
|
1079
|
|
1080
|
dcp = int(_mem.dcp)
|
1081
|
if dcp > len(self.FHSS_LIST)-1:
|
1082
|
_mem.dcp = cur = 0
|
1083
|
LOG.debug('DCP ID / FHSS overflow for channel %d' % num)
|
1084
|
cur = self.FHSS_LIST[dcp]
|
1085
|
s = RadioSetting('dcp', 'FHSS (Encryption)',
|
1086
|
RadioSettingValueList(self.FHSS_LIST, cur))
|
1087
|
group.append(s)
|
1088
|
|
1089
|
# Does not work, no error, why ??? TODO
|
1090
|
"""
|
1091
|
code = ""
|
1092
|
if self._memobj.memcode[num-1].code[3] < 0xFF:
|
1093
|
code = self._decode_key(self._memobj.memcode[num-1].code)
|
1094
|
code = code.zfill(6)
|
1095
|
LOG.debug('CODE "%s"' % code)
|
1096
|
s = RadioSetting('dcp_code', 'DCP code',
|
1097
|
RadioSettingValueString(0, 6, code,
|
1098
|
autopad=False,
|
1099
|
charset=self.KEY_CHARS))
|
1100
|
group.append(s) """
|
1101
|
|
1102
|
pttid = int(_mem.pttid)
|
1103
|
if pttid > len(self.PTTID)-1:
|
1104
|
_mem.pttid = cur = 0
|
1105
|
LOG.debug('PTTID overflow for channel %d' % num)
|
1106
|
cur = self.PTTID[pttid]
|
1107
|
s = RadioSetting('pttid', 'Send DTMF Code (PTT ID)',
|
1108
|
RadioSettingValueList(self.PTTID, cur))
|
1109
|
group.append(s)
|
1110
|
|
1111
|
cur = self.SIGNAL[int(_mem.signal)]
|
1112
|
s = RadioSetting('signal', 'PTT ID Code (S-Code)',
|
1113
|
RadioSettingValueList(self.SIGNAL, cur))
|
1114
|
group.append(s)
|
1115
|
|
1116
|
s = RadioSetting('learn',
|
1117
|
'Use Memory Privacy Code as Tx/Rx DCS (Learn)',
|
1118
|
RadioSettingValueBoolean(_mem.learn))
|
1119
|
group.append(s)
|
1120
|
|
1121
|
return group
|
1122
|
|
1123
|
# TODO Add Code when RadioSettingValueString is fixed
|
1124
|
def _set_extra(self, _mem, mem):
|
1125
|
# memidx = mem.number - 1 # commented because not used
|
1126
|
_mem.bcl = int(mem.extra['bcl'].value)
|
1127
|
_mem.dcp = int(mem.extra['dcp'].value)
|
1128
|
_mem.pttid = int(mem.extra['pttid'].value)
|
1129
|
_mem.signal = int(mem.extra['signal'].value)
|
1130
|
# self._memobj.memcode[mem.number].code = \
|
1131
|
# self._encode_key(mem.extra['dcp_code'].value)
|
1132
|
if (int(mem.extra['learn'].value) > 0) and \
|
1133
|
(self._memobj.memcode[mem.number-1].code[3] == 0xA0):
|
1134
|
_mem.learn = 1
|
1135
|
elif (int(mem.extra['learn'].value) > 0) and \
|
1136
|
(self._memobj.memcode[mem.number-1].code[3] != 0xA0):
|
1137
|
_mem.learn = 0
|
1138
|
raise InvalidValueError(dedent("""\
|
1139
|
>>Use Memory Privacy Code as Tx/Rx DCS (Learn)<< requires
|
1140
|
that a memory code has been previously set for this memory.
|
1141
|
|
1142
|
Go in 'Settings' -> 'Memory Channel Privacy Codes' and set
|
1143
|
a code for the current memory before enabling 'Learn'.
|
1144
|
"""))
|
1145
|
else:
|
1146
|
_mem.learn = 0
|
1147
|
|
1148
|
def _is_txinh(self, _mem):
|
1149
|
raw_tx = ""
|
1150
|
for i in range(0, 4):
|
1151
|
raw_tx += _mem.txfreq[i].get_raw()
|
1152
|
return raw_tx == "\xFF\xFF\xFF\xFF"
|
1153
|
|
1154
|
def get_memory(self, num):
|
1155
|
memidx = num - 1
|
1156
|
_mem = self._memobj.memory[memidx]
|
1157
|
_nam = self._memobj.memname[memidx]
|
1158
|
|
1159
|
mem = chirp_common.Memory()
|
1160
|
mem.number = num
|
1161
|
if int(_mem.rxfreq) == 166666665:
|
1162
|
mem.empty = True
|
1163
|
return mem
|
1164
|
|
1165
|
mem.name = ''.join([str(c) for c in _nam.name
|
1166
|
if ord(str(c)) < 127]).rstrip()
|
1167
|
mem.freq = int(_mem.rxfreq) * 10
|
1168
|
offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
|
1169
|
if self._is_txinh(_mem) or _mem.tx_enable == 0:
|
1170
|
mem.duplex = 'off'
|
1171
|
# _mem.txfreq = _mem.rxfreq # TODO REMOVE (force fix broken saves)
|
1172
|
elif offset == 0:
|
1173
|
mem.duplex = ''
|
1174
|
mem.offset = 0
|
1175
|
elif abs(offset) < 100000000:
|
1176
|
mem.duplex = offset < 0 and '-' or '+'
|
1177
|
mem.offset = abs(offset)
|
1178
|
else:
|
1179
|
mem.duplex = 'split'
|
1180
|
mem.offset = int(_mem.txfreq) * 10
|
1181
|
|
1182
|
mem.power = self.POWER_LEVELS[_mem.power]
|
1183
|
|
1184
|
if _mem.unknown3_0 and _mem.narrow:
|
1185
|
mem.mode = 'NAM'
|
1186
|
elif _mem.unknown3_0 and not _mem.narrow:
|
1187
|
mem.mode = 'AM'
|
1188
|
elif not _mem.unknown3_0 and _mem.narrow:
|
1189
|
mem.mode = 'NFM'
|
1190
|
elif not _mem.unknown3_0 and not _mem.narrow:
|
1191
|
mem.mode = 'FM'
|
1192
|
else:
|
1193
|
LOG.exception('Failed to get mode for %i' % num)
|
1194
|
|
1195
|
mem.skip = '' if _mem.scan else 'S'
|
1196
|
|
1197
|
# LOG.warning('got txtone: %s' % repr(self._decode_tone(_mem.txtone)))
|
1198
|
# LOG.warning('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone)))
|
1199
|
txtone = self._decode_tone(_mem.txtone)
|
1200
|
rxtone = self._decode_tone(_mem.rxtone)
|
1201
|
chirp_common.split_tone_decode(mem, txtone, rxtone)
|
1202
|
try:
|
1203
|
mem.extra = self._get_extra(_mem, num)
|
1204
|
except Exception:
|
1205
|
LOG.exception('Failed to get extra for %i' % num)
|
1206
|
return mem
|
1207
|
|
1208
|
def set_memory(self, mem):
|
1209
|
memidx = mem.number - 1
|
1210
|
_mem = self._memobj.memory[memidx]
|
1211
|
_nam = self._memobj.memname[memidx]
|
1212
|
|
1213
|
if mem.empty:
|
1214
|
_mem.set_raw(b'\xff' * 16)
|
1215
|
_nam.set_raw(b'\xff' * 16)
|
1216
|
return
|
1217
|
|
1218
|
if int(_mem.rxfreq) == 166666665:
|
1219
|
LOG.debug('Initializing new memory %i' % memidx)
|
1220
|
_mem.set_raw(b'\x00' * 16)
|
1221
|
|
1222
|
_nam.name = mem.name.ljust(12, chr(255)) # with xFF pad (mimic factory
|
1223
|
# behavior)
|
1224
|
|
1225
|
_mem.rxfreq = mem.freq / 10
|
1226
|
_mem.tx_enable = 1
|
1227
|
if mem.duplex == '':
|
1228
|
_mem.txfreq = mem.freq / 10
|
1229
|
elif mem.duplex == 'split':
|
1230
|
_mem.txfreq = mem.offset / 10
|
1231
|
elif mem.duplex == 'off':
|
1232
|
_mem.tx_enable = 0
|
1233
|
_mem.txfreq = mem.freq / 10 # Optional but keeps compat with
|
1234
|
# vendor software
|
1235
|
elif mem.duplex == '-':
|
1236
|
_mem.txfreq = (mem.freq - mem.offset) / 10
|
1237
|
elif mem.duplex == '+':
|
1238
|
_mem.txfreq = (mem.freq + mem.offset) / 10
|
1239
|
else:
|
1240
|
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)
|
1241
|
|
1242
|
txtone, rxtone = chirp_common.split_tone_encode(mem)
|
1243
|
# LOG.warning('tx tone is %s' % repr(txtone))
|
1244
|
# LOG.warning('rx tone is %s' % repr(rxtone))
|
1245
|
_mem.txtone = self._encode_tone(*txtone)
|
1246
|
_mem.rxtone = self._encode_tone(*rxtone)
|
1247
|
|
1248
|
try:
|
1249
|
_mem.power = self.POWER_LEVELS.index(mem.power)
|
1250
|
except ValueError:
|
1251
|
_mem.power = 0
|
1252
|
|
1253
|
if int(_mem.rxfreq) < 30000000:
|
1254
|
_mem.unknown3_0 = mem.mode in ['AM', 'NAM']
|
1255
|
else:
|
1256
|
_mem.unknown3_0 = 0
|
1257
|
_mem.narrow = mem.mode[0] == 'N'
|
1258
|
|
1259
|
_mem.scan = mem.skip != 'S'
|
1260
|
|
1261
|
if mem.extra:
|
1262
|
self._set_extra(_mem, mem)
|
1263
|
|
1264
|
def sync_out(self):
|
1265
|
try:
|
1266
|
do_upload(self)
|
1267
|
except errors.RadioError:
|
1268
|
# Pass through any real errors we raise
|
1269
|
raise
|
1270
|
except Exception:
|
1271
|
# If anything unexpected happens, make sure we raise
|
1272
|
# a RadioError and log the problem
|
1273
|
LOG.exception('Unexpected error during upload')
|
1274
|
raise errors.RadioError('Unexpected error communicating '
|
1275
|
'with the radio')
|
1276
|
|
1277
|
def sync_in(self):
|
1278
|
"""Download from radio"""
|
1279
|
try:
|
1280
|
data = do_download(self)
|
1281
|
except errors.RadioError:
|
1282
|
# Pass through any real errors we raise
|
1283
|
raise
|
1284
|
except Exception:
|
1285
|
# If anything unexpected happens, make sure we raise
|
1286
|
# a RadioError and log the problem
|
1287
|
LOG.exception('Unexpected error during download')
|
1288
|
raise errors.RadioError('Unexpected error communicating '
|
1289
|
'with the radio')
|
1290
|
self._mmap = data
|
1291
|
self.process_mmap()
|
1292
|
|
1293
|
def process_mmap(self):
|
1294
|
self._memobj = bitwise.parse(MEM_FORMAT_RT490 %
|
1295
|
{"memsize": self._memory_size},
|
1296
|
self._mmap)
|
1297
|
|
1298
|
def get_features(self): # GOOD ?
|
1299
|
rf = chirp_common.RadioFeatures()
|
1300
|
rf.has_settings = True
|
1301
|
rf.has_bank = False
|
1302
|
rf.has_cross = True
|
1303
|
rf.has_rx_dtcs = True
|
1304
|
rf.has_dtcs_polarity = True
|
1305
|
rf.has_tuning_step = False
|
1306
|
rf.can_odd_split = True
|
1307
|
rf.has_name = True
|
1308
|
rf.valid_name_length = 12
|
1309
|
rf.valid_skips = ["", "S"]
|
1310
|
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
|
1311
|
rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS",
|
1312
|
"DTCS->Tone", "->Tone", "DTCS->DTCS"]
|
1313
|
rf.valid_power_levels = self.POWER_LEVELS
|
1314
|
rf.valid_duplexes = ["", "+", "-", "split", "off"]
|
1315
|
rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
|
1316
|
rf.memory_bounds = (1, 256)
|
1317
|
rf.valid_tuning_steps = self.TUNING_STEPS
|
1318
|
rf.valid_bands = [(108000000, 136000000),
|
1319
|
(136000000, 180000000),
|
1320
|
(200000000, 260000000),
|
1321
|
(350000000, 400000000),
|
1322
|
(400000000, 520000000)]
|
1323
|
return rf
|
1324
|
|
1325
|
@classmethod
|
1326
|
def get_prompts(cls):
|
1327
|
rp = chirp_common.RadioPrompts()
|
1328
|
rp.pre_upload = _(dedent("""\
|
1329
|
This driver is in development and should be considered
|
1330
|
as experimental.
|
1331
|
"""))
|
1332
|
rp.experimental = _(dedent("""\
|
1333
|
This driver is in development and should be considered
|
1334
|
as experimental.
|
1335
|
"""))
|
1336
|
rp.info = _(dedent("""\
|
1337
|
This driver is in development and should be considered
|
1338
|
as experimental.
|
1339
|
"""))
|
1340
|
return rp
|
1341
|
|
1342
|
def _encode_key(self, key):
|
1343
|
arr = bytearray(4)
|
1344
|
arr[3] = 160
|
1345
|
arr[2] = self.KEY_CHARS.index(key[0]) # << 4
|
1346
|
arr[2] = arr[2] << 4
|
1347
|
arr[2] |= self.KEY_CHARS.index(key[1])
|
1348
|
arr[1] = self.KEY_CHARS.index(key[2]) # << 4
|
1349
|
arr[1] = arr[1] << 4
|
1350
|
arr[1] |= self.KEY_CHARS.index(key[3])
|
1351
|
arr[0] = self.KEY_CHARS.index(key[4]) # << 4
|
1352
|
arr[0] = arr[0] << 4
|
1353
|
arr[0] |= self.KEY_CHARS.index(key[5])
|
1354
|
return arr
|
1355
|
|
1356
|
def _decode_key(self, key):
|
1357
|
ret = ""
|
1358
|
if key[3] == 0xA0:
|
1359
|
ret += self.KEY_CHARS[key[2] >> 4]
|
1360
|
ret += self.KEY_CHARS[key[2] & 0xF]
|
1361
|
ret += self.KEY_CHARS[key[1] >> 4]
|
1362
|
ret += self.KEY_CHARS[key[1] & 0xF]
|
1363
|
ret += self.KEY_CHARS[key[0] >> 4]
|
1364
|
ret += self.KEY_CHARS[key[0] & 0xF]
|
1365
|
LOG.debug('DCP Code: "%s"' % ret)
|
1366
|
return ret
|
1367
|
|
1368
|
def _decode_tone(self, toneval):
|
1369
|
if toneval in (0, 0xFFFF):
|
1370
|
# LOG.debug('no tone value: %s' % toneval)
|
1371
|
return None, None, None
|
1372
|
elif toneval < 670:
|
1373
|
toneval = toneval - 1
|
1374
|
index = toneval % len(DTCS_CODES)
|
1375
|
if index != int(toneval):
|
1376
|
pol = 'R'
|
1377
|
# index -= 1
|
1378
|
else:
|
1379
|
pol = 'N'
|
1380
|
return 'DTCS', DTCS_CODES[index], pol
|
1381
|
else:
|
1382
|
return 'Tone', toneval / 10.0, 'N'
|
1383
|
|
1384
|
def _encode_tone(self, mode, val, pol):
|
1385
|
if not mode:
|
1386
|
return 0x0000
|
1387
|
elif mode == 'Tone':
|
1388
|
return int(val * 10)
|
1389
|
elif mode == 'DTCS':
|
1390
|
index = DTCS_CODES.index(val)
|
1391
|
if pol == 'R':
|
1392
|
index += len(DTCS_CODES)
|
1393
|
index += 1
|
1394
|
# LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index))
|
1395
|
return index
|
1396
|
else:
|
1397
|
raise errors.RadioError('Unsupported tone mode %r' % mode)
|
1398
|
|
1399
|
@classmethod
|
1400
|
def match_model(cls, filedata, filename):
|
1401
|
# This radio has always been post-metadata, so never do
|
1402
|
# old-school detection
|
1403
|
return False
|
1404
|
|
1405
|
|
1406
|
class MML8629Alias(chirp_common.Alias):
|
1407
|
VENDOR = "MMLradio"
|
1408
|
MODEL = "JC-8629"
|
1409
|
|
1410
|
|
1411
|
class JJCC8629Alias(chirp_common.Alias):
|
1412
|
VENDOR = "JJCC"
|
1413
|
MODEL = "JC-8629"
|
1414
|
|
1415
|
|
1416
|
class SocotranJC8629Alias(chirp_common.Alias):
|
1417
|
VENDOR = "Socotran"
|
1418
|
MODEL = "JC-8629"
|
1419
|
|
1420
|
|
1421
|
class SocotranFB8629Alias(chirp_common.Alias):
|
1422
|
VENDOR = "Socotran"
|
1423
|
MODEL = "FB-8629"
|
1424
|
|
1425
|
|
1426
|
class Jianpai8629Alias(chirp_common.Alias):
|
1427
|
VENDOR = "Jianpai"
|
1428
|
MODEL = "8800 Plus"
|
1429
|
|
1430
|
|
1431
|
class Boristone8RSAlias(chirp_common.Alias):
|
1432
|
VENDOR = "Boristone"
|
1433
|
MODEL = "8RS"
|
1434
|
|
1435
|
|
1436
|
class AbbreeAR869Alias(chirp_common.Alias):
|
1437
|
VENDOR = "Abbree"
|
1438
|
MODEL = "AR-869"
|
1439
|
|
1440
|
|
1441
|
class HamGeekHG590Alias(chirp_common.Alias):
|
1442
|
VENDOR = "HamGeek"
|
1443
|
MODEL = "HG-590"
|
1444
|
|
1445
|
|
1446
|
@directory.register
|
1447
|
class RT490RadioGeneric(RT490Radio):
|
1448
|
ALIASES = [MML8629Alias, JJCC8629Alias, SocotranJC8629Alias,
|
1449
|
SocotranFB8629Alias, Jianpai8629Alias, Boristone8RSAlias,
|
1450
|
AbbreeAR869Alias, HamGeekHG590Alias]
|
1451
|
|
1452
|
|
1453
|
IMHEX_DESC = """
|
1454
|
// Perfect tool for binary reverse engineering
|
1455
|
// https://github.com/WerWolv/ImHex
|
1456
|
// Below is the pattern
|
1457
|
"""
|
1458
|
IMHEX_PATTERN = """
|
1459
|
struct memory { // Memory settings
|
1460
|
u8 rxfreq[4];
|
1461
|
u8 txfreq[4];
|
1462
|
u16 rxtone;
|
1463
|
u16 txtone;
|
1464
|
u8 signal; // int 0->14, Signal 1->15
|
1465
|
u8 pttid; // ['OFF', 'BOT', 'EOT', 'Both']
|
1466
|
u8 dcp_power; // POWER_LEVELS
|
1467
|
u8 unknown3_0_narrow_unknown3_1_bcl_scan_tx_enable_learn; // bool ??? TODO
|
1468
|
};
|
1469
|
struct memname {
|
1470
|
char name[12];
|
1471
|
padding[4];
|
1472
|
};
|
1473
|
struct memcode {
|
1474
|
u8 code[4];
|
1475
|
};
|
1476
|
struct custom_ani_names {
|
1477
|
char name[12];
|
1478
|
padding[4];
|
1479
|
};
|
1480
|
struct anicodes {
|
1481
|
u8 anicode[6];
|
1482
|
padding[10];
|
1483
|
};
|
1484
|
struct custom_channel_names {
|
1485
|
char name[12];
|
1486
|
padding[4];
|
1487
|
};
|
1488
|
bitfield workmode {
|
1489
|
b : 4;
|
1490
|
a : 4;
|
1491
|
};
|
1492
|
struct settings {
|
1493
|
u8 squelch; // 0: int 0 -> 9
|
1494
|
u8 savemode; // 1: ['OFF', 'Normal', 'Super', 'Deep']
|
1495
|
u8 vox; // 2: off=0, 1 -> 9
|
1496
|
u8 backlight; // 3: ['OFF', '5s', '15s', '20s', '30s', '1m', '2m',
|
1497
|
// '3m']
|
1498
|
u8 tdr; // 4: bool
|
1499
|
u8 timeout; // 5: n*30seconds, 0-240s
|
1500
|
u8 beep; // 6: bool
|
1501
|
u8 voice; // 7: bool
|
1502
|
u8 byte_not_used_10; // 8: Allways 1
|
1503
|
u8 dtmfst; // 9: ['OFF', 'KB Side Tone', 'ANI Side Tone',
|
1504
|
// 'KB ST + ANI ST']
|
1505
|
u8 scanmode; // 10: ['TO', 'CO', 'SE']
|
1506
|
u8 pttid; // 11: ['OFF', 'BOT', 'EOT', 'Both']
|
1507
|
u8 pttiddelay; // 12: ['0', '100ms', '200ms', '400ms', '600ms',
|
1508
|
// '800ms', '1000ms']
|
1509
|
u8 cha_disp; // 13: ['Name', 'Freq', 'Channel ID']
|
1510
|
u8 chb_disp; // 14: ['Name', 'Freq', 'Channel ID']
|
1511
|
u8 bcl; // 15: bool
|
1512
|
u8 autolock; // 0: ['OFF', '5s', '10s', 15s']
|
1513
|
u8 alarm_mode; // 1: ['Site', 'Tone', 'Code']
|
1514
|
u8 alarmsound; // 2: bool
|
1515
|
u8 txundertdr; // 3: ['OFF', 'A', 'B']
|
1516
|
u8 tailnoiseclear; // 4: [off, on]
|
1517
|
u8 rptnoiseclear; // 5: n*100ms, 0-1000
|
1518
|
u8 rptnoisedelay; // 6: n*100ms, 0-1000
|
1519
|
u8 roger; // 7: bool
|
1520
|
u8 active_channel; // 8: 0 or 1
|
1521
|
u8 fmradio; // 9: boolean, inverted
|
1522
|
workmode _workmode; // 10: up ['VFO', 'CH Mode']
|
1523
|
u8 kblock; // 11: bool // TODO TEST WITH autolock
|
1524
|
u8 powermsg; // 12: 0=Image / 1=Voltage
|
1525
|
u8 byte_not_used_21; // 13: Allways 0
|
1526
|
u8 rpttone; // 14: ['1000Hz', '1450Hz', '1750Hz', '2100Hz']
|
1527
|
u8 byte_not_used_22; // 15: pad with xFF
|
1528
|
u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)]
|
1529
|
// '0.5s' to '2.0s'
|
1530
|
u8 timer_menu_quit; // 1: ['5s', '10s', '15s', '20s', '25s', '30s',
|
1531
|
// '35s', '40s', '45s', '50s', '60s']
|
1532
|
u8 byte_not_used_30; // 2: pad with xFF
|
1533
|
u8 byte_not_used_31; // 3: pad with xFF
|
1534
|
u8 enable_killsw; // 4: bool
|
1535
|
u8 display_ani; // 5: bool
|
1536
|
u8 byte_not_used_32; // 6: pad with xFF
|
1537
|
u8 enable_gps; // 7: bool
|
1538
|
u8 scan_dcs; // 8: ['All', 'Receive', 'Transmit']
|
1539
|
u8 ani_id; // 9: int 0-59 (ANI 1-60)
|
1540
|
u8 rx_time; // 10: bool
|
1541
|
padding[5]; // 11: Pad xFF
|
1542
|
u8 cha_memidx; // 0: Memory index when channel A use memories
|
1543
|
u8 byte_not_used_40;
|
1544
|
u8 chb_memidx; // 2: Memory index when channel B use memories
|
1545
|
u8 byte_not_used_41;
|
1546
|
padding[10];
|
1547
|
u16 fmpreset;
|
1548
|
};
|
1549
|
struct settings_vfo_chan {
|
1550
|
u8 rxfreq[8]; // 0
|
1551
|
u16 rxtone; // 8
|
1552
|
u16 txtone; // 10
|
1553
|
u16 byte_not_used0; // 12 Pad xFF
|
1554
|
u8 sftd_signal; // 14 int 0->14, Signal 1->15
|
1555
|
u8 byte_not_used1; // 15 Pad xFF
|
1556
|
u8 power; // 16:0 POWER_LEVELS
|
1557
|
u8 fhss_narrow; // 17 bool true=NFM false=FM
|
1558
|
u8 byte_not_used2; // 18 Pad xFF but received 0x00 ???
|
1559
|
u8 freqstep; // 19:3 ['2.5 KHz', '5.0 KHz', '6.25 KHz',
|
1560
|
// '10.0 KHz', '12.5 KHz', '20.0 KHz',
|
1561
|
// '25.0 KHz', '50.0 KHz']
|
1562
|
u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO
|
1563
|
u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST
|
1564
|
u8 byte_not_used4; // 27:11 Pad xFF
|
1565
|
u8 byte_not_used5; // 28 Pad xFF
|
1566
|
u8 byte_not_used6; // 29 Pad xFF
|
1567
|
u8 byte_not_used7; // 30 Pad xFF
|
1568
|
u8 byte_not_used8; // 31:15 Pad xFF
|
1569
|
};
|
1570
|
struct settings_vfo {
|
1571
|
settings_vfo_chan vfo_a;
|
1572
|
settings_vfo_chan vfo_b;
|
1573
|
};
|
1574
|
struct settings_sidekeys { // Values from Radio
|
1575
|
u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
1576
|
// '29': 'Search, '1': 'PPT B' }
|
1577
|
u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
1578
|
// '29': 'Search' }
|
1579
|
u8 pf3_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan',
|
1580
|
// '29': 'Search' }
|
1581
|
u8 ffpad; // Pad xFF
|
1582
|
};
|
1583
|
struct dtmfcode {
|
1584
|
u8 code[5]; // 5 digits DTMF
|
1585
|
padding[11]; // Pad xFF
|
1586
|
};
|
1587
|
struct settings_dtmf { // @15296+3x16
|
1588
|
u8 byte_not_used1; // 0: Pad xFF
|
1589
|
u8 byte_not_used2; // 1: Pad xFF
|
1590
|
u8 byte_not_used3; // 2: Pad xFF
|
1591
|
u8 byte_not_used4; // 3: Pad xFF
|
1592
|
u8 byte_not_used5; // 4: Pad xFF
|
1593
|
u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO
|
1594
|
u8 pttid; // 6: [off, BOT, EOT, Both]
|
1595
|
u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms']
|
1596
|
u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms']
|
1597
|
};
|
1598
|
struct settings_dtmf_global {
|
1599
|
dtmfcode settings_dtmfgroup[15];
|
1600
|
settings_dtmf _settings_dtmf;
|
1601
|
};
|
1602
|
struct settings_killswitch {
|
1603
|
u8 kill_dtmf[6]; // 0: Kill DTMF
|
1604
|
padding[2]; // Pad xFF
|
1605
|
u8 revive_dtmf[6]; // 8: Revive DTMF
|
1606
|
padding[2]; // Pad xFF
|
1607
|
};
|
1608
|
struct management_settings {
|
1609
|
u8 unknown_data_0[16];
|
1610
|
u8 unknown_data_1;
|
1611
|
u8 active; // Bool radio killed (killed=0, active=1)
|
1612
|
padding[46];
|
1613
|
};
|
1614
|
struct band {
|
1615
|
u8 enable; // 0 bool / enable-disable Tx on band
|
1616
|
u8 freq_low[2]; // 1 lowest band frequency
|
1617
|
u8 freq_high[2]; // 3 highest band frequency
|
1618
|
};
|
1619
|
struct settings_bands {
|
1620
|
band band136; // 0 Settings for 136MHz band
|
1621
|
band band400; // 5 Settings for 400MHz band
|
1622
|
band band200; // 10 Settings for 200MHz band
|
1623
|
padding[1]; // 15
|
1624
|
band band350; // 0 Settings for 350MHz band
|
1625
|
padding[43];// 5
|
1626
|
};
|
1627
|
|
1628
|
memory mem[256] @ 0x0000;
|
1629
|
memname mname[256] @ 0x1000;
|
1630
|
memcode mcode[256] @ 0x2000;
|
1631
|
custom_ani_names caninames[10] @ 0x3400;
|
1632
|
anicodes anic[60] @ 0x3500;
|
1633
|
custom_channel_names ccnames[10] @ 0x3900;
|
1634
|
settings _settings @ 0x3A00;
|
1635
|
settings_vfo _settings_vfo @ 0x3A40;
|
1636
|
settings_sidekeys _settings_sidekeys @ 0x3A80;
|
1637
|
settings_dtmf_global _settings_dtmf_global @ 0x3B00;
|
1638
|
settings_killswitch _settings_killswitch @ 0x3C00;
|
1639
|
management_settings _management_settings @ 0x3F80;
|
1640
|
settings_bands _settings_bands @ 0x3FC0;
|
1641
|
"""
|