Project

General

Profile

New Model #4129 » thd74.py

TH-D74 driver - Angus Ainslie, 06/21/2020 09:32 AM

 
1
import logging
2
import struct
3
import binascii
4

    
5
import time
6

    
7
from chirp import directory, bitwise, errors, chirp_common, memmap
8
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
9
            RadioSettingValueInteger, RadioSettingValueString, \
10
            RadioSettingValueList, RadioSettingValueBoolean, \
11
            InvalidValueError
12

    
13
from . import thd72
14
from chirp.util import hexprint
15

    
16
LOG = logging.getLogger(__name__)
17

    
18
# Save files from MCP-D74 have a 256-byte header, and possibly some oddness
19
# TH-D74 memory map
20

    
21
# 0x02000: memory flags, 4 bytes per memory
22
# 0x04000: memories, each 40 bytes long
23
# 0x10000: names, each 16 bytes long, null padded, ascii encoded
24

    
25
# memory channel
26
# 0 1 2 3  4 5     6            7     8     9    a          b c d e   f
27
# [freq ]  ? mode  tmode/duplex rtone ctone dtcs cross_mode [offset]  ?
28

    
29
# frequency is 32-bit unsigned little-endian Hz
30

    
31
DEFAULT_PROG_VFO = (
32
    (136000000, 174000000),
33
    (410000000, 470000000),
34
    (118000000, 136000000),
35
    (136000000, 174000000),
36
    (320000000, 400000000),
37
    (400000000, 524000000),
38
)
39

    
40
# Some of the blocks written by MCP have a length set of less than 0x00/256
41
BLOCK_SIZES = {
42
    0x0003: 0x00B4,
43
    0x0007: 0x0068,
44
}
45

    
46
mem_format = """
47
// TODO: find lockout
48
struct {
49
  u8 variant;
50
  u8 region;
51
} radio_type;
52

    
53
#seekto 0x390;
54
struct {
55
  u8 ukn[14];
56
  u8 empty; // no stations 0xFF, staions 0x0
57
  u8 ukn2;
58
  ul32 tuned_station;
59
} current_fm_radio;
60

    
61
#seekto 0x1040;
62
struct {
63
  u8 enable; // FM radio on 0x01 off 0x00
64
  u8 seconds;
65
} fm_radio_settings;
66

    
67
#seekto 0x1088;
68
struct {
69
  u8 dist_units; // 0 - mile, 1 - KM
70
  u8 rain_units; // 0 - inch, 1 - mm
71
  u8 temp_units; // 0 - F, 1 - C
72
} unit_settings;
73

    
74
#seekto 0x10c0;
75
struct {
76
  char power_on_msg[16];
77
  char modem_name[16];
78
} onmsg_name;
79

    
80
#seekto 0x1100;
81
struct {
82
  u8 enable; // gps on 0x01 gps off 0x00
83
} gps_settings;
84

    
85
#seekto 0x1200;
86
struct {
87
  char callsign[8];
88
} callsign;
89

    
90
#seekto 0x02000;
91
struct {
92
// 4 bytes long
93
  u8   disabled;
94
  u8   unk;
95
  u8   group;
96
  u8   unk2;
97
} flag[1032];
98

    
99
#seekto 0x04000;
100
// TODO: deal with the 16-byte trailers of every block
101
struct {
102
    struct {
103
      ul32 freq;
104
      ul32 offset;
105
      
106
      u8   tuning_step:4,
107
           unk:4;
108
      u8   mode:4,
109
           unk1:4;
110
      u8   tone_mode:4,
111
           duplex:4;
112
      u8   rtone;
113
      
114
      u8   ctone;
115
      u8   dtcs;
116
      u8   cross_mode:4
117
           digital_squelch:4;
118
      char urcall[8];
119
      char rpt1[8];
120
      char rpt2[8];
121
      
122
      u8   digital_squelch_code;
123
      
124
    } mem[6];
125
    
126
    u8 pad[16];
127
} memory[172]; // TODO: correct number of memories
128

    
129
#seekto 0x4cd00;
130
struct {
131
    ul32 freq;
132
    char name[16];
133
} fm_radio_memory[10];
134

    
135
#seekto 0x10000;
136
struct {
137
  char name[16];
138
} channel_name[1000];
139

    
140
#seekto 0x14700;
141
struct {
142
  char name[16];
143
} wx_name[10];
144

    
145
#seekto 0x144d0;
146
struct {
147
  char name[16];
148
} call_name[6];
149

    
150
#seekto 0x14800;
151
struct {
152
  char name[16];
153
} group_name[31];
154
"""
155

    
156
STEPS = [5.0, 6.25, None, None, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0, 9.0]
157

    
158
MODES = [
159
    "FM",
160
    "DV",
161
    "AM",
162
    "LSB",
163
    "USB",
164
    "CW",
165
    "NFM",
166
    "DV"
167
]
168

    
169
MODES_REV = {
170
    "FM" : 000,
171
    "DV" : 0x1,
172
    "AM" : 0x2,
173
    "LSB": 0x3,
174
    "USB": 0x4,
175
    "CW" : 0x5,
176
    "NFM": 0x6,
177
    "DV" : 0x7
178
}
179

    
180
DEFAULT_PROG_VFO = (
181
    (136000000, 174000000),
182
    (216000000, 259000000),
183
    (410000000, 470000000),
184
    (   540000,   3500000),
185
    (  3500000,   5100000),
186
    ( 51000000,  87000000),
187
    ( 87000000, 108000000),
188
    (118000000, 136000000),
189
    (136000000, 174000000),
190
    (216000000, 259000000),
191
    (400000000, 524000000),
192
    (400000000, 524000000),
193
)
194

    
195
def get_prog_vfo(frequency):
196
    for i, (start, end) in enumerate(DEFAULT_PROG_VFO):
197
        if start <= frequency < end:
198
            return i
199
    raise ValueError("Frequency is out of range.")
200

    
201
def hex(data):
202
    data_txt = ""
203
    for idx in range(0, len(data), 16):
204
        bytes = binascii.hexlify(str(data[idx:idx+16]).encode('utf8')).upper()
205
        for idx in range(0, len(bytes), 2):
206
            data_txt += str(bytes[idx:idx+2]) + " "
207
        data_txt += "\n"
208
    return data_txt.strip()
209

    
210
class SProxy(object):
211
    def __init__(self, delegate):
212
        self.delegate = delegate
213

    
214
    def read(self, len):
215
        r = self.delegate.read(len)
216
        LOG.debug("READ\n" + hex(r))
217
        return r
218

    
219
    def write(self, data):
220
        LOG.debug("WRITE\n" + hex(data))
221
        return self.delegate.write(str(data))
222

    
223
    @property
224
    def timeout(self):
225
        return self.delegate.timeout
226

    
227
    @timeout.setter
228
    def timeout(self, timeout):
229
        self.delegate.timeout = timeout
230

    
231

    
232

    
233
@directory.register
234
class THD74Radio(thd72.THD72Radio):
235
    MODEL = "TH-D74 (clone mode)"
236
    #MODEL = "TH-D74"
237
    _memsize = 500480
238
    # I think baud rate might be ignored by USB-Serial stack of the D74's
239
    # on-board FTDI chip, but it doesn't seem to hurt.
240
    BAUD_RATE = 115200
241

    
242

    
243
    #def __init__(self, pipe):
244
    #    pipe = SProxy(pipe)
245
    #    super(THD74Radio, self).__init__(pipe)
246

    
247
    def get_features(self):
248
        rf = super(THD74Radio, self).get_features()
249
        rf.valid_bands = [(118000000, 174000000),
250
                          (216000000, 260000000),
251
                          (320000000, 524000000)]
252
        rf.valid_modes = list(MODES_REV.keys())
253
        rf.has_tuning_step = True
254
        rf.valid_name_length = 16
255
        return rf
256

    
257
    def process_mmap(self):
258
        self._memobj = bitwise.parse(mem_format, self._mmap)
259
        self._dirty_blocks = []
260

    
261
    def sync_in(self):
262
        # self._detect_baud()
263
        self._mmap = self.download()
264
        self.process_mmap()
265

    
266
    def sync_out(self):
267
        if len(self._dirty_blocks):
268
            self.upload(self._dirty_blocks)
269
        else:
270
            self.upload()
271

    
272
    def read_block(self, block, count=256):
273
        cmd = struct.pack(">cHH", b"R", block, count%256)
274
        #print( "Read cmd %s" % cmd )
275
        self.pipe.write(''.join(chr(b) for b in cmd))
276

    
277
        r = self.pipe.read(5)
278
        if len(r) != 5:
279
            raise Exception("Did not receive block response")
280

    
281
        #print( "Read input %s %i %i %i %i" % ( r, ord(r[1]), ord(r[2]), ord(r[3]), ord(r[4] )))
282

    
283
        #cmd, _block, _ = struct.unpack(">cHH", b''.join(ord(b) for b in r))
284
        cmd = r[0]
285
        _block = (ord(r[1]) << 8) + ord(r[2])
286
        if cmd != 'W' or _block != block:
287
            raise Exception("Invalid response: %s %i %i" % (cmd, block, _block))
288

    
289
        data = ""
290
        while len(data) < count:
291
            data += self.pipe.read(count - len(data))
292

    
293
        self.pipe.write(chr(0x06))
294
        if self.pipe.read(1) != chr(0x06):
295
            raise Exception("Did not receive post-block ACK!")
296

    
297
        return data
298

    
299
    def write_block(self, block, map, count=256):
300
        #print("Write block ", block )
301
        c = struct.pack(">cHH", b"W", block, count%256)
302
        base = block * 256
303
        data = map[base:base + count]
304
        # It's crucial that these are written together. Otherwise the radio
305
        # will fail to ACK under some conditions.
306
        c_d = ''.join(chr(b) for b in c) + data
307
        self.pipe.write(c_d)
308

    
309
        ack = self.pipe.read(1)
310

    
311
        if len(ack) == 0:
312
            print("write timed out block %d - trying again" % block )
313
            time.sleep(0.5)
314
            ack = self.pipe.read(1)
315

    
316
        if ack != chr(0x06):
317
            print("Block %d write failed %d" % ( block, ord(ack)))
318

    
319
        return ack == chr(0x06)
320

    
321
    def _unlock(self):
322
        """Voodoo sequence of operations to get the radio to accept our programming."""
323

    
324
        h = self.read_block(0, 6)
325

    
326
        unlock = ("\x57\x00\x00\x00\x30\xff\x01\xff\x00\x00\xff\xff\xff\xff\xff\x01" +
327
            "\x00\x00\x00\x03\x01\x00\x00\x00\x00\x02\x00\x30\x30\x30\x00\xff" +
328
            "\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
329
            "\xff\xff\xff\xff\xff")
330

    
331
        #"\x57\x00\x00\x00\x30\xff\x01\xff\x00\x00\xff\xff\xff\xff\xff\x01" \
332
        #"\x00\x00\x00\x03\x01\x00\x00\x00\x00\x02\x00\x30\x30\x30\x00\xff" \
333
        #"\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" \
334
        #"\xff\xff\xff\xff\xff"
335

    
336

    
337
        self.pipe.write(unlock)
338

    
339
        ack = self.pipe.read(1)
340

    
341
        if ack != chr(0x06):
342
            raise errors.RadioError("Expected ack but got {} ({})".format(ord(ack), type(ack)))
343

    
344
        c = struct.pack(">cHH", b"W", 0, 0x38C8)
345
        self.pipe.write(''.join(chr(b) for b in c))
346
        # magic unlock sequence
347
        unlock = [0xFF] * 8 + [0] * 160 + [0xFF] * 32
348
        unlock = "".join([chr(x) for x in unlock])
349
        self.pipe.write(unlock)
350

    
351
        time.sleep(0.01)
352
        ack = self.pipe.read(1)
353

    
354
        if ack != chr(0x06):
355
            raise errors.RadioError("Expected ack but got {} ({})".format(ord(ack), type(ack)))
356

    
357
    def download(self, raw=False, blocks=None):
358
        if blocks is None:
359
            blocks = list(range(int(self._memsize / 256)))
360
        else:
361
            blocks = [b for b in blocks if b < int(self._memsize / 256)]
362

    
363
        if self.command("0M PROGRAM", 2, timeout=1.5) != "0M":
364
            raise errors.RadioError("Radio didn't go into PROGRAM mode")
365

    
366
        allblocks = list(range(int(self._memsize / 256)))
367
        self.pipe.baudrate = 57600
368
        try:
369
            self.pipe.setRTS()
370
        except AttributeError:
371
            self.pipe.rts = True
372
        self.pipe.read(1)
373
        data = ""
374
        LOG.debug("reading blocks %d..%d" % (blocks[0], blocks[-1]))
375
        total = len(blocks)
376
        count = 0
377
        for i in allblocks:
378
            if i not in blocks:
379
                data += 256 * '\xff'
380
                continue
381
            data += self.read_block(i)
382
            count += 1
383
            if self.status_fn:
384
                s = chirp_common.Status()
385
                s.msg = "Cloning from radio"
386
                s.max = total
387
                s.cur = count
388
                self.status_fn(s)
389

    
390

    
391
        self.pipe.write("E")
392
        if raw:
393
            return data
394
        return memmap.MemoryMap(data)
395

    
396
    def upload(self, blocks=None):
397
        # MCP-D74 sets DTR, so we should too
398
        try:
399
            self.pipe.setDTR()
400
        except AttributeError:
401
            self.pipe.dtr = True
402

    
403
        if blocks is None:
404
            blocks = list(range((int(self._memsize / 256)) - 2))
405
        else:
406
            blocks = [b for b in blocks if b < int(self._memsize / 256)]
407

    
408
        if self.command("0M PROGRAM", 2, timeout=1.5) != "0M":
409
            raise errors.RadioError("Radio didn't go into PROGRAM mode")
410

    
411
        if self._unlock():
412
            raise errors.RadioError("Unlock failed")
413

    
414
        # This block definitely isn't written conventionally, so we let _unlock
415
        # handle it and skip.
416
        if 0 in blocks:
417
            blocks.remove(0)
418

    
419
        # For some reason MCP-D74 skips this block. If we don't, we'll get a NACK
420
        # on the next one. There is also a more than 500 ms delay for the ACK.
421
        if 1279 in blocks:
422
            blocks.remove(1279)
423

    
424
        #print("writing blocks %d..%d" % (blocks[0], blocks[-1]))
425
        total = len(blocks)
426
        count = 0
427
        for i in blocks:
428
            time.sleep(0.001)
429
            r = self.write_block(i, self._mmap, BLOCK_SIZES.get(i, 256))
430
            count += 1
431
            if not r:
432
                raise errors.RadioError("write of block %i failed" % i)
433
            if self.status_fn:
434
                s = chirp_common.Status()
435
                s.msg = "Cloning to radio"
436
                s.max = total
437
                s.cur = count
438
                self.status_fn(s)
439

    
440
        #lock = ("\x57\x00\x00\x00\x06\x02\x01\xff\x00\x00\xff")
441
        # unmodified variant 2
442
        radio_type = self._memobj.radio_type
443
        lock = ("\x57\x00\x00\x00\x06%c%c\xff\x00\x00\xff" %  
444
                (chr(radio_type.variant), chr(radio_type.region)))
445

    
446
        print("Locking radio %s", lock)
447

    
448
        self.pipe.write(lock)
449

    
450
        self.pipe.write("F")
451
        # clear out blocks we uploaded from the dirty blocks list
452
        self._dirty_blocks = [b for b in self._dirty_blocks if b not in blocks]
453

    
454
    def command(self, cmd, response_length, timeout=0.5):
455
        start = time.time()
456

    
457
        LOG.debug("PC->D72: %s" % cmd)
458
        default_timeout = self.pipe.timeout
459
        self.pipe.write(cmd + "\r")
460
        self.pipe.timeout = timeout
461
        try:
462
            data = self.pipe.read(response_length + 1)
463
            LOG.debug("D72->PC: %s" % data.strip())
464
        finally:
465
            self.pipe.timeout = default_timeout
466
        return data.strip()
467

    
468
    def get_raw_memory(self, number):
469
        bank = number // 6
470
        idx = number % 6
471

    
472
        _mem = self._memobj.memory[bank].mem[idx]
473
        return repr(_mem) + \
474
               repr(self._memobj.flag[number])
475

    
476
    def get_id(self):
477
        r = self.command("ID", 9)
478
        if r.startswith("ID "):
479
            return r.split(" ")[1]
480
        else:
481
            raise errors.RadioError("No response to ID command")
482

    
483
    def set_channel_name(self, number, name):
484
        name = name[:16] + '\x00' * 16
485
        if number < 999:
486
            self._memobj.channel_name[number].name = name[:16]
487
            self.add_dirty_block(self._memobj.channel_name[number])
488
        elif number >= 1020 and number < 1030:
489
            number -= 1020
490
            self._memobj.wx_name[number].name = name[:16]
491
            self.add_dirty_block(self._memobj.wx_name[number])
492

    
493
    def get_memory(self, number):
494
        if isinstance(number, str):
495
            try:
496
                number = thd72.THD72_SPECIAL[number]
497
            except KeyError:
498
                raise errors.InvalidMemoryLocation("Unknown channel %s" %
499
                                                   number)
500

    
501
        if number < 0 or number > (max(thd72.THD72_SPECIAL.values()) + 1):
502
            raise errors.InvalidMemoryLocation(
503
                "Number must be between 0 and 999")
504

    
505
        bank = number // 6
506
        idx = number % 6
507

    
508
        #print("reading memory #%d bank %d entry %d" %(number, bank, idx))
509
        _mem = self._memobj.memory[bank].mem[idx]
510
        flag = self._memobj.flag[number]
511

    
512
        #print("Memory mode %d" % _mem.mode)
513
        if _mem.mode < len( MODES ) and MODES[_mem.mode] == "DV":
514
            mem = chirp_common.DVMemory()
515
        else:
516
            mem = chirp_common.Memory()
517

    
518
        mem.number = number
519

    
520
        if number > 999:
521
            mem.extd_number = thd72.THD72_SPECIAL_REV[number]
522
        if flag.disabled == 0xFF:
523
            mem.empty = True
524
            return mem
525

    
526
        mem.name = self.get_channel_name(number)
527
        mem.freq = int(_mem.freq)
528
        mem.tmode = thd72.TMODES[int(_mem.tone_mode)]
529
        mem.rtone = chirp_common.TONES[_mem.rtone]
530
        mem.ctone = chirp_common.TONES[_mem.ctone]
531
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
532
        mem.duplex = thd72.DUPLEX[int(_mem.duplex)]
533
        mem.offset = _mem.offset
534
        mem.mode = MODES[int(_mem.mode)]
535
        if _mem.tuning_step < len( STEPS ):
536
            mem.tuning_step = STEPS[_mem.tuning_step]
537
        else :
538
            mem.tuning_step = 0xff
539

    
540
        if mem.mode == "DV":
541
            mem.dv_urcall = _mem.urcall
542
            mem.dv_rpt1call = _mem.rpt1
543
            mem.dv_rpt2call = _mem.rpt2
544
            mem.dv_code = _mem.digital_squelch_code
545

    
546
        if number < 999:
547
            # mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)]
548
            mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode]
549
        if number > 999:
550
            mem.cross_mode = chirp_common.CROSS_MODES[0]
551
            mem.immutable = ["number", "bank", "extd_number", "cross_mode"]
552
            if number >= 1020 and number < 1030:
553
                mem.immutable += ["freq", "offset", "tone", "mode",
554
                                  "tmode", "ctone", "skip"]  # FIXME: ALL
555
            else:
556
                mem.immutable += ["name"]
557

    
558
        return mem
559

    
560
    def set_memory(self, mem):
561
        LOG.debug("set_memory(%d)" % mem.number)
562
        if mem.number < 0 or mem.number > (max(thd72.THD72_SPECIAL.values()) + 1):
563
            raise errors.InvalidMemoryLocation(
564
                "Number must be between 0 and 999")
565

    
566
        # weather channels can only change name, nothing else
567
        if mem.number >= 1020 and mem.number < 1030:
568
            self.set_channel_name(mem.number, mem.name)
569
            return
570

    
571
        flag = self._memobj.flag[mem.number]
572
        self.add_dirty_block(self._memobj.flag[mem.number])
573

    
574
        # only delete non-WX channels
575
        was_empty = flag.disabled == 0xf
576
        if mem.empty:
577
            flag.disabled = 0xf
578
            return
579
        flag.disabled = 0
580

    
581
        bank = mem.number // 6
582
        idx = mem.number % 6
583

    
584
        #print("seting memory #%d bank %d entry %d" %(mem.number, bank, idx))
585
        _mem = self._memobj.memory[bank].mem[idx]
586
        self.add_dirty_block(_mem)
587
        if was_empty:
588
            self.initialize(_mem)
589

    
590
        _mem.freq = mem.freq
591

    
592
        if mem.number < 999:
593
            self.set_channel_name(mem.number, mem.name)
594

    
595
        _mem.tone_mode = thd72.TMODES_REV[mem.tmode]
596
        _mem.rtone = chirp_common.TONES.index(mem.rtone)
597
        _mem.ctone = chirp_common.TONES.index(mem.ctone)
598
        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
599
        _mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode)
600
        _mem.duplex = thd72.DUPLEX_REV[mem.duplex]
601
        _mem.offset = mem.offset
602
        _mem.mode = MODES_REV[mem.mode]
603

    
604
        prog_vfo = thd72.get_prog_vfo(mem.freq)
605
        #flag.prog_vfo = prog_vfo
606

    
607
        #if mem.number < 999:
608
        #    flag.skip = chirp_common.SKIP_VALUES.index(mem.skip)
609

    
610

    
611
    @staticmethod
612
    def _add_00_pad(val, length):
613
        return val.ljust(length, "\x00")[:length]
614

    
615

    
616
    @classmethod
617
    def apply_callsign(cls, setting, obj):
618
        callsign = setting.value.get_value().upper()
619
        setattr(obj, "callsign", cls._add_00_pad(callsign, 8))
620

    
621

    
622
    @classmethod
623
    def apply_power_on_msg(cls, setting, obj):
624
        msg = setting.value.get_value()
625
        setattr(obj, "power_on_msg", cls._add_00_pad(msg, 16))
626

    
627

    
628
    def _get_general_settings(self):
629
        menu = RadioSettingGroup("general", "General")
630
        
631
        cs = self._memobj.callsign
632

    
633
        val = RadioSettingValueString(
634
            0, 6, str(cs.callsign).rstrip("\x00"))
635
        rs = RadioSetting("cs.callsign", "Callsign", val)
636
        rs.set_apply_callback(self.apply_callsign, cs)
637
        menu.append(rs)
638

    
639
        msg = self._memobj.onmsg_name
640

    
641
        val = RadioSettingValueString(
642
            0, 16, str(msg.power_on_msg).rstrip("\x00"))
643
        rs = RadioSetting("msg.power_on_msg", "Power on message", val)
644
        rs.set_apply_callback(self.apply_power_on_msg, msg)
645
        menu.append(rs)
646

    
647
        units = self._memobj.unit_settings
648
        
649
        val = RadioSettingValueBoolean(units.dist_units)
650
        rs = RadioSetting("unit_settings.dist_units", "distance in KM", val)
651
        menu.append(rs)
652

    
653
        val = RadioSettingValueBoolean(units.dist_units)
654
        rs = RadioSetting("unit_settings.rain_units", "rain in mm", val)
655
        menu.append(rs)
656

    
657
        val = RadioSettingValueBoolean(units.dist_units)
658
        rs = RadioSetting("unit_settings.temp_units", "degrees in Centigrade", val)
659
        menu.append(rs)
660

    
661
        gpss = self._memobj.gps_settings
662
        
663
        val = RadioSettingValueBoolean(gpss.enable)
664
        rs = RadioSetting("gps_settings.enable", "enable GPS", val)
665
        menu.append(rs)
666

    
667
        return menu
668

    
669
    def _get_fm_radio_settings(self):
670
        menu = RadioSettingGroup("fm_radio", "FM radio")
671

    
672
#struct {
673
#    ul32 freq;
674
#    chat name[16];
675
#} fm_radio_memory[10];
676
        
677
        fms = self._memobj.fm_radio_settings
678

    
679
        val = RadioSettingValueBoolean(fms.enable)
680
        rs = RadioSetting("fm_radio_settings.enable", "Enable FM radio", val)
681
        menu.append(rs)
682

    
683
        val = RadioSettingValueInteger(3, 32, fms.seconds)
684
        rs = RadioSetting("fm_radio_settings.seconds", "FM radio timeout", val)
685
        menu.append(rs)
686

    
687
        return menu
688

    
689

    
690
    def _get_settings(self):
691
        top = RadioSettings(self._get_general_settings(),
692
                self._get_fm_radio_settings())
693
        return top
694

    
695

    
696
    def get_settings(self):
697
        try:
698
            return self._get_settings()
699
        except:
700
            import traceback
701
            LOG.error("Failed to parse settings: %s", traceback.format_exc())
702
            return None
703

    
(9-9/10)