1
|
# Copyright 2010 Vernon Mauery <vernon@mauery.org>
|
2
|
# Copyright 2016 Angus Ainslie <angus@akkea.ca>
|
3
|
#
|
4
|
# This program is free software: you can redistribute it and/or modify
|
5
|
# it under the terms of the GNU General Public License as published by
|
6
|
# the Free Software Foundation, either version 3 of the License, or
|
7
|
# (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
|
17
|
from chirp import chirp_common, errors, util, directory
|
18
|
from chirp import bitwise, memmap
|
19
|
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings
|
20
|
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
|
21
|
from chirp.settings import RadioSettingValueList, RadioSettingValueBoolean
|
22
|
import time
|
23
|
import struct
|
24
|
import sys
|
25
|
import logging
|
26
|
|
27
|
LOG = logging.getLogger(__name__)
|
28
|
|
29
|
# TH-D72 memory map
|
30
|
# 0x0000..0x0200: startup password and other stuff
|
31
|
# 0x0200..0x0400: current channel and other settings
|
32
|
# 0x244,0x246: last menu numbers
|
33
|
# 0x249: last f menu number
|
34
|
# 0x0400..0x0c00: APRS settings and likely other settings
|
35
|
# 0x0c00..0x1500: memory channel flags
|
36
|
# 0x1500..0x5380: 0-999 channels
|
37
|
# 0x5380..0x54c0: 0-9 scan channels
|
38
|
# 0x54c0..0x5560: 0-9 wx channels
|
39
|
# 0x5560..0x5e00: ?
|
40
|
# 0x5e00..0x7d40: 0-999 channel names
|
41
|
# 0x7d40..0x7de0: ?
|
42
|
# 0x7de0..0x7e30: wx channel names
|
43
|
# 0x7e30..0x7ed0: ?
|
44
|
# 0x7ed0..0x7f20: group names
|
45
|
# 0x7f20..0x8b00: ?
|
46
|
# 0x8b00..0x9c00: last 20 APRS entries
|
47
|
# 0x9c00..0xe500: ?
|
48
|
# 0xe500..0xe7d0: startup bitmap
|
49
|
# 0xe7d0..0xe800: startup bitmap filename
|
50
|
# 0xe800..0xead0: gps-logger bitmap
|
51
|
# 0xe8d0..0xeb00: gps-logger bipmap filename
|
52
|
# 0xeb00..0xff00: ?
|
53
|
# 0xff00..0xffff: stuff?
|
54
|
|
55
|
# memory channel
|
56
|
# 0 1 2 3 4 5 6 7 8 9 a b c d e f
|
57
|
# [freq ] ? mode tmode/duplex rtone ctone dtcs cross_mode [offset] ?
|
58
|
|
59
|
mem_format = """
|
60
|
#seekto 0x0000;
|
61
|
struct {
|
62
|
ul16 version;
|
63
|
u8 shouldbe32;
|
64
|
u8 efs[11];
|
65
|
u8 unknown0[3];
|
66
|
u8 radio_custom_image;
|
67
|
u8 gps_custom_image;
|
68
|
u8 unknown1[7];
|
69
|
u8 passwd[6];
|
70
|
} frontmatter;
|
71
|
|
72
|
#seekto 0x02c0;
|
73
|
struct {
|
74
|
ul32 start_freq;
|
75
|
ul32 end_freq;
|
76
|
} prog_vfo[6];
|
77
|
|
78
|
#seekto 0x0300;
|
79
|
struct {
|
80
|
char power_on_msg[8];
|
81
|
u8 unknown0[8];
|
82
|
u8 unknown1[2];
|
83
|
u8 lamp_timer;
|
84
|
u8 contrast;
|
85
|
u8 battery_saver;
|
86
|
u8 APO;
|
87
|
u8 unknown2;
|
88
|
u8 key_beep;
|
89
|
u8 unknown3[8];
|
90
|
u8 unknown4;
|
91
|
u8 balance;
|
92
|
u8 unknown5[23];
|
93
|
u8 lamp_control;
|
94
|
} settings;
|
95
|
|
96
|
#seekto 0x0c00;
|
97
|
struct {
|
98
|
u8 disabled:4,
|
99
|
prog_vfo:4;
|
100
|
u8 skip;
|
101
|
} flag[1032];
|
102
|
|
103
|
#seekto 0x1500;
|
104
|
struct {
|
105
|
ul32 freq;
|
106
|
u8 unknown1;
|
107
|
u8 mode;
|
108
|
u8 tone_mode:4,
|
109
|
duplex:4;
|
110
|
u8 rtone;
|
111
|
u8 ctone;
|
112
|
u8 dtcs;
|
113
|
u8 cross_mode;
|
114
|
ul32 offset;
|
115
|
u8 unknown2;
|
116
|
} memory[1032];
|
117
|
|
118
|
#seekto 0x5e00;
|
119
|
struct {
|
120
|
char name[8];
|
121
|
} channel_name[1000];
|
122
|
|
123
|
#seekto 0x7de0;
|
124
|
struct {
|
125
|
char name[8];
|
126
|
} wx_name[10];
|
127
|
|
128
|
#seekto 0x7ed0;
|
129
|
struct {
|
130
|
char name[8];
|
131
|
} group_name[10];
|
132
|
"""
|
133
|
|
134
|
THD72_SPECIAL = {}
|
135
|
|
136
|
for i in range(0, 10):
|
137
|
THD72_SPECIAL["L%i" % i] = 1000 + (i * 2)
|
138
|
THD72_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1
|
139
|
for i in range(0, 10):
|
140
|
THD72_SPECIAL["WX%i" % (i + 1)] = 1020 + i
|
141
|
THD72_SPECIAL["C VHF"] = 1030
|
142
|
THD72_SPECIAL["C UHF"] = 1031
|
143
|
|
144
|
THD72_SPECIAL_REV = {}
|
145
|
for k, v in THD72_SPECIAL.items():
|
146
|
THD72_SPECIAL_REV[v] = k
|
147
|
|
148
|
TMODES = {
|
149
|
0x08: "Tone",
|
150
|
0x04: "TSQL",
|
151
|
0x02: "DTCS",
|
152
|
0x01: "Cross",
|
153
|
0x00: "",
|
154
|
}
|
155
|
TMODES_REV = {
|
156
|
"": 0x00,
|
157
|
"Cross": 0x01,
|
158
|
"DTCS": 0x02,
|
159
|
"TSQL": 0x04,
|
160
|
"Tone": 0x08,
|
161
|
}
|
162
|
|
163
|
MODES = {
|
164
|
0x00: "FM",
|
165
|
0x01: "NFM",
|
166
|
0x02: "AM",
|
167
|
}
|
168
|
|
169
|
MODES_REV = {
|
170
|
"FM": 0x00,
|
171
|
"NFM": 0x01,
|
172
|
"AM": 0x2,
|
173
|
}
|
174
|
|
175
|
DUPLEX = {
|
176
|
0x00: "",
|
177
|
0x01: "+",
|
178
|
0x02: "-",
|
179
|
0x04: "split",
|
180
|
}
|
181
|
DUPLEX_REV = {
|
182
|
"": 0x00,
|
183
|
"+": 0x01,
|
184
|
"-": 0x02,
|
185
|
"split": 0x04,
|
186
|
}
|
187
|
|
188
|
|
189
|
EXCH_R = "R\x00\x00\x00\x00"
|
190
|
EXCH_W = "W\x00\x00\x00\x00"
|
191
|
|
192
|
DEFAULT_PROG_VFO = (
|
193
|
(136000000, 174000000),
|
194
|
(410000000, 470000000),
|
195
|
(118000000, 136000000),
|
196
|
(136000000, 174000000),
|
197
|
(320000000, 400000000),
|
198
|
(400000000, 524000000),
|
199
|
)
|
200
|
# index of PROG_VFO used for setting memory.unknown1 and memory.unknown2
|
201
|
# see http://chirp.danplanet.com/issues/1611#note-9
|
202
|
UNKNOWN_LOOKUP = (0, 7, 4, 0, 4, 7)
|
203
|
|
204
|
|
205
|
def get_prog_vfo(frequency):
|
206
|
for i, (start, end) in enumerate(DEFAULT_PROG_VFO):
|
207
|
if start <= frequency < end:
|
208
|
return i
|
209
|
raise ValueError("Frequency is out of range.")
|
210
|
|
211
|
|
212
|
@directory.register
|
213
|
class THD72Radio(chirp_common.CloneModeRadio):
|
214
|
|
215
|
BAUD_RATE = 9600
|
216
|
VENDOR = "Kenwood"
|
217
|
MODEL = "TH-D72 (clone mode)"
|
218
|
HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow
|
219
|
|
220
|
mem_upper_limit = 1022
|
221
|
_memsize = 65536
|
222
|
_model = "" # FIXME: REMOVE
|
223
|
_dirty_blocks = []
|
224
|
|
225
|
_LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
|
226
|
_LAMP_CONTROL = ["Manual", "Auto"]
|
227
|
_LAMP_TIMER = ["Seconds %d" % x for x in range(2, 11)]
|
228
|
_BATTERY_SAVER = ["OFF", "0.03 Seconds", "0.2 Seconds", "0.4 Seconds",
|
229
|
"0.6 Seconds", "0.8 Seconds", "1 Seconds", "2 Seconds",
|
230
|
"3 Seconds", "4 Seconds", "5 Seconds"]
|
231
|
_APO = ["OFF", "15 Minutes", "30 Minutes", "60 Minutes"]
|
232
|
_AUDIO_BALANCE = ["Center", "A +50%", "A +100%", "B +50%", "B +100%"]
|
233
|
_KEY_BEEP = ["OFF", "Radio & GPS", "Radio Only", "GPS Only"]
|
234
|
|
235
|
def get_features(self):
|
236
|
rf = chirp_common.RadioFeatures()
|
237
|
rf.memory_bounds = (0, 1031)
|
238
|
rf.valid_bands = [(118000000, 174000000),
|
239
|
(320000000, 524000000)]
|
240
|
rf.has_cross = True
|
241
|
rf.can_odd_split = True
|
242
|
rf.has_dtcs_polarity = False
|
243
|
rf.has_tuning_step = False
|
244
|
rf.has_bank = False
|
245
|
rf.has_settings = True
|
246
|
rf.valid_tuning_steps = []
|
247
|
rf.valid_modes = list(MODES_REV.keys())
|
248
|
rf.valid_tmodes = list(TMODES_REV.keys())
|
249
|
rf.valid_duplexes = list(DUPLEX_REV.keys())
|
250
|
rf.valid_skips = ["", "S"]
|
251
|
rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
|
252
|
rf.valid_name_length = 8
|
253
|
return rf
|
254
|
|
255
|
def process_mmap(self):
|
256
|
self._memobj = bitwise.parse(mem_format, self._mmap)
|
257
|
self._dirty_blocks = []
|
258
|
|
259
|
def _detect_baud(self):
|
260
|
for baud in [9600, 19200, 38400, 57600]:
|
261
|
self.pipe.baudrate = baud
|
262
|
try:
|
263
|
self.pipe.write("\r\r")
|
264
|
except:
|
265
|
break
|
266
|
self.pipe.read(32)
|
267
|
try:
|
268
|
id = self.get_id()
|
269
|
LOG.info("Radio %s at %i baud" % (id, baud))
|
270
|
return True
|
271
|
except errors.RadioError:
|
272
|
pass
|
273
|
|
274
|
raise errors.RadioError("No response from radio")
|
275
|
|
276
|
def get_special_locations(self):
|
277
|
return sorted(THD72_SPECIAL.keys())
|
278
|
|
279
|
def add_dirty_block(self, memobj):
|
280
|
block = memobj._offset // 256
|
281
|
if block not in self._dirty_blocks:
|
282
|
self._dirty_blocks.append(block)
|
283
|
self._dirty_blocks.sort()
|
284
|
print("dirty blocks: ", self._dirty_blocks)
|
285
|
|
286
|
def get_channel_name(self, number):
|
287
|
if number < 999:
|
288
|
name = str(self._memobj.channel_name[number].name) + '\xff'
|
289
|
elif number >= 1020 and number < 1030:
|
290
|
number -= 1020
|
291
|
name = str(self._memobj.wx_name[number].name) + '\xff'
|
292
|
else:
|
293
|
return ''
|
294
|
return name[:name.index('\xff')].rstrip()
|
295
|
|
296
|
def set_channel_name(self, number, name):
|
297
|
name = name[:8] + '\xff' * 8
|
298
|
if number < 999:
|
299
|
self._memobj.channel_name[number].name = name[:8]
|
300
|
self.add_dirty_block(self._memobj.channel_name[number])
|
301
|
elif number >= 1020 and number < 1030:
|
302
|
number -= 1020
|
303
|
self._memobj.wx_name[number].name = name[:8]
|
304
|
self.add_dirty_block(self._memobj.wx_name[number])
|
305
|
|
306
|
def get_raw_memory(self, number):
|
307
|
return repr(self._memobj.memory[number]) + \
|
308
|
repr(self._memobj.flag[number])
|
309
|
|
310
|
def get_memory(self, number):
|
311
|
if isinstance(number, str):
|
312
|
try:
|
313
|
number = THD72_SPECIAL[number]
|
314
|
except KeyError:
|
315
|
raise errors.InvalidMemoryLocation("Unknown channel %s" %
|
316
|
number)
|
317
|
|
318
|
if number < 0 or number > (max(THD72_SPECIAL.values()) + 1):
|
319
|
raise errors.InvalidMemoryLocation(
|
320
|
"Number must be between 0 and 999")
|
321
|
|
322
|
_mem = self._memobj.memory[number]
|
323
|
flag = self._memobj.flag[number]
|
324
|
|
325
|
mem = chirp_common.Memory()
|
326
|
mem.number = number
|
327
|
|
328
|
if number > 999:
|
329
|
mem.extd_number = THD72_SPECIAL_REV[number]
|
330
|
if flag.disabled == 0xf:
|
331
|
mem.empty = True
|
332
|
return mem
|
333
|
|
334
|
mem.name = self.get_channel_name(number)
|
335
|
mem.freq = int(_mem.freq)
|
336
|
mem.tmode = TMODES[int(_mem.tone_mode)]
|
337
|
mem.rtone = chirp_common.TONES[_mem.rtone]
|
338
|
mem.ctone = chirp_common.TONES[_mem.ctone]
|
339
|
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
|
340
|
mem.duplex = DUPLEX[int(_mem.duplex)]
|
341
|
mem.offset = int(_mem.offset)
|
342
|
mem.mode = MODES[int(_mem.mode)]
|
343
|
|
344
|
if number < 999:
|
345
|
mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)]
|
346
|
mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode]
|
347
|
if number > 999:
|
348
|
mem.cross_mode = chirp_common.CROSS_MODES[0]
|
349
|
mem.immutable = ["number", "bank", "extd_number", "cross_mode"]
|
350
|
if number >= 1020 and number < 1030:
|
351
|
mem.immutable += ["freq", "offset", "tone", "mode",
|
352
|
"tmode", "ctone", "skip"] # FIXME: ALL
|
353
|
else:
|
354
|
mem.immutable += ["name"]
|
355
|
|
356
|
return mem
|
357
|
|
358
|
def set_memory(self, mem):
|
359
|
LOG.debug("set_memory(%d)" % mem.number)
|
360
|
if mem.number < 0 or mem.number > (max(THD72_SPECIAL.values()) + 1):
|
361
|
raise errors.InvalidMemoryLocation(
|
362
|
"Number must be between 0 and 999")
|
363
|
|
364
|
# weather channels can only change name, nothing else
|
365
|
if mem.number >= 1020 and mem.number < 1030:
|
366
|
self.set_channel_name(mem.number, mem.name)
|
367
|
return
|
368
|
|
369
|
flag = self._memobj.flag[mem.number]
|
370
|
self.add_dirty_block(self._memobj.flag[mem.number])
|
371
|
|
372
|
# only delete non-WX channels
|
373
|
was_empty = flag.disabled == 0xf
|
374
|
if mem.empty:
|
375
|
flag.disabled = 0xf
|
376
|
return
|
377
|
flag.disabled = 0
|
378
|
|
379
|
_mem = self._memobj.memory[mem.number]
|
380
|
self.add_dirty_block(_mem)
|
381
|
if was_empty:
|
382
|
self.initialize(_mem)
|
383
|
|
384
|
_mem.freq = mem.freq
|
385
|
|
386
|
if mem.number < 999:
|
387
|
self.set_channel_name(mem.number, mem.name)
|
388
|
|
389
|
_mem.tone_mode = TMODES_REV[mem.tmode]
|
390
|
_mem.rtone = chirp_common.TONES.index(mem.rtone)
|
391
|
_mem.ctone = chirp_common.TONES.index(mem.ctone)
|
392
|
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
|
393
|
_mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode)
|
394
|
_mem.duplex = DUPLEX_REV[mem.duplex]
|
395
|
_mem.offset = mem.offset
|
396
|
_mem.mode = MODES_REV[mem.mode]
|
397
|
|
398
|
prog_vfo = get_prog_vfo(mem.freq)
|
399
|
flag.prog_vfo = prog_vfo
|
400
|
_mem.unknown1 = _mem.unknown2 = UNKNOWN_LOOKUP[prog_vfo]
|
401
|
|
402
|
if mem.number < 999:
|
403
|
flag.skip = chirp_common.SKIP_VALUES.index(mem.skip)
|
404
|
|
405
|
def sync_in(self):
|
406
|
self._detect_baud()
|
407
|
self._mmap = self.download()
|
408
|
self.process_mmap()
|
409
|
|
410
|
def sync_out(self):
|
411
|
self._detect_baud()
|
412
|
if len(self._dirty_blocks):
|
413
|
self.upload(self._dirty_blocks)
|
414
|
else:
|
415
|
self.upload()
|
416
|
|
417
|
def read_block(self, block, count=256):
|
418
|
self.pipe.write(struct.pack("<cBHB", "R", 0, block, 0))
|
419
|
r = self.pipe.read(5)
|
420
|
if len(r) != 5:
|
421
|
raise Exception("Did not receive block response")
|
422
|
|
423
|
cmd, _zero, _block, zero = struct.unpack("<cBHB", r)
|
424
|
if cmd != "W" or _block != block:
|
425
|
raise Exception("Invalid response: %s %i" % (cmd, _block))
|
426
|
|
427
|
data = ""
|
428
|
while len(data) < count:
|
429
|
data += self.pipe.read(count - len(data))
|
430
|
|
431
|
self.pipe.write(chr(0x06))
|
432
|
if self.pipe.read(1) != chr(0x06):
|
433
|
raise Exception("Did not receive post-block ACK!")
|
434
|
|
435
|
return data
|
436
|
|
437
|
def write_block(self, block, map):
|
438
|
self.pipe.write(struct.pack("<cBHB", "W", 0, block, 0))
|
439
|
base = block * 256
|
440
|
self.pipe.write(map[base:base + 256])
|
441
|
|
442
|
ack = self.pipe.read(1)
|
443
|
|
444
|
return ack == chr(0x06)
|
445
|
|
446
|
def download(self, raw=False, blocks=None):
|
447
|
if blocks is None:
|
448
|
blocks = range(self._memsize / 256)
|
449
|
else:
|
450
|
blocks = [b for b in blocks if b < self._memsize / 256]
|
451
|
|
452
|
if self.command("0M PROGRAM") != "0M":
|
453
|
raise errors.RadioError("No response from self")
|
454
|
|
455
|
allblocks = range(self._memsize / 256)
|
456
|
self.pipe.baudrate = 57600
|
457
|
try:
|
458
|
self.pipe.setRTS()
|
459
|
except AttributeError:
|
460
|
self.pipe.rts = True
|
461
|
self.pipe.read(1)
|
462
|
data = ""
|
463
|
LOG.debug("reading blocks %d..%d" % (blocks[0], blocks[-1]))
|
464
|
total = len(blocks)
|
465
|
count = 0
|
466
|
for i in allblocks:
|
467
|
if i not in blocks:
|
468
|
data += 256 * '\xff'
|
469
|
continue
|
470
|
data += self.read_block(i)
|
471
|
count += 1
|
472
|
if self.status_fn:
|
473
|
s = chirp_common.Status()
|
474
|
s.msg = "Cloning from radio"
|
475
|
s.max = total
|
476
|
s.cur = count
|
477
|
self.status_fn(s)
|
478
|
|
479
|
self.pipe.write("E")
|
480
|
|
481
|
if raw:
|
482
|
return data
|
483
|
return memmap.MemoryMap(data)
|
484
|
|
485
|
def upload(self, blocks=None):
|
486
|
if blocks is None:
|
487
|
blocks = range((self._memsize / 256) - 2)
|
488
|
else:
|
489
|
blocks = [b for b in blocks if b < self._memsize / 256]
|
490
|
|
491
|
if self.command("0M PROGRAM") != "0M":
|
492
|
raise errors.RadioError("No response from self")
|
493
|
|
494
|
self.pipe.baudrate = 57600
|
495
|
try:
|
496
|
self.pipe.setRTS()
|
497
|
except AttributeError:
|
498
|
self.pipe.rts = True
|
499
|
self.pipe.read(1)
|
500
|
LOG.debug("writing blocks %d..%d" % (blocks[0], blocks[-1]))
|
501
|
total = len(blocks)
|
502
|
count = 0
|
503
|
for i in blocks:
|
504
|
r = self.write_block(i, self._mmap)
|
505
|
count += 1
|
506
|
if not r:
|
507
|
raise errors.RadioError("self NAK'd block %i" % i)
|
508
|
if self.status_fn:
|
509
|
s = chirp_common.Status()
|
510
|
s.msg = "Cloning to radio"
|
511
|
s.max = total
|
512
|
s.cur = count
|
513
|
self.status_fn(s)
|
514
|
|
515
|
self.pipe.write("E")
|
516
|
# clear out blocks we uploaded from the dirty blocks list
|
517
|
self._dirty_blocks = [b for b in self._dirty_blocks if b not in blocks]
|
518
|
|
519
|
def command(self, cmd, timeout=0.5):
|
520
|
start = time.time()
|
521
|
|
522
|
data = ""
|
523
|
LOG.debug("PC->D72: %s" % cmd)
|
524
|
self.pipe.write(cmd + "\r")
|
525
|
while not data.endswith("\r") and (time.time() - start) < timeout:
|
526
|
data += self.pipe.read(1)
|
527
|
LOG.debug("D72->PC: %s" % data.strip())
|
528
|
return data.strip()
|
529
|
|
530
|
def get_id(self):
|
531
|
r = self.command("ID")
|
532
|
if r.startswith("ID "):
|
533
|
return r.split(" ")[1]
|
534
|
else:
|
535
|
raise errors.RadioError("No response to ID command")
|
536
|
|
537
|
def initialize(self, mmap):
|
538
|
mmap.set_raw("\x00\xc8\xb3\x08\x00\x01\x00\x08"
|
539
|
"\x08\x00\xc0\x27\x09\x00\x00\x00")
|
540
|
|
541
|
def _get_settings(self):
|
542
|
top = RadioSettings(self._get_display_settings(),
|
543
|
self._get_audio_settings(),
|
544
|
self._get_battery_settings())
|
545
|
return top
|
546
|
|
547
|
def set_settings(self, settings):
|
548
|
_mem = self._memobj
|
549
|
for element in settings:
|
550
|
if not isinstance(element, RadioSetting):
|
551
|
self.set_settings(element)
|
552
|
continue
|
553
|
if not element.changed():
|
554
|
continue
|
555
|
try:
|
556
|
if element.has_apply_callback():
|
557
|
LOG.debug("Using apply callback")
|
558
|
try:
|
559
|
element.run_apply_callback()
|
560
|
except NotImplementedError as e:
|
561
|
LOG.error("thd72: %s", e)
|
562
|
continue
|
563
|
|
564
|
# Find the object containing setting.
|
565
|
obj = _mem
|
566
|
bits = element.get_name().split(".")
|
567
|
setting = bits[-1]
|
568
|
for name in bits[:-1]:
|
569
|
if name.endswith("]"):
|
570
|
name, index = name.split("[")
|
571
|
index = int(index[:-1])
|
572
|
obj = getattr(obj, name)[index]
|
573
|
else:
|
574
|
obj = getattr(obj, name)
|
575
|
|
576
|
try:
|
577
|
old_val = getattr(obj, setting)
|
578
|
LOG.debug("Setting %s(%r) <= %s" % (
|
579
|
element.get_name(), old_val, element.value))
|
580
|
setattr(obj, setting, element.value)
|
581
|
except AttributeError as e:
|
582
|
LOG.error("Setting %s is not in the memory map: %s" %
|
583
|
(element.get_name(), e))
|
584
|
except Exception as e:
|
585
|
LOG.debug(element.get_name())
|
586
|
raise
|
587
|
|
588
|
def get_settings(self):
|
589
|
try:
|
590
|
return self._get_settings()
|
591
|
except:
|
592
|
import traceback
|
593
|
LOG.error("Failed to parse settings: %s", traceback.format_exc())
|
594
|
return None
|
595
|
|
596
|
@classmethod
|
597
|
def apply_power_on_msg(cls, setting, obj):
|
598
|
message = setting.value.get_value()
|
599
|
setattr(obj, "power_on_msg", cls._add_ff_pad(message, 8))
|
600
|
|
601
|
def apply_lcd_contrast(cls, setting, obj):
|
602
|
rawval = setting.value.get_value()
|
603
|
val = cls._LCD_CONTRAST.index(rawval) + 1
|
604
|
obj.contrast = val
|
605
|
|
606
|
def apply_lamp_control(cls, setting, obj):
|
607
|
rawval = setting.value.get_value()
|
608
|
val = cls._LAMP_CONTROL.index(rawval)
|
609
|
obj.lamp_control = val
|
610
|
|
611
|
def apply_lamp_timer(cls, setting, obj):
|
612
|
rawval = setting.value.get_value()
|
613
|
val = cls._LAMP_TIMER.index(rawval) + 2
|
614
|
obj.lamp_timer = val
|
615
|
|
616
|
def _get_display_settings(self):
|
617
|
menu = RadioSettingGroup("display", "Display")
|
618
|
display_settings = self._memobj.settings
|
619
|
|
620
|
val = RadioSettingValueString(
|
621
|
0, 8, str(display_settings.power_on_msg).rstrip("\xFF"))
|
622
|
rs = RadioSetting("display.power_on_msg", "Power on message", val)
|
623
|
rs.set_apply_callback(self.apply_power_on_msg, display_settings)
|
624
|
menu.append(rs)
|
625
|
|
626
|
val = RadioSettingValueList(
|
627
|
self._LCD_CONTRAST,
|
628
|
self._LCD_CONTRAST[display_settings.contrast - 1])
|
629
|
rs = RadioSetting("display.contrast", "LCD Contrast",
|
630
|
val)
|
631
|
rs.set_apply_callback(self.apply_lcd_contrast, display_settings)
|
632
|
menu.append(rs)
|
633
|
|
634
|
val = RadioSettingValueList(
|
635
|
self._LAMP_CONTROL,
|
636
|
self._LAMP_CONTROL[display_settings.lamp_control])
|
637
|
rs = RadioSetting("display.lamp_control", "Lamp Control",
|
638
|
val)
|
639
|
rs.set_apply_callback(self.apply_lamp_control, display_settings)
|
640
|
menu.append(rs)
|
641
|
|
642
|
val = RadioSettingValueList(
|
643
|
self._LAMP_TIMER,
|
644
|
self._LAMP_TIMER[display_settings.lamp_timer - 2])
|
645
|
rs = RadioSetting("display.lamp_timer", "Lamp Timer",
|
646
|
val)
|
647
|
rs.set_apply_callback(self.apply_lamp_timer, display_settings)
|
648
|
menu.append(rs)
|
649
|
|
650
|
return menu
|
651
|
|
652
|
def apply_battery_saver(cls, setting, obj):
|
653
|
rawval = setting.value.get_value()
|
654
|
val = cls._BATTERY_SAVER.index(rawval)
|
655
|
obj.battery_saver = val
|
656
|
|
657
|
def apply_APO(cls, setting, obj):
|
658
|
rawval = setting.value.get_value()
|
659
|
val = cls._APO.index(rawval)
|
660
|
obj.APO = val
|
661
|
|
662
|
def _get_battery_settings(self):
|
663
|
menu = RadioSettingGroup("battery", "Battery")
|
664
|
battery_settings = self._memobj.settings
|
665
|
|
666
|
val = RadioSettingValueList(
|
667
|
self._BATTERY_SAVER,
|
668
|
self._BATTERY_SAVER[battery_settings.battery_saver])
|
669
|
rs = RadioSetting("battery.battery_saver", "Battery Saver",
|
670
|
val)
|
671
|
rs.set_apply_callback(self.apply_battery_saver, battery_settings)
|
672
|
menu.append(rs)
|
673
|
|
674
|
val = RadioSettingValueList(
|
675
|
self._APO,
|
676
|
self._APO[battery_settings.APO])
|
677
|
rs = RadioSetting("battery.APO", "Auto Power Off",
|
678
|
val)
|
679
|
rs.set_apply_callback(self.apply_APO, battery_settings)
|
680
|
menu.append(rs)
|
681
|
|
682
|
return menu
|
683
|
|
684
|
def apply_balance(cls, setting, obj):
|
685
|
rawval = setting.value.get_value()
|
686
|
val = cls._AUDIO_BALANCE.index(rawval)
|
687
|
obj.balance = val
|
688
|
|
689
|
def apply_key_beep(cls, setting, obj):
|
690
|
rawval = setting.value.get_value()
|
691
|
val = cls._KEY_BEEP.index(rawval)
|
692
|
obj.key_beep = val
|
693
|
|
694
|
def _get_audio_settings(self):
|
695
|
menu = RadioSettingGroup("audio", "Audio")
|
696
|
audio_settings = self._memobj.settings
|
697
|
|
698
|
val = RadioSettingValueList(
|
699
|
self._AUDIO_BALANCE,
|
700
|
self._AUDIO_BALANCE[audio_settings.balance])
|
701
|
rs = RadioSetting("audio.balance", "Balance",
|
702
|
val)
|
703
|
rs.set_apply_callback(self.apply_balance, audio_settings)
|
704
|
menu.append(rs)
|
705
|
|
706
|
val = RadioSettingValueList(
|
707
|
self._KEY_BEEP,
|
708
|
self._KEY_BEEP[audio_settings.key_beep])
|
709
|
rs = RadioSetting("audio.key_beep", "Key Beep",
|
710
|
val)
|
711
|
rs.set_apply_callback(self.apply_key_beep, audio_settings)
|
712
|
menu.append(rs)
|
713
|
|
714
|
return menu
|
715
|
|
716
|
@staticmethod
|
717
|
def _add_ff_pad(val, length):
|
718
|
return val.ljust(length, "\xFF")[:length]
|
719
|
|
720
|
@classmethod
|
721
|
def _strip_ff_pads(cls, messages):
|
722
|
result = []
|
723
|
for msg_text in messages:
|
724
|
result.append(str(msg_text).rstrip("\xFF"))
|
725
|
return result
|
726
|
|
727
|
if __name__ == "__main__":
|
728
|
import sys
|
729
|
import serial
|
730
|
import detect
|
731
|
import getopt
|
732
|
|
733
|
def fixopts(opts):
|
734
|
r = {}
|
735
|
for opt in opts:
|
736
|
k, v = opt
|
737
|
r[k] = v
|
738
|
return r
|
739
|
|
740
|
def usage():
|
741
|
print("Usage: %s <-i input.img>|<-o output.img> -p port " \
|
742
|
"[[-f first-addr] [-l last-addr] | [-b list,of,blocks]]" % \
|
743
|
sys.argv[0])
|
744
|
sys.exit(1)
|
745
|
|
746
|
opts, args = getopt.getopt(sys.argv[1:], "i:o:p:f:l:b:")
|
747
|
opts = fixopts(opts)
|
748
|
first = last = 0
|
749
|
blocks = None
|
750
|
if '-i' in opts:
|
751
|
fname = opts['-i']
|
752
|
download = False
|
753
|
elif '-o' in opts:
|
754
|
fname = opts['-o']
|
755
|
download = True
|
756
|
else:
|
757
|
usage()
|
758
|
if '-p' in opts:
|
759
|
port = opts['-p']
|
760
|
else:
|
761
|
usage()
|
762
|
|
763
|
if '-f' in opts:
|
764
|
first = int(opts['-f'], 0)
|
765
|
if '-l' in opts:
|
766
|
last = int(opts['-l'], 0)
|
767
|
if '-b' in opts:
|
768
|
blocks = [int(b, 0) for b in opts['-b'].split(',')]
|
769
|
blocks.sort()
|
770
|
|
771
|
ser = serial.Serial(port=port, baudrate=9600, timeout=0.25)
|
772
|
r = THD72Radio(ser)
|
773
|
memmax = r._memsize
|
774
|
if not download:
|
775
|
memmax -= 512
|
776
|
|
777
|
if blocks is None:
|
778
|
if first < 0 or first > (r._memsize - 1):
|
779
|
raise errors.RadioError("first address out of range")
|
780
|
if (last > 0 and last < first) or last > memmax:
|
781
|
raise errors.RadioError("last address out of range")
|
782
|
elif last == 0:
|
783
|
last = memmax
|
784
|
first /= 256
|
785
|
if last % 256 != 0:
|
786
|
last += 256
|
787
|
last /= 256
|
788
|
blocks = range(first, last)
|
789
|
|
790
|
if download:
|
791
|
data = r.download(True, blocks)
|
792
|
file(fname, "wb").write(data)
|
793
|
else:
|
794
|
r._mmap = file(fname, "rb").read(r._memsize)
|
795
|
r.upload(blocks)
|
796
|
print("\nDone")
|