Project

General

Profile

Bug #8947 » kguv9dplus_toa_abrlvl_fix.py

KG-UV9DPlus update to fix ABR_LVL and TOA settings - Mel Terechenok, 11/29/2022 02:03 AM

 
1
# Copyright 2018 Jim Lieb <lieb@sea-troll.net>
2
#
3
# Driver for Wouxon KG-UV9D Plus
4
#
5
# Borrowed from other chirp drivers, especially the KG-UV8D Plus
6
# by Krystian Struzik <toner_82@tlen.pl>
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20

    
21
"""Wouxun KG-UV9D Plus radio management module"""
22

    
23
import time
24
import os
25
import logging
26
import struct
27
import string
28
from chirp import util, chirp_common, bitwise, memmap, errors, directory
29
from chirp.settings import RadioSetting, RadioSettingValue, \
30
     RadioSettingGroup, \
31
     RadioSettingValueBoolean, RadioSettingValueList, \
32
     RadioSettingValueInteger, RadioSettingValueString, \
33
     RadioSettings, RadioSettingValueMap, InvalidValueError
34

    
35
LOG = logging.getLogger(__name__)
36

    
37
CMD_IDENT = 0x80
38
CMD_HANGUP = 0x81
39
CMD_RCONF = 0x82
40
CMD_WCONF = 0x83
41
CMD_RCHAN = 0x84
42
CMD_WCHAN = 0x85
43

    
44
cmd_name = {
45
    CMD_IDENT:  "ident",
46
    CMD_HANGUP: "hangup",
47
    CMD_RCONF:  "read config",
48
    CMD_WCONF:  "write config",
49
    CMD_RCHAN:  "read channel memory",  # Unused
50
    CMD_WCHAN:  "write channel memory"  # Unused because it is a hack.
51
    }
52

    
53
# This is used to write the configuration of the radio base on info
54
# gleaned from the downloaded app. There are empty spaces and we honor
55
# them because we don't know what they are (yet) although we read the
56
# whole of memory.
57
#
58
# Channel memory is separate. There are 1000 (1-999) channels.
59
# These are read/written to the radio in 4 channel (96 byte)
60
# records starting at address 0xa00 and ending at
61
# 0x4800 (presuming the end of channel 1000 is 0x4860-1
62

    
63
config_map = (          # map address, write size, write count
64
    (0x40,   16, 1),    # Passwords
65
    (0x740,  40, 1),    # FM chan 1-20
66
    (0x780,  16, 1),    # vfo-b-150
67
    (0x790,  16, 1),    # vfo-b-450
68
    (0x800,  16, 1),    # vfo-a-150
69
    (0x810,  16, 1),    # vfo-a-450
70
    (0x820,  16, 1),    # vfo-a-300
71
    (0x830,  16, 1),    # vfo-a-700
72
    (0x840,  16, 1),    # vfo-a-200
73
    (0x860,  16, 1),    # area-a-conf
74
    (0x870,  16, 1),    # area-b-conf
75
    (0x880,  16, 1),    # radio conf 0
76
    (0x890,  16, 1),    # radio conf 1
77
    (0x8a0,  16, 1),    # radio conf 2
78
    (0x8b0,  16, 1),    # radio conf 3
79
    (0x8c0,  16, 1),    # PTT-ANI
80
    (0x8d0,  16, 1),    # SCC
81
    (0x8e0,  16, 1),    # power save
82
    (0x8f0,  16, 1),    # Display banner
83
    (0x940,  64, 2),    # Scan groups and names
84
    (0xa00,  64, 249),  # Memory Channels 1-996
85
    (0x4840, 48, 1),    # Memory Channels 997-999
86
    (0x4900, 32, 249),  # Memory Names    1-996
87
    (0x6820, 24, 1),    # Memory Names    997-999
88
    (0x7400, 64, 5),    # CALL-ID 1-20, names 1-20
89
    )
90

    
91

    
92
MEM_VALID = 0xfc
93
MEM_INVALID = 0xff
94

    
95
# Radio memory map. This matches the reads/writes above.
96
# structure elements whose name starts with x are currently unidentified
97

    
98
_MEM_FORMAT02 = """
99
#seekto 0x40;
100

    
101
struct {
102
    char reset[6];
103
    char x46[2];
104
    char mode_sw[6];
105
    char x4e;
106
}  passwords;
107

    
108
#seekto 0x740;
109

    
110
struct {
111
    u16 fm_freq;
112
} fm_chans[20];
113

    
114
// each band has its own configuration, essentially its default params
115

    
116
struct vfo {
117
    u32 freq;
118
    u32 offset;
119
    u16 encqt;
120
    u16 decqt;
121
    u8  bit7_4:3,
122
        qt:3,
123
        bit1_0:2;
124
    u8  bit7:1,
125
        scan:1,
126
        bit5:1,
127
        pwr:2,
128
        mod:1,
129
        fm_dev:2;
130
    u8  pad2:6,
131
        shift:2;
132
    u8  zeros;
133
};
134

    
135
#seekto 0x780;
136

    
137
struct {
138
    struct vfo band_150;
139
    struct vfo band_450;
140
} vfo_b;
141

    
142
#seekto 0x800;
143

    
144
struct {
145
    struct vfo band_150;
146
    struct vfo band_450;
147
    struct vfo band_300;
148
    struct vfo band_700;
149
    struct vfo band_200;
150
} vfo_a;
151

    
152
// There are two independent radios, aka areas (as described
153
// in the manual as the upper and lower portions of the display...
154

    
155
struct area_conf {
156
    u8 w_mode;
157
    u8 x861;
158
    u8 w_chan;
159
    u8 scan_grp;
160
    u8 bcl;
161
    u8 sql;
162
    u8 cset;
163
    u8 step;
164
    u8 scan_mode;
165
    u8 x869;
166
    u8 scan_range;
167
    u8 x86b;
168
    u8 x86c;
169
    u8 x86d;
170
    u8 x86e;
171
    u8 x86f;
172
};
173

    
174
#seekto 0x860;
175

    
176
struct area_conf a_conf;
177

    
178
#seekto 0x870;
179

    
180
struct area_conf b_conf;
181

    
182
#seekto 0x880;
183

    
184
struct {
185
    u8 menu_avail;
186
    u8 reset_avail;
187
    u8 x882;
188
    u8 x883;
189
    u8 lang;
190
    u8 x885;
191
    u8 beep;
192
    u8 auto_am;
193
    u8 qt_sw;
194
    u8 lock;
195
    u8 x88a;
196
    u8 pf1;
197
    u8 pf2;
198
    u8 pf3;
199
    u8 s_mute;
200
    u8 type_set;
201
    u8 tot;
202
    u8 toa;
203
    u8 ptt_id;
204
    u8 x893;
205
    u8 id_dly;
206
    u8 x895;
207
    u8 voice_sw;
208
    u8 s_tone;
209
    u8 abr_lvl;
210
    u8 ring_time;
211
    u8 roger;
212
    u8 x89b;
213
    u8 abr;
214
    u8 save_m;
215
    u8 lock_m;
216
    u8 auto_lk;
217
    u8 rpt_ptt;
218
    u8 rpt_spk;
219
    u8 rpt_rct;
220
    u8 prich_sw;
221
    u16 pri_ch;
222
    u8 x8a6;
223
    u8 x8a7;
224
    u8 dtmf_st;
225
    u8 dtmf_tx;
226
    u8 x8aa;
227
    u8 sc_qt;
228
    u8 apo_tmr;
229
    u8 vox_grd;
230
    u8 vox_dly;
231
    u8 rpt_kpt;
232
    struct {
233
        u16 scan_st;
234
        u16 scan_end;
235
    } a;
236
    struct {
237
        u16 scan_st;
238
        u16 scan_end;
239
    } b;
240
    u8 x8b8;
241
    u8 x8b9;
242
    u8 x8ba;
243
    u8 ponmsg;
244
    u8 blcdsw;
245
    u8 bledsw;
246
    u8 x8be;
247
    u8 x8bf;
248
} settings;
249

    
250

    
251
#seekto 0x8c0;
252
struct {
253
    u8 code[6];
254
    char x8c6[10];
255
} my_callid;
256

    
257
#seekto 0x8d0;
258
struct {
259
    u8 scc[6];
260
    char x8d6[10];
261
} stun;
262

    
263
#seekto 0x8e0;
264
struct {
265
    u16 wake;
266
    u16 sleep;
267
} save[4];
268

    
269
#seekto 0x8f0;
270
struct {
271
    char banner[16];
272
} display;
273

    
274
#seekto 0x940;
275
struct {
276
    struct {
277
        i16 scan_st;
278
        i16 scan_end;
279
    } addrs[10];
280
    u8 x0968[8];
281
    struct {
282
        char name[8];
283
    } names[10];
284
} scn_grps;
285

    
286
// this array of structs is marshalled via the R/WCHAN commands
287
#seekto 0xa00;
288
struct {
289
    u32 rxfreq;
290
    u32 txfreq;
291
    u16 encQT;
292
    u16 decQT;
293
    u8  bit7_5:3,  // all ones
294
        qt:3,
295
        bit1_0:2;
296
    u8  bit7:1,
297
        scan:1,
298
        bit5:1,
299
        pwr:2,
300
        mod:1,
301
        fm_dev:2;
302
    u8  state;
303
    u8  c3;
304
} chan_blk[999];
305

    
306
// nobody really sees this. It is marshalled with chan_blk
307
// in 4 entry chunks
308
#seekto 0x4900;
309

    
310
// Tracks with the index of  chan_blk[]
311
struct {
312
    char name[8];
313
} chan_name[999];
314

    
315
#seekto 0x7400;
316
struct {
317
    u8 cid[6];
318
    u8 pad[2];
319
}call_ids[20];
320

    
321
// This array tracks with the index of call_ids[]
322
struct {
323
    char name[6];
324
    char pad[2];
325
} cid_names[20];
326
    """
327

    
328

    
329
# Support for the Wouxun KG-UV9D Plus radio
330
# Serial coms are at 19200 baud
331
# The data is passed in variable length records
332
# Record structure:
333
#  Offset   Usage
334
#    0      start of record (\x7d)
335
#    1      Command (6 commands, see above)
336
#    2      direction (\xff PC-> Radio, \x00 Radio -> PC)
337
#    3      length of payload (excluding header/checksum) (n)
338
#    4      payload (n bytes)
339
#    4+n+1  checksum - byte sum (% 256) of bytes 1 -> 4+n
340
#
341
# Memory Read Records:
342
# the payload is 3 bytes, first 2 are offset (big endian),
343
# 3rd is number of bytes to read
344
# Memory Write Records:
345
# the maximum payload size (from the Wouxun software)
346
# seems to be 66 bytes (2 bytes location + 64 bytes data).
347

    
348
def _pkt_encode(op, payload):
349
    """Assemble a packet for the radio and encode it for transmission.
350
    Yes indeed, the checksum we store is only 4 bits. Why?
351
    I suspect it's a bug in the radio firmware guys didn't want to fix,
352
    i.e. a typo 0xff -> 0xf..."""
353

    
354
    data = bytearray()
355
    data.append(0x7d)  # tag that marks the beginning of the packet
356
    data.append(op)
357
    data.append(0xff)  # 0xff is from app to radio
358
    # calc checksum from op to end
359
    cksum = op + 0xff
360
    if (payload):
361
        data.append(len(payload))
362
        cksum += len(payload)
363
        for byte in payload:
364
            cksum += byte
365
            data.append(byte)
366
    else:
367
        data.append(0x00)
368
        # Yea, this is a 4 bit cksum (also known as a bug)
369
    data.append(cksum & 0xf)
370

    
371
    # now obfuscate by an xor starting with first payload byte ^ 0x52
372
    # including the trailing cksum.
373
    xorbits = 0x52
374
    for i, byte in enumerate(data[4:]):
375
        xord = xorbits ^ byte
376
        data[i + 4] = xord
377
        xorbits = xord
378
    return(data)
379

    
380

    
381
def _pkt_decode(data):
382
    """Take a packet hot off the wire and decode it into clear text
383
    and return the fields. We say <<cleartext>> here because all it
384
    turns out to be is annoying obfuscation.
385
    This is the inverse of pkt_decode"""
386

    
387
    # we don't care about data[0].
388
    # It is always 0x7d and not included in checksum
389
    op = data[1]
390
    direction = data[2]
391
    bytecount = data[3]
392

    
393
    # First un-obfuscate the payload and cksum
394
    payload = bytearray()
395
    xorbits = 0x52
396
    for i, byte in enumerate(data[4:]):
397
        payload.append(xorbits ^ byte)
398
        xorbits = byte
399

    
400
    # Calculate the checksum starting with the 3 bytes of the header
401
    cksum = op + direction + bytecount
402
    for byte in payload[:-1]:
403
        cksum += byte
404
    # yes, a 4 bit cksum to match the encode
405
    cksum_match = (cksum & 0xf) == payload[-1]
406
    if (not cksum_match):
407
        LOG.debug(
408
            "Checksum missmatch: %x != %x; " % (cksum, payload[-1]))
409
    return (cksum_match, op, payload[:-1])
410

    
411
# UI callbacks to process input for mapping UI fields to memory cells
412

    
413

    
414
def freq2int(val, min, max):
415
    """Convert a frequency as a string to a u32. Units is Hz
416
    """
417
    _freq = chirp_common.parse_freq(str(val))
418
    if _freq > max or _freq < min:
419
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
420
                                (chirp_common.format_freq(_freq),
421
                                 chirp_common.format_freq(min),
422
                                 chirp_common.format_freq(max)))
423
    return _freq
424

    
425

    
426
def int2freq(freq):
427
    """
428
    Convert a u32 frequency to a string for UI data entry/display
429
    This is stored in the radio as units of 10Hz which we compensate to Hz.
430
    A value of -1 indicates <no freqency>, i.e. unused channel.
431
    """
432
    if (int(freq) > 0):
433
        f = chirp_common.format_freq(freq)
434
        return f
435
    else:
436
        return ""
437

    
438

    
439
def freq2short(val, min, max):
440
    """Convert a frequency as a string to a u16 which is units of 10KHz
441
    """
442
    _freq = chirp_common.parse_freq(str(val))
443
    if _freq > max or _freq < min:
444
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
445
                                (chirp_common.format_freq(_freq),
446
                                 chirp_common.format_freq(min),
447
                                 chirp_common.format_freq(max)))
448
    return _freq/100000 & 0xFFFF
449

    
450

    
451
def short2freq(freq):
452
    """
453
       Convert a short frequency to a string for UI data entry/display
454
       This is stored in the radio as units of 10KHz which we
455
       compensate to Hz.
456
       A value of -1 indicates <no frequency>, i.e. unused channel.
457
    """
458
    if (int(freq) > 0):
459
        f = chirp_common.format_freq(freq * 100000)
460
        return f
461
    else:
462
        return ""
463

    
464

    
465
def tone2short(t):
466
    """Convert a string tone or DCS to an encoded u16
467
    """
468
    tone = str(t)
469
    if tone == "----":
470
        u16tone = 0x0000
471
    elif tone[0] == 'D':  # This is a DCS code
472
        c = tone[1: -1]
473
        code = int(c, 8)
474
        if tone[-1] == 'I':
475
            code |= 0x4000
476
        u16tone = code | 0x8000
477
    else:              # This is an analog CTCSS
478
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff  # strip the '.'
479
    return u16tone
480

    
481

    
482
def short2tone(tone):
483
    """ Map a binary CTCSS/DCS to a string name for the tone
484
    """
485
    if tone == 0 or tone == 0xffff:
486
        ret = "----"
487
    else:
488
        code = tone & 0x3fff
489
        if tone & 0x8000:      # This is a DCS
490
            if tone & 0x4000:  # This is an inverse code
491
                ret = "D%0.3oI" % code
492
            else:
493
                ret = "D%0.3oN" % code
494
        else:   # Just plain old analog CTCSS
495
            ret = "%4.1f" % (code / 10.0)
496
    return ret
497

    
498

    
499
def callid2str(cid):
500
    """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
501
       One digit (binary) per byte, terminated with '0xc'
502
    """
503

    
504
    bin2ascii = " 1234567890"
505
    cidstr = ""
506
    for i in range(0, 6):
507
        b = cid[i].get_value()
508
        if b == 0xc:  # the cid EOL
509
            break
510
        if b == 0 or b > 0xa:
511
            raise InvalidValueError(
512
                "Caller ID code has illegal byte 0x%x" % b)
513
        cidstr += bin2ascii[b]
514
    return cidstr
515

    
516

    
517
def str2callid(val):
518
    """ Convert caller id strings from callid2str.
519
    """
520
    ascii2bin = "0123456789"
521
    s = str(val).strip()
522
    if len(s) < 3 or len(s) > 6:
523
        raise InvalidValueError(
524
            "Caller ID must be at least 3 and no more than 6 digits")
525
    if s[0] == '0':
526
        raise InvalidValueError(
527
            "First digit of a Caller ID cannot be a zero '0'")
528
    blk = bytearray()
529
    for c in s:
530
        if c not in ascii2bin:
531
            raise InvalidValueError(
532
                "Caller ID must be all digits 0x%x" % c)
533
        b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
534
        blk.append(b)
535
    if len(blk) < 6:
536
        blk.append(0xc)  # EOL a short ID
537
    if len(blk) < 6:
538
        for i in range(0, (6 - len(blk))):
539
            blk.append(0xf0)
540
    return blk
541

    
542

    
543
def digits2str(digits, padding=' ', width=6):
544
    """Convert a password or SCC digit string to a string
545
    Passwords are expanded to and must be 6 chars. Fill them with '0'
546
    """
547

    
548
    bin2ascii = "0123456789"
549
    digitsstr = ""
550
    for i in range(0, 6):
551
        b = digits[i].get_value()
552
        if b == 0xc:  # the digits EOL
553
            break
554
        if b >= 0xa:
555
            raise InvalidValueError(
556
                "Value has illegal byte 0x%x" % ord(b))
557
        digitsstr += bin2ascii[b]
558
    digitsstr = digitsstr.ljust(width, padding)
559
    return digitsstr
560

    
561

    
562
def str2digits(val):
563
    """ Callback for edited strings from digits2str.
564
    """
565
    ascii2bin = " 0123456789"
566
    s = str(val).strip()
567
    if len(s) < 3 or len(s) > 6:
568
        raise InvalidValueError(
569
            "Value must be at least 3 and no more than 6 digits")
570
    blk = bytearray()
571
    for c in s:
572
        if c not in ascii2bin:
573
            raise InvalidValueError("Value must be all digits 0x%x" % c)
574
        blk.append(int(c))
575
    for i in range(len(blk), 6):
576
        blk.append(0xc)  # EOL a short ID
577
    return blk
578

    
579

    
580
def name2str(name):
581
    """ Convert a callid or scan group name to a string
582
    Deal with fixed field padding (\0 or \0xff)
583
    """
584

    
585
    namestr = ""
586
    for i in range(0, len(name)):
587
        b = ord(name[i].get_value())
588
        if b != 0 and b != 0xff:
589
            namestr += chr(b)
590
    return namestr
591

    
592

    
593
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
594
    """ Convert a string to a name. A name is a 6 element bytearray
595
    with ascii chars.
596
    """
597
    val = str(val).rstrip(' \t\r\n\0\0xff')
598
    if len(val) == 0:
599
        name = "".ljust(size, emptyfill)
600
    else:
601
        name = val.ljust(size, fillchar)
602
    return name
603

    
604

    
605
def pw2str(pw):
606
    """Convert a password string (6 digits) to a string
607
    Passwords must be 6 digits. If it is shorter, pad right with '0'
608
    """
609
    pwstr = ""
610
    ascii2bin = "0123456789"
611
    for i in range(0, len(pw)):
612
        b = pw[i].get_value()
613
        if b not in ascii2bin:
614
            raise InvalidValueError("Value must be digits 0-9")
615
        pwstr += b
616
    pwstr = pwstr.ljust(6, '0')
617
    return pwstr
618

    
619

    
620
def str2pw(val):
621
    """Store a password from UI to memory obj
622
    If we clear the password (make it go away), change the
623
    empty string to '000000' since the radio must have *something*
624
    Also, fill a < 6 digit pw with 0's
625
    """
626
    ascii2bin = "0123456789"
627
    val = str(val).rstrip(' \t\r\n\0\0xff')
628
    if len(val) == 0:  # a null password
629
        val = "000000"
630
    for i in range(0, len(val)):
631
        b = val[i]
632
        if b not in ascii2bin:
633
            raise InvalidValueError("Value must be digits 0-9")
634
    if len(val) == 0:
635
        pw = "".ljust(6, '\0')
636
    else:
637
        pw = val.ljust(6, '0')
638
    return pw
639

    
640

    
641
# Helpers to replace python2 things like confused str/byte
642

    
643
def _hex_print(data, addrfmt=None):
644
    """Return a hexdump-like encoding of @data
645
    We expect data to be a bytearray, not a string.
646
    Expanded from borrowed code to use the first 2 bytes as the address
647
    per comm packet format.
648
    """
649
    if addrfmt is None:
650
        addrfmt = '%(addr)03i'
651
        addr = 0
652
    else:  # assume first 2 bytes are address
653
        a = struct.unpack(">H", data[0:2])
654
        addr = a[0]
655
        data = data[2:]
656

    
657
    block_size = 16
658

    
659
    lines = (len(data) / block_size)
660
    if (len(data) % block_size > 0):
661
        lines += 1
662

    
663
    out = ""
664
    left = len(data)
665
    for block in range(0, lines):
666
        addr += block * block_size
667
        try:
668
            out += addrfmt % locals()
669
        except (OverflowError, ValueError, TypeError, KeyError):
670
            out += "%03i" % addr
671
        out += ': '
672

    
673
        if left < block_size:
674
            limit = left
675
        else:
676
            limit = block_size
677

    
678
        for j in range(0, block_size):
679
            if (j < limit):
680
                out += "%02x " % data[(block * block_size) + j]
681
            else:
682
                out += "   "
683

    
684
        out += "  "
685

    
686
        for j in range(0, block_size):
687

    
688
            if (j < limit):
689
                _byte = data[(block * block_size) + j]
690
                if _byte >= 0x20 and _byte < 0x7F:
691
                    out += "%s" % chr(_byte)
692
                else:
693
                    out += "."
694
            else:
695
                out += " "
696
        out += "\n"
697
        if (left > block_size):
698
            left -= block_size
699

    
700
    return out
701

    
702

    
703
# Useful UI lists
704
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
705
S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]]
706
STEP_LIST = [str(x)+"kHz" for x in STEPS]
707
ROGER_LIST = ["Off", "Begin", "End", "Both"]
708
TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)]
709
TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 11)]
710
BANDWIDTH_LIST = ["Wide", "Narrow"]
711
LANGUAGE_LIST = ["English", "Chinese"]
712
PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"]
713
PF2KEY_LIST = ["OFF", "Scan", "Second", "lamp", "SDF-DIR", "K-lamp"]
714
PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX"]
715
WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.",
716
                 "Ch. No.+Name"]
717
BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + \
718
                 ["Always On"]
719
SAVE_MODES = ["Off", "1", "2", "3", "4"]
720
LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"]
721
APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)]
722
OFFSET_LIST = ["none", "+", "-"]
723
PONMSG_LIST = ["Battery Volts", "Bitmap"]
724
SPMUTE_LIST = ["QT", "QT*T", "QT&T"]
725
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"]
726
DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)]
727
PTTID_LIST = ["Off", "Begin", "End", "Both"]
728
ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)]
729
VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1, 11)]
730
VOX_DLYS = ["Off"] + ["%ds" % t for t in range(1, 5)]
731
RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)]
732
ABR_LVL_MAP = [("1",1), ("2",2), ("3",3), ("4",4), ("5",5)]
733
LIST_1_5 = ["%s" % x for x in range(1, 6)]
734
LIST_0_9 = ["%s" % x for x in range(0, 10)]
735
LIST_1_20 = ["%s" % x for x in range(1, 21)]
736
LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
737
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
738
SCANMODE_LIST = ["TO", "CO", "SE"]
739
SCANRANGE_LIST = ["Current band", "freq range", "ALL"]
740
SCQT_LIST = ["Decoder", "Encoder", "Both"]
741
S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"]
742
POWER_LIST = ["Low", "Med", "High"]
743
RPTMODE_LIST = ["Radio", "One direction Repeater",
744
                "Two direction repeater"]
745
TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \
746
            ["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \
747
            ["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES]
748

    
749

    
750
@directory.register
751
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
752
                      chirp_common.ExperimentalRadio):
753

    
754
    """Wouxun KG-UV9D Plus"""
755
    VENDOR = "Wouxun"
756
    MODEL = "KG-UV9D Plus"
757
    _model = "KG-UV9D"
758
    _rev = "00"  # default rev for the radio I know about...
759
    _file_ident = "kg-uv9d"
760
    BAUD_RATE = 19200
761
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
762
                    chirp_common.PowerLevel("M", watts=2),
763
                    chirp_common.PowerLevel("H", watts=5)]
764
    _mmap = ""
765

    
766
    def _read_record(self):
767
        """ Read and validate the header of a radio reply.
768
        A record is a formatted byte stream as follows:
769
            0x7D   All records start with this
770
            opcode This is in the set of legal commands.
771
                   The radio reply matches the request
772
            dir    This is the direction, 0xFF to the radio,
773
                   0x00 from the radio.
774
            cnt    Count of bytes in payload
775
                   (not including the trailing checksum byte)
776
            <cnt bytes>
777
            <checksum byte>
778
        """
779

    
780
        # first get the header and validate it
781
        data = bytearray(self.pipe.read(4))
782
        if (len(data) < 4):
783
            raise errors.RadioError('Radio did not respond')
784
        if (data[0] != 0x7D):
785
            raise errors.RadioError(
786
                'Radio reply garbled (%02x)' % data[0])
787
        if (data[1] not in cmd_name):
788
            raise errors.RadioError(
789
                "Unrecognized opcode (%02x)" % data[1])
790
        if (data[2] != 0x00):
791
            raise errors.RadioError(
792
                "Direction incorrect. Got (%02x)" % data[2])
793
        payload_len = data[3]
794
        # don't forget to read the checksum byte
795
        data.extend(self.pipe.read(payload_len + 1))
796
        if (len(data) != (payload_len + 5)):  # we got a short read
797
            raise errors.RadioError(
798
                "Radio reply wrong size. Wanted %d, got %d" %
799
                ((payload_len + 1), (len(data) - 4)))
800
        return _pkt_decode(data)
801

    
802
    def _write_record(self, cmd, payload=None):
803
        """ Write a request packet to the radio.
804
        """
805

    
806
        packet = _pkt_encode(cmd, payload)
807
        self.pipe.write(packet)
808

    
809
    @classmethod
810
    def match_model(cls, filedata, filename):
811
        """Look for bits in the file image and see if it looks
812
        like ours...
813
        TODO: there is a bunch of rubbish between 0x50 and 0x160
814
        that is still a known unknown
815
        """
816
        return cls._file_ident in filedata[0x51:0x59].lower()
817

    
818
    def _identify(self):
819
        """ Identify the radio
820
        The ident block identifies the radio and its capabilities.
821
        This block is always 78 bytes. The rev == '01' is the base
822
        radio and '02' seems to be the '-Plus' version.
823
        I don't really trust the content after the model and revision.
824
        One would assume this is pretty much constant data but I have
825
        seen differences between my radio and the dump named
826
        KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first
827
        five bands match the OEM windows
828
        app except the 350-400 band. The OOB trace has the 700MHz
829
        band different. This is speculation at this point.
830

    
831
        TODO: This could be smarter and reject a radio not actually
832
        a UV9D...
833
        """
834

    
835
        for _i in range(0, 10):  # retry 10 times if we get junk
836
            self._write_record(CMD_IDENT)
837
            chksum_match, op, _resp = self._read_record()
838
            if len(_resp) == 0:
839
                raise Exception("Radio not responding")
840
            if len(_resp) != 74:
841
                LOG.error(
842
                    "Expected and IDENT reply of 78 bytes. Got (%d)" %
843
                    len(_resp))
844
                continue
845
            if not chksum_match:
846
                LOG.error("Checksum error: retrying ident...")
847
                time.sleep(0.100)
848
                continue
849
            if op != CMD_IDENT:
850
                LOG.error("Expected IDENT reply. Got (%02x)" % op)
851
                continue
852
            LOG.debug("Got:\n%s" % _hex_print(_resp))
853
            (mod, rev) = struct.unpack(">7s2s", _resp[0:9])
854
            LOG.debug("Model %s, rev %s" % (mod, rev))
855
            if mod == self._model:
856
                self._rev = rev
857
                return
858
            else:
859
                raise Exception("Unable to identify radio")
860
        raise Exception("All retries to identify failed")
861

    
862
    def process_mmap(self):
863
        if self._rev == "02" or self._rev == "00":
864
            self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
865
        else:  # this is where you elif the other variants and non-Plus  radios
866
            raise errors.RadioError(
867
                "Unrecognized model variation (%s). No memory map for it" %
868
                self._rev)
869

    
870
    def sync_in(self):
871
        """ Public sync_in
872
            Download contents of the radio. Throw errors back
873
            to the core if the radio does not respond.
874
            """
875
        try:
876
            self._identify()
877
            self._mmap = self._do_download()
878
            self._write_record(CMD_HANGUP)
879
        except errors.RadioError:
880
            raise
881
        except Exception, e:
882
            LOG.exception('Unknown error during download process')
883
            raise errors.RadioError(
884
                "Failed to communicate with radio: %s" % e)
885
        self.process_mmap()
886

    
887
    def sync_out(self):
888
        """ Public sync_out
889
            Upload the modified memory image into the radio.
890
            """
891

    
892
        try:
893
            self._identify()
894
            self._do_upload()
895
            self._write_record(CMD_HANGUP)
896
        except errors.RadioError:
897
            raise
898
        except Exception, e:
899
            raise errors.RadioError(
900
                "Failed to communicate with radio: %s" % e)
901
        return
902

    
903
    def _do_download(self):
904
        """ Read the whole of radio memory in 64 byte chunks.
905
        We load the config space followed by loading memory channels.
906
        The radio seems to be a "clone" type and the memory channels
907
        are actually within the config space. There are separate
908
        commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
909
        these seem to be a hack that can only do 4 channels at a time.
910
        Since the radio only supports 999, (can only support 3 chars
911
        in the display UI?) although the vendors app reads 1000
912
        channels, it hacks back to config writes (CMD_WCONF) for the
913
        last 3 channels and names. We keep it simple and just read
914
        the whole thing even though the vendor app doesn't. Channels
915
        are separate in their app simply because the radio protocol
916
        has read/write commands to access it. What they do is simply
917
        marshal the frequency+mode bits in 4 channel chunks followed
918
        by a separate chunk of for names. In config space, they are two
919
        separate arrays 1..999. Given that this space is not a
920
        multiple of 4, there is hackery on upload to do the writes to
921
        config space. See upload for this.
922
        """
923

    
924
        mem = bytearray(0x8000)  # The radio's memory map is 32k
925
        for addr in range(0, 0x8000, 64):
926
            req = bytearray(struct.pack(">HB", addr, 64))
927
            self._write_record(CMD_RCONF, req)
928
            chksum_match, op, resp = self._read_record()
929
            if not chksum_match:
930
                LOG.debug(_hex_print(resp))
931
                raise Exception(
932
                    "Checksum error while reading configuration (0x%x)" %
933
                    addr)
934
            pa = struct.unpack(">H", resp[0:2])
935
            pkt_addr = pa[0]
936
            payload = resp[2:]
937
            if op != CMD_RCONF or addr != pkt_addr:
938
                raise Exception(
939
                    "Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
940
                    (addr, op, pkt_addr))
941
            LOG.debug("Config read (0x%x):\n%s" %
942
                      (addr, _hex_print(resp, '0x%(addr)04x')))
943
            for i in range(0, len(payload) - 1):
944
                mem[addr + i] = payload[i]
945
            if self.status_fn:
946
                status = chirp_common.Status()
947
                status.cur = addr
948
                status.max = 0x8000
949
                status.msg = "Cloning from radio"
950
                self.status_fn(status)
951
        strmem = "".join([chr(x) for x in mem])
952
        return memmap.MemoryMap(strmem)
953

    
954
    def _do_upload(self):
955
        """Walk through the config map and write updated records to
956
        the radio. The config map contains only the regions we know
957
        about. We don't use the channel memory commands to avoid the
958
        hackery of using config write commands to fill in the last
959
        3 channel memory and names slots. As we discover other useful
960
        goodies in the map, we can add more slots...
961
        """
962
        for ar, size, count in config_map:
963
            for addr in range(ar, ar + (size*count), size):
964
                req = bytearray(struct.pack(">H", addr))
965
                req.extend(self.get_mmap()[addr:addr + size])
966
                self._write_record(CMD_WCONF, req)
967
                LOG.debug("Config write (0x%x):\n%s" %
968
                          (addr, _hex_print(req)))
969
                chksum_match, op, ack = self._read_record()
970
                LOG.debug("Config write ack [%x]\n%s" %
971
                          (addr, _hex_print(ack)))
972
                a = struct.unpack(">H", ack)  # big endian short...
973
                ack = a[0]
974
                if not chksum_match or op != CMD_WCONF or addr != ack:
975
                    msg = ""
976
                    if not chksum_match:
977
                        msg += "Checksum err, "
978
                    if op != CMD_WCONF:
979
                        msg += "cmd mismatch %x != %x, " % \
980
                               (op, CMD_WCONF)
981
                    if addr != ack:
982
                        msg += "ack error %x != %x, " % (addr, ack)
983
                    raise Exception("Radio did not ack block: %s error" % msg)
984
                if self.status_fn:
985
                    status = chirp_common.Status()
986
                    status.cur = addr
987
                    status.max = 0x8000
988
                    status.msg = "Update radio"
989
                    self.status_fn(status)
990

    
991
    def get_features(self):
992
        """ Public get_features
993
            Return the features of this radio once we have identified
994
            it and gotten its bits
995
            """
996
        rf = chirp_common.RadioFeatures()
997
        rf.has_settings = True
998
        rf.has_ctone = True
999
        rf.has_rx_dtcs = True
1000
        rf.has_cross = True
1001
        rf.has_tuning_step = False
1002
        rf.has_bank = False
1003
        rf.can_odd_split = True
1004
        rf.valid_skips = ["", "S"]
1005
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
1006
        rf.valid_cross_modes = [
1007
            "Tone->Tone",
1008
            "Tone->DTCS",
1009
            "DTCS->Tone",
1010
            "DTCS->",
1011
            "->Tone",
1012
            "->DTCS",
1013
            "DTCS->DTCS",
1014
        ]
1015
        rf.valid_modes = ["FM", "NFM", "AM"]
1016
        rf.valid_power_levels = self.POWER_LEVELS
1017
        rf.valid_name_length = 8
1018
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
1019
        rf.valid_bands = [(108000000, 136000000),  # Aircraft  AM
1020
                          (136000000, 180000000),  # supports 2m
1021
                          (230000000, 250000000),
1022
                          (350000000, 400000000),
1023
                          (400000000, 520000000),  # supports 70cm
1024
                          (700000000, 985000000)]
1025
        rf.valid_characters = chirp_common.CHARSET_ASCII
1026
        rf.valid_tuning_steps = STEPS
1027
        rf.memory_bounds = (1, 999)  # 999 memories
1028
        return rf
1029

    
1030
    @classmethod
1031
    def get_prompts(cls):
1032
        rp = chirp_common.RadioPrompts()
1033
        rp.experimental = ("This radio driver is currently under development. "
1034
                           "There are no known issues with it, but you should "
1035
                           "proceed with caution.")
1036
        return rp
1037

    
1038
    def get_raw_memory(self, number):
1039
        return repr(self._memobj.chan_blk[number])
1040

    
1041
    def _get_tone(self, _mem, mem):
1042
        """Decode both the encode and decode CTSS/DCS codes from
1043
        the memory channel and stuff them into the UI
1044
        memory channel row.
1045
        """
1046
        txtone = short2tone(_mem.encQT)
1047
        rxtone = short2tone(_mem.decQT)
1048
        pt = "N"
1049
        pr = "N"
1050

    
1051
        if txtone == "----":
1052
            txmode = ""
1053
        elif txtone[0] == "D":
1054
            mem.dtcs = int(txtone[1:4])
1055
            if txtone[4] == "I":
1056
                pt = "R"
1057
            txmode = "DTCS"
1058
        else:
1059
            mem.rtone = float(txtone)
1060
            txmode = "Tone"
1061

    
1062
        if rxtone == "----":
1063
            rxmode = ""
1064
        elif rxtone[0] == "D":
1065
            mem.rx_dtcs = int(rxtone[1:4])
1066
            if rxtone[4] == "I":
1067
                pr = "R"
1068
            rxmode = "DTCS"
1069
        else:
1070
            mem.ctone = float(rxtone)
1071
            rxmode = "Tone"
1072

    
1073
        if txmode == "Tone" and len(rxmode) == 0:
1074
            mem.tmode = "Tone"
1075
        elif (txmode == rxmode and txmode == "Tone" and
1076
              mem.rtone == mem.ctone):
1077
            mem.tmode = "TSQL"
1078
        elif (txmode == rxmode and txmode == "DTCS" and
1079
              mem.dtcs == mem.rx_dtcs):
1080
            mem.tmode = "DTCS"
1081
        elif (len(rxmode) + len(txmode)) > 0:
1082
            mem.tmode = "Cross"
1083
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1084

    
1085
        mem.dtcs_polarity = pt + pr
1086

    
1087
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1088
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1089

    
1090
    def get_memory(self, number):
1091
        """ Public get_memory
1092
            Return the channel memory referenced by number to the UI.
1093
        """
1094
        _mem = self._memobj.chan_blk[number - 1]
1095
        _nam = self._memobj.chan_name[number - 1]
1096

    
1097
        mem = chirp_common.Memory()
1098
        mem.number = number
1099
        _valid = _mem.state
1100
        if _valid != MEM_VALID and _valid != 0 and _valid != 2:
1101
            # In Issue #6995 we can find _valid values of 0 and 2 in the IMG
1102
            # so these values should be treated like MEM_VALID.
1103
            mem.empty = True
1104
            return mem
1105
        else:
1106
            mem.empty = False
1107

    
1108
        mem.freq = int(_mem.rxfreq) * 10
1109

    
1110
        if _mem.txfreq == 0xFFFFFFFF:
1111
            # TX freq not set
1112
            mem.duplex = "off"
1113
            mem.offset = 0
1114
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1115
            mem.duplex = ""
1116
            mem.offset = 0
1117
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1118
            mem.duplex = "split"
1119
            mem.offset = int(_mem.txfreq) * 10
1120
        else:
1121
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1122
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1123

    
1124
        mem.name = name2str(_nam.name)
1125

    
1126
        self._get_tone(_mem, mem)
1127

    
1128
        mem.skip = "" if bool(_mem.scan) else "S"
1129

    
1130
        mem.power = self.POWER_LEVELS[_mem.pwr]
1131
        if _mem.mod == 1:
1132
            mem.mode = "AM"
1133
        elif _mem.fm_dev == 0:
1134
            mem.mode = "FM"
1135
        else:
1136
            mem.mode = "NFM"
1137
        #  qt has no home in the UI
1138
        return mem
1139

    
1140
    def _set_tone(self, mem, _mem):
1141
        """Update the memory channel block CTCC/DCS tones
1142
        from the UI fields
1143
        """
1144
        def _set_dcs(code, pol):
1145
            val = int("%i" % code, 8) | 0x8000
1146
            if pol == "R":
1147
                val |= 0x4000
1148
            return val
1149

    
1150
        rx_mode = tx_mode = None
1151
        rxtone = txtone = 0x0000
1152

    
1153
        if mem.tmode == "Tone":
1154
            tx_mode = "Tone"
1155
            txtone = int(mem.rtone * 10)
1156
        elif mem.tmode == "TSQL":
1157
            rx_mode = tx_mode = "Tone"
1158
            rxtone = txtone = int(mem.ctone * 10)
1159
        elif mem.tmode == "DTCS":
1160
            tx_mode = rx_mode = "DTCS"
1161
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1162
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1163
        elif mem.tmode == "Cross":
1164
            tx_mode, rx_mode = mem.cross_mode.split("->")
1165
            if tx_mode == "DTCS":
1166
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1167
            elif tx_mode == "Tone":
1168
                txtone = int(mem.rtone * 10)
1169
            if rx_mode == "DTCS":
1170
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1171
            elif rx_mode == "Tone":
1172
                rxtone = int(mem.ctone * 10)
1173

    
1174
        _mem.decQT = rxtone
1175
        _mem.encQT = txtone
1176

    
1177
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1178
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1179

    
1180
    def set_memory(self, mem):
1181
        """ Public set_memory
1182
            Inverse of get_memory. Update the radio memory image
1183
            from the mem object
1184
            """
1185
        number = mem.number
1186

    
1187
        _mem = self._memobj.chan_blk[number - 1]
1188
        _nam = self._memobj.chan_name[number - 1]
1189

    
1190
        if mem.empty:
1191
            _mem.set_raw("\xFF" * (_mem.size() / 8))
1192
            _nam.name = str2name("", 8, '\0', '\0')
1193
            _mem.state = MEM_INVALID
1194
            return
1195

    
1196
        _mem.rxfreq = int(mem.freq / 10)
1197
        if mem.duplex == "off":
1198
            _mem.txfreq = 0xFFFFFFFF
1199
        elif mem.duplex == "split":
1200
            _mem.txfreq = int(mem.offset / 10)
1201
        elif mem.duplex == "+":
1202
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1203
        elif mem.duplex == "-":
1204
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1205
        else:
1206
            _mem.txfreq = int(mem.freq / 10)
1207
        _mem.scan = int(mem.skip != "S")
1208
        if mem.mode == "FM":
1209
            _mem.mod = 0    # make sure forced AM is off
1210
            _mem.fm_dev = 0
1211
        elif mem.mode == "NFM":
1212
            _mem.mod = 0
1213
            _mem.fm_dev = 1
1214
        elif mem.mode == "AM":
1215
            _mem.mod = 1     # AM on
1216
            _mem.fm_dev = 1  # set NFM bandwidth
1217
        else:
1218
            _mem.mod = 0
1219
            _mem.fm_dev = 0  # Catchall default is FM
1220
        # set the tone
1221
        self._set_tone(mem, _mem)
1222
        # set the power
1223
        if mem.power:
1224
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1225
        else:
1226
            _mem.pwr = True
1227

    
1228
        # Set fields we can't access via the UI table to safe defaults
1229
        _mem.qt = 0   # mute mode to QT
1230

    
1231
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1232
        _mem.state = MEM_VALID
1233

    
1234
# Build the UI configuration tabs
1235
# the channel memory tab is built by the core.
1236
# We have no control over it
1237

    
1238
    def _core_tab(self):
1239
        """ Build Core Configuration tab
1240
        Radio settings common to all modes and areas go here.
1241
        """
1242
        s = self._memobj.settings
1243

    
1244
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1245

    
1246
        cf.append(RadioSetting("auto_am",
1247
                               "Auto detect AM(53)",
1248
                               RadioSettingValueBoolean(s.auto_am)))
1249
        cf.append(RadioSetting("qt_sw",
1250
                               "Scan tone detect(59)",
1251
                               RadioSettingValueBoolean(s.qt_sw)))
1252
        cf.append(
1253
            RadioSetting("s_mute",
1254
                         "SubFreq Mute(60)",
1255
                         RadioSettingValueList(S_MUTE_LIST,
1256
                                               S_MUTE_LIST[s.s_mute])))
1257
        cf.append(
1258
            RadioSetting("tot",
1259
                         "Transmit timeout Timer(10)",
1260
                         RadioSettingValueList(TIMEOUT_LIST,
1261
                                               TIMEOUT_LIST[s.tot])))
1262
        cf.append(
1263
            RadioSetting("toa",
1264
                         "Transmit Timeout Alarm(11)",
1265
                         RadioSettingValueList(TOA_LIST,
1266
                                               TOA_LIST[s.toa])))
1267
        cf.append(
1268
            RadioSetting("ptt_id",
1269
                         "PTT Caller ID mode(23)",
1270
                         RadioSettingValueList(PTTID_LIST,
1271
                                               PTTID_LIST[s.ptt_id])))
1272
        cf.append(
1273
            RadioSetting("id_dly",
1274
                         "Caller ID Delay time(25)",
1275
                         RadioSettingValueList(ID_DLY_LIST,
1276
                                               ID_DLY_LIST[s.id_dly])))
1277
        cf.append(RadioSetting("voice_sw",
1278
                               "Voice Guide(12)",
1279
                               RadioSettingValueBoolean(s.voice_sw)))
1280
        cf.append(RadioSetting("beep",
1281
                               "Keypad Beep(13)",
1282
                               RadioSettingValueBoolean(s.beep)))
1283
        cf.append(
1284
            RadioSetting("s_tone",
1285
                         "Side Tone(36)",
1286
                         RadioSettingValueList(S_TONES,
1287
                                               S_TONES[s.s_tone])))
1288
        cf.append(
1289
            RadioSetting("ring_time",
1290
                         "Ring Time(26)",
1291
                         RadioSettingValueList(
1292
                             LIST_OFF_10,
1293
                             LIST_OFF_10[s.ring_time])))
1294
        cf.append(
1295
            RadioSetting("roger",
1296
                         "Roger Beep(9)",
1297
                         RadioSettingValueList(ROGER_LIST,
1298
                                               ROGER_LIST[s.roger])))
1299
        cf.append(RadioSetting("blcdsw",
1300
                               "Backlight(41)",
1301
                               RadioSettingValueBoolean(s.blcdsw)))
1302
        cf.append(
1303
            RadioSetting("abr",
1304
                         "Auto Backlight Time(1)",
1305
                         RadioSettingValueList(BACKLIGHT_LIST,
1306
                                               BACKLIGHT_LIST[s.abr])))
1307
        cf.append(
1308
            RadioSetting("abr_lvl",
1309
                         "Backlight Brightness(27)",
1310
                         RadioSettingValueMap(ABR_LVL_MAP,
1311
                                               s.abr_lvl)))
1312
        cf.append(RadioSetting("lock",
1313
                               "Keypad Lock",
1314
                               RadioSettingValueBoolean(s.lock)))
1315
        cf.append(
1316
            RadioSetting("lock_m",
1317
                         "Keypad Lock Mode(35)",
1318
                         RadioSettingValueList(LOCK_MODES,
1319
                                               LOCK_MODES[s.lock_m])))
1320
        cf.append(RadioSetting("auto_lk",
1321
                               "Keypad Autolock(34)",
1322
                               RadioSettingValueBoolean(s.auto_lk)))
1323
        cf.append(RadioSetting("prich_sw",
1324
                               "Priority Channel Scan(33)",
1325
                               RadioSettingValueBoolean(s.prich_sw)))
1326
        cf.append(RadioSetting("pri_ch",
1327
                               "Priority Channel(32)",
1328
                               RadioSettingValueInteger(1, 999,
1329
                                                        s.pri_ch)))
1330
        cf.append(
1331
            RadioSetting("dtmf_st",
1332
                         "DTMF Sidetone(22)",
1333
                         RadioSettingValueList(DTMFST_LIST,
1334
                                               DTMFST_LIST[s.dtmf_st])))
1335
        cf.append(RadioSetting("sc_qt",
1336
                               "Scan QT Save Mode(38)",
1337
                               RadioSettingValueList(
1338
                                   SCQT_LIST,
1339
                                   SCQT_LIST[s.sc_qt])))
1340
        cf.append(
1341
            RadioSetting("apo_tmr",
1342
                         "Automatic Power-off(39)",
1343
                         RadioSettingValueList(APO_TIMES,
1344
                                               APO_TIMES[s.apo_tmr])))
1345
        cf.append(  # VOX "guard" is really VOX trigger audio level
1346
            RadioSetting("vox_grd",
1347
                         "VOX level(7)",
1348
                         RadioSettingValueList(VOX_GRDS,
1349
                                               VOX_GRDS[s.vox_grd])))
1350
        cf.append(
1351
            RadioSetting("vox_dly",
1352
                         "VOX Delay(37)",
1353
                         RadioSettingValueList(VOX_DLYS,
1354
                                               VOX_DLYS[s.vox_dly])))
1355
        cf.append(
1356
            RadioSetting("lang",
1357
                         "Menu Language(14)",
1358
                         RadioSettingValueList(LANGUAGE_LIST,
1359
                                               LANGUAGE_LIST[s.lang])))
1360
        cf.append(RadioSetting("ponmsg",
1361
                               "Poweron message(40)",
1362
                               RadioSettingValueList(
1363
                                   PONMSG_LIST, PONMSG_LIST[s.ponmsg])))
1364
        cf.append(RadioSetting("bledsw",
1365
                               "Receive LED(42)",
1366
                               RadioSettingValueBoolean(s.bledsw)))
1367
        return cf
1368

    
1369
    def _repeater_tab(self):
1370
        """Repeater mode functions
1371
        """
1372
        s = self._memobj.settings
1373
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1374

    
1375
        cf.append(
1376
            RadioSetting("type_set",
1377
                         "Radio Mode(43)",
1378
                         RadioSettingValueList(
1379
                             RPTMODE_LIST,
1380
                             RPTMODE_LIST[s.type_set])))
1381
        cf.append(RadioSetting("rpt_ptt",
1382
                               "Repeater PTT(45)",
1383
                               RadioSettingValueBoolean(s.rpt_ptt)))
1384
        cf.append(RadioSetting("rpt_spk",
1385
                               "Repeater Mode Speaker(44)",
1386
                               RadioSettingValueBoolean(s.rpt_spk)))
1387
        cf.append(
1388
            RadioSetting("rpt_kpt",
1389
                         "Repeater Hold Time(46)",
1390
                         RadioSettingValueList(RPT_KPTS,
1391
                                               RPT_KPTS[s.rpt_kpt])))
1392
        cf.append(RadioSetting("rpt_rct",
1393
                               "Repeater Receipt Tone(47)",
1394
                               RadioSettingValueBoolean(s.rpt_rct)))
1395
        return cf
1396

    
1397
    def _admin_tab(self):
1398
        """Admin functions not present in radio menu...
1399
        These are admin functions not radio operation configuration
1400
        """
1401

    
1402
        def apply_cid(setting, obj):
1403
            c = str2callid(setting.value)
1404
            obj.code = c
1405

    
1406
        def apply_scc(setting, obj):
1407
            c = str2digits(setting.value)
1408
            obj.scc = c
1409

    
1410
        def apply_mode_sw(setting, obj):
1411
            pw = str2pw(setting.value)
1412
            obj.mode_sw = pw
1413
            setting.value = pw2str(obj.mode_sw)
1414

    
1415
        def apply_reset(setting, obj):
1416
            pw = str2pw(setting.value)
1417
            obj.reset = pw
1418
            setting.value = pw2str(obj.reset)
1419

    
1420
        def apply_wake(setting, obj):
1421
            obj.wake = int(setting.value)/10
1422

    
1423
        def apply_sleep(setting, obj):
1424
            obj.sleep = int(setting.value)/10
1425

    
1426
        pw = self._memobj.passwords  # admin passwords
1427
        s = self._memobj.settings
1428

    
1429
        cf = RadioSettingGroup("admin", "Admin Functions")
1430

    
1431
        cf.append(RadioSetting("menu_avail",
1432
                               "Menu available in channel mode",
1433
                               RadioSettingValueBoolean(s.menu_avail)))
1434
        mode_sw = RadioSettingValueString(0, 6,
1435
                                          pw2str(pw.mode_sw), False)
1436
        rs = RadioSetting("passwords.mode_sw",
1437
                          "Mode Switch Password", mode_sw)
1438
        rs.set_apply_callback(apply_mode_sw, pw)
1439
        cf.append(rs)
1440

    
1441
        cf.append(RadioSetting("reset_avail",
1442
                               "Radio Reset Available",
1443
                               RadioSettingValueBoolean(s.reset_avail)))
1444
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1445
        rs = RadioSetting("passwords.reset",
1446
                          "Radio Reset Password", reset)
1447
        rs.set_apply_callback(apply_reset, pw)
1448
        cf.append(rs)
1449

    
1450
        cf.append(
1451
            RadioSetting("dtmf_tx",
1452
                         "DTMF Tx Duration",
1453
                         RadioSettingValueList(DTMF_TIMES,
1454
                                               DTMF_TIMES[s.dtmf_tx])))
1455
        cid = self._memobj.my_callid
1456
        my_callid = RadioSettingValueString(3, 6,
1457
                                            callid2str(cid.code), False)
1458
        rs = RadioSetting("my_callid.code",
1459
                          "PTT Caller ID code(24)", my_callid)
1460
        rs.set_apply_callback(apply_cid, cid)
1461
        cf.append(rs)
1462

    
1463
        stun = self._memobj.stun
1464
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1465
        rs = RadioSetting("stun.scc", "Security code", st)
1466
        rs.set_apply_callback(apply_scc, stun)
1467
        cf.append(rs)
1468

    
1469
        cf.append(
1470
            RadioSetting("settings.save_m",
1471
                         "Save Mode (2)",
1472
                         RadioSettingValueList(SAVE_MODES,
1473
                                               SAVE_MODES[s.save_m])))
1474
        for i in range(0, 4):
1475
            sm = self._memobj.save[i]
1476
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1477
            wf = RadioSetting("save[%i].wake" % i,
1478
                              "Save Mode %d Wake Time" % (i+1), wake)
1479
            wf.set_apply_callback(apply_wake, sm)
1480
            cf.append(wf)
1481

    
1482
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1483
            wf = RadioSetting("save[%i].sleep" % i,
1484
                              "Save Mode %d Sleep Time" % (i+1), slp)
1485
            wf.set_apply_callback(apply_sleep, sm)
1486
            cf.append(wf)
1487

    
1488
        _msg = str(self._memobj.display.banner).split("\0")[0]
1489
        val = RadioSettingValueString(0, 16, _msg)
1490
        val.set_mutable(True)
1491
        cf.append(RadioSetting("display.banner",
1492
                               "Display Message", val))
1493
        return cf
1494

    
1495
    def _fm_tab(self):
1496
        """FM Broadcast channels
1497
        """
1498
        def apply_fm(setting, obj):
1499
            f = freq2short(setting.value, 76000000, 108000000)
1500
            obj.fm_freq = f
1501

    
1502
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1503
        for ch in range(0, 20):
1504
            chan = self._memobj.fm_chans[ch]
1505
            freq = RadioSettingValueString(0, 20,
1506
                                           short2freq(chan.fm_freq))
1507
            rs = RadioSetting("fm_%d" % (ch + 1),
1508
                              "FM Channel %d" % (ch + 1), freq)
1509
            rs.set_apply_callback(apply_fm, chan)
1510
            fm.append(rs)
1511
        return fm
1512

    
1513
    def _scan_grp(self):
1514
        """Scan groups
1515
        """
1516
        def apply_name(setting, obj):
1517
            name = str2name(setting.value, 8, '\0', '\0')
1518
            obj.name = name
1519

    
1520
        def apply_start(setting, obj):
1521
            """Do a callback to deal with RadioSettingInteger limitation
1522
            on memory address resolution
1523
            """
1524
            obj.scan_st = int(setting.value)
1525

    
1526
        def apply_end(setting, obj):
1527
            """Do a callback to deal with RadioSettingInteger limitation
1528
            on memory address resolution
1529
            """
1530
            obj.scan_end = int(setting.value)
1531

    
1532
        sgrp = self._memobj.scn_grps
1533
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1534
        for i in range(0, 10):
1535
            s_grp = sgrp.addrs[i]
1536
            s_name = sgrp.names[i]
1537
            rs_name = RadioSettingValueString(0, 8,
1538
                                              name2str(s_name.name))
1539
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1540
                              "Group %i Name" % (i + 1), rs_name)
1541
            rs.set_apply_callback(apply_name, s_name)
1542
            scan.append(rs)
1543
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1544
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1545
                              "Starting Channel", rs_st)
1546
            rs.set_apply_callback(apply_start, s_grp)
1547
            scan.append(rs)
1548
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1549
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1550
                              "Last Channel", rs_end)
1551
            rs.set_apply_callback(apply_end, s_grp)
1552
            scan.append(rs)
1553
        return scan
1554

    
1555
    def _callid_grp(self):
1556
        """Caller IDs to be recognized by radio
1557
        This really should be a table in the UI
1558
        """
1559
        def apply_callid(setting, obj):
1560
            c = str2callid(setting.value)
1561
            obj.cid = c
1562

    
1563
        def apply_name(setting, obj):
1564
            name = str2name(setting.value, 6, '\0', '\xff')
1565
            obj.name = name
1566

    
1567
        cid = RadioSettingGroup("callids", "Caller IDs")
1568
        for i in range(0, 20):
1569
            callid = self._memobj.call_ids[i]
1570
            name = self._memobj.cid_names[i]
1571
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1572
            rs = RadioSetting("cid_names[%i].name" % i,
1573
                              "Caller ID %i Name" % (i + 1), c_name)
1574
            rs.set_apply_callback(apply_name, name)
1575
            cid.append(rs)
1576
            c_id = RadioSettingValueString(0, 6,
1577
                                           callid2str(callid.cid),
1578
                                           False)
1579
            rs = RadioSetting("call_ids[%i].cid" % i,
1580
                              "Caller ID Code", c_id)
1581
            rs.set_apply_callback(apply_callid, callid)
1582
            cid.append(rs)
1583
        return cid
1584

    
1585
    def _band_tab(self, area, band):
1586
        """ Build a band tab inside a VFO/Area
1587
        """
1588
        def apply_freq(setting, lo, hi, obj):
1589
            f = freq2int(setting.value, lo, hi)
1590
            obj.freq = f/10
1591

    
1592
        def apply_offset(setting, obj):
1593
            f = freq2int(setting.value, 0, 5000000)
1594
            obj.offset = f/10
1595

    
1596
        def apply_enc(setting, obj):
1597
            t = tone2short(setting.value)
1598
            obj.encqt = t
1599

    
1600
        def apply_dec(setting, obj):
1601
            t = tone2short(setting.value)
1602
            obj.decqt = t
1603

    
1604
        if area == "a":
1605
            if band == 150:
1606
                c = self._memobj.vfo_a.band_150
1607
                lo = 108000000
1608
                hi = 180000000
1609
            elif band == 200:
1610
                c = self._memobj.vfo_a.band_200
1611
                lo = 230000000
1612
                hi = 250000000
1613
            elif band == 300:
1614
                c = self._memobj.vfo_a.band_300
1615
                lo = 350000000
1616
                hi = 400000000
1617
            elif band == 450:
1618
                c = self._memobj.vfo_a.band_450
1619
                lo = 400000000
1620
                hi = 512000000
1621
            else:   # 700
1622
                c = self._memobj.vfo_a.band_700
1623
                lo = 700000000
1624
                hi = 985000000
1625
        else:  # area 'b'
1626
            if band == 150:
1627
                c = self._memobj.vfo_b.band_150
1628
                lo = 136000000
1629
                hi = 180000000
1630
            else:  # 450
1631
                c = self._memobj.vfo_b.band_450
1632
                lo = 400000000
1633
                hi = 512000000
1634

    
1635
        prefix = "vfo_%s.band_%d" % (area, band)
1636
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
1637
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
1638
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
1639
        rs.set_apply_callback(apply_freq, lo, hi, c)
1640
        bf.append(rs)
1641

    
1642
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
1643
        rs = RadioSetting(prefix + ".offset", "Tx Offset(28)", off)
1644
        rs.set_apply_callback(apply_offset, c)
1645
        bf.append(rs)
1646

    
1647
        rs = RadioSetting(prefix + ".encqt",
1648
                          "Encode QT(17,19)",
1649
                          RadioSettingValueList(TONE_LIST,
1650
                                                short2tone(c.encqt)))
1651
        rs.set_apply_callback(apply_enc, c)
1652
        bf.append(rs)
1653

    
1654
        rs = RadioSetting(prefix + ".decqt",
1655
                          "Decode QT(16,18)",
1656
                          RadioSettingValueList(TONE_LIST,
1657
                                                short2tone(c.decqt)))
1658
        rs.set_apply_callback(apply_dec, c)
1659
        bf.append(rs)
1660

    
1661
        bf.append(RadioSetting(prefix + ".qt",
1662
                               "Mute Mode(21)",
1663
                               RadioSettingValueList(SPMUTE_LIST,
1664
                                                     SPMUTE_LIST[c.qt])))
1665
        bf.append(RadioSetting(prefix + ".scan",
1666
                               "Scan this(48)",
1667
                               RadioSettingValueBoolean(c.scan)))
1668
        bf.append(RadioSetting(prefix + ".pwr",
1669
                               "Power(5)",
1670
                               RadioSettingValueList(
1671
                                   POWER_LIST, POWER_LIST[c.pwr])))
1672
        bf.append(RadioSetting(prefix + ".mod",
1673
                               "AM Modulation(54)",
1674
                               RadioSettingValueBoolean(c.mod)))
1675
        bf.append(RadioSetting(prefix + ".fm_dev",
1676
                               "FM Deviation(4)",
1677
                               RadioSettingValueList(
1678
                                   BANDWIDTH_LIST,
1679
                                   BANDWIDTH_LIST[c.fm_dev])))
1680
        bf.append(
1681
            RadioSetting(prefix + ".shift",
1682
                         "Frequency Shift(6)",
1683
                         RadioSettingValueList(OFFSET_LIST,
1684
                                               OFFSET_LIST[c.shift])))
1685
        return bf
1686

    
1687
    def _area_tab(self, area):
1688
        """Build a VFO tab
1689
        """
1690
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
1691
            f = freq2short(setting.value, scan_lo, scan_hi)
1692
            obj.scan_st = f
1693

    
1694
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
1695
            f = freq2short(setting.value, scan_lo, scan_hi)
1696
            obj.scan_end = f
1697

    
1698
        if area == "a":
1699
            desc = "Area A Settings"
1700
            c = self._memobj.a_conf
1701
            scan_lo = 108000000
1702
            scan_hi = 985000000
1703
            scan_rng = self._memobj.settings.a
1704
            band_list = (150, 200, 300, 450, 700)
1705
        else:
1706
            desc = "Area B Settings"
1707
            c = self._memobj.b_conf
1708
            scan_lo = 136000000
1709
            scan_hi = 512000000
1710
            scan_rng = self._memobj.settings.b
1711
            band_list = (150, 450)
1712

    
1713
        prefix = "%s_conf" % area
1714
        af = RadioSettingGroup(prefix, desc)
1715
        af.append(
1716
            RadioSetting(prefix + ".w_mode",
1717
                         "Workmode",
1718
                         RadioSettingValueList(
1719
                             WORKMODE_LIST,
1720
                             WORKMODE_LIST[c.w_mode])))
1721
        af.append(RadioSetting(prefix + ".w_chan",
1722
                               "Channel",
1723
                               RadioSettingValueInteger(1, 999,
1724
                                                        c.w_chan)))
1725
        af.append(
1726
            RadioSetting(prefix + ".scan_grp",
1727
                         "Scan Group(49)",
1728
                         RadioSettingValueList(
1729
                             SCANGRP_LIST,
1730
                             SCANGRP_LIST[c.scan_grp])))
1731
        af.append(RadioSetting(prefix + ".bcl",
1732
                               "Busy Channel Lock-out(15)",
1733
                               RadioSettingValueBoolean(c.bcl)))
1734
        af.append(
1735
            RadioSetting(prefix + ".sql",
1736
                         "Squelch Level(8)",
1737
                         RadioSettingValueList(LIST_0_9,
1738
                                               LIST_0_9[c.sql])))
1739
        af.append(
1740
            RadioSetting(prefix + ".cset",
1741
                         "Call ID Group(52)",
1742
                         RadioSettingValueList(LIST_1_20,
1743
                                               LIST_1_20[c.cset])))
1744
        af.append(
1745
            RadioSetting(prefix + ".step",
1746
                         "Frequency Step(3)",
1747
                         RadioSettingValueList(
1748
                             STEP_LIST, STEP_LIST[c.step])))
1749
        af.append(
1750
            RadioSetting(prefix + ".scan_mode",
1751
                         "Scan Mode(20)",
1752
                         RadioSettingValueList(
1753
                             SCANMODE_LIST,
1754
                             SCANMODE_LIST[c.scan_mode])))
1755
        af.append(
1756
            RadioSetting(prefix + ".scan_range",
1757
                         "Scan Range(50)",
1758
                         RadioSettingValueList(
1759
                             SCANRANGE_LIST,
1760
                             SCANRANGE_LIST[c.scan_range])))
1761
        st = RadioSettingValueString(0, 15,
1762
                                     short2freq(scan_rng.scan_st))
1763
        rs = RadioSetting("settings.%s.scan_st" % area,
1764
                          "Frequency Scan Start", st)
1765
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
1766
        af.append(rs)
1767

    
1768
        end = RadioSettingValueString(0, 15,
1769
                                      short2freq(scan_rng.scan_end))
1770
        rs = RadioSetting("settings.%s.scan_end" % area,
1771
                          "Frequency Scan End", end)
1772
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
1773
                              scan_rng)
1774
        af.append(rs)
1775
        # Each area has its own set of bands
1776
        for band in (band_list):
1777
            af.append(self._band_tab(area, band))
1778
        return af
1779

    
1780
    def _key_tab(self):
1781
        """Build radio key/button menu
1782
        """
1783
        s = self._memobj.settings
1784
        kf = RadioSettingGroup("key_grp", "Key Settings")
1785

    
1786
        kf.append(RadioSetting("settings.pf1",
1787
                               "PF1 Key function(55)",
1788
                               RadioSettingValueList(
1789
                                   PF1KEY_LIST,
1790
                                   PF1KEY_LIST[s.pf1])))
1791
        kf.append(RadioSetting("settings.pf2",
1792
                               "PF2 Key function(56)",
1793
                               RadioSettingValueList(
1794
                                   PF2KEY_LIST,
1795
                                   PF2KEY_LIST[s.pf2])))
1796
        kf.append(RadioSetting("settings.pf3",
1797
                               "PF3 Key function(57)",
1798
                               RadioSettingValueList(
1799
                                   PF3KEY_LIST,
1800
                                   PF3KEY_LIST[s.pf3])))
1801
        return kf
1802

    
1803
    def _get_settings(self):
1804
        """Build the radio configuration settings menus
1805
        """
1806

    
1807
        core_grp = self._core_tab()
1808
        fm_grp = self._fm_tab()
1809
        area_a_grp = self._area_tab("a")
1810
        area_b_grp = self._area_tab("b")
1811
        key_grp = self._key_tab()
1812
        scan_grp = self._scan_grp()
1813
        callid_grp = self._callid_grp()
1814
        admin_grp = self._admin_tab()
1815
        rpt_grp = self._repeater_tab()
1816

    
1817
        core_grp.append(key_grp)
1818
        core_grp.append(admin_grp)
1819
        core_grp.append(rpt_grp)
1820
        group = RadioSettings(core_grp,
1821
                              area_a_grp,
1822
                              area_b_grp,
1823
                              fm_grp,
1824
                              scan_grp,
1825
                              callid_grp
1826
                              )
1827
        return group
1828

    
1829
    def get_settings(self):
1830
        """ Public build out linkage between radio settings and UI
1831
        """
1832
        try:
1833
            return self._get_settings()
1834
        except Exception:
1835
            import traceback
1836
            LOG.error("Failed to parse settings: %s",
1837
                      traceback.format_exc())
1838
            return None
1839

    
1840
    def _is_freq(self, element):
1841
        """This is a hack to smoke out whether we need to do
1842
        frequency translations for otherwise innocent u16s and u32s
1843
        """
1844
        return "rxfreq" in element.get_name() or \
1845
               "txfreq" in element.get_name() or \
1846
               "scan_st" in element.get_name() or \
1847
               "scan_end" in element.get_name() or \
1848
               "offset" in element.get_name() or \
1849
               "fm_stop" in element.get_name()
1850

    
1851
    def set_settings(self, settings):
1852
        """ Public update radio settings via UI callback
1853
        A lot of this should be in common code....
1854
        """
1855

    
1856
        for element in settings:
1857
            if not isinstance(element, RadioSetting):
1858
                LOG.debug("set_settings: not instance %s" %
1859
                          element.get_name())
1860
                self.set_settings(element)
1861
                continue
1862
            else:
1863
                try:
1864
                    if "." in element.get_name():
1865
                        bits = element.get_name().split(".")
1866
                        obj = self._memobj
1867
                        for bit in bits[:-1]:
1868
                            # decode an array index
1869
                            if "[" in bit and "]" in bit:
1870
                                bit, index = bit.split("[", 1)
1871
                                index, junk = index.split("]", 1)
1872
                                index = int(index)
1873
                                obj = getattr(obj, bit)[index]
1874
                            else:
1875
                                obj = getattr(obj, bit)
1876
                        setting = bits[-1]
1877
                    else:
1878
                        obj = self._memobj.settings
1879
                        setting = element.get_name()
1880

    
1881
                    if element.has_apply_callback():
1882
                        LOG.debug("Using apply callback")
1883
                        element.run_apply_callback()
1884
                    else:
1885
                        LOG.debug("Setting %s = %s" %
1886
                                  (setting, element.value))
1887
                        if self._is_freq(element):
1888
                            setattr(obj, setting, int(element.value)/10)
1889
                        else:
1890
                            setattr(obj, setting, element.value)
1891
                except Exception, e:
1892
                    LOG.debug("set_settings: Exception with %s" %
1893
                              element.get_name())
1894
                    raise
(3-3/3)