Project

General

Profile

New Model #10067 » kguv9px b1.0.py

Wouxun KG-UV9PX Chirp Driver Beta 1.0 - Mel Terechenok, 12/15/2022 05:20 PM

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

    
22
"""Wouxun KG-UV9PX radio management module"""
23

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

    
36
LOG = logging.getLogger(__name__)
37

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

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

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

    
64
config_map = (          # map address, write size, write count
65
    (0x40,   16, 1),    # Passwords
66
    (0x50,   10, 1),    # OEM Display Name
67
    (0x60,   20, 1),    # Rx Freq Limits Area A
68
    (0x74,   8,  1),    # TX Frequency Limits 150M and 450M
69
    (0x7c,   4,  1),    # Rx 150M Freq Limits Area B
70
#   (0x80,   8,  1),    # unknown Freq limits
71
    (0x740,  40, 1),    # FM chan 1-20
72
    (0x780,  16, 1),    # vfo-b-150
73
    (0x790,  16, 1),    # vfo-b-450
74
    (0x800,  16, 1),    # vfo-a-150
75
    (0x810,  16, 1),    # vfo-a-450
76
    (0x820,  16, 1),    # vfo-a-300
77
    (0x830,  16, 1),    # vfo-a-700
78
    (0x840,  16, 1),    # vfo-a-200
79
    (0x860,  16, 1),    # area-a-conf
80
    (0x870,  16, 1),    # area-b-conf
81
    (0x880,  16, 1),    # radio conf 0
82
    (0x890,  16, 1),    # radio conf 1
83
    (0x8a0,  16, 1),    # radio conf 2
84
    (0x8b0,  16, 1),    # radio conf 3
85
    (0x8c0,  16, 1),    # PTT-ANI
86
    (0x8d0,  16, 1),    # SCC
87
    (0x8e0,  16, 1),    # power save
88
    (0x8f0,  16, 1),    # Display banner
89
    (0x940,  64, 2),    # Scan groups and names
90
    (0xa00,  64, 249),  # Memory Channels 1-996
91
    (0x4840, 48, 1),    # Memory Channels 997-999
92
    (0x4900, 32, 249),  # Memory Names    1-996
93
    (0x6820, 24, 1),    # Memory Names    997-999
94
    (0x7400, 64, 5),    # CALL-ID 1-20, names 1-20
95
    (0x7600,  1, 1)     # Screen Mode
96
    )
97

    
98

    
99
MEM_VALID = 0xfc
100
MEM_INVALID = 0xff
101
# MEM_INVALID = 0xfd
102

    
103
# Radio memory map. This matches the reads/writes above.
104
# structure elements whose name starts with x are currently unidentified
105

    
106
_MEM_FORMAT02 = """
107
#seekto 0x40;
108

    
109
struct {
110
    char reset[6];
111
    char x46[2];
112
    char mode_sw[6];
113
    char x4e;
114
}  passwords;
115

    
116
#seekto 0x50;
117
struct {
118
    char model[10];
119
} oemmodel;
120

    
121
#seekto 0x60;
122
struct {
123
    u16 lim_150M_area_a_rxlower_limit; // 0x60
124
    u16 lim_150M_area_a_rxupper_limit;
125
    u16 lim_450M_rxlower_limit;
126
    u16 lim_450M_rxupper_limit;
127
    u16 lim_300M_rxlower_limit;
128
    u16 lim_300M_rxupper_limit;
129
    u16 lim_800M_rxlower_limit;
130
    u16 lim_800M_rxupper_limit;
131
    u16 lim_210M_rxlower_limit;
132
    u16 lim_210M_rxupper_limit;
133
    u16 lim_150M_Txlower_limit;
134
    u16 lim_150M_Txupper_limit;
135
    u16 lim_450M_Txlower_limit;
136
    u16 lim_450M_Txupper_limit;
137
    u16 lim_150M_area_b_rxlower_limit;
138
    u16 lim_150M_area_b_rxupper_limit;
139
    u16 unknown_lower_limit;
140
    u16 unknown_upper_limit;
141
    u16 unknown2_lower_limit;
142
    u16 unknown2_upper_limit;
143
}  limits;
144

    
145
#seekto 0x740;
146

    
147
struct {
148
    u16 fm_freq;
149
} fm_chans[20];
150

    
151
// each band has its own configuration, essentially its default params
152

    
153
struct vfo {
154
    u32 freq;
155
    u32 offset;
156
    u16 encqt;
157
    u16 decqt;
158
    u8  bit7_4:3,
159
        qt:3,
160
        bit1_0:2;
161
    u8  bit7:1,
162
        scan:1,
163
        bit5:1,
164
        pwr:2,
165
        mod:1,
166
        fm_dev:2;
167
    u8  pad2:6,
168
        shift:2;
169
    u8  zeros;
170
};
171

    
172
#seekto 0x780;
173

    
174
struct {
175
    struct vfo band_150;
176
    struct vfo band_450;
177
} vfo_b;
178

    
179
#seekto 0x800;
180

    
181
struct {
182
    struct vfo band_150;
183
    struct vfo band_450;
184
    struct vfo band_300;
185
    struct vfo band_700;
186
    struct vfo band_200;
187
} vfo_a;
188

    
189
// There are two independent radios, aka areas (as described
190
// in the manual as the upper and lower portions of the display...
191

    
192
struct area_conf {
193
    u8 w_mode;
194
    u8 x861;
195
    u8 w_chan;
196
    u8 scan_grp;
197
    u8 bcl;
198
    u8 sql;
199
    u8 cset;
200
    u8 step;
201
    u8 scan_mode;
202
    u8 x869;
203
    u8 scan_range;
204
    u8 x86b;
205
    u8 x86c;
206
    u8 x86d;
207
    u8 x86e;
208
    u8 x86f;
209
};
210

    
211
#seekto 0x860;
212

    
213
struct area_conf a_conf;
214

    
215
#seekto 0x870;
216

    
217
struct area_conf b_conf;
218

    
219
#seekto 0x880;
220

    
221
struct {
222
    u8 menu_avail;
223
    u8 reset_avail;
224
    u8 act_area;
225
    u8 tdr;
226
    u8 lang;
227
    u8 x885;
228
    u8 beep;
229
    u8 auto_am;
230
    u8 qt_sw;
231
    u8 lock;
232
    u8 x88a;
233
    u8 pf1;
234
    u8 pf2;
235
    u8 pf3;
236
    u8 s_mute;
237
    u8 type_set;
238
    u8 tot;
239
    u8 toa;
240
    u8 ptt_id;
241
    u8 x893;
242
    u8 id_dly;
243
    u8 x895;
244
    u8 voice_sw;
245
    u8 s_tone;
246
    u8 abr_lvl;
247
    u8 ring_time;
248
    u8 roger;
249
    u8 x89b;
250
    u8 abr;
251
    u8 save_m;
252
    u8 lock_m;
253
    u8 auto_lk;
254
    u8 rpt_ptt;
255
    u8 rpt_spk;
256
    u8 rpt_rct;
257
    u8 prich_sw;
258
    u16 pri_ch;
259
    u8 x8a6;
260
    u8 x8a7;
261
    u8 dtmf_st;
262
    u8 dtmf_tx;
263
    u8 x8aa;
264
    u8 sc_qt;
265
    u8 apo_tmr;
266
    u8 vox_grd;
267
    u8 vox_dly;
268
    u8 rpt_kpt;
269
    struct {
270
        u16 scan_st;
271
        u16 scan_end;
272
    } a;
273
    struct {
274
        u16 scan_st;
275
        u16 scan_end;
276
    } b;
277
    u8 x8b8;
278
    u8 x8b9;
279
    u8 x8ba;
280
    u8 ponmsg;
281
    u8 blcdsw;
282
    u8 bledsw;
283
    u8 x8be;
284
    u8 x8bf;
285

    
286
} settings;
287

    
288

    
289
#seekto 0x8c0;
290
struct {
291
    u8 code[6];
292
    char x8c6[10];
293
} my_callid;
294

    
295
#seekto 0x8d0;
296
struct {
297
    u8 scc[6];
298
    char x8d6[10];
299
} stun;
300

    
301
#seekto 0x8e0;
302
struct {
303
    u16 wake;
304
    u16 sleep;
305
} save[4];
306

    
307
#seekto 0x8f0;
308
struct {
309
    char banner[16];
310
} display;
311

    
312
#seekto 0x940;
313
struct {
314
    struct {
315
        i16 scan_st;
316
        i16 scan_end;
317
    } addrs[10];
318
    u8 x0968[8];
319
    struct {
320
        char name[8];
321
    } names[10];
322
} scn_grps;
323

    
324
// this array of structs is marshalled via the R/WCHAN commands
325
#seekto 0xa00;
326
struct {
327
    u32 rxfreq;
328
    u32 txfreq;
329
    u16 encQT;
330
    u16 decQT;
331
    u8  bit7_5:3,  // all ones
332
        qt:3,
333
        bit1_0:2;
334
    u8  bit7:1,
335
        scan:1,
336
        bit5:1,
337
        pwr:2,
338
        mod:1,
339
        fm_dev:2;
340
    u8  state;
341
    u8  c3;
342
} chan_blk[999];
343

    
344
// nobody really sees this. It is marshalled with chan_blk
345
// in 4 entry chunks
346
#seekto 0x4900;
347

    
348
// Tracks with the index of  chan_blk[]
349
struct {
350
    char name[8];
351
} chan_name[999];
352

    
353
#seekto 0x7400;
354
struct {
355
    u8 cid[6];
356
    u8 pad[2];
357
}call_ids[20];
358

    
359
// This array tracks with the index of call_ids[]
360
struct {
361
    char name[6];
362
    char pad[2];
363
} cid_names[20];
364

    
365
#seekto 0x7600;
366
struct {
367
    u8 screen_mode;
368
} screen;
369
"""
370

    
371

    
372
# Support for the Wouxun KG-UV9PX radio
373
# Serial coms are at 19200 baud
374
# The data is passed in variable length records
375
# Record structure:
376
#  Offset   Usage
377
#    0      start of record (\x7d)
378
#    1      Command (6 commands, see above)
379
#    2      direction (\xff PC-> Radio, \x00 Radio -> PC)
380
#    3      length of payload (excluding header/checksum) (n)
381
#    4      payload (n bytes)
382
#    4+n+1  checksum - byte sum (% 256) of bytes 1 -> 4+n
383
#
384
# Memory Read Records:
385
# the payload is 3 bytes, first 2 are offset (big endian),
386
# 3rd is number of bytes to read
387
# Memory Write Records:
388
# the maximum payload size (from the Wouxun software)
389
# seems to be 66 bytes (2 bytes location + 64 bytes data).
390

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

    
397
    data = bytearray()
398
    data.append(0x7d)  # tag that marks the beginning of the packet
399
    data.append(op)
400
    data.append(0xff)  # 0xff is from app to radio
401
    # calc checksum from op to end
402
    cksum = op + 0xff
403
    if (payload):
404
        data.append(len(payload))
405
        cksum += len(payload)
406
        for byte in payload:
407
            cksum += byte
408
            data.append(byte)
409
    else:
410
        data.append(0x00)
411
        # Yea, this is a 4 bit cksum (also known as a bug)
412
    data.append(cksum & 0xf)
413

    
414
    # now obfuscate by an xor starting with first payload byte ^ 0x52
415
    # including the trailing cksum.
416
    xorbits = 0x52
417
    for i, byte in enumerate(data[4:]):
418
        xord = xorbits ^ byte
419
        data[i + 4] = xord
420
        xorbits = xord
421
    return(data)
422

    
423

    
424
def _pkt_decode(data):
425
    """Take a packet hot off the wire and decode it into clear text
426
    and return the fields. We say <<cleartext>> here because all it
427
    turns out to be is annoying obfuscation.
428
    This is the inverse of pkt_decode"""
429

    
430
    # we don't care about data[0].
431
    # It is always 0x7d and not included in checksum
432
    op = data[1]
433
    direction = data[2]
434
    bytecount = data[3]
435

    
436
    # First un-obfuscate the payload and cksum
437
    payload = bytearray()
438
    xorbits = 0x52
439
    for i, byte in enumerate(data[4:]):
440
        payload.append(xorbits ^ byte)
441
        xorbits = byte
442

    
443
    # Calculate the checksum starting with the 3 bytes of the header
444
    cksum = op + direction + bytecount
445
    for byte in payload[:-1]:
446
        cksum += byte
447
    # yes, a 4 bit cksum to match the encode
448
    cksum_match = (cksum & 0xf) == payload[-1]
449
    if (not cksum_match):
450
        LOG.debug(
451
            "Checksum missmatch: %x != %x; " % (cksum, payload[-1]))
452
    return (cksum_match, op, payload[:-1])
453

    
454
# UI callbacks to process input for mapping UI fields to memory cells
455

    
456

    
457
def freq2int(val, min, max):
458
    """Convert a frequency as a string to a u32. Units is Hz
459
    """
460
    _freq = chirp_common.parse_freq(str(val))
461
    if _freq > max or _freq < min:
462
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
463
                                (chirp_common.format_freq(_freq),
464
                                 chirp_common.format_freq(min),
465
                                 chirp_common.format_freq(max)))
466
    return _freq
467

    
468

    
469
def int2freq(freq):
470
    """
471
    Convert a u32 frequency to a string for UI data entry/display
472
    This is stored in the radio as units of 10Hz which we compensate to Hz.
473
    A value of -1 indicates <no freqency>, i.e. unused channel.
474
    """
475
    if (int(freq) > 0):
476
        f = chirp_common.format_freq(freq)
477
        return f
478
    else:
479
        return ""
480

    
481

    
482
def freq2short(val, min, max):
483
    """Convert a frequency as a string to a u16 which is units of 10KHz
484
    """
485
    _freq = chirp_common.parse_freq(str(val))
486
    if _freq > max or _freq < min:
487
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
488
                                (chirp_common.format_freq(_freq),
489
                                 chirp_common.format_freq(min),
490
                                 chirp_common.format_freq(max)))
491
    return _freq/100000 & 0xFFFF
492

    
493

    
494
def short2freq(freq):
495
    """
496
       Convert a short frequency to a string for UI data entry/display
497
       This is stored in the radio as units of 10KHz which we
498
       compensate to Hz.
499
       A value of -1 indicates <no frequency>, i.e. unused channel.
500
    """
501
    if (int(freq) > 0):
502
        f = chirp_common.format_freq(freq * 100000)
503
        return f
504
    else:
505
        return ""
506

    
507

    
508
def tone2short(t):
509
    """Convert a string tone or DCS to an encoded u16
510
    """
511
    tone = str(t)
512
    if tone == "----":
513
        u16tone = 0x0000
514
    elif tone[0] == 'D':  # This is a DCS code
515
        c = tone[1: -1]
516
        code = int(c, 8)
517
        if tone[-1] == 'I':
518
            code |= 0x4000
519
        u16tone = code | 0x8000
520
    else:              # This is an analog CTCSS
521
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff  # strip the '.'
522
    return u16tone
523

    
524

    
525
def short2tone(tone):
526
    """ Map a binary CTCSS/DCS to a string name for the tone
527
    """
528
    if tone == 0 or tone == 0xffff:
529
        ret = "----"
530
    else:
531
        code = tone & 0x3fff
532
        if tone & 0x8000:      # This is a DCS
533
            if tone & 0x4000:  # This is an inverse code
534
                ret = "D%0.3oI" % code
535
            else:
536
                ret = "D%0.3oN" % code
537
        else:   # Just plain old analog CTCSS
538
            ret = "%4.1f" % (code / 10.0)
539
    return ret
540

    
541

    
542
def callid2str(cid):
543
    """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
544
       One digit (binary) per byte, terminated with '0xc'
545
    """
546

    
547
    bin2ascii = " 1234567890"
548
    cidstr = ""
549
    for i in range(0, 6):
550
        b = cid[i].get_value()
551
        if b == 0xc:  # the cid EOL
552
            break
553
        if b == 0 or b > 0xa:
554
            raise InvalidValueError(
555
                "Caller ID code has illegal byte 0x%x" % b)
556
        cidstr += bin2ascii[b]
557
    return cidstr
558

    
559

    
560
def str2callid(val):
561
    """ Convert caller id strings from callid2str.
562
    """
563
    ascii2bin = "0123456789"
564
    s = str(val).strip()
565
    if len(s) < 3 or len(s) > 6:
566
        raise InvalidValueError(
567
            "Caller ID must be at least 3 and no more than 6 digits")
568
    if s[0] == '0':
569
        raise InvalidValueError(
570
            "First digit of a Caller ID cannot be a zero '0'")
571
    blk = bytearray()
572
    for c in s:
573
        if c not in ascii2bin:
574
            raise InvalidValueError(
575
                "Caller ID must be all digits 0x%x" % c)
576
        b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
577
        blk.append(b)
578
    if len(blk) < 6:
579
        blk.append(0xc)  # EOL a short ID
580
    if len(blk) < 6:
581
        for i in range(0, (6 - len(blk))):
582
            blk.append(0xf0)
583
    return blk
584

    
585

    
586
def digits2str(digits, padding=' ', width=6):
587
    """Convert a password or SCC digit string to a string
588
    Passwords are expanded to and must be 6 chars. Fill them with '0'
589
    """
590

    
591
    bin2ascii = "0123456789"
592
    digitsstr = ""
593
    for i in range(0, 6):
594
        b = digits[i].get_value()
595
        if b == 0xc:  # the digits EOL
596
            break
597
        if b >= 0xa:
598
            raise InvalidValueError(
599
                "Value has illegal byte 0x%x" % ord(b))
600
        digitsstr += bin2ascii[b]
601
    digitsstr = digitsstr.ljust(width, padding)
602
    return digitsstr
603

    
604

    
605
def str2digits(val):
606
    """ Callback for edited strings from digits2str.
607
    """
608
    ascii2bin = " 0123456789"
609
    s = str(val).strip()
610
    if len(s) < 3 or len(s) > 6:
611
        raise InvalidValueError(
612
            "Value must be at least 3 and no more than 6 digits")
613
    blk = bytearray()
614
    for c in s:
615
        if c not in ascii2bin:
616
            raise InvalidValueError("Value must be all digits 0x%x" % c)
617
        blk.append(int(c))
618
    for i in range(len(blk), 6):
619
        blk.append(0xc)  # EOL a short ID
620
    return blk
621

    
622

    
623
def name2str(name):
624
    """ Convert a callid or scan group name to a string
625
    Deal with fixed field padding (\0 or \0xff)
626
    """
627

    
628
    namestr = ""
629
    for i in range(0, len(name)):
630
        b = ord(name[i].get_value())
631
        if b != 0 and b != 0xff:
632
            namestr += chr(b)
633
    return namestr
634

    
635

    
636
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
637
    """ Convert a string to a name. A name is a 6 element bytearray
638
    with ascii chars.
639
    """
640
    val = str(val).rstrip(' \t\r\n\0\0xff')
641
    if len(val) == 0:
642
        name = "".ljust(size, emptyfill)
643
    else:
644
        name = val.ljust(size, fillchar)
645
    return name
646

    
647

    
648
def pw2str(pw):
649
    """Convert a password string (6 digits) to a string
650
    Passwords must be 6 digits. If it is shorter, pad right with '0'
651
    """
652
    pwstr = ""
653
    ascii2bin = "0123456789"
654
    for i in range(0, len(pw)):
655
        b = pw[i].get_value()
656
        if b not in ascii2bin:
657
            raise InvalidValueError("Value must be digits 0-9")
658
        pwstr += b
659
    pwstr = pwstr.ljust(6, '0')
660
    return pwstr
661

    
662

    
663
def str2pw(val):
664
    """Store a password from UI to memory obj
665
    If we clear the password (make it go away), change the
666
    empty string to '000000' since the radio must have *something*
667
    Also, fill a < 6 digit pw with 0's
668
    """
669
    ascii2bin = "0123456789"
670
    val = str(val).rstrip(' \t\r\n\0\0xff')
671
    if len(val) == 0:  # a null password
672
        val = "000000"
673
    for i in range(0, len(val)):
674
        b = val[i]
675
        if b not in ascii2bin:
676
            raise InvalidValueError("Value must be digits 0-9")
677
    if len(val) == 0:
678
        pw = "".ljust(6, '\0')
679
    else:
680
        pw = val.ljust(6, '0')
681
    return pw
682

    
683

    
684
# Helpers to replace python2 things like confused str/byte
685

    
686
def _hex_print(data, addrfmt=None):
687
    """Return a hexdump-like encoding of @data
688
    We expect data to be a bytearray, not a string.
689
    Expanded from borrowed code to use the first 2 bytes as the address
690
    per comm packet format.
691
    """
692
    if addrfmt is None:
693
        addrfmt = '%(addr)03i'
694
        addr = 0
695
    else:  # assume first 2 bytes are address
696
        a = struct.unpack(">H", data[0:2])
697
        addr = a[0]
698
        data = data[2:]
699

    
700
    block_size = 16
701

    
702
    lines = (len(data) / block_size)
703
    if (len(data) % block_size > 0):
704
        lines += 1
705

    
706
    out = ""
707
    left = len(data)
708
    for block in range(0, lines):
709
        addr += block * block_size
710
        try:
711
            out += addrfmt % locals()
712
        except (OverflowError, ValueError, TypeError, KeyError):
713
            out += "%03i" % addr
714
        out += ': '
715

    
716
        if left < block_size:
717
            limit = left
718
        else:
719
            limit = block_size
720

    
721
        for j in range(0, block_size):
722
            if (j < limit):
723
                out += "%02x " % data[(block * block_size) + j]
724
            else:
725
                out += "   "
726

    
727
        out += "  "
728

    
729
        for j in range(0, block_size):
730

    
731
            if (j < limit):
732
                _byte = data[(block * block_size) + j]
733
                if _byte >= 0x20 and _byte < 0x7F:
734
                    out += "%s" % chr(_byte)
735
                else:
736
                    out += "."
737
            else:
738
                out += " "
739
        out += "\n"
740
        if (left > block_size):
741
            left -= block_size
742

    
743
    return out
744

    
745

    
746
# Useful UI lists
747
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
748
S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]]
749
STEP_LIST = [str(x)+"kHz" for x in STEPS]
750
ROGER_LIST = ["Off", "Begin", "End", "Both"]
751
TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)]
752
TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 11)]
753
BANDWIDTH_LIST = ["Wide", "Narrow"]
754
LANGUAGE_LIST = ["English", "Chinese-DISABLED"]
755
PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"]
756
PF2KEY_LIST = ["OFF", "Scan", "Second", "Lamp", "SDF-DIR", "K-lamp"]
757
PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX", "Scan", "Second", "Lamp"]
758
WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.",
759
                 "Ch. No.+Name"]
760
BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + \
761
                 ["Always On"]
762
SAVE_MODES = ["Off", "1", "2", "3", "4"]
763
LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"]
764
APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)]
765
OFFSET_LIST = ["none", "+", "-"]
766
PONMSG_LIST = ["Battery Volts", "Bitmap-DISABLED"]
767
SPMUTE_LIST = ["QT", "QT*T", "QT&T"]
768
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"]
769
DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)]
770
PTTID_LIST = ["Off", "Begin", "End", "Both"]
771
ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)]
772
VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1, 11)]
773
VOX_DLYS = ["Off"] + ["%ds" % t for t in range(1, 5)]
774
RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)]
775
ABR_LVL_MAP = [("1",1), ("2",2), ("3",3), ("4",4), ("5",5)]
776
LIST_1_5 = ["%s" % x for x in range(1, 6)]
777
LIST_0_9 = ["%s" % x for x in range(0, 10)]
778
LIST_1_20 = ["%s" % x for x in range(1, 21)]
779
LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
780
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
781
SCANMODE_LIST = ["TO", "CO", "SE"]
782
SCANRANGE_LIST = ["Current band", "freq range", "ALL"]
783
SCQT_LIST = ["Decoder", "Encoder", "Both"]
784
S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"]
785
POWER_LIST = ["Low", "Med", "High"]
786
RPTMODE_LIST = ["Radio/Talkie", "One direction Repeater",
787
                "Two direction repeater"]
788
TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \
789
            ["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \
790
            ["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES]
791
SCREEN_MODE_LIST = ["Classic", "Covert", "Day_1", "Day_2"]
792
ACTIVE_AREA_LIST = ["Receiver A - Top", "Receiver B - Bottom"]
793
TDR_LIST = ["TDR ON", "TDR OFF"]
794

    
795

    
796
@directory.register
797
class KGUV9PXRadio(chirp_common.CloneModeRadio,
798
                      chirp_common.ExperimentalRadio):
799

    
800
    """Wouxun KG-UV9PX"""
801
    VENDOR = "Wouxun"
802
    MODEL = "KG-UV9PX"
803
    _model = "KG-UV9D"
804
    _rev = "00"  # default rev for the radio I know about...
805
    _file_ident = "kg-uv9px"
806
    BAUD_RATE = 19200
807
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
808
                    chirp_common.PowerLevel("M", watts=2),
809
                    chirp_common.PowerLevel("H", watts=5)]
810
    _mmap = ""
811

    
812
    def _read_record(self):
813
        """ Read and validate the header of a radio reply.
814
        A record is a formatted byte stream as follows:
815
            0x7D   All records start with this
816
            opcode This is in the set of legal commands.
817
                   The radio reply matches the request
818
            dir    This is the direction, 0xFF to the radio,
819
                   0x00 from the radio.
820
            cnt    Count of bytes in payload
821
                   (not including the trailing checksum byte)
822
            <cnt bytes>
823
            <checksum byte>
824
        """
825

    
826
        # first get the header and validate it
827
        data = bytearray(self.pipe.read(4))
828
        if (len(data) < 4):
829
            raise errors.RadioError('Radio did not respond')
830
        if (data[0] != 0x7D):
831
            raise errors.RadioError(
832
                'Radio reply garbled (%02x)' % data[0])
833
        if (data[1] not in cmd_name):
834
            raise errors.RadioError(
835
                "Unrecognized opcode (%02x)" % data[1])
836
        if (data[2] != 0x00):
837
            raise errors.RadioError(
838
                "Direction incorrect. Got (%02x)" % data[2])
839
        payload_len = data[3]
840
        # don't forget to read the checksum byte
841
        data.extend(self.pipe.read(payload_len + 1))
842
        if (len(data) != (payload_len + 5)):  # we got a short read
843
            raise errors.RadioError(
844
                "Radio reply wrong size. Wanted %d, got %d" %
845
                ((payload_len + 1), (len(data) - 4)))
846
        return _pkt_decode(data)
847

    
848
    def _write_record(self, cmd, payload=None):
849
        """ Write a request packet to the radio.
850
        """
851

    
852
        packet = _pkt_encode(cmd, payload)
853
        self.pipe.write(packet)
854

    
855
    @classmethod
856
    def match_model(cls, filedata, filename):
857
        """Look for bits in the file image and see if it looks
858
        like ours...
859
        TODO: there is a bunch of rubbish between 0x50 and 0x160
860
        that is still a known unknown
861
        """
862
        return cls._file_ident in filedata[0x51:0x59].lower()
863

    
864
    def _identify(self):
865
        """ Identify the radio
866
        The ident block identifies the radio and its capabilities.
867
        This block is always 78 bytes. The rev == '01' is the base
868
        radio and '02' seems to be the '-Plus' version.
869
        I don't really trust the content after the model and revision.
870
        One would assume this is pretty much constant data but I have
871
        seen differences between my radio and the dump named
872
        KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first
873
        five bands match the OEM windows
874
        app except the 350-400 band. The OOB trace has the 700MHz
875
        band different. This is speculation at this point.
876

    
877
        TODO: This could be smarter and reject a radio not actually
878
        a UV9D...
879
        """
880

    
881
        for _i in range(0, 10):  # retry 10 times if we get junk
882
            self._write_record(CMD_IDENT)
883
            chksum_match, op, _resp = self._read_record()
884
            if len(_resp) == 0:
885
                raise Exception("Radio not responding")
886
            if len(_resp) != 74:
887
                LOG.error(
888
                    "Expected and IDENT reply of 78 bytes. Got (%d)" %
889
                    len(_resp))
890
                continue
891
            if not chksum_match:
892
                LOG.error("Checksum error: retrying ident...")
893
                time.sleep(0.100)
894
                continue
895
            if op != CMD_IDENT:
896
                LOG.error("Expected IDENT reply. Got (%02x)" % op)
897
                continue
898
            LOG.debug("Got:\n%s" % _hex_print(_resp))
899
            (mod, rev) = struct.unpack(">7s2s", _resp[0:9])
900
            LOG.debug("Model %s, rev %s" % (mod, rev))
901
            if mod == self._model:
902
                self._rev = rev
903
                return
904
            else:
905
                raise Exception("Unable to identify radio")
906
        raise Exception("All retries to identify failed")
907

    
908
    def process_mmap(self):
909
        if self._rev == "02" or self._rev == "00":
910
            self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
911
        else:  # this is where you elif the other variants and non-Plus  radios
912
            raise errors.RadioError(
913
                "Unrecognized model variation (%s). No memory map for it" %
914
                self._rev)
915

    
916
    def sync_in(self):
917
        """ Public sync_in
918
            Download contents of the radio. Throw errors back
919
            to the core if the radio does not respond.
920
            """
921
        try:
922
            self._identify()
923
            self._mmap = self._do_download()
924
            self._write_record(CMD_HANGUP)
925
        except errors.RadioError:
926
            raise
927
        except Exception, e:
928
            LOG.exception('Unknown error during download process')
929
            raise errors.RadioError(
930
                "Failed to communicate with radio: %s" % e)
931
        self.process_mmap()
932

    
933
    def sync_out(self):
934
        """ Public sync_out
935
            Upload the modified memory image into the radio.
936
            """
937

    
938
        try:
939
            self._identify()
940
            self._do_upload()
941
            self._write_record(CMD_HANGUP)
942
        except errors.RadioError:
943
            raise
944
        except Exception, e:
945
            raise errors.RadioError(
946
                "Failed to communicate with radio: %s" % e)
947
        return
948

    
949
    def _do_download(self):
950
        """ Read the whole of radio memory in 64 byte chunks.
951
        We load the config space followed by loading memory channels.
952
        The radio seems to be a "clone" type and the memory channels
953
        are actually within the config space. There are separate
954
        commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
955
        these seem to be a hack that can only do 4 channels at a time.
956
        Since the radio only supports 999, (can only support 3 chars
957
        in the display UI?) although the vendors app reads 1000
958
        channels, it hacks back to config writes (CMD_WCONF) for the
959
        last 3 channels and names. We keep it simple and just read
960
        the whole thing even though the vendor app doesn't. Channels
961
        are separate in their app simply because the radio protocol
962
        has read/write commands to access it. What they do is simply
963
        marshal the frequency+mode bits in 4 channel chunks followed
964
        by a separate chunk of for names. In config space, they are two
965
        separate arrays 1..999. Given that this space is not a
966
        multiple of 4, there is hackery on upload to do the writes to
967
        config space. See upload for this.
968
        """
969

    
970
        mem = bytearray(0x8000)  # The radio's memory map is 32k
971
        for addr in range(0, 0x8000, 64):
972
            req = bytearray(struct.pack(">HB", addr, 64))
973
            self._write_record(CMD_RCONF, req)
974
            LOG.debug("Sent:\n%s" % _hex_print(req))           
975
            chksum_match, op, resp = self._read_record()
976
            LOG.debug("Got:\n%s" % _hex_print(resp))
977

    
978
            if not chksum_match:
979
                LOG.debug(_hex_print(resp))
980
                raise Exception(
981
                    "Checksum error while reading configuration (0x%x)" %
982
                    addr)
983
            pa = struct.unpack(">H", resp[0:2])
984
            pkt_addr = pa[0]
985
            payload = resp[2:]
986
            if op != CMD_RCONF or addr != pkt_addr:
987
                raise Exception(
988
                    "Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
989
                    (addr, op, pkt_addr))
990
            LOG.debug("Config read (0x%x):\n%s" %
991
                      (addr, _hex_print(resp, '0x%(addr)04x')))
992
# Code from 9D Plus driver was len(Payload)-1:  Caused every 64th byte to = 00
993
            for i in range(0, len(payload)):
994
                mem[addr + i] = payload[i]
995
            if self.status_fn:
996
                status = chirp_common.Status()
997
                status.cur = addr
998
                status.max = 0x8000
999
                status.msg = "Cloning from radio"
1000
                self.status_fn(status)
1001
        strmem = "".join([chr(x) for x in mem])
1002
        return memmap.MemoryMap(strmem)
1003

    
1004
    def _do_upload(self):
1005
        """Walk through the config map and write updated records to
1006
        the radio. The config map contains only the regions we know
1007
        about. We don't use the channel memory commands to avoid the
1008
        hackery of using config write commands to fill in the last
1009
        3 channel memory and names slots. As we discover other useful
1010
        goodies in the map, we can add more slots...
1011
        """
1012
        for ar, size, count in config_map:
1013
            for addr in range(ar, ar + (size*count), size):
1014
                req = bytearray(struct.pack(">H", addr))
1015
                req.extend(self.get_mmap()[addr:addr + size])
1016
                self._write_record(CMD_WCONF, req)
1017
                LOG.debug("Config write (0x%x):\n%s" %
1018
                          (addr, _hex_print(req)))
1019
                chksum_match, op, ack = self._read_record()
1020
                LOG.debug("Config write ack [%x]\n%s" %
1021
                          (addr, _hex_print(ack)))
1022
                a = struct.unpack(">H", ack)  # big endian short...
1023
                ack = a[0]
1024
                if not chksum_match or op != CMD_WCONF or addr != ack:
1025
                    msg = ""
1026
                    if not chksum_match:
1027
                        msg += "Checksum err, "
1028
                    if op != CMD_WCONF:
1029
                        msg += "cmd mismatch %x != %x, " % \
1030
                               (op, CMD_WCONF)
1031
                    if addr != ack:
1032
                        msg += "ack error %x != %x, " % (addr, ack)
1033
                    raise Exception("Radio did not ack block: %s error" % msg)
1034
                if self.status_fn:
1035
                    status = chirp_common.Status()
1036
                    status.cur = addr
1037
                    status.max = 0x8000
1038
                    status.msg = "Update radio"
1039
                    self.status_fn(status)
1040

    
1041
    def get_features(self):
1042
        """ Public get_features
1043
            Return the features of this radio once we have identified
1044
            it and gotten its bits
1045
            """
1046
        rf = chirp_common.RadioFeatures()
1047
        rf.has_settings = True
1048
        rf.has_ctone = True
1049
        rf.has_rx_dtcs = True
1050
        rf.has_cross = True
1051
        rf.has_tuning_step = False
1052
        rf.has_bank = False
1053
        rf.can_odd_split = True
1054
        rf.valid_skips = ["", "S"]
1055
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
1056
        rf.valid_cross_modes = [
1057
            "Tone->Tone",
1058
            "Tone->DTCS",
1059
            "DTCS->Tone",
1060
            "DTCS->",
1061
            "->Tone",
1062
            "->DTCS",
1063
            "DTCS->DTCS",
1064
        ]
1065
        rf.valid_modes = ["FM", "NFM", "AM"]
1066
        rf.valid_power_levels = self.POWER_LEVELS
1067
        rf.valid_name_length = 8
1068
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
1069
        rf.valid_bands = [(108000000, 136000000),  # Aircraft  AM
1070
                          (136000000, 180997500),  # supports 2m
1071
                          (219000000, 250997500),
1072
                          (350000000, 400000000),
1073
                          (400000000, 520000000),  # supports 70cm
1074
                          (700000000, 986997500)]
1075
        rf.valid_characters = chirp_common.CHARSET_ASCII
1076
        rf.valid_tuning_steps = STEPS
1077
        rf.memory_bounds = (1, 999)  # 999 memories
1078
        return rf
1079

    
1080
    @classmethod
1081
    def get_prompts(cls):
1082
        rp = chirp_common.RadioPrompts()
1083
        rp.experimental = ('This driver is experimental and may contain bugs. \n'
1084
             'USE AT YOUR OWN RISK  - '
1085
             'SAVE A COPY OF DOWNLOAD FROM YOUR RADIO BEFORE MAKING CHANGES\n'
1086
             '\nAll known CPS settings are implemented \n'
1087
             '\n Additional settings found only on radio are also included'
1088
             '\nMute, Compander and Scrambler are defaulted to '
1089
             'QT, OFF , OFF for all channel memories\n'
1090
             '\n'
1091
             'Modification of Freq Limit Interfaces is done '
1092
             'AT YOUR OWN RISK and '
1093
             'may affect radio performance and may violate rules, '
1094
             'regulations '
1095
             'or laws in your jurisdiction.\n'
1096
             )
1097

    
1098
        return rp
1099

    
1100
    def get_raw_memory(self, number):
1101
        return repr(self._memobj.chan_blk[number])
1102

    
1103
    def _get_tone(self, _mem, mem):
1104
        """Decode both the encode and decode CTSS/DCS codes from
1105
        the memory channel and stuff them into the UI
1106
        memory channel row.
1107
        """
1108
        txtone = short2tone(_mem.encQT)
1109
        rxtone = short2tone(_mem.decQT)
1110
        pt = "N"
1111
        pr = "N"
1112

    
1113
        if txtone == "----":
1114
            txmode = ""
1115
        elif txtone[0] == "D":
1116
            mem.dtcs = int(txtone[1:4])
1117
            if txtone[4] == "I":
1118
                pt = "R"
1119
            txmode = "DTCS"
1120
        else:
1121
            mem.rtone = float(txtone)
1122
            txmode = "Tone"
1123

    
1124
        if rxtone == "----":
1125
            rxmode = ""
1126
        elif rxtone[0] == "D":
1127
            mem.rx_dtcs = int(rxtone[1:4])
1128
            if rxtone[4] == "I":
1129
                pr = "R"
1130
            rxmode = "DTCS"
1131
        else:
1132
            mem.ctone = float(rxtone)
1133
            rxmode = "Tone"
1134

    
1135
        if txmode == "Tone" and len(rxmode) == 0:
1136
            mem.tmode = "Tone"
1137
        elif (txmode == rxmode and txmode == "Tone" and
1138
              mem.rtone == mem.ctone):
1139
            mem.tmode = "TSQL"
1140
        elif (txmode == rxmode and txmode == "DTCS" and
1141
              mem.dtcs == mem.rx_dtcs):
1142
            mem.tmode = "DTCS"
1143
        elif (len(rxmode) + len(txmode)) > 0:
1144
            mem.tmode = "Cross"
1145
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1146

    
1147
        mem.dtcs_polarity = pt + pr
1148

    
1149
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1150
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1151

    
1152
    def get_memory(self, number):
1153
        """ Public get_memory
1154
            Return the channel memory referenced by number to the UI.
1155
        """
1156
        _mem = self._memobj.chan_blk[number - 1]
1157
        _nam = self._memobj.chan_name[number - 1]
1158

    
1159
        mem = chirp_common.Memory()
1160
        mem.number = number
1161
        _valid = _mem.state
1162
        if _valid != MEM_VALID and _valid != 0 and _valid != 2 and _valid != 0x40:
1163
            # In Issue #6995 we can find _valid values of 0 and 2 in the IMG
1164
            # so these values should be treated like MEM_VALID.
1165
            # state value of 0x40 found in deleted memory but still shows in CPS
1166
            mem.empty = True
1167
            return mem
1168
        else:
1169
            mem.empty = False
1170

    
1171
        mem.freq = int(_mem.rxfreq) * 10
1172

    
1173
        if _mem.txfreq == 0xFFFFFFFF:
1174
            # TX freq not set
1175
            mem.duplex = "off"
1176
            mem.offset = 0
1177
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1178
            mem.duplex = ""
1179
            mem.offset = 0
1180
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1181
            mem.duplex = "split"
1182
            mem.offset = int(_mem.txfreq) * 10
1183
        else:
1184
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1185
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1186

    
1187
        mem.name = name2str(_nam.name)
1188

    
1189
        self._get_tone(_mem, mem)
1190

    
1191
        mem.skip = "" if bool(_mem.scan) else "S"
1192

    
1193
        mem.power = self.POWER_LEVELS[_mem.pwr]
1194
        if _mem.mod == 1:
1195
            mem.mode = "AM"
1196
        elif _mem.fm_dev == 0:
1197
            mem.mode = "FM"
1198
        else:
1199
            mem.mode = "NFM"
1200
        #  qt has no home in the UI
1201
        return mem
1202

    
1203
    def _set_tone(self, mem, _mem):
1204
        """Update the memory channel block CTCC/DCS tones
1205
        from the UI fields
1206
        """
1207
        def _set_dcs(code, pol):
1208
            val = int("%i" % code, 8) | 0x8000
1209
            if pol == "R":
1210
                val |= 0x4000
1211
            return val
1212

    
1213
        rx_mode = tx_mode = None
1214
        rxtone = txtone = 0x0000
1215

    
1216
        if mem.tmode == "Tone":
1217
            tx_mode = "Tone"
1218
            txtone = int(mem.rtone * 10)
1219
        elif mem.tmode == "TSQL":
1220
            rx_mode = tx_mode = "Tone"
1221
            rxtone = txtone = int(mem.ctone * 10)
1222
        elif mem.tmode == "DTCS":
1223
            tx_mode = rx_mode = "DTCS"
1224
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1225
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1226
        elif mem.tmode == "Cross":
1227
            tx_mode, rx_mode = mem.cross_mode.split("->")
1228
            if tx_mode == "DTCS":
1229
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1230
            elif tx_mode == "Tone":
1231
                txtone = int(mem.rtone * 10)
1232
            if rx_mode == "DTCS":
1233
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1234
            elif rx_mode == "Tone":
1235
                rxtone = int(mem.ctone * 10)
1236

    
1237
        _mem.decQT = rxtone
1238
        _mem.encQT = txtone
1239

    
1240
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1241
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1242

    
1243
    def set_memory(self, mem):
1244
        """ Public set_memory
1245
            Inverse of get_memory. Update the radio memory image
1246
            from the mem object
1247
            """
1248
        number = mem.number
1249

    
1250
        _mem = self._memobj.chan_blk[number - 1]
1251
        _nam = self._memobj.chan_name[number - 1]
1252

    
1253
        if mem.empty:
1254
# consider putting in a check for chan # that is empty but 
1255
# listed as one of the 2 working channels and change them
1256
# to channel 1 to be consistent with CPS and allow
1257
# complete deletion from radio.  Otherwise,
1258
# a deleted channel will still show on radio with no name.
1259
            _mem.set_raw("\xFF" * (_mem.size() / 8))
1260
            _nam.name = str2name("", 8, '\0', '\0')
1261
            _mem.state = MEM_INVALID
1262
            return
1263

    
1264
        _mem.rxfreq = int(mem.freq / 10)
1265
        if mem.duplex == "off":
1266
            _mem.txfreq = 0xFFFFFFFF
1267
        elif mem.duplex == "split":
1268
            _mem.txfreq = int(mem.offset / 10)
1269
        elif mem.duplex == "+":
1270
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1271
        elif mem.duplex == "-":
1272
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1273
        else:
1274
            _mem.txfreq = int(mem.freq / 10)
1275
        _mem.scan = int(mem.skip != "S")
1276
        if mem.mode == "FM":
1277
            _mem.mod = 0    # make sure forced AM is off
1278
            _mem.fm_dev = 0
1279
        elif mem.mode == "NFM":
1280
            _mem.mod = 0
1281
            _mem.fm_dev = 1
1282
        elif mem.mode == "AM":
1283
            _mem.mod = 1     # AM on
1284
            _mem.fm_dev = 1  # set NFM bandwidth
1285
        else:
1286
            _mem.mod = 0
1287
            _mem.fm_dev = 0  # Catchall default is FM
1288
        # set the tone
1289
        self._set_tone(mem, _mem)
1290
        # set the power
1291
        if mem.power:
1292
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1293
        else:
1294
            _mem.pwr = True
1295

    
1296
        # Set fields we can't access via the UI table to safe defaults
1297
        _mem.qt = 0   # mute mode to QT
1298

    
1299
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1300
        _mem.state = MEM_VALID
1301

    
1302
# Build the UI configuration tabs
1303
# the channel memory tab is built by the core.
1304
# We have no control over it
1305

    
1306
    def _core_tab(self):
1307
        """ Build Core Configuration tab
1308
        Radio settings common to all modes and areas go here.
1309
        """
1310
        s = self._memobj.settings
1311
        sm = self._memobj.screen
1312

    
1313
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1314

    
1315
        cf.append(RadioSetting("auto_am",
1316
                               "Auto detect AM (Menu 53)",
1317
                               RadioSettingValueBoolean(s.auto_am)))
1318
        cf.append(RadioSetting("qt_sw",
1319
                               "Scan tone detect (Menu 59)",
1320
                               RadioSettingValueBoolean(s.qt_sw)))
1321
        cf.append(
1322
            RadioSetting("s_mute",
1323
                         "SubFreq Mute (Menu 60)",
1324
                         RadioSettingValueList(S_MUTE_LIST,
1325
                                               S_MUTE_LIST[s.s_mute])))
1326
        cf.append(
1327
            RadioSetting("tot",
1328
                         "Transmit timeout Timer (Menu 10)",
1329
                         RadioSettingValueList(TIMEOUT_LIST,
1330
                                               TIMEOUT_LIST[s.tot])))
1331
        cf.append(
1332
            RadioSetting("toa",
1333
                         "Transmit Timeout Alarm (Menu 11)",
1334
                         RadioSettingValueList(TOA_LIST,
1335
                                               TOA_LIST[s.toa])))
1336
        cf.append(
1337
            RadioSetting("ptt_id",
1338
                         "PTT Caller ID mode (Menu 23)",
1339
                         RadioSettingValueList(PTTID_LIST,
1340
                                               PTTID_LIST[s.ptt_id])))
1341
        cf.append(
1342
            RadioSetting("id_dly",
1343
                         "Caller ID Delay time (Menu 25)",
1344
                         RadioSettingValueList(ID_DLY_LIST,
1345
                                               ID_DLY_LIST[s.id_dly])))
1346
        cf.append(RadioSetting("voice_sw",
1347
                               "Voice Guide (Menu 12)",
1348
                               RadioSettingValueBoolean(s.voice_sw)))
1349
        cf.append(RadioSetting("beep",
1350
                               "Keypad Beep (Menu 13)",
1351
                               RadioSettingValueBoolean(s.beep)))
1352
        cf.append(
1353
            RadioSetting("s_tone",
1354
                         "Side Tone (Menu 36)",
1355
                         RadioSettingValueList(S_TONES,
1356
                                               S_TONES[s.s_tone])))
1357
        cf.append(
1358
            RadioSetting("ring_time",
1359
                         "Ring Time (Menu 26)",
1360
                         RadioSettingValueList(
1361
                             LIST_OFF_10,
1362
                             LIST_OFF_10[s.ring_time])))
1363
        cf.append(
1364
            RadioSetting("roger",
1365
                         "Roger Beep (Menu 9)",
1366
                         RadioSettingValueList(ROGER_LIST,
1367
                                               ROGER_LIST[s.roger])))
1368
        cf.append(RadioSetting("blcdsw",
1369
                               "Backlight (Menu 41)",
1370
                               RadioSettingValueBoolean(s.blcdsw)))
1371
        cf.append(
1372
            RadioSetting("abr",
1373
                         "Auto Backlight Time (Menu 1)",
1374
                         RadioSettingValueList(BACKLIGHT_LIST,
1375
                                               BACKLIGHT_LIST[s.abr])))
1376
        cf.append(
1377
            RadioSetting("abr_lvl",
1378
                         "Backlight Brightness (Menu 27)",
1379
                         RadioSettingValueMap(ABR_LVL_MAP,
1380
                                               s.abr_lvl)))
1381
        cf.append(RadioSetting("lock",
1382
                               "Keypad Lock",
1383
                               RadioSettingValueBoolean(s.lock)))
1384
        cf.append(
1385
            RadioSetting("lock_m",
1386
                         "Keypad Lock Mode (Menu 35)",
1387
                         RadioSettingValueList(LOCK_MODES,
1388
                                               LOCK_MODES[s.lock_m])))
1389
        cf.append(RadioSetting("auto_lk",
1390
                               "Keypad Autolock (Menu 34)",
1391
                               RadioSettingValueBoolean(s.auto_lk)))
1392
        cf.append(RadioSetting("prich_sw",
1393
                               "Priority Channel Scan (Menu 33)",
1394
                               RadioSettingValueBoolean(s.prich_sw)))
1395
        cf.append(RadioSetting("pri_ch",
1396
                               "Priority Channel (Menu 32)",
1397
                               RadioSettingValueInteger(1, 999,
1398
                                                        s.pri_ch)))
1399
        cf.append(
1400
            RadioSetting("dtmf_st",
1401
                         "DTMF Sidetone (Menu 22)",
1402
                         RadioSettingValueList(DTMFST_LIST,
1403
                                               DTMFST_LIST[s.dtmf_st])))
1404
        cf.append(RadioSetting("sc_qt",
1405
                               "Scan QT Save Mode (Menu 38)",
1406
                               RadioSettingValueList(
1407
                                   SCQT_LIST,
1408
                                   SCQT_LIST[s.sc_qt])))
1409
        cf.append(
1410
            RadioSetting("apo_tmr",
1411
                         "Automatic Power-off (Menu 39)",
1412
                         RadioSettingValueList(APO_TIMES,
1413
                                               APO_TIMES[s.apo_tmr])))
1414
        cf.append(  # VOX "guard" is really VOX trigger audio level
1415
            RadioSetting("vox_grd",
1416
                         "VOX level (Menu 7)",
1417
                         RadioSettingValueList(VOX_GRDS,
1418
                                               VOX_GRDS[s.vox_grd])))
1419
        cf.append(
1420
            RadioSetting("vox_dly",
1421
                         "VOX Delay (Menu 37)",
1422
                         RadioSettingValueList(VOX_DLYS,
1423
                                               VOX_DLYS[s.vox_dly])))
1424
        cf.append(RadioSetting("bledsw",
1425
                               "Receive LED (Menu 42)",
1426
                               RadioSettingValueBoolean(s.bledsw)))
1427

    
1428
        cf.append(RadioSetting("screen.screen_mode",
1429
                               "Screen Mode (Menu 62)",
1430
                               RadioSettingValueList(
1431
                                   SCREEN_MODE_LIST,
1432
                                   SCREEN_MODE_LIST[
1433
                                    sm.screen_mode])))
1434
        cf.append(
1435
            RadioSetting("lang",
1436
                         "Menu Language (Menu 14)"
1437
                         "\nDisabled in radio"
1438
                         "\nENGLISH Only",
1439
                         RadioSettingValueList(LANGUAGE_LIST,
1440
                                               LANGUAGE_LIST[s.lang])))
1441
        cf.append(RadioSetting("ponmsg",
1442
                               "Poweron message (Menu 40)"
1443
                               "\n Bitmap is DISABLED in Radio",
1444
                               RadioSettingValueList(
1445
                                   PONMSG_LIST, PONMSG_LIST[s.ponmsg])))
1446
        return cf
1447

    
1448
    def _repeater_tab(self):
1449
        """Repeater mode functions
1450
        """
1451
        s = self._memobj.settings
1452
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1453

    
1454
        cf.append(
1455
            RadioSetting("type_set",
1456
                         "Radio Mode (Menu 43)",
1457
                         RadioSettingValueList(
1458
                             RPTMODE_LIST,
1459
                             RPTMODE_LIST[s.type_set])))
1460
        cf.append(RadioSetting("rpt_ptt",
1461
                               "Repeater PTT (Menu 45)",
1462
                               RadioSettingValueBoolean(s.rpt_ptt)))
1463
        cf.append(RadioSetting("rpt_spk",
1464
                               "Repeater Mode Speaker (Menu 44)",
1465
                               RadioSettingValueBoolean(s.rpt_spk)))
1466
        cf.append(
1467
            RadioSetting("rpt_kpt",
1468
                         "Repeater Hold Time (Menu 46)",
1469
                         RadioSettingValueList(RPT_KPTS,
1470
                                               RPT_KPTS[s.rpt_kpt])))
1471
        cf.append(RadioSetting("rpt_rct",
1472
                               "Repeater Receipt Tone (Menu 47)",
1473
                               RadioSettingValueBoolean(s.rpt_rct)))
1474
        return cf
1475

    
1476
    def _admin_tab(self):
1477
        """Admin functions not present in radio menu...
1478
        These are admin functions not radio operation configuration
1479
        """
1480

    
1481
        def apply_cid(setting, obj):
1482
            c = str2callid(setting.value)
1483
            obj.code = c
1484

    
1485
        def apply_scc(setting, obj):
1486
            c = str2digits(setting.value)
1487
            obj.scc = c
1488

    
1489
        def apply_mode_sw(setting, obj):
1490
            pw = str2pw(setting.value)
1491
            obj.mode_sw = pw
1492
            setting.value = pw2str(obj.mode_sw)
1493

    
1494
        def apply_reset(setting, obj):
1495
            pw = str2pw(setting.value)
1496
            obj.reset = pw
1497
            setting.value = pw2str(obj.reset)
1498

    
1499
        def apply_wake(setting, obj):
1500
            obj.wake = int(setting.value)/10
1501

    
1502
        def apply_sleep(setting, obj):
1503
            obj.sleep = int(setting.value)/10
1504

    
1505
        pw = self._memobj.passwords  # admin passwords
1506
        s = self._memobj.settings
1507

    
1508
        cf = RadioSettingGroup("admin", "Admin Functions")
1509

    
1510
        cf.append(RadioSetting("menu_avail",
1511
                               "Menu available in channel mode",
1512
                               RadioSettingValueBoolean(s.menu_avail)))
1513
        mode_sw = RadioSettingValueString(0, 6,
1514
                                          pw2str(pw.mode_sw), False)
1515
        rs = RadioSetting("passwords.mode_sw",
1516
                          "Mode Switch Password", mode_sw)
1517
        rs.set_apply_callback(apply_mode_sw, pw)
1518
        cf.append(rs)
1519

    
1520
        cf.append(RadioSetting("reset_avail",
1521
                               "Radio Reset Available",
1522
                               RadioSettingValueBoolean(s.reset_avail)))
1523
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1524
        rs = RadioSetting("passwords.reset",
1525
                          "Radio Reset Password", reset)
1526
        rs.set_apply_callback(apply_reset, pw)
1527
        cf.append(rs)
1528

    
1529
        cf.append(
1530
            RadioSetting("dtmf_tx",
1531
                         "DTMF Tx Duration",
1532
                         RadioSettingValueList(DTMF_TIMES,
1533
                                               DTMF_TIMES[s.dtmf_tx])))
1534
        cid = self._memobj.my_callid
1535
        my_callid = RadioSettingValueString(3, 6,
1536
                                            callid2str(cid.code), False)
1537
        rs = RadioSetting("my_callid.code",
1538
                          "PTT Caller ID code (Menu 24)", my_callid)
1539
        rs.set_apply_callback(apply_cid, cid)
1540
        cf.append(rs)
1541

    
1542
        stun = self._memobj.stun
1543
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1544
        rs = RadioSetting("stun.scc", "Security code", st)
1545
        rs.set_apply_callback(apply_scc, stun)
1546
        cf.append(rs)
1547

    
1548
        cf.append(
1549
            RadioSetting("settings.save_m",
1550
                         "Save Mode  (Menu 2)",
1551
                         RadioSettingValueList(SAVE_MODES,
1552
                                               SAVE_MODES[s.save_m])))
1553
        for i in range(0, 4):
1554
            sm = self._memobj.save[i]
1555
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1556
            wf = RadioSetting("save[%i].wake" % i,
1557
                              "Save Mode %d Wake Time" % (i+1), wake)
1558
            wf.set_apply_callback(apply_wake, sm)
1559
            cf.append(wf)
1560

    
1561
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1562
            wf = RadioSetting("save[%i].sleep" % i,
1563
                              "Save Mode %d Sleep Time" % (i+1), slp)
1564
            wf.set_apply_callback(apply_sleep, sm)
1565
            cf.append(wf)
1566

    
1567
        _msg = str(self._memobj.display.banner).split("\0")[0]
1568
        val = RadioSettingValueString(0, 16, _msg)
1569
        val.set_mutable(True)
1570
        cf.append(RadioSetting("display.banner",
1571
                               "Display Message", val))
1572

    
1573
        _str = str(self._memobj.oemmodel.model).split("\0")[0]
1574
        val = RadioSettingValueString(0, 10, _str)
1575
        val.set_mutable(True)
1576
        cf.append(RadioSetting("oemmodel.model",
1577
                              "Custom Sub-Receiver Message", val))
1578

    
1579
        val = RadioSettingValueList(
1580
                              TDR_LIST,
1581
                              TDR_LIST[s.tdr])
1582
        val.set_mutable(True)
1583
        cf.append(RadioSetting("tdr", "TDR", val))
1584

    
1585
        val = RadioSettingValueList(
1586
                              ACTIVE_AREA_LIST,
1587
                              ACTIVE_AREA_LIST[s.act_area])
1588
        val.set_mutable(True)
1589
        cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1590

    
1591
        return cf
1592

    
1593
    def _fm_tab(self):
1594
        """FM Broadcast channels
1595
        """
1596
        def apply_fm(setting, obj):
1597
            f = freq2short(setting.value, 76000000, 108000000)
1598
            obj.fm_freq = f
1599

    
1600
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1601
        for ch in range(0, 20):
1602
            chan = self._memobj.fm_chans[ch]
1603
            freq = RadioSettingValueString(0, 20,
1604
                                           short2freq(chan.fm_freq))
1605
            rs = RadioSetting("fm_%d" % (ch + 1),
1606
                              "FM Channel %d" % (ch + 1), freq)
1607
            rs.set_apply_callback(apply_fm, chan)
1608
            fm.append(rs)
1609
        return fm
1610

    
1611
    def _scan_grp(self):
1612
        """Scan groups
1613
        """
1614
        def apply_name(setting, obj):
1615
            name = str2name(setting.value, 8, '\0', '\0')
1616
            obj.name = name
1617

    
1618
        def apply_start(setting, obj):
1619
            """Do a callback to deal with RadioSettingInteger limitation
1620
            on memory address resolution
1621
            """
1622
            obj.scan_st = int(setting.value)
1623

    
1624
        def apply_end(setting, obj):
1625
            """Do a callback to deal with RadioSettingInteger limitation
1626
            on memory address resolution
1627
            """
1628
            obj.scan_end = int(setting.value)
1629

    
1630
        sgrp = self._memobj.scn_grps
1631
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1632
        for i in range(0, 10):
1633
            s_grp = sgrp.addrs[i]
1634
            s_name = sgrp.names[i]
1635
            rs_name = RadioSettingValueString(0, 6,
1636
                                              name2str(s_name.name))
1637
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1638
                              "Group %i Name" % (i + 1), rs_name)
1639
            rs.set_apply_callback(apply_name, s_name)
1640
            scan.append(rs)
1641
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1642
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1643
                              "Starting Channel", rs_st)
1644
            rs.set_apply_callback(apply_start, s_grp)
1645
            scan.append(rs)
1646
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1647
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1648
                              "Last Channel", rs_end)
1649
            rs.set_apply_callback(apply_end, s_grp)
1650
            scan.append(rs)
1651
        return scan
1652

    
1653
    def _callid_grp(self):
1654
        """Caller IDs to be recognized by radio
1655
        This really should be a table in the UI
1656
        """
1657
        def apply_callid(setting, obj):
1658
            c = str2callid(setting.value)
1659
            obj.cid = c
1660

    
1661
        def apply_name(setting, obj):
1662
            name = str2name(setting.value, 6, '\0', '\xff')
1663
            obj.name = name
1664

    
1665
        cid = RadioSettingGroup("callids", "Caller IDs")
1666
        for i in range(0, 20):
1667
            callid = self._memobj.call_ids[i]
1668
            name = self._memobj.cid_names[i]
1669
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1670
            rs = RadioSetting("cid_names[%i].name" % i,
1671
                              "Caller ID %i Name" % (i + 1), c_name)
1672
            rs.set_apply_callback(apply_name, name)
1673
            cid.append(rs)
1674
            c_id = RadioSettingValueString(0, 6,
1675
                                           callid2str(callid.cid),
1676
                                           False)
1677
            rs = RadioSetting("call_ids[%i].cid" % i,
1678
                              "Caller ID Code", c_id)
1679
            rs.set_apply_callback(apply_callid, callid)
1680
            cid.append(rs)
1681
        return cid
1682

    
1683
    def _limits_tab(self):
1684

    
1685
        limgrp = RadioSettingGroup("limits",
1686
                                   "Freq Limits - USE AT YOUR RISK"
1687
                                   "\n Changes may violate laws, rules"
1688
                                   "\n or regulations in your region"
1689
                                   "\n upper limits include .9975 Mhz"
1690
                                   "\n Ex: 449 = 449.9975")
1691

    
1692
        l = self._memobj.limits
1693

    
1694
        val = RadioSettingValueInteger(136, 180,
1695
                                       (l.lim_150M_Txlower_limit) / 10.0)
1696
        rs = RadioSetting("limits.lim_150M_Txlower_limit",
1697
                          "150M Tx Lower Limit (MHz)"
1698
                          "\n 136 Min 180 Max",
1699
                          RadioSettingValueInteger(136, 180,
1700
                                                   val))
1701
        limgrp.append(rs)
1702

    
1703
        val = RadioSettingValueInteger(136, 180,
1704
                                       (l.lim_150M_Txupper_limit) / 10.0)
1705
        rs = RadioSetting("limits.lim_150M_Txupper_limit",
1706
                          "150M Tx Upper Limit (MHz)"
1707
                          "\n 136 Min 180 Max",
1708
                          RadioSettingValueInteger(136, 180,
1709
                                                   val))
1710
        limgrp.append(rs)
1711

    
1712
        val = RadioSettingValueInteger(400, 512,
1713
                                       (l.lim_450M_Txlower_limit) / 10.0)
1714
        rs = RadioSetting("limits.lim_450M_Txlower_limit",
1715
                          "450M Tx Lower Limit (MHz)"
1716
                          "\n 400 Min 512 Max",
1717
                          RadioSettingValueInteger(400, 512,
1718
                                                   val))
1719
        limgrp.append(rs)
1720

    
1721
        val = RadioSettingValueInteger(400, 512,
1722
                                       (l.lim_450M_Txupper_limit) / 10.0)
1723
        rs = RadioSetting("limits.lim_450M_Txupper_limit",
1724
                          "450M Tx Upper Limit (MHz)"
1725
                           "\n 400 Min 512 Max",
1726
                         RadioSettingValueInteger(400, 512,
1727
                                                   val))
1728
        limgrp.append(rs)
1729

    
1730
        val = RadioSettingValueInteger(108, 180,
1731
                                       (l.lim_150M_area_a_rxlower_limit) / 10.0)
1732
        rs = RadioSetting("limits.lim_150M_area_a_rxlower_limit",
1733
                          "Rcvr A 150M Rx Lower Limit (MHz)"
1734
                           "\n 108 Min 180 Max",
1735
                          RadioSettingValueInteger(108, 180,
1736
                                                   val))
1737
        limgrp.append(rs)
1738

    
1739
        val = RadioSettingValueInteger(108, 180,
1740
                                       (l.lim_150M_area_a_rxupper_limit) / 10.0)
1741
        rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
1742
                          "Rcvr A 150M Rx Upper Limit (MHz)"
1743
                           "\n 108 Min 180 Max",
1744
                          RadioSettingValueInteger(108, 180,
1745
                                                   val))
1746
        limgrp.append(rs)
1747

    
1748
        val = RadioSettingValueInteger(136, 180,
1749
                                       (l.lim_150M_area_b_rxlower_limit) / 10.0)
1750
        rs = RadioSetting("limits.lim_150M_area_b_rxlower_limit",
1751
                          "Rcvr B 150M Rx Lower Limit (MHz)"
1752
                           "\n 136 Min 180 Max",
1753
                          RadioSettingValueInteger(136, 180,
1754
                                                   val))
1755
        limgrp.append(rs)
1756

    
1757
        val = RadioSettingValueInteger(136, 180,
1758
                                       (l.lim_150M_area_b_rxupper_limit) / 10.0)
1759
        rs = RadioSetting("limits.lim_150M_area_b_rxupper_limit",
1760
                          "Rcvr B 150M Rx Upper Limit (MHz)"
1761
                           "\n 136 Min 180 Max",
1762
                          RadioSettingValueInteger(136, 180,
1763
                                                   val))
1764
        limgrp.append(rs)
1765

    
1766
        val = RadioSettingValueInteger(400, 512,
1767
                                       (l.lim_450M_rxlower_limit) / 10.0)
1768
        rs = RadioSetting("limits.lim_450M_rxlower_limit",
1769
                          "450M Rx Lower Limit (MHz)"
1770
                           "\n 400 Min 512 Max",
1771
                          RadioSettingValueInteger(400, 512,
1772
                                                   val))
1773
        limgrp.append(rs)
1774

    
1775
        val = RadioSettingValueInteger(400, 512,
1776
                                       (l.lim_450M_rxupper_limit) / 10.0)
1777
        rs = RadioSetting("limits.lim_450M_rxupper_limit",
1778
                          "450M Rx Upper Limit (MHz)"
1779
                           "\n 400 Min 512 Max",
1780
                          RadioSettingValueInteger(400, 512,
1781
                                                   val))
1782
        limgrp.append(rs)
1783

    
1784
        val = RadioSettingValueInteger(350, 399,
1785
                                       (l.lim_300M_rxlower_limit) / 10.0)
1786
        rs = RadioSetting("limits.lim_300M_rxlower_limit",
1787
                          "300M Rx Lower Limit (MHz)"
1788
                           "\n 350 Min 399 Max",
1789
                          RadioSettingValueInteger(350, 399,
1790
                                                   val))
1791
        limgrp.append(rs)
1792

    
1793
        val = RadioSettingValueInteger(350, 399,
1794
                                       (l.lim_300M_rxupper_limit) / 10.0)
1795
        rs = RadioSetting("limits.lim_300M_rxupper_limit",
1796
                          "300M Rx Upper Limit (MHz)"
1797
                           "\n 350 Min 399 Max",
1798
                          RadioSettingValueInteger(350, 399,
1799
                                                   val))
1800
        limgrp.append(rs)
1801
        val = RadioSettingValueInteger(700, 986,
1802
                                       (l.lim_800M_rxlower_limit) / 10.0)
1803
        rs = RadioSetting("limits.lim_800M_rxlower_limit",
1804
                          "800M Rx Lower Limit (MHz)"
1805
                           "\n 700 Min 986 Max",
1806
                          RadioSettingValueInteger(700, 986,
1807
                                                   val))
1808
        limgrp.append(rs)
1809

    
1810
        val = RadioSettingValueInteger(700, 986,
1811
                                       (l.lim_800M_rxupper_limit) / 10.0)
1812
        rs = RadioSetting("limits.lim_800M_rxupper_limit",
1813
                          "800M Rx Upper Limit (MHz)"
1814
                           "\n 700 Min 986 Max",
1815
                          RadioSettingValueInteger(700, 986,
1816
                                                   val))
1817
        limgrp.append(rs)
1818

    
1819

    
1820
        val = RadioSettingValueInteger(219, 250,
1821
                                       (l.lim_210M_rxlower_limit) / 10.0)
1822
        rs = RadioSetting("limits.lim_210M_rxlower_limit",
1823
                          "210M Rx Lower Limit (MHz)"
1824
                           "\n 219 Min 250 Max",
1825
                          RadioSettingValueInteger(219, 250,
1826
                                                   val))
1827
        limgrp.append(rs)
1828

    
1829
        val = RadioSettingValueInteger(219, 250,
1830
                                       (l.lim_210M_rxupper_limit) / 10.0)
1831
        rs = RadioSetting("limits.lim_210M_rxupper_limit",
1832
                          "210M Rx Upper Limit (MHz)"
1833
                           "\n 219 Min 250 Max",
1834
                          RadioSettingValueInteger(219, 250,
1835
                                                   val))
1836
        limgrp.append(rs)
1837

    
1838
        return limgrp
1839

    
1840

    
1841
    def _band_tab(self, area, band):
1842
        """ Build a band tab inside a VFO/Area
1843
        """
1844
        def apply_freq(setting, lo, hi, obj):
1845
            f = freq2int(setting.value, lo, hi)
1846
            obj.freq = f/10
1847

    
1848
        def apply_offset(setting, obj):
1849
            f = freq2int(setting.value, 0, 5000000)
1850
            obj.offset = f/10
1851

    
1852
        def apply_enc(setting, obj):
1853
            t = tone2short(setting.value)
1854
            obj.encqt = t
1855

    
1856
        def apply_dec(setting, obj):
1857
            t = tone2short(setting.value)
1858
            obj.decqt = t
1859

    
1860
        if area == "a":
1861
            if band == 150:
1862
                c = self._memobj.vfo_a.band_150
1863
                lo = 108000000
1864
                hi = 180997500
1865
            elif band == 200:
1866
                c = self._memobj.vfo_a.band_200
1867
                lo = 219000000
1868
                hi = 250997500
1869
            elif band == 300:
1870
                c = self._memobj.vfo_a.band_300
1871
                lo = 350000000
1872
                hi = 399997500
1873
            elif band == 450:
1874
                c = self._memobj.vfo_a.band_450
1875
                lo = 400000000
1876
                hi = 512997500
1877
            else:   # 700
1878
                c = self._memobj.vfo_a.band_700
1879
                lo = 700000000
1880
                hi = 985997500
1881
        else:  # area 'b'
1882
            if band == 150:
1883
                c = self._memobj.vfo_b.band_150
1884
                lo = 136000000
1885
                hi = 180997500
1886
            else:  # 450
1887
                c = self._memobj.vfo_b.band_450
1888
                lo = 400000000
1889
                hi = 512997500
1890

    
1891
        prefix = "vfo_%s.band_%d" % (area, band)
1892
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
1893
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
1894
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
1895
        rs.set_apply_callback(apply_freq, lo, hi, c)
1896
        bf.append(rs)
1897

    
1898
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
1899
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
1900
        rs.set_apply_callback(apply_offset, c)
1901
        bf.append(rs)
1902

    
1903
        rs = RadioSetting(prefix + ".encqt",
1904
                          "Encode QT (Menu 17,19)",
1905
                          RadioSettingValueList(TONE_LIST,
1906
                                                short2tone(c.encqt)))
1907
        rs.set_apply_callback(apply_enc, c)
1908
        bf.append(rs)
1909

    
1910
        rs = RadioSetting(prefix + ".decqt",
1911
                          "Decode QT (Menu 16,18)",
1912
                          RadioSettingValueList(TONE_LIST,
1913
                                                short2tone(c.decqt)))
1914
        rs.set_apply_callback(apply_dec, c)
1915
        bf.append(rs)
1916

    
1917
        bf.append(RadioSetting(prefix + ".qt",
1918
                               "Mute Mode (Menu 21)",
1919
                               RadioSettingValueList(SPMUTE_LIST,
1920
                                                     SPMUTE_LIST[c.qt])))
1921
        bf.append(RadioSetting(prefix + ".scan",
1922
                               "Scan this (Menu 48)",
1923
                               RadioSettingValueBoolean(c.scan)))
1924
        bf.append(RadioSetting(prefix + ".pwr",
1925
                               "Power (Menu 5)",
1926
                               RadioSettingValueList(
1927
                                   POWER_LIST, POWER_LIST[c.pwr])))
1928
        bf.append(RadioSetting(prefix + ".mod",
1929
                               "AM Modulation (Menu 54)",
1930
                               RadioSettingValueBoolean(c.mod)))
1931
        bf.append(RadioSetting(prefix + ".fm_dev",
1932
                               "FM Deviation (Menu 4)",
1933
                               RadioSettingValueList(
1934
                                   BANDWIDTH_LIST,
1935
                                   BANDWIDTH_LIST[c.fm_dev])))
1936
        bf.append(
1937
            RadioSetting(prefix + ".shift",
1938
                         "Frequency Shift (Menu 6)",
1939
                         RadioSettingValueList(OFFSET_LIST,
1940
                                               OFFSET_LIST[c.shift])))
1941
        return bf
1942

    
1943
    def _area_tab(self, area):
1944
        """Build a VFO tab
1945
        """
1946
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
1947
            f = freq2short(setting.value, scan_lo, scan_hi)
1948
            obj.scan_st = f
1949

    
1950
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
1951
            f = freq2short(setting.value, scan_lo, scan_hi)
1952
            obj.scan_end = f
1953

    
1954
        if area == "a":
1955
            desc = "Receiver A Settings"
1956
            c = self._memobj.a_conf
1957
            scan_lo = 108000000
1958
            scan_hi = 985000000
1959
            scan_rng = self._memobj.settings.a
1960
            band_list = (150, 200, 300, 450, 700)
1961
        else:
1962
            desc = "Receiver B Settings"
1963
            c = self._memobj.b_conf
1964
            scan_lo = 136000000
1965
            scan_hi = 512000000
1966
            scan_rng = self._memobj.settings.b
1967
            band_list = (150, 450)
1968

    
1969
        prefix = "%s_conf" % area
1970
        af = RadioSettingGroup(prefix, desc)
1971
        af.append(
1972
            RadioSetting(prefix + ".w_mode",
1973
                         "Workmode",
1974
                         RadioSettingValueList(
1975
                             WORKMODE_LIST,
1976
                             WORKMODE_LIST[c.w_mode])))
1977
        af.append(RadioSetting(prefix + ".w_chan",
1978
                               "Channel",
1979
                               RadioSettingValueInteger(1, 999,
1980
                                                        c.w_chan)))
1981
        af.append(
1982
            RadioSetting(prefix + ".scan_grp",
1983
                         "Scan Group (Menu 49)",
1984
                         RadioSettingValueList(
1985
                             SCANGRP_LIST,
1986
                             SCANGRP_LIST[c.scan_grp])))
1987
        af.append(RadioSetting(prefix + ".bcl",
1988
                               "Busy Channel Lock-out (Menu 15)",
1989
                               RadioSettingValueBoolean(c.bcl)))
1990
        af.append(
1991
            RadioSetting(prefix + ".sql",
1992
                         "Squelch Level (Menu 8)",
1993
                         RadioSettingValueList(LIST_0_9,
1994
                                               LIST_0_9[c.sql])))
1995
        af.append(
1996
            RadioSetting(prefix + ".cset",
1997
                         "Call ID Group (Menu 52)",
1998
                         RadioSettingValueList(LIST_1_20,
1999
                                               LIST_1_20[c.cset])))
2000
        af.append(
2001
            RadioSetting(prefix + ".step",
2002
                         "Frequency Step (Menu 3)",
2003
                         RadioSettingValueList(
2004
                             STEP_LIST, STEP_LIST[c.step])))
2005
        af.append(
2006
            RadioSetting(prefix + ".scan_mode",
2007
                         "Scan Mode (Menu 20)",
2008
                         RadioSettingValueList(
2009
                             SCANMODE_LIST,
2010
                             SCANMODE_LIST[c.scan_mode])))
2011
        af.append(
2012
            RadioSetting(prefix + ".scan_range",
2013
                         "Scan Range (Menu 50)",
2014
                         RadioSettingValueList(
2015
                             SCANRANGE_LIST,
2016
                             SCANRANGE_LIST[c.scan_range])))
2017
        st = RadioSettingValueString(0, 15,
2018
                                     short2freq(scan_rng.scan_st))
2019
        rs = RadioSetting("settings.%s.scan_st" % area,
2020
                          "Frequency Scan Start", st)
2021
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2022
        af.append(rs)
2023

    
2024
        end = RadioSettingValueString(0, 15,
2025
                                      short2freq(scan_rng.scan_end))
2026
        rs = RadioSetting("settings.%s.scan_end" % area,
2027
                          "Frequency Scan End", end)
2028
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2029
                              scan_rng)
2030
        af.append(rs)
2031
        # Each area has its own set of bands
2032
        for band in (band_list):
2033
            af.append(self._band_tab(area, band))
2034
        return af
2035

    
2036
    def _key_tab(self):
2037
        """Build radio key/button menu
2038
        """
2039
        s = self._memobj.settings
2040
        kf = RadioSettingGroup("key_grp", "Key Settings")
2041

    
2042
        kf.append(RadioSetting("settings.pf1",
2043
                               "PF1 Key function (Menu 55)",
2044
                               RadioSettingValueList(
2045
                                   PF1KEY_LIST,
2046
                                   PF1KEY_LIST[s.pf1])))
2047
        kf.append(RadioSetting("settings.pf2",
2048
                               "PF2 Key function (Menu 56)",
2049
                               RadioSettingValueList(
2050
                                   PF2KEY_LIST,
2051
                                   PF2KEY_LIST[s.pf2])))
2052
        kf.append(RadioSetting("settings.pf3",
2053
                               "PF3 Key function (Menu 57)",
2054
                               RadioSettingValueList(
2055
                                   PF3KEY_LIST,
2056
                                   PF3KEY_LIST[s.pf3])))
2057
        return kf
2058

    
2059
    def _get_settings(self):
2060
        """Build the radio configuration settings menus
2061
        """
2062

    
2063
        core_grp = self._core_tab()
2064
        fm_grp = self._fm_tab()
2065
        area_a_grp = self._area_tab("a")
2066
        area_b_grp = self._area_tab("b")
2067
        key_grp = self._key_tab()
2068
        scan_grp = self._scan_grp()
2069
        callid_grp = self._callid_grp()
2070
        admin_grp = self._admin_tab()
2071
        rpt_grp = self._repeater_tab()
2072
        limit_grp = self._limits_tab()
2073

    
2074
        core_grp.append(key_grp)
2075
        core_grp.append(admin_grp)
2076
        core_grp.append(rpt_grp)
2077
        group = RadioSettings(core_grp,
2078
                              area_a_grp,
2079
                              area_b_grp,
2080
                              fm_grp,
2081
                              scan_grp,
2082
                              callid_grp,
2083
                              limit_grp
2084
                              )
2085
        return group
2086

    
2087
    def get_settings(self):
2088
        """ Public build out linkage between radio settings and UI
2089
        """
2090
        try:
2091
            return self._get_settings()
2092
        except Exception:
2093
            import traceback
2094
            LOG.error("Failed to parse settings: %s",
2095
                      traceback.format_exc())
2096
            return None
2097

    
2098
    def _is_freq(self, element):
2099
        """This is a hack to smoke out whether we need to do
2100
        frequency translations for otherwise innocent u16s and u32s
2101
        """
2102
        return "rxfreq" in element.get_name() or \
2103
               "txfreq" in element.get_name() or \
2104
               "scan_st" in element.get_name() or \
2105
               "scan_end" in element.get_name() or \
2106
               "offset" in element.get_name() or \
2107
               "fm_stop" in element.get_name()
2108

    
2109
    def _is_limit(self, element):
2110
        return "lower_limit" in element.get_name() or\
2111
               "upper_limit" in element.get_name()
2112

    
2113
    def set_settings(self, settings):
2114
        """ Public update radio settings via UI callback
2115
        A lot of this should be in common code....
2116
        """
2117

    
2118
        for element in settings:
2119
            if not isinstance(element, RadioSetting):
2120
                LOG.debug("set_settings: not instance %s" %
2121
                          element.get_name())
2122
                self.set_settings(element)
2123
                continue
2124
            else:
2125
                try:
2126
                    if "." in element.get_name():
2127
                        bits = element.get_name().split(".")
2128
                        obj = self._memobj
2129
                        for bit in bits[:-1]:
2130
                            # decode an array index
2131
                            if "[" in bit and "]" in bit:
2132
                                bit, index = bit.split("[", 1)
2133
                                index, junk = index.split("]", 1)
2134
                                index = int(index)
2135
                                obj = getattr(obj, bit)[index]
2136
                            else:
2137
                                obj = getattr(obj, bit)
2138
                        setting = bits[-1]
2139
                    else:
2140
                        obj = self._memobj.settings
2141
                        setting = element.get_name()
2142

    
2143
                    if element.has_apply_callback():
2144
                        LOG.debug("Using apply callback")
2145
                        element.run_apply_callback()
2146
                    else:
2147
                        LOG.debug("Setting %s = %s" %
2148
                                  (setting, element.value))
2149
                        if self._is_freq(element):
2150
                            setattr(obj, setting, int(element.value)/10)
2151
                        elif self._is_limit(element):
2152
                            setattr(obj, setting, int(element.value)*10)
2153
                        else:
2154
                            setattr(obj, setting, element.value)
2155
                except Exception, e:
2156
                    LOG.debug("set_settings: Exception with %s" %
2157
                              element.get_name())
2158
                    raise
(2-2/10)