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
|
UNKNOWN_LOOKUP = (0, 7, 4, 0, 4, 7)
|
202
|
|
203
|
def get_prog_vfo(frequency):
|
204
|
for i, (start, end) in enumerate(DEFAULT_PROG_VFO):
|
205
|
if start <= frequency < end:
|
206
|
return i
|
207
|
raise ValueError("Frequency is out of range.")
|
208
|
|
209
|
|
210
|
@directory.register
|
211
|
class THD72Radio(chirp_common.CloneModeRadio):
|
212
|
|
213
|
BAUD_RATE = 9600
|
214
|
VENDOR = "Kenwood"
|
215
|
MODEL = "TH-D72 (clone mode)"
|
216
|
HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow
|
217
|
|
218
|
mem_upper_limit = 1022
|
219
|
_memsize = 65536
|
220
|
_model = "" # FIXME: REMOVE
|
221
|
_dirty_blocks = []
|
222
|
|
223
|
_LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
|
224
|
_LAMP_CONTROL = ["Manual", "Auto"]
|
225
|
_LAMP_TIMER = ["Seconds %d" % x for x in range(2, 11)]
|
226
|
_BATTERY_SAVER = ["OFF", "0.03 Seconds", "0.2 Seconds", "0.4 Seconds",
|
227
|
"0.6 Seconds", "0.8 Seconds", "1 Seconds", "2 Seconds",
|
228
|
"3 Seconds", "4 Seconds", "5 Seconds"]
|
229
|
_APO = ["OFF", "15 Minutes", "30 Minutes", "60 Minutes"]
|
230
|
_AUDIO_BALANCE = ["Center", "A +50%", "A +100%", "B +50%", "B +100%"]
|
231
|
_KEY_BEEP = ["OFF", "Radio & GPS", "Radio Only", "GPS Only"]
|
232
|
|
233
|
def get_features(self):
|
234
|
rf = chirp_common.RadioFeatures()
|
235
|
rf.memory_bounds = (0, 1031)
|
236
|
rf.valid_bands = [(118000000, 174000000),
|
237
|
(320000000, 524000000)]
|
238
|
rf.has_cross = True
|
239
|
rf.can_odd_split = True
|
240
|
rf.has_dtcs_polarity = False
|
241
|
rf.has_tuning_step = False
|
242
|
rf.has_bank = False
|
243
|
rf.has_settings = True
|
244
|
rf.valid_tuning_steps = []
|
245
|
rf.valid_modes = MODES_REV.keys()
|
246
|
rf.valid_tmodes = TMODES_REV.keys()
|
247
|
rf.valid_duplexes = DUPLEX_REV.keys()
|
248
|
rf.valid_skips = ["", "S"]
|
249
|
rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
|
250
|
rf.valid_name_length = 8
|
251
|
return rf
|
252
|
|
253
|
def process_mmap(self):
|
254
|
self._memobj = bitwise.parse(mem_format, self._mmap)
|
255
|
self._dirty_blocks = []
|
256
|
|
257
|
def _detect_baud(self):
|
258
|
for baud in [9600, 19200, 38400, 57600]:
|
259
|
self.pipe.baudrate = baud
|
260
|
try:
|
261
|
self.pipe.write("\r\r")
|
262
|
except:
|
263
|
break
|
264
|
self.pipe.read(32)
|
265
|
try:
|
266
|
id = self.get_id()
|
267
|
LOG.info("Radio %s at %i baud" % (id, baud))
|
268
|
return True
|
269
|
except errors.RadioError:
|
270
|
pass
|
271
|
|
272
|
raise errors.RadioError("No response from radio")
|
273
|
|
274
|
def get_special_locations(self):
|
275
|
return sorted(THD72_SPECIAL.keys())
|
276
|
|
277
|
def add_dirty_block(self, memobj):
|
278
|
block = memobj._offset / 256
|
279
|
if block not in self._dirty_blocks:
|
280
|
self._dirty_blocks.append(block)
|
281
|
self._dirty_blocks.sort()
|
282
|
print("dirty blocks: ", self._dirty_blocks)
|
283
|
|
284
|
def get_channel_name(self, number):
|
285
|
if number < 999:
|
286
|
name = str(self._memobj.channel_name[number].name) + '\xff'
|
287
|
elif number >= 1020 and number < 1030:
|
288
|
number -= 1020
|
289
|
name = str(self._memobj.wx_name[number].name) + '\xff'
|
290
|
else:
|
291
|
return ''
|
292
|
return name[:name.index('\xff')].rstrip()
|
293
|
|
294
|
def set_channel_name(self, number, name):
|
295
|
name = name[:8] + '\xff' * 8
|
296
|
if number < 999:
|
297
|
self._memobj.channel_name[number].name = name[:8]
|
298
|
self.add_dirty_block(self._memobj.channel_name[number])
|
299
|
elif number >= 1020 and number < 1030:
|
300
|
number -= 1020
|
301
|
self._memobj.wx_name[number].name = name[:8]
|
302
|
self.add_dirty_block(self._memobj.wx_name[number])
|
303
|
|
304
|
def get_raw_memory(self, number):
|
305
|
return repr(self._memobj.memory[number]) + \
|
306
|
repr(self._memobj.flag[number])
|
307
|
|
308
|
def get_memory(self, number):
|
309
|
if isinstance(number, str):
|
310
|
try:
|
311
|
number = THD72_SPECIAL[number]
|
312
|
except KeyError:
|
313
|
raise errors.InvalidMemoryLocation("Unknown channel %s" %
|
314
|
number)
|
315
|
|
316
|
if number < 0 or number > (max(THD72_SPECIAL.values()) + 1):
|
317
|
raise errors.InvalidMemoryLocation(
|
318
|
"Number must be between 0 and 999")
|
319
|
|
320
|
_mem = self._memobj.memory[number]
|
321
|
flag = self._memobj.flag[number]
|
322
|
|
323
|
mem = chirp_common.Memory()
|
324
|
mem.number = number
|
325
|
|
326
|
if number > 999:
|
327
|
mem.extd_number = THD72_SPECIAL_REV[number]
|
328
|
if flag.disabled == 0xf:
|
329
|
mem.empty = True
|
330
|
return mem
|
331
|
|
332
|
mem.name = self.get_channel_name(number)
|
333
|
mem.freq = int(_mem.freq)
|
334
|
mem.tmode = TMODES[int(_mem.tone_mode)]
|
335
|
mem.rtone = chirp_common.TONES[_mem.rtone]
|
336
|
mem.ctone = chirp_common.TONES[_mem.ctone]
|
337
|
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
|
338
|
mem.duplex = DUPLEX[int(_mem.duplex)]
|
339
|
mem.offset = int(_mem.offset)
|
340
|
mem.mode = MODES[int(_mem.mode)]
|
341
|
|
342
|
if number < 999:
|
343
|
mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)]
|
344
|
mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode]
|
345
|
if number > 999:
|
346
|
mem.cross_mode = chirp_common.CROSS_MODES[0]
|
347
|
mem.immutable = ["number", "bank", "extd_number", "cross_mode"]
|
348
|
if number >= 1020 and number < 1030:
|
349
|
mem.immutable += ["freq", "offset", "tone", "mode",
|
350
|
"tmode", "ctone", "skip"] # FIXME: ALL
|
351
|
else:
|
352
|
mem.immutable += ["name"]
|
353
|
|
354
|
return mem
|
355
|
|
356
|
def set_memory(self, mem):
|
357
|
LOG.debug("set_memory(%d)" % mem.number)
|
358
|
if mem.number < 0 or mem.number > (max(THD72_SPECIAL.values()) + 1):
|
359
|
raise errors.InvalidMemoryLocation(
|
360
|
"Number must be between 0 and 999")
|
361
|
|
362
|
# weather channels can only change name, nothing else
|
363
|
if mem.number >= 1020 and mem.number < 1030:
|
364
|
self.set_channel_name(mem.number, mem.name)
|
365
|
return
|
366
|
|
367
|
flag = self._memobj.flag[mem.number]
|
368
|
self.add_dirty_block(self._memobj.flag[mem.number])
|
369
|
|
370
|
# only delete non-WX channels
|
371
|
was_empty = flag.disabled == 0xf
|
372
|
if mem.empty:
|
373
|
flag.disabled = 0xf
|
374
|
return
|
375
|
flag.disabled = 0
|
376
|
|
377
|
_mem = self._memobj.memory[mem.number]
|
378
|
self.add_dirty_block(_mem)
|
379
|
if was_empty:
|
380
|
self.initialize(_mem)
|
381
|
|
382
|
_mem.freq = mem.freq
|
383
|
|
384
|
if mem.number < 999:
|
385
|
self.set_channel_name(mem.number, mem.name)
|
386
|
|
387
|
_mem.tone_mode = TMODES_REV[mem.tmode]
|
388
|
_mem.rtone = chirp_common.TONES.index(mem.rtone)
|
389
|
_mem.ctone = chirp_common.TONES.index(mem.ctone)
|
390
|
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
|
391
|
_mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode)
|
392
|
_mem.duplex = DUPLEX_REV[mem.duplex]
|
393
|
_mem.offset = mem.offset
|
394
|
_mem.mode = MODES_REV[mem.mode]
|
395
|
|
396
|
prog_vfo = get_prog_vfo(mem.freq)
|
397
|
flag.prog_vfo = prog_vfo
|
398
|
_mem.unknown1 = _mem.unknown2 = UNKNOWN_LOOKUP[prog_vfo]
|
399
|
|
400
|
if mem.number < 999:
|
401
|
flag.skip = chirp_common.SKIP_VALUES.index(mem.skip)
|
402
|
|
403
|
def sync_in(self):
|
404
|
self._detect_baud()
|
405
|
self._mmap = self.download()
|
406
|
self.process_mmap()
|
407
|
|
408
|
def sync_out(self):
|
409
|
self._detect_baud()
|
410
|
if len(self._dirty_blocks):
|
411
|
self.upload(self._dirty_blocks)
|
412
|
else:
|
413
|
self.upload()
|
414
|
|
415
|
def read_block(self, block, count=256):
|
416
|
self.pipe.write(struct.pack("<cBHB", "R", 0, block, 0))
|
417
|
r = self.pipe.read(5)
|
418
|
if len(r) != 5:
|
419
|
raise Exception("Did not receive block response")
|
420
|
|
421
|
cmd, _zero, _block, zero = struct.unpack("<cBHB", r)
|
422
|
if cmd != "W" or _block != block:
|
423
|
raise Exception("Invalid response: %s %i" % (cmd, _block))
|
424
|
|
425
|
data = ""
|
426
|
while len(data) < count:
|
427
|
data += self.pipe.read(count - len(data))
|
428
|
|
429
|
self.pipe.write(chr(0x06))
|
430
|
if self.pipe.read(1) != chr(0x06):
|
431
|
raise Exception("Did not receive post-block ACK!")
|
432
|
|
433
|
return data
|
434
|
|
435
|
def write_block(self, block, map):
|
436
|
self.pipe.write(struct.pack("<cBHB", "W", 0, block, 0))
|
437
|
base = block * 256
|
438
|
self.pipe.write(map[base:base + 256])
|
439
|
|
440
|
ack = self.pipe.read(1)
|
441
|
|
442
|
return ack == chr(0x06)
|
443
|
|
444
|
def download(self, raw=False, blocks=None):
|
445
|
if blocks is None:
|
446
|
blocks = range(self._memsize / 256)
|
447
|
else:
|
448
|
blocks = [b for b in blocks if b < self._memsize / 256]
|
449
|
|
450
|
if self.command("0M PROGRAM") != "0M":
|
451
|
raise errors.RadioError("No response from self")
|
452
|
|
453
|
allblocks = range(self._memsize / 256)
|
454
|
self.pipe.baudrate = 57600
|
455
|
try:
|
456
|
self.pipe.setRTS()
|
457
|
except AttributeError:
|
458
|
self.pipe.rts = True
|
459
|
self.pipe.read(1)
|
460
|
data = ""
|
461
|
LOG.debug("reading blocks %d..%d" % (blocks[0], blocks[-1]))
|
462
|
total = len(blocks)
|
463
|
count = 0
|
464
|
for i in allblocks:
|
465
|
if i not in blocks:
|
466
|
data += 256 * '\xff'
|
467
|
continue
|
468
|
data += self.read_block(i)
|
469
|
count += 1
|
470
|
if self.status_fn:
|
471
|
s = chirp_common.Status()
|
472
|
s.msg = "Cloning from radio"
|
473
|
s.max = total
|
474
|
s.cur = count
|
475
|
self.status_fn(s)
|
476
|
|
477
|
self.pipe.write("E")
|
478
|
|
479
|
if raw:
|
480
|
return data
|
481
|
return memmap.MemoryMap(data)
|
482
|
|
483
|
def upload(self, blocks=None):
|
484
|
if blocks is None:
|
485
|
blocks = range((self._memsize / 256) - 2)
|
486
|
else:
|
487
|
blocks = [b for b in blocks if b < self._memsize / 256]
|
488
|
|
489
|
if self.command("0M PROGRAM") != "0M":
|
490
|
raise errors.RadioError("No response from self")
|
491
|
|
492
|
self.pipe.baudrate = 57600
|
493
|
try:
|
494
|
self.pipe.setRTS()
|
495
|
except AttributeError:
|
496
|
self.pipe.rts = True
|
497
|
self.pipe.read(1)
|
498
|
LOG.debug("writing blocks %d..%d" % (blocks[0], blocks[-1]))
|
499
|
total = len(blocks)
|
500
|
count = 0
|
501
|
for i in blocks:
|
502
|
r = self.write_block(i, self._mmap)
|
503
|
count += 1
|
504
|
if not r:
|
505
|
raise errors.RadioError("self NAK'd block %i" % i)
|
506
|
if self.status_fn:
|
507
|
s = chirp_common.Status()
|
508
|
s.msg = "Cloning to radio"
|
509
|
s.max = total
|
510
|
s.cur = count
|
511
|
self.status_fn(s)
|
512
|
|
513
|
self.pipe.write("E")
|
514
|
# clear out blocks we uploaded from the dirty blocks list
|
515
|
self._dirty_blocks = [b for b in self._dirty_blocks if b not in blocks]
|
516
|
|
517
|
def command(self, cmd, timeout=0.5):
|
518
|
start = time.time()
|
519
|
|
520
|
data = ""
|
521
|
LOG.debug("PC->D72: %s" % cmd)
|
522
|
self.pipe.write(cmd + "\r")
|
523
|
while not data.endswith("\r") and (time.time() - start) < timeout:
|
524
|
data += self.pipe.read(1)
|
525
|
LOG.debug("D72->PC: %s" % data.strip())
|
526
|
return data.strip()
|
527
|
|
528
|
def get_id(self):
|
529
|
r = self.command("ID")
|
530
|
if r.startswith("ID "):
|
531
|
return r.split(" ")[1]
|
532
|
else:
|
533
|
raise errors.RadioError("No response to ID command")
|
534
|
|
535
|
def initialize(self, mmap):
|
536
|
mmap.set_raw("\x00\xc8\xb3\x08\x00\x01\x00\x08"
|
537
|
"\x08\x00\xc0\x27\x09\x00\x00\x00")
|
538
|
|
539
|
def _get_settings(self):
|
540
|
top = RadioSettings(self._get_display_settings(),
|
541
|
self._get_audio_settings(),
|
542
|
self._get_battery_settings())
|
543
|
return top
|
544
|
|
545
|
def set_settings(self, settings):
|
546
|
_mem = self._memobj
|
547
|
for element in settings:
|
548
|
if not isinstance(element, RadioSetting):
|
549
|
self.set_settings(element)
|
550
|
continue
|
551
|
if not element.changed():
|
552
|
continue
|
553
|
try:
|
554
|
if element.has_apply_callback():
|
555
|
LOG.debug("Using apply callback")
|
556
|
try:
|
557
|
element.run_apply_callback()
|
558
|
except NotImplementedError as e:
|
559
|
LOG.error(e)
|
560
|
continue
|
561
|
|
562
|
# Find the object containing setting.
|
563
|
obj = _mem
|
564
|
bits = element.get_name().split(".")
|
565
|
setting = bits[-1]
|
566
|
for name in bits[:-1]:
|
567
|
if name.endswith("]"):
|
568
|
name, index = name.split("[")
|
569
|
index = int(index[:-1])
|
570
|
obj = getattr(obj, name)[index]
|
571
|
else:
|
572
|
obj = getattr(obj, name)
|
573
|
|
574
|
try:
|
575
|
old_val = getattr(obj, setting)
|
576
|
LOG.debug("Setting %s(%r) <= %s" % (
|
577
|
element.get_name(), old_val, element.value))
|
578
|
setattr(obj, setting, element.value)
|
579
|
except AttributeError as e:
|
580
|
LOG.error("Setting %s is not in the memory map: %s" %
|
581
|
(element.get_name(), e))
|
582
|
except Exception, e:
|
583
|
LOG.debug(element.get_name())
|
584
|
raise
|
585
|
|
586
|
def get_settings(self):
|
587
|
try:
|
588
|
return self._get_settings()
|
589
|
except:
|
590
|
import traceback
|
591
|
LOG.error("Failed to parse settings: %s", traceback.format_exc())
|
592
|
return None
|
593
|
|
594
|
@classmethod
|
595
|
def apply_power_on_msg(cls, setting, obj):
|
596
|
message = setting.value.get_value()
|
597
|
setattr(obj, "power_on_msg", cls._add_ff_pad(message, 8))
|
598
|
|
599
|
def apply_lcd_contrast(cls, setting, obj):
|
600
|
rawval = setting.value.get_value()
|
601
|
val = cls._LCD_CONTRAST.index(rawval) + 1
|
602
|
obj.contrast = val
|
603
|
|
604
|
def apply_lamp_control(cls, setting, obj):
|
605
|
rawval = setting.value.get_value()
|
606
|
val = cls._LAMP_CONTROL.index(rawval)
|
607
|
obj.lamp_control = val
|
608
|
|
609
|
def apply_lamp_timer(cls, setting, obj):
|
610
|
rawval = setting.value.get_value()
|
611
|
val = cls._LAMP_TIMER.index(rawval) + 2
|
612
|
obj.lamp_timer = val
|
613
|
|
614
|
def _get_display_settings(self):
|
615
|
menu = RadioSettingGroup("display", "Display")
|
616
|
display_settings = self._memobj.settings
|
617
|
|
618
|
val = RadioSettingValueString(
|
619
|
0, 8, str(display_settings.power_on_msg).rstrip("\xFF"))
|
620
|
rs = RadioSetting("display.power_on_msg", "Power on message", val)
|
621
|
rs.set_apply_callback(self.apply_power_on_msg, display_settings)
|
622
|
menu.append(rs)
|
623
|
|
624
|
val = RadioSettingValueList(
|
625
|
self._LCD_CONTRAST,
|
626
|
self._LCD_CONTRAST[display_settings.contrast - 1])
|
627
|
rs = RadioSetting("display.contrast", "LCD Contrast",
|
628
|
val)
|
629
|
rs.set_apply_callback(self.apply_lcd_contrast, display_settings)
|
630
|
menu.append(rs)
|
631
|
|
632
|
val = RadioSettingValueList(
|
633
|
self._LAMP_CONTROL,
|
634
|
self._LAMP_CONTROL[display_settings.lamp_control])
|
635
|
rs = RadioSetting("display.lamp_control", "Lamp Control",
|
636
|
val)
|
637
|
rs.set_apply_callback(self.apply_lamp_control, display_settings)
|
638
|
menu.append(rs)
|
639
|
|
640
|
val = RadioSettingValueList(
|
641
|
self._LAMP_TIMER,
|
642
|
self._LAMP_TIMER[display_settings.lamp_timer - 2])
|
643
|
rs = RadioSetting("display.lamp_timer", "Lamp Timer",
|
644
|
val)
|
645
|
rs.set_apply_callback(self.apply_lamp_timer, display_settings)
|
646
|
menu.append(rs)
|
647
|
|
648
|
return menu
|
649
|
|
650
|
def apply_battery_saver(cls, setting, obj):
|
651
|
rawval = setting.value.get_value()
|
652
|
val = cls._BATTERY_SAVER.index(rawval)
|
653
|
obj.battery_saver = val
|
654
|
|
655
|
def apply_APO(cls, setting, obj):
|
656
|
rawval = setting.value.get_value()
|
657
|
val = cls._APO.index(rawval)
|
658
|
obj.APO = val
|
659
|
|
660
|
def _get_battery_settings(self):
|
661
|
menu = RadioSettingGroup("battery", "Battery")
|
662
|
battery_settings = self._memobj.settings
|
663
|
|
664
|
val = RadioSettingValueList(
|
665
|
self._BATTERY_SAVER,
|
666
|
self._BATTERY_SAVER[battery_settings.battery_saver])
|
667
|
rs = RadioSetting("battery.battery_saver", "Battery Saver",
|
668
|
val)
|
669
|
rs.set_apply_callback(self.apply_battery_saver, battery_settings)
|
670
|
menu.append(rs)
|
671
|
|
672
|
val = RadioSettingValueList(
|
673
|
self._APO,
|
674
|
self._APO[battery_settings.APO])
|
675
|
rs = RadioSetting("battery.APO", "Auto Power Off",
|
676
|
val)
|
677
|
rs.set_apply_callback(self.apply_APO, battery_settings)
|
678
|
menu.append(rs)
|
679
|
|
680
|
return menu
|
681
|
|
682
|
def apply_balance(cls, setting, obj):
|
683
|
rawval = setting.value.get_value()
|
684
|
val = cls._AUDIO_BALANCE.index(rawval)
|
685
|
obj.balance = val
|
686
|
|
687
|
def apply_key_beep(cls, setting, obj):
|
688
|
rawval = setting.value.get_value()
|
689
|
val = cls._KEY_BEEP.index(rawval)
|
690
|
obj.key_beep = val
|
691
|
|
692
|
def _get_audio_settings(self):
|
693
|
menu = RadioSettingGroup("audio", "Audio")
|
694
|
audio_settings = self._memobj.settings
|
695
|
|
696
|
val = RadioSettingValueList(
|
697
|
self._AUDIO_BALANCE,
|
698
|
self._AUDIO_BALANCE[audio_settings.balance])
|
699
|
rs = RadioSetting("audio.balance", "Balance",
|
700
|
val)
|
701
|
rs.set_apply_callback(self.apply_balance, audio_settings)
|
702
|
menu.append(rs)
|
703
|
|
704
|
val = RadioSettingValueList(
|
705
|
self._KEY_BEEP,
|
706
|
self._KEY_BEEP[audio_settings.key_beep])
|
707
|
rs = RadioSetting("audio.key_beep", "Key Beep",
|
708
|
val)
|
709
|
rs.set_apply_callback(self.apply_key_beep, audio_settings)
|
710
|
menu.append(rs)
|
711
|
|
712
|
return menu
|
713
|
|
714
|
@staticmethod
|
715
|
def _add_ff_pad(val, length):
|
716
|
return val.ljust(length, "\xFF")[:length]
|
717
|
|
718
|
@classmethod
|
719
|
def _strip_ff_pads(cls, messages):
|
720
|
result = []
|
721
|
for msg_text in messages:
|
722
|
result.append(str(msg_text).rstrip("\xFF"))
|
723
|
return result
|
724
|
|
725
|
if __name__ == "__main__":
|
726
|
import sys
|
727
|
import serial
|
728
|
import detect
|
729
|
import getopt
|
730
|
|
731
|
def fixopts(opts):
|
732
|
r = {}
|
733
|
for opt in opts:
|
734
|
k, v = opt
|
735
|
r[k] = v
|
736
|
return r
|
737
|
|
738
|
def usage():
|
739
|
print "Usage: %s <-i input.img>|<-o output.img> -p port " \
|
740
|
"[[-f first-addr] [-l last-addr] | [-b list,of,blocks]]" % \
|
741
|
sys.argv[0]
|
742
|
sys.exit(1)
|
743
|
|
744
|
opts, args = getopt.getopt(sys.argv[1:], "i:o:p:f:l:b:")
|
745
|
opts = fixopts(opts)
|
746
|
first = last = 0
|
747
|
blocks = None
|
748
|
if '-i' in opts:
|
749
|
fname = opts['-i']
|
750
|
download = False
|
751
|
elif '-o' in opts:
|
752
|
fname = opts['-o']
|
753
|
download = True
|
754
|
else:
|
755
|
usage()
|
756
|
if '-p' in opts:
|
757
|
port = opts['-p']
|
758
|
else:
|
759
|
usage()
|
760
|
|
761
|
if '-f' in opts:
|
762
|
first = int(opts['-f'], 0)
|
763
|
if '-l' in opts:
|
764
|
last = int(opts['-l'], 0)
|
765
|
if '-b' in opts:
|
766
|
blocks = [int(b, 0) for b in opts['-b'].split(',')]
|
767
|
blocks.sort()
|
768
|
|
769
|
ser = serial.Serial(port=port, baudrate=9600, timeout=0.25)
|
770
|
r = THD72Radio(ser)
|
771
|
memmax = r._memsize
|
772
|
if not download:
|
773
|
memmax -= 512
|
774
|
|
775
|
if blocks is None:
|
776
|
if first < 0 or first > (r._memsize - 1):
|
777
|
raise errors.RadioError("first address out of range")
|
778
|
if (last > 0 and last < first) or last > memmax:
|
779
|
raise errors.RadioError("last address out of range")
|
780
|
elif last == 0:
|
781
|
last = memmax
|
782
|
first /= 256
|
783
|
if last % 256 != 0:
|
784
|
last += 256
|
785
|
last /= 256
|
786
|
blocks = range(first, last)
|
787
|
|
788
|
if download:
|
789
|
data = r.download(True, blocks)
|
790
|
file(fname, "wb").write(data)
|
791
|
else:
|
792
|
r._mmap = file(fname, "rb").read(r._memsize)
|
793
|
r.upload(blocks)
|
794
|
print "\nDone"
|