thd72.py

d72 driver with some python3 mods - Angus Ainslie, 06/14/2020 06:01 pm

Download (23.7 kB)

 
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")