Project

General

Profile

Bug #10482 » kguv9dplus.py

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

    
1069

    
1070
@directory.register
1071
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
1072
                      chirp_common.ExperimentalRadio):
1073

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

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

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

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

    
1127
        packet = _pkt_encode(cmd, payload)
1128
        self.pipe.write(packet)
1129

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1412
        mem.dtcs_polarity = pt + pr
1413

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

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

    
1424
        mem = chirp_common.Memory()
1425
        mem.number = number
1426
        _valid = _mem.state
1427

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

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

    
1464
        mem.freq = int(_mem.rxfreq) * 10
1465

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

    
1480
        mem.name = name2str(_nam.name)
1481

    
1482
        self._get_tone(_mem, mem)
1483

    
1484
        mem.skip = "" if bool(_mem.scan) else "S"
1485

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

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

    
1506
        rx_mode = tx_mode = None
1507
        rxtone = txtone = 0x0000
1508

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

    
1530
        _mem.decQT = rxtone
1531
        _mem.encQT = txtone
1532

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

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

    
1543
        _mem = self._memobj.chan_blk[number - 1]
1544
        _nam = self._memobj.chan_name[number - 1]
1545

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

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

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

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

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

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

    
1612
            sm = self._memobj.screen
1613

    
1614
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1615

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

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

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

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

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

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

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

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

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

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

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

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

    
1814
        pw = self._memobj.passwords  # admin passwords
1815
        s = self._memobj.settings
1816

    
1817
        cf = RadioSettingGroup("admin", "Admin Functions")
1818

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

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

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

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

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

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

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

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

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

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

    
1901
        return cf
1902

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2205
        kf = RadioSettingGroup("key_grp", "Key Settings")
2206

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

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

    
2225
    def _fl_tab(self):
2226
        """Build the frequency limits tab
2227
        """
2228

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

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

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

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

    
2254
        fl = RadioSettingGroup("freq_limit_grp", "Frequency Limits")
2255

    
2256
        rx = self._memobj.rx_freq_limits
2257
        tx = self._memobj.tx_freq_limits
2258

    
2259
        for rx_band in rx.items():
2260
            name, limits = rx_band
2261

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

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

    
2288
        for tx_band in tx.items():
2289
            name, limits = tx_band
2290

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

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

    
2316
        return fl
2317

    
2318
    def _get_settings(self):
2319
        """Build the radio configuration settings menus
2320
        """
2321

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

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

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

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

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

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

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

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

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

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

    
2436

    
2437
@directory.register
2438
class KGUV9PXRadio(KGUV9DPlusRadio):
2439

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

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

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

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

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

    
2515
    def _get_settings(self):
2516
        """Build the radio configuration settings menus
2517
        """
2518

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2736
    def _fl_tab(self):
2737

    
2738
        freq_limit_grp = RadioSettingGroup("limits",
2739
                                           "Freq Limits")
2740
        limgrp = freq_limit_grp
2741

    
2742
        l = self._memobj.limits
2743

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2872
        return limgrp
2873

    
2874

    
2875
@directory.register
2876
class KGUV9GXRadio(KGUV9PXRadio):
2877

    
2878
    """Wouxun KG-UV9GX"""
2879
    VENDOR = "Wouxun"
2880
    MODEL = "KG-UV9GX"
2881
    _model = b"KG-UV9D"
2882
    _rev = b"02"  # default rev for the radio I know about...
2883
    NEEDS_COMPAT_SERIAL = False
(5-5/6)