Project

General

Profile

New Model #10067 » kguv9px b1.1.py

Wouxun KG-UV9PX Chirp Driver Beta 1.1 - Mel Terechenok, 12/19/2022 03:02 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
        # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
552
        # remap 0x00 to 0x0a
553
        if b == 0x00:
554
            b = 0x0a
555
        if b == 0xc or b == 0xf0:  # the cid EOL
556
            break
557
        if b > 0xa:
558
            raise InvalidValueError(
559
                "Caller ID code has illegal byte 0x%x" % b)
560
        cidstr += bin2ascii[b]
561
    return cidstr
562

    
563

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

    
589

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

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

    
608

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

    
626

    
627
def name2str(name):
628
    """ Convert a callid or scan group name to a string
629
    Deal with fixed field padding (\0 or \0xff)
630
    """
631

    
632
    namestr = ""
633
    for i in range(0, len(name)):
634
        b = ord(name[i].get_value())
635
        if b != 0 and b != 0xff:
636
            namestr += chr(b)
637
    return namestr
638

    
639

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

    
651

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

    
666

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

    
687

    
688
# Helpers to replace python2 things like confused str/byte
689

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

    
704
    block_size = 16
705

    
706
    lines = (len(data) / block_size)
707
    if (len(data) % block_size > 0):
708
        lines += 1
709

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

    
720
        if left < block_size:
721
            limit = left
722
        else:
723
            limit = block_size
724

    
725
        for j in range(0, block_size):
726
            if (j < limit):
727
                out += "%02x " % data[(block * block_size) + j]
728
            else:
729
                out += "   "
730

    
731
        out += "  "
732

    
733
        for j in range(0, block_size):
734

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

    
747
    return out
748

    
749

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

    
799

    
800
@directory.register
801
class KGUV9PXRadio(chirp_common.CloneModeRadio,
802
                      chirp_common.ExperimentalRadio):
803

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

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

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

    
852
    def _write_record(self, cmd, payload=None):
853
        """ Write a request packet to the radio.
854
        """
855

    
856
        packet = _pkt_encode(cmd, payload)
857
        self.pipe.write(packet)
858

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

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

    
881
        TODO: This could be smarter and reject a radio not actually
882
        a UV9D...
883
        """
884

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

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

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

    
937
    def sync_out(self):
938
        """ Public sync_out
939
            Upload the modified memory image into the radio.
940
            """
941

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

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

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

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

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

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

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

    
1102
        return rp
1103

    
1104
    def get_raw_memory(self, number):
1105
        return repr(self._memobj.chan_blk[number])
1106

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

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

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

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

    
1151
        mem.dtcs_polarity = pt + pr
1152

    
1153
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1154
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1155

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

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

    
1175
        mem.freq = int(_mem.rxfreq) * 10
1176

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

    
1191
        mem.name = name2str(_nam.name)
1192

    
1193
        self._get_tone(_mem, mem)
1194

    
1195
        mem.skip = "" if bool(_mem.scan) else "S"
1196

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

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

    
1217
        rx_mode = tx_mode = None
1218
        rxtone = txtone = 0x0000
1219

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

    
1241
        _mem.decQT = rxtone
1242
        _mem.encQT = txtone
1243

    
1244
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1245
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1246

    
1247
    def set_memory(self, mem):
1248
        """ Public set_memory
1249
            Inverse of get_memory. Update the radio memory image
1250
            from the mem object
1251
            """
1252
        number = mem.number
1253

    
1254
        _mem = self._memobj.chan_blk[number - 1]
1255
        _nam = self._memobj.chan_name[number - 1]
1256

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

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

    
1300
        # Set fields we can't access via the UI table to safe defaults
1301
        _mem.qt = 0   # mute mode to QT
1302

    
1303
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1304
        _mem.state = MEM_VALID
1305

    
1306
# Build the UI configuration tabs
1307
# the channel memory tab is built by the core.
1308
# We have no control over it
1309

    
1310
    def _core_tab(self):
1311
        """ Build Core Configuration tab
1312
        Radio settings common to all modes and areas go here.
1313
        """
1314
        s = self._memobj.settings
1315
        sm = self._memobj.screen
1316

    
1317
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1318

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

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

    
1452
    def _repeater_tab(self):
1453
        """Repeater mode functions
1454
        """
1455
        s = self._memobj.settings
1456
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1457

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

    
1480
    def _admin_tab(self):
1481
        """Admin functions not present in radio menu...
1482
        These are admin functions not radio operation configuration
1483
        """
1484

    
1485
        def apply_cid(setting, obj):
1486
            c = str2callid(setting.value)
1487
            obj.code = c
1488

    
1489
        def apply_scc(setting, obj):
1490
            c = str2digits(setting.value)
1491
            obj.scc = c
1492

    
1493
        def apply_mode_sw(setting, obj):
1494
            pw = str2pw(setting.value)
1495
            obj.mode_sw = pw
1496
            setting.value = pw2str(obj.mode_sw)
1497

    
1498
        def apply_reset(setting, obj):
1499
            pw = str2pw(setting.value)
1500
            obj.reset = pw
1501
            setting.value = pw2str(obj.reset)
1502

    
1503
        def apply_wake(setting, obj):
1504
            obj.wake = int(setting.value)/10
1505

    
1506
        def apply_sleep(setting, obj):
1507
            obj.sleep = int(setting.value)/10
1508

    
1509
        pw = self._memobj.passwords  # admin passwords
1510
        s = self._memobj.settings
1511

    
1512
        cf = RadioSettingGroup("admin", "Admin Functions")
1513

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

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

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

    
1546
        stun = self._memobj.stun
1547
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1548
        rs = RadioSetting("stun.scc", "Security code", st)
1549
        rs.set_apply_callback(apply_scc, stun)
1550
        cf.append(rs)
1551

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

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

    
1571
        _msg = str(self._memobj.display.banner).split("\0")[0]
1572
        val = RadioSettingValueString(0, 16, _msg)
1573
        val.set_mutable(True)
1574
        cf.append(RadioSetting("display.banner",
1575
                               "Display Message", val))
1576

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

    
1583
        val = RadioSettingValueList(
1584
                              TDR_LIST,
1585
                              TDR_LIST[s.tdr])
1586
        val.set_mutable(True)
1587
        cf.append(RadioSetting("tdr", "TDR", val))
1588

    
1589
        val = RadioSettingValueList(
1590
                              ACTIVE_AREA_LIST,
1591
                              ACTIVE_AREA_LIST[s.act_area])
1592
        val.set_mutable(True)
1593
        cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1594

    
1595
        return cf
1596

    
1597
    def _fm_tab(self):
1598
        """FM Broadcast channels
1599
        """
1600
        def apply_fm(setting, obj):
1601
            f = freq2short(setting.value, 76000000, 108000000)
1602
            obj.fm_freq = f
1603

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

    
1615
    def _scan_grp(self):
1616
        """Scan groups
1617
        """
1618
        def apply_name(setting, obj):
1619
            name = str2name(setting.value, 8, '\0', '\0')
1620
            obj.name = name
1621

    
1622
        def apply_start(setting, obj):
1623
            """Do a callback to deal with RadioSettingInteger limitation
1624
            on memory address resolution
1625
            """
1626
            obj.scan_st = int(setting.value)
1627

    
1628
        def apply_end(setting, obj):
1629
            """Do a callback to deal with RadioSettingInteger limitation
1630
            on memory address resolution
1631
            """
1632
            obj.scan_end = int(setting.value)
1633

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

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

    
1665
        def apply_name(setting, obj):
1666
            name = str2name(setting.value, 6, '\0', '\xff')
1667
            obj.name = name
1668

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

    
1687
    def _limits_tab(self):
1688

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

    
1696
        l = self._memobj.limits
1697

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1823

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

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

    
1842
        return limgrp
1843

    
1844

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

    
1852
        def apply_offset(setting, obj):
1853
            f = freq2int(setting.value, 0, 5000000)
1854
            obj.offset = f/10
1855

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

    
1860
        def apply_dec(setting, obj):
1861
            t = tone2short(setting.value)
1862
            obj.decqt = t
1863

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

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

    
1902
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
1903
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
1904
        rs.set_apply_callback(apply_offset, c)
1905
        bf.append(rs)
1906

    
1907
        rs = RadioSetting(prefix + ".encqt",
1908
                          "Encode QT (Menu 17,19)",
1909
                          RadioSettingValueList(TONE_LIST,
1910
                                                short2tone(c.encqt)))
1911
        rs.set_apply_callback(apply_enc, c)
1912
        bf.append(rs)
1913

    
1914
        rs = RadioSetting(prefix + ".decqt",
1915
                          "Decode QT (Menu 16,18)",
1916
                          RadioSettingValueList(TONE_LIST,
1917
                                                short2tone(c.decqt)))
1918
        rs.set_apply_callback(apply_dec, c)
1919
        bf.append(rs)
1920

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

    
1947
    def _area_tab(self, area):
1948
        """Build a VFO tab
1949
        """
1950
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
1951
            f = freq2short(setting.value, scan_lo, scan_hi)
1952
            obj.scan_st = f
1953

    
1954
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
1955
            f = freq2short(setting.value, scan_lo, scan_hi)
1956
            obj.scan_end = f
1957

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

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

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

    
2040
    def _key_tab(self):
2041
        """Build radio key/button menu
2042
        """
2043
        s = self._memobj.settings
2044
        kf = RadioSettingGroup("key_grp", "Key Settings")
2045

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

    
2063
    def _get_settings(self):
2064
        """Build the radio configuration settings menus
2065
        """
2066

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

    
2078
        core_grp.append(key_grp)
2079
        core_grp.append(admin_grp)
2080
        core_grp.append(rpt_grp)
2081
        group = RadioSettings(core_grp,
2082
                              area_a_grp,
2083
                              area_b_grp,
2084
                              fm_grp,
2085
                              scan_grp,
2086
                              callid_grp,
2087
                              limit_grp
2088
                              )
2089
        return group
2090

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

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

    
2113
    def _is_limit(self, element):
2114
        return "lower_limit" in element.get_name() or\
2115
               "upper_limit" in element.get_name()
2116

    
2117
    def set_settings(self, settings):
2118
        """ Public update radio settings via UI callback
2119
        A lot of this should be in common code....
2120
        """
2121

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

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