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