Project

General

Profile

New Model #10457 ยป kguv9gx.py

Wouxun KG-UV9GX Chirp Driver Beta 1.0 - Mel Terechenok, 03/18/2023 08:17 AM

 
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_INVALID = 0xff
129

    
130
# Radio memory map. This matches the reads/writes above.
131
# structure elements whose name starts with x are currently unidentified
132

    
133
_MEM_FORMAT02 = """
134
#seekto 0x40;
135

    
136
struct {
137
    char reset[6];
138
    char x46[2];
139
    char mode_sw[6];
140
    char x4e;
141
}  passwords;
142

    
143
#seekto 0x60;
144

    
145
struct freq_limit {
146
    u16 start;
147
    u16 stop;
148
};
149

    
150
struct {
151
    struct freq_limit band_150;
152
    struct freq_limit band_450;
153
    struct freq_limit band_300;
154
    struct freq_limit band_700;
155
    struct freq_limit band_200;
156
} rx_freq_limits;
157

    
158
struct {
159
    struct freq_limit band_150;
160
    struct freq_limit band_450;
161
} tx_freq_limits;
162

    
163
#seekto 0x740;
164

    
165
struct {
166
    u16 fm_freq;
167
} fm_chans[20];
168

    
169
// each band has its own configuration, essentially its default params
170

    
171
struct vfo {
172
    u32 freq;
173
    u32 offset;
174
    u16 encqt;
175
    u16 decqt;
176
    u8  bit7_4:3,
177
        qt:3,
178
        bit1_0:2;
179
    u8  bit7:1,
180
        scan:1,
181
        bit5:1,
182
        pwr:2,
183
        mod:1,
184
        fm_dev:2;
185
    u8  pad2:6,
186
        shift:2;
187
    u8  zeros;
188
};
189

    
190
#seekto 0x780;
191

    
192
struct {
193
    struct vfo band_150;
194
    struct vfo band_450;
195
} vfo_b;
196

    
197
#seekto 0x800;
198

    
199
struct {
200
    struct vfo band_150;
201
    struct vfo band_450;
202
    struct vfo band_300;
203
    struct vfo band_700;
204
    struct vfo band_200;
205
} vfo_a;
206

    
207
// There are two independent radios, aka areas (as described
208
// in the manual as the upper and lower portions of the display...
209

    
210
struct area_conf {
211
    u8 w_mode;
212
    u16 w_chan;
213
    u8 scan_grp;
214
    u8 bcl;
215
    u8 sql;
216
    u8 cset;
217
    u8 step;
218
    u8 scan_mode;
219
    u8 x869;
220
    u8 scan_range;
221
    u8 x86b;
222
    u8 x86c;
223
    u8 x86d;
224
    u8 x86e;
225
    u8 x86f;
226
};
227

    
228
#seekto 0x860;
229

    
230
struct area_conf a_conf;
231

    
232
#seekto 0x870;
233

    
234
struct area_conf b_conf;
235

    
236
#seekto 0x880;
237

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

    
304

    
305
#seekto 0x8c0;
306
struct {
307
    u8 code[6];
308
    char x8c6[10];
309
} my_callid;
310

    
311
#seekto 0x8d0;
312
struct {
313
    u8 scc[6];
314
    char x8d6[10];
315
} stun;
316

    
317
#seekto 0x8e0;
318
struct {
319
    u16 wake;
320
    u16 sleep;
321
} save[4];
322

    
323
#seekto 0x8f0;
324
struct {
325
    char banner[16];
326
} display;
327

    
328
#seekto 0x940;
329
struct {
330
    struct {
331
        i16 scan_st;
332
        i16 scan_end;
333
    } addrs[10];
334
    u8 x0968[8];
335
    struct {
336
        char name[8];
337
    } names[10];
338
} scn_grps;
339

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

    
360
// nobody really sees this. It is marshalled with chan_blk
361
// in 4 entry chunks
362
#seekto 0x4900;
363

    
364
// Tracks with the index of  chan_blk[]
365
struct {
366
    char name[8];
367
} chan_name[999];
368

    
369
#seekto 0x7400;
370
struct {
371
    u8 cid[6];
372
    u8 pad[2];
373
}call_ids[20];
374

    
375
// This array tracks with the index of call_ids[]
376
struct {
377
    char name[6];
378
    char pad[2];
379
} cid_names[20];
380
    """
381

    
382
_MEM_FORMAT_9PX = """
383
#seekto 0x40;
384

    
385
struct {
386
    char reset[6];
387
    char x46[2];
388
    char mode_sw[6];
389
    char x4e;
390
}  passwords;
391

    
392
#seekto 0x50;
393
struct {
394
    char model[10];
395
} oemmodel;
396

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

    
421
#seekto 0x740;
422

    
423
struct {
424
    u16 fm_freq;
425
} fm_chans[20];
426

    
427
// each band has its own configuration, essentially its default params
428

    
429
struct vfo {
430
    u32 freq;
431
    u32 offset;
432
    u16 encqt;
433
    u16 decqt;
434
    u8  bit7_4:3,
435
        qt:3,
436
        bit1_0:2;
437
    u8  bit7:1,
438
        scan:1,
439
        bit5:1,
440
        pwr:2,
441
        mod:1,
442
        fm_dev:2;
443
    u8  pad2:6,
444
        shift:2;
445
    u8  zeros;
446
};
447

    
448
#seekto 0x780;
449

    
450
struct {
451
    struct vfo band_150;
452
    struct vfo band_450;
453
} vfo_b;
454

    
455
#seekto 0x800;
456

    
457
struct {
458
    struct vfo band_150;
459
    struct vfo band_450;
460
    struct vfo band_300;
461
    struct vfo band_700;
462
    struct vfo band_200;
463
} vfo_a;
464

    
465
// There are two independent radios, aka areas (as described
466
// in the manual as the upper and lower portions of the display...
467

    
468
struct area_conf {
469
    u8 w_mode;
470
    u16 w_chan; // fix issue in 9D Plus -  w_chan is 2bytes
471
    u8 scan_grp;
472
    u8 bcl;
473
    u8 sql;
474
    u8 cset;
475
    u8 step;
476
    u8 scan_mode;
477
    u8 x869;
478
    u8 scan_range;
479
    u8 x86b;
480
    u8 x86c;
481
    u8 x86d;
482
    u8 x86e;
483
    u8 x86f;
484
};
485

    
486
#seekto 0x860;
487

    
488
struct area_conf a_conf;
489

    
490
#seekto 0x870;
491

    
492
struct area_conf b_conf;
493

    
494
#seekto 0x880;
495

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

    
561
} settings;
562

    
563

    
564
#seekto 0x8c0;
565
struct {
566
    u8 code[6];
567
    char x8c6[10];
568
} my_callid;
569

    
570
#seekto 0x8d0;
571
struct {
572
    u8 scc[6];
573
    char x8d6[10];
574
} stun;
575

    
576
#seekto 0x8e0;
577
struct {
578
    u16 wake;
579
    u16 sleep;
580
} save[4];
581

    
582
#seekto 0x8f0;
583
struct {
584
    char banner[16];
585
} display;
586

    
587
#seekto 0x940;
588
struct {
589
    struct {
590
        i16 scan_st;
591
        i16 scan_end;
592
    } addrs[10];
593
    u8 x0968[8];
594
    struct {
595
        char name[8];
596
    } names[10];
597
} scn_grps;
598

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

    
619
// nobody really sees this. It is marshalled with chan_blk
620
// in 4 entry chunks
621
#seekto 0x4900;
622

    
623
// Tracks with the index of  chan_blk[]
624
struct {
625
    char name[8];
626
} chan_name[999];
627

    
628
#seekto 0x7400;
629
struct {
630
    u8 cid[6];
631
    u8 pad[2];
632
}call_ids[20];
633

    
634
// This array tracks with the index of call_ids[]
635
struct {
636
    char name[6];
637
    char pad[2];
638
} cid_names[20];
639

    
640
#seekto 0x7600;
641
struct {
642
    u8 screen_mode;
643
} screen;
644
"""
645

    
646

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

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

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

    
689
    # now obfuscate by an xor starting with first payload byte ^ 0x52
690
    # including the trailing cksum.
691
    xorbits = 0x52
692
    for i, byte in enumerate(data[4:]):
693
        xord = xorbits ^ byte
694
        data[i + 4] = xord
695
        xorbits = xord
696
    return(data)
697

    
698

    
699
def _pkt_decode(data):
700
    """Take a packet hot off the wire and decode it into clear text
701
    and return the fields. We say <<cleartext>> here because all it
702
    turns out to be is annoying obfuscation.
703
    This is the inverse of pkt_decode"""
704

    
705
    # we don't care about data[0].
706
    # It is always 0x7d and not included in checksum
707
    op = data[1]
708
    direction = data[2]
709
    bytecount = data[3]
710

    
711
    # First un-obfuscate the payload and cksum
712
    payload = bytearray()
713
    xorbits = 0x52
714
    for i, byte in enumerate(data[4:]):
715
        payload.append(xorbits ^ byte)
716
        xorbits = byte
717

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

    
729
# UI callbacks to process input for mapping UI fields to memory cells
730

    
731

    
732
def freq2int(val, min, max):
733
    """Convert a frequency as a string to a u32. Units is Hz
734
    """
735
    _freq = chirp_common.parse_freq(str(val))
736
    if _freq > max or _freq < min:
737
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
738
                                (chirp_common.format_freq(_freq),
739
                                 chirp_common.format_freq(min),
740
                                 chirp_common.format_freq(max)))
741
    return _freq
742

    
743

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

    
756

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

    
768

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

    
782

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

    
799

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

    
816

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

    
842

    
843
def digits2str(digits, padding=' ', width=6):
844
    """Convert a password or SCC digit string to a string
845
    Passwords are expanded to and must be 6 chars. Fill them with '0'
846
    """
847

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

    
861

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

    
879

    
880
def name2str(name):
881
    """ Convert a callid or scan group name to a string
882
    Deal with fixed field padding (\0 or \0xff)
883
    """
884

    
885
    namestr = ""
886
    for i in range(0, len(name)):
887
        b = ord(name[i].get_value())
888
        if b != 0 and b != 0xff:
889
            namestr += chr(b)
890
    return namestr
891

    
892

    
893
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
894
    """ Convert a string to a name. A name is a 6 element bytearray
895
    with ascii chars.
896
    """
897
    val = str(val).rstrip(' \t\r\n\0\0xff')
898
    if len(val) == 0:
899
        name = "".ljust(size, emptyfill)
900
    else:
901
        name = val.ljust(size, fillchar)
902
    return name
903

    
904

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

    
919

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

    
940

    
941
# Helpers to replace python2 things like confused str/byte
942

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

    
957
    block_size = 16
958

    
959
    lines = (len(data) // block_size)
960
    if (len(data) % block_size > 0):
961
        lines += 1
962

    
963
    out = ""
964
    left = len(data)
965
    for block in range(0, lines):
966
        addr += block * block_size
967
        try:
968
            out += addrfmt % locals()
969
        except (OverflowError, ValueError, TypeError, KeyError):
970
            out += "%03i" % addr
971
        out += ': '
972

    
973
        if left < block_size:
974
            limit = left
975
        else:
976
            limit = block_size
977

    
978
        for j in range(0, block_size):
979
            if (j < limit):
980
                out += "%02x " % data[(block * block_size) + j]
981
            else:
982
                out += "   "
983

    
984
        out += "  "
985

    
986
        for j in range(0, block_size):
987

    
988
            if (j < limit):
989
                _byte = data[(block * block_size) + j]
990
                if _byte >= 0x20 and _byte < 0x7F:
991
                    out += "%s" % chr(_byte)
992
                else:
993
                    out += "."
994
            else:
995
                out += " "
996
        out += "\n"
997
        if (left > block_size):
998
            left -= block_size
999

    
1000
    return out
1001

    
1002

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

    
1063

    
1064
@directory.register
1065
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
1066
                      chirp_common.ExperimentalRadio):
1067

    
1068
    """Wouxun KG-UV9D Plus"""
1069
    VENDOR = "Wouxun"
1070
    MODEL = "KG-UV9D Plus"
1071
    _model = b"KG-UV9D"
1072
    _rev = b"00"  # default rev for the radio I know about...
1073
    _file_ident = b"kg-uv9d"
1074
    NEEDS_COMPAT_SERIAL = False
1075
    BAUD_RATE = 19200
1076
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
1077
                    chirp_common.PowerLevel("M", watts=2),
1078
                    chirp_common.PowerLevel("H", watts=5)]
1079
    _mmap = ""
1080
    MEM_VALID = 0xfc
1081

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

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

    
1118
    def _write_record(self, cmd, payload=None):
1119
        """ Write a request packet to the radio.
1120
        """
1121

    
1122
        packet = _pkt_encode(cmd, payload)
1123
        self.pipe.write(packet)
1124

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

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

    
1147
        TODO: This could be smarter and reject a radio not actually
1148
        a UV9D...
1149
        """
1150

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

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

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

    
1202
    def sync_out(self):
1203
        """ Public sync_out
1204
            Upload the modified memory image into the radio.
1205
            """
1206

    
1207
        try:
1208
            self._identify()
1209
            self._do_upload()
1210
            self._write_record(CMD_HANGUP)
1211
        except errors.RadioError:
1212
            raise
1213
        except Exception as e:
1214
            raise errors.RadioError(
1215
                "Failed to communicate with radio: %s" % e)
1216
        return
1217

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

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

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

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

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

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

    
1361
    def get_raw_memory(self, number):
1362
        return repr(self._memobj.chan_blk[number])
1363

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

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

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

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

    
1408
        mem.dtcs_polarity = pt + pr
1409

    
1410
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1411
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1412

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

    
1420
        mem = chirp_common.Memory()
1421
        mem.number = number
1422
        _valid = _mem.state
1423
        # Override Mem Valid state to handle quirky 9PX CPS New codeplug
1424
        # issue where there is a channel programmed but the CPS
1425
        # "state" value is 0xFF indicating an invalid memory
1426
        if _valid == MEM_INVALID and _mem.rxfreq != 0xFFFFFFFF and _nam != '':
1427
            _valid = self.MEM_VALID
1428

    
1429
        if (_valid != self.MEM_VALID and _valid != 0 and _valid != 2 and
1430
           _valid != 0x40 and _valid != 0xFC):
1431
            # In Issue #6995 we can find _valid values of 0 and 2 in the IMG
1432
            # so these values should be treated like MEM_VALID.
1433
            # state value of 0x40 found in deleted memory - still shows in CPS
1434
            mem.empty = True
1435
            return mem
1436
        else:
1437
            mem.empty = False
1438

    
1439
        mem.freq = int(_mem.rxfreq) * 10
1440

    
1441
        if _mem.txfreq == 0xFFFFFFFF:
1442
            # TX freq not set
1443
            mem.duplex = "off"
1444
            mem.offset = 0
1445
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1446
            mem.duplex = ""
1447
            mem.offset = 0
1448
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1449
            mem.duplex = "split"
1450
            mem.offset = int(_mem.txfreq) * 10
1451
        else:
1452
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1453
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1454

    
1455
        mem.name = name2str(_nam.name)
1456

    
1457
        self._get_tone(_mem, mem)
1458

    
1459
        mem.skip = "" if bool(_mem.scan) else "S"
1460

    
1461
        mem.power = self.POWER_LEVELS[_mem.pwr]
1462
        if _mem.mod == 1:
1463
            mem.mode = "AM"
1464
        elif _mem.fm_dev == 0:
1465
            mem.mode = "FM"
1466
        else:
1467
            mem.mode = "NFM"
1468
        #  qt has no home in the UI
1469
        return mem
1470

    
1471
    def _set_tone(self, mem, _mem):
1472
        """Update the memory channel block CTCC/DCS tones
1473
        from the UI fields
1474
        """
1475
        def _set_dcs(code, pol):
1476
            val = int("%i" % code, 8) | 0x8000
1477
            if pol == "R":
1478
                val |= 0x4000
1479
            return val
1480

    
1481
        rx_mode = tx_mode = None
1482
        rxtone = txtone = 0x0000
1483

    
1484
        if mem.tmode == "Tone":
1485
            tx_mode = "Tone"
1486
            txtone = int(mem.rtone * 10)
1487
        elif mem.tmode == "TSQL":
1488
            rx_mode = tx_mode = "Tone"
1489
            rxtone = txtone = int(mem.ctone * 10)
1490
        elif mem.tmode == "DTCS":
1491
            tx_mode = rx_mode = "DTCS"
1492
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1493
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1494
        elif mem.tmode == "Cross":
1495
            tx_mode, rx_mode = mem.cross_mode.split("->")
1496
            if tx_mode == "DTCS":
1497
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1498
            elif tx_mode == "Tone":
1499
                txtone = int(mem.rtone * 10)
1500
            if rx_mode == "DTCS":
1501
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1502
            elif rx_mode == "Tone":
1503
                rxtone = int(mem.ctone * 10)
1504

    
1505
        _mem.decQT = rxtone
1506
        _mem.encQT = txtone
1507

    
1508
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1509
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1510

    
1511
    def set_memory(self, mem):
1512
        """ Public set_memory
1513
            Inverse of get_memory. Update the radio memory image
1514
            from the mem object
1515
            """
1516
        number = mem.number
1517

    
1518
        _mem = self._memobj.chan_blk[number - 1]
1519
        _nam = self._memobj.chan_name[number - 1]
1520

    
1521
        if mem.empty:
1522
            # consider putting in a check for chan # that is empty but
1523
            # listed as one of the 2 working channels and change them
1524
            # to channel 1 to be consistent with CPS and allow
1525
            # complete deletion from radio.  Otherwise,
1526
            # a deleted channel will still show on radio with no name.
1527
            # MRT implement the above working channel check
1528
            if self._memobj.a_conf.w_chan == number:
1529
                self._memobj.a_conf.w_chan = 1
1530
            if self._memobj.b_conf.w_chan == number:
1531
                self._memobj.b_conf.w_chan = 1
1532

    
1533
            _mem.set_raw("\xFF" * (_mem.size() // 8))
1534
            _nam.name = str2name("", 8, '\0', '\0')
1535
            _mem.state = MEM_INVALID
1536
            return
1537

    
1538
        _mem.rxfreq = int(mem.freq / 10)
1539
        if mem.duplex == "off":
1540
            _mem.txfreq = 0xFFFFFFFF
1541
        elif mem.duplex == "split":
1542
            _mem.txfreq = int(mem.offset / 10)
1543
        elif mem.duplex == "+":
1544
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1545
        elif mem.duplex == "-":
1546
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1547
        else:
1548
            _mem.txfreq = int(mem.freq / 10)
1549
        _mem.scan = int(mem.skip != "S")
1550
        if mem.mode == "FM":
1551
            _mem.mod = 0    # make sure forced AM is off
1552
            _mem.fm_dev = 0
1553
        elif mem.mode == "NFM":
1554
            _mem.mod = 0
1555
            _mem.fm_dev = 1
1556
        elif mem.mode == "AM":
1557
            _mem.mod = 1     # AM on
1558
            _mem.fm_dev = 1  # set NFM bandwidth
1559
        else:
1560
            _mem.mod = 0
1561
            _mem.fm_dev = 0  # Catchall default is FM
1562
        # set the tone
1563
        self._set_tone(mem, _mem)
1564
        # set the power
1565
        if mem.power:
1566
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1567
        else:
1568
            _mem.pwr = True
1569

    
1570
        # Set fields we can't access via the UI table to safe defaults
1571
        _mem.qt = 0   # mute mode to QT
1572
        _mem.bit5 = 0 # clear this bit to ensure accurate CPS power level
1573
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1574
        _mem.state = self.MEM_VALID
1575

    
1576
# Build the UI configuration tabs
1577
# the channel memory tab is built by the core.
1578
# We have no control over it
1579

    
1580
    def _core_tab(self):
1581
        """ Build Core Configuration tab
1582
        Radio settings common to all modes and areas go here.
1583
        """
1584
        s = self._memobj.settings
1585
        if (self.MODEL == "KG-UV9PX" or
1586
            self.MODEL == "KG-UV9GX"):
1587

    
1588
             sm = self._memobj.screen
1589

    
1590
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1591

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

    
1706
        if (self.MODEL == "KG-UV9PX" or
1707
            self.MODEL == "KG-UV9GX"):
1708
             cf.append(RadioSetting("screen.screen_mode",
1709
                                    "Screen Mode (Menu 62)",
1710
                                    RadioSettingValueList(
1711
                                     SCREEN_MODE_LIST,
1712
                                     SCREEN_MODE_LIST[
1713
                                         sm.screen_mode])))
1714

    
1715
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1716
            langlst = LANGUAGE_LIST2
1717
        else:
1718
            langlst = LANGUAGE_LIST
1719
        cf.append(
1720
            RadioSetting("lang",
1721
                         "Menu Language (Menu 14)",
1722
                         RadioSettingValueList(langlst,
1723
                                               langlst[s.lang])))
1724

    
1725
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1726
            ponmsglst = PONMSG_LIST2
1727
        else:
1728
            ponmsglst = PONMSG_LIST
1729
        cf.append(RadioSetting("ponmsg",
1730
                               "Poweron message (Menu 40)",
1731
                               RadioSettingValueList(
1732
                                   ponmsglst, ponmsglst[s.ponmsg])))
1733
        return cf
1734

    
1735
    def _repeater_tab(self):
1736
        """Repeater mode functions
1737
        """
1738
        s = self._memobj.settings
1739
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1740

    
1741
        cf.append(
1742
            RadioSetting("type_set",
1743
                         "Radio Mode (Menu 43)",
1744
                         RadioSettingValueList(
1745
                             RPTMODE_LIST,
1746
                             RPTMODE_LIST[s.type_set])))
1747
        cf.append(RadioSetting("rpt_ptt",
1748
                               "Repeater PTT (Menu 45)",
1749
                               RadioSettingValueBoolean(s.rpt_ptt)))
1750
        cf.append(RadioSetting("rpt_spk",
1751
                               "Repeater Mode Speaker (Menu 44)",
1752
                               RadioSettingValueBoolean(s.rpt_spk)))
1753
        cf.append(
1754
            RadioSetting("rpt_kpt",
1755
                         "Repeater Hold Time (Menu 46)",
1756
                         RadioSettingValueList(RPT_KPTS,
1757
                                               RPT_KPTS[s.rpt_kpt])))
1758
        cf.append(RadioSetting("rpt_rct",
1759
                               "Repeater Receipt Tone (Menu 47)",
1760
                               RadioSettingValueBoolean(s.rpt_rct)))
1761
        return cf
1762

    
1763
    def _admin_tab(self):
1764
        """Admin functions not present in radio menu...
1765
        These are admin functions not radio operation configuration
1766
        """
1767

    
1768
        def apply_cid(setting, obj):
1769
            c = str2callid(setting.value)
1770
            obj.code = c
1771

    
1772
        def apply_scc(setting, obj):
1773
            c = str2digits(setting.value)
1774
            obj.scc = c
1775

    
1776
        def apply_mode_sw(setting, obj):
1777
            pw = str2pw(setting.value)
1778
            obj.mode_sw = pw
1779
            setting.value = pw2str(obj.mode_sw)
1780

    
1781
        def apply_reset(setting, obj):
1782
            pw = str2pw(setting.value)
1783
            obj.reset = pw
1784
            setting.value = pw2str(obj.reset)
1785

    
1786
        def apply_wake(setting, obj):
1787
            obj.wake = int(setting.value)/10
1788

    
1789
        def apply_sleep(setting, obj):
1790
            obj.sleep = int(setting.value)/10
1791

    
1792
        pw = self._memobj.passwords  # admin passwords
1793
        s = self._memobj.settings
1794

    
1795
        cf = RadioSettingGroup("admin", "Admin Functions")
1796

    
1797
        cf.append(RadioSetting("menu_avail",
1798
                               "Menu available in channel mode",
1799
                               RadioSettingValueBoolean(s.menu_avail)))
1800
        mode_sw = RadioSettingValueString(0, 6,
1801
                                          pw2str(pw.mode_sw), False)
1802
        rs = RadioSetting("passwords.mode_sw",
1803
                          "Mode Switch Password", mode_sw)
1804
        rs.set_apply_callback(apply_mode_sw, pw)
1805
        cf.append(rs)
1806

    
1807
        cf.append(RadioSetting("reset_avail",
1808
                               "Radio Reset Available",
1809
                               RadioSettingValueBoolean(s.reset_avail)))
1810
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1811
        rs = RadioSetting("passwords.reset",
1812
                          "Radio Reset Password", reset)
1813
        rs.set_apply_callback(apply_reset, pw)
1814
        cf.append(rs)
1815

    
1816
        cf.append(
1817
            RadioSetting("dtmf_tx",
1818
                         "DTMF Tx Duration",
1819
                         RadioSettingValueList(DTMF_TIMES,
1820
                                               DTMF_TIMES[s.dtmf_tx])))
1821
        cid = self._memobj.my_callid
1822
        my_callid = RadioSettingValueString(3, 6,
1823
                                            self.callid2str(cid.code), False)
1824
        rs = RadioSetting("my_callid.code",
1825
                          "PTT Caller ID code (Menu 24)", my_callid)
1826
        rs.set_apply_callback(apply_cid, cid)
1827
        cf.append(rs)
1828

    
1829
        stun = self._memobj.stun
1830
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1831
        rs = RadioSetting("stun.scc", "Security code", st)
1832
        rs.set_apply_callback(apply_scc, stun)
1833
        cf.append(rs)
1834

    
1835
        cf.append(
1836
            RadioSetting("settings.save_m",
1837
                         "Save Mode  (Menu 2)",
1838
                         RadioSettingValueList(SAVE_MODES,
1839
                                               SAVE_MODES[s.save_m])))
1840
        for i in range(0, 4):
1841
            sm = self._memobj.save[i]
1842
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1843
            wf = RadioSetting("save[%i].wake" % i,
1844
                              "Save Mode %d Wake Time" % (i+1), wake)
1845
            wf.set_apply_callback(apply_wake, sm)
1846
            cf.append(wf)
1847

    
1848
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1849
            wf = RadioSetting("save[%i].sleep" % i,
1850
                              "Save Mode %d Sleep Time" % (i+1), slp)
1851
            wf.set_apply_callback(apply_sleep, sm)
1852
            cf.append(wf)
1853

    
1854
        _msg = str(self._memobj.display.banner).split("\0")[0]
1855
        val = RadioSettingValueString(0, 16, _msg)
1856
        val.set_mutable(True)
1857
        cf.append(RadioSetting("display.banner",
1858
                               "Display Message", val))
1859

    
1860
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1861
            _str = str(self._memobj.oemmodel.model).split("\0")[0]
1862
            val = RadioSettingValueString(0, 10, _str)
1863
            val.set_mutable(True)
1864
            cf.append(RadioSetting("oemmodel.model",
1865
                                   "Custom Sub-Receiver Message", val))
1866

    
1867
            val = RadioSettingValueList(
1868
                                TDR_LIST,
1869
                                TDR_LIST[s.tdr])
1870
            val.set_mutable(True)
1871
            cf.append(RadioSetting("tdr", "TDR", val))
1872

    
1873
            val = RadioSettingValueList(
1874
                                ACTIVE_AREA_LIST,
1875
                                ACTIVE_AREA_LIST[s.act_area])
1876
            val.set_mutable(True)
1877
            cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1878

    
1879
        return cf
1880

    
1881
    def _fm_tab(self):
1882
        """FM Broadcast channels
1883
        """
1884
        def apply_fm(setting, obj):
1885
            f = freq2short(setting.value, 76000000, 108000000)
1886
            obj.fm_freq = f
1887

    
1888
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1889
        for ch in range(0, 20):
1890
            chan = self._memobj.fm_chans[ch]
1891
            freq = RadioSettingValueString(0, 20,
1892
                                           short2freq(chan.fm_freq))
1893
            rs = RadioSetting("fm_%d" % (ch + 1),
1894
                              "FM Channel %d" % (ch + 1), freq)
1895
            rs.set_apply_callback(apply_fm, chan)
1896
            fm.append(rs)
1897
        return fm
1898

    
1899
    def _scan_grp(self):
1900
        """Scan groups
1901
        """
1902
        def apply_name(setting, obj):
1903
            name = str2name(setting.value, 8, '\0', '\0')
1904
            obj.name = name
1905

    
1906
        def apply_start(setting, obj):
1907
            """Do a callback to deal with RadioSettingInteger limitation
1908
            on memory address resolution
1909
            """
1910
            obj.scan_st = int(setting.value)
1911

    
1912
        def apply_end(setting, obj):
1913
            """Do a callback to deal with RadioSettingInteger limitation
1914
            on memory address resolution
1915
            """
1916
            obj.scan_end = int(setting.value)
1917

    
1918
        sgrp = self._memobj.scn_grps
1919
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1920
        for i in range(0, 10):
1921
            s_grp = sgrp.addrs[i]
1922
            s_name = sgrp.names[i]
1923
            rs_name = RadioSettingValueString(0, 8,
1924
                                              name2str(s_name.name))
1925
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1926
                              "Group %i Name" % (i + 1), rs_name)
1927
            rs.set_apply_callback(apply_name, s_name)
1928
            scan.append(rs)
1929
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1930
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1931
                              "Starting Channel", rs_st)
1932
            rs.set_apply_callback(apply_start, s_grp)
1933
            scan.append(rs)
1934
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1935
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1936
                              "Last Channel", rs_end)
1937
            rs.set_apply_callback(apply_end, s_grp)
1938
            scan.append(rs)
1939
        return scan
1940

    
1941
    def _callid_grp(self):
1942
        """Caller IDs to be recognized by radio
1943
        This really should be a table in the UI
1944
        """
1945
        def apply_callid(setting, obj):
1946
            c = str2callid(setting.value)
1947
            obj.cid = c
1948

    
1949
        def apply_name(setting, obj):
1950
            name = str2name(setting.value, 6, '\0', '\xff')
1951
            obj.name = name
1952

    
1953
        cid = RadioSettingGroup("callids", "Caller IDs")
1954
        for i in range(0, 20):
1955
            callid = self._memobj.call_ids[i]
1956
            name = self._memobj.cid_names[i]
1957
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
1958
            rs = RadioSetting("cid_names[%i].name" % i,
1959
                              "Caller ID %i Name" % (i + 1), c_name)
1960
            rs.set_apply_callback(apply_name, name)
1961
            cid.append(rs)
1962
            c_id = RadioSettingValueString(0, 6,
1963
                                           self.callid2str(callid.cid),
1964
                                           False)
1965
            rs = RadioSetting("call_ids[%i].cid" % i,
1966
                              "Caller ID Code", c_id)
1967
            rs.set_apply_callback(apply_callid, callid)
1968
            cid.append(rs)
1969
        return cid
1970

    
1971
    def _band_tab(self, area, band):
1972
        """ Build a band tab inside a VFO/Area
1973
        """
1974
        def apply_freq(setting, lo, hi, obj):
1975
            f = freq2int(setting.value, lo, hi)
1976
            obj.freq = f/10
1977

    
1978
        def apply_offset(setting, obj):
1979
            f = freq2int(setting.value, 0, 5000000)
1980
            obj.offset = f/10
1981

    
1982
        def apply_enc(setting, obj):
1983
            t = tone2short(setting.value)
1984
            obj.encqt = t
1985

    
1986
        def apply_dec(setting, obj):
1987
            t = tone2short(setting.value)
1988
            obj.decqt = t
1989

    
1990
        if area == "a":
1991
            if band == 150:
1992
                c = self._memobj.vfo_a.band_150
1993
                lo = 108000000
1994
                hi = 180000000
1995
            elif band == 200:
1996
                c = self._memobj.vfo_a.band_200
1997
                lo = 230000000
1998
                hi = 250000000
1999
            elif band == 300:
2000
                c = self._memobj.vfo_a.band_300
2001
                lo = 350000000
2002
                hi = 400000000
2003
            elif band == 450:
2004
                c = self._memobj.vfo_a.band_450
2005
                lo = 400000000
2006
                hi = 512000000
2007
            else:   # 700
2008
                c = self._memobj.vfo_a.band_700
2009
                lo = 700000000
2010
                hi = 985000000
2011
        else:  # area 'b'
2012
            if band == 150:
2013
                c = self._memobj.vfo_b.band_150
2014
                lo = 136000000
2015
                hi = 180000000
2016
            else:  # 450
2017
                c = self._memobj.vfo_b.band_450
2018
                lo = 400000000
2019
                hi = 512000000
2020

    
2021
        prefix = "vfo_%s.band_%d" % (area, band)
2022
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2023
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2024
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2025
        rs.set_apply_callback(apply_freq, lo, hi, c)
2026
        bf.append(rs)
2027

    
2028
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2029
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2030
        rs.set_apply_callback(apply_offset, c)
2031
        bf.append(rs)
2032

    
2033
        rs = RadioSetting(prefix + ".encqt",
2034
                          "Encode QT (Menu 17,19)",
2035
                          RadioSettingValueList(TONE_LIST,
2036
                                                short2tone(c.encqt)))
2037
        rs.set_apply_callback(apply_enc, c)
2038
        bf.append(rs)
2039

    
2040
        rs = RadioSetting(prefix + ".decqt",
2041
                          "Decode QT (Menu 16,18)",
2042
                          RadioSettingValueList(TONE_LIST,
2043
                                                short2tone(c.decqt)))
2044
        rs.set_apply_callback(apply_dec, c)
2045
        bf.append(rs)
2046

    
2047
        bf.append(RadioSetting(prefix + ".qt",
2048
                               "Mute Mode (Menu 21)",
2049
                               RadioSettingValueList(SPMUTE_LIST,
2050
                                                     SPMUTE_LIST[c.qt])))
2051
        bf.append(RadioSetting(prefix + ".scan",
2052
                               "Scan this (Menu 48)",
2053
                               RadioSettingValueBoolean(c.scan)))
2054
        bf.append(RadioSetting(prefix + ".pwr",
2055
                               "Power (Menu 5)",
2056
                               RadioSettingValueList(
2057
                                   POWER_LIST, POWER_LIST[c.pwr])))
2058
        bf.append(RadioSetting(prefix + ".mod",
2059
                               "AM Modulation (Menu 54)",
2060
                               RadioSettingValueBoolean(c.mod)))
2061
        bf.append(RadioSetting(prefix + ".fm_dev",
2062
                               "FM Deviation (Menu 4)",
2063
                               RadioSettingValueList(
2064
                                   BANDWIDTH_LIST,
2065
                                   BANDWIDTH_LIST[c.fm_dev])))
2066
        bf.append(
2067
            RadioSetting(prefix + ".shift",
2068
                         "Frequency Shift (Menu 6)",
2069
                         RadioSettingValueList(OFFSET_LIST,
2070
                                               OFFSET_LIST[c.shift])))
2071
        return bf
2072

    
2073
    def _area_tab(self, area):
2074
        """Build a VFO tab
2075
        """
2076
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2077
            f = freq2short(setting.value, scan_lo, scan_hi)
2078
            obj.scan_st = f
2079

    
2080
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2081
            f = freq2short(setting.value, scan_lo, scan_hi)
2082
            obj.scan_end = f
2083

    
2084
        if area == "a":
2085
            desc = "Area A Settings"
2086
            c = self._memobj.a_conf
2087
            scan_lo = 108000000
2088
            scan_hi = 985000000
2089
            scan_rng = self._memobj.settings.a
2090
            band_list = (150, 200, 300, 450, 700)
2091
        else:
2092
            desc = "Area B Settings"
2093
            c = self._memobj.b_conf
2094
            scan_lo = 136000000
2095
            scan_hi = 512000000
2096
            scan_rng = self._memobj.settings.b
2097
            band_list = (150, 450)
2098

    
2099
        prefix = "%s_conf" % area
2100
        af = RadioSettingGroup(prefix, desc)
2101
        af.append(
2102
            RadioSetting(prefix + ".w_mode",
2103
                         "Workmode",
2104
                         RadioSettingValueList(
2105
                             WORKMODE_LIST,
2106
                             WORKMODE_LIST[c.w_mode])))
2107
        af.append(RadioSetting(prefix + ".w_chan",
2108
                               "Channel",
2109
                               RadioSettingValueInteger(1, 999,
2110
                                                        c.w_chan)))
2111
        af.append(
2112
            RadioSetting(prefix + ".scan_grp",
2113
                         "Scan Group (Menu 49)",
2114
                         RadioSettingValueList(
2115
                             SCANGRP_LIST,
2116
                             SCANGRP_LIST[c.scan_grp])))
2117
        af.append(RadioSetting(prefix + ".bcl",
2118
                               "Busy Channel Lock-out (Menu 15)",
2119
                               RadioSettingValueBoolean(c.bcl)))
2120
        af.append(
2121
            RadioSetting(prefix + ".sql",
2122
                         "Squelch Level (Menu 8)",
2123
                         RadioSettingValueList(LIST_0_9,
2124
                                               LIST_0_9[c.sql])))
2125
        af.append(
2126
            RadioSetting(prefix + ".cset",
2127
                         "Call ID Group (Menu 52)",
2128
                         RadioSettingValueList(LIST_1_20,
2129
                                               LIST_1_20[c.cset])))
2130
        af.append(
2131
            RadioSetting(prefix + ".step",
2132
                         "Frequency Step (Menu 3)",
2133
                         RadioSettingValueList(
2134
                             STEP_LIST, STEP_LIST[c.step])))
2135
        af.append(
2136
            RadioSetting(prefix + ".scan_mode",
2137
                         "Scan Mode (Menu 20)",
2138
                         RadioSettingValueList(
2139
                             SCANMODE_LIST,
2140
                             SCANMODE_LIST[c.scan_mode])))
2141
        af.append(
2142
            RadioSetting(prefix + ".scan_range",
2143
                         "Scan Range (Menu 50)",
2144
                         RadioSettingValueList(
2145
                             SCANRANGE_LIST,
2146
                             SCANRANGE_LIST[c.scan_range])))
2147
        st = RadioSettingValueString(0, 15,
2148
                                     short2freq(scan_rng.scan_st))
2149
        rs = RadioSetting("settings.%s.scan_st" % area,
2150
                          "Frequency Scan Start", st)
2151
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2152
        af.append(rs)
2153

    
2154
        end = RadioSettingValueString(0, 15,
2155
                                      short2freq(scan_rng.scan_end))
2156
        rs = RadioSetting("settings.%s.scan_end" % area,
2157
                          "Frequency Scan End", end)
2158
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2159
                              scan_rng)
2160
        af.append(rs)
2161
        # Each area has its own set of bands
2162
        for band in (band_list):
2163
            af.append(self._band_tab(area, band))
2164
        return af
2165

    
2166
    def _key_tab(self):
2167
        """Build radio key/button menu
2168
        """
2169
        s = self._memobj.settings
2170
        if self.MODEL == "KG-UV9PX":
2171
            pfkey1 = PF1KEY_LIST
2172
            pfkey2 = PF2KEY_LIST
2173
            pfkey3 = PF3KEY_LIST2
2174
        elif self.MODEL == "KG-UV9GX":
2175
            pfkey1 = PF1KEY_LIST9GX
2176
            pfkey2 = PF2KEY_LIST9GX
2177
            pfkey3 = PF3KEY_LIST9GX
2178
        else:
2179
            pfkey1 = PF1KEY_LIST
2180
            pfkey2 = PF2KEY_LIST
2181
            pfkey3 = PF3KEY_LIST
2182

    
2183
        kf = RadioSettingGroup("key_grp", "Key Settings")
2184

    
2185
        kf.append(RadioSetting("settings.pf1",
2186
                               "PF1 Key function (Menu 55)",
2187
                               RadioSettingValueList(
2188
                                   pfkey1,
2189
                                   pfkey1[s.pf1])))
2190
        kf.append(RadioSetting("settings.pf2",
2191
                               "PF2 Key function (Menu 56)",
2192
                               RadioSettingValueList(
2193
                                   pfkey2,
2194
                                   pfkey2[s.pf2])))
2195

    
2196
        kf.append(RadioSetting("settings.pf3",
2197
                               "PF3 Key function (Menu 57)",
2198
                               RadioSettingValueList(
2199
                                   pfkey3,
2200
                                   pfkey3[s.pf3])))
2201
        return kf
2202

    
2203
    def _fl_tab(self):
2204
        """Build the frequency limits tab
2205
        """
2206

    
2207
        # The stop limits in the factory KG-UV9D Mate memory image are 1MHz
2208
        # higher than the published specs. The settings panel will crash if
2209
        # it encounters a value outside of these ranges.
2210
        hard_limits = {
2211
            "band_150": (108000000, 181000000),
2212
            "band_450": (400000000, 513000000),
2213
            "band_300": (350000000, 401000000),
2214
            "band_700": (700000000, 987000000),
2215
            "band_200": (230000000, 251000000)
2216
        }
2217

    
2218
        def apply_freq_start(setting, low, high, obj):
2219
            f = freq2short(setting.value, low, high)
2220
            obj.start = f
2221

    
2222
        def apply_freq_stop(setting, low, high, obj):
2223
            """Sets the stop limit to 1MHz below the input value"""
2224

    
2225
            # The firmware has an off-by-1MHz error with stop limits.
2226
            # If you set the stop limit to 1480 (148MHz), you can still tune
2227
            # up to 148.99MHz. To compensate for this,
2228
            # we subtract 10 increments of 100MHz before storing the value.
2229
            f = freq2short(setting.value, low, high) - 10
2230
            obj.stop = f
2231

    
2232
        fl = RadioSettingGroup("freq_limit_grp", "Frequency Limits")
2233

    
2234
        rx = self._memobj.rx_freq_limits
2235
        tx = self._memobj.tx_freq_limits
2236

    
2237
        for rx_band in rx.items():
2238
            name, limits = rx_band
2239

    
2240
            start_freq = RadioSettingValueString(1,
2241
                                                 20,
2242
                                                 short2freq(limits.start))
2243
            start_rs = RadioSetting("rx_start_" + name,
2244
                                    name + " Receive Start",
2245
                                    start_freq)
2246
            start_rs.set_apply_callback(apply_freq_start,
2247
                                        hard_limits[name][0],
2248
                                        hard_limits[name][1],
2249
                                        limits)
2250
            fl.append(start_rs)
2251

    
2252
            # Add 10 increments of 100MHz before displaying to compensate for
2253
            # the firmware off-by-1MHz problem.
2254
            stop_freq = RadioSettingValueString(1,
2255
                                                20,
2256
                                                short2freq(limits.stop + 10))
2257
            stop_rs = RadioSetting("rx_stop_" + name,
2258
                                   name + " Receive Stop",
2259
                                   stop_freq)
2260
            stop_rs.set_apply_callback(apply_freq_stop,
2261
                                       hard_limits[name][0],
2262
                                       hard_limits[name][1],
2263
                                       limits)
2264
            fl.append(stop_rs)
2265

    
2266
        for tx_band in tx.items():
2267
            name, limits = tx_band
2268

    
2269
            start_freq = RadioSettingValueString(1,
2270
                                                 20,
2271
                                                 short2freq(limits.start))
2272
            start_rs = RadioSetting("tx_start_" + name,
2273
                                    name + " Transmit Start",
2274
                                    start_freq)
2275
            start_rs.set_apply_callback(apply_freq_start,
2276
                                        hard_limits[name][0],
2277
                                        hard_limits[name][1], limits)
2278
            fl.append(start_rs)
2279

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

    
2294
        return fl
2295

    
2296
    def _get_settings(self):
2297
        """Build the radio configuration settings menus
2298
        """
2299

    
2300
        core_grp = self._core_tab()
2301
        fm_grp = self._fm_tab()
2302
        area_a_grp = self._area_tab("a")
2303
        area_b_grp = self._area_tab("b")
2304
        key_grp = self._key_tab()
2305
        scan_grp = self._scan_grp()
2306
        callid_grp = self._callid_grp()
2307
        admin_grp = self._admin_tab()
2308
        rpt_grp = self._repeater_tab()
2309
        freq_limit_grp = self._fl_tab()
2310

    
2311
        core_grp.append(key_grp)
2312
        core_grp.append(admin_grp)
2313
        core_grp.append(rpt_grp)
2314
        core_grp.append(freq_limit_grp)
2315
        group = RadioSettings(core_grp,
2316
                              area_a_grp,
2317
                              area_b_grp,
2318
                              fm_grp,
2319
                              scan_grp,
2320
                              callid_grp
2321
                              )
2322
        return group
2323

    
2324
    def get_settings(self):
2325
        """ Public build out linkage between radio settings and UI
2326
        """
2327
        try:
2328
            return self._get_settings()
2329
        except Exception:
2330
            import traceback
2331
            LOG.error("Failed to parse settings: %s",
2332
                      traceback.format_exc())
2333
            return None
2334

    
2335
    def _is_freq(self, element):
2336
        """This is a hack to smoke out whether we need to do
2337
        frequency translations for otherwise innocent u16s and u32s
2338
        """
2339
        return "rxfreq" in element.get_name() or \
2340
               "txfreq" in element.get_name() or \
2341
               "scan_st" in element.get_name() or \
2342
               "scan_end" in element.get_name() or \
2343
               "offset" in element.get_name() or \
2344
               "fm_stop" in element.get_name()
2345

    
2346
    def _is_limit(self, element):
2347
        return "lower_limit" in element.get_name() or\
2348
               "upper_limit" in element.get_name()
2349

    
2350
    def set_settings(self, settings):
2351
        """ Public update radio settings via UI callback
2352
        A lot of this should be in common code....
2353
        """
2354

    
2355
        for element in settings:
2356
            if not isinstance(element, RadioSetting):
2357
                LOG.debug("set_settings: not instance %s" %
2358
                          element.get_name())
2359
                self.set_settings(element)
2360
                continue
2361
            else:
2362
                try:
2363
                    if "." in element.get_name():
2364
                        bits = element.get_name().split(".")
2365
                        obj = self._memobj
2366
                        for bit in bits[:-1]:
2367
                            # decode an array index
2368
                            if "[" in bit and "]" in bit:
2369
                                bit, index = bit.split("[", 1)
2370
                                index, junk = index.split("]", 1)
2371
                                index = int(index)
2372
                                obj = getattr(obj, bit)[index]
2373
                            else:
2374
                                obj = getattr(obj, bit)
2375
                        setting = bits[-1]
2376
                    else:
2377
                        obj = self._memobj.settings
2378
                        setting = element.get_name()
2379

    
2380
                    if element.has_apply_callback():
2381
                        LOG.debug("Using apply callback")
2382
                        element.run_apply_callback()
2383
                    else:
2384
                        LOG.debug("Setting %s = %s" %
2385
                                  (setting, element.value))
2386
                        if self._is_freq(element):
2387
                            setattr(obj, setting, int(element.value)/10)
2388
                        elif self._is_limit(element):
2389
                            setattr(obj, setting, int(element.value)*10)
2390
                        else:
2391
                            setattr(obj, setting, element.value)
2392
                except Exception as e:
2393
                    LOG.debug("set_settings: Exception with %s" %
2394
                              element.get_name())
2395
                    raise
2396

    
2397
    def callid2str(self, cid):
2398
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2399
        One digit (binary) per byte, terminated with '0xc'
2400
        """
2401

    
2402
        bin2ascii = " 1234567890"
2403
        cidstr = ""
2404
        for i in range(0, 6):
2405
            b = cid[i].get_value()
2406
            if b == 0xc:  # the cid EOL
2407
                break
2408
            if b == 0 or b > 0xa:
2409
                raise InvalidValueError(
2410
                    "Caller ID code has illegal byte 0x%x" % b)
2411
            cidstr += bin2ascii[b]
2412
        return cidstr
2413

    
2414

    
2415
@directory.register
2416
class KGUV9PXRadio(KGUV9DPlusRadio):
2417

    
2418
    """Wouxun KG-UV9PX"""
2419
    VENDOR = "Wouxun"
2420
    MODEL = "KG-UV9PX"
2421
    _model = b"KG-UV9D"
2422
    _rev = b"02"  # default rev for the radio I know about...
2423
    _file_ident = b"kg-uv9px"
2424
    NEEDS_COMPAT_SERIAL = False
2425
    MEM_VALID = 0x00
2426

    
2427
    def process_mmap(self):
2428
        if self._rev != b"02" and self._rev != b"00":
2429
            # new revision found - log it and assume same map and proceed
2430
            LOG.debug("Unrecognized model variation (%s) Using default Map" %
2431
                      self._rev)
2432
        self._memobj = bitwise.parse(_MEM_FORMAT_9PX, self._mmap)
2433

    
2434
    def get_features(self):
2435
        """ Public get_features
2436
            Return the features of this radio once we have identified
2437
            it and gotten its bits
2438
            """
2439
        rf = chirp_common.RadioFeatures()
2440
        rf.has_settings = True
2441
        rf.has_ctone = True
2442
        rf.has_rx_dtcs = True
2443
        rf.has_cross = True
2444
        rf.has_tuning_step = False
2445
        rf.has_bank = False
2446
        rf.can_odd_split = True
2447
        rf.valid_skips = ["", "S"]
2448
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
2449
        rf.valid_cross_modes = [
2450
            "Tone->Tone",
2451
            "Tone->DTCS",
2452
            "DTCS->Tone",
2453
            "DTCS->",
2454
            "->Tone",
2455
            "->DTCS",
2456
            "DTCS->DTCS",
2457
        ]
2458
        rf.valid_modes = ["FM", "NFM", "AM"]
2459
        rf.valid_power_levels = self.POWER_LEVELS
2460
        rf.valid_name_length = 8
2461
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
2462
        rf.valid_bands = [(108000000, 135997500),  # Aircraft  AM
2463
                          (136000000, 180997500),  # supports 2m
2464
                          (219000000, 250997500),
2465
                          (350000000, 399997500),
2466
                          (400000000, 512997500),  # supports 70cm
2467
                          (700000000, 986997500)]
2468
        rf.valid_characters = chirp_common.CHARSET_ASCII
2469
        rf.valid_tuning_steps = STEPS
2470
        rf.memory_bounds = (1, 999)  # 999 memories
2471
        return rf
2472

    
2473
    def callid2str(self, cid):
2474
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2475
        One digit (binary) per byte, terminated with '0xc'
2476
        """
2477

    
2478
        bin2ascii = " 1234567890"
2479
        cidstr = ""
2480
        for i in range(0, 6):
2481
            b = cid[i].get_value()
2482
            # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
2483
            # remap 0x00 to 0x0a
2484
            if b == 0x00:
2485
                b = 0x0a
2486
            if b == 0xc or b == 0xf0:  # the cid EOL
2487
                break
2488
            if b > 0xa:
2489
                raise InvalidValueError(
2490
                    "Caller ID code has illegal byte 0x%x" % b)
2491
            cidstr += bin2ascii[b]
2492
        return cidstr
2493

    
2494
    def _get_settings(self):
2495
        """Build the radio configuration settings menus
2496
        """
2497

    
2498
        core_grp = self._core_tab()
2499
        fm_grp = self._fm_tab()
2500
        area_a_grp = self._area_tab("a")
2501
        area_b_grp = self._area_tab("b")
2502
        key_grp = self._key_tab()
2503
        scan_grp = self._scan_grp()
2504
        callid_grp = self._callid_grp()
2505
        admin_grp = self._admin_tab()
2506
        rpt_grp = self._repeater_tab()
2507
        freq_limit_grp = self._fl_tab()
2508
        core_grp.append(key_grp)
2509
        core_grp.append(admin_grp)
2510
        core_grp.append(rpt_grp)
2511
        group = RadioSettings(core_grp,
2512
                              area_a_grp,
2513
                              area_b_grp,
2514
                              fm_grp,
2515
                              scan_grp,
2516
                              callid_grp,
2517
                              freq_limit_grp,)
2518
        return group
2519

    
2520
    def _area_tab(self, area):
2521
        """Build a VFO tab
2522
        """
2523
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2524
            f = freq2short(setting.value, scan_lo, scan_hi)
2525
            obj.scan_st = f
2526

    
2527
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2528
            f = freq2short(setting.value, scan_lo, scan_hi)
2529
            obj.scan_end = f
2530

    
2531
        if area == "a":
2532
            desc = "Receiver A Settings"
2533
            c = self._memobj.a_conf
2534
            scan_lo = 108000000
2535
            scan_hi = 985997500
2536
            scan_rng = self._memobj.settings.a
2537
            band_list = (150, 200, 300, 450, 700)
2538
        else:
2539
            desc = "Receiver B Settings"
2540
            c = self._memobj.b_conf
2541
            scan_lo = 136000000
2542
            scan_hi = 512997500
2543
            scan_rng = self._memobj.settings.b
2544
            band_list = (150, 450)
2545

    
2546
        prefix = "%s_conf" % area
2547
        af = RadioSettingGroup(prefix, desc)
2548
        af.append(
2549
            RadioSetting(prefix + ".w_mode",
2550
                         "Workmode",
2551
                         RadioSettingValueList(
2552
                             WORKMODE_LIST,
2553
                             WORKMODE_LIST[c.w_mode])))
2554
        af.append(RadioSetting(prefix + ".w_chan",
2555
                               "Channel",
2556
                               RadioSettingValueInteger(1, 999,
2557
                                                        c.w_chan)))
2558
        af.append(
2559
            RadioSetting(prefix + ".scan_grp",
2560
                         "Scan Group (Menu 49)",
2561
                         RadioSettingValueList(
2562
                             SCANGRP_LIST,
2563
                             SCANGRP_LIST[c.scan_grp])))
2564
        af.append(RadioSetting(prefix + ".bcl",
2565
                               "Busy Channel Lock-out (Menu 15)",
2566
                               RadioSettingValueBoolean(c.bcl)))
2567
        af.append(
2568
            RadioSetting(prefix + ".sql",
2569
                         "Squelch Level (Menu 8)",
2570
                         RadioSettingValueList(LIST_0_9,
2571
                                               LIST_0_9[c.sql])))
2572
        af.append(
2573
            RadioSetting(prefix + ".cset",
2574
                         "Call ID Group (Menu 52)",
2575
                         RadioSettingValueList(LIST_1_20,
2576
                                               LIST_1_20[c.cset])))
2577
        af.append(
2578
            RadioSetting(prefix + ".step",
2579
                         "Frequency Step (Menu 3)",
2580
                         RadioSettingValueList(
2581
                             STEP_LIST, STEP_LIST[c.step])))
2582
        af.append(
2583
            RadioSetting(prefix + ".scan_mode",
2584
                         "Scan Mode (Menu 20)",
2585
                         RadioSettingValueList(
2586
                             SCANMODE_LIST,
2587
                             SCANMODE_LIST[c.scan_mode])))
2588
        af.append(
2589
            RadioSetting(prefix + ".scan_range",
2590
                         "Scan Range (Menu 50)",
2591
                         RadioSettingValueList(
2592
                             SCANRANGE_LIST,
2593
                             SCANRANGE_LIST[c.scan_range])))
2594
        st = RadioSettingValueString(0, 15,
2595
                                     short2freq(scan_rng.scan_st))
2596
        rs = RadioSetting("settings.%s.scan_st" % area,
2597
                          "Frequency Scan Start", st)
2598
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2599
        af.append(rs)
2600

    
2601
        end = RadioSettingValueString(0, 15,
2602
                                      short2freq(scan_rng.scan_end))
2603
        rs = RadioSetting("settings.%s.scan_end" % area,
2604
                          "Frequency Scan End", end)
2605
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2606
                              scan_rng)
2607
        af.append(rs)
2608
        # Each area has its own set of bands
2609
        for band in (band_list):
2610
            af.append(self._band_tab(area, band))
2611
        return af
2612

    
2613
    def _band_tab(self, area, band):
2614
        """ Build a band tab inside a VFO/Area
2615
        """
2616
        def apply_freq(setting, lo, hi, obj):
2617
            f = freq2int(setting.value, lo, hi)
2618
            obj.freq = f/10
2619

    
2620
        def apply_offset(setting, obj):
2621
            f = freq2int(setting.value, 0, 5000000)
2622
            obj.offset = f/10
2623

    
2624
        def apply_enc(setting, obj):
2625
            t = tone2short(setting.value)
2626
            obj.encqt = t
2627

    
2628
        def apply_dec(setting, obj):
2629
            t = tone2short(setting.value)
2630
            obj.decqt = t
2631

    
2632
        if area == "a":
2633
            if band == 150:
2634
                c = self._memobj.vfo_a.band_150
2635
                lo = 108000000
2636
                hi = 180997500
2637
            elif band == 200:
2638
                c = self._memobj.vfo_a.band_200
2639
                lo = 219000000
2640
                hi = 250997500
2641
            elif band == 300:
2642
                c = self._memobj.vfo_a.band_300
2643
                lo = 350000000
2644
                hi = 399997500
2645
            elif band == 450:
2646
                c = self._memobj.vfo_a.band_450
2647
                lo = 400000000
2648
                hi = 512997500
2649
            else:   # 700
2650
                c = self._memobj.vfo_a.band_700
2651
                lo = 700000000
2652
                hi = 986997500
2653
        else:  # area 'b'
2654
            if band == 150:
2655
                c = self._memobj.vfo_b.band_150
2656
                lo = 136000000
2657
                hi = 180997500
2658
            else:  # 450
2659
                c = self._memobj.vfo_b.band_450
2660
                lo = 400000000
2661
                hi = 512997500
2662

    
2663
        prefix = "vfo_%s.band_%d" % (area, band)
2664
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2665
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2666
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2667
        rs.set_apply_callback(apply_freq, lo, hi, c)
2668
        bf.append(rs)
2669

    
2670
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2671
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2672
        rs.set_apply_callback(apply_offset, c)
2673
        bf.append(rs)
2674

    
2675
        rs = RadioSetting(prefix + ".encqt",
2676
                          "Encode QT (Menu 17,19)",
2677
                          RadioSettingValueList(TONE_LIST,
2678
                                                short2tone(c.encqt)))
2679
        rs.set_apply_callback(apply_enc, c)
2680
        bf.append(rs)
2681

    
2682
        rs = RadioSetting(prefix + ".decqt",
2683
                          "Decode QT (Menu 16,18)",
2684
                          RadioSettingValueList(TONE_LIST,
2685
                                                short2tone(c.decqt)))
2686
        rs.set_apply_callback(apply_dec, c)
2687
        bf.append(rs)
2688

    
2689
        bf.append(RadioSetting(prefix + ".qt",
2690
                               "Mute Mode (Menu 21)",
2691
                               RadioSettingValueList(SPMUTE_LIST,
2692
                                                     SPMUTE_LIST[c.qt])))
2693
        bf.append(RadioSetting(prefix + ".scan",
2694
                               "Scan this (Menu 48)",
2695
                               RadioSettingValueBoolean(c.scan)))
2696
        bf.append(RadioSetting(prefix + ".pwr",
2697
                               "Power (Menu 5)",
2698
                               RadioSettingValueList(
2699
                                   POWER_LIST, POWER_LIST[c.pwr])))
2700
        bf.append(RadioSetting(prefix + ".mod",
2701
                               "AM Modulation (Menu 54)",
2702
                               RadioSettingValueBoolean(c.mod)))
2703
        bf.append(RadioSetting(prefix + ".fm_dev",
2704
                               "FM Deviation (Menu 4)",
2705
                               RadioSettingValueList(
2706
                                   BANDWIDTH_LIST,
2707
                                   BANDWIDTH_LIST[c.fm_dev])))
2708
        bf.append(
2709
            RadioSetting(prefix + ".shift",
2710
                         "Frequency Shift (Menu 6)",
2711
                         RadioSettingValueList(OFFSET_LIST,
2712
                                               OFFSET_LIST[c.shift])))
2713
        return bf
2714

    
2715
    def _fl_tab(self):
2716

    
2717
        freq_limit_grp = RadioSettingGroup("limits",
2718
                                           "Freq Limits")
2719
        limgrp = freq_limit_grp
2720

    
2721
        l = self._memobj.limits
2722

    
2723
        if self.MODEL == "KG-UV9PX":
2724
            val = RadioSettingValueInteger(136, 180,
2725
                                        (l.lim_150M_Txlower_limit) / 10.0)
2726
            rs = RadioSetting("limits.lim_150M_Txlower_limit",
2727
                            "150M Tx Lower Limit (MHz)",
2728
                            RadioSettingValueInteger(136, 180,
2729
                                                    val))
2730
            limgrp.append(rs)
2731

    
2732
            val = RadioSettingValueInteger(136, 180,
2733
                                        (l.lim_150M_Txupper_limit) / 10.0)
2734
            rs = RadioSetting("limits.lim_150M_Txupper_limit",
2735
                            "150M Tx Upper Limit (MHz + 0.9975)",
2736
                            RadioSettingValueInteger(136, 180,
2737
                                                    val))
2738
            limgrp.append(rs)
2739

    
2740
            val = RadioSettingValueInteger(400, 512,
2741
                                        (l.lim_450M_Txlower_limit) / 10.0)
2742
            rs = RadioSetting("limits.lim_450M_Txlower_limit",
2743
                            "450M Tx Lower Limit (MHz)",
2744
                            RadioSettingValueInteger(400, 512,
2745
                                                    val))
2746
            limgrp.append(rs)
2747

    
2748
            val = RadioSettingValueInteger(400, 512,
2749
                                        (l.lim_450M_Txupper_limit) / 10.0)
2750
            rs = RadioSetting("limits.lim_450M_Txupper_limit",
2751
                            "450M Tx Upper Limit (MHz + 0.9975)",
2752
                            RadioSettingValueInteger(400, 512,
2753
                                                    val))
2754
            limgrp.append(rs)
2755

    
2756
        val = RadioSettingValueInteger(108, 180,
2757
                                       (l.lim_150M_area_a_rxlower_limit) /
2758
                                       10.0)
2759
        rs = RadioSetting("limits.lim_150M_area_a_rxlower_limit",
2760
                          "Rcvr A 150M Rx Lower Limit (MHz)",
2761
                          RadioSettingValueInteger(108, 180,
2762
                                                   val))
2763
        limgrp.append(rs)
2764

    
2765
        val = RadioSettingValueInteger(108, 180,
2766
                                       (l.lim_150M_area_a_rxupper_limit) /
2767
                                       10.0)
2768
        rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
2769
                          "Rcvr A 150M Rx Upper Limit (MHz + 0.9975)",
2770
                          RadioSettingValueInteger(108, 180,
2771
                                                   val))
2772
        limgrp.append(rs)
2773

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

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

    
2792
        val = RadioSettingValueInteger(400, 512,
2793
                                       (l.lim_450M_rxlower_limit) / 10.0)
2794
        rs = RadioSetting("limits.lim_450M_rxlower_limit",
2795
                          "450M Rx Lower Limit (MHz)",
2796
                          RadioSettingValueInteger(400, 512,
2797
                                                   val))
2798
        limgrp.append(rs)
2799

    
2800
        val = RadioSettingValueInteger(400, 512,
2801
                                       (l.lim_450M_rxupper_limit) / 10.0)
2802
        rs = RadioSetting("limits.lim_450M_rxupper_limit",
2803
                          "450M Rx Upper Limit (MHz + 0.9975)",
2804
                          RadioSettingValueInteger(400, 512,
2805
                                                   val))
2806
        limgrp.append(rs)
2807

    
2808
        val = RadioSettingValueInteger(350, 399,
2809
                                       (l.lim_300M_rxlower_limit) / 10.0)
2810
        rs = RadioSetting("limits.lim_300M_rxlower_limit",
2811
                          "300M Rx Lower Limit (MHz)",
2812
                          RadioSettingValueInteger(350, 399,
2813
                                                   val))
2814
        limgrp.append(rs)
2815

    
2816
        val = RadioSettingValueInteger(350, 399,
2817
                                       (l.lim_300M_rxupper_limit) / 10.0)
2818
        rs = RadioSetting("limits.lim_300M_rxupper_limit",
2819
                          "300M Rx Upper Limit (MHz + 0.9975)",
2820
                          RadioSettingValueInteger(350, 399,
2821
                                                   val))
2822
        limgrp.append(rs)
2823
        val = RadioSettingValueInteger(700, 986,
2824
                                       (l.lim_800M_rxlower_limit) / 10.0)
2825
        rs = RadioSetting("limits.lim_800M_rxlower_limit",
2826
                          "800M Rx Lower Limit (MHz)",
2827
                          RadioSettingValueInteger(700, 986,
2828
                                                   val))
2829
        limgrp.append(rs)
2830

    
2831
        val = RadioSettingValueInteger(700, 986,
2832
                                       (l.lim_800M_rxupper_limit) / 10.0)
2833
        rs = RadioSetting("limits.lim_800M_rxupper_limit",
2834
                          "800M Rx Upper Limit (MHz + 0.9975)",
2835
                          RadioSettingValueInteger(700, 986,
2836
                                                   val))
2837
        limgrp.append(rs)
2838

    
2839
        val = RadioSettingValueInteger(219, 250,
2840
                                       (l.lim_210M_rxlower_limit) / 10.0)
2841
        rs = RadioSetting("limits.lim_210M_rxlower_limit",
2842
                          "210M Rx Lower Limit (MHz)",
2843
                          RadioSettingValueInteger(219, 250,
2844
                                                   val))
2845
        limgrp.append(rs)
2846

    
2847
        val = RadioSettingValueInteger(219, 250,
2848
                                       (l.lim_210M_rxupper_limit) / 10.0)
2849
        rs = RadioSetting("limits.lim_210M_rxupper_limit",
2850
                          "210M Rx Upper Limit (MHz + 0.9975)",
2851
                          RadioSettingValueInteger(219, 250,
2852
                                                   val))
2853
        limgrp.append(rs)
2854

    
2855
        return limgrp
2856

    
2857

    
2858
@directory.register
2859
class KGUV9GXRadio(KGUV9PXRadio):
2860

    
2861
    """Wouxun KG-UV9GX"""
2862
    VENDOR = "Wouxun"
2863
    MODEL = "KG-UV9GX"
2864
    _model = b"KG-UV9D"
2865
    _rev = b"02"  # default rev for the radio I know about...
2866
    NEEDS_COMPAT_SERIAL = False
2867
    MEM_VALID = 0x00
2868

    
    (1-1/1)