Project

General

Profile

Bug #1611 » thd72.py

Tom Hayward, 03/17/2017 03:54 PM

 
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"
(10-10/11)