Project

General

Profile

New Model #3509 » add_kguv9dplus.patch

HG patch file for driver based on current upstream 'tip' - James Lieb, 12/03/2018 06:08 PM

View differences:

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

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

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

  
34
LOG = logging.getLogger(__name__)
35

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

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

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

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

  
88

  
89
MEM_VALID = 0xfc
90
MEM_INVALID = 0xff
91

  
92
# Radio memory map. This matches the reads/writes above.
93
# structure elements whose name starts with x are currently unidentified
94

  
95
_MEM_FORMAT02 = """
96
#seekto 0x40;
97

  
98
struct {
99
    char reset[6];
100
    char x46[2];
101
    char mode_sw[6];
102
    char x4e;
103
}  passwords;
104

  
105
#seekto 0x740;
106

  
107
struct {
108
    u16 fm_freq;
109
} fm_chans[20];
110

  
111
// each band has its own configuration, essentially its default params
112

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

  
133
#seekto 0x780;
134

  
135
struct {
136
    struct vfo band_150;
137
    struct vfo band_450;
138
} vfo_b;
139

  
140
#seekto 0x800;
141

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

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

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

  
172
#seekto 0x860;
173

  
174
struct area_conf a_conf;
175

  
176
#seekto 0x870;
177

  
178
struct area_conf b_conf;
179

  
180
#seekto 0x880;
181

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

  
248

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

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

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

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

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

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

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

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

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

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

  
326

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

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

  
351
    data = bytearray()
352
    data.append(0x7d) # tag that marks the beginning of the packet
353
    data.append(op)
354
    data.append(0xff) # 0xff is from app to radio
355
    # calc checksum from op to end
356
    cksum = op + 0xff
357
    if (payload):
358
        data.append(len(payload))
359
        cksum += len(payload)
360
        for byte in payload:
361
            cksum += byte
362
            data.append(byte)
363
    else:
364
        data.append(0x00)
365

  
366
    data.append(cksum & 0xf) # Yea, this is a 4 bit cksum (also known as a bug)
367

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

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

  
382
    # we don't care about data[0]. It is always 0x7d and not included in checksum
383
    op = data[1]
384
    direction = data[2]
385
    bytecount = data[3]
386

  
387
    # First un-obfuscate the payload and cksum
388
    payload = bytearray()
389
    xorbits = 0x52
390
    for i, byte in enumerate(data[4:]):
391
        payload.append(xorbits ^ byte)
392
        xorbits = byte
393

  
394
    # Calculate the checksum starting with the 3 bytes of the header
395
    cksum = op + direction + bytecount
396
    for byte in payload[:-1]:
397
        cksum += byte
398
    cksum_match = (cksum & 0xf) == payload[-1] # yes, a 4 bit cksum to match the encode
399
    if (not cksum_match):
400
        LOG.debug("Checksum missmatch: %x != %x; " % (cksum, payload[-1]))
401
    return (cksum_match, op, payload[:-1])
402

  
403
# UI callbacks to process input for mapping UI fields to memory cells
404

  
405
def freq2int(val, min, max):
406
    """Convert a frequency as a string to a u32. Units is Hz
407
    """
408
    _freq = chirp_common.parse_freq(str(val))
409
    if _freq > max or _freq < min:
410
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
411
                                (chirp_common.format_freq(_freq),
412
                                 chirp_common.format_freq(min),
413
                                 chirp_common.format_freq(max)))
414
    return _freq
415

  
416
def int2freq(freq):
417
    """
418
    Convert a u32 frequency to a string for UI data entry/display
419
    This is stored in the radio as units of 10Hz which we compensate to Hz.
420
    A value of -1 indicates <no freqency>, i.e. unused channel.
421
    """
422
    if (int(freq) > 0):
423
        f = chirp_common.format_freq(freq)
424
        return f
425
    else:
426
        return ""
427
    
428
def freq2short(val, min, max):
429
    """Convert a frequency as a string to a u16 which is units of 10KHz
430
    """
431
    _freq = chirp_common.parse_freq(str(val))
432
    if _freq > max or _freq < min:
433
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
434
                                (chirp_common.format_freq(_freq),
435
                                 chirp_common.format_freq(min),
436
                                 chirp_common.format_freq(max)))
437
    return _freq/100000 & 0xFFFF
438

  
439
def short2freq(freq):
440
    """
441
       Convert a short frequency to a string for UI data entry/display
442
       This is stored in the radio as units of 10KHz which we compensate to Hz.
443
       A value of -1 indicates <no frequency>, i.e. unused channel.
444
    """
445
    if (int(freq) > 0):
446
        f = chirp_common.format_freq(freq *100000)
447
        return f
448
    else:
449
        return ""
450

  
451
def tone2short(t):
452
    """Convert a string tone or DCS to an encoded u16
453
    """
454
    tone = str(t)
455
    if tone == "----":
456
        u16tone = 0x0000
457
    elif tone[0] == 'D': # This is a DCS code
458
        c = tone[1: -1]
459
        code = int(c, 8)
460
        if tone[-1] == 'I':
461
            code |= 0x4000
462
        u16tone = code | 0x8000
463
    else:              # This is an analog CTCSS
464
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff # strip the '.'
465
    return u16tone
466

  
467
def short2tone(tone):
468
    """ Map a binary CTCSS/DCS to a string name for the tone
469
    """
470
    if tone == 0 or tone == 0xffff:
471
        ret = "----"
472
    else:
473
        code = tone & 0x3fff
474
        if tone & 0x8000: # This is a DCS
475
            if tone & 0x4000: #This is an inverse code
476
                ret = "D%0.3oI" % code
477
            else:
478
                ret = "D%0.3oN" % code
479
        else:   # Just plain old analog CTCSS
480
            ret = "%4.1f" % (code / 10.0)
481
    return ret
482
    
483
def callid2str(cid):
484
    """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
485
       One digit (binary) per byte, terminated with '0xc'
486
    """
487

  
488
    bin2ascii = " 1234567890"
489
    cidstr = ""
490
    for i in range(0, 6):
491
        b = cid[i].get_value()
492
        if b == 0xc: # the cid EOL
493
            break;
494
        if b == 0 or b > 0xa:
495
            raise InvalidValueError("Caller ID code has illegal byte 0x%x" % b)
496
        cidstr += bin2ascii[b]
497
    return cidstr
498

  
499
def str2callid(val):
500
    """ Convert caller id strings from callid2str.
501
    """
502
    ascii2bin = "0123456789"
503
    s = str(val).strip()
504
    if len(s) < 3 or len(s) > 6:
505
        raise InvalidValueError("Caller ID must be at least 3 and no more than 6 digits")
506
    if s[0] == '0':
507
        raise InvalidValueError("First digit of a Caller ID cannot be a zero '0'")
508
    blk = bytearray()
509
    for c in s:
510
        if c not in ascii2bin:
511
            raise InvalidValueError("Caller ID must be all digits 0x%x" % c)
512
        b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
513
        blk.append(b)
514
    if len(blk) < 6:
515
        blk.append(0xc) # EOL a short ID
516
    if len(blk) < 6:
517
        for i in range(0, (6 - len(blk))):
518
            blk.append(0xf0)
519
    return blk
520

  
521
def digits2str(digits, padding=' ', width=6):
522
    """Convert a password or SCC digit string to a string
523
    Passwords are expanded to and must be 6 chars. Fill them with '0'
524
    """
525

  
526
    bin2ascii = "0123456789"
527
    digitsstr = ""
528
    for i in range(0, 6):
529
        b = digits[i].get_value()
530
        if b == 0xc: # the digits EOL
531
            break;
532
        if b >= 0xa:
533
            raise InvalidValueError("Value has illegal byte 0x%x" % ord(b))
534
        digitsstr += bin2ascii[b]
535
    digitsstr = digitsstr.ljust(width, padding)
536
    return digitsstr
537

  
538
def str2digits(val):
539
    """ Callback for edited strings from digits2str.
540
    """
541
    ascii2bin = " 0123456789"
542
    s = str(val).strip()
543
    if len(s) < 3 or len(s) > 6:
544
        raise InvalidValueError("Value must be at least 3 and no more than 6 digits")
545
    blk = bytearray()
546
    for c in s:
547
        if c not in ascii2bin:
548
            raise InvalidValueError("Value must be all digits 0x%x" % c)
549
        blk.append(int(c))
550
    for i in range(len(blk), 6):
551
        blk.append(0xc) # EOL a short ID
552
    return blk
553

  
554
def name2str(name):
555
    """ Convert a callid or scan group name to a string
556
    Deal with fixed field padding (\0 or \0xff)
557
    """
558

  
559
    namestr = ""
560
    for i in range(0, len(name)):
561
        b = ord(name[i].get_value())
562
        if b != 0 and b != 0xff:
563
            namestr += chr(b)
564
    return namestr
565

  
566
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
567
    """ Convert a string to a name. A name is a 6 element bytearray
568
    with ascii chars.
569
    """
570
    val = str(val).rstrip(' \t\r\n\0\0xff')
571
    if len(val) == 0:
572
        name = "".ljust(size, emptyfill)
573
    else:
574
        name = val.ljust(size, fillchar)
575
    return name
576

  
577
def pw2str(pw):
578
    """Convert a password string (6 digits) to a string
579
    Passwords must be 6 digits. If it is shorter, pad right with '0'
580
    """
581
    pwstr = ""
582
    ascii2bin = "0123456789"
583
    for i in range(0, len(pw)):
584
        b = pw[i].get_value()
585
        if b not in ascii2bin:
586
            raise InvalidValueError("Value must be digits 0-9")
587
        pwstr += b
588
    pwstr = pwstr.ljust(6, '0')
589
    return pwstr
590
                
591
def str2pw(val):
592
    """Store a password from UI to memory obj
593
    If we clear the password (make it go away), change the
594
    empty string to '000000' since the radio must have *something*
595
    Also, fill a < 6 digit pw with 0's
596
    """
597
    ascii2bin = "0123456789"
598
    val = str(val).rstrip(' \t\r\n\0\0xff')
599
    if len(val) == 0: # a null password
600
        val = "000000"
601
    for i in range(0, len(val)):
602
        b = val[i]
603
        if b not in ascii2bin:
604
            raise InvalidValueError("Value must be digits 0-9")
605
    if len(val) == 0:
606
        pw = "".ljust(6, '\0')
607
    else:
608
        pw = val.ljust(6, '0')
609
    return pw
610
        
611
    
612
# Helpers to replace python2 things like confused str/byte
613

  
614
def _hex_print(data, addrfmt=None):
615
    """Return a hexdump-like encoding of @data
616
    We expect data to be a bytearray, not a string.
617
    Expanded from borrowed code to use the first 2 bytes as the address
618
    per comm packet format.
619
    """
620
    if addrfmt is None:
621
        addrfmt = '%(addr)03i'
622
        addr = 0
623
    else: # assume first 2 bytes are address
624
        a = struct.unpack(">H", data[0:2])
625
        addr = a[0]
626
        data = data[2:]
627

  
628
    block_size = 16
629

  
630
    lines = (len(data) / block_size)
631
    if (len(data) % block_size > 0):
632
        lines += 1
633

  
634
    out = ""
635
    left = len(data)
636
    for block in range(0, lines):
637
        addr += block * block_size
638
        try:
639
            out += addrfmt % locals()
640
        except (OverflowError, ValueError, TypeError, KeyError):
641
            out += "%03i" % addr
642
        out += ': '
643

  
644
        if left < block_size:
645
            limit = left
646
        else:
647
            limit = block_size
648

  
649
        for j in range(0, block_size):
650
            if (j < limit):
651
                out += "%02x " % data[(block * block_size) + j]
652
            else:
653
                out += "   "
654

  
655
        out += "  "
656

  
657
        for j in range(0, block_size):
658

  
659
            if (j < limit):
660
                _byte = data[(block * block_size) + j]
661
                if _byte >= 0x20 and _byte < 0x7F:
662
                    out += "%s" % chr(_byte)
663
                else:
664
                    out += "."
665
            else:
666
                out += " "
667
        out += "\n"
668
        if (left > block_size):
669
            left -= block_size
670

  
671
    return out
672

  
673
# Useful UI lists
674

  
675
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
676
S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]]
677
STEP_LIST = [str(x)+"kHz" for x in STEPS]
678
ROGER_LIST = ["Off", "Begin", "End", "Both"]
679
TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)]
680
TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 10)]
681
BANDWIDTH_LIST = ["Wide", "Narrow"]
682
LANGUAGE_LIST = ["English", "Chinese"]
683
PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"]
684
PF2KEY_LIST = ["OFF", "Scan", "Second", "lamp", "SDF-DIR", "K-lamp"]
685
PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS","SF-TX"]
686
WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"]
687
BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + ["Always On"]
688
SAVE_MODES = ["Off", "1", "2", "3", "4"]
689
LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"]
690
APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)]
691
OFFSET_LIST = ["none", "+", "-"]
692
PONMSG_LIST = ["Battery Volts", "Bitmap"]
693
SPMUTE_LIST = ["QT", "QT*T", "QT&T"]
694
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"]
695
DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)]
696
PTTID_LIST = ["Off", "Begin", "End", "Both"]
697
ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)]
698
VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1,11)]
699
VOX_DLYS = ["Off"] + ["%ds" %t for t in range(1, 5)]
700
RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)]
701
LIST_1_5 = ["%s" % x for x in range(1,6)]
702
LIST_0_9 = ["%s" % x for x in range(0, 10)]
703
LIST_1_20 = ["%s" % x for x in range(1, 21)]
704
LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
705
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
706
SCANMODE_LIST = ["TO", "CO", "SE"]
707
SCANRANGE_LIST = ["Current band", "freq range", "ALL"]
708
SCQT_LIST = ["Decoder", "Encoder", "Both"]
709
S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"]
710
POWER_LIST = ["Low", "Med", "High"]
711
RPTMODE_LIST = ["Radio", "One direction Repeater", "Two direction repeater"]
712
TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \
713
            ["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \
714
            ["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES]
715

  
716
@directory.register
717
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
718
                  chirp_common.ExperimentalRadio):
719

  
720
    """Wouxun KG-UV9D Plus"""
721
    VENDOR = "Wouxun"
722
    MODEL = "KG-UV9D Plus"
723
    _model = "KG-UV9D"
724
    _rev = "00"  # default rev for the radio I know about...
725
    _file_ident = "kg-uv9d"
726
    BAUD_RATE = 19200
727
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
728
                    chirp_common.PowerLevel("M", watts=2),
729
                    chirp_common.PowerLevel("H", watts=5)]
730
    _mmap = ""
731

  
732
    def _read_record(self):
733
        """ Read and validate the header of a radio reply.
734
        A record is a formatted byte stream as follows:
735
            0x7D   All records start with this
736
            opcode This is in the set of legal commands. The radio reply matches the request
737
            dir    This is the direction, 0xFF to the radio, 0x00 from the radio.
738
            cnt    Count of bytes in payload (not including the trailing checksum byte)
739
        """
740

  
741
        data = bytearray(self.pipe.read(4)) # first get the header and validate it
742
        if (len(data) < 4):
743
            raise errors.RadioError('Radio did not respond')
744
        if (data[0] != 0x7D):
745
            raise errors.RadioError('Radio reply garbled (%02x)' % data[0])
746
        if (data[1] not in cmd_name):
747
            raise errors.RadioError("Unrecognized opcode (%02x)" % data[1])
748
        if (data[2] != 0x00):
749
            raise errors.RadioError("Direction incorrect. Got (%02x)" % data[2])
750
        payload_len = data[3]
751
        data.extend(self.pipe.read(payload_len + 1)) # don't forget to read the checksum byte
752
        if (len(data) != (payload_len + 5)):  # we got a short read
753
            raise errors.RadioError("Radio reply wrong size. Wanted %d, got %d" %
754
                                    ((payload_len + 1), (len(data) - 4)))
755
        return _pkt_decode(data)
756

  
757
    def _write_record(self, cmd, payload = None):
758
        """ Write a request packet to the radio.
759
        """
760

  
761
        packet = _pkt_encode(cmd, payload)
762
        self.pipe.write(packet)
763
        
764
        
765
    @classmethod
766
    def match_model(cls, filedata, filename):
767
        """Look for bits in the file image and see if it looks like ours...
768
        TODO: there is a bunch of rubbish between 0x50 and 0x160 that is still an known unknown
769
        """
770
        return cls._file_ident in filedata[0x51:0x59].lower()
771

  
772
    def _identify(self):
773
        """ Identify the radio
774
        The ident block identifies the radio and its capabilities. This block is always 78 bytes
775
        The rev == '01' is the base radio and '02' seems to be the '-Plus' version
776
        I don't really trust the content after the model and revision. One would assume this is
777
        pretty much constant data but I have seen differences between my radio and the dump named
778
        KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first five bands match the OEM windows
779
        app except the 350-400 band. The OOB trace has the 700MHz band different.
780

  
781
        TODO: This could be smarter and reject a radio not actually a UV9D...
782
        """
783

  
784
        for _i in range(0, 10):  # retry 10 times just in case we get junk until sync'd up
785
            self._write_record(CMD_IDENT)
786
            chksum_match, op, _resp = self._read_record()
787
            if len(_resp) == 0:
788
                raise Exception("Radio not responding")
789
            if len(_resp) != 74:
790
                LOG.error("Expected and IDENT reply of 78 bytes. Got (%d)" % len(_resp))
791
                continue
792
            if not chksum_match:
793
                LOG.error("Checksum error: retrying ident...")
794
                time.sleep(0.100)
795
                continue
796
            if op != CMD_IDENT:
797
                LOG.error("Expected IDENT reply. Got (%02x)" % op)
798
                continue
799
            LOG.debug("Got:\n%s" % _hex_print(_resp))
800
            (mod, rev) = struct.unpack(">7s2s", _resp[0:9])
801
            LOG.debug("Model %s, rev %s" % (mod, rev))
802
            if mod == self._model:
803
                self._rev = rev
804
                return
805
            else:
806
                raise Exception("Unable to identify radio")
807
        raise Exception("All retries to identify failed")
808

  
809
    def process_mmap(self):
810
        if self._rev == "02" or self._rev == "00":
811
            self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
812
        else: ## this is where you elif the other variants and non-Plus radios
813
            raise errors.RadioError("Unrecognized model variation (%s). No memory map for it" %
814
                                    self._rev)
815
        
816
    def sync_in(self):
817
        """ Public sync_in
818
            Download contents of the radio. Throw errors back to the core if the radio does
819
            not respond.
820
            """
821
        try:
822
            self._identify()
823
            self._mmap = self._do_download()
824
            self._write_record(CMD_HANGUP)
825
        except errors.RadioError:
826
            raise
827
        except Exception, e:
828
            LOG.exception('Unknown error during download process')
829
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
830
        self.process_mmap()
831

  
832
    def sync_out(self):
833
        """ Public sync_out
834
            Upload the modified memory image into the radio.
835
            """
836
        
837
        try:
838
            self._identify()
839
            self._do_upload()
840
            self._write_record(CMD_HANGUP)
841
        except errors.RadioError:
842
            raise
843
        except Exception, e:
844
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
845
        return
846

  
847
    def _do_download(self):
848
        """ Read the whole of radio memory in 64 byte chunks.
849
        We load the config space followed by loading memory channels. The radio seems
850
        to be a "clone" type and the memory channels are actually within the config space.
851
        There are separate commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
852
        these seem to be a hack that can only do 4 channels at a time. Since the radio only
853
        supports 999, (can only support 3 chars in the display UI?) although the vendors
854
        app reads 1000 channels, it hacks back to config writes (CMD_WCONF) for the last 3
855
        channels and names. We keep it simple and just read the whole thing even though
856
        the vendor app doesn't. Channels are separate in their app simply
857
        because the radio protocol has read/write commands to access it. What they do
858
        is simply marshal the frequency+mode bits in 4 channel chunks followed by a separate
859
        chunk of for names. In config space, they are two separate arrays 1..999. Given that
860
        this space is not a multiple of 4, there is hackery on upload to do the writes to
861
        config space. See upload for this.
862
        """
863

  
864
        mem = bytearray(0x8000)  # The radio's memory map is 32k
865
        for addr in range(0, 0x8000, 64):
866
            req = bytearray(struct.pack(">HB", addr, 64))
867
            self._write_record(CMD_RCONF, req)
868
            chksum_match, op, resp = self._read_record()
869
            if not chksum_match:
870
                LOG.debug(_hex_print(resp))
871
                raise Exception("Checksum error while reading configuration (0x%x)" % addr)
872
            pa = struct.unpack(">H", resp[0:2])
873
            pkt_addr = pa[0]
874
            payload = resp[2:]
875
            if op != CMD_RCONF or addr != pkt_addr:
876
                raise Exception("Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
877
                                (addr, op, pkt_addr))
878
            LOG.debug("Config read (0x%x):\n%s" % (addr, _hex_print(resp, '0x%(addr)04x')))
879
            for i in range(0, len(payload) - 1):
880
                mem[addr + i] = payload[i]
881
            if self.status_fn:
882
                status = chirp_common.Status()
883
                status.cur = addr
884
                status.max = 0x8000
885
                status.msg = "Cloning from radio"
886
                self.status_fn(status)
887
        strmem = "".join([chr(x) for x in mem])
888
        return memmap.MemoryMap(strmem)
889
    
890
    def _do_upload(self):
891
        """Walk through the config map and write updated records to the radio. The
892
        config map contains only the regions we know about. We don't use the channel
893
        memory commands to avoid the hackery of using config write commands to fill
894
        in the last 3 channel memory and names slots. As we discover other useful goodies
895
        in the map, we can add more slots...
896
        """
897
        for ar,size, count in config_map:
898
            for addr in range(ar, ar +(size*count), size):
899
                req = bytearray(struct.pack(">H", addr))
900
                req.extend(self.get_mmap()[addr:addr + size])
901
                self._write_record(CMD_WCONF, req)
902
                LOG.debug("Config write (0x%x):\n%s" % (addr, _hex_print(req)))
903
                chksum_match, op, ack = self._read_record()
904
                LOG.debug("Config write ack [%x]\n%s" % (addr, _hex_print(ack)))
905
                a = struct.unpack(">H", ack) # big endian int()...
906
                ack = a[0]
907
                if not chksum_match or op != CMD_WCONF or addr != ack:
908
                    msg = ""
909
                    if not chksum_match:
910
                        msg += "Checksum err, "
911
                    if op != CMD_WCONF:
912
                        msg += "cmd mismatch %x != %x, " % (op, CMD_WCONF)
913
                    if addr != ack:
914
                        msg += "ack error %x != %x, " % (addr, ack)
915
                    raise Exception("Radio did not ack block: %s error" % msg)
916
                if self.status_fn:
917
                    status = chirp_common.Status()
918
                    status.cur = addr
919
                    status.max = 0x8000
920
                    status.msg = "Update radio"
921
                    self.status_fn(status)
922
        
923
    def get_features(self):
924
        """ Public get_features
925
            Return the features of this radio once we have identified it and
926
            gotten its bits
927
            """
928
        rf = chirp_common.RadioFeatures()
929
        rf.has_settings = True
930
        rf.has_ctone = True
931
        rf.has_rx_dtcs = True
932
        rf.has_cross = True
933
        rf.has_tuning_step = False
934
        rf.has_bank = False
935
        rf.can_odd_split = True
936
        rf.valid_skips = ["", "S"]
937
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
938
        rf.valid_cross_modes = [
939
            "Tone->Tone",
940
            "Tone->DTCS",
941
            "DTCS->Tone",
942
            "DTCS->",
943
            "->Tone",
944
            "->DTCS",
945
            "DTCS->DTCS",
946
        ]
947
        rf.valid_modes = ["FM", "NFM", "AM"]
948
        rf.valid_power_levels = self.POWER_LEVELS
949
        rf.valid_name_length = 8
950
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
951
        rf.valid_bands = [(108000000, 136000000),  # Aircraft (receive only AM)
952
                          (136000000, 180000000),  # supports 2m
953
                          (230000000, 250000000),
954
                          (350000000, 400000000),
955
                          (400000000, 520000000),  # supports 70cm
956
                          (700000000, 985000000)]
957
        rf.valid_characters = chirp_common.CHARSET_ASCII
958
        rf.memory_bounds = (1, 999)  # 999 memories
959
        return rf
960

  
961
    @classmethod
962
    def get_prompts(cls):
963
        rp = chirp_common.RadioPrompts()
964
        rp.experimental = ("This radio driver is currently under development. "
965
                           "There are no known issues with it, but you should "
966
                           "proceed with caution.")
967
        return rp
968

  
969
    def get_raw_memory(self, number):
970
        return repr(self._memobj.chan_blk[number])
971

  
972
    def _get_tone(self, _mem, mem):
973
        """Decode both the encode and decode CTSS/DCS codes from the memory channel
974
        and stuff them into the UI memory channel row.
975
        """
976
        txtone = short2tone(_mem.encQT)
977
        rxtone = short2tone(_mem.decQT)
978
        polarity = "NN"
979
        
980
        if txtone == "----":
981
            txmode = ""
982
        elif txtone[0] == "D":
983
            mem.tx_dtcs = int(txtone[1,-1])
984
            polarity[0] = txtone[-1]
985
            txmode = "DTCS"
986
        else:
987
            mem.rtone = float(txtone)
988
            txmode = "Tone"
989

  
990
        if rxtone == "----":
991
            rxmode = ""
992
        elif rxtone[0] == "D":
993
            mem.rx_dtcs = int(rxtone[1,4])
994
            polarity[1] = rxtone[-1]
995
            rxmode = "DTCS"
996
        else:
997
            mem.ctone = float(rxtone)
998
            rxmode = "Tone"
999

  
1000
        if txmode == "Tone" and len(rxmode) == 0:
1001
            mem.tmode = "Tone"
1002
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
1003
            mem.tmode = "TSQL"
1004
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
1005
            mem.tmode = "DTCS"
1006
        elif (len(rxmode) + len(txmode)) > 0:
1007
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1008

  
1009
        mem.dtcs_polarity = polarity
1010
        
1011
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1012
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1013

  
1014
    def get_memory(self, number):
1015
        """ Public get_memory
1016
            Return the channel memory referenced by number to the UI.
1017
        """
1018
        _mem = self._memobj.chan_blk[number - 1]
1019
        _nam = self._memobj.chan_name[number - 1]
1020

  
1021
        mem = chirp_common.Memory()
1022
        mem.number = number
1023
        _valid = _mem.state
1024
        if _valid != MEM_VALID:
1025
            mem.empty = True
1026
            return mem
1027
        else:
1028
            mem.empty = False
1029

  
1030
        mem.freq = int(_mem.rxfreq) * 10
1031

  
1032
        if _mem.txfreq == 0xFFFFFFFF:
1033
            # TX freq not set
1034
            mem.duplex = "off"
1035
            mem.offset = 0
1036
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1037
            mem.duplex = ""
1038
            mem.offset = 0
1039
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1040
            mem.duplex = "split"
1041
            mem.offset = int(_mem.txfreq) * 10
1042
        else:
1043
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1044
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1045

  
1046
        for char in _nam.name:
1047
            if char != 0:
1048
                mem.name += chr(char)
1049
        mem.name = mem.name.rstrip()
1050

  
1051
        self._get_tone(_mem, mem)
1052

  
1053
        mem.skip = "" if bool(_mem.scan) else "S"
1054

  
1055
        mem.power = self.POWER_LEVELS[_mem.pwr]
1056
        if _mem.mod == 1:
1057
            mem.mode = "AM"
1058
        elif _mem.fm_dev == 0:
1059
            mem.mode = "FM"
1060
        else:
1061
            mem.mode = "NFM"
1062
        #  qt has no home in the UI
1063
        return mem
1064

  
1065
    def _set_tone(self, mem, _mem):
1066
        """Update the memory channel block CTCC/DCS tones from the UI fields
1067
        """
1068
        def _set_dcs(code, pol):
1069
            val = int("%i" % code, 8) + 0x8000
1070
            if pol == "R":
1071
                val += 0x4000
1072
            return val
1073

  
1074
        rx_mode = tx_mode = None
1075
        rxtone = txtone = 0x0000
1076

  
1077
        if mem.tmode == "Tone":
1078
            tx_mode = "Tone"
1079
            txtone = int(mem.rtone * 10)
1080
        elif mem.tmode == "TSQL":
1081
            rx_mode = tx_mode = "Tone"
1082
            rxtone = txtone = int(mem.ctone * 10)
1083
        elif mem.tmode == "DTCS":
1084
            tx_mode = rx_mode = "DTCS"
1085
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1086
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1087
        elif mem.tmode == "Cross":
1088
            tx_mode, rx_mode = mem.cross_mode.split("->")
1089
            if tx_mode == "DTCS":
1090
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1091
            elif tx_mode == "Tone":
1092
                txtone = int(mem.rtone * 10)
1093
            if rx_mode == "DTCS":
1094
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1095
            elif rx_mode == "Tone":
1096
                rxtone = int(mem.ctone * 10)
1097

  
1098
        _mem.decQT = rxtone
1099
        _mem.encQT = txtone
1100

  
1101
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1102
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1103

  
1104
    def set_memory(self, mem):
1105
        """ Public set_memory
1106
            Inverse of get_memory. Update the radio memory image from the
1107
            mem object
1108
            """
1109
        number = mem.number
1110

  
1111
        _mem = self._memobj.chan_blk[number - 1]
1112
        _nam = self._memobj.chan_name[number - 1]
1113

  
1114
        if mem.empty:
1115
            _mem.set_raw("\xFF" * (_mem.size() / 8))
1116
            _nam.name = str2name("", 8, '\0', '\0')
1117
            _mem.state = MEM_INVALID
1118
            return
1119

  
1120
        _mem.rxfreq = int(mem.freq / 10)
1121
        if mem.duplex == "off":
1122
            _mem.txfreq = 0xFFFFFFFF
1123
        elif mem.duplex == "split":
1124
            _mem.txfreq = int(mem.offset / 10)
1125
        elif mem.duplex == "+":
1126
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1127
        elif mem.duplex == "-":
1128
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1129
        else:
1130
            _mem.txfreq = int(mem.freq / 10)
1131
        _mem.scan = int(mem.skip != "S")
1132
        if mem.mode == "FM":
1133
            _mem.mod == 0    # make sure forced AM is off
1134
            _mem.fm_dev = 0
1135
        elif mem.mode == "NFM":
1136
            _mem.mod = 0
1137
            _mem.fm_dev = 1
1138
        elif mem.mode == "AM":
1139
            _mem.mod = 1     # AM on
1140
            _mem.fm_dev = 0  # default to wide FM bandwidth
1141
        else:
1142
            _mem.mod = 0
1143
            _mem.fm_dev = 0  # default is wide FM
1144
        _mem.fm_dev = int(mem.mode != "FM")
1145
        # set the tone
1146
        self._set_tone(mem, _mem)
1147
        # set the power
1148
        if mem.power:
1149
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1150
        else:
1151
            _mem.pwr = True
1152
            
1153
        # Set fields we can't access via the UI table to safe defaults
1154
        _mem.qt = 0   # mute mode to QT
1155

  
1156
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1157
        _mem.state = MEM_VALID
1158

  
1159
## Build the UI configuration tabs
1160
## the channel memory tab is built by the core. We have no control over it
1161

  
1162
    def _core_tab(self):
1163
        """ Build Core Configuration tab
1164
        Radio settings common to all modes and areas go here.
1165
        """
1166
        s = self._memobj.settings
1167
        
1168
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1169
    
1170
        cf.append(RadioSetting("auto_am",
1171
                               "Auto detect AM(53)",
1172
                               RadioSettingValueBoolean(s.auto_am)))
1173
        cf.append(RadioSetting("qt_sw",
1174
                               "Scan tone detect(59)",
1175
                               RadioSettingValueBoolean(s.qt_sw)))
1176
        cf.append(RadioSetting("s_mute",
1177
                               "SubFreq Mute(60)",
1178
                               RadioSettingValueList(S_MUTE_LIST,
1179
                                                     S_MUTE_LIST[s.s_mute])))
1180
        cf.append(RadioSetting("tot",
1181
                               "Transmit timeout Timer(10)",
1182
                               RadioSettingValueList(TIMEOUT_LIST,
1183
                                                     TIMEOUT_LIST[s.tot])))
1184
        cf.append(RadioSetting("toa",
1185
                               "Transmit Timeout Alarm(11)",
1186
                               RadioSettingValueList(TOA_LIST, TOA_LIST[s.toa])))
1187
        cf.append(RadioSetting("ptt_id",
1188
                               "PTT Caller ID mode(23)",
1189
                               RadioSettingValueList(PTTID_LIST,
1190
                                                     PTTID_LIST[s.ptt_id])))
1191
        cf.append(RadioSetting("id_dly",
1192
                               "Caller ID Delay time(25)",
1193
                               RadioSettingValueList(ID_DLY_LIST,
1194
                                                     ID_DLY_LIST[s.id_dly])))
1195
        cf.append(RadioSetting("voice_sw",
1196
                               "Voice Guide(12)",
1197
                               RadioSettingValueBoolean(s.voice_sw)))
1198
        cf.append(RadioSetting("beep",
1199
                               "Keypad Beep(13)",
1200
                               RadioSettingValueBoolean(s.beep)))
1201
        cf.append(RadioSetting("s_tone",
1202
                               "Side Tone(36)",
1203
                               RadioSettingValueList(S_TONES,
1204
                                                     S_TONES[s.s_tone])))
1205
        cf.append(RadioSetting("ring_time",
1206
                               "Ring Time(26)",
1207
                               RadioSettingValueList(LIST_OFF_10,
1208
                                                     LIST_OFF_10[s.ring_time])))
1209
        cf.append(RadioSetting("roger",
1210
                               "Roger Beep(9)",
1211
                               RadioSettingValueList(ROGER_LIST, 
1212
                                                     ROGER_LIST[s.roger])))
1213
        cf.append(RadioSetting("blcdsw",
1214
                               "Backlight(41)",
1215
                               RadioSettingValueBoolean(s.blcdsw)))
1216
        cf.append(RadioSetting("abr",
1217
                               "Auto Backlight Time(1)",
1218
                               RadioSettingValueList(BACKLIGHT_LIST,
1219
                                                     BACKLIGHT_LIST[s.abr])))
1220
        cf.append(RadioSetting("abr_lvl",
1221
                               "Backlight Brightness(27)",
1222
                               RadioSettingValueList(LIST_1_5,
1223
                                                     LIST_1_5[s.abr_lvl])))
1224
        cf.append(RadioSetting("lock",
1225
                               "Keypad Lock",
1226
                               RadioSettingValueBoolean(s.lock)))
1227
        cf.append(RadioSetting("lock_m",
1228
                               "Keypad Lock Mode(35)",
1229
                               RadioSettingValueList(LOCK_MODES,
1230
                                                     LOCK_MODES[s.lock_m])))
1231
        cf.append(RadioSetting("auto_lk",
1232
                               "Keypad Autolock(34)",
1233
                               RadioSettingValueBoolean(s.auto_lk)))
1234
        cf.append(RadioSetting("prich_sw",
1235
                               "Priority Channel Scan(33)",
1236
                               RadioSettingValueBoolean(s.prich_sw)))
1237
        cf.append(RadioSetting("pri_ch",
1238
                               "Priority Channel(32)",
1239
                               RadioSettingValueInteger(1, 999, s.pri_ch)))
1240
        cf.append(RadioSetting("dtmf_st",
1241
                               "DTMF Sidetone(22)",
1242
                               RadioSettingValueList(DTMFST_LIST,
1243
                                                     DTMFST_LIST[s.dtmf_st])))
1244
        cf.append(RadioSetting("sc_qt",
1245
                               "Scan QT Save Mode(38)",
1246
                               RadioSettingValueList(SCQT_LIST,
1247
                                                     SCQT_LIST[s.sc_qt])))
1248
        cf.append(RadioSetting("apo_tmr",
1249
                               "Automatic Power-off(39)",
1250
                               RadioSettingValueList(APO_TIMES,
1251
                                                     APO_TIMES[s.apo_tmr])))
1252
        cf.append(RadioSetting("vox_grd",
1253
                               "VOX level(7)", # VOX "guard" is really VOX trigger audio level
1254
                               RadioSettingValueList(VOX_GRDS,
1255
                                                     VOX_GRDS[s.vox_grd])))
1256
        cf.append(RadioSetting("vox_dly",
1257
                               "VOX Delay(37)",
1258
                               RadioSettingValueList(VOX_DLYS,
1259
                                                     VOX_DLYS[s.vox_dly])))
1260
        cf.append(RadioSetting("lang",
1261
                               "Menu Language(14)",
1262
                               RadioSettingValueList(LANGUAGE_LIST,
1263
                                                     LANGUAGE_LIST[s.lang])))
1264
        cf.append(RadioSetting("ponmsg",
1265
                               "Poweron message(40)",
1266
                               RadioSettingValueList(
1267
                                   PONMSG_LIST, PONMSG_LIST[s.ponmsg])))
1268
        cf.append(RadioSetting("bledsw",
1269
                               "Receive LED(42)",
1270
                               RadioSettingValueBoolean(s.bledsw)))
1271
        return cf
1272

  
1273
    def _repeater_tab(self):
1274
        """Repeater mode functions
1275
        """
1276
        s = self._memobj.settings
1277
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1278

  
1279
        cf.append(RadioSetting("type_set",
1280
                               "Radio Mode(43)",
1281
                               RadioSettingValueList(RPTMODE_LIST,
1282
                                                     RPTMODE_LIST[s.type_set])))
1283
        cf.append(RadioSetting("rpt_ptt",
1284
                               "Repeater PTT(45)",
1285
                               RadioSettingValueBoolean(s.rpt_ptt)))
1286
        cf.append(RadioSetting("rpt_spk",
1287
                               "Repeater Mode Speaker(44)",
1288
                               RadioSettingValueBoolean(s.rpt_spk)))
1289
        cf.append(RadioSetting("rpt_kpt",
1290
                               "Repeater Hold Time(46)",
1291
                               RadioSettingValueList(RPT_KPTS,
1292
                                                     RPT_KPTS[s.rpt_kpt])))
1293
        cf.append(RadioSetting("rpt_rct",
1294
                               "Repeater Receipt Tone(47)",
1295
                               RadioSettingValueBoolean(s.rpt_rct)))
1296
        return cf
1297

  
1298
    def _admin_tab(self):
1299
        """Admin functions not present in radio menu...
1300
        These are admin functions not radio operation configuration
1301
        """
1302

  
1303
        def apply_cid(setting, obj):
1304
            c = str2callid(setting.value)
1305
            obj.code = c
1306

  
1307
        def apply_scc(setting, obj):
1308
            c = str2digits(setting.value)
1309
            obj.scc = c
1310

  
1311
        def apply_mode_sw(setting, obj):
1312
            pw = str2pw(setting.value)
1313
            obj.mode_sw = pw
1314
            setting.value = pw2str(obj.mode_sw)
1315
                
1316
        def apply_reset(setting, obj):
1317
            pw = str2pw(setting.value)
1318
            obj.reset = pw
1319
            setting.value = pw2str(obj.reset)
1320

  
1321
        def apply_wake(setting, obj):
1322
            obj.wake = int(setting.value)/10
1323

  
1324
        def apply_sleep(setting, obj):
1325
            obj.sleep = int(setting.value)/10
1326
                
1327
        pw = self._memobj.passwords  # admin passwords
1328
        s = self._memobj.settings
1329

  
1330
        cf = RadioSettingGroup("admin", "Admin Functions")
1331

  
1332
        cf.append(RadioSetting("menu_avail",
1333
                               "Menu available in channel mode",
1334
                               RadioSettingValueBoolean(s.menu_avail)))
1335
        mode_sw = RadioSettingValueString(0, 6, pw2str(pw.mode_sw), False)
1336
        rs = RadioSetting("passwords.mode_sw", "Mode Switch Password", mode_sw)
1337
        rs.set_apply_callback(apply_mode_sw, pw)
1338
        cf.append(rs)
1339

  
1340
        cf.append(RadioSetting("reset_avail",
1341
                               "Radio Reset Available",
1342
                               RadioSettingValueBoolean(s.reset_avail)))
1343
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1344
        rs = RadioSetting("passwords.reset", "Radio Reset Password", reset)
1345
        rs.set_apply_callback(apply_reset, pw)
1346
        cf.append(rs)
1347
        
1348
        cf.append(RadioSetting("dtmf_tx",
1349
                               "DTMF Tx Duration",
1350
                               RadioSettingValueList(DTMF_TIMES,
1351
                                                     DTMF_TIMES[s.dtmf_tx])))
1352
        cid = self._memobj.my_callid
1353
        my_callid = RadioSettingValueString(3, 6, callid2str(cid.code), False)
1354
        rs = RadioSetting("my_callid.code", "PTT Caller ID code(24)", my_callid)
1355
        rs.set_apply_callback(apply_cid, cid)
1356
        cf.append(rs)
1357

  
1358
        stun = self._memobj.stun
1359
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1360
        rs = RadioSetting("stun.scc", "Security code", st)
1361
        rs.set_apply_callback(apply_scc, stun)
1362
        cf.append(rs)
1363
        
1364
        cf.append(RadioSetting("settings.save_m",
1365
                               "Save Mode (2)",
1366
                               RadioSettingValueList(SAVE_MODES,
1367
                                                     SAVE_MODES[s.save_m])))
1368
        for i in range(0,4):
1369
            sm = self._memobj.save[i]
1370
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1371
            wf = RadioSetting("save[%i].wake" % i, "Save Mode %d Wake Time" % (i+1), wake)
1372
            wf.set_apply_callback(apply_wake, sm)
1373
            cf.append(wf)
1374
            
1375
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1376
            wf = RadioSetting("save[%i].sleep" % i, "Save Mode %d Sleep Time" % (i+1), slp)
1377
            wf.set_apply_callback(apply_sleep, sm)
1378
            cf.append(wf)
1379

  
1380
        _msg = str(self._memobj.display.banner).split("\0")[0]
1381
        val = RadioSettingValueString(0, 16, _msg)
1382
        val.set_mutable(True)
1383
        cf.append(RadioSetting("display.banner", "Display Message", val))
1384
        return cf
1385
    
1386
    def _fm_tab(self):
1387
        """FM Broadcast channels
1388
        """
1389
        def apply_fm(setting, obj):
1390
            f = freq2short(setting.value, 76000000, 108000000)
1391
            obj.fm_freq = f
1392
            
1393
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1394
        for ch in range(0,20):
1395
            chan = self._memobj.fm_chans[ch]
1396
            freq = RadioSettingValueString(0,20, short2freq(chan.fm_freq))
1397
            rs = RadioSetting("fm_%d" % (ch + 1),
1398
                                   "FM Channel %d" % (ch + 1), freq)
1399
            rs.set_apply_callback(apply_fm, chan)
1400
            fm.append(rs)
1401
        return fm
1402

  
1403
    def _scan_grp(self):
1404
        """Scan groups
1405
        """
1406
        def apply_name(setting, obj):
1407
            name = str2name(setting.value, 8, '\0', '\0')
1408
            obj.name = name
1409

  
1410
        def apply_start(setting, obj):
1411
            """Do a callback to deal with RadioSettingInteger limitation
1412
            on memory address resolution
1413
            """
1414
            obj.scan_st = int(setting.value)
1415

  
1416
        def apply_end(setting, obj):
1417
            """Do a callback to deal with RadioSettingInteger limitation
1418
            on memory address resolution
1419
            """
1420
            obj.scan_end = int(setting.value)
1421

  
1422
        sgrp = self._memobj.scn_grps
1423
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1424
        for i in range(0, 10):
1425
            s_grp = sgrp.addrs[i]
1426
            s_name = sgrp.names[i]
1427
            rs_name = RadioSettingValueString(0, 8, name2str(s_name.name))
1428
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1429
                              "Group %i Name" % (i + 1), rs_name)
1430
            rs.set_apply_callback(apply_name, s_name)
1431
            scan.append(rs)
1432
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1433
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1434
                              "Starting Channel", rs_st)
1435
            rs.set_apply_callback(apply_start, s_grp)
1436
            scan.append(rs)
1437
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1438
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1439
                              "Last Channel", rs_end)
1440
            rs.set_apply_callback(apply_end, s_grp)
1441
            scan.append(rs)
1442
        return scan
1443

  
1444
    def _callid_grp(self):
1445
        """Caller IDs to be recognized by radio
1446
        This really should be a table in the UI
1447
        """
1448
        def apply_callid(setting, obj):
1449
            c = str2callid(setting.value)
1450
            obj.cid = c
1451

  
1452
        def apply_name(setting, obj):
1453
            name = str2name(setting.value, 6, '\0', '\xff')
1454
            obj.name = name
1455
        
1456
        cid = RadioSettingGroup("callids", "Caller IDs")
1457
        for i in range(0, 20):
1458
            callid = self._memobj.call_ids[i]
1459
            name = self._memobj.cid_names[i]
1460
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1461
            rs = RadioSetting("cid_names[%i].name" % i,
... This diff was truncated because it exceeds the maximum size that can be displayed.
(5-5/11)