Project

General

Profile

New Model #6895 » kguv9dplus with 9K.py

Modified to support 8.33K step of UV9K radios - Mel Terechenok, 03/29/2023 04:10 PM

 
1
# Copyright 2022 Mel Terechenok <melvin.terechenok@gmail.com>
2
# Updated Driver to support Wouxon KG-UV9PX
3
# 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-UV9D Plus 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, 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
    (0x60,   20, 1),    # RX frequency limits
67
    (0x74,    8, 1),    # TX frequency limits
68
    (0x740,  40, 1),    # FM chan 1-20
69
    (0x780,  16, 1),    # vfo-b-150
70
    (0x790,  16, 1),    # vfo-b-450
71
    (0x800,  16, 1),    # vfo-a-150
72
    (0x810,  16, 1),    # vfo-a-450
73
    (0x820,  16, 1),    # vfo-a-300
74
    (0x830,  16, 1),    # vfo-a-700
75
    (0x840,  16, 1),    # vfo-a-200
76
    (0x860,  16, 1),    # area-a-conf
77
    (0x870,  16, 1),    # area-b-conf
78
    (0x880,  16, 1),    # radio conf 0
79
    (0x890,  16, 1),    # radio conf 1
80
    (0x8a0,  16, 1),    # radio conf 2
81
    (0x8b0,  16, 1),    # radio conf 3
82
    (0x8c0,  16, 1),    # PTT-ANI
83
    (0x8d0,  16, 1),    # SCC
84
    (0x8e0,  16, 1),    # power save
85
    (0x8f0,  16, 1),    # Display banner
86
    (0x940,  64, 2),    # Scan groups and names
87
    (0xa00,  64, 249),  # Memory Channels 1-996
88
    (0x4840, 48, 1),    # Memory Channels 997-999
89
    (0x4900, 32, 249),  # Memory Names    1-996
90
    (0x6820, 24, 1),    # Memory Names    997-999
91
    (0x7400, 64, 5),    # CALL-ID 1-20, names 1-20
92
    )
93

    
94
config_map2 = (          # map address, write size, write count
95
    (0x40,   16, 1),    # Passwords
96
    (0x50,   10, 1),    # OEM Display Name
97
    (0x60,   20, 1),    # Rx Freq Limits Area A
98
    (0x74,   8,  1),    # TX Frequency Limits 150M and 450M
99
    (0x7c,   4,  1),    # Rx 150M Freq Limits Area B
100
    #   (0x80,   8,  1),    # unknown Freq limits
101
    (0x740,  40, 1),    # FM chan 1-20
102
    (0x780,  16, 1),    # vfo-b-150
103
    (0x790,  16, 1),    # vfo-b-450
104
    (0x800,  16, 1),    # vfo-a-150
105
    (0x810,  16, 1),    # vfo-a-450
106
    (0x820,  16, 1),    # vfo-a-300
107
    (0x830,  16, 1),    # vfo-a-700
108
    (0x840,  16, 1),    # vfo-a-200
109
    (0x860,  16, 1),    # area-a-conf
110
    (0x870,  16, 1),    # area-b-conf
111
    (0x880,  16, 1),    # radio conf 0
112
    (0x890,  16, 1),    # radio conf 1
113
    (0x8a0,  16, 1),    # radio conf 2
114
    (0x8b0,  16, 1),    # radio conf 3
115
    (0x8c0,  16, 1),    # PTT-ANI
116
    (0x8d0,  16, 1),    # SCC
117
    (0x8e0,  16, 1),    # power save
118
    (0x8f0,  16, 1),    # Display banner
119
    (0x940,  64, 2),    # Scan groups and names
120
    (0xa00,  64, 249),  # Memory Channels 1-996
121
    (0x4840, 48, 1),    # Memory Channels 997-999
122
    (0x4900, 32, 249),  # Memory Names    1-996
123
    (0x6820, 24, 1),    # Memory Names    997-999
124
    (0x7400, 64, 5),    # CALL-ID 1-20, names 1-20
125
    (0x7600,  1, 1)     # Screen Mode
126
    )
127

    
128
MEM_VALID = 0xfc
129
MEM_INVALID = 0xff
130

    
131
# In Issue #6995 we can find _valid values of 0 and 2 in the IMG
132
# so these values should be treated like MEM_VALID.
133
# state value of 0x40 found in deleted memory - still shows in CPS
134
VALID_MEM_VALUES = [MEM_VALID, 0x00, 0x02, 0x40, 0x3D]
135
INVALID_MEM_VALUES = [MEM_INVALID]
136
# Radio memory map. This matches the reads/writes above.
137
# structure elements whose name starts with x are currently unidentified
138

    
139
_MEM_FORMAT02 = """
140
#seekto 0x40;
141

    
142
struct {
143
    char reset[6];
144
    char x46[2];
145
    char mode_sw[6];
146
    char x4e;
147
}  passwords;
148

    
149
#seekto 0x60;
150

    
151
struct freq_limit {
152
    u16 start;
153
    u16 stop;
154
};
155

    
156
struct {
157
    struct freq_limit band_150;
158
    struct freq_limit band_450;
159
    struct freq_limit band_300;
160
    struct freq_limit band_700;
161
    struct freq_limit band_200;
162
} rx_freq_limits;
163

    
164
struct {
165
    struct freq_limit band_150;
166
    struct freq_limit band_450;
167
} tx_freq_limits;
168

    
169
#seekto 0x740;
170

    
171
struct {
172
    u16 fm_freq;
173
} fm_chans[20];
174

    
175
// each band has its own configuration, essentially its default params
176

    
177
struct vfo {
178
    u32 freq;
179
    u32 offset;
180
    u16 encqt;
181
    u16 decqt;
182
    u8  bit7_4:3,
183
        qt:3,
184
        bit1_0:2;
185
    u8  bit7:1,
186
        scan:1,
187
        bit5:1,
188
        pwr:2,
189
        mod:1,
190
        fm_dev:2;
191
    u8  pad2:6,
192
        shift:2;
193
    u8  zeros;
194
};
195

    
196
#seekto 0x780;
197

    
198
struct {
199
    struct vfo band_150;
200
    struct vfo band_450;
201
} vfo_b;
202

    
203
#seekto 0x800;
204

    
205
struct {
206
    struct vfo band_150;
207
    struct vfo band_450;
208
    struct vfo band_300;
209
    struct vfo band_700;
210
    struct vfo band_200;
211
} vfo_a;
212

    
213
// There are two independent radios, aka areas (as described
214
// in the manual as the upper and lower portions of the display...
215

    
216
struct area_conf {
217
    u8 w_mode;
218
    u16 w_chan;
219
    u8 scan_grp;
220
    u8 bcl;
221
    u8 sql;
222
    u8 cset;
223
    u8 step;
224
    u8 scan_mode;
225
    u8 x869;
226
    u8 scan_range;
227
    u8 x86b;
228
    u8 x86c;
229
    u8 x86d;
230
    u8 x86e;
231
    u8 x86f;
232
};
233

    
234
#seekto 0x860;
235

    
236
struct area_conf a_conf;
237

    
238
#seekto 0x870;
239

    
240
struct area_conf b_conf;
241

    
242
#seekto 0x880;
243

    
244
struct {
245
    u8 menu_avail;
246
    u8 reset_avail;
247
    u8 x882;
248
    u8 x883;
249
    u8 lang;
250
    u8 x885;
251
    u8 beep;
252
    u8 auto_am;
253
    u8 qt_sw;
254
    u8 lock;
255
    u8 x88a;
256
    u8 pf1;
257
    u8 pf2;
258
    u8 pf3;
259
    u8 s_mute;
260
    u8 type_set;
261
    u8 tot;
262
    u8 toa;
263
    u8 ptt_id;
264
    u8 x893;
265
    u8 id_dly;
266
    u8 x895;
267
    u8 voice_sw;
268
    u8 s_tone;
269
    u8 abr_lvl;
270
    u8 ring_time;
271
    u8 roger;
272
    u8 x89b;
273
    u8 abr;
274
    u8 save_m;
275
    u8 lock_m;
276
    u8 auto_lk;
277
    u8 rpt_ptt;
278
    u8 rpt_spk;
279
    u8 rpt_rct;
280
    u8 prich_sw;
281
    u16 pri_ch;
282
    u8 x8a6;
283
    u8 x8a7;
284
    u8 dtmf_st;
285
    u8 dtmf_tx;
286
    u8 x8aa;
287
    u8 sc_qt;
288
    u8 apo_tmr;
289
    u8 vox_grd;
290
    u8 vox_dly;
291
    u8 rpt_kpt;
292
    struct {
293
        u16 scan_st;
294
        u16 scan_end;
295
    } a;
296
    struct {
297
        u16 scan_st;
298
        u16 scan_end;
299
    } b;
300
    u8 x8b8;
301
    u8 x8b9;
302
    u8 x8ba;
303
    u8 ponmsg;
304
    u8 blcdsw;
305
    u8 bledsw;
306
    u8 x8be;
307
    u8 x8bf;
308
} settings;
309

    
310

    
311
#seekto 0x8c0;
312
struct {
313
    u8 code[6];
314
    char x8c6[10];
315
} my_callid;
316

    
317
#seekto 0x8d0;
318
struct {
319
    u8 scc[6];
320
    char x8d6[10];
321
} stun;
322

    
323
#seekto 0x8e0;
324
struct {
325
    u16 wake;
326
    u16 sleep;
327
} save[4];
328

    
329
#seekto 0x8f0;
330
struct {
331
    char banner[16];
332
} display;
333

    
334
#seekto 0x940;
335
struct {
336
    struct {
337
        i16 scan_st;
338
        i16 scan_end;
339
    } addrs[10];
340
    u8 x0968[8];
341
    struct {
342
        char name[8];
343
    } names[10];
344
} scn_grps;
345

    
346
// this array of structs is marshalled via the R/WCHAN commands
347
#seekto 0xa00;
348
struct {
349
    u32 rxfreq;
350
    u32 txfreq;
351
    u16 encQT;
352
    u16 decQT;
353
    u8  bit7_5:3,  // all ones
354
        qt:3,
355
        bit1_0:2;
356
    u8  bit7:1,
357
        scan:1,
358
        bit5:1,
359
        pwr:2,
360
        mod:1,
361
        fm_dev:2;
362
    u8  state;
363
    u8  c3;
364
} chan_blk[999];
365

    
366
// nobody really sees this. It is marshalled with chan_blk
367
// in 4 entry chunks
368
#seekto 0x4900;
369

    
370
// Tracks with the index of  chan_blk[]
371
struct {
372
    char name[8];
373
} chan_name[999];
374

    
375
#seekto 0x7400;
376
struct {
377
    u8 cid[6];
378
    u8 pad[2];
379
}call_ids[20];
380

    
381
// This array tracks with the index of call_ids[]
382
struct {
383
    char name[6];
384
    char pad[2];
385
} cid_names[20];
386
    """
387

    
388
_MEM_FORMAT_9PX = """
389
#seekto 0x40;
390

    
391
struct {
392
    char reset[6];
393
    char x46[2];
394
    char mode_sw[6];
395
    char x4e;
396
}  passwords;
397

    
398
#seekto 0x50;
399
struct {
400
    char model[10];
401
} oemmodel;
402

    
403
#seekto 0x60;
404
struct {
405
    u16 lim_150M_area_a_rxlower_limit; // 0x60
406
    u16 lim_150M_area_a_rxupper_limit;
407
    u16 lim_450M_rxlower_limit;
408
    u16 lim_450M_rxupper_limit;
409
    u16 lim_300M_rxlower_limit;
410
    u16 lim_300M_rxupper_limit;
411
    u16 lim_800M_rxlower_limit;
412
    u16 lim_800M_rxupper_limit;
413
    u16 lim_210M_rxlower_limit;
414
    u16 lim_210M_rxupper_limit;
415
    u16 lim_150M_Txlower_limit;
416
    u16 lim_150M_Txupper_limit;
417
    u16 lim_450M_Txlower_limit;
418
    u16 lim_450M_Txupper_limit;
419
    u16 lim_150M_area_b_rxlower_limit;
420
    u16 lim_150M_area_b_rxupper_limit;
421
    u16 unknown_lower_limit;
422
    u16 unknown_upper_limit;
423
    u16 unknown2_lower_limit;
424
    u16 unknown2_upper_limit;
425
}  limits;
426

    
427
#seekto 0x740;
428

    
429
struct {
430
    u16 fm_freq;
431
} fm_chans[20];
432

    
433
// each band has its own configuration, essentially its default params
434

    
435
struct vfo {
436
    u32 freq;
437
    u32 offset;
438
    u16 encqt;
439
    u16 decqt;
440
    u8  bit7_4:3,
441
        qt:3,
442
        bit1_0:2;
443
    u8  bit7:1,
444
        scan:1,
445
        bit5:1,
446
        pwr:2,
447
        mod:1,
448
        fm_dev:2;
449
    u8  pad2:6,
450
        shift:2;
451
    u8  zeros;
452
};
453

    
454
#seekto 0x780;
455

    
456
struct {
457
    struct vfo band_150;
458
    struct vfo band_450;
459
} vfo_b;
460

    
461
#seekto 0x800;
462

    
463
struct {
464
    struct vfo band_150;
465
    struct vfo band_450;
466
    struct vfo band_300;
467
    struct vfo band_700;
468
    struct vfo band_200;
469
} vfo_a;
470

    
471
// There are two independent radios, aka areas (as described
472
// in the manual as the upper and lower portions of the display...
473

    
474
struct area_conf {
475
    u8 w_mode;
476
    u16 w_chan; // fix issue in 9D Plus -  w_chan is 2bytes
477
    u8 scan_grp;
478
    u8 bcl;
479
    u8 sql;
480
    u8 cset;
481
    u8 step;
482
    u8 scan_mode;
483
    u8 x869;
484
    u8 scan_range;
485
    u8 x86b;
486
    u8 x86c;
487
    u8 x86d;
488
    u8 x86e;
489
    u8 x86f;
490
};
491

    
492
#seekto 0x860;
493

    
494
struct area_conf a_conf;
495

    
496
#seekto 0x870;
497

    
498
struct area_conf b_conf;
499

    
500
#seekto 0x880;
501

    
502
struct {
503
    u8 menu_avail;
504
    u8 reset_avail;
505
    u8 act_area;
506
    u8 tdr;
507
    u8 lang;
508
    u8 x885;
509
    u8 beep;
510
    u8 auto_am;
511
    u8 qt_sw;
512
    u8 lock;
513
    u8 x88a;
514
    u8 pf1;
515
    u8 pf2;
516
    u8 pf3;
517
    u8 s_mute;
518
    u8 type_set;
519
    u8 tot;
520
    u8 toa;
521
    u8 ptt_id;
522
    u8 x893;
523
    u8 id_dly;
524
    u8 x895;
525
    u8 voice_sw;
526
    u8 s_tone;
527
    u8 abr_lvl;
528
    u8 ring_time;
529
    u8 roger;
530
    u8 x89b;
531
    u8 abr;
532
    u8 save_m;
533
    u8 lock_m;
534
    u8 auto_lk;
535
    u8 rpt_ptt;
536
    u8 rpt_spk;
537
    u8 rpt_rct;
538
    u8 prich_sw;
539
    u16 pri_ch;
540
    u8 x8a6;
541
    u8 x8a7;
542
    u8 dtmf_st;
543
    u8 dtmf_tx;
544
    u8 x8aa;
545
    u8 sc_qt;
546
    u8 apo_tmr;
547
    u8 vox_grd;
548
    u8 vox_dly;
549
    u8 rpt_kpt;
550
    struct {
551
        u16 scan_st;
552
        u16 scan_end;
553
    } a;
554
    struct {
555
        u16 scan_st;
556
        u16 scan_end;
557
    } b;
558
    u8 x8b8;
559
    u8 x8b9;
560
    u8 x8ba;
561
    u8 ponmsg;
562
    u8 blcdsw;
563
    u8 bledsw;
564
    u8 x8be;
565
    u8 x8bf;
566

    
567
} settings;
568

    
569

    
570
#seekto 0x8c0;
571
struct {
572
    u8 code[6];
573
    char x8c6[10];
574
} my_callid;
575

    
576
#seekto 0x8d0;
577
struct {
578
    u8 scc[6];
579
    char x8d6[10];
580
} stun;
581

    
582
#seekto 0x8e0;
583
struct {
584
    u16 wake;
585
    u16 sleep;
586
} save[4];
587

    
588
#seekto 0x8f0;
589
struct {
590
    char banner[16];
591
} display;
592

    
593
#seekto 0x940;
594
struct {
595
    struct {
596
        i16 scan_st;
597
        i16 scan_end;
598
    } addrs[10];
599
    u8 x0968[8];
600
    struct {
601
        char name[8];
602
    } names[10];
603
} scn_grps;
604

    
605
// this array of structs is marshalled via the R/WCHAN commands
606
#seekto 0xa00;
607
struct {
608
    u32 rxfreq;
609
    u32 txfreq;
610
    u16 encQT;
611
    u16 decQT;
612
    u8  bit7_5:3,  // all ones
613
        qt:3,
614
        bit1_0:2;
615
    u8  bit7:1,
616
        scan:1,
617
        bit5:1,
618
        pwr:2,
619
        mod:1,
620
        fm_dev:2;
621
    u8  state;
622
    u8  c3;
623
} chan_blk[999];
624

    
625
// nobody really sees this. It is marshalled with chan_blk
626
// in 4 entry chunks
627
#seekto 0x4900;
628

    
629
// Tracks with the index of  chan_blk[]
630
struct {
631
    char name[8];
632
} chan_name[999];
633

    
634
#seekto 0x7400;
635
struct {
636
    u8 cid[6];
637
    u8 pad[2];
638
}call_ids[20];
639

    
640
// This array tracks with the index of call_ids[]
641
struct {
642
    char name[6];
643
    char pad[2];
644
} cid_names[20];
645

    
646
#seekto 0x7600;
647
struct {
648
    u8 screen_mode;
649
} screen;
650
"""
651

    
652

    
653
# Support for the Wouxun KG-UV9D Plus and KG-UV9PX radio
654
# Serial coms are at 19200 baud
655
# The data is passed in variable length records
656
# Record structure:
657
#  Offset   Usage
658
#    0      start of record (\x7d)
659
#    1      Command (6 commands, see above)
660
#    2      direction (\xff PC-> Radio, \x00 Radio -> PC)
661
#    3      length of payload (excluding header/checksum) (n)
662
#    4      payload (n bytes)
663
#    4+n+1  checksum - byte sum (% 256) of bytes 1 -> 4+n
664
#
665
# Memory Read Records:
666
# the payload is 3 bytes, first 2 are offset (big endian),
667
# 3rd is number of bytes to read
668
# Memory Write Records:
669
# the maximum payload size (from the Wouxun software)
670
# seems to be 66 bytes (2 bytes location + 64 bytes data).
671

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

    
678
    data = bytearray()
679
    data.append(0x7d)  # tag that marks the beginning of the packet
680
    data.append(op)
681
    data.append(0xff)  # 0xff is from app to radio
682
    # calc checksum from op to end
683
    cksum = op + 0xff
684
    if (payload):
685
        data.append(len(payload))
686
        cksum += len(payload)
687
        for byte in payload:
688
            cksum += byte
689
            data.append(byte)
690
    else:
691
        data.append(0x00)
692
        # Yea, this is a 4 bit cksum (also known as a bug)
693
    data.append(cksum & 0xf)
694

    
695
    # now obfuscate by an xor starting with first payload byte ^ 0x52
696
    # including the trailing cksum.
697
    xorbits = 0x52
698
    for i, byte in enumerate(data[4:]):
699
        xord = xorbits ^ byte
700
        data[i + 4] = xord
701
        xorbits = xord
702
    return(data)
703

    
704

    
705
def _pkt_decode(data):
706
    """Take a packet hot off the wire and decode it into clear text
707
    and return the fields. We say <<cleartext>> here because all it
708
    turns out to be is annoying obfuscation.
709
    This is the inverse of pkt_decode"""
710

    
711
    # we don't care about data[0].
712
    # It is always 0x7d and not included in checksum
713
    op = data[1]
714
    direction = data[2]
715
    bytecount = data[3]
716

    
717
    # First un-obfuscate the payload and cksum
718
    payload = bytearray()
719
    xorbits = 0x52
720
    for i, byte in enumerate(data[4:]):
721
        payload.append(xorbits ^ byte)
722
        xorbits = byte
723

    
724
    # Calculate the checksum starting with the 3 bytes of the header
725
    cksum = op + direction + bytecount
726
    for byte in payload[:-1]:
727
        cksum += byte
728
    # yes, a 4 bit cksum to match the encode
729
    cksum_match = (cksum & 0xf) == payload[-1]
730
    if (not cksum_match):
731
        LOG.debug(
732
            "Checksum mismatch: %x != %x; " % (cksum, payload[-1]))
733
    return (cksum_match, op, payload[:-1])
734

    
735
# UI callbacks to process input for mapping UI fields to memory cells
736

    
737

    
738
def freq2int(val, min, max):
739
    """Convert a frequency as a string to a u32. Units is Hz
740
    """
741
    _freq = chirp_common.parse_freq(str(val))
742
    if _freq > max or _freq < min:
743
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
744
                                (chirp_common.format_freq(_freq),
745
                                 chirp_common.format_freq(min),
746
                                 chirp_common.format_freq(max)))
747
    return _freq
748

    
749

    
750
def int2freq(freq):
751
    """
752
    Convert a u32 frequency to a string for UI data entry/display
753
    This is stored in the radio as units of 10Hz which we compensate to Hz.
754
    A value of -1 indicates <no frequency>, i.e. unused channel.
755
    """
756
    if (int(freq) > 0):
757
        f = chirp_common.format_freq(freq)
758
        return f
759
    else:
760
        return ""
761

    
762

    
763
def freq2short(val, min, max):
764
    """Convert a frequency as a string to a u16 which is units of 10KHz
765
    """
766
    _freq = chirp_common.parse_freq(str(val))
767
    if _freq > max or _freq < min:
768
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
769
                                (chirp_common.format_freq(_freq),
770
                                 chirp_common.format_freq(min),
771
                                 chirp_common.format_freq(max)))
772
    return _freq // 100000 & 0xFFFF
773

    
774

    
775
def short2freq(freq):
776
    """
777
       Convert a short frequency to a string for UI data entry/display
778
       This is stored in the radio as units of 10KHz which we
779
       compensate to Hz.
780
       A value of -1 indicates <no frequency>, i.e. unused channel.
781
    """
782
    if (int(freq) > 0):
783
        f = chirp_common.format_freq(freq * 100000)
784
        return f
785
    else:
786
        return ""
787

    
788

    
789
def tone2short(t):
790
    """Convert a string tone or DCS to an encoded u16
791
    """
792
    tone = str(t)
793
    if tone == "----":
794
        u16tone = 0x0000
795
    elif tone[0] == 'D':  # This is a DCS code
796
        c = tone[1: -1]
797
        code = int(c, 8)
798
        if tone[-1] == 'I':
799
            code |= 0x4000
800
        u16tone = code | 0x8000
801
    else:              # This is an analog CTCSS
802
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff  # strip the '.'
803
    return u16tone
804

    
805

    
806
def short2tone(tone):
807
    """ Map a binary CTCSS/DCS to a string name for the tone
808
    """
809
    if tone == 0 or tone == 0xffff:
810
        ret = "----"
811
    else:
812
        code = tone & 0x3fff
813
        if tone & 0x8000:      # This is a DCS
814
            if tone & 0x4000:  # This is an inverse code
815
                ret = "D%0.3oI" % code
816
            else:
817
                ret = "D%0.3oN" % code
818
        else:   # Just plain old analog CTCSS
819
            ret = "%4.1f" % (code / 10.0)
820
    return ret
821

    
822

    
823
def str2callid(val):
824
    """ Convert caller id strings from callid2str.
825
    """
826
    ascii2bin = "0123456789"
827
    s = str(val).strip()
828
    if len(s) < 3 or len(s) > 6:
829
        raise InvalidValueError(
830
            "Caller ID must be at least 3 and no more than 6 digits")
831
    if s[0] == '0':
832
        raise InvalidValueError(
833
            "First digit of a Caller ID cannot be a zero '0'")
834
    blk = bytearray()
835
    for c in s:
836
        if c not in ascii2bin:
837
            raise InvalidValueError(
838
                "Caller ID must be all digits 0x%x" % c)
839
        b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
840
        blk.append(b)
841
    if len(blk) < 6:
842
        blk.append(0xc)  # EOL a short ID
843
    if len(blk) < 6:
844
        for i in range(0, (6 - len(blk))):
845
            blk.append(0xf0)
846
    return blk
847

    
848

    
849
def digits2str(digits, padding=' ', width=6):
850
    """Convert a password or SCC digit string to a string
851
    Passwords are expanded to and must be 6 chars. Fill them with '0'
852
    """
853

    
854
    bin2ascii = "0123456789"
855
    digitsstr = ""
856
    for i in range(0, 6):
857
        b = digits[i].get_value()
858
        if b == 0xc:  # the digits EOL
859
            break
860
        if b >= 0xa:
861
            raise InvalidValueError(
862
                "Value has illegal byte 0x%x" % ord(b))
863
        digitsstr += bin2ascii[b]
864
    digitsstr = digitsstr.ljust(width, padding)
865
    return digitsstr
866

    
867

    
868
def str2digits(val):
869
    """ Callback for edited strings from digits2str.
870
    """
871
    ascii2bin = " 0123456789"
872
    s = str(val).strip()
873
    if len(s) < 3 or len(s) > 6:
874
        raise InvalidValueError(
875
            "Value must be at least 3 and no more than 6 digits")
876
    blk = bytearray()
877
    for c in s:
878
        if c not in ascii2bin:
879
            raise InvalidValueError("Value must be all digits 0x%x" % c)
880
        blk.append(int(c))
881
    for i in range(len(blk), 6):
882
        blk.append(0xc)  # EOL a short ID
883
    return blk
884

    
885

    
886
def name2str(name):
887
    """ Convert a callid or scan group name to a string
888
    Deal with fixed field padding (\0 or \0xff)
889
    """
890

    
891
    namestr = ""
892
    for i in range(0, len(name)):
893
        b = ord(name[i].get_value())
894
        if b != 0 and b != 0xff:
895
            namestr += chr(b)
896
    return namestr
897

    
898

    
899
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
900
    """ Convert a string to a name. A name is a 6 element bytearray
901
    with ascii chars.
902
    """
903
    val = str(val).rstrip(' \t\r\n\0\0xff')
904
    if len(val) == 0:
905
        name = "".ljust(size, emptyfill)
906
    else:
907
        name = val.ljust(size, fillchar)
908
    return name
909

    
910

    
911
def pw2str(pw):
912
    """Convert a password string (6 digits) to a string
913
    Passwords must be 6 digits. If it is shorter, pad right with '0'
914
    """
915
    pwstr = ""
916
    ascii2bin = "0123456789"
917
    for i in range(0, len(pw)):
918
        b = pw[i].get_value()
919
        if b not in ascii2bin:
920
            raise InvalidValueError("Value must be digits 0-9")
921
        pwstr += b
922
    pwstr = pwstr.ljust(6, '0')
923
    return pwstr
924

    
925

    
926
def str2pw(val):
927
    """Store a password from UI to memory obj
928
    If we clear the password (make it go away), change the
929
    empty string to '000000' since the radio must have *something*
930
    Also, fill a < 6 digit pw with 0's
931
    """
932
    ascii2bin = "0123456789"
933
    val = str(val).rstrip(' \t\r\n\0\0xff')
934
    if len(val) == 0:  # a null password
935
        val = "000000"
936
    for i in range(0, len(val)):
937
        b = val[i]
938
        if b not in ascii2bin:
939
            raise InvalidValueError("Value must be digits 0-9")
940
    if len(val) == 0:
941
        pw = "".ljust(6, '\0')
942
    else:
943
        pw = val.ljust(6, '0')
944
    return pw
945

    
946

    
947
# Helpers to replace python2 things like confused str/byte
948

    
949
def _hex_print(data, addrfmt=None):
950
    """Return a hexdump-like encoding of @data
951
    We expect data to be a bytearray, not a string.
952
    Expanded from borrowed code to use the first 2 bytes as the address
953
    per comm packet format.
954
    """
955
    if addrfmt is None:
956
        addrfmt = '%(addr)03i'
957
        addr = 0
958
    else:  # assume first 2 bytes are address
959
        a = struct.unpack(">H", data[0:2])
960
        addr = a[0]
961
        data = data[2:]
962

    
963
    block_size = 16
964

    
965
    lines = (len(data) // block_size)
966
    if (len(data) % block_size > 0):
967
        lines += 1
968

    
969
    out = ""
970
    left = len(data)
971
    for block in range(0, lines):
972
        addr += block * block_size
973
        try:
974
            out += addrfmt % locals()
975
        except (OverflowError, ValueError, TypeError, KeyError):
976
            out += "%03i" % addr
977
        out += ': '
978

    
979
        if left < block_size:
980
            limit = left
981
        else:
982
            limit = block_size
983

    
984
        for j in range(0, block_size):
985
            if (j < limit):
986
                out += "%02x " % data[(block * block_size) + j]
987
            else:
988
                out += "   "
989

    
990
        out += "  "
991

    
992
        for j in range(0, block_size):
993

    
994
            if (j < limit):
995
                _byte = data[(block * block_size) + j]
996
                if _byte >= 0x20 and _byte < 0x7F:
997
                    out += "%s" % chr(_byte)
998
                else:
999
                    out += "."
1000
            else:
1001
                out += " "
1002
        out += "\n"
1003
        if (left > block_size):
1004
            left -= block_size
1005

    
1006
    return out
1007

    
1008

    
1009
# Useful UI lists
1010
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
1011
STEPS_9K = [2.5, 5.0, 6.25, 8.33, 10.0, 12.5, 25.0, 50.0, 100.0]
1012
S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]]
1013
STEP_LIST = [str(x)+"kHz" for x in STEPS]
1014
STEP_LIST_9K = [str(x)+"kHz" for x in STEPS_9K]
1015
ROGER_LIST = ["Off", "Begin", "End", "Both"]
1016
TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)]
1017
TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 11)]
1018
BANDWIDTH_LIST = ["Wide", "Narrow"]
1019
LANGUAGE_LIST = ["English", "Chinese"]
1020
LANGUAGE_LIST2 = ["English", "Chinese-DISABLED"]
1021
PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"]
1022
PF1KEY_LIST9GX = ["OFF", "call id", "r-alarm", "SOS", "SF-TX", "Scan",
1023
                  "Second", "Lamp"]
1024
PF2KEY_LIST = ["OFF", "Scan", "Second", "Lamp", "SDF-DIR", "K-lamp"]
1025
PF2KEY_LIST9GX = ["OFF", "Scan", "Second", "Lamp", "K-lamp"]
1026
PF3KEY_LIST2 = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX", "Scan",
1027
                "Second", "Lamp"]
1028
PF3KEY_LIST9GX = ["OFF", "call id", "r-alarm", "SOS", "SF-TX", "Scan",
1029
                  "Second", "Lamp"]
1030
PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX"]
1031
WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.",
1032
                 "Ch. No.+Name"]
1033
BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + \
1034
                 ["Always On"]
1035
BACKLIGHT_BRIGHT_MIN = 1
1036
BACKLIGHT_BRIGHT_MAX = 5
1037
SAVE_MODES = ["Off", "1", "2", "3", "4"]
1038
LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"]
1039
APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)]
1040
OFFSET_LIST = ["none", "+", "-"]
1041
PONMSG_LIST = ["Battery Volts", "Bitmap"]
1042
PONMSG_LIST2 = ["Battery Volts", "Bitmap-DISABLED"]
1043
SPMUTE_LIST = ["QT", "QT*T", "QT&T"]
1044
DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"]
1045
DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)]
1046
PTTID_LIST = ["Off", "Begin", "End", "Both"]
1047
ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)]
1048
VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1, 11)]
1049
VOX_DLYS = ["Off"] + ["%ds" % t for t in range(1, 5)]
1050
RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)]
1051
ABR_LVL_MAP = [("1", 1), ("2", 2), ("3", 3), ("4", 4), ("5", 5)]
1052
LIST_1_5 = ["%s" % x for x in range(1, 6)]
1053
LIST_0_9 = ["%s" % x for x in range(0, 10)]
1054
LIST_1_20 = ["%s" % x for x in range(1, 21)]
1055
LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)]
1056
SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)]
1057
SCANMODE_LIST = ["TO", "CO", "SE"]
1058
SCANRANGE_LIST = ["Current band", "freq range", "ALL"]
1059
SCQT_LIST = ["Decoder", "Encoder", "Both"]
1060
S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"]
1061
POWER_LIST = ["Low", "Med", "High"]
1062
RPTMODE_LIST = ["Radio/Talkie", "One direction Repeater",
1063
                "Two direction repeater"]
1064
TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \
1065
            ["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \
1066
            ["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES]
1067
SCREEN_MODE_LIST = ["Classic", "Covert", "Day_1", "Day_2"]
1068
ACTIVE_AREA_LIST = ["Receiver A - Top", "Receiver B - Bottom"]
1069
TDR_LIST = ["TDR ON", "TDR OFF"]
1070

    
1071

    
1072
@directory.register
1073
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
1074
                      chirp_common.ExperimentalRadio):
1075

    
1076
    """Wouxun KG-UV9D Plus"""
1077
    VENDOR = "Wouxun"
1078
    MODEL = "KG-UV9D Plus"
1079
    _model = b"KG-UV9D"
1080
    _rev = b"00"  # default rev for the radio I know about...
1081
    _file_ident = b"kg-uv9d"
1082
    NEEDS_COMPAT_SERIAL = False
1083
    BAUD_RATE = 19200
1084
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
1085
                    chirp_common.PowerLevel("M", watts=2),
1086
                    chirp_common.PowerLevel("H", watts=5)]
1087
    _mmap = ""
1088

    
1089
    def _read_record(self):
1090
        """ Read and validate the header of a radio reply.
1091
        A record is a formatted byte stream as follows:
1092
            0x7D   All records start with this
1093
            opcode This is in the set of legal commands.
1094
                   The radio reply matches the request
1095
            dir    This is the direction, 0xFF to the radio,
1096
                   0x00 from the radio.
1097
            cnt    Count of bytes in payload
1098
                   (not including the trailing checksum byte)
1099
            <cnt bytes>
1100
            <checksum byte>
1101
        """
1102

    
1103
        # first get the header and validate it
1104
        data = bytearray(self.pipe.read(4))
1105
        if (len(data) < 4):
1106
            raise errors.RadioError('Radio did not respond')
1107
        if (data[0] != 0x7D):
1108
            raise errors.RadioError(
1109
                'Radio reply garbled (%02x)' % data[0])
1110
        if (data[1] not in cmd_name):
1111
            raise errors.RadioError(
1112
                "Unrecognized opcode (%02x)" % data[1])
1113
        if (data[2] != 0x00):
1114
            raise errors.RadioError(
1115
                "Direction incorrect. Got (%02x)" % data[2])
1116
        payload_len = data[3]
1117
        # don't forget to read the checksum byte
1118
        data.extend(self.pipe.read(payload_len + 1))
1119
        if (len(data) != (payload_len + 5)):  # we got a short read
1120
            raise errors.RadioError(
1121
                "Radio reply wrong size. Wanted %d, got %d" %
1122
                ((payload_len + 1), (len(data) - 4)))
1123
        return _pkt_decode(data)
1124

    
1125
    def _write_record(self, cmd, payload=None):
1126
        """ Write a request packet to the radio.
1127
        """
1128

    
1129
        packet = _pkt_encode(cmd, payload)
1130
        self.pipe.write(packet)
1131

    
1132
    @classmethod
1133
    def match_model(cls, filedata, filename):
1134
        """Look for bits in the file image and see if it looks
1135
        like ours...
1136
        TODO: there is a bunch of rubbish between 0x50 and 0x160
1137
        that is still a known unknown
1138
        """
1139
        return cls._file_ident in filedata[0x51:0x59].lower()
1140

    
1141
    def _identify(self):
1142
        """ Identify the radio
1143
        The ident block identifies the radio and its capabilities.
1144
        This block is always 78 bytes. The rev == '01' is the base
1145
        radio and '02' seems to be the '-Plus' version.
1146
        I don't really trust the content after the model and revision.
1147
        One would assume this is pretty much constant data but I have
1148
        seen differences between my radio and the dump named
1149
        KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first
1150
        five bands match the OEM windows
1151
        app except the 350-400 band. The OOB trace has the 700MHz
1152
        band different. This is speculation at this point.
1153

    
1154
        TODO: This could be smarter and reject a radio not actually
1155
        a UV9D...
1156
        """
1157

    
1158
        for _i in range(0, 10):  # retry 10 times if we get junk
1159
            self._write_record(CMD_IDENT)
1160
            chksum_match, op, _resp = self._read_record()
1161
            if len(_resp) == 0:
1162
                raise Exception("Radio not responding")
1163
            if len(_resp) != 74:
1164
                LOG.error(
1165
                    "Expected and IDENT reply of 78 bytes. Got (%d)" %
1166
                    len(_resp))
1167
                continue
1168
            if not chksum_match:
1169
                LOG.error("Checksum error: retrying ident...")
1170
                time.sleep(0.100)
1171
                continue
1172
            if op != CMD_IDENT:
1173
                LOG.error("Expected IDENT reply. Got (%02x)" % op)
1174
                continue
1175
            LOG.debug("Got:\n%s" % _hex_print(_resp))
1176
            (mod, rev) = struct.unpack(">7s2s", _resp[0:9])
1177
            LOG.debug("Model %s, rev %s" % (mod, rev))
1178
            if mod == self._model:
1179
                self._rev = rev
1180
                return
1181
            else:
1182
                raise Exception("Unable to identify radio")
1183
        raise Exception("All retries to identify failed")
1184

    
1185
    def process_mmap(self):
1186
        if self._rev != b"02" and self._rev != b"00":
1187
            # new revision found - log it and assume same map and proceed
1188
            LOG.debug("Unrecognized model variation (%s) Using default Map" %
1189
                      self._rev)
1190
        self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
1191

    
1192
    def sync_in(self):
1193
        """ Public sync_in
1194
            Download contents of the radio. Throw errors back
1195
            to the core if the radio does not respond.
1196
            """
1197
        try:
1198
            self._identify()
1199
            self._mmap = self._do_download()
1200
            self._write_record(CMD_HANGUP)
1201
        except errors.RadioError:
1202
            raise
1203
        except Exception as e:
1204
            LOG.exception('Unknown error during download process')
1205
            raise errors.RadioError(
1206
                "Failed to communicate with radio: %s" % e)
1207
        self.process_mmap()
1208

    
1209
    def sync_out(self):
1210
        """ Public sync_out
1211
            Upload the modified memory image into the radio.
1212
            """
1213

    
1214
        try:
1215
            self._identify()
1216
            self._do_upload()
1217
            self._write_record(CMD_HANGUP)
1218
        except errors.RadioError:
1219
            raise
1220
        except Exception as e:
1221
            raise errors.RadioError(
1222
                "Failed to communicate with radio: %s" % e)
1223
        return
1224

    
1225
    def _do_download(self):
1226
        """ Read the whole of radio memory in 64 byte chunks.
1227
        We load the config space followed by loading memory channels.
1228
        The radio seems to be a "clone" type and the memory channels
1229
        are actually within the config space. There are separate
1230
        commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
1231
        these seem to be a hack that can only do 4 channels at a time.
1232
        Since the radio only supports 999, (can only support 3 chars
1233
        in the display UI?) although the vendors app reads 1000
1234
        channels, it hacks back to config writes (CMD_WCONF) for the
1235
        last 3 channels and names. We keep it simple and just read
1236
        the whole thing even though the vendor app doesn't. Channels
1237
        are separate in their app simply because the radio protocol
1238
        has read/write commands to access it. What they do is simply
1239
        marshal the frequency+mode bits in 4 channel chunks followed
1240
        by a separate chunk of for names. In config space, they are two
1241
        separate arrays 1..999. Given that this space is not a
1242
        multiple of 4, there is hackery on upload to do the writes to
1243
        config space. See upload for this.
1244
        """
1245

    
1246
        mem = bytearray(0x8000)  # The radio's memory map is 32k
1247
        for addr in range(0, 0x8000, 64):
1248
            req = bytearray(struct.pack(">HB", addr, 64))
1249
            self._write_record(CMD_RCONF, req)
1250
            chksum_match, op, resp = self._read_record()
1251
            if not chksum_match:
1252
                LOG.debug(_hex_print(resp))
1253
                raise Exception(
1254
                    "Checksum error while reading configuration (0x%x)" %
1255
                    addr)
1256
            pa = struct.unpack(">H", resp[0:2])
1257
            pkt_addr = pa[0]
1258
            payload = resp[2:]
1259
            if op != CMD_RCONF or addr != pkt_addr:
1260
                raise Exception(
1261
                    "Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
1262
                    (addr, op, pkt_addr))
1263
            LOG.debug("Config read (0x%x):\n%s" %
1264
                      (addr, _hex_print(resp, '0x%(addr)04x')))
1265
            # Orig Code from 9D Plus driver was len(Payload)-1:
1266
            # This Caused every 64th byte to = 00
1267
            for i in range(0, len(payload)):
1268
                mem[addr + i] = payload[i]
1269
            if self.status_fn:
1270
                status = chirp_common.Status()
1271
                status.cur = addr
1272
                status.max = 0x8000
1273
                status.msg = "Cloning from radio"
1274
                self.status_fn(status)
1275
        return memmap.MemoryMapBytes(bytes(mem))
1276

    
1277
    def _do_upload(self):
1278
        """Walk through the config map and write updated records to
1279
        the radio. The config map contains only the regions we know
1280
        about. We don't use the channel memory commands to avoid the
1281
        hackery of using config write commands to fill in the last
1282
        3 channel memory and names slots. As we discover other useful
1283
        goodies in the map, we can add more slots...
1284
        """
1285
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1286
            cfgmap = config_map2
1287
        else:
1288
            cfgmap = config_map
1289

    
1290
        for start, blocksize, count in cfgmap:
1291
            end = start + (blocksize * count)
1292
            for addr in range(start, end, blocksize):
1293
                req = bytearray(struct.pack(">H", addr))
1294
                req.extend(self.get_mmap()[addr:addr + blocksize])
1295
                self._write_record(CMD_WCONF, req)
1296
                LOG.debug("Config write (0x%x):\n%s" %
1297
                          (addr, _hex_print(req)))
1298
                chksum_match, op, ack = self._read_record()
1299
                LOG.debug("Config write ack [%x]\n%s" %
1300
                          (addr, _hex_print(ack)))
1301
                a = struct.unpack(">H", ack)  # big endian short...
1302
                ack = a[0]
1303
                if not chksum_match or op != CMD_WCONF or addr != ack:
1304
                    msg = ""
1305
                    if not chksum_match:
1306
                        msg += "Checksum err, "
1307
                    if op != CMD_WCONF:
1308
                        msg += "cmd mismatch %x != %x, " % \
1309
                               (op, CMD_WCONF)
1310
                    if addr != ack:
1311
                        msg += "ack error %x != %x, " % (addr, ack)
1312
                    raise Exception("Radio did not ack block: %s error" % msg)
1313
                if self.status_fn:
1314
                    status = chirp_common.Status()
1315
                    status.cur = addr
1316
                    status.max = 0x8000
1317
                    status.msg = "Cloning to radio"
1318
                    self.status_fn(status)
1319

    
1320
    def get_features(self):
1321
        """ Public get_features
1322
            Return the features of this radio once we have identified
1323
            it and gotten its bits
1324
            """
1325
        rf = chirp_common.RadioFeatures()
1326
        rf.has_settings = True
1327
        rf.has_ctone = True
1328
        rf.has_rx_dtcs = True
1329
        rf.has_cross = True
1330
        rf.has_tuning_step = False
1331
        rf.has_bank = False
1332
        rf.can_odd_split = True
1333
        rf.valid_skips = ["", "S"]
1334
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
1335
        rf.valid_cross_modes = [
1336
            "Tone->Tone",
1337
            "Tone->DTCS",
1338
            "DTCS->Tone",
1339
            "DTCS->",
1340
            "->Tone",
1341
            "->DTCS",
1342
            "DTCS->DTCS",
1343
        ]
1344
        rf.valid_modes = ["FM", "NFM", "AM"]
1345
        rf.valid_power_levels = self.POWER_LEVELS
1346
        rf.valid_name_length = 8
1347
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
1348
        rf.valid_bands = [(108000000, 136000000),  # Aircraft  AM
1349
                          (136000000, 180000000),  # supports 2m
1350
                          (230000000, 250000000),
1351
                          (350000000, 400000000),
1352
                          (400000000, 520000000),  # supports 70cm
1353
                          (700000000, 985000000)]
1354
        rf.valid_characters = chirp_common.CHARSET_ASCII
1355
        rf.valid_tuning_steps = STEPS
1356
        rf.memory_bounds = (1, 999)  # 999 memories
1357
        return rf
1358

    
1359
    @classmethod
1360
    def get_prompts(cls):
1361
        rp = chirp_common.RadioPrompts()
1362
        rp.experimental = ("This radio driver is currently under development. "
1363
                           "There are no known issues with it, but you should "
1364
                           "proceed with caution.")
1365
        return rp
1366

    
1367
    def get_raw_memory(self, number):
1368
        return repr(self._memobj.chan_blk[number - 1])
1369

    
1370
    def _get_tone(self, _mem, mem):
1371
        """Decode both the encode and decode CTSS/DCS codes from
1372
        the memory channel and stuff them into the UI
1373
        memory channel row.
1374
        """
1375
        txtone = short2tone(_mem.encQT)
1376
        rxtone = short2tone(_mem.decQT)
1377
        pt = "N"
1378
        pr = "N"
1379

    
1380
        if txtone == "----":
1381
            txmode = ""
1382
        elif txtone[0] == "D":
1383
            mem.dtcs = int(txtone[1:4])
1384
            if txtone[4] == "I":
1385
                pt = "R"
1386
            txmode = "DTCS"
1387
        else:
1388
            mem.rtone = float(txtone)
1389
            txmode = "Tone"
1390

    
1391
        if rxtone == "----":
1392
            rxmode = ""
1393
        elif rxtone[0] == "D":
1394
            mem.rx_dtcs = int(rxtone[1:4])
1395
            if rxtone[4] == "I":
1396
                pr = "R"
1397
            rxmode = "DTCS"
1398
        else:
1399
            mem.ctone = float(rxtone)
1400
            rxmode = "Tone"
1401

    
1402
        if txmode == "Tone" and len(rxmode) == 0:
1403
            mem.tmode = "Tone"
1404
        elif (txmode == rxmode and txmode == "Tone" and
1405
              mem.rtone == mem.ctone):
1406
            mem.tmode = "TSQL"
1407
        elif (txmode == rxmode and txmode == "DTCS" and
1408
              mem.dtcs == mem.rx_dtcs):
1409
            mem.tmode = "DTCS"
1410
        elif (len(rxmode) + len(txmode)) > 0:
1411
            mem.tmode = "Cross"
1412
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1413

    
1414
        mem.dtcs_polarity = pt + pr
1415

    
1416
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1417
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1418

    
1419
    def get_memory(self, number):
1420
        """ Public get_memory
1421
            Return the channel memory referenced by number to the UI.
1422
        """
1423
        _mem = self._memobj.chan_blk[number - 1]
1424
        _nam = self._memobj.chan_name[number - 1]
1425

    
1426
        mem = chirp_common.Memory()
1427
        mem.number = number
1428
        _valid = _mem.state
1429

    
1430
        # this code attempts to robustly decipher what Wouxun considers valid
1431
        # memory locations on the 9 series radios and the factory CPS.
1432
        # it appears they use a combination of State and Rx Freq to determine
1433
        # validity rather than just the State value.
1434
        # It is possible the State value is not even used at all.
1435
        # Rather than continuously adding new Mem Valid values as they arise
1436
        # assume any value other than 0xFF is likely valid and use Rx Freq to
1437
        # further assess validity
1438

    
1439
        if _mem.rxfreq == 0xFFFFFFFF:
1440
            # Rx freq indicates empty channel memory
1441
            # assume empty regardless of _valid and proceed to next channel
1442
            if _valid not in INVALID_MEM_VALUES:
1443
                # only log if _valid indicates the channel is not invalid
1444
                LOG.debug("Rx Freq = 0xFFFFFFFF - Treating memory as empty")
1445
            mem.empty = True
1446
            return mem
1447
        elif _valid in INVALID_MEM_VALUES:
1448
            # Check for 9PX case where CPS creates a valid channel with
1449
            # 0xFF for State -  accept it as valid as long as Rx Freq is
1450
            # <= max value
1451
            if _mem.rxfreq > 99999999: # Max poss Value = 999.999999 MHz
1452
                LOG.debug("State invalid-Rx Frq > Max: Treating mem as empty")
1453
                mem.empty = True
1454
                return mem
1455
            else:
1456
                LOG.debug("State invalid-Rx Freq valid: Assume chan valid")
1457
                mem.empty = False
1458
        else: # State not Invalid and Rx Freq not 0xFFFFFFFF
1459
            if _mem.rxfreq > 99999999: # Max poss Value = 999.999999 MHz
1460
                LOG.debug("Invalid Rx Frq: Treating mem as empty")
1461
                mem.empty = True
1462
                return mem
1463
            else:
1464
                mem.empty = False
1465

    
1466
        mem.freq = int(_mem.rxfreq) * 10
1467

    
1468
        if _mem.txfreq == 0xFFFFFFFF:
1469
            # TX freq not set
1470
            mem.duplex = "off"
1471
            mem.offset = 0
1472
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1473
            mem.duplex = ""
1474
            mem.offset = 0
1475
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1476
            mem.duplex = "split"
1477
            mem.offset = int(_mem.txfreq) * 10
1478
        else:
1479
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1480
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1481

    
1482
        mem.name = name2str(_nam.name)
1483

    
1484
        self._get_tone(_mem, mem)
1485

    
1486
        mem.skip = "" if bool(_mem.scan) else "S"
1487

    
1488
        mem.power = self.POWER_LEVELS[_mem.pwr]
1489
        if _mem.mod == 1:
1490
            mem.mode = "AM"
1491
        elif _mem.fm_dev == 0:
1492
            mem.mode = "FM"
1493
        else:
1494
            mem.mode = "NFM"
1495
        #  qt has no home in the UI
1496
        return mem
1497

    
1498
    def _set_tone(self, mem, _mem):
1499
        """Update the memory channel block CTCC/DCS tones
1500
        from the UI fields
1501
        """
1502
        def _set_dcs(code, pol):
1503
            val = int("%i" % code, 8) | 0x8000
1504
            if pol == "R":
1505
                val |= 0x4000
1506
            return val
1507

    
1508
        rx_mode = tx_mode = None
1509
        rxtone = txtone = 0x0000
1510

    
1511
        if mem.tmode == "Tone":
1512
            tx_mode = "Tone"
1513
            txtone = int(mem.rtone * 10)
1514
        elif mem.tmode == "TSQL":
1515
            rx_mode = tx_mode = "Tone"
1516
            rxtone = txtone = int(mem.ctone * 10)
1517
        elif mem.tmode == "DTCS":
1518
            tx_mode = rx_mode = "DTCS"
1519
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1520
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1521
        elif mem.tmode == "Cross":
1522
            tx_mode, rx_mode = mem.cross_mode.split("->")
1523
            if tx_mode == "DTCS":
1524
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1525
            elif tx_mode == "Tone":
1526
                txtone = int(mem.rtone * 10)
1527
            if rx_mode == "DTCS":
1528
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1529
            elif rx_mode == "Tone":
1530
                rxtone = int(mem.ctone * 10)
1531

    
1532
        _mem.decQT = rxtone
1533
        _mem.encQT = txtone
1534

    
1535
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1536
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1537

    
1538
    def set_memory(self, mem):
1539
        """ Public set_memory
1540
            Inverse of get_memory. Update the radio memory image
1541
            from the mem object
1542
            """
1543
        number = mem.number
1544

    
1545
        _mem = self._memobj.chan_blk[number - 1]
1546
        _nam = self._memobj.chan_name[number - 1]
1547

    
1548
        if mem.empty:
1549
            # consider putting in a check for chan # that is empty but
1550
            # listed as one of the 2 working channels and change them
1551
            # to channel 1 to be consistent with CPS and allow
1552
            # complete deletion from radio.  Otherwise,
1553
            # a deleted channel will still show on radio with no name.
1554
            # MRT implement the above working channel check
1555
            if self._memobj.a_conf.w_chan == number:
1556
                self._memobj.a_conf.w_chan = 1
1557
            if self._memobj.b_conf.w_chan == number:
1558
                self._memobj.b_conf.w_chan = 1
1559

    
1560
            _mem.set_raw("\xFF" * (_mem.size() // 8))
1561
            _nam.name = str2name("", 8, '\0', '\0')
1562
            _mem.state = MEM_INVALID
1563
            return
1564

    
1565
        _mem.rxfreq = int(mem.freq / 10)
1566
        if mem.duplex == "off":
1567
            _mem.txfreq = 0xFFFFFFFF
1568
        elif mem.duplex == "split":
1569
            _mem.txfreq = int(mem.offset / 10)
1570
        elif mem.duplex == "+":
1571
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1572
        elif mem.duplex == "-":
1573
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1574
        else:
1575
            _mem.txfreq = int(mem.freq / 10)
1576
        _mem.scan = int(mem.skip != "S")
1577
        if mem.mode == "FM":
1578
            _mem.mod = 0    # make sure forced AM is off
1579
            _mem.fm_dev = 0
1580
        elif mem.mode == "NFM":
1581
            _mem.mod = 0
1582
            _mem.fm_dev = 1
1583
        elif mem.mode == "AM":
1584
            _mem.mod = 1     # AM on
1585
            _mem.fm_dev = 1  # set NFM bandwidth
1586
        else:
1587
            _mem.mod = 0
1588
            _mem.fm_dev = 0  # Catchall default is FM
1589
        # set the tone
1590
        self._set_tone(mem, _mem)
1591
        # set the power
1592
        if mem.power:
1593
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1594
        else:
1595
            _mem.pwr = True
1596

    
1597
        # Set fields we can't access via the UI table to safe defaults
1598
        _mem.qt = 0   # mute mode to QT
1599
        _mem.bit5 = 0   # clear this bit to ensure accurate CPS power level
1600
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1601
        _mem.state = MEM_VALID
1602

    
1603
# Build the UI configuration tabs
1604
# the channel memory tab is built by the core.
1605
# We have no control over it
1606

    
1607
    def _core_tab(self):
1608
        """ Build Core Configuration tab
1609
        Radio settings common to all modes and areas go here.
1610
        """
1611
        s = self._memobj.settings
1612
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1613

    
1614
            sm = self._memobj.screen
1615

    
1616
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1617

    
1618
        cf.append(RadioSetting("auto_am",
1619
                               "Auto detect AM (Menu 53)",
1620
                               RadioSettingValueBoolean(s.auto_am)))
1621
        cf.append(RadioSetting("qt_sw",
1622
                               "Scan tone detect (Menu 59)",
1623
                               RadioSettingValueBoolean(s.qt_sw)))
1624
        cf.append(
1625
            RadioSetting("s_mute",
1626
                         "SubFreq Mute (Menu 60)",
1627
                         RadioSettingValueList(S_MUTE_LIST,
1628
                                               S_MUTE_LIST[s.s_mute])))
1629
        cf.append(
1630
            RadioSetting("tot",
1631
                         "Transmit timeout Timer (Menu 10)",
1632
                         RadioSettingValueList(TIMEOUT_LIST,
1633
                                               TIMEOUT_LIST[s.tot])))
1634
        cf.append(
1635
            RadioSetting("toa",
1636
                         "Transmit Timeout Alarm (Menu 11)",
1637
                         RadioSettingValueList(TOA_LIST,
1638
                                               TOA_LIST[s.toa])))
1639
        cf.append(
1640
            RadioSetting("ptt_id",
1641
                         "PTT Caller ID mode (Menu 23)",
1642
                         RadioSettingValueList(PTTID_LIST,
1643
                                               PTTID_LIST[s.ptt_id])))
1644
        cf.append(
1645
            RadioSetting("id_dly",
1646
                         "Caller ID Delay time (Menu 25)",
1647
                         RadioSettingValueList(ID_DLY_LIST,
1648
                                               ID_DLY_LIST[s.id_dly])))
1649
        cf.append(RadioSetting("voice_sw",
1650
                               "Voice Guide (Menu 12)",
1651
                               RadioSettingValueBoolean(s.voice_sw)))
1652
        cf.append(RadioSetting("beep",
1653
                               "Keypad Beep (Menu 13)",
1654
                               RadioSettingValueBoolean(s.beep)))
1655
        cf.append(
1656
            RadioSetting("s_tone",
1657
                         "Side Tone (Menu 36)",
1658
                         RadioSettingValueList(S_TONES,
1659
                                               S_TONES[s.s_tone])))
1660
        cf.append(
1661
            RadioSetting("ring_time",
1662
                         "Ring Time (Menu 26)",
1663
                         RadioSettingValueList(
1664
                             LIST_OFF_10,
1665
                             LIST_OFF_10[s.ring_time])))
1666
        cf.append(
1667
            RadioSetting("roger",
1668
                         "Roger Beep (Menu 9)",
1669
                         RadioSettingValueList(ROGER_LIST,
1670
                                               ROGER_LIST[s.roger])))
1671
        cf.append(RadioSetting("blcdsw",
1672
                               "Backlight (Menu 41)",
1673
                               RadioSettingValueBoolean(s.blcdsw)))
1674
        cf.append(
1675
            RadioSetting("abr",
1676
                         "Auto Backlight Time (Menu 1)",
1677
                         RadioSettingValueList(BACKLIGHT_LIST,
1678
                                               BACKLIGHT_LIST[s.abr])))
1679
        cf.append(
1680
            RadioSetting("abr_lvl",
1681
                         "Backlight Brightness (Menu 27)",
1682
                         RadioSettingValueInteger(BACKLIGHT_BRIGHT_MIN,
1683
                                                  BACKLIGHT_BRIGHT_MAX,
1684
                                                  s.abr_lvl)))
1685
        cf.append(RadioSetting("lock",
1686
                               "Keypad Lock",
1687
                               RadioSettingValueBoolean(s.lock)))
1688
        cf.append(
1689
            RadioSetting("lock_m",
1690
                         "Keypad Lock Mode (Menu 35)",
1691
                         RadioSettingValueList(LOCK_MODES,
1692
                                               LOCK_MODES[s.lock_m])))
1693
        cf.append(RadioSetting("auto_lk",
1694
                               "Keypad Autolock (Menu 34)",
1695
                               RadioSettingValueBoolean(s.auto_lk)))
1696
        cf.append(RadioSetting("prich_sw",
1697
                               "Priority Channel Scan (Menu 33)",
1698
                               RadioSettingValueBoolean(s.prich_sw)))
1699
        cf.append(RadioSetting("pri_ch",
1700
                               "Priority Channel (Menu 32)",
1701
                               RadioSettingValueInteger(1, 999,
1702
                                                        s.pri_ch)))
1703
        cf.append(
1704
            RadioSetting("dtmf_st",
1705
                         "DTMF Sidetone (Menu 22)",
1706
                         RadioSettingValueList(DTMFST_LIST,
1707
                                               DTMFST_LIST[s.dtmf_st])))
1708
        cf.append(RadioSetting("sc_qt",
1709
                               "Scan QT Save Mode (Menu 38)",
1710
                               RadioSettingValueList(
1711
                                   SCQT_LIST,
1712
                                   SCQT_LIST[s.sc_qt])))
1713
        cf.append(
1714
            RadioSetting("apo_tmr",
1715
                         "Automatic Power-off (Menu 39)",
1716
                         RadioSettingValueList(APO_TIMES,
1717
                                               APO_TIMES[s.apo_tmr])))
1718
        cf.append(  # VOX "guard" is really VOX trigger audio level
1719
            RadioSetting("vox_grd",
1720
                         "VOX level (Menu 7)",
1721
                         RadioSettingValueList(VOX_GRDS,
1722
                                               VOX_GRDS[s.vox_grd])))
1723
        cf.append(
1724
            RadioSetting("vox_dly",
1725
                         "VOX Delay (Menu 37)",
1726
                         RadioSettingValueList(VOX_DLYS,
1727
                                               VOX_DLYS[s.vox_dly])))
1728
        cf.append(RadioSetting("bledsw",
1729
                               "Receive LED (Menu 42)",
1730
                               RadioSettingValueBoolean(s.bledsw)))
1731

    
1732
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1733
                cf.append(RadioSetting("screen.screen_mode",
1734
                                       "Screen Mode (Menu 62)",
1735
                                       RadioSettingValueList(
1736
                                             SCREEN_MODE_LIST,
1737
                                             SCREEN_MODE_LIST[
1738
                                                 sm.screen_mode])))
1739
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1740
            langlst = LANGUAGE_LIST2
1741
        else:
1742
            langlst = LANGUAGE_LIST
1743
        cf.append(
1744
            RadioSetting("lang",
1745
                         "Menu Language (Menu 14)",
1746
                         RadioSettingValueList(langlst,
1747
                                               langlst[s.lang])))
1748

    
1749
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1750
            ponmsglst = PONMSG_LIST2
1751
        else:
1752
            ponmsglst = PONMSG_LIST
1753
        cf.append(RadioSetting("ponmsg",
1754
                               "Poweron message (Menu 40)",
1755
                               RadioSettingValueList(
1756
                                   ponmsglst, ponmsglst[s.ponmsg])))
1757
        return cf
1758

    
1759
    def _repeater_tab(self):
1760
        """Repeater mode functions
1761
        """
1762
        s = self._memobj.settings
1763
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1764

    
1765
        cf.append(
1766
            RadioSetting("type_set",
1767
                         "Radio Mode (Menu 43)",
1768
                         RadioSettingValueList(
1769
                             RPTMODE_LIST,
1770
                             RPTMODE_LIST[s.type_set])))
1771
        cf.append(RadioSetting("rpt_ptt",
1772
                               "Repeater PTT (Menu 45)",
1773
                               RadioSettingValueBoolean(s.rpt_ptt)))
1774
        cf.append(RadioSetting("rpt_spk",
1775
                               "Repeater Mode Speaker (Menu 44)",
1776
                               RadioSettingValueBoolean(s.rpt_spk)))
1777
        cf.append(
1778
            RadioSetting("rpt_kpt",
1779
                         "Repeater Hold Time (Menu 46)",
1780
                         RadioSettingValueList(RPT_KPTS,
1781
                                               RPT_KPTS[s.rpt_kpt])))
1782
        cf.append(RadioSetting("rpt_rct",
1783
                               "Repeater Receipt Tone (Menu 47)",
1784
                               RadioSettingValueBoolean(s.rpt_rct)))
1785
        return cf
1786

    
1787
    def _admin_tab(self):
1788
        """Admin functions not present in radio menu...
1789
        These are admin functions not radio operation configuration
1790
        """
1791

    
1792
        def apply_cid(setting, obj):
1793
            c = str2callid(setting.value)
1794
            obj.code = c
1795

    
1796
        def apply_scc(setting, obj):
1797
            c = str2digits(setting.value)
1798
            obj.scc = c
1799

    
1800
        def apply_mode_sw(setting, obj):
1801
            pw = str2pw(setting.value)
1802
            obj.mode_sw = pw
1803
            setting.value = pw2str(obj.mode_sw)
1804

    
1805
        def apply_reset(setting, obj):
1806
            pw = str2pw(setting.value)
1807
            obj.reset = pw
1808
            setting.value = pw2str(obj.reset)
1809

    
1810
        def apply_wake(setting, obj):
1811
            obj.wake = int(setting.value)/10
1812

    
1813
        def apply_sleep(setting, obj):
1814
            obj.sleep = int(setting.value)/10
1815

    
1816
        pw = self._memobj.passwords  # admin passwords
1817
        s = self._memobj.settings
1818

    
1819
        cf = RadioSettingGroup("admin", "Admin Functions")
1820

    
1821
        cf.append(RadioSetting("menu_avail",
1822
                               "Menu available in channel mode",
1823
                               RadioSettingValueBoolean(s.menu_avail)))
1824
        mode_sw = RadioSettingValueString(0, 6,
1825
                                          pw2str(pw.mode_sw), False)
1826
        rs = RadioSetting("passwords.mode_sw",
1827
                          "Mode Switch Password", mode_sw)
1828
        rs.set_apply_callback(apply_mode_sw, pw)
1829
        cf.append(rs)
1830

    
1831
        cf.append(RadioSetting("reset_avail",
1832
                               "Radio Reset Available",
1833
                               RadioSettingValueBoolean(s.reset_avail)))
1834
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1835
        rs = RadioSetting("passwords.reset",
1836
                          "Radio Reset Password", reset)
1837
        rs.set_apply_callback(apply_reset, pw)
1838
        cf.append(rs)
1839

    
1840
        cf.append(
1841
            RadioSetting("dtmf_tx",
1842
                         "DTMF Tx Duration",
1843
                         RadioSettingValueList(DTMF_TIMES,
1844
                                               DTMF_TIMES[s.dtmf_tx])))
1845
        cid = self._memobj.my_callid
1846
        my_callid = RadioSettingValueString(3, 6,
1847
                                            self.callid2str(cid.code), False)
1848
        rs = RadioSetting("my_callid.code",
1849
                          "PTT Caller ID code (Menu 24)", my_callid)
1850
        rs.set_apply_callback(apply_cid, cid)
1851
        cf.append(rs)
1852

    
1853
        stun = self._memobj.stun
1854
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1855
        rs = RadioSetting("stun.scc", "Security code", st)
1856
        rs.set_apply_callback(apply_scc, stun)
1857
        cf.append(rs)
1858

    
1859
        cf.append(
1860
            RadioSetting("settings.save_m",
1861
                         "Save Mode  (Menu 2)",
1862
                         RadioSettingValueList(SAVE_MODES,
1863
                                               SAVE_MODES[s.save_m])))
1864
        for i in range(0, 4):
1865
            sm = self._memobj.save[i]
1866
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1867
            wf = RadioSetting("save[%i].wake" % i,
1868
                              "Save Mode %d Wake Time" % (i+1), wake)
1869
            wf.set_apply_callback(apply_wake, sm)
1870
            cf.append(wf)
1871

    
1872
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1873
            wf = RadioSetting("save[%i].sleep" % i,
1874
                              "Save Mode %d Sleep Time" % (i+1), slp)
1875
            wf.set_apply_callback(apply_sleep, sm)
1876
            cf.append(wf)
1877

    
1878
        _msg = str(self._memobj.display.banner).split("\0")[0]
1879
        val = RadioSettingValueString(0, 16, _msg)
1880
        val.set_mutable(True)
1881
        cf.append(RadioSetting("display.banner",
1882
                               "Display Message", val))
1883

    
1884
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1885
            _str = str(self._memobj.oemmodel.model).split("\0")[0]
1886
            val = RadioSettingValueString(0, 10, _str)
1887
            val.set_mutable(True)
1888
            cf.append(RadioSetting("oemmodel.model",
1889
                                   "Custom Sub-Receiver Message", val))
1890

    
1891
            val = RadioSettingValueList(
1892
                                TDR_LIST,
1893
                                TDR_LIST[s.tdr])
1894
            val.set_mutable(True)
1895
            cf.append(RadioSetting("tdr", "TDR", val))
1896

    
1897
            val = RadioSettingValueList(
1898
                                ACTIVE_AREA_LIST,
1899
                                ACTIVE_AREA_LIST[s.act_area])
1900
            val.set_mutable(True)
1901
            cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1902

    
1903
        return cf
1904

    
1905
    def _fm_tab(self):
1906
        """FM Broadcast channels
1907
        """
1908
        def apply_fm(setting, obj):
1909
            f = freq2short(setting.value, 76000000, 108000000)
1910
            obj.fm_freq = f
1911

    
1912
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1913
        for ch in range(0, 20):
1914
            chan = self._memobj.fm_chans[ch]
1915
            freq = RadioSettingValueString(0, 20,
1916
                                           short2freq(chan.fm_freq))
1917
            rs = RadioSetting("fm_%d" % (ch + 1),
1918
                              "FM Channel %d" % (ch + 1), freq)
1919
            rs.set_apply_callback(apply_fm, chan)
1920
            fm.append(rs)
1921
        return fm
1922

    
1923
    def _scan_grp(self):
1924
        """Scan groups
1925
        """
1926
        def apply_name(setting, obj):
1927
            name = str2name(setting.value, 8, '\0', '\0')
1928
            obj.name = name
1929

    
1930
        def apply_start(setting, obj):
1931
            """Do a callback to deal with RadioSettingInteger limitation
1932
            on memory address resolution
1933
            """
1934
            obj.scan_st = int(setting.value)
1935

    
1936
        def apply_end(setting, obj):
1937
            """Do a callback to deal with RadioSettingInteger limitation
1938
            on memory address resolution
1939
            """
1940
            obj.scan_end = int(setting.value)
1941

    
1942
        sgrp = self._memobj.scn_grps
1943
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1944
        for i in range(0, 10):
1945
            s_grp = sgrp.addrs[i]
1946
            s_name = sgrp.names[i]
1947
            rs_name = RadioSettingValueString(0, 8,
1948
                                              name2str(s_name.name))
1949
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1950
                              "Group %i Name" % (i + 1), rs_name)
1951
            rs.set_apply_callback(apply_name, s_name)
1952
            scan.append(rs)
1953
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1954
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1955
                              "Starting Channel", rs_st)
1956
            rs.set_apply_callback(apply_start, s_grp)
1957
            scan.append(rs)
1958
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1959
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1960
                              "Last Channel", rs_end)
1961
            rs.set_apply_callback(apply_end, s_grp)
1962
            scan.append(rs)
1963
        return scan
1964

    
1965
    def _callid_grp(self):
1966
        """Caller IDs to be recognized by radio
1967
        This really should be a table in the UI
1968
        """
1969
        def apply_callid(setting, obj):
1970
            c = str2callid(setting.value)
1971
            obj.cid = c
1972

    
1973
        def apply_name(setting, obj):
1974
            name = str2name(setting.value, 6, '\0', '\xff')
1975
            obj.name = name
1976

    
1977
        cid = RadioSettingGroup("callids", "Caller IDs")
1978
        for i in range(0, 20):
1979
            callid = self._memobj.call_ids[i]
1980
            name = self._memobj.cid_names[i]
1981
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1982
            rs = RadioSetting("cid_names[%i].name" % i,
1983
                              "Caller ID %i Name" % (i + 1), c_name)
1984
            rs.set_apply_callback(apply_name, name)
1985
            cid.append(rs)
1986
            c_id = RadioSettingValueString(0, 6,
1987
                                           self.callid2str(callid.cid),
1988
                                           False)
1989
            rs = RadioSetting("call_ids[%i].cid" % i,
1990
                              "Caller ID Code", c_id)
1991
            rs.set_apply_callback(apply_callid, callid)
1992
            cid.append(rs)
1993
        return cid
1994

    
1995
    def _band_tab(self, area, band):
1996
        """ Build a band tab inside a VFO/Area
1997
        """
1998
        def apply_freq(setting, lo, hi, obj):
1999
            f = freq2int(setting.value, lo, hi)
2000
            obj.freq = f/10
2001

    
2002
        def apply_offset(setting, obj):
2003
            f = freq2int(setting.value, 0, 5000000)
2004
            obj.offset = f/10
2005

    
2006
        def apply_enc(setting, obj):
2007
            t = tone2short(setting.value)
2008
            obj.encqt = t
2009

    
2010
        def apply_dec(setting, obj):
2011
            t = tone2short(setting.value)
2012
            obj.decqt = t
2013

    
2014
        if area == "a":
2015
            if band == 150:
2016
                c = self._memobj.vfo_a.band_150
2017
                lo = 108000000
2018
                hi = 180000000
2019
            elif band == 200:
2020
                c = self._memobj.vfo_a.band_200
2021
                lo = 230000000
2022
                hi = 250000000
2023
            elif band == 300:
2024
                c = self._memobj.vfo_a.band_300
2025
                lo = 350000000
2026
                hi = 400000000
2027
            elif band == 450:
2028
                c = self._memobj.vfo_a.band_450
2029
                lo = 400000000
2030
                hi = 512000000
2031
            else:   # 700
2032
                c = self._memobj.vfo_a.band_700
2033
                lo = 700000000
2034
                hi = 985000000
2035
        else:  # area 'b'
2036
            if band == 150:
2037
                c = self._memobj.vfo_b.band_150
2038
                lo = 136000000
2039
                hi = 180000000
2040
            else:  # 450
2041
                c = self._memobj.vfo_b.band_450
2042
                lo = 400000000
2043
                hi = 512000000
2044

    
2045
        prefix = "vfo_%s.band_%d" % (area, band)
2046
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2047
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2048
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2049
        rs.set_apply_callback(apply_freq, lo, hi, c)
2050
        bf.append(rs)
2051

    
2052
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2053
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2054
        rs.set_apply_callback(apply_offset, c)
2055
        bf.append(rs)
2056

    
2057
        rs = RadioSetting(prefix + ".encqt",
2058
                          "Encode QT (Menu 17,19)",
2059
                          RadioSettingValueList(TONE_LIST,
2060
                                                short2tone(c.encqt)))
2061
        rs.set_apply_callback(apply_enc, c)
2062
        bf.append(rs)
2063

    
2064
        rs = RadioSetting(prefix + ".decqt",
2065
                          "Decode QT (Menu 16,18)",
2066
                          RadioSettingValueList(TONE_LIST,
2067
                                                short2tone(c.decqt)))
2068
        rs.set_apply_callback(apply_dec, c)
2069
        bf.append(rs)
2070

    
2071
        bf.append(RadioSetting(prefix + ".qt",
2072
                               "Mute Mode (Menu 21)",
2073
                               RadioSettingValueList(SPMUTE_LIST,
2074
                                                     SPMUTE_LIST[c.qt])))
2075
        bf.append(RadioSetting(prefix + ".scan",
2076
                               "Scan this (Menu 48)",
2077
                               RadioSettingValueBoolean(c.scan)))
2078
        bf.append(RadioSetting(prefix + ".pwr",
2079
                               "Power (Menu 5)",
2080
                               RadioSettingValueList(
2081
                                   POWER_LIST, POWER_LIST[c.pwr])))
2082
        bf.append(RadioSetting(prefix + ".mod",
2083
                               "AM Modulation (Menu 54)",
2084
                               RadioSettingValueBoolean(c.mod)))
2085
        bf.append(RadioSetting(prefix + ".fm_dev",
2086
                               "FM Deviation (Menu 4)",
2087
                               RadioSettingValueList(
2088
                                   BANDWIDTH_LIST,
2089
                                   BANDWIDTH_LIST[c.fm_dev])))
2090
        bf.append(
2091
            RadioSetting(prefix + ".shift",
2092
                         "Frequency Shift (Menu 6)",
2093
                         RadioSettingValueList(OFFSET_LIST,
2094
                                               OFFSET_LIST[c.shift])))
2095
        return bf
2096

    
2097
    def _area_tab(self, area):
2098
        """Build a VFO tab
2099
        """
2100
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2101
            f = freq2short(setting.value, scan_lo, scan_hi)
2102
            obj.scan_st = f
2103

    
2104
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2105
            f = freq2short(setting.value, scan_lo, scan_hi)
2106
            obj.scan_end = f
2107

    
2108
        if area == "a":
2109
            desc = "Area A Settings"
2110
            c = self._memobj.a_conf
2111
            scan_lo = 108000000
2112
            scan_hi = 985000000
2113
            scan_rng = self._memobj.settings.a
2114
            band_list = (150, 200, 300, 450, 700)
2115
        else:
2116
            desc = "Area B Settings"
2117
            c = self._memobj.b_conf
2118
            scan_lo = 136000000
2119
            scan_hi = 512000000
2120
            scan_rng = self._memobj.settings.b
2121
            band_list = (150, 450)
2122

    
2123
        prefix = "%s_conf" % area
2124
        af = RadioSettingGroup(prefix, desc)
2125
        af.append(
2126
            RadioSetting(prefix + ".w_mode",
2127
                         "Workmode",
2128
                         RadioSettingValueList(
2129
                             WORKMODE_LIST,
2130
                             WORKMODE_LIST[c.w_mode])))
2131
        af.append(RadioSetting(prefix + ".w_chan",
2132
                               "Channel",
2133
                               RadioSettingValueInteger(1, 999,
2134
                                                        c.w_chan)))
2135
        af.append(
2136
            RadioSetting(prefix + ".scan_grp",
2137
                         "Scan Group (Menu 49)",
2138
                         RadioSettingValueList(
2139
                             SCANGRP_LIST,
2140
                             SCANGRP_LIST[c.scan_grp])))
2141
        af.append(RadioSetting(prefix + ".bcl",
2142
                               "Busy Channel Lock-out (Menu 15)",
2143
                               RadioSettingValueBoolean(c.bcl)))
2144
        af.append(
2145
            RadioSetting(prefix + ".sql",
2146
                         "Squelch Level (Menu 8)",
2147
                         RadioSettingValueList(LIST_0_9,
2148
                                               LIST_0_9[c.sql])))
2149
        af.append(
2150
            RadioSetting(prefix + ".cset",
2151
                         "Call ID Group (Menu 52)",
2152
                         RadioSettingValueList(LIST_1_20,
2153
                                               LIST_1_20[c.cset])))
2154
        af.append(
2155
            RadioSetting(prefix + ".step",
2156
                         "Frequency Step (Menu 3)",
2157
                         RadioSettingValueList(
2158
                             STEP_LIST, STEP_LIST[c.step])))
2159
        af.append(
2160
            RadioSetting(prefix + ".scan_mode",
2161
                         "Scan Mode (Menu 20)",
2162
                         RadioSettingValueList(
2163
                             SCANMODE_LIST,
2164
                             SCANMODE_LIST[c.scan_mode])))
2165
        af.append(
2166
            RadioSetting(prefix + ".scan_range",
2167
                         "Scan Range (Menu 50)",
2168
                         RadioSettingValueList(
2169
                             SCANRANGE_LIST,
2170
                             SCANRANGE_LIST[c.scan_range])))
2171
        st = RadioSettingValueString(0, 15,
2172
                                     short2freq(scan_rng.scan_st))
2173
        rs = RadioSetting("settings.%s.scan_st" % area,
2174
                          "Frequency Scan Start", st)
2175
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2176
        af.append(rs)
2177

    
2178
        end = RadioSettingValueString(0, 15,
2179
                                      short2freq(scan_rng.scan_end))
2180
        rs = RadioSetting("settings.%s.scan_end" % area,
2181
                          "Frequency Scan End", end)
2182
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2183
                              scan_rng)
2184
        af.append(rs)
2185
        # Each area has its own set of bands
2186
        for band in (band_list):
2187
            af.append(self._band_tab(area, band))
2188
        return af
2189

    
2190
    def _key_tab(self):
2191
        """Build radio key/button menu
2192
        """
2193
        s = self._memobj.settings
2194
        if self.MODEL == "KG-UV9PX":
2195
            pfkey1 = PF1KEY_LIST
2196
            pfkey2 = PF2KEY_LIST
2197
            pfkey3 = PF3KEY_LIST2
2198
        elif self.MODEL == "KG-UV9GX":
2199
            pfkey1 = PF1KEY_LIST9GX
2200
            pfkey2 = PF2KEY_LIST9GX
2201
            pfkey3 = PF3KEY_LIST9GX
2202
        else:
2203
            pfkey1 = PF1KEY_LIST
2204
            pfkey2 = PF2KEY_LIST
2205
            pfkey3 = PF3KEY_LIST
2206

    
2207
        kf = RadioSettingGroup("key_grp", "Key Settings")
2208

    
2209
        kf.append(RadioSetting("settings.pf1",
2210
                               "PF1 Key function (Menu 55)",
2211
                               RadioSettingValueList(
2212
                                   pfkey1,
2213
                                   pfkey1[s.pf1])))
2214
        kf.append(RadioSetting("settings.pf2",
2215
                               "PF2 Key function (Menu 56)",
2216
                               RadioSettingValueList(
2217
                                   pfkey2,
2218
                                   pfkey2[s.pf2])))
2219

    
2220
        kf.append(RadioSetting("settings.pf3",
2221
                               "PF3 Key function (Menu 57)",
2222
                               RadioSettingValueList(
2223
                                   pfkey3,
2224
                                   pfkey3[s.pf3])))
2225
        return kf
2226

    
2227
    def _fl_tab(self):
2228
        """Build the frequency limits tab
2229
        """
2230

    
2231
        # The stop limits in the factory KG-UV9D Mate memory image are 1MHz
2232
        # higher than the published specs. The settings panel will crash if
2233
        # it encounters a value outside of these ranges.
2234
        hard_limits = {
2235
            "band_150": (108000000, 181000000),
2236
            "band_450": (400000000, 513000000),
2237
            "band_300": (350000000, 401000000),
2238
            "band_700": (700000000, 987000000),
2239
            "band_200": (230000000, 251000000)
2240
        }
2241

    
2242
        def apply_freq_start(setting, low, high, obj):
2243
            f = freq2short(setting.value, low, high)
2244
            obj.start = f
2245

    
2246
        def apply_freq_stop(setting, low, high, obj):
2247
            """Sets the stop limit to 1MHz below the input value"""
2248

    
2249
            # The firmware has an off-by-1MHz error with stop limits.
2250
            # If you set the stop limit to 1480 (148MHz), you can still tune
2251
            # up to 148.99MHz. To compensate for this,
2252
            # we subtract 10 increments of 100MHz before storing the value.
2253
            f = freq2short(setting.value, low, high) - 10
2254
            obj.stop = f
2255

    
2256
        fl = RadioSettingGroup("freq_limit_grp", "Frequency Limits")
2257

    
2258
        rx = self._memobj.rx_freq_limits
2259
        tx = self._memobj.tx_freq_limits
2260

    
2261
        for rx_band in rx.items():
2262
            name, limits = rx_band
2263

    
2264
            start_freq = RadioSettingValueString(1,
2265
                                                 20,
2266
                                                 short2freq(limits.start))
2267
            start_rs = RadioSetting("rx_start_" + name,
2268
                                    name + " Receive Start",
2269
                                    start_freq)
2270
            start_rs.set_apply_callback(apply_freq_start,
2271
                                        hard_limits[name][0],
2272
                                        hard_limits[name][1],
2273
                                        limits)
2274
            fl.append(start_rs)
2275

    
2276
            # Add 10 increments of 100MHz before displaying to compensate for
2277
            # the firmware off-by-1MHz problem.
2278
            stop_freq = RadioSettingValueString(1,
2279
                                                20,
2280
                                                short2freq(limits.stop + 10))
2281
            stop_rs = RadioSetting("rx_stop_" + name,
2282
                                   name + " Receive Stop",
2283
                                   stop_freq)
2284
            stop_rs.set_apply_callback(apply_freq_stop,
2285
                                       hard_limits[name][0],
2286
                                       hard_limits[name][1],
2287
                                       limits)
2288
            fl.append(stop_rs)
2289

    
2290
        for tx_band in tx.items():
2291
            name, limits = tx_band
2292

    
2293
            start_freq = RadioSettingValueString(1,
2294
                                                 20,
2295
                                                 short2freq(limits.start))
2296
            start_rs = RadioSetting("tx_start_" + name,
2297
                                    name + " Transmit Start",
2298
                                    start_freq)
2299
            start_rs.set_apply_callback(apply_freq_start,
2300
                                        hard_limits[name][0],
2301
                                        hard_limits[name][1], limits)
2302
            fl.append(start_rs)
2303

    
2304
            # Add 10 increments of 100MHz before displaying to compensate for
2305
            # the firmware off-by-1MHz problem.
2306
            stop_freq = RadioSettingValueString(1,
2307
                                                20,
2308
                                                short2freq(limits.stop + 10))
2309
            stop_rs = RadioSetting("tx_stop_" + name,
2310
                                   name + " Transmit Stop",
2311
                                   stop_freq)
2312
            stop_rs.set_apply_callback(apply_freq_stop,
2313
                                       hard_limits[name][0],
2314
                                       hard_limits[name][1],
2315
                                       limits)
2316
            fl.append(stop_rs)
2317

    
2318
        return fl
2319

    
2320
    def _get_settings(self):
2321
        """Build the radio configuration settings menus
2322
        """
2323

    
2324
        core_grp = self._core_tab()
2325
        fm_grp = self._fm_tab()
2326
        area_a_grp = self._area_tab("a")
2327
        area_b_grp = self._area_tab("b")
2328
        key_grp = self._key_tab()
2329
        scan_grp = self._scan_grp()
2330
        callid_grp = self._callid_grp()
2331
        admin_grp = self._admin_tab()
2332
        rpt_grp = self._repeater_tab()
2333
        freq_limit_grp = self._fl_tab()
2334

    
2335
        core_grp.append(key_grp)
2336
        core_grp.append(admin_grp)
2337
        core_grp.append(rpt_grp)
2338
        core_grp.append(freq_limit_grp)
2339
        group = RadioSettings(core_grp,
2340
                              area_a_grp,
2341
                              area_b_grp,
2342
                              fm_grp,
2343
                              scan_grp,
2344
                              callid_grp
2345
                              )
2346
        return group
2347

    
2348
    def get_settings(self):
2349
        """ Public build out linkage between radio settings and UI
2350
        """
2351
        try:
2352
            return self._get_settings()
2353
        except Exception:
2354
            import traceback
2355
            LOG.error("Failed to parse settings: %s",
2356
                      traceback.format_exc())
2357
            return None
2358

    
2359
    def _is_freq(self, element):
2360
        """This is a hack to smoke out whether we need to do
2361
        frequency translations for otherwise innocent u16s and u32s
2362
        """
2363
        return "rxfreq" in element.get_name() or \
2364
               "txfreq" in element.get_name() or \
2365
               "scan_st" in element.get_name() or \
2366
               "scan_end" in element.get_name() or \
2367
               "offset" in element.get_name() or \
2368
               "fm_stop" in element.get_name()
2369

    
2370
    def _is_limit(self, element):
2371
        return "lower_limit" in element.get_name() or\
2372
               "upper_limit" in element.get_name()
2373

    
2374
    def set_settings(self, settings):
2375
        """ Public update radio settings via UI callback
2376
        A lot of this should be in common code....
2377
        """
2378

    
2379
        for element in settings:
2380
            if not isinstance(element, RadioSetting):
2381
                LOG.debug("set_settings: not instance %s" %
2382
                          element.get_name())
2383
                self.set_settings(element)
2384
                continue
2385
            else:
2386
                try:
2387
                    if "." in element.get_name():
2388
                        bits = element.get_name().split(".")
2389
                        obj = self._memobj
2390
                        for bit in bits[:-1]:
2391
                            # decode an array index
2392
                            if "[" in bit and "]" in bit:
2393
                                bit, index = bit.split("[", 1)
2394
                                index, junk = index.split("]", 1)
2395
                                index = int(index)
2396
                                obj = getattr(obj, bit)[index]
2397
                            else:
2398
                                obj = getattr(obj, bit)
2399
                        setting = bits[-1]
2400
                    else:
2401
                        obj = self._memobj.settings
2402
                        setting = element.get_name()
2403

    
2404
                    if element.has_apply_callback():
2405
                        LOG.debug("Using apply callback")
2406
                        element.run_apply_callback()
2407
                    else:
2408
                        LOG.debug("Setting %s = %s" %
2409
                                  (setting, element.value))
2410
                        if self._is_freq(element):
2411
                            setattr(obj, setting, int(element.value)/10)
2412
                        elif self._is_limit(element):
2413
                            setattr(obj, setting, int(element.value)*10)
2414
                        else:
2415
                            setattr(obj, setting, element.value)
2416
                except Exception as e:
2417
                    LOG.debug("set_settings: Exception with %s" %
2418
                              element.get_name())
2419
                    raise
2420

    
2421
    def callid2str(self, cid):
2422
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2423
        One digit (binary) per byte, terminated with '0xc'
2424
        """
2425

    
2426
        bin2ascii = " 1234567890"
2427
        cidstr = ""
2428
        for i in range(0, 6):
2429
            b = cid[i].get_value()
2430
            if b == 0xc:  # the cid EOL
2431
                break
2432
            if b == 0 or b > 0xa:
2433
                raise InvalidValueError(
2434
                    "Caller ID code has illegal byte 0x%x" % b)
2435
            cidstr += bin2ascii[b]
2436
        return cidstr
2437

    
2438

    
2439
@directory.register
2440
class KGUV9PXRadio(KGUV9DPlusRadio):
2441

    
2442
    """Wouxun KG-UV9PX"""
2443
    VENDOR = "Wouxun"
2444
    MODEL = "KG-UV9PX"
2445
    _model = b"KG-UV9D"
2446
    _rev = b"02"  # default rev for the radio I know about...
2447
    _file_ident = b"kg-uv9px"
2448
    NEEDS_COMPAT_SERIAL = False
2449

    
2450
    def process_mmap(self):
2451
        if self._rev != b"02" and self._rev != b"00":
2452
            # new revision found - log it and assume same map and proceed
2453
            LOG.debug("Unrecognized model variation (%s) Using default Map" %
2454
                      self._rev)
2455
        self._memobj = bitwise.parse(_MEM_FORMAT_9PX, self._mmap)
2456

    
2457
    def get_features(self):
2458
        """ Public get_features
2459
            Return the features of this radio once we have identified
2460
            it and gotten its bits
2461
            """
2462
        rf = chirp_common.RadioFeatures()
2463
        rf.has_settings = True
2464
        rf.has_ctone = True
2465
        rf.has_rx_dtcs = True
2466
        rf.has_cross = True
2467
        rf.has_tuning_step = False
2468
        rf.has_bank = False
2469
        rf.can_odd_split = True
2470
        rf.valid_skips = ["", "S"]
2471
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
2472
        rf.valid_cross_modes = [
2473
            "Tone->Tone",
2474
            "Tone->DTCS",
2475
            "DTCS->Tone",
2476
            "DTCS->",
2477
            "->Tone",
2478
            "->DTCS",
2479
            "DTCS->DTCS",
2480
        ]
2481
        rf.valid_modes = ["FM", "NFM", "AM"]
2482
        rf.valid_power_levels = self.POWER_LEVELS
2483
        rf.valid_name_length = 8
2484
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
2485
        rf.valid_bands = [(108000000, 135997500),  # Aircraft  AM
2486
                          (136000000, 180997500),  # supports 2m
2487
                          (219000000, 250997500),
2488
                          (350000000, 399997500),
2489
                          (400000000, 512997500),  # supports 70cm
2490
                          (700000000, 986997500)]
2491
        rf.valid_characters = chirp_common.CHARSET_ASCII
2492
        rf.valid_tuning_steps = STEPS
2493
        rf.memory_bounds = (1, 999)  # 999 memories
2494
        return rf
2495

    
2496
    def callid2str(self, cid):
2497
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2498
        One digit (binary) per byte, terminated with '0xc'
2499
        """
2500

    
2501
        bin2ascii = " 1234567890"
2502
        cidstr = ""
2503
        for i in range(0, 6):
2504
            b = cid[i].get_value()
2505
            # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
2506
            # remap 0x00 to 0x0a
2507
            if b == 0x00:
2508
                b = 0x0a
2509
            if b == 0xc or b == 0xf0:  # the cid EOL
2510
                break
2511
            if b > 0xa:
2512
                raise InvalidValueError(
2513
                    "Caller ID code has illegal byte 0x%x" % b)
2514
            cidstr += bin2ascii[b]
2515
        return cidstr
2516

    
2517
    def _get_settings(self):
2518
        """Build the radio configuration settings menus
2519
        """
2520

    
2521
        core_grp = self._core_tab()
2522
        fm_grp = self._fm_tab()
2523
        area_a_grp = self._area_tab("a")
2524
        area_b_grp = self._area_tab("b")
2525
        key_grp = self._key_tab()
2526
        scan_grp = self._scan_grp()
2527
        callid_grp = self._callid_grp()
2528
        admin_grp = self._admin_tab()
2529
        rpt_grp = self._repeater_tab()
2530
        freq_limit_grp = self._fl_tab()
2531
        core_grp.append(key_grp)
2532
        core_grp.append(admin_grp)
2533
        core_grp.append(rpt_grp)
2534
        group = RadioSettings(core_grp,
2535
                              area_a_grp,
2536
                              area_b_grp,
2537
                              fm_grp,
2538
                              scan_grp,
2539
                              callid_grp,
2540
                              freq_limit_grp,)
2541
        return group
2542

    
2543
    def _area_tab(self, area):
2544
        """Build a VFO tab
2545
        """
2546
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2547
            f = freq2short(setting.value, scan_lo, scan_hi)
2548
            obj.scan_st = f
2549

    
2550
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2551
            f = freq2short(setting.value, scan_lo, scan_hi)
2552
            obj.scan_end = f
2553

    
2554
        if area == "a":
2555
            desc = "Receiver A Settings"
2556
            c = self._memobj.a_conf
2557
            scan_lo = 108000000
2558
            scan_hi = 985997500
2559
            scan_rng = self._memobj.settings.a
2560
            band_list = (150, 200, 300, 450, 700)
2561
        else:
2562
            desc = "Receiver B Settings"
2563
            c = self._memobj.b_conf
2564
            scan_lo = 136000000
2565
            scan_hi = 512997500
2566
            scan_rng = self._memobj.settings.b
2567
            band_list = (150, 450)
2568

    
2569
        prefix = "%s_conf" % area
2570
        af = RadioSettingGroup(prefix, desc)
2571
        af.append(
2572
            RadioSetting(prefix + ".w_mode",
2573
                         "Workmode",
2574
                         RadioSettingValueList(
2575
                             WORKMODE_LIST,
2576
                             WORKMODE_LIST[c.w_mode])))
2577
        af.append(RadioSetting(prefix + ".w_chan",
2578
                               "Channel",
2579
                               RadioSettingValueInteger(1, 999,
2580
                                                        c.w_chan)))
2581
        af.append(
2582
            RadioSetting(prefix + ".scan_grp",
2583
                         "Scan Group (Menu 49)",
2584
                         RadioSettingValueList(
2585
                             SCANGRP_LIST,
2586
                             SCANGRP_LIST[c.scan_grp])))
2587
        af.append(RadioSetting(prefix + ".bcl",
2588
                               "Busy Channel Lock-out (Menu 15)",
2589
                               RadioSettingValueBoolean(c.bcl)))
2590
        af.append(
2591
            RadioSetting(prefix + ".sql",
2592
                         "Squelch Level (Menu 8)",
2593
                         RadioSettingValueList(LIST_0_9,
2594
                                               LIST_0_9[c.sql])))
2595
        af.append(
2596
            RadioSetting(prefix + ".cset",
2597
                         "Call ID Group (Menu 52)",
2598
                         RadioSettingValueList(LIST_1_20,
2599
                                               LIST_1_20[c.cset])))
2600
        af.append(
2601
            RadioSetting(prefix + ".step",
2602
                         "Frequency Step (Menu 3)",
2603
                         RadioSettingValueList(
2604
                             STEP_LIST, STEP_LIST[c.step])))
2605
        af.append(
2606
            RadioSetting(prefix + ".scan_mode",
2607
                         "Scan Mode (Menu 20)",
2608
                         RadioSettingValueList(
2609
                             SCANMODE_LIST,
2610
                             SCANMODE_LIST[c.scan_mode])))
2611
        af.append(
2612
            RadioSetting(prefix + ".scan_range",
2613
                         "Scan Range (Menu 50)",
2614
                         RadioSettingValueList(
2615
                             SCANRANGE_LIST,
2616
                             SCANRANGE_LIST[c.scan_range])))
2617
        st = RadioSettingValueString(0, 15,
2618
                                     short2freq(scan_rng.scan_st))
2619
        rs = RadioSetting("settings.%s.scan_st" % area,
2620
                          "Frequency Scan Start", st)
2621
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2622
        af.append(rs)
2623

    
2624
        end = RadioSettingValueString(0, 15,
2625
                                      short2freq(scan_rng.scan_end))
2626
        rs = RadioSetting("settings.%s.scan_end" % area,
2627
                          "Frequency Scan End", end)
2628
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2629
                              scan_rng)
2630
        af.append(rs)
2631
        # Each area has its own set of bands
2632
        for band in (band_list):
2633
            af.append(self._band_tab(area, band))
2634
        return af
2635

    
2636
    def _band_tab(self, area, band):
2637
        """ Build a band tab inside a VFO/Area
2638
        """
2639
        def apply_freq(setting, lo, hi, obj):
2640
            f = freq2int(setting.value, lo, hi)
2641
            obj.freq = f/10
2642

    
2643
        def apply_offset(setting, obj):
2644
            f = freq2int(setting.value, 0, 5000000)
2645
            obj.offset = f/10
2646

    
2647
        def apply_enc(setting, obj):
2648
            t = tone2short(setting.value)
2649
            obj.encqt = t
2650

    
2651
        def apply_dec(setting, obj):
2652
            t = tone2short(setting.value)
2653
            obj.decqt = t
2654

    
2655
        if area == "a":
2656
            if band == 150:
2657
                c = self._memobj.vfo_a.band_150
2658
                lo = 108000000
2659
                hi = 180997500
2660
            elif band == 200:
2661
                c = self._memobj.vfo_a.band_200
2662
                lo = 219000000
2663
                hi = 250997500
2664
            elif band == 300:
2665
                c = self._memobj.vfo_a.band_300
2666
                lo = 350000000
2667
                hi = 399997500
2668
            elif band == 450:
2669
                c = self._memobj.vfo_a.band_450
2670
                lo = 400000000
2671
                hi = 512997500
2672
            else:   # 700
2673
                c = self._memobj.vfo_a.band_700
2674
                lo = 700000000
2675
                hi = 986997500
2676
        else:  # area 'b'
2677
            if band == 150:
2678
                c = self._memobj.vfo_b.band_150
2679
                lo = 136000000
2680
                hi = 180997500
2681
            else:  # 450
2682
                c = self._memobj.vfo_b.band_450
2683
                lo = 400000000
2684
                hi = 512997500
2685

    
2686
        prefix = "vfo_%s.band_%d" % (area, band)
2687
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2688
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2689
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2690
        rs.set_apply_callback(apply_freq, lo, hi, c)
2691
        bf.append(rs)
2692

    
2693
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2694
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2695
        rs.set_apply_callback(apply_offset, c)
2696
        bf.append(rs)
2697

    
2698
        rs = RadioSetting(prefix + ".encqt",
2699
                          "Encode QT (Menu 17,19)",
2700
                          RadioSettingValueList(TONE_LIST,
2701
                                                short2tone(c.encqt)))
2702
        rs.set_apply_callback(apply_enc, c)
2703
        bf.append(rs)
2704

    
2705
        rs = RadioSetting(prefix + ".decqt",
2706
                          "Decode QT (Menu 16,18)",
2707
                          RadioSettingValueList(TONE_LIST,
2708
                                                short2tone(c.decqt)))
2709
        rs.set_apply_callback(apply_dec, c)
2710
        bf.append(rs)
2711

    
2712
        bf.append(RadioSetting(prefix + ".qt",
2713
                               "Mute Mode (Menu 21)",
2714
                               RadioSettingValueList(SPMUTE_LIST,
2715
                                                     SPMUTE_LIST[c.qt])))
2716
        bf.append(RadioSetting(prefix + ".scan",
2717
                               "Scan this (Menu 48)",
2718
                               RadioSettingValueBoolean(c.scan)))
2719
        bf.append(RadioSetting(prefix + ".pwr",
2720
                               "Power (Menu 5)",
2721
                               RadioSettingValueList(
2722
                                   POWER_LIST, POWER_LIST[c.pwr])))
2723
        bf.append(RadioSetting(prefix + ".mod",
2724
                               "AM Modulation (Menu 54)",
2725
                               RadioSettingValueBoolean(c.mod)))
2726
        bf.append(RadioSetting(prefix + ".fm_dev",
2727
                               "FM Deviation (Menu 4)",
2728
                               RadioSettingValueList(
2729
                                   BANDWIDTH_LIST,
2730
                                   BANDWIDTH_LIST[c.fm_dev])))
2731
        bf.append(
2732
            RadioSetting(prefix + ".shift",
2733
                         "Frequency Shift (Menu 6)",
2734
                         RadioSettingValueList(OFFSET_LIST,
2735
                                               OFFSET_LIST[c.shift])))
2736
        return bf
2737

    
2738
    def _fl_tab(self):
2739

    
2740
        freq_limit_grp = RadioSettingGroup("limits",
2741
                                           "Freq Limits")
2742
        limgrp = freq_limit_grp
2743

    
2744
        l = self._memobj.limits
2745

    
2746
        if self.MODEL == "KG-UV9PX":
2747
            val = RadioSettingValueInteger(136, 180,
2748
                                           (l.lim_150M_Txlower_limit) / 10.0)
2749
            rs = RadioSetting("limits.lim_150M_Txlower_limit",
2750
                              "150M Tx Lower Limit (MHz)",
2751
                              RadioSettingValueInteger(136, 180, val))
2752
            limgrp.append(rs)
2753

    
2754
            val = RadioSettingValueInteger(136, 180,
2755
                                           (l.lim_150M_Txupper_limit) / 10.0)
2756
            rs = RadioSetting("limits.lim_150M_Txupper_limit",
2757
                              "150M Tx Upper Limit (MHz + 0.9975)",
2758
                              RadioSettingValueInteger(136, 180, val))
2759
            limgrp.append(rs)
2760

    
2761
            val = RadioSettingValueInteger(400, 512,
2762
                                           (l.lim_450M_Txlower_limit) / 10.0)
2763
            rs = RadioSetting("limits.lim_450M_Txlower_limit",
2764
                              "450M Tx Lower Limit (MHz)",
2765
                              RadioSettingValueInteger(400, 512, val))
2766
            limgrp.append(rs)
2767

    
2768
            val = RadioSettingValueInteger(400, 512,
2769
                                           (l.lim_450M_Txupper_limit) / 10.0)
2770
            rs = RadioSetting("limits.lim_450M_Txupper_limit",
2771
                              "450M Tx Upper Limit (MHz + 0.9975)",
2772
                              RadioSettingValueInteger(400, 512, val))
2773
            limgrp.append(rs)
2774

    
2775
        val = RadioSettingValueInteger(108, 180,
2776
                                       (l.lim_150M_area_a_rxlower_limit) /
2777
                                       10.0)
2778
        rs = RadioSetting("limits.lim_150M_area_a_rxlower_limit",
2779
                          "Rcvr A 150M Rx Lower Limit (MHz)",
2780
                          RadioSettingValueInteger(108, 180,
2781
                                                   val))
2782
        limgrp.append(rs)
2783

    
2784
        val = RadioSettingValueInteger(108, 180,
2785
                                       (l.lim_150M_area_a_rxupper_limit) /
2786
                                       10.0)
2787
        rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
2788
                          "Rcvr A 150M Rx Upper Limit (MHz + 0.9975)",
2789
                          RadioSettingValueInteger(108, 180,
2790
                                                   val))
2791
        limgrp.append(rs)
2792

    
2793
        val = RadioSettingValueInteger(136, 180,
2794
                                       (l.lim_150M_area_b_rxlower_limit) /
2795
                                       10.0)
2796
        rs = RadioSetting("limits.lim_150M_area_b_rxlower_limit",
2797
                          "Rcvr B 150M Rx Lower Limit (MHz)",
2798
                          RadioSettingValueInteger(136, 180,
2799
                                                   val))
2800
        limgrp.append(rs)
2801

    
2802
        val = RadioSettingValueInteger(136, 180,
2803
                                       (l.lim_150M_area_b_rxupper_limit) /
2804
                                       10.0)
2805
        rs = RadioSetting("limits.lim_150M_area_b_rxupper_limit",
2806
                          "Rcvr B 150M Rx Upper Limit (MHz + 0.9975)",
2807
                          RadioSettingValueInteger(136, 180,
2808
                                                   val))
2809
        limgrp.append(rs)
2810

    
2811
        val = RadioSettingValueInteger(400, 512,
2812
                                       (l.lim_450M_rxlower_limit) / 10.0)
2813
        rs = RadioSetting("limits.lim_450M_rxlower_limit",
2814
                          "450M Rx Lower Limit (MHz)",
2815
                          RadioSettingValueInteger(400, 512,
2816
                                                   val))
2817
        limgrp.append(rs)
2818

    
2819
        val = RadioSettingValueInteger(400, 512,
2820
                                       (l.lim_450M_rxupper_limit) / 10.0)
2821
        rs = RadioSetting("limits.lim_450M_rxupper_limit",
2822
                          "450M Rx Upper Limit (MHz + 0.9975)",
2823
                          RadioSettingValueInteger(400, 512,
2824
                                                   val))
2825
        limgrp.append(rs)
2826

    
2827
        val = RadioSettingValueInteger(350, 399,
2828
                                       (l.lim_300M_rxlower_limit) / 10.0)
2829
        rs = RadioSetting("limits.lim_300M_rxlower_limit",
2830
                          "300M Rx Lower Limit (MHz)",
2831
                          RadioSettingValueInteger(350, 399,
2832
                                                   val))
2833
        limgrp.append(rs)
2834

    
2835
        val = RadioSettingValueInteger(350, 399,
2836
                                       (l.lim_300M_rxupper_limit) / 10.0)
2837
        rs = RadioSetting("limits.lim_300M_rxupper_limit",
2838
                          "300M Rx Upper Limit (MHz + 0.9975)",
2839
                          RadioSettingValueInteger(350, 399,
2840
                                                   val))
2841
        limgrp.append(rs)
2842
        val = RadioSettingValueInteger(700, 986,
2843
                                       (l.lim_800M_rxlower_limit) / 10.0)
2844
        rs = RadioSetting("limits.lim_800M_rxlower_limit",
2845
                          "800M Rx Lower Limit (MHz)",
2846
                          RadioSettingValueInteger(700, 986,
2847
                                                   val))
2848
        limgrp.append(rs)
2849

    
2850
        val = RadioSettingValueInteger(700, 986,
2851
                                       (l.lim_800M_rxupper_limit) / 10.0)
2852
        rs = RadioSetting("limits.lim_800M_rxupper_limit",
2853
                          "800M Rx Upper Limit (MHz + 0.9975)",
2854
                          RadioSettingValueInteger(700, 986,
2855
                                                   val))
2856
        limgrp.append(rs)
2857

    
2858
        val = RadioSettingValueInteger(219, 250,
2859
                                       (l.lim_210M_rxlower_limit) / 10.0)
2860
        rs = RadioSetting("limits.lim_210M_rxlower_limit",
2861
                          "210M Rx Lower Limit (MHz)",
2862
                          RadioSettingValueInteger(219, 250,
2863
                                                   val))
2864
        limgrp.append(rs)
2865

    
2866
        val = RadioSettingValueInteger(219, 250,
2867
                                       (l.lim_210M_rxupper_limit) / 10.0)
2868
        rs = RadioSetting("limits.lim_210M_rxupper_limit",
2869
                          "210M Rx Upper Limit (MHz + 0.9975)",
2870
                          RadioSettingValueInteger(219, 250,
2871
                                                   val))
2872
        limgrp.append(rs)
2873

    
2874
        return limgrp
2875

    
2876

    
2877
@directory.register
2878
class KGUV9GXRadio(KGUV9PXRadio):
2879

    
2880
    """Wouxun KG-UV9GX"""
2881
    VENDOR = "Wouxun"
2882
    MODEL = "KG-UV9GX"
2883
    _model = b"KG-UV9D"
2884
    _rev = b"02"  # default rev for the radio I know about...
2885
    NEEDS_COMPAT_SERIAL = False
2886

    
2887
@directory.register
2888
class KGUV9KRadio(KGUV9DPlusRadio):
2889

    
2890
    """Wouxun KG-UV9K"""
2891
    VENDOR = "Wouxun"
2892
    MODEL = "KG-UV9K"
2893
    _model = b"KG-UV9D"
2894
    _rev = b"02"  # default rev for the radio I know about...
2895
    NEEDS_COMPAT_SERIAL = False
2896

    
2897
    def callid2str(self, cid):
2898
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2899
        One digit (binary) per byte, terminated with '0xc'
2900
        """
2901

    
2902
        bin2ascii = " 1234567890"
2903
        cidstr = ""
2904
        for i in range(0, 6):
2905
            b = cid[i].get_value()
2906
            # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
2907
            # remap 0x00 to 0x0a
2908
            if b == 0x00:
2909
                b = 0x0a
2910
            if b == 0xc or b == 0xf0:  # the cid EOL
2911
                break
2912
            if b > 0xa:
2913
                raise InvalidValueError(
2914
                    "Caller ID code has illegal byte 0x%x" % b)
2915
            cidstr += bin2ascii[b]
2916
        return cidstr
2917

    
2918
    def get_features(self):
2919
        """ Public get_features
2920
            Return the features of this radio once we have identified
2921
            it and gotten its bits
2922
            """
2923
        rf = chirp_common.RadioFeatures()
2924
        rf.has_settings = True
2925
        rf.has_ctone = True
2926
        rf.has_rx_dtcs = True
2927
        rf.has_cross = True
2928
        rf.has_tuning_step = False
2929
        rf.has_bank = False
2930
        rf.can_odd_split = True
2931
        rf.valid_skips = ["", "S"]
2932
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
2933
        rf.valid_cross_modes = [
2934
            "Tone->Tone",
2935
            "Tone->DTCS",
2936
            "DTCS->Tone",
2937
            "DTCS->",
2938
            "->Tone",
2939
            "->DTCS",
2940
            "DTCS->DTCS",
2941
        ]
2942
        rf.valid_modes = ["FM", "NFM", "AM"]
2943
        rf.valid_power_levels = self.POWER_LEVELS
2944
        rf.valid_name_length = 8
2945
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
2946
        rf.valid_bands = [(108000000, 136000000),  # Aircraft  AM
2947
                          (136000000, 180000000),  # supports 2m
2948
                          (230000000, 250000000),
2949
                          (350000000, 400000000),
2950
                          (400000000, 520000000),  # supports 70cm
2951
                          (700000000, 985000000)]
2952
        rf.valid_characters = chirp_common.CHARSET_ASCII
2953
        rf.valid_tuning_steps = STEPS_9K
2954
        rf.memory_bounds = (1, 999)  # 999 memories
2955
        return rf
2956

    
2957
    def _area_tab(self, area):
2958
        """Build a VFO tab
2959
        """
2960
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2961
            f = freq2short(setting.value, scan_lo, scan_hi)
2962
            obj.scan_st = f
2963

    
2964
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2965
            f = freq2short(setting.value, scan_lo, scan_hi)
2966
            obj.scan_end = f
2967

    
2968
        if area == "a":
2969
            desc = "Area A Settings"
2970
            c = self._memobj.a_conf
2971
            scan_lo = 108000000
2972
            scan_hi = 985000000
2973
            scan_rng = self._memobj.settings.a
2974
            band_list = (150, 200, 300, 450, 700)
2975
        else:
2976
            desc = "Area B Settings"
2977
            c = self._memobj.b_conf
2978
            scan_lo = 136000000
2979
            scan_hi = 512000000
2980
            scan_rng = self._memobj.settings.b
2981
            band_list = (150, 450)
2982

    
2983
        prefix = "%s_conf" % area
2984
        af = RadioSettingGroup(prefix, desc)
2985
        af.append(
2986
            RadioSetting(prefix + ".w_mode",
2987
                         "Workmode",
2988
                         RadioSettingValueList(
2989
                             WORKMODE_LIST,
2990
                             WORKMODE_LIST[c.w_mode])))
2991
        af.append(RadioSetting(prefix + ".w_chan",
2992
                               "Channel",
2993
                               RadioSettingValueInteger(1, 999,
2994
                                                        c.w_chan)))
2995
        af.append(
2996
            RadioSetting(prefix + ".scan_grp",
2997
                         "Scan Group (Menu 49)",
2998
                         RadioSettingValueList(
2999
                             SCANGRP_LIST,
3000
                             SCANGRP_LIST[c.scan_grp])))
3001
        af.append(RadioSetting(prefix + ".bcl",
3002
                               "Busy Channel Lock-out (Menu 15)",
3003
                               RadioSettingValueBoolean(c.bcl)))
3004
        af.append(
3005
            RadioSetting(prefix + ".sql",
3006
                         "Squelch Level (Menu 8)",
3007
                         RadioSettingValueList(LIST_0_9,
3008
                                               LIST_0_9[c.sql])))
3009
        af.append(
3010
            RadioSetting(prefix + ".cset",
3011
                         "Call ID Group (Menu 52)",
3012
                         RadioSettingValueList(LIST_1_20,
3013
                                               LIST_1_20[c.cset])))
3014
        af.append(
3015
            RadioSetting(prefix + ".step",
3016
                         "Frequency Step (Menu 3)",
3017
                         RadioSettingValueList(
3018
                             STEP_LIST_9K, STEP_LIST_9K[c.step])))
3019
        af.append(
3020
            RadioSetting(prefix + ".scan_mode",
3021
                         "Scan Mode (Menu 20)",
3022
                         RadioSettingValueList(
3023
                             SCANMODE_LIST,
3024
                             SCANMODE_LIST[c.scan_mode])))
3025
        af.append(
3026
            RadioSetting(prefix + ".scan_range",
3027
                         "Scan Range (Menu 50)",
3028
                         RadioSettingValueList(
3029
                             SCANRANGE_LIST,
3030
                             SCANRANGE_LIST[c.scan_range])))
3031
        st = RadioSettingValueString(0, 15,
3032
                                     short2freq(scan_rng.scan_st))
3033
        rs = RadioSetting("settings.%s.scan_st" % area,
3034
                          "Frequency Scan Start", st)
3035
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
3036
        af.append(rs)
3037

    
3038
        end = RadioSettingValueString(0, 15,
3039
                                      short2freq(scan_rng.scan_end))
3040
        rs = RadioSetting("settings.%s.scan_end" % area,
3041
                          "Frequency Scan End", end)
3042
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
3043
                              scan_rng)
3044
        af.append(rs)
3045
        # Each area has its own set of bands
3046
        for band in (band_list):
3047
            af.append(self._band_tab(area, band))
3048
        return af
(7-7/8)