1
|
# Version 1.0 for TYT-UV88
|
2
|
# Initial radio protocol decode, channels and memory layout
|
3
|
# by James Berry <james@coppermoth.com>, Summer 2020
|
4
|
# Additional configuration and help, Jim Unroe <rock.unroe@gmail.com>
|
5
|
#
|
6
|
# This program is free software: you can redistribute it and/or modify
|
7
|
# it under the terms of the GNU General Public License as published by
|
8
|
# the Free Software Foundation, either version 2 of the License, or
|
9
|
# (at your option) any later version.
|
10
|
#
|
11
|
# This program is distributed in the hope that it will be useful,
|
12
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
# GNU General Public License for more details.
|
15
|
#
|
16
|
# You should have received a copy of the GNU General Public License
|
17
|
# along with this program.
|
18
|
|
19
|
import time
|
20
|
import struct
|
21
|
import logging
|
22
|
import re
|
23
|
import math
|
24
|
from chirp import chirp_common, directory, memmap
|
25
|
from chirp import bitwise, errors, util
|
26
|
from chirp.settings import RadioSettingGroup, RadioSetting, \
|
27
|
RadioSettingValueBoolean, RadioSettingValueList, \
|
28
|
RadioSettingValueString, RadioSettingValueInteger, \
|
29
|
RadioSettingValueFloat, RadioSettings, InvalidValueError
|
30
|
from textwrap import dedent
|
31
|
|
32
|
LOG = logging.getLogger(__name__)
|
33
|
|
34
|
MEM_FORMAT = """
|
35
|
struct chns {
|
36
|
ul32 rxfreq;
|
37
|
ul32 txfreq;
|
38
|
ul16 scramble:4
|
39
|
rxtone:12; //decode:12
|
40
|
ul16 decodeDSCI:1
|
41
|
encodeDSCI:1
|
42
|
unk1:1
|
43
|
unk2:1
|
44
|
txtone:12; //encode:12
|
45
|
u8 power:2
|
46
|
wide:2
|
47
|
b_lock:2
|
48
|
unk3:2;
|
49
|
u8 unk4:3
|
50
|
signal:2
|
51
|
displayName:1
|
52
|
unk5:2;
|
53
|
u8 unk6:2
|
54
|
pttid:2
|
55
|
step:4; // not required
|
56
|
u8 name[6];
|
57
|
};
|
58
|
|
59
|
struct vfo {
|
60
|
ul32 rxfreq;
|
61
|
ul32 txfreq; // displayed as an offset
|
62
|
ul16 scramble:4
|
63
|
rxtone:12; //decode:12
|
64
|
ul16 decodeDSCI:1
|
65
|
encodeDSCI:1
|
66
|
unk1:1
|
67
|
unk2:1
|
68
|
txtone:12; //encode:12
|
69
|
u8 power:2
|
70
|
wide:2
|
71
|
b_lock:2
|
72
|
unk3:2;
|
73
|
u8 unk4:3
|
74
|
signal:2
|
75
|
displayName:1
|
76
|
unk5:2;
|
77
|
u8 unk6:2
|
78
|
pttid:2
|
79
|
step:4;
|
80
|
u8 name[6];
|
81
|
};
|
82
|
|
83
|
struct chname {
|
84
|
u8 extra_name[10];
|
85
|
};
|
86
|
|
87
|
#seekto 0x0000;
|
88
|
struct chns chan_mem[199];
|
89
|
|
90
|
#seekto 0x1960;
|
91
|
struct chname chan_name[199];
|
92
|
|
93
|
#seekto 0x1180;
|
94
|
struct {
|
95
|
u8 bitmap[26]; // one bit for each channel marked in use
|
96
|
} chan_avail;
|
97
|
|
98
|
#seekto 0x11A0;
|
99
|
struct {
|
100
|
u8 bitmap[26]; // one bit for each channel skipped
|
101
|
} chan_skip;
|
102
|
|
103
|
#seekto 0x1140;
|
104
|
struct {
|
105
|
u8 autoKeylock:1, // 0x1140 [18] *OFF, On
|
106
|
unk_bit6_5:1, //
|
107
|
vfomrmodeb:1, // *VFO B, MR B
|
108
|
vfomrmode:1, // *VFO, MR
|
109
|
unk_bit3_0:4; //
|
110
|
u8 mrAch; // 0x1141 MR A Channel #
|
111
|
u8 mrBch; // 0x1142 MR B Channel #
|
112
|
u8 unk_bit7_3:5, //
|
113
|
ab:1, // * A, B
|
114
|
unk_bit1_0:2; //
|
115
|
} workmodesettings;
|
116
|
|
117
|
#seekto 0x1160;
|
118
|
struct {
|
119
|
u8 introScreen1[12]; // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
|
120
|
// text characters)
|
121
|
u8 offFreqVoltage : 3, // 0x116C unknown referred to in code but not on
|
122
|
// screen
|
123
|
unk_bit4 : 1, //
|
124
|
sqlLevel : 4; // [05] *OFF, 1-9
|
125
|
u8 beep : 1 // 0x116D [09] *OFF, On
|
126
|
callKind : 2, // code says 1750,2100,1000,1450 as options
|
127
|
// not on screen
|
128
|
introScreen: 2, // [20] *OFF, Voltage, Char String
|
129
|
unkstr2: 2, //
|
130
|
txChSelect : 1; // [02] *Last CH, Main CH
|
131
|
u8 autoPowOff : 3, // 0x116E not on screen? OFF, 30Min, 1HR, 2HR
|
132
|
unk : 1, //
|
133
|
tot : 4; // [11] *OFF, 30 Second, 60 Second, 90 Second,
|
134
|
// ... , 270 Second
|
135
|
u8 unk_bit7:1, // 0x116F
|
136
|
roger:1, // [14] *OFF, On
|
137
|
dailDef:1, // Unknown - 'Volume, Frequency'
|
138
|
language:1, // ?Chinese, English (English only FW BQ1.38+)
|
139
|
unk_bit3:1, //
|
140
|
endToneElim:1, // *OFF, Frequency
|
141
|
unkCheckBox1:1, //
|
142
|
unkCheckBox2:1; //
|
143
|
u8 scanResumeTime : 2, // 0x1170 2S, 5S, 10S, 15S (not on screen)
|
144
|
disMode : 2, // [33] *Frequency, Channel, Name
|
145
|
scanType: 2, // [17] *To, Co, Se
|
146
|
ledMode: 2; // [07] *Off, On, Auto
|
147
|
u8 unky; // 0x1171
|
148
|
u8 str6; // 0x1172 Has flags to do with logging - factory
|
149
|
// enabled (bits 16,64,128)
|
150
|
u8 unk; // 0x1173
|
151
|
u8 swAudio : 1, // 0x1174 [19] *OFF, On
|
152
|
radioMoni : 1, // [34] *OFF, On
|
153
|
keylock : 1, // [18] *OFF, On
|
154
|
dualWait : 1, // [06] *OFF, On
|
155
|
unk_bit3 : 1, //
|
156
|
light : 3; // [08] *1, 2, 3, 4, 5, 6, 7
|
157
|
u8 voxSw : 1, // 0x1175 [13] *OFF, On
|
158
|
voxDelay: 4, // *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S,
|
159
|
// 4.0S, 4.5S, 5.0S
|
160
|
voxLevel : 3; // [03] *1, 2, 3, 4, 5, 6, 7
|
161
|
u8 str9 : 4, // 0x1176
|
162
|
saveMode : 2, // [16] *OFF, 1:1, 1:2, 1:4
|
163
|
keyMode : 2; // [32] *ALL, PTT, KEY, Key & Side Key
|
164
|
u8 unk2; // 0x1177
|
165
|
u8 unk3; // 0x1178
|
166
|
u8 unk4; // 0x1179
|
167
|
u8 name2[6]; // 0x117A unused
|
168
|
} basicsettings;
|
169
|
|
170
|
#seekto 0x191E;
|
171
|
struct {
|
172
|
u8 unknown191e:4, //
|
173
|
region:4; // 0x191E Radio Region (read only)
|
174
|
// 0 = Unlocked TX: 136-174 MHz / 400-480 MHz
|
175
|
// 2-3 = Unknown
|
176
|
// 3 = EU TX: 144-146 MHz / 430-440 MHz
|
177
|
// 4 = US TX: 144-148 MHz / 420-450 MHz
|
178
|
// 5-15 = Unknown
|
179
|
} settings2;
|
180
|
|
181
|
#seekto 0x1940;
|
182
|
struct {
|
183
|
char name1[15]; // Intro Screen Line 1 (16 alpha text characters)
|
184
|
u8 unk1;
|
185
|
char name2[15]; // Intro Screen Line 2 (16 alpha text characters)
|
186
|
u8 unk2;
|
187
|
} openradioname;
|
188
|
|
189
|
"""
|
190
|
|
191
|
MEM_SIZE = 0x22A0
|
192
|
BLOCK_SIZE = 0x20
|
193
|
STIMEOUT = 2
|
194
|
BAUDRATE = 57600
|
195
|
|
196
|
# Channel power: 3 levels
|
197
|
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
|
198
|
chirp_common.PowerLevel("Mid", watts=2.50),
|
199
|
chirp_common.PowerLevel("Low", watts=0.50)]
|
200
|
|
201
|
SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"]
|
202
|
B_LOCK_LIST = ["OFF", "Sub", "Carrier"]
|
203
|
OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"]
|
204
|
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
|
205
|
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
|
206
|
LIST_STEPS = [str(x) for x in STEPS]
|
207
|
|
208
|
|
209
|
def _clean_buffer(radio):
|
210
|
radio.pipe.timeout = 0.005
|
211
|
junk = radio.pipe.read(256)
|
212
|
radio.pipe.timeout = STIMEOUT
|
213
|
if junk:
|
214
|
LOG.debug("Got %i bytes of junk before starting" % len(junk))
|
215
|
|
216
|
|
217
|
def _rawrecv(radio, amount):
|
218
|
"""Raw read from the radio device"""
|
219
|
data = ""
|
220
|
try:
|
221
|
data = radio.pipe.read(amount)
|
222
|
except Exception:
|
223
|
_exit_program_mode(radio)
|
224
|
msg = "Generic error reading data from radio; check your cable."
|
225
|
raise errors.RadioError(msg)
|
226
|
|
227
|
if len(data) != amount:
|
228
|
_exit_program_mode(radio)
|
229
|
msg = "Error reading from radio: not the amount of data we want."
|
230
|
raise errors.RadioError(msg)
|
231
|
|
232
|
return data
|
233
|
|
234
|
|
235
|
def _rawsend(radio, data):
|
236
|
"""Raw send to the radio device"""
|
237
|
try:
|
238
|
radio.pipe.write(data)
|
239
|
except Exception:
|
240
|
raise errors.RadioError("Error sending data to radio")
|
241
|
|
242
|
|
243
|
def _make_read_frame(addr, length):
|
244
|
frame = "\xFE\xFE\xEE\xEF\xEB"
|
245
|
"""Pack the info in the header format"""
|
246
|
frame += struct.pack(">ih", addr, length)
|
247
|
|
248
|
frame += "\xFD"
|
249
|
# Return the data
|
250
|
return frame
|
251
|
|
252
|
|
253
|
def _make_write_frame(addr, length, data=""):
|
254
|
frame = "\xFE\xFE\xEE\xEF\xE4"
|
255
|
|
256
|
"""Pack the info in the header format"""
|
257
|
output = struct.pack(">ih", addr, length)
|
258
|
# Add the data if set
|
259
|
if len(data) != 0:
|
260
|
output += data
|
261
|
|
262
|
frame += output
|
263
|
frame += _calculate_checksum(output)
|
264
|
|
265
|
frame += "\xFD"
|
266
|
# Return the data
|
267
|
return frame
|
268
|
|
269
|
|
270
|
def _calculate_checksum(data):
|
271
|
num = 0
|
272
|
for x in range(0, len(data)):
|
273
|
num = (num + ord(data[x])) % 256
|
274
|
|
275
|
if num == 0:
|
276
|
return chr(0)
|
277
|
|
278
|
return chr(256 - num)
|
279
|
|
280
|
|
281
|
def _recv(radio, addr, length):
|
282
|
"""Get data from the radio """
|
283
|
|
284
|
data = _rawrecv(radio, length)
|
285
|
|
286
|
# DEBUG
|
287
|
LOG.info("Response:")
|
288
|
LOG.debug(util.hexprint(data))
|
289
|
|
290
|
return data
|
291
|
|
292
|
|
293
|
def _do_ident(radio):
|
294
|
"""Put the radio in PROGRAM mode & identify it"""
|
295
|
radio.pipe.baudrate = BAUDRATE
|
296
|
radio.pipe.parity = "N"
|
297
|
radio.pipe.timeout = STIMEOUT
|
298
|
|
299
|
# Flush input buffer
|
300
|
_clean_buffer(radio)
|
301
|
|
302
|
# Ident radio
|
303
|
magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD"
|
304
|
_rawsend(radio, magic)
|
305
|
ack = _rawrecv(radio, 36)
|
306
|
|
307
|
if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38"
|
308
|
) or not ack.endswith("\xFD"):
|
309
|
_exit_program_mode(radio)
|
310
|
if ack:
|
311
|
LOG.debug(repr(ack))
|
312
|
raise errors.RadioError("Radio did not respond as expected (A)")
|
313
|
|
314
|
return True
|
315
|
|
316
|
|
317
|
def _exit_program_mode(radio):
|
318
|
# This may be the last part of a read
|
319
|
magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD"
|
320
|
_rawsend(radio, magic)
|
321
|
ack = _rawrecv(radio, 7)
|
322
|
if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
|
323
|
_exit_program_mode(radio)
|
324
|
if ack:
|
325
|
LOG.debug(repr(ack))
|
326
|
raise errors.RadioError("Radio did not respond as expected (B)")
|
327
|
|
328
|
|
329
|
def _download(radio):
|
330
|
"""Get the memory map"""
|
331
|
|
332
|
# Put radio in program mode and identify it
|
333
|
_do_ident(radio)
|
334
|
|
335
|
# Enter read mode
|
336
|
magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD"
|
337
|
_rawsend(radio, magic)
|
338
|
ack = _rawrecv(radio, 7)
|
339
|
if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
|
340
|
_exit_program_mode(radio)
|
341
|
if ack:
|
342
|
LOG.debug(repr(ack))
|
343
|
raise errors.RadioError("Radio did not respond to enter read mode")
|
344
|
|
345
|
# UI progress
|
346
|
status = chirp_common.Status()
|
347
|
status.cur = 0
|
348
|
status.max = MEM_SIZE / BLOCK_SIZE
|
349
|
status.msg = "Cloning from radio..."
|
350
|
radio.status_fn(status)
|
351
|
|
352
|
data = ""
|
353
|
for addr in range(0, MEM_SIZE, BLOCK_SIZE):
|
354
|
frame = _make_read_frame(addr, BLOCK_SIZE)
|
355
|
# DEBUG
|
356
|
LOG.debug("Frame=" + util.hexprint(frame))
|
357
|
|
358
|
# Sending the read request
|
359
|
_rawsend(radio, frame)
|
360
|
|
361
|
# Now we read data
|
362
|
d = _recv(radio, addr, BLOCK_SIZE + 13)
|
363
|
|
364
|
LOG.debug("Response Data= " + util.hexprint(d))
|
365
|
|
366
|
if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
|
367
|
LOG.warning("Incorrect start")
|
368
|
if not d.endswith("\xFD"):
|
369
|
LOG.warning("Incorrect end")
|
370
|
# could validate the block data
|
371
|
|
372
|
# Aggregate the data
|
373
|
data += d[11:-2]
|
374
|
|
375
|
# UI Update
|
376
|
status.cur = addr / BLOCK_SIZE
|
377
|
status.msg = "Cloning from radio..."
|
378
|
radio.status_fn(status)
|
379
|
|
380
|
_exit_program_mode(radio)
|
381
|
|
382
|
return data
|
383
|
|
384
|
|
385
|
def _upload(radio):
|
386
|
"""Upload procedure"""
|
387
|
# Put radio in program mode and identify it
|
388
|
_do_ident(radio)
|
389
|
|
390
|
magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD"
|
391
|
_rawsend(radio, magic)
|
392
|
ack = _rawrecv(radio, 7)
|
393
|
if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
|
394
|
_exit_program_mode(radio)
|
395
|
if ack:
|
396
|
LOG.debug(repr(ack))
|
397
|
raise errors.RadioError("Radio did not respond to enter write mode")
|
398
|
|
399
|
# UI progress
|
400
|
status = chirp_common.Status()
|
401
|
status.cur = 0
|
402
|
status.max = MEM_SIZE / BLOCK_SIZE
|
403
|
status.msg = "Cloning to radio..."
|
404
|
radio.status_fn(status)
|
405
|
|
406
|
# The fun starts here
|
407
|
for addr in range(0, MEM_SIZE, BLOCK_SIZE):
|
408
|
# Official programmer skips writing these memory locations
|
409
|
if addr >= 0x1680 and addr < 0x1940:
|
410
|
continue
|
411
|
|
412
|
# Sending the data
|
413
|
data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
|
414
|
|
415
|
frame = _make_write_frame(addr, BLOCK_SIZE, data)
|
416
|
LOG.warning("Frame:%s:" % util.hexprint(frame))
|
417
|
_rawsend(radio, frame)
|
418
|
|
419
|
ack = _rawrecv(radio, 7)
|
420
|
LOG.debug("Response Data= " + util.hexprint(ack))
|
421
|
|
422
|
if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
|
423
|
LOG.warning("Unexpected response")
|
424
|
_exit_program_mode(radio)
|
425
|
msg = "Bad ack writing block 0x%04x" % addr
|
426
|
raise errors.RadioError(msg)
|
427
|
|
428
|
# UI Update
|
429
|
status.cur = addr / BLOCK_SIZE
|
430
|
status.msg = "Cloning to radio..."
|
431
|
radio.status_fn(status)
|
432
|
|
433
|
_exit_program_mode(radio)
|
434
|
|
435
|
|
436
|
def _do_map(chn, sclr, mary):
|
437
|
"""Set or Clear the chn (1-128) bit in mary[] word array map"""
|
438
|
# chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
|
439
|
# mary[] is u8 array, but the map is by nibbles
|
440
|
ndx = int(math.floor((chn - 1) / 8))
|
441
|
bv = (chn - 1) % 8
|
442
|
msk = 1 << bv
|
443
|
mapbit = sclr
|
444
|
if sclr == 1: # Set the bit
|
445
|
mary[ndx] = mary[ndx] | msk
|
446
|
elif sclr == 0: # clear
|
447
|
mary[ndx] = mary[ndx] & (~ msk) # ~ is complement
|
448
|
else: # return current bit state
|
449
|
mapbit = 0
|
450
|
if (mary[ndx] & msk) > 0:
|
451
|
mapbit = 1
|
452
|
return mapbit
|
453
|
|
454
|
|
455
|
@directory.register
|
456
|
class THUV88Radio(chirp_common.CloneModeRadio):
|
457
|
"""TYT UV88 Radio"""
|
458
|
VENDOR = "TYT"
|
459
|
MODEL = "TH-UV88"
|
460
|
MODES = ['WFM', 'FM', 'NFM']
|
461
|
TONES = chirp_common.TONES
|
462
|
DTCS_CODES = chirp_common.DTCS_CODES
|
463
|
NAME_LENGTH = 10
|
464
|
DTMF_CHARS = list("0123456789ABCD*#")
|
465
|
# 136-174, 400-480
|
466
|
VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
|
467
|
|
468
|
# Valid chars on the LCD
|
469
|
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
|
470
|
"`!\"#$%&'()*+,-./:;<=>?@[]^_"
|
471
|
|
472
|
@classmethod
|
473
|
def get_prompts(cls):
|
474
|
rp = chirp_common.RadioPrompts()
|
475
|
rp.info = \
|
476
|
(cls.VENDOR + ' ' + cls.MODEL + '\n')
|
477
|
|
478
|
rp.pre_download = _(dedent("""\
|
479
|
This is an early stage beta driver
|
480
|
"""))
|
481
|
rp.pre_upload = _(dedent("""\
|
482
|
This is an early stage beta driver - upload at your own risk
|
483
|
"""))
|
484
|
return rp
|
485
|
|
486
|
def get_features(self):
|
487
|
rf = chirp_common.RadioFeatures()
|
488
|
rf.has_settings = True
|
489
|
rf.has_bank = False
|
490
|
rf.has_comment = False
|
491
|
rf.has_tuning_step = False # Not as chan feature
|
492
|
rf.valid_tuning_steps = STEPS
|
493
|
rf.can_odd_split = True
|
494
|
rf.has_name = True
|
495
|
rf.has_offset = True
|
496
|
rf.has_mode = True
|
497
|
rf.has_dtcs = True
|
498
|
rf.has_rx_dtcs = True
|
499
|
rf.has_dtcs_polarity = True
|
500
|
rf.has_ctone = True
|
501
|
rf.has_cross = True
|
502
|
rf.has_sub_devices = False
|
503
|
rf.valid_name_length = self.NAME_LENGTH
|
504
|
rf.valid_modes = self.MODES
|
505
|
rf.valid_characters = self.VALID_CHARS
|
506
|
rf.valid_duplexes = ["", "-", "+", "split", "off"]
|
507
|
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
|
508
|
rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
|
509
|
"Tone->DTCS", "DTCS->Tone", "->Tone",
|
510
|
"DTCS->DTCS"]
|
511
|
rf.valid_skips = []
|
512
|
rf.valid_power_levels = POWER_LEVELS
|
513
|
rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES # this is just to
|
514
|
# get it working, not sure this is right
|
515
|
rf.valid_bands = self.VALID_BANDS
|
516
|
rf.memory_bounds = (1, 199)
|
517
|
rf.valid_skips = ["", "S"]
|
518
|
return rf
|
519
|
|
520
|
def sync_in(self):
|
521
|
"""Download from radio"""
|
522
|
try:
|
523
|
data = _download(self)
|
524
|
except errors.RadioError:
|
525
|
# Pass through any real errors we raise
|
526
|
raise
|
527
|
except Exception:
|
528
|
# If anything unexpected happens, make sure we raise
|
529
|
# a RadioError and log the problem
|
530
|
LOG.exception('Unexpected error during download')
|
531
|
raise errors.RadioError('Unexpected error communicating '
|
532
|
'with the radio')
|
533
|
self._mmap = memmap.MemoryMap(data)
|
534
|
self.process_mmap()
|
535
|
|
536
|
def sync_out(self):
|
537
|
"""Upload to radio"""
|
538
|
|
539
|
try:
|
540
|
_upload(self)
|
541
|
except Exception:
|
542
|
# If anything unexpected happens, make sure we raise
|
543
|
# a RadioError and log the problem
|
544
|
LOG.exception('Unexpected error during upload')
|
545
|
raise errors.RadioError('Unexpected error communicating '
|
546
|
'with the radio')
|
547
|
|
548
|
def process_mmap(self):
|
549
|
"""Process the mem map into the mem object"""
|
550
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
551
|
|
552
|
def get_raw_memory(self, number):
|
553
|
return repr(self._memobj.memory[number - 1])
|
554
|
|
555
|
def set_memory(self, memory):
|
556
|
"""A value in a UI column for chan 'number' has been modified."""
|
557
|
# update all raw channel memory values (_mem) from UI (mem)
|
558
|
_mem = self._memobj.chan_mem[memory.number - 1]
|
559
|
_name = self._memobj.chan_name[memory.number - 1]
|
560
|
|
561
|
if memory.empty:
|
562
|
_do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
|
563
|
return
|
564
|
|
565
|
_do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
|
566
|
|
567
|
if memory.skip == "":
|
568
|
_do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
|
569
|
else:
|
570
|
_do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
|
571
|
|
572
|
return self._set_memory(memory, _mem, _name)
|
573
|
|
574
|
def get_memory(self, number):
|
575
|
# radio first channel is 1, mem map is base 0
|
576
|
_mem = self._memobj.chan_mem[number - 1]
|
577
|
_name = self._memobj.chan_name[number - 1]
|
578
|
mem = chirp_common.Memory()
|
579
|
mem.number = number
|
580
|
|
581
|
# Determine if channel is empty
|
582
|
|
583
|
if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
|
584
|
mem.empty = True
|
585
|
return mem
|
586
|
|
587
|
if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
|
588
|
mem.skip = ""
|
589
|
else:
|
590
|
mem.skip = "S"
|
591
|
|
592
|
return self._get_memory(mem, _mem, _name)
|
593
|
|
594
|
def _get_memory(self, mem, _mem, _name):
|
595
|
"""Convert raw channel memory data into UI columns"""
|
596
|
mem.extra = RadioSettingGroup("extra", "Extra")
|
597
|
|
598
|
mem.empty = False
|
599
|
# This function process both 'normal' and Freq up/down' entries
|
600
|
mem.freq = int(_mem.rxfreq) * 10
|
601
|
|
602
|
if _mem.txfreq == 0xFFFFFFFF:
|
603
|
# TX freq not set
|
604
|
mem.duplex = "off"
|
605
|
mem.offset = 0
|
606
|
elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 25000000:
|
607
|
mem.duplex = "split"
|
608
|
mem.offset = int(_mem.txfreq) * 10
|
609
|
elif int(_mem.rxfreq) == int(_mem.txfreq):
|
610
|
mem.duplex = ""
|
611
|
mem.offset = 0
|
612
|
else:
|
613
|
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \
|
614
|
and "-" or "+"
|
615
|
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
|
616
|
|
617
|
mem.name = ""
|
618
|
for i in range(6): # 0 - 6
|
619
|
mem.name += chr(_mem.name[i])
|
620
|
for i in range(10):
|
621
|
mem.name += chr(_name.extra_name[i])
|
622
|
|
623
|
mem.name = mem.name.rstrip() # remove trailing spaces
|
624
|
|
625
|
# ########## TONE ##########
|
626
|
|
627
|
if _mem.txtone > 2600:
|
628
|
# All off
|
629
|
txmode = ""
|
630
|
elif _mem.txtone > 511:
|
631
|
txmode = "Tone"
|
632
|
mem.rtone = int(_mem.txtone) / 10.0
|
633
|
else:
|
634
|
# DTSC
|
635
|
txmode = "DTCS"
|
636
|
mem.dtcs = int(format(int(_mem.txtone), 'o'))
|
637
|
|
638
|
if _mem.rxtone > 2600:
|
639
|
rxmode = ""
|
640
|
elif _mem.rxtone > 511:
|
641
|
rxmode = "Tone"
|
642
|
mem.ctone = int(_mem.rxtone) / 10.0
|
643
|
else:
|
644
|
rxmode = "DTCS"
|
645
|
mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
|
646
|
|
647
|
mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
|
648
|
"N", "R")[_mem.decodeDSCI]
|
649
|
|
650
|
mem.tmode = ""
|
651
|
if txmode == "Tone" and not rxmode:
|
652
|
mem.tmode = "Tone"
|
653
|
elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
|
654
|
mem.tmode = "TSQL"
|
655
|
elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
|
656
|
mem.tmode = "DTCS"
|
657
|
elif rxmode or txmode:
|
658
|
mem.tmode = "Cross"
|
659
|
mem.cross_mode = "%s->%s" % (txmode, rxmode)
|
660
|
|
661
|
# ########## TONE ##########
|
662
|
|
663
|
mem.mode = self.MODES[_mem.wide]
|
664
|
mem.power = POWER_LEVELS[int(_mem.power)]
|
665
|
|
666
|
b_lock = RadioSetting("b_lock", "B_Lock",
|
667
|
RadioSettingValueList(B_LOCK_LIST,
|
668
|
B_LOCK_LIST[_mem.b_lock]))
|
669
|
mem.extra.append(b_lock)
|
670
|
|
671
|
b_lock = RadioSetting("step", "Step",
|
672
|
RadioSettingValueList(LIST_STEPS,
|
673
|
LIST_STEPS[_mem.step]))
|
674
|
mem.extra.append(b_lock)
|
675
|
|
676
|
scramble_value = _mem.scramble
|
677
|
if scramble_value >= 8: # Looks like OFF is 0x0f ** CONFIRM
|
678
|
scramble_value = 0
|
679
|
scramble = RadioSetting("scramble", "Scramble",
|
680
|
RadioSettingValueList(SCRAMBLE_LIST,
|
681
|
SCRAMBLE_LIST[
|
682
|
scramble_value]))
|
683
|
mem.extra.append(scramble)
|
684
|
|
685
|
optsig = RadioSetting("signal", "Optional signaling",
|
686
|
RadioSettingValueList(
|
687
|
OPTSIG_LIST,
|
688
|
OPTSIG_LIST[_mem.signal]))
|
689
|
mem.extra.append(optsig)
|
690
|
|
691
|
rs = RadioSetting("pttid", "PTT ID",
|
692
|
RadioSettingValueList(PTTID_LIST,
|
693
|
PTTID_LIST[_mem.pttid]))
|
694
|
mem.extra.append(rs)
|
695
|
|
696
|
return mem
|
697
|
|
698
|
def _set_memory(self, mem, _mem, _name):
|
699
|
# """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
|
700
|
|
701
|
_mem.rxfreq = mem.freq / 10
|
702
|
if mem.duplex == "off":
|
703
|
_mem.txfreq = 0xFFFFFFFF
|
704
|
elif mem.duplex == "split":
|
705
|
_mem.txfreq = mem.offset / 10
|
706
|
elif mem.duplex == "+":
|
707
|
_mem.txfreq = (mem.freq + mem.offset) / 10
|
708
|
elif mem.duplex == "-":
|
709
|
_mem.txfreq = (mem.freq - mem.offset) / 10
|
710
|
else:
|
711
|
_mem.txfreq = _mem.rxfreq
|
712
|
|
713
|
out_name = mem.name.ljust(16)
|
714
|
|
715
|
for i in range(6): # 0 - 6
|
716
|
_mem.name[i] = ord(out_name[i])
|
717
|
for i in range(10):
|
718
|
_name.extra_name[i] = ord(out_name[i+6])
|
719
|
|
720
|
if mem.name != "":
|
721
|
_mem.displayName = 1 # Name only displayed if this is set on
|
722
|
else:
|
723
|
_mem.displayName = 0
|
724
|
|
725
|
rxmode = ""
|
726
|
txmode = ""
|
727
|
|
728
|
if mem.tmode == "Tone":
|
729
|
txmode = "Tone"
|
730
|
elif mem.tmode == "TSQL":
|
731
|
rxmode = "Tone"
|
732
|
txmode = "TSQL"
|
733
|
elif mem.tmode == "DTCS":
|
734
|
rxmode = "DTCSSQL"
|
735
|
txmode = "DTCS"
|
736
|
elif mem.tmode == "Cross":
|
737
|
txmode, rxmode = mem.cross_mode.split("->", 1)
|
738
|
|
739
|
if mem.dtcs_polarity[1] == "N":
|
740
|
_mem.decodeDSCI = 0
|
741
|
else:
|
742
|
_mem.decodeDSCI = 1
|
743
|
|
744
|
if rxmode == "":
|
745
|
_mem.rxtone = 0xFFF
|
746
|
elif rxmode == "Tone":
|
747
|
_mem.rxtone = int(float(mem.ctone) * 10)
|
748
|
elif rxmode == "DTCSSQL":
|
749
|
_mem.rxtone = int(str(mem.dtcs), 8)
|
750
|
elif rxmode == "DTCS":
|
751
|
_mem.rxtone = int(str(mem.rx_dtcs), 8)
|
752
|
|
753
|
if mem.dtcs_polarity[0] == "N":
|
754
|
_mem.encodeDSCI = 0
|
755
|
else:
|
756
|
_mem.encodeDSCI = 1
|
757
|
|
758
|
if txmode == "":
|
759
|
_mem.txtone = 0xFFF
|
760
|
elif txmode == "Tone":
|
761
|
_mem.txtone = int(float(mem.rtone) * 10)
|
762
|
elif txmode == "TSQL":
|
763
|
_mem.txtone = int(float(mem.ctone) * 10)
|
764
|
elif txmode == "DTCS":
|
765
|
_mem.txtone = int(str(mem.dtcs), 8)
|
766
|
|
767
|
_mem.wide = self.MODES.index(mem.mode)
|
768
|
_mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
|
769
|
|
770
|
for element in mem.extra:
|
771
|
setattr(_mem, element.get_name(), element.value)
|
772
|
|
773
|
return
|
774
|
|
775
|
def get_settings(self):
|
776
|
"""Translate the MEM_FORMAT structs into setstuf in the UI"""
|
777
|
_settings = self._memobj.basicsettings
|
778
|
_settings2 = self._memobj.settings2
|
779
|
_workmode = self._memobj.workmodesettings
|
780
|
|
781
|
basic = RadioSettingGroup("basic", "Basic Settings")
|
782
|
group = RadioSettings(basic)
|
783
|
|
784
|
# Menu 02 - TX Channel Select
|
785
|
options = ["Last Channel", "Main Channel"]
|
786
|
rx = RadioSettingValueList(options, options[_settings.txChSelect])
|
787
|
rset = RadioSetting("basicsettings.txChSelect",
|
788
|
"Priority Transmit", rx)
|
789
|
basic.append(rset)
|
790
|
|
791
|
# Menu 03 - VOX Level
|
792
|
##rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1)
|
793
|
rx = RadioSettingValueInteger(1, 7, _settings.voxLevel + 1)
|
794
|
rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
|
795
|
basic.append(rset)
|
796
|
|
797
|
# Menu 05 - Squelch Level
|
798
|
options = ["OFF"] + ["%s" % x for x in range(1, 10)]
|
799
|
rx = RadioSettingValueList(options, options[_settings.sqlLevel])
|
800
|
rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
|
801
|
basic.append(rset)
|
802
|
|
803
|
# Menu 06 - Dual Wait
|
804
|
rx = RadioSettingValueBoolean(_settings.dualWait)
|
805
|
rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
|
806
|
basic.append(rset)
|
807
|
|
808
|
# Menu 07 - LED Mode
|
809
|
options = ["Off", "On", "Auto"]
|
810
|
rx = RadioSettingValueList(options, options[_settings.ledMode])
|
811
|
rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
|
812
|
basic.append(rset)
|
813
|
|
814
|
# Menu 08 - Light
|
815
|
options = ["%s" % x for x in range(1, 8)]
|
816
|
rx = RadioSettingValueList(options, options[_settings.light])
|
817
|
rset = RadioSetting("basicsettings.light",
|
818
|
"Background Light Color", rx)
|
819
|
basic.append(rset)
|
820
|
|
821
|
# Menu 09 - Beep
|
822
|
rx = RadioSettingValueBoolean(_settings.beep)
|
823
|
rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
|
824
|
basic.append(rset)
|
825
|
|
826
|
# Menu 11 - TOT
|
827
|
options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
|
828
|
rx = RadioSettingValueList(options, options[_settings.tot])
|
829
|
rset = RadioSetting("basicsettings.tot",
|
830
|
"Transmission Time-out Timer", rx)
|
831
|
basic.append(rset)
|
832
|
|
833
|
# Menu 13 - VOX Switch
|
834
|
rx = RadioSettingValueBoolean(_settings.voxSw)
|
835
|
rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
|
836
|
basic.append(rset)
|
837
|
|
838
|
# Menu 14 - Roger
|
839
|
rx = RadioSettingValueBoolean(_settings.roger)
|
840
|
rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
|
841
|
basic.append(rset)
|
842
|
|
843
|
# Menu 16 - Save Mode
|
844
|
options = ["Off", "1:1", "1:2", "1:4"]
|
845
|
rx = RadioSettingValueList(options, options[_settings.saveMode])
|
846
|
rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
|
847
|
basic.append(rset)
|
848
|
|
849
|
# Menu 17 - Scan Type
|
850
|
options = ["CO", "TO", "SE"]
|
851
|
rx = RadioSettingValueList(options, options[_settings.scanType])
|
852
|
rset = RadioSetting("basicsettings.scanType", "Scan Type", rx)
|
853
|
basic.append(rset)
|
854
|
|
855
|
# Menu 18 - Key Lock
|
856
|
rx = RadioSettingValueBoolean(_settings.keylock)
|
857
|
rset = RadioSetting("basicsettings.keylock", "Auto Key Lock", rx)
|
858
|
basic.append(rset)
|
859
|
|
860
|
# Menu 19 - SW Audio
|
861
|
rx = RadioSettingValueBoolean(_settings.swAudio)
|
862
|
rset = RadioSetting("basicsettings.swAudio", "Voice Prompts", rx)
|
863
|
basic.append(rset)
|
864
|
|
865
|
# Menu 20 - Intro Screen
|
866
|
options = ["Off", "Voltage", "Character String"]
|
867
|
rx = RadioSettingValueList(options, options[_settings.introScreen])
|
868
|
rset = RadioSetting("basicsettings.introScreen", "Intro Screen", rx)
|
869
|
basic.append(rset)
|
870
|
|
871
|
# Menu 32 - Key Mode
|
872
|
options = ["ALL", "PTT", "KEY", "Key & Side Key"]
|
873
|
rx = RadioSettingValueList(options, options[_settings.keyMode])
|
874
|
rset = RadioSetting("basicsettings.keyMode", "Key Lock Mode", rx)
|
875
|
basic.append(rset)
|
876
|
|
877
|
# Menu 33 - Display Mode
|
878
|
options = ['Frequency', 'Channel #', 'Name']
|
879
|
rx = RadioSettingValueList(options, options[_settings.disMode])
|
880
|
rset = RadioSetting("basicsettings.disMode", "Display Mode", rx)
|
881
|
basic.append(rset)
|
882
|
|
883
|
# Menu 34 - FM Dual Wait
|
884
|
rx = RadioSettingValueBoolean(_settings.radioMoni)
|
885
|
rset = RadioSetting("basicsettings.radioMoni", "Radio Monitor", rx)
|
886
|
basic.append(rset)
|
887
|
|
888
|
advanced = RadioSettingGroup("advanced", "Advanced Settings")
|
889
|
group.append(advanced)
|
890
|
|
891
|
# software only
|
892
|
options = ['Off', 'Frequency']
|
893
|
rx = RadioSettingValueList(options, options[_settings.endToneElim])
|
894
|
rset = RadioSetting("basicsettings.endToneElim", "End Tone Elim", rx)
|
895
|
advanced.append(rset)
|
896
|
|
897
|
# software only
|
898
|
name = ""
|
899
|
for i in range(15): # 0 - 15
|
900
|
name += chr(self._memobj.openradioname.name1[i])
|
901
|
name = name.rstrip() # remove trailing spaces
|
902
|
|
903
|
rx = RadioSettingValueString(0, 15, name)
|
904
|
rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
|
905
|
advanced.append(rset)
|
906
|
|
907
|
# software only
|
908
|
name = ""
|
909
|
for i in range(15): # 0 - 15
|
910
|
name += chr(self._memobj.openradioname.name2[i])
|
911
|
name = name.rstrip() # remove trailing spaces
|
912
|
|
913
|
rx = RadioSettingValueString(0, 15, name)
|
914
|
rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
|
915
|
advanced.append(rset)
|
916
|
|
917
|
# software only
|
918
|
options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
|
919
|
'4.0S', '4.5S', '5.0S']
|
920
|
rx = RadioSettingValueList(options, options[_settings.voxDelay])
|
921
|
rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
|
922
|
advanced.append(rset)
|
923
|
|
924
|
options = ['Unlocked', 'Unknown 1', 'Unknown 2', 'EU', 'US']
|
925
|
# extend option list with unknown description for values 5 - 15.
|
926
|
for ix in range(len(options), _settings2.region + 1):
|
927
|
item_to_add = 'Unknown {region_code}'.format(region_code=ix)
|
928
|
options.append(item_to_add)
|
929
|
# log unknown region codes greater than 4
|
930
|
if _settings2.region > 4:
|
931
|
LOG.debug("Unknown region code: {value}".
|
932
|
format(value=_settings2.region))
|
933
|
rx = RadioSettingValueList(options, options[_settings2.region])
|
934
|
rx.set_mutable(False)
|
935
|
rset = RadioSetting("settings2.region", "Region", rx)
|
936
|
advanced.append(rset)
|
937
|
|
938
|
workmode = RadioSettingGroup("workmode", "Work Mode Settings")
|
939
|
group.append(workmode)
|
940
|
|
941
|
# Toggle with [#] key
|
942
|
options = ["Frequency", "Channel"]
|
943
|
rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
|
944
|
rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
|
945
|
workmode.append(rset)
|
946
|
|
947
|
# Toggle with [#] key
|
948
|
options = ["Frequency", "Channel"]
|
949
|
rx = RadioSettingValueList(options, options[_workmode.vfomrmodeb])
|
950
|
rset = RadioSetting("workmodesettings.vfomrmodeb",
|
951
|
"VFO/MR Mode B (BQ1.41+)", rx)
|
952
|
workmode.append(rset)
|
953
|
|
954
|
# Toggle with [A/B] key
|
955
|
options = ["B", "A"]
|
956
|
rx = RadioSettingValueList(options, options[_workmode.ab])
|
957
|
rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
|
958
|
workmode.append(rset)
|
959
|
|
960
|
rx = RadioSettingValueInteger(1, 199, _workmode.mrAch + 1)
|
961
|
rset = RadioSetting("workmodesettings.mrAch", "MR A Channel #", rx)
|
962
|
workmode.append(rset)
|
963
|
|
964
|
rx = RadioSettingValueInteger(1, 199, _workmode.mrBch + 1)
|
965
|
rset = RadioSetting("workmodesettings.mrBch", "MR B Channel #", rx)
|
966
|
workmode.append(rset)
|
967
|
|
968
|
return group # END get_settings()
|
969
|
|
970
|
def set_settings(self, settings):
|
971
|
_settings = self._memobj.basicsettings
|
972
|
_mem = self._memobj
|
973
|
for element in settings:
|
974
|
if not isinstance(element, RadioSetting):
|
975
|
self.set_settings(element)
|
976
|
continue
|
977
|
else:
|
978
|
try:
|
979
|
name = element.get_name()
|
980
|
if "." in name:
|
981
|
bits = name.split(".")
|
982
|
obj = self._memobj
|
983
|
for bit in bits[:-1]:
|
984
|
if "/" in bit:
|
985
|
bit, index = bit.split("/", 1)
|
986
|
index = int(index)
|
987
|
obj = getattr(obj, bit)[index]
|
988
|
else:
|
989
|
obj = getattr(obj, bit)
|
990
|
setting = bits[-1]
|
991
|
else:
|
992
|
obj = _settings
|
993
|
setting = element.get_name()
|
994
|
|
995
|
if element.has_apply_callback():
|
996
|
LOG.debug("Using apply callback")
|
997
|
element.run_apply_callback()
|
998
|
elif setting == "mrAch" or setting == "mrBch":
|
999
|
setattr(obj, setting, int(element.value) - 1)
|
1000
|
elif setting == "voxLevel":
|
1001
|
##setattr(obj, setting, int(element.value) + 1)
|
1002
|
setattr(obj, setting, int(element.value) - 1)
|
1003
|
elif element.value.get_mutable():
|
1004
|
LOG.debug("Setting %s = %s" % (setting, element.value))
|
1005
|
setattr(obj, setting, element.value)
|
1006
|
except Exception, e:
|
1007
|
LOG.debug(element.get_name())
|
1008
|
raise
|
1009
|
|
1010
|
|
1011
|
@directory.register
|
1012
|
class RT85(THUV88Radio):
|
1013
|
VENDOR = "Retevis"
|
1014
|
MODEL = "RT85"
|