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