Project

General

Profile

New Model #10067 » kguv9px b1.2.py

KG-UV9PX Beta 1.2 - Mel Terechenok, 12/23/2022 07:24 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
    u16 w_chan; // fix issue in 9D Plus -  w_chan is 2bytes
195
    u8 scan_grp;
196
    u8 bcl;
197
    u8 sql;
198
    u8 cset;
199
    u8 step;
200
    u8 scan_mode;
201
    u8 x869;
202
    u8 scan_range;
203
    u8 x86b;
204
    u8 x86c;
205
    u8 x86d;
206
    u8 x86e;
207
    u8 x86f;
208
};
209

    
210
#seekto 0x860;
211

    
212
struct area_conf a_conf;
213

    
214
#seekto 0x870;
215

    
216
struct area_conf b_conf;
217

    
218
#seekto 0x880;
219

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

    
285
} settings;
286

    
287

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

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

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

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

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

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

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

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

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

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

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

    
370

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

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

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

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

    
422

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

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

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

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

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

    
455

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

    
467

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

    
480

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

    
492

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

    
506

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

    
523

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

    
540

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

    
546
    bin2ascii = " 1234567890"
547
    cidstr = ""
548
    for i in range(0, 6):
549
        b = cid[i].get_value()
550
        # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
551
        # remap 0x00 to 0x0a
552
        if b == 0x00:
553
            b = 0x0a
554
        if b == 0xc or b == 0xf0:  # the cid EOL
555
            break
556
        if b > 0xa:
557
            raise InvalidValueError(
558
                "Caller ID code has illegal byte 0x%x" % b)
559
        cidstr += bin2ascii[b]
560
    return cidstr
561

    
562

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

    
588

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

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

    
607

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

    
625

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

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

    
638

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

    
650

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

    
665

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

    
686

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

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

    
703
    block_size = 16
704

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

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

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

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

    
730
        out += "  "
731

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

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

    
746
    return out
747

    
748

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

    
798

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1101
        return rp
1102

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

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

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

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

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

    
1150
        mem.dtcs_polarity = pt + pr
1151

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

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

    
1162
        mem = chirp_common.Memory()
1163
        mem.number = number
1164
        _valid = _mem.state
1165
        # Override Mem Valid state to handle quirky CPS New codeplug
1166
        # issue where there is a channel programmed but the CPS
1167
        # "state" value is 0xFF indicating an invalid memory
1168
        if _valid == MEM_INVALID and _mem.rxfreq != 0xFFFFFFFF and _nam != '':
1169
            _valid = MEM_VALID
1170

    
1171
        if _valid != MEM_VALID and _valid != 0 and _valid != 2 and _valid != 0x40:
1172
            # In Issue #6995 we can find _valid values of 0 and 2 in the IMG
1173
            # so these values should be treated like MEM_VALID.
1174
            # state value of 0x40 found in deleted memory but still shows in CPS
1175
            mem.empty = True
1176
            return mem
1177
        else:
1178
            mem.empty = False
1179

    
1180
        mem.freq = int(_mem.rxfreq) * 10
1181

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

    
1196
        mem.name = name2str(_nam.name)
1197

    
1198
        self._get_tone(_mem, mem)
1199

    
1200
        mem.skip = "" if bool(_mem.scan) else "S"
1201

    
1202
        mem.power = self.POWER_LEVELS[_mem.pwr]
1203
        if _mem.mod == 1:
1204
            mem.mode = "AM"
1205
        elif _mem.fm_dev == 0:
1206
            mem.mode = "FM"
1207
        else:
1208
            mem.mode = "NFM"
1209
        #  qt has no home in the UI
1210
        return mem
1211

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

    
1222
        rx_mode = tx_mode = None
1223
        rxtone = txtone = 0x0000
1224

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

    
1246
        _mem.decQT = rxtone
1247
        _mem.encQT = txtone
1248

    
1249
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1250
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1251

    
1252
    def set_memory(self, mem):
1253
        """ Public set_memory
1254
            Inverse of get_memory. Update the radio memory image
1255
            from the mem object
1256
            """
1257
        number = mem.number
1258

    
1259
        _mem = self._memobj.chan_blk[number - 1]
1260
        _nam = self._memobj.chan_name[number - 1]
1261

    
1262
        if mem.empty:
1263
# consider putting in a check for chan # that is empty but 
1264
# listed as one of the 2 working channels and change them
1265
# to channel 1 to be consistent with CPS and allow
1266
# complete deletion from radio.  Otherwise,
1267
# a deleted channel will still show on radio with no name.
1268
            #MRT implement the above working channel check
1269
            if self._memobj.a_conf.w_chan == number:
1270
                self._memobj.a_conf.w_chan = 1
1271
            if self._memobj.b_conf.w_chan == number:
1272
                self._memobj.b_conf.w_chan = 1
1273

    
1274
            _mem.set_raw("\xFF" * (_mem.size() / 8))
1275
            _nam.name = str2name("", 8, '\0', '\0')
1276
            _mem.state = MEM_INVALID
1277
            return
1278

    
1279
        _mem.rxfreq = int(mem.freq / 10)
1280
        if mem.duplex == "off":
1281
            _mem.txfreq = 0xFFFFFFFF
1282
        elif mem.duplex == "split":
1283
            _mem.txfreq = int(mem.offset / 10)
1284
        elif mem.duplex == "+":
1285
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1286
        elif mem.duplex == "-":
1287
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1288
        else:
1289
            _mem.txfreq = int(mem.freq / 10)
1290
        _mem.scan = int(mem.skip != "S")
1291
        if mem.mode == "FM":
1292
            _mem.mod = 0    # make sure forced AM is off
1293
            _mem.fm_dev = 0
1294
        elif mem.mode == "NFM":
1295
            _mem.mod = 0
1296
            _mem.fm_dev = 1
1297
        elif mem.mode == "AM":
1298
            _mem.mod = 1     # AM on
1299
            _mem.fm_dev = 1  # set NFM bandwidth
1300
        else:
1301
            _mem.mod = 0
1302
            _mem.fm_dev = 0  # Catchall default is FM
1303
        # set the tone
1304
        self._set_tone(mem, _mem)
1305
        # set the power
1306
        if mem.power:
1307
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1308
        else:
1309
            _mem.pwr = True
1310

    
1311
        # Set fields we can't access via the UI table to safe defaults
1312
        _mem.qt = 0   # mute mode to QT
1313

    
1314
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1315
        _mem.state = MEM_VALID
1316

    
1317
# Build the UI configuration tabs
1318
# the channel memory tab is built by the core.
1319
# We have no control over it
1320

    
1321
    def _core_tab(self):
1322
        """ Build Core Configuration tab
1323
        Radio settings common to all modes and areas go here.
1324
        """
1325
        s = self._memobj.settings
1326
        sm = self._memobj.screen
1327

    
1328
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1329

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

    
1443
        cf.append(RadioSetting("screen.screen_mode",
1444
                               "Screen Mode (Menu 62)",
1445
                               RadioSettingValueList(
1446
                                   SCREEN_MODE_LIST,
1447
                                   SCREEN_MODE_LIST[
1448
                                    sm.screen_mode])))
1449
        cf.append(
1450
            RadioSetting("lang",
1451
                         "Menu Language (Menu 14)"
1452
                         "\nDisabled in radio"
1453
                         "\nENGLISH Only",
1454
                         RadioSettingValueList(LANGUAGE_LIST,
1455
                                               LANGUAGE_LIST[s.lang])))
1456
        cf.append(RadioSetting("ponmsg",
1457
                               "Poweron message (Menu 40)"
1458
                               "\n Bitmap is DISABLED in Radio",
1459
                               RadioSettingValueList(
1460
                                   PONMSG_LIST, PONMSG_LIST[s.ponmsg])))
1461
        return cf
1462

    
1463
    def _repeater_tab(self):
1464
        """Repeater mode functions
1465
        """
1466
        s = self._memobj.settings
1467
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1468

    
1469
        cf.append(
1470
            RadioSetting("type_set",
1471
                         "Radio Mode (Menu 43)",
1472
                         RadioSettingValueList(
1473
                             RPTMODE_LIST,
1474
                             RPTMODE_LIST[s.type_set])))
1475
        cf.append(RadioSetting("rpt_ptt",
1476
                               "Repeater PTT (Menu 45)",
1477
                               RadioSettingValueBoolean(s.rpt_ptt)))
1478
        cf.append(RadioSetting("rpt_spk",
1479
                               "Repeater Mode Speaker (Menu 44)",
1480
                               RadioSettingValueBoolean(s.rpt_spk)))
1481
        cf.append(
1482
            RadioSetting("rpt_kpt",
1483
                         "Repeater Hold Time (Menu 46)",
1484
                         RadioSettingValueList(RPT_KPTS,
1485
                                               RPT_KPTS[s.rpt_kpt])))
1486
        cf.append(RadioSetting("rpt_rct",
1487
                               "Repeater Receipt Tone (Menu 47)",
1488
                               RadioSettingValueBoolean(s.rpt_rct)))
1489
        return cf
1490

    
1491
    def _admin_tab(self):
1492
        """Admin functions not present in radio menu...
1493
        These are admin functions not radio operation configuration
1494
        """
1495

    
1496
        def apply_cid(setting, obj):
1497
            c = str2callid(setting.value)
1498
            obj.code = c
1499

    
1500
        def apply_scc(setting, obj):
1501
            c = str2digits(setting.value)
1502
            obj.scc = c
1503

    
1504
        def apply_mode_sw(setting, obj):
1505
            pw = str2pw(setting.value)
1506
            obj.mode_sw = pw
1507
            setting.value = pw2str(obj.mode_sw)
1508

    
1509
        def apply_reset(setting, obj):
1510
            pw = str2pw(setting.value)
1511
            obj.reset = pw
1512
            setting.value = pw2str(obj.reset)
1513

    
1514
        def apply_wake(setting, obj):
1515
            obj.wake = int(setting.value)/10
1516

    
1517
        def apply_sleep(setting, obj):
1518
            obj.sleep = int(setting.value)/10
1519

    
1520
        pw = self._memobj.passwords  # admin passwords
1521
        s = self._memobj.settings
1522

    
1523
        cf = RadioSettingGroup("admin", "Admin Functions")
1524

    
1525
        cf.append(RadioSetting("menu_avail",
1526
                               "Menu available in channel mode",
1527
                               RadioSettingValueBoolean(s.menu_avail)))
1528
        mode_sw = RadioSettingValueString(0, 6,
1529
                                          pw2str(pw.mode_sw), False)
1530
        rs = RadioSetting("passwords.mode_sw",
1531
                          "Mode Switch Password", mode_sw)
1532
        rs.set_apply_callback(apply_mode_sw, pw)
1533
        cf.append(rs)
1534

    
1535
        cf.append(RadioSetting("reset_avail",
1536
                               "Radio Reset Available",
1537
                               RadioSettingValueBoolean(s.reset_avail)))
1538
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1539
        rs = RadioSetting("passwords.reset",
1540
                          "Radio Reset Password", reset)
1541
        rs.set_apply_callback(apply_reset, pw)
1542
        cf.append(rs)
1543

    
1544
        cf.append(
1545
            RadioSetting("dtmf_tx",
1546
                         "DTMF Tx Duration",
1547
                         RadioSettingValueList(DTMF_TIMES,
1548
                                               DTMF_TIMES[s.dtmf_tx])))
1549
        cid = self._memobj.my_callid
1550
        my_callid = RadioSettingValueString(3, 6,
1551
                                            callid2str(cid.code), False)
1552
        rs = RadioSetting("my_callid.code",
1553
                          "PTT Caller ID code (Menu 24)", my_callid)
1554
        rs.set_apply_callback(apply_cid, cid)
1555
        cf.append(rs)
1556

    
1557
        stun = self._memobj.stun
1558
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1559
        rs = RadioSetting("stun.scc", "Security code", st)
1560
        rs.set_apply_callback(apply_scc, stun)
1561
        cf.append(rs)
1562

    
1563
        cf.append(
1564
            RadioSetting("settings.save_m",
1565
                         "Save Mode  (Menu 2)",
1566
                         RadioSettingValueList(SAVE_MODES,
1567
                                               SAVE_MODES[s.save_m])))
1568
        for i in range(0, 4):
1569
            sm = self._memobj.save[i]
1570
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1571
            wf = RadioSetting("save[%i].wake" % i,
1572
                              "Save Mode %d Wake Time" % (i+1), wake)
1573
            wf.set_apply_callback(apply_wake, sm)
1574
            cf.append(wf)
1575

    
1576
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1577
            wf = RadioSetting("save[%i].sleep" % i,
1578
                              "Save Mode %d Sleep Time" % (i+1), slp)
1579
            wf.set_apply_callback(apply_sleep, sm)
1580
            cf.append(wf)
1581

    
1582
        _msg = str(self._memobj.display.banner).split("\0")[0]
1583
        val = RadioSettingValueString(0, 16, _msg)
1584
        val.set_mutable(True)
1585
        cf.append(RadioSetting("display.banner",
1586
                               "Display Message", val))
1587

    
1588
        _str = str(self._memobj.oemmodel.model).split("\0")[0]
1589
        val = RadioSettingValueString(0, 10, _str)
1590
        val.set_mutable(True)
1591
        cf.append(RadioSetting("oemmodel.model",
1592
                              "Custom Sub-Receiver Message", val))
1593

    
1594
        val = RadioSettingValueList(
1595
                              TDR_LIST,
1596
                              TDR_LIST[s.tdr])
1597
        val.set_mutable(True)
1598
        cf.append(RadioSetting("tdr", "TDR", val))
1599

    
1600
        val = RadioSettingValueList(
1601
                              ACTIVE_AREA_LIST,
1602
                              ACTIVE_AREA_LIST[s.act_area])
1603
        val.set_mutable(True)
1604
        cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1605

    
1606
        return cf
1607

    
1608
    def _fm_tab(self):
1609
        """FM Broadcast channels
1610
        """
1611
        def apply_fm(setting, obj):
1612
            f = freq2short(setting.value, 76000000, 108000000)
1613
            obj.fm_freq = f
1614

    
1615
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1616
        for ch in range(0, 20):
1617
            chan = self._memobj.fm_chans[ch]
1618
            freq = RadioSettingValueString(0, 20,
1619
                                           short2freq(chan.fm_freq))
1620
            rs = RadioSetting("fm_%d" % (ch + 1),
1621
                              "FM Channel %d" % (ch + 1), freq)
1622
            rs.set_apply_callback(apply_fm, chan)
1623
            fm.append(rs)
1624
        return fm
1625

    
1626
    def _scan_grp(self):
1627
        """Scan groups
1628
        """
1629
        def apply_name(setting, obj):
1630
            name = str2name(setting.value, 8, '\0', '\0')
1631
            obj.name = name
1632

    
1633
        def apply_start(setting, obj):
1634
            """Do a callback to deal with RadioSettingInteger limitation
1635
            on memory address resolution
1636
            """
1637
            obj.scan_st = int(setting.value)
1638

    
1639
        def apply_end(setting, obj):
1640
            """Do a callback to deal with RadioSettingInteger limitation
1641
            on memory address resolution
1642
            """
1643
            obj.scan_end = int(setting.value)
1644

    
1645
        sgrp = self._memobj.scn_grps
1646
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1647
        for i in range(0, 10):
1648
            s_grp = sgrp.addrs[i]
1649
            s_name = sgrp.names[i]
1650
            rs_name = RadioSettingValueString(0, 6,
1651
                                              name2str(s_name.name))
1652
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1653
                              "Group %i Name" % (i + 1), rs_name)
1654
            rs.set_apply_callback(apply_name, s_name)
1655
            scan.append(rs)
1656
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1657
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1658
                              "Starting Channel", rs_st)
1659
            rs.set_apply_callback(apply_start, s_grp)
1660
            scan.append(rs)
1661
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1662
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1663
                              "Last Channel", rs_end)
1664
            rs.set_apply_callback(apply_end, s_grp)
1665
            scan.append(rs)
1666
        return scan
1667

    
1668
    def _callid_grp(self):
1669
        """Caller IDs to be recognized by radio
1670
        This really should be a table in the UI
1671
        """
1672
        def apply_callid(setting, obj):
1673
            c = str2callid(setting.value)
1674
            obj.cid = c
1675

    
1676
        def apply_name(setting, obj):
1677
            name = str2name(setting.value, 6, '\0', '\xff')
1678
            obj.name = name
1679

    
1680
        cid = RadioSettingGroup("callids", "Caller IDs")
1681
        for i in range(0, 20):
1682
            callid = self._memobj.call_ids[i]
1683
            name = self._memobj.cid_names[i]
1684
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1685
            rs = RadioSetting("cid_names[%i].name" % i,
1686
                              "Caller ID %i Name" % (i + 1), c_name)
1687
            rs.set_apply_callback(apply_name, name)
1688
            cid.append(rs)
1689
            c_id = RadioSettingValueString(0, 6,
1690
                                           callid2str(callid.cid),
1691
                                           False)
1692
            rs = RadioSetting("call_ids[%i].cid" % i,
1693
                              "Caller ID Code", c_id)
1694
            rs.set_apply_callback(apply_callid, callid)
1695
            cid.append(rs)
1696
        return cid
1697

    
1698
    def _limits_tab(self):
1699

    
1700
        limgrp = RadioSettingGroup("limits",
1701
                                   "Freq Limits - USE AT YOUR RISK"
1702
                                   "\n Changes may violate laws, rules"
1703
                                   "\n or regulations in your region"
1704
                                   "\n upper limits include .9975 Mhz"
1705
                                   "\n Ex: 449 = 449.9975")
1706

    
1707
        l = self._memobj.limits
1708

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

    
1718
        val = RadioSettingValueInteger(136, 180,
1719
                                       (l.lim_150M_Txupper_limit) / 10.0)
1720
        rs = RadioSetting("limits.lim_150M_Txupper_limit",
1721
                          "150M Tx Upper Limit (MHz)"
1722
                          "\n 136 Min 180 Max",
1723
                          RadioSettingValueInteger(136, 180,
1724
                                                   val))
1725
        limgrp.append(rs)
1726

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

    
1736
        val = RadioSettingValueInteger(400, 512,
1737
                                       (l.lim_450M_Txupper_limit) / 10.0)
1738
        rs = RadioSetting("limits.lim_450M_Txupper_limit",
1739
                          "450M Tx Upper Limit (MHz)"
1740
                           "\n 400 Min 512 Max",
1741
                         RadioSettingValueInteger(400, 512,
1742
                                                   val))
1743
        limgrp.append(rs)
1744

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

    
1754
        val = RadioSettingValueInteger(108, 180,
1755
                                       (l.lim_150M_area_a_rxupper_limit) / 10.0)
1756
        rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
1757
                          "Rcvr A 150M Rx Upper Limit (MHz)"
1758
                           "\n 108 Min 180 Max",
1759
                          RadioSettingValueInteger(108, 180,
1760
                                                   val))
1761
        limgrp.append(rs)
1762

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

    
1772
        val = RadioSettingValueInteger(136, 180,
1773
                                       (l.lim_150M_area_b_rxupper_limit) / 10.0)
1774
        rs = RadioSetting("limits.lim_150M_area_b_rxupper_limit",
1775
                          "Rcvr B 150M Rx Upper Limit (MHz)"
1776
                           "\n 136 Min 180 Max",
1777
                          RadioSettingValueInteger(136, 180,
1778
                                                   val))
1779
        limgrp.append(rs)
1780

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

    
1790
        val = RadioSettingValueInteger(400, 512,
1791
                                       (l.lim_450M_rxupper_limit) / 10.0)
1792
        rs = RadioSetting("limits.lim_450M_rxupper_limit",
1793
                          "450M Rx Upper Limit (MHz)"
1794
                           "\n 400 Min 512 Max",
1795
                          RadioSettingValueInteger(400, 512,
1796
                                                   val))
1797
        limgrp.append(rs)
1798

    
1799
        val = RadioSettingValueInteger(350, 399,
1800
                                       (l.lim_300M_rxlower_limit) / 10.0)
1801
        rs = RadioSetting("limits.lim_300M_rxlower_limit",
1802
                          "300M Rx Lower Limit (MHz)"
1803
                           "\n 350 Min 399 Max",
1804
                          RadioSettingValueInteger(350, 399,
1805
                                                   val))
1806
        limgrp.append(rs)
1807

    
1808
        val = RadioSettingValueInteger(350, 399,
1809
                                       (l.lim_300M_rxupper_limit) / 10.0)
1810
        rs = RadioSetting("limits.lim_300M_rxupper_limit",
1811
                          "300M Rx Upper Limit (MHz)"
1812
                           "\n 350 Min 399 Max",
1813
                          RadioSettingValueInteger(350, 399,
1814
                                                   val))
1815
        limgrp.append(rs)
1816
        val = RadioSettingValueInteger(700, 986,
1817
                                       (l.lim_800M_rxlower_limit) / 10.0)
1818
        rs = RadioSetting("limits.lim_800M_rxlower_limit",
1819
                          "800M Rx Lower Limit (MHz)"
1820
                           "\n 700 Min 986 Max",
1821
                          RadioSettingValueInteger(700, 986,
1822
                                                   val))
1823
        limgrp.append(rs)
1824

    
1825
        val = RadioSettingValueInteger(700, 986,
1826
                                       (l.lim_800M_rxupper_limit) / 10.0)
1827
        rs = RadioSetting("limits.lim_800M_rxupper_limit",
1828
                          "800M Rx Upper Limit (MHz)"
1829
                           "\n 700 Min 986 Max",
1830
                          RadioSettingValueInteger(700, 986,
1831
                                                   val))
1832
        limgrp.append(rs)
1833

    
1834

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

    
1844
        val = RadioSettingValueInteger(219, 250,
1845
                                       (l.lim_210M_rxupper_limit) / 10.0)
1846
        rs = RadioSetting("limits.lim_210M_rxupper_limit",
1847
                          "210M Rx Upper Limit (MHz)"
1848
                           "\n 219 Min 250 Max",
1849
                          RadioSettingValueInteger(219, 250,
1850
                                                   val))
1851
        limgrp.append(rs)
1852

    
1853
        return limgrp
1854

    
1855

    
1856
    def _band_tab(self, area, band):
1857
        """ Build a band tab inside a VFO/Area
1858
        """
1859
        def apply_freq(setting, lo, hi, obj):
1860
            f = freq2int(setting.value, lo, hi)
1861
            obj.freq = f/10
1862

    
1863
        def apply_offset(setting, obj):
1864
            f = freq2int(setting.value, 0, 5000000)
1865
            obj.offset = f/10
1866

    
1867
        def apply_enc(setting, obj):
1868
            t = tone2short(setting.value)
1869
            obj.encqt = t
1870

    
1871
        def apply_dec(setting, obj):
1872
            t = tone2short(setting.value)
1873
            obj.decqt = t
1874

    
1875
        if area == "a":
1876
            if band == 150:
1877
                c = self._memobj.vfo_a.band_150
1878
                lo = 108000000
1879
                hi = 180997500
1880
            elif band == 200:
1881
                c = self._memobj.vfo_a.band_200
1882
                lo = 219000000
1883
                hi = 250997500
1884
            elif band == 300:
1885
                c = self._memobj.vfo_a.band_300
1886
                lo = 350000000
1887
                hi = 399997500
1888
            elif band == 450:
1889
                c = self._memobj.vfo_a.band_450
1890
                lo = 400000000
1891
                hi = 512997500
1892
            else:   # 700
1893
                c = self._memobj.vfo_a.band_700
1894
                lo = 700000000
1895
                hi = 985997500
1896
        else:  # area 'b'
1897
            if band == 150:
1898
                c = self._memobj.vfo_b.band_150
1899
                lo = 136000000
1900
                hi = 180997500
1901
            else:  # 450
1902
                c = self._memobj.vfo_b.band_450
1903
                lo = 400000000
1904
                hi = 512997500
1905

    
1906
        prefix = "vfo_%s.band_%d" % (area, band)
1907
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
1908
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
1909
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
1910
        rs.set_apply_callback(apply_freq, lo, hi, c)
1911
        bf.append(rs)
1912

    
1913
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
1914
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
1915
        rs.set_apply_callback(apply_offset, c)
1916
        bf.append(rs)
1917

    
1918
        rs = RadioSetting(prefix + ".encqt",
1919
                          "Encode QT (Menu 17,19)",
1920
                          RadioSettingValueList(TONE_LIST,
1921
                                                short2tone(c.encqt)))
1922
        rs.set_apply_callback(apply_enc, c)
1923
        bf.append(rs)
1924

    
1925
        rs = RadioSetting(prefix + ".decqt",
1926
                          "Decode QT (Menu 16,18)",
1927
                          RadioSettingValueList(TONE_LIST,
1928
                                                short2tone(c.decqt)))
1929
        rs.set_apply_callback(apply_dec, c)
1930
        bf.append(rs)
1931

    
1932
        bf.append(RadioSetting(prefix + ".qt",
1933
                               "Mute Mode (Menu 21)",
1934
                               RadioSettingValueList(SPMUTE_LIST,
1935
                                                     SPMUTE_LIST[c.qt])))
1936
        bf.append(RadioSetting(prefix + ".scan",
1937
                               "Scan this (Menu 48)",
1938
                               RadioSettingValueBoolean(c.scan)))
1939
        bf.append(RadioSetting(prefix + ".pwr",
1940
                               "Power (Menu 5)",
1941
                               RadioSettingValueList(
1942
                                   POWER_LIST, POWER_LIST[c.pwr])))
1943
        bf.append(RadioSetting(prefix + ".mod",
1944
                               "AM Modulation (Menu 54)",
1945
                               RadioSettingValueBoolean(c.mod)))
1946
        bf.append(RadioSetting(prefix + ".fm_dev",
1947
                               "FM Deviation (Menu 4)",
1948
                               RadioSettingValueList(
1949
                                   BANDWIDTH_LIST,
1950
                                   BANDWIDTH_LIST[c.fm_dev])))
1951
        bf.append(
1952
            RadioSetting(prefix + ".shift",
1953
                         "Frequency Shift (Menu 6)",
1954
                         RadioSettingValueList(OFFSET_LIST,
1955
                                               OFFSET_LIST[c.shift])))
1956
        return bf
1957

    
1958
    def _area_tab(self, area):
1959
        """Build a VFO tab
1960
        """
1961
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
1962
            f = freq2short(setting.value, scan_lo, scan_hi)
1963
            obj.scan_st = f
1964

    
1965
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
1966
            f = freq2short(setting.value, scan_lo, scan_hi)
1967
            obj.scan_end = f
1968

    
1969
        if area == "a":
1970
            desc = "Receiver A Settings"
1971
            c = self._memobj.a_conf
1972
            scan_lo = 108000000
1973
            scan_hi = 985000000
1974
            scan_rng = self._memobj.settings.a
1975
            band_list = (150, 200, 300, 450, 700)
1976
        else:
1977
            desc = "Receiver B Settings"
1978
            c = self._memobj.b_conf
1979
            scan_lo = 136000000
1980
            scan_hi = 512000000
1981
            scan_rng = self._memobj.settings.b
1982
            band_list = (150, 450)
1983

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

    
2039
        end = RadioSettingValueString(0, 15,
2040
                                      short2freq(scan_rng.scan_end))
2041
        rs = RadioSetting("settings.%s.scan_end" % area,
2042
                          "Frequency Scan End", end)
2043
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2044
                              scan_rng)
2045
        af.append(rs)
2046
        # Each area has its own set of bands
2047
        for band in (band_list):
2048
            af.append(self._band_tab(area, band))
2049
        return af
2050

    
2051
    def _key_tab(self):
2052
        """Build radio key/button menu
2053
        """
2054
        s = self._memobj.settings
2055
        kf = RadioSettingGroup("key_grp", "Key Settings")
2056

    
2057
        kf.append(RadioSetting("settings.pf1",
2058
                               "PF1 Key function (Menu 55)",
2059
                               RadioSettingValueList(
2060
                                   PF1KEY_LIST,
2061
                                   PF1KEY_LIST[s.pf1])))
2062
        kf.append(RadioSetting("settings.pf2",
2063
                               "PF2 Key function (Menu 56)",
2064
                               RadioSettingValueList(
2065
                                   PF2KEY_LIST,
2066
                                   PF2KEY_LIST[s.pf2])))
2067
        kf.append(RadioSetting("settings.pf3",
2068
                               "PF3 Key function (Menu 57)",
2069
                               RadioSettingValueList(
2070
                                   PF3KEY_LIST,
2071
                                   PF3KEY_LIST[s.pf3])))
2072
        return kf
2073

    
2074
    def _get_settings(self):
2075
        """Build the radio configuration settings menus
2076
        """
2077

    
2078
        core_grp = self._core_tab()
2079
        fm_grp = self._fm_tab()
2080
        area_a_grp = self._area_tab("a")
2081
        area_b_grp = self._area_tab("b")
2082
        key_grp = self._key_tab()
2083
        scan_grp = self._scan_grp()
2084
        callid_grp = self._callid_grp()
2085
        admin_grp = self._admin_tab()
2086
        rpt_grp = self._repeater_tab()
2087
        limit_grp = self._limits_tab()
2088

    
2089
        core_grp.append(key_grp)
2090
        core_grp.append(admin_grp)
2091
        core_grp.append(rpt_grp)
2092
        group = RadioSettings(core_grp,
2093
                              area_a_grp,
2094
                              area_b_grp,
2095
                              fm_grp,
2096
                              scan_grp,
2097
                              callid_grp,
2098
                              limit_grp
2099
                              )
2100
        return group
2101

    
2102
    def get_settings(self):
2103
        """ Public build out linkage between radio settings and UI
2104
        """
2105
        try:
2106
            return self._get_settings()
2107
        except Exception:
2108
            import traceback
2109
            LOG.error("Failed to parse settings: %s",
2110
                      traceback.format_exc())
2111
            return None
2112

    
2113
    def _is_freq(self, element):
2114
        """This is a hack to smoke out whether we need to do
2115
        frequency translations for otherwise innocent u16s and u32s
2116
        """
2117
        return "rxfreq" in element.get_name() or \
2118
               "txfreq" in element.get_name() or \
2119
               "scan_st" in element.get_name() or \
2120
               "scan_end" in element.get_name() or \
2121
               "offset" in element.get_name() or \
2122
               "fm_stop" in element.get_name()
2123

    
2124
    def _is_limit(self, element):
2125
        return "lower_limit" in element.get_name() or\
2126
               "upper_limit" in element.get_name()
2127

    
2128
    def set_settings(self, settings):
2129
        """ Public update radio settings via UI callback
2130
        A lot of this should be in common code....
2131
        """
2132

    
2133
        for element in settings:
2134
            if not isinstance(element, RadioSetting):
2135
                LOG.debug("set_settings: not instance %s" %
2136
                          element.get_name())
2137
                self.set_settings(element)
2138
                continue
2139
            else:
2140
                try:
2141
                    if "." in element.get_name():
2142
                        bits = element.get_name().split(".")
2143
                        obj = self._memobj
2144
                        for bit in bits[:-1]:
2145
                            # decode an array index
2146
                            if "[" in bit and "]" in bit:
2147
                                bit, index = bit.split("[", 1)
2148
                                index, junk = index.split("]", 1)
2149
                                index = int(index)
2150
                                obj = getattr(obj, bit)[index]
2151
                            else:
2152
                                obj = getattr(obj, bit)
2153
                        setting = bits[-1]
2154
                    else:
2155
                        obj = self._memobj.settings
2156
                        setting = element.get_name()
2157

    
2158
                    if element.has_apply_callback():
2159
                        LOG.debug("Using apply callback")
2160
                        element.run_apply_callback()
2161
                    else:
2162
                        LOG.debug("Setting %s = %s" %
2163
                                  (setting, element.value))
2164
                        if self._is_freq(element):
2165
                            setattr(obj, setting, int(element.value)/10)
2166
                        elif self._is_limit(element):
2167
                            setattr(obj, setting, int(element.value)*10)
2168
                        else:
2169
                            setattr(obj, setting, element.value)
2170
                except Exception, e:
2171
                    LOG.debug("set_settings: Exception with %s" %
2172
                              element.get_name())
2173
                    raise
(10-10/10)