Project

General

Profile

New Model #10535 » kguv9dplus.py

Wouxun KG-UV9G Pro Test Driver - Mel Terechenok, 04/21/2023 11:09 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
    (0x7000, 8, 124),   # Valid Channel bytes 1-992
92
    (0x73E0, 1, 7),     # Valid Channel Bytes 993-999
93
    (0x7400, 64, 5),    # CALL-ID 1-20, names 1-20
94
    )
95

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

    
133
MEM_VALID = 0xfc
134
MEM_INVALID = 0xff
135
VALID_MEM_VALUES = [MEM_VALID, 0x00, 0x02, 0x40, 0x3D]
136
INVALID_MEM_VALUES = [MEM_INVALID]
137
# new CHAN_VALID/INVALID mem values to address some radios not showing new channels  
138
CHAN_VALID = 0x41
139
CHAN_INVALID = 0xFC
140
# Radio memory map. This matches the reads/writes above.
141
# structure elements whose name starts with x are currently unidentified
142

    
143
_MEM_FORMAT02 = """
144
#seekto 0x40;
145

    
146
struct {
147
    char reset[6];
148
    char x46[2];
149
    char mode_sw[6];
150
    char x4e;
151
}  passwords;
152

    
153
#seekto 0x60;
154

    
155
struct freq_limit {
156
    u16 start;
157
    u16 stop;
158
};
159

    
160
struct {
161
    struct freq_limit band_150;
162
    struct freq_limit band_450;
163
    struct freq_limit band_300;
164
    struct freq_limit band_700;
165
    struct freq_limit band_200;
166
} rx_freq_limits;
167

    
168
struct {
169
    struct freq_limit band_150;
170
    struct freq_limit band_450;
171
} tx_freq_limits;
172

    
173
#seekto 0x740;
174

    
175
struct {
176
    u16 fm_freq;
177
} fm_chans[20];
178

    
179
// each band has its own configuration, essentially its default params
180

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

    
200
#seekto 0x780;
201

    
202
struct {
203
    struct vfo band_150;
204
    struct vfo band_450;
205
} vfo_b;
206

    
207
#seekto 0x800;
208

    
209
struct {
210
    struct vfo band_150;
211
    struct vfo band_450;
212
    struct vfo band_300;
213
    struct vfo band_700;
214
    struct vfo band_200;
215
} vfo_a;
216

    
217
// There are two independent radios, aka areas (as described
218
// in the manual as the upper and lower portions of the display...
219

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

    
238
#seekto 0x860;
239

    
240
struct area_conf a_conf;
241

    
242
#seekto 0x870;
243

    
244
struct area_conf b_conf;
245

    
246
#seekto 0x880;
247

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

    
314

    
315
#seekto 0x8c0;
316
struct {
317
    u8 code[6];
318
    char x8c6[10];
319
} my_callid;
320

    
321
#seekto 0x8d0;
322
struct {
323
    u8 scc[6];
324
    char x8d6[10];
325
} stun;
326

    
327
#seekto 0x8e0;
328
struct {
329
    u16 wake;
330
    u16 sleep;
331
} save[4];
332

    
333
#seekto 0x8f0;
334
struct {
335
    char banner[16];
336
} display;
337

    
338
#seekto 0x940;
339
struct {
340
    struct {
341
        i16 scan_st;
342
        i16 scan_end;
343
    } addrs[10];
344
    u8 x0968[8];
345
    struct {
346
        char name[8];
347
    } names[10];
348
} scn_grps;
349

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

    
370
// nobody really sees this. It is marshalled with chan_blk
371
// in 4 entry chunks
372
#seekto 0x4900;
373

    
374
// Tracks with the index of  chan_blk[]
375
struct {
376
    char name[8];
377
} chan_name[999];
378

    
379
#seekto 0x7000;
380
struct {
381
    u8 ch_valid;
382
} chan_valid[999];
383

    
384
#seekto 0x7400;
385
struct {
386
    u8 cid[6];
387
    u8 pad[2];
388
}call_ids[20];
389

    
390
// This array tracks with the index of call_ids[]
391
struct {
392
    char name[6];
393
    char pad[2];
394
} cid_names[20];
395
    """
396

    
397
_MEM_FORMAT_9PX = """
398
#seekto 0x40;
399

    
400
struct {
401
    char reset[6];
402
    char x46[2];
403
    char mode_sw[6];
404
    char x4e;
405
}  passwords;
406

    
407
#seekto 0x50;
408
struct {
409
    char model[10];
410
} oemmodel;
411

    
412
#seekto 0x60;
413
struct {
414
    u16 lim_150M_area_a_rxlower_limit; // 0x60
415
    u16 lim_150M_area_a_rxupper_limit;
416
    u16 lim_450M_rxlower_limit;
417
    u16 lim_450M_rxupper_limit;
418
    u16 lim_300M_rxlower_limit;
419
    u16 lim_300M_rxupper_limit;
420
    u16 lim_800M_rxlower_limit;
421
    u16 lim_800M_rxupper_limit;
422
    u16 lim_210M_rxlower_limit;
423
    u16 lim_210M_rxupper_limit;
424
    u16 lim_150M_Txlower_limit;
425
    u16 lim_150M_Txupper_limit;
426
    u16 lim_450M_Txlower_limit;
427
    u16 lim_450M_Txupper_limit;
428
    u16 lim_150M_area_b_rxlower_limit;
429
    u16 lim_150M_area_b_rxupper_limit;
430
    u16 unknown_lower_limit;
431
    u16 unknown_upper_limit;
432
    u16 unknown2_lower_limit;
433
    u16 unknown2_upper_limit;
434
}  limits;
435

    
436
#seekto 0x740;
437

    
438
struct {
439
    u16 fm_freq;
440
} fm_chans[20];
441

    
442
// each band has its own configuration, essentially its default params
443

    
444
struct vfo {
445
    u32 freq;
446
    u32 offset;
447
    u16 encqt;
448
    u16 decqt;
449
    u8  bit7_4:3,
450
        qt:3,
451
        bit1_0:2;
452
    u8  bit7:1,
453
        scan:1,
454
        bit5:1,
455
        pwr:2,
456
        mod:1,
457
        fm_dev:2;
458
    u8  pad2:6,
459
        shift:2;
460
    u8  zeros;
461
};
462

    
463
#seekto 0x780;
464

    
465
struct {
466
    struct vfo band_150;
467
    struct vfo band_450;
468
} vfo_b;
469

    
470
#seekto 0x800;
471

    
472
struct {
473
    struct vfo band_150;
474
    struct vfo band_450;
475
    struct vfo band_300;
476
    struct vfo band_700;
477
    struct vfo band_200;
478
} vfo_a;
479

    
480
// There are two independent radios, aka areas (as described
481
// in the manual as the upper and lower portions of the display...
482

    
483
struct area_conf {
484
    u8 w_mode;
485
    u16 w_chan; // fix issue in 9D Plus -  w_chan is 2bytes
486
    u8 scan_grp;
487
    u8 bcl;
488
    u8 sql;
489
    u8 cset;
490
    u8 step;
491
    u8 scan_mode;
492
    u8 x869;
493
    u8 scan_range;
494
    u8 x86b;
495
    u8 x86c;
496
    u8 x86d;
497
    u8 x86e;
498
    u8 x86f;
499
};
500

    
501
#seekto 0x860;
502

    
503
struct area_conf a_conf;
504

    
505
#seekto 0x870;
506

    
507
struct area_conf b_conf;
508

    
509
#seekto 0x880;
510

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

    
576
} settings;
577

    
578

    
579
#seekto 0x8c0;
580
struct {
581
    u8 code[6];
582
    char x8c6[10];
583
} my_callid;
584

    
585
#seekto 0x8d0;
586
struct {
587
    u8 scc[6];
588
    char x8d6[10];
589
} stun;
590

    
591
#seekto 0x8e0;
592
struct {
593
    u16 wake;
594
    u16 sleep;
595
} save[4];
596

    
597
#seekto 0x8f0;
598
struct {
599
    char banner[16];
600
} display;
601

    
602
#seekto 0x940;
603
struct {
604
    struct {
605
        i16 scan_st;
606
        i16 scan_end;
607
    } addrs[10];
608
    u8 x0968[8];
609
    struct {
610
        char name[8];
611
    } names[10];
612
} scn_grps;
613

    
614
// this array of structs is marshalled via the R/WCHAN commands
615
#seekto 0xa00;
616
struct {
617
    u32 rxfreq;
618
    u32 txfreq;
619
    u16 encQT;
620
    u16 decQT;
621
    u8  bit7_5:3,  // all ones
622
        qt:3,
623
        bit1_0:2;
624
    u8  bit7:1,
625
        scan:1,
626
        bit5:1,
627
        pwr:2,
628
        mod:1,
629
        fm_dev:2;
630
    u8  state;
631
    u8  c3;
632
} chan_blk[999];
633

    
634
// nobody really sees this. It is marshalled with chan_blk
635
// in 4 entry chunks
636
#seekto 0x4900;
637

    
638
// Tracks with the index of  chan_blk[]
639
struct {
640
    char name[8];
641
} chan_name[999];
642

    
643
#seekto 0x7400;
644
struct {
645
    u8 cid[6];
646
    u8 pad[2];
647
}call_ids[20];
648

    
649
// This array tracks with the index of call_ids[]
650
struct {
651
    char name[6];
652
    char pad[2];
653
} cid_names[20];
654

    
655
#seekto 0x7000;
656
struct {
657
    u8 ch_valid;
658
} chan_valid[999];
659

    
660
#seekto 0x7600;
661
struct {
662
    u8 screen_mode;
663
} screen;
664
"""
665

    
666

    
667
# Support for the Wouxun KG-UV9D Plus and KG-UV9PX radio
668
# Serial coms are at 19200 baud
669
# The data is passed in variable length records
670
# Record structure:
671
#  Offset   Usage
672
#    0      start of record (\x7d)
673
#    1      Command (6 commands, see above)
674
#    2      direction (\xff PC-> Radio, \x00 Radio -> PC)
675
#    3      length of payload (excluding header/checksum) (n)
676
#    4      payload (n bytes)
677
#    4+n+1  checksum - byte sum (% 256) of bytes 1 -> 4+n
678
#
679
# Memory Read Records:
680
# the payload is 3 bytes, first 2 are offset (big endian),
681
# 3rd is number of bytes to read
682
# Memory Write Records:
683
# the maximum payload size (from the Wouxun software)
684
# seems to be 66 bytes (2 bytes location + 64 bytes data).
685

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

    
692
    data = bytearray()
693
    data.append(0x7d)  # tag that marks the beginning of the packet
694
    data.append(op)
695
    data.append(0xff)  # 0xff is from app to radio
696
    # calc checksum from op to end
697
    cksum = op + 0xff
698
    if (payload):
699
        data.append(len(payload))
700
        cksum += len(payload)
701
        for byte in payload:
702
            cksum += byte
703
            data.append(byte)
704
    else:
705
        data.append(0x00)
706
        # Yea, this is a 4 bit cksum (also known as a bug)
707
    data.append(cksum & 0xf)
708

    
709
    # now obfuscate by an xor starting with first payload byte ^ 0x52
710
    # including the trailing cksum.
711
    xorbits = 0x52
712
    for i, byte in enumerate(data[4:]):
713
        xord = xorbits ^ byte
714
        data[i + 4] = xord
715
        xorbits = xord
716
    return(data)
717

    
718

    
719
def _pkt_decode(data):
720
    """Take a packet hot off the wire and decode it into clear text
721
    and return the fields. We say <<cleartext>> here because all it
722
    turns out to be is annoying obfuscation.
723
    This is the inverse of pkt_decode"""
724

    
725
    # we don't care about data[0].
726
    # It is always 0x7d and not included in checksum
727
    op = data[1]
728
    direction = data[2]
729
    bytecount = data[3]
730

    
731
    # First un-obfuscate the payload and cksum
732
    payload = bytearray()
733
    xorbits = 0x52
734
    for i, byte in enumerate(data[4:]):
735
        payload.append(xorbits ^ byte)
736
        xorbits = byte
737

    
738
    # Calculate the checksum starting with the 3 bytes of the header
739
    cksum = op + direction + bytecount
740
    for byte in payload[:-1]:
741
        cksum += byte
742
    # yes, a 4 bit cksum to match the encode
743
    cksum_match = (cksum & 0xf) == payload[-1]
744
    if (not cksum_match):
745
        LOG.debug(
746
            "Checksum mismatch: %x != %x; " % (cksum, payload[-1]))
747
    return (cksum_match, op, payload[:-1])
748

    
749
# UI callbacks to process input for mapping UI fields to memory cells
750

    
751

    
752
def freq2int(val, min, max):
753
    """Convert a frequency as a string to a u32. Units is Hz
754
    """
755
    _freq = chirp_common.parse_freq(str(val))
756
    if _freq > max or _freq < min:
757
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
758
                                (chirp_common.format_freq(_freq),
759
                                 chirp_common.format_freq(min),
760
                                 chirp_common.format_freq(max)))
761
    return _freq
762

    
763

    
764
def int2freq(freq):
765
    """
766
    Convert a u32 frequency to a string for UI data entry/display
767
    This is stored in the radio as units of 10Hz which we compensate to Hz.
768
    A value of -1 indicates <no frequency>, i.e. unused channel.
769
    """
770
    if (int(freq) > 0):
771
        f = chirp_common.format_freq(freq)
772
        return f
773
    else:
774
        return ""
775

    
776

    
777
def freq2short(val, min, max):
778
    """Convert a frequency as a string to a u16 which is units of 10KHz
779
    """
780
    _freq = chirp_common.parse_freq(str(val))
781
    if _freq > max or _freq < min:
782
        raise InvalidValueError("Frequency %s is not with in %s-%s" %
783
                                (chirp_common.format_freq(_freq),
784
                                 chirp_common.format_freq(min),
785
                                 chirp_common.format_freq(max)))
786
    return _freq // 100000 & 0xFFFF
787

    
788

    
789
def short2freq(freq):
790
    """
791
       Convert a short frequency to a string for UI data entry/display
792
       This is stored in the radio as units of 10KHz which we
793
       compensate to Hz.
794
       A value of -1 indicates <no frequency>, i.e. unused channel.
795
    """
796
    if (int(freq) > 0):
797
        f = chirp_common.format_freq(freq * 100000)
798
        return f
799
    else:
800
        return ""
801

    
802

    
803
def tone2short(t):
804
    """Convert a string tone or DCS to an encoded u16
805
    """
806
    tone = str(t)
807
    if tone == "----":
808
        u16tone = 0x0000
809
    elif tone[0] == 'D':  # This is a DCS code
810
        c = tone[1: -1]
811
        code = int(c, 8)
812
        if tone[-1] == 'I':
813
            code |= 0x4000
814
        u16tone = code | 0x8000
815
    else:              # This is an analog CTCSS
816
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff  # strip the '.'
817
    return u16tone
818

    
819

    
820
def short2tone(tone):
821
    """ Map a binary CTCSS/DCS to a string name for the tone
822
    """
823
    if tone == 0 or tone == 0xffff:
824
        ret = "----"
825
    else:
826
        code = tone & 0x3fff
827
        if tone & 0x8000:      # This is a DCS
828
            if tone & 0x4000:  # This is an inverse code
829
                ret = "D%0.3oI" % code
830
            else:
831
                ret = "D%0.3oN" % code
832
        else:   # Just plain old analog CTCSS
833
            ret = "%4.1f" % (code / 10.0)
834
    return ret
835

    
836

    
837
def str2callid(val):
838
    """ Convert caller id strings from callid2str.
839
    """
840
    ascii2bin = "0123456789"
841
    s = str(val).strip()
842
    if len(s) < 3 or len(s) > 6:
843
        raise InvalidValueError(
844
            "Caller ID must be at least 3 and no more than 6 digits")
845
    if s[0] == '0':
846
        raise InvalidValueError(
847
            "First digit of a Caller ID cannot be a zero '0'")
848
    blk = bytearray()
849
    for c in s:
850
        if c not in ascii2bin:
851
            raise InvalidValueError(
852
                "Caller ID must be all digits 0x%x" % c)
853
        b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)]
854
        blk.append(b)
855
    if len(blk) < 6:
856
        blk.append(0xc)  # EOL a short ID
857
    if len(blk) < 6:
858
        for i in range(0, (6 - len(blk))):
859
            blk.append(0xf0)
860
    return blk
861

    
862

    
863
def digits2str(digits, padding=' ', width=6):
864
    """Convert a password or SCC digit string to a string
865
    Passwords are expanded to and must be 6 chars. Fill them with '0'
866
    """
867

    
868
    bin2ascii = "0123456789"
869
    digitsstr = ""
870
    for i in range(0, 6):
871
        b = digits[i].get_value()
872
        if b == 0xc:  # the digits EOL
873
            break
874
        if b >= 0xa:
875
            raise InvalidValueError(
876
                "Value has illegal byte 0x%x" % ord(b))
877
        digitsstr += bin2ascii[b]
878
    digitsstr = digitsstr.ljust(width, padding)
879
    return digitsstr
880

    
881

    
882
def str2digits(val):
883
    """ Callback for edited strings from digits2str.
884
    """
885
    ascii2bin = " 0123456789"
886
    s = str(val).strip()
887
    if len(s) < 3 or len(s) > 6:
888
        raise InvalidValueError(
889
            "Value must be at least 3 and no more than 6 digits")
890
    blk = bytearray()
891
    for c in s:
892
        if c not in ascii2bin:
893
            raise InvalidValueError("Value must be all digits 0x%x" % c)
894
        blk.append(int(c))
895
    for i in range(len(blk), 6):
896
        blk.append(0xc)  # EOL a short ID
897
    return blk
898

    
899

    
900
def name2str(name):
901
    """ Convert a callid or scan group name to a string
902
    Deal with fixed field padding (\0 or \0xff)
903
    """
904

    
905
    namestr = ""
906
    for i in range(0, len(name)):
907
        b = ord(name[i].get_value())
908
        if b != 0 and b != 0xff:
909
            namestr += chr(b)
910
    return namestr
911

    
912

    
913
def str2name(val, size=6, fillchar='\0', emptyfill='\0'):
914
    """ Convert a string to a name. A name is a 6 element bytearray
915
    with ascii chars.
916
    """
917
    val = str(val).rstrip(' \t\r\n\0\0xff')
918
    if len(val) == 0:
919
        name = "".ljust(size, emptyfill)
920
    else:
921
        name = val.ljust(size, fillchar)
922
    return name
923

    
924

    
925
def pw2str(pw):
926
    """Convert a password string (6 digits) to a string
927
    Passwords must be 6 digits. If it is shorter, pad right with '0'
928
    """
929
    pwstr = ""
930
    ascii2bin = "0123456789"
931
    for i in range(0, len(pw)):
932
        b = pw[i].get_value()
933
        if b not in ascii2bin:
934
            raise InvalidValueError("Value must be digits 0-9")
935
        pwstr += b
936
    pwstr = pwstr.ljust(6, '0')
937
    return pwstr
938

    
939

    
940
def str2pw(val):
941
    """Store a password from UI to memory obj
942
    If we clear the password (make it go away), change the
943
    empty string to '000000' since the radio must have *something*
944
    Also, fill a < 6 digit pw with 0's
945
    """
946
    ascii2bin = "0123456789"
947
    val = str(val).rstrip(' \t\r\n\0\0xff')
948
    if len(val) == 0:  # a null password
949
        val = "000000"
950
    for i in range(0, len(val)):
951
        b = val[i]
952
        if b not in ascii2bin:
953
            raise InvalidValueError("Value must be digits 0-9")
954
    if len(val) == 0:
955
        pw = "".ljust(6, '\0')
956
    else:
957
        pw = val.ljust(6, '0')
958
    return pw
959

    
960

    
961
# Helpers to replace python2 things like confused str/byte
962

    
963
def _hex_print(data, addrfmt=None):
964
    """Return a hexdump-like encoding of @data
965
    We expect data to be a bytearray, not a string.
966
    Expanded from borrowed code to use the first 2 bytes as the address
967
    per comm packet format.
968
    """
969
    if addrfmt is None:
970
        addrfmt = '%(addr)03i'
971
        addr = 0
972
    else:  # assume first 2 bytes are address
973
        a = struct.unpack(">H", data[0:2])
974
        addr = a[0]
975
        data = data[2:]
976

    
977
    block_size = 16
978

    
979
    lines = (len(data) // block_size)
980
    if (len(data) % block_size > 0):
981
        lines += 1
982

    
983
    out = ""
984
    left = len(data)
985
    for block in range(0, lines):
986
        addr += block * block_size
987
        try:
988
            out += addrfmt % locals()
989
        except (OverflowError, ValueError, TypeError, KeyError):
990
            out += "%03i" % addr
991
        out += ': '
992

    
993
        if left < block_size:
994
            limit = left
995
        else:
996
            limit = block_size
997

    
998
        for j in range(0, block_size):
999
            if (j < limit):
1000
                out += "%02x " % data[(block * block_size) + j]
1001
            else:
1002
                out += "   "
1003

    
1004
        out += "  "
1005

    
1006
        for j in range(0, block_size):
1007

    
1008
            if (j < limit):
1009
                _byte = data[(block * block_size) + j]
1010
                if _byte >= 0x20 and _byte < 0x7F:
1011
                    out += "%s" % chr(_byte)
1012
                else:
1013
                    out += "."
1014
            else:
1015
                out += " "
1016
        out += "\n"
1017
        if (left > block_size):
1018
            left -= block_size
1019

    
1020
    return out
1021

    
1022

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

    
1085

    
1086
@directory.register
1087
class KGUV9DPlusRadio(chirp_common.CloneModeRadio,
1088
                      chirp_common.ExperimentalRadio):
1089

    
1090
    """Wouxun KG-UV9D Plus"""
1091
    VENDOR = "Wouxun"
1092
    MODEL = "KG-UV9D Plus"
1093
    _model = b"KG-UV9D"
1094
    _rev = b"00"  # default rev for the radio I know about...
1095
    _file_ident = b"kg-uv9d"
1096
    NEEDS_COMPAT_SERIAL = False
1097
    BAUD_RATE = 19200
1098
    POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1),
1099
                    chirp_common.PowerLevel("M", watts=2),
1100
                    chirp_common.PowerLevel("H", watts=5)]
1101
    _step_list = STEP_LIST
1102
    _valid_steps = STEPS
1103
    _mmap = ""
1104

    
1105
    def _read_record(self):
1106
        """ Read and validate the header of a radio reply.
1107
        A record is a formatted byte stream as follows:
1108
            0x7D   All records start with this
1109
            opcode This is in the set of legal commands.
1110
                   The radio reply matches the request
1111
            dir    This is the direction, 0xFF to the radio,
1112
                   0x00 from the radio.
1113
            cnt    Count of bytes in payload
1114
                   (not including the trailing checksum byte)
1115
            <cnt bytes>
1116
            <checksum byte>
1117
        """
1118

    
1119
        # first get the header and validate it
1120
        data = bytearray(self.pipe.read(4))
1121
        if (len(data) < 4):
1122
            raise errors.RadioError('Radio did not respond')
1123
        if (data[0] != 0x7D):
1124
            raise errors.RadioError(
1125
                'Radio reply garbled (%02x)' % data[0])
1126
        if (data[1] not in cmd_name):
1127
            raise errors.RadioError(
1128
                "Unrecognized opcode (%02x)" % data[1])
1129
        if (data[2] != 0x00):
1130
            raise errors.RadioError(
1131
                "Direction incorrect. Got (%02x)" % data[2])
1132
        payload_len = data[3]
1133
        # don't forget to read the checksum byte
1134
        data.extend(self.pipe.read(payload_len + 1))
1135
        if (len(data) != (payload_len + 5)):  # we got a short read
1136
            raise errors.RadioError(
1137
                "Radio reply wrong size. Wanted %d, got %d" %
1138
                ((payload_len + 1), (len(data) - 4)))
1139
        return _pkt_decode(data)
1140

    
1141
    def _write_record(self, cmd, payload=None):
1142
        """ Write a request packet to the radio.
1143
        """
1144

    
1145
        packet = _pkt_encode(cmd, payload)
1146
        self.pipe.write(packet)
1147

    
1148
    @classmethod
1149
    def match_model(cls, filedata, filename):
1150
        """Look for bits in the file image and see if it looks
1151
        like ours...
1152
        TODO: there is a bunch of rubbish between 0x50 and 0x160
1153
        that is still a known unknown
1154
        """
1155
        return cls._file_ident in filedata[0x51:0x59].lower()
1156

    
1157
    def _identify(self):
1158
        """ Identify the radio
1159
        The ident block identifies the radio and its capabilities.
1160
        This block is always 78 bytes. The rev == '01' is the base
1161
        radio and '02' seems to be the '-Plus' version.
1162
        I don't really trust the content after the model and revision.
1163
        One would assume this is pretty much constant data but I have
1164
        seen differences between my radio and the dump named
1165
        KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first
1166
        five bands match the OEM windows
1167
        app except the 350-400 band. The OOB trace has the 700MHz
1168
        band different. This is speculation at this point.
1169

    
1170
        TODO: This could be smarter and reject a radio not actually
1171
        a UV9D...
1172
        """
1173

    
1174
        for _i in range(0, 10):  # retry 10 times if we get junk
1175
            self._write_record(CMD_IDENT)
1176
            chksum_match, op, _resp = self._read_record()
1177
            if len(_resp) == 0:
1178
                raise Exception("Radio not responding")
1179
            if len(_resp) != 74:
1180
                LOG.error(
1181
                    "Expected and IDENT reply of 78 bytes. Got (%d)" %
1182
                    len(_resp))
1183
                continue
1184
            if not chksum_match:
1185
                LOG.error("Checksum error: retrying ident...")
1186
                time.sleep(0.100)
1187
                continue
1188
            if op != CMD_IDENT:
1189
                LOG.error("Expected IDENT reply. Got (%02x)" % op)
1190
                continue
1191
            LOG.debug("Got:\n%s" % _hex_print(_resp))
1192
            (mod, rev) = struct.unpack(">7s2s", _resp[0:9])
1193
            LOG.debug("Model %s, rev %s" % (mod, rev))
1194
            if mod == self._model:
1195
                self._rev = rev
1196
                return
1197
            else:
1198
                raise Exception("Unable to identify radio")
1199
        raise Exception("All retries to identify failed")
1200

    
1201
    def process_mmap(self):
1202
        if self._rev != b"02" and self._rev != b"00":
1203
            # new revision found - log it and assume same map and proceed
1204
            LOG.debug("Unrecognized model variation (%s) Using default Map" %
1205
                      self._rev)
1206
        self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap)
1207

    
1208
    def sync_in(self):
1209
        """ Public sync_in
1210
            Download contents of the radio. Throw errors back
1211
            to the core if the radio does not respond.
1212
            """
1213
        try:
1214
            self._identify()
1215
            self._mmap = self._do_download()
1216
            self._write_record(CMD_HANGUP)
1217
        except errors.RadioError:
1218
            raise
1219
        except Exception as e:
1220
            LOG.exception('Unknown error during download process')
1221
            raise errors.RadioError(
1222
                "Failed to communicate with radio: %s" % e)
1223
        self.process_mmap()
1224

    
1225
    def sync_out(self):
1226
        """ Public sync_out
1227
            Upload the modified memory image into the radio.
1228
            """
1229

    
1230
        try:
1231
            self._identify()
1232
            self._do_upload()
1233
            self._write_record(CMD_HANGUP)
1234
        except errors.RadioError:
1235
            raise
1236
        except Exception as e:
1237
            raise errors.RadioError(
1238
                "Failed to communicate with radio: %s" % e)
1239
        return
1240

    
1241
    def _do_download(self):
1242
        """ Read the whole of radio memory in 64 byte chunks.
1243
        We load the config space followed by loading memory channels.
1244
        The radio seems to be a "clone" type and the memory channels
1245
        are actually within the config space. There are separate
1246
        commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but
1247
        these seem to be a hack that can only do 4 channels at a time.
1248
        Since the radio only supports 999, (can only support 3 chars
1249
        in the display UI?) although the vendors app reads 1000
1250
        channels, it hacks back to config writes (CMD_WCONF) for the
1251
        last 3 channels and names. We keep it simple and just read
1252
        the whole thing even though the vendor app doesn't. Channels
1253
        are separate in their app simply because the radio protocol
1254
        has read/write commands to access it. What they do is simply
1255
        marshal the frequency+mode bits in 4 channel chunks followed
1256
        by a separate chunk of for names. In config space, they are two
1257
        separate arrays 1..999. Given that this space is not a
1258
        multiple of 4, there is hackery on upload to do the writes to
1259
        config space. See upload for this.
1260
        """
1261

    
1262
        mem = bytearray(0x8000)  # The radio's memory map is 32k
1263
        for addr in range(0, 0x8000, 64):
1264
            req = bytearray(struct.pack(">HB", addr, 64))
1265
            self._write_record(CMD_RCONF, req)
1266
            chksum_match, op, resp = self._read_record()
1267
            if not chksum_match:
1268
                LOG.debug(_hex_print(resp))
1269
                raise Exception(
1270
                    "Checksum error while reading configuration (0x%x)" %
1271
                    addr)
1272
            pa = struct.unpack(">H", resp[0:2])
1273
            pkt_addr = pa[0]
1274
            payload = resp[2:]
1275
            if op != CMD_RCONF or addr != pkt_addr:
1276
                raise Exception(
1277
                    "Expected CMD_RCONF (%x) reply. Got (%02x: %x)" %
1278
                    (addr, op, pkt_addr))
1279
            LOG.debug("Config read (0x%x):\n%s" %
1280
                      (addr, _hex_print(resp, '0x%(addr)04x')))
1281
            # Orig Code from 9D Plus driver was len(Payload)-1:
1282
            # This Caused every 64th byte to = 00
1283
            for i in range(0, len(payload)):
1284
                mem[addr + i] = payload[i]
1285
            if self.status_fn:
1286
                status = chirp_common.Status()
1287
                status.cur = addr
1288
                status.max = 0x8000
1289
                status.msg = "Cloning from radio"
1290
                self.status_fn(status)
1291
        return memmap.MemoryMapBytes(bytes(mem))
1292

    
1293
    def _do_upload(self):
1294
        """Walk through the config map and write updated records to
1295
        the radio. The config map contains only the regions we know
1296
        about. We don't use the channel memory commands to avoid the
1297
        hackery of using config write commands to fill in the last
1298
        3 channel memory and names slots. As we discover other useful
1299
        goodies in the map, we can add more slots...
1300
        """
1301
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1302
            cfgmap = config_map2
1303
        else:
1304
            cfgmap = config_map
1305

    
1306
        for start, blocksize, count in cfgmap:
1307
            end = start + (blocksize * count)
1308
            for addr in range(start, end, blocksize):
1309
                req = bytearray(struct.pack(">H", addr))
1310
                req.extend(self.get_mmap()[addr:addr + blocksize])
1311
                self._write_record(CMD_WCONF, req)
1312
                LOG.debug("Config write (0x%x):\n%s" %
1313
                          (addr, _hex_print(req)))
1314
                chksum_match, op, ack = self._read_record()
1315
                LOG.debug("Config write ack [%x]\n%s" %
1316
                          (addr, _hex_print(ack)))
1317
                a = struct.unpack(">H", ack)  # big endian short...
1318
                ack = a[0]
1319
                if not chksum_match or op != CMD_WCONF or addr != ack:
1320
                    msg = ""
1321
                    if not chksum_match:
1322
                        msg += "Checksum err, "
1323
                    if op != CMD_WCONF:
1324
                        msg += "cmd mismatch %x != %x, " % \
1325
                               (op, CMD_WCONF)
1326
                    if addr != ack:
1327
                        msg += "ack error %x != %x, " % (addr, ack)
1328
                    raise Exception("Radio did not ack block: %s error" % msg)
1329
                if self.status_fn:
1330
                    status = chirp_common.Status()
1331
                    status.cur = addr
1332
                    status.max = 0x8000
1333
                    status.msg = "Cloning to radio"
1334
                    self.status_fn(status)
1335

    
1336
    def get_features(self):
1337
        """ Public get_features
1338
            Return the features of this radio once we have identified
1339
            it and gotten its bits
1340
            """
1341
        rf = chirp_common.RadioFeatures()
1342
        rf.has_settings = True
1343
        rf.has_ctone = True
1344
        rf.has_rx_dtcs = True
1345
        rf.has_cross = True
1346
        rf.has_tuning_step = False
1347
        rf.has_bank = False
1348
        rf.can_odd_split = True
1349
        rf.valid_skips = ["", "S"]
1350
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
1351
        rf.valid_cross_modes = [
1352
            "Tone->Tone",
1353
            "Tone->DTCS",
1354
            "DTCS->Tone",
1355
            "DTCS->",
1356
            "->Tone",
1357
            "->DTCS",
1358
            "DTCS->DTCS",
1359
        ]
1360
        rf.valid_modes = ["FM", "NFM", "AM"]
1361
        rf.valid_power_levels = self.POWER_LEVELS
1362
        rf.valid_name_length = 8
1363
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
1364
        rf.valid_bands = [(108000000, 136000000),  # Aircraft  AM
1365
                          (136000000, 180000000),  # supports 2m
1366
                          (230000000, 250000000),
1367
                          (350000000, 400000000),
1368
                          (400000000, 520000000),  # supports 70cm
1369
                          (700000000, 985000000)]
1370
        rf.valid_characters = chirp_common.CHARSET_ASCII
1371
        rf.valid_tuning_steps = self._valid_steps
1372
        rf.memory_bounds = (1, 999)  # 999 memories
1373
        return rf
1374

    
1375
    @classmethod
1376
    def get_prompts(cls):
1377
        rp = chirp_common.RadioPrompts()
1378
        rp.experimental = ("This radio driver is currently under development. "
1379
                           "There are no known issues with it, but you should "
1380
                           "proceed with caution.")
1381
        return rp
1382

    
1383
    def get_raw_memory(self, number):
1384
        return repr(self._memobj.chan_blk[number - 1])
1385

    
1386
    def _get_tone(self, _mem, mem):
1387
        """Decode both the encode and decode CTSS/DCS codes from
1388
        the memory channel and stuff them into the UI
1389
        memory channel row.
1390
        """
1391
        txtone = short2tone(_mem.encQT)
1392
        rxtone = short2tone(_mem.decQT)
1393
        pt = "N"
1394
        pr = "N"
1395

    
1396
        if txtone == "----":
1397
            txmode = ""
1398
        elif txtone[0] == "D":
1399
            mem.dtcs = int(txtone[1:4])
1400
            if txtone[4] == "I":
1401
                pt = "R"
1402
            txmode = "DTCS"
1403
        else:
1404
            mem.rtone = float(txtone)
1405
            txmode = "Tone"
1406

    
1407
        if rxtone == "----":
1408
            rxmode = ""
1409
        elif rxtone[0] == "D":
1410
            mem.rx_dtcs = int(rxtone[1:4])
1411
            if rxtone[4] == "I":
1412
                pr = "R"
1413
            rxmode = "DTCS"
1414
        else:
1415
            mem.ctone = float(rxtone)
1416
            rxmode = "Tone"
1417

    
1418
        if txmode == "Tone" and len(rxmode) == 0:
1419
            mem.tmode = "Tone"
1420
        elif (txmode == rxmode and txmode == "Tone" and
1421
              mem.rtone == mem.ctone):
1422
            mem.tmode = "TSQL"
1423
        elif (txmode == rxmode and txmode == "DTCS" and
1424
              mem.dtcs == mem.rx_dtcs):
1425
            mem.tmode = "DTCS"
1426
        elif (len(rxmode) + len(txmode)) > 0:
1427
            mem.tmode = "Cross"
1428
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1429

    
1430
        mem.dtcs_polarity = pt + pr
1431

    
1432
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
1433
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
1434

    
1435
    def get_memory(self, number):
1436
        """ Public get_memory
1437
            Return the channel memory referenced by number to the UI.
1438
        """
1439
        _mem = self._memobj.chan_blk[number - 1]
1440
        _nam = self._memobj.chan_name[number - 1]
1441
        _val = self._memobj.chan_valid[number - 1]
1442

    
1443
        mem = chirp_common.Memory()
1444
        mem.number = number
1445
        _valid = _mem.state
1446

    
1447
        # This code attempts to robustly decipher what Wouxun considers valid
1448
        # memory locations on the 9 series radios and the factory CPS.
1449
        # It appears they use a combination of State and Rx Freq to determine
1450
        # validity rather than just the State value.
1451
        # It is possible the State value is not even used at all.
1452
        # Rather than continuously adding new Mem Valid values as they are
1453
        # found, assume any value other than 0xFF is likely valid and use
1454
        # Rx Freq to further assess validity
1455

    
1456
        if _mem.rxfreq == 0xFFFFFFFF:
1457
            # Rx freq indicates empty channel memory
1458
            # assume empty regardless of _valid and proceed to next channel
1459
            if _valid not in INVALID_MEM_VALUES:
1460
                # only log if _valid indicates the channel is not invalid
1461
                LOG.debug("CH %s Rx Freq = 0xFFFFFFFF - "
1462
                          "Treating chan as empty", mem.number)
1463
            mem.empty = True
1464
            _val.ch_valid = CHAN_INVALID
1465
            return mem
1466
        elif _valid in INVALID_MEM_VALUES:
1467
            # Check for 9PX case where CPS creates a valid channel with
1468
            # 0xFF for State -  accept it as valid as long as Rx Freq is
1469
            # <= max value
1470
            if _mem.rxfreq > 99999999:  # Max poss Value = 999.99999 MHz
1471
                LOG.debug("CH %s State invalid - Rx Frq > 999.99999 MHz: "
1472
                          "Treating chan as empty", mem.number)
1473
                mem.empty = True
1474
                _val.ch_valid = CHAN_INVALID
1475
                return mem
1476
            else:
1477
                LOG.debug("CH %s State invalid - Rx Freq valid: "
1478
                          "Assume chan valid", mem.number)
1479
                mem.empty = False
1480
                _val.ch_valid = CHAN_VALID
1481
        else:  # State not Invalid and Rx Freq not 0xFFFFFFFF
1482
            if _mem.rxfreq > 99999999:  # Max poss Value = 999.99999 MHz
1483
                LOG.debug("CH %s Invalid Rx Frq: %s MHz - "
1484
                          "Treating chan as empty", mem.number,
1485
                          int(_mem.rxfreq) / 100000)
1486
                mem.empty = True
1487
                _val.ch_valid = CHAN_INVALID
1488
                return mem
1489
            else:
1490
                _val.ch_valid= CHAN_VALID
1491
                mem.empty = False
1492

    
1493
        mem.freq = int(_mem.rxfreq) * 10
1494

    
1495
        if _mem.txfreq == 0xFFFFFFFF:
1496
            # TX freq not set
1497
            mem.duplex = "off"
1498
            mem.offset = 0
1499
        elif int(_mem.rxfreq) == int(_mem.txfreq):
1500
            mem.duplex = ""
1501
            mem.offset = 0
1502
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000:
1503
            mem.duplex = "split"
1504
            mem.offset = int(_mem.txfreq) * 10
1505
        else:
1506
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
1507
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
1508

    
1509
        mem.name = name2str(_nam.name)
1510

    
1511
        self._get_tone(_mem, mem)
1512

    
1513
        mem.skip = "" if bool(_mem.scan) else "S"
1514

    
1515
        mem.power = self.POWER_LEVELS[_mem.pwr]
1516
        if _mem.mod == 1:
1517
            mem.mode = "AM"
1518
        elif _mem.fm_dev == 0:
1519
            mem.mode = "FM"
1520
        else:
1521
            mem.mode = "NFM"
1522
        #  qt has no home in the UI
1523
        return mem
1524

    
1525
    def _set_tone(self, mem, _mem):
1526
        """Update the memory channel block CTCC/DCS tones
1527
        from the UI fields
1528
        """
1529
        def _set_dcs(code, pol):
1530
            val = int("%i" % code, 8) | 0x8000
1531
            if pol == "R":
1532
                val |= 0x4000
1533
            return val
1534

    
1535
        rx_mode = tx_mode = None
1536
        rxtone = txtone = 0x0000
1537

    
1538
        if mem.tmode == "Tone":
1539
            tx_mode = "Tone"
1540
            txtone = int(mem.rtone * 10)
1541
        elif mem.tmode == "TSQL":
1542
            rx_mode = tx_mode = "Tone"
1543
            rxtone = txtone = int(mem.ctone * 10)
1544
        elif mem.tmode == "DTCS":
1545
            tx_mode = rx_mode = "DTCS"
1546
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1547
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
1548
        elif mem.tmode == "Cross":
1549
            tx_mode, rx_mode = mem.cross_mode.split("->")
1550
            if tx_mode == "DTCS":
1551
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
1552
            elif tx_mode == "Tone":
1553
                txtone = int(mem.rtone * 10)
1554
            if rx_mode == "DTCS":
1555
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
1556
            elif rx_mode == "Tone":
1557
                rxtone = int(mem.ctone * 10)
1558

    
1559
        _mem.decQT = rxtone
1560
        _mem.encQT = txtone
1561

    
1562
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
1563
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
1564

    
1565
    def set_memory(self, mem):
1566
        """ Public set_memory
1567
            Inverse of get_memory. Update the radio memory image
1568
            from the mem object
1569
            """
1570
        number = mem.number
1571

    
1572
        _mem = self._memobj.chan_blk[number - 1]
1573
        _nam = self._memobj.chan_name[number - 1]
1574
        _val = self._memobj.chan_valid[number - 1]
1575

    
1576
        if mem.empty:
1577
            # consider putting in a check for chan # that is empty but
1578
            # listed as one of the 2 working channels and change them
1579
            # to channel 1 to be consistent with CPS and allow
1580
            # complete deletion from radio.  Otherwise,
1581
            # a deleted channel will still show on radio with no name.
1582
            # MRT implement the above working channel check
1583
            if self._memobj.a_conf.w_chan == number:
1584
                self._memobj.a_conf.w_chan = 1
1585
            if self._memobj.b_conf.w_chan == number:
1586
                self._memobj.b_conf.w_chan = 1
1587

    
1588
            _mem.set_raw("\xFF" * (_mem.size() // 8))
1589
            _nam.name = str2name("", 8, '\0', '\0')
1590
            _mem.state = MEM_INVALID
1591
            _val.ch_valid = CHAN_INVALID
1592
            return
1593

    
1594
        _mem.rxfreq = int(mem.freq / 10)
1595
        if mem.duplex == "off":
1596
            _mem.txfreq = 0xFFFFFFFF
1597
        elif mem.duplex == "split":
1598
            _mem.txfreq = int(mem.offset / 10)
1599
        elif mem.duplex == "+":
1600
            _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10)
1601
        elif mem.duplex == "-":
1602
            _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10)
1603
        else:
1604
            _mem.txfreq = int(mem.freq / 10)
1605
        _mem.scan = int(mem.skip != "S")
1606
        if mem.mode == "FM":
1607
            _mem.mod = 0    # make sure forced AM is off
1608
            _mem.fm_dev = 0
1609
        elif mem.mode == "NFM":
1610
            _mem.mod = 0
1611
            _mem.fm_dev = 1
1612
        elif mem.mode == "AM":
1613
            _mem.mod = 1     # AM on
1614
            _mem.fm_dev = 1  # set NFM bandwidth
1615
        else:
1616
            _mem.mod = 0
1617
            _mem.fm_dev = 0  # Catchall default is FM
1618
        # set the tone
1619
        self._set_tone(mem, _mem)
1620
        # set the power
1621
        if mem.power:
1622
            _mem.pwr = self.POWER_LEVELS.index(mem.power)
1623
        else:
1624
            _mem.pwr = True
1625

    
1626
        # Set fields we can't access via the UI table to safe defaults
1627
        _mem.qt = 0   # mute mode to QT
1628
        _mem.bit5 = 0   # clear this bit to ensure accurate CPS power level
1629
        _nam.name = str2name(mem.name, 8, '\0', '\0')
1630
        _mem.state = MEM_VALID
1631
        _val.ch_valid = CHAN_VALID
1632

    
1633
# Build the UI configuration tabs
1634
# the channel memory tab is built by the core.
1635
# We have no control over it
1636

    
1637
    def _core_tab(self):
1638
        """ Build Core Configuration tab
1639
        Radio settings common to all modes and areas go here.
1640
        """
1641
        s = self._memobj.settings
1642
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1643

    
1644
            sm = self._memobj.screen
1645

    
1646
        cf = RadioSettingGroup("cfg_grp", "Configuration")
1647

    
1648
        cf.append(RadioSetting("auto_am",
1649
                               "Auto detect AM (Menu 53)",
1650
                               RadioSettingValueBoolean(s.auto_am)))
1651
        cf.append(RadioSetting("qt_sw",
1652
                               "Scan tone detect (Menu 59)",
1653
                               RadioSettingValueBoolean(s.qt_sw)))
1654
        cf.append(
1655
            RadioSetting("s_mute",
1656
                         "SubFreq Mute (Menu 60)",
1657
                         RadioSettingValueList(S_MUTE_LIST,
1658
                                               S_MUTE_LIST[s.s_mute])))
1659
        cf.append(
1660
            RadioSetting("tot",
1661
                         "Transmit timeout Timer (Menu 10)",
1662
                         RadioSettingValueList(TIMEOUT_LIST,
1663
                                               TIMEOUT_LIST[s.tot])))
1664
        cf.append(
1665
            RadioSetting("toa",
1666
                         "Transmit Timeout Alarm (Menu 11)",
1667
                         RadioSettingValueList(TOA_LIST,
1668
                                               TOA_LIST[s.toa])))
1669
        cf.append(
1670
            RadioSetting("ptt_id",
1671
                         "PTT Caller ID mode (Menu 23)",
1672
                         RadioSettingValueList(PTTID_LIST,
1673
                                               PTTID_LIST[s.ptt_id])))
1674
        cf.append(
1675
            RadioSetting("id_dly",
1676
                         "Caller ID Delay time (Menu 25)",
1677
                         RadioSettingValueList(ID_DLY_LIST,
1678
                                               ID_DLY_LIST[s.id_dly])))
1679
        cf.append(RadioSetting("voice_sw",
1680
                               "Voice Guide (Menu 12)",
1681
                               RadioSettingValueBoolean(s.voice_sw)))
1682
        cf.append(RadioSetting("beep",
1683
                               "Keypad Beep (Menu 13)",
1684
                               RadioSettingValueBoolean(s.beep)))
1685
        cf.append(
1686
            RadioSetting("s_tone",
1687
                         "Side Tone (Menu 36)",
1688
                         RadioSettingValueList(S_TONES,
1689
                                               S_TONES[s.s_tone])))
1690
        cf.append(
1691
            RadioSetting("ring_time",
1692
                         "Ring Time (Menu 26)",
1693
                         RadioSettingValueList(
1694
                             LIST_OFF_10,
1695
                             LIST_OFF_10[s.ring_time])))
1696
        cf.append(
1697
            RadioSetting("roger",
1698
                         "Roger Beep (Menu 9)",
1699
                         RadioSettingValueList(ROGER_LIST,
1700
                                               ROGER_LIST[s.roger])))
1701
        cf.append(RadioSetting("blcdsw",
1702
                               "Backlight (Menu 41)",
1703
                               RadioSettingValueBoolean(s.blcdsw)))
1704
        cf.append(
1705
            RadioSetting("abr",
1706
                         "Auto Backlight Time (Menu 1)",
1707
                         RadioSettingValueList(BACKLIGHT_LIST,
1708
                                               BACKLIGHT_LIST[s.abr])))
1709
        cf.append(
1710
            RadioSetting("abr_lvl",
1711
                         "Backlight Brightness (Menu 27)",
1712
                         RadioSettingValueInteger(BACKLIGHT_BRIGHT_MIN,
1713
                                                  BACKLIGHT_BRIGHT_MAX,
1714
                                                  s.abr_lvl)))
1715
        cf.append(RadioSetting("lock",
1716
                               "Keypad Lock",
1717
                               RadioSettingValueBoolean(s.lock)))
1718
        cf.append(
1719
            RadioSetting("lock_m",
1720
                         "Keypad Lock Mode (Menu 35)",
1721
                         RadioSettingValueList(LOCK_MODES,
1722
                                               LOCK_MODES[s.lock_m])))
1723
        cf.append(RadioSetting("auto_lk",
1724
                               "Keypad Autolock (Menu 34)",
1725
                               RadioSettingValueBoolean(s.auto_lk)))
1726
        cf.append(RadioSetting("prich_sw",
1727
                               "Priority Channel Scan (Menu 33)",
1728
                               RadioSettingValueBoolean(s.prich_sw)))
1729
        cf.append(RadioSetting("pri_ch",
1730
                               "Priority Channel (Menu 32)",
1731
                               RadioSettingValueInteger(1, 999,
1732
                                                        s.pri_ch)))
1733
        cf.append(
1734
            RadioSetting("dtmf_st",
1735
                         "DTMF Sidetone (Menu 22)",
1736
                         RadioSettingValueList(DTMFST_LIST,
1737
                                               DTMFST_LIST[s.dtmf_st])))
1738
        cf.append(RadioSetting("sc_qt",
1739
                               "Scan QT Save Mode (Menu 38)",
1740
                               RadioSettingValueList(
1741
                                   SCQT_LIST,
1742
                                   SCQT_LIST[s.sc_qt])))
1743
        cf.append(
1744
            RadioSetting("apo_tmr",
1745
                         "Automatic Power-off (Menu 39)",
1746
                         RadioSettingValueList(APO_TIMES,
1747
                                               APO_TIMES[s.apo_tmr])))
1748
        cf.append(  # VOX "guard" is really VOX trigger audio level
1749
            RadioSetting("vox_grd",
1750
                         "VOX level (Menu 7)",
1751
                         RadioSettingValueList(VOX_GRDS,
1752
                                               VOX_GRDS[s.vox_grd])))
1753
        cf.append(
1754
            RadioSetting("vox_dly",
1755
                         "VOX Delay (Menu 37)",
1756
                         RadioSettingValueList(VOX_DLYS,
1757
                                               VOX_DLYS[s.vox_dly])))
1758
        cf.append(RadioSetting("bledsw",
1759
                               "Receive LED (Menu 42)",
1760
                               RadioSettingValueBoolean(s.bledsw)))
1761

    
1762
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1763
                cf.append(RadioSetting("screen.screen_mode",
1764
                                       "Screen Mode (Menu 62)",
1765
                                       RadioSettingValueList(
1766
                                             SCREEN_MODE_LIST,
1767
                                             SCREEN_MODE_LIST[
1768
                                                 sm.screen_mode])))
1769
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1770
            langlst = LANGUAGE_LIST2
1771
        else:
1772
            langlst = LANGUAGE_LIST
1773
        cf.append(
1774
            RadioSetting("lang",
1775
                         "Menu Language (Menu 14)",
1776
                         RadioSettingValueList(langlst,
1777
                                               langlst[s.lang])))
1778

    
1779
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1780
            ponmsglst = PONMSG_LIST2
1781
        else:
1782
            ponmsglst = PONMSG_LIST
1783
        cf.append(RadioSetting("ponmsg",
1784
                               "Poweron message (Menu 40)",
1785
                               RadioSettingValueList(
1786
                                   ponmsglst, ponmsglst[s.ponmsg])))
1787
        return cf
1788

    
1789
    def _repeater_tab(self):
1790
        """Repeater mode functions
1791
        """
1792
        s = self._memobj.settings
1793
        cf = RadioSettingGroup("repeater", "Repeater Functions")
1794

    
1795
        cf.append(
1796
            RadioSetting("type_set",
1797
                         "Radio Mode (Menu 43)",
1798
                         RadioSettingValueList(
1799
                             RPTMODE_LIST,
1800
                             RPTMODE_LIST[s.type_set])))
1801
        cf.append(RadioSetting("rpt_ptt",
1802
                               "Repeater PTT (Menu 45)",
1803
                               RadioSettingValueBoolean(s.rpt_ptt)))
1804
        cf.append(RadioSetting("rpt_spk",
1805
                               "Repeater Mode Speaker (Menu 44)",
1806
                               RadioSettingValueBoolean(s.rpt_spk)))
1807
        cf.append(
1808
            RadioSetting("rpt_kpt",
1809
                         "Repeater Hold Time (Menu 46)",
1810
                         RadioSettingValueList(RPT_KPTS,
1811
                                               RPT_KPTS[s.rpt_kpt])))
1812
        cf.append(RadioSetting("rpt_rct",
1813
                               "Repeater Receipt Tone (Menu 47)",
1814
                               RadioSettingValueBoolean(s.rpt_rct)))
1815
        return cf
1816

    
1817
    def _admin_tab(self):
1818
        """Admin functions not present in radio menu...
1819
        These are admin functions not radio operation configuration
1820
        """
1821

    
1822
        def apply_cid(setting, obj):
1823
            c = str2callid(setting.value)
1824
            obj.code = c
1825

    
1826
        def apply_scc(setting, obj):
1827
            c = str2digits(setting.value)
1828
            obj.scc = c
1829

    
1830
        def apply_mode_sw(setting, obj):
1831
            pw = str2pw(setting.value)
1832
            obj.mode_sw = pw
1833
            setting.value = pw2str(obj.mode_sw)
1834

    
1835
        def apply_reset(setting, obj):
1836
            pw = str2pw(setting.value)
1837
            obj.reset = pw
1838
            setting.value = pw2str(obj.reset)
1839

    
1840
        def apply_wake(setting, obj):
1841
            obj.wake = int(setting.value)/10
1842

    
1843
        def apply_sleep(setting, obj):
1844
            obj.sleep = int(setting.value)/10
1845

    
1846
        pw = self._memobj.passwords  # admin passwords
1847
        s = self._memobj.settings
1848

    
1849
        cf = RadioSettingGroup("admin", "Admin Functions")
1850

    
1851
        cf.append(RadioSetting("menu_avail",
1852
                               "Menu available in channel mode",
1853
                               RadioSettingValueBoolean(s.menu_avail)))
1854
        mode_sw = RadioSettingValueString(0, 6,
1855
                                          pw2str(pw.mode_sw), False)
1856
        rs = RadioSetting("passwords.mode_sw",
1857
                          "Mode Switch Password", mode_sw)
1858
        rs.set_apply_callback(apply_mode_sw, pw)
1859
        cf.append(rs)
1860

    
1861
        cf.append(RadioSetting("reset_avail",
1862
                               "Radio Reset Available",
1863
                               RadioSettingValueBoolean(s.reset_avail)))
1864
        reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False)
1865
        rs = RadioSetting("passwords.reset",
1866
                          "Radio Reset Password", reset)
1867
        rs.set_apply_callback(apply_reset, pw)
1868
        cf.append(rs)
1869

    
1870
        cf.append(
1871
            RadioSetting("dtmf_tx",
1872
                         "DTMF Tx Duration",
1873
                         RadioSettingValueList(DTMF_TIMES,
1874
                                               DTMF_TIMES[s.dtmf_tx])))
1875
        cid = self._memobj.my_callid
1876
        my_callid = RadioSettingValueString(3, 6,
1877
                                            self.callid2str(cid.code), False)
1878
        rs = RadioSetting("my_callid.code",
1879
                          "PTT Caller ID code (Menu 24)", my_callid)
1880
        rs.set_apply_callback(apply_cid, cid)
1881
        cf.append(rs)
1882

    
1883
        stun = self._memobj.stun
1884
        st = RadioSettingValueString(0, 6, digits2str(stun.scc), False)
1885
        rs = RadioSetting("stun.scc", "Security code", st)
1886
        rs.set_apply_callback(apply_scc, stun)
1887
        cf.append(rs)
1888

    
1889
        cf.append(
1890
            RadioSetting("settings.save_m",
1891
                         "Save Mode  (Menu 2)",
1892
                         RadioSettingValueList(SAVE_MODES,
1893
                                               SAVE_MODES[s.save_m])))
1894
        for i in range(0, 4):
1895
            sm = self._memobj.save[i]
1896
            wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1)
1897
            wf = RadioSetting("save[%i].wake" % i,
1898
                              "Save Mode %d Wake Time" % (i+1), wake)
1899
            wf.set_apply_callback(apply_wake, sm)
1900
            cf.append(wf)
1901

    
1902
            slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1)
1903
            wf = RadioSetting("save[%i].sleep" % i,
1904
                              "Save Mode %d Sleep Time" % (i+1), slp)
1905
            wf.set_apply_callback(apply_sleep, sm)
1906
            cf.append(wf)
1907

    
1908
        _msg = str(self._memobj.display.banner).split("\0")[0]
1909
        val = RadioSettingValueString(0, 16, _msg)
1910
        val.set_mutable(True)
1911
        cf.append(RadioSetting("display.banner",
1912
                               "Display Message", val))
1913

    
1914
        if (self.MODEL == "KG-UV9PX" or self.MODEL == "KG-UV9GX"):
1915
            _str = str(self._memobj.oemmodel.model).split("\0")[0]
1916
            val = RadioSettingValueString(0, 10, _str)
1917
            val.set_mutable(True)
1918
            cf.append(RadioSetting("oemmodel.model",
1919
                                   "Custom Sub-Receiver Message", val))
1920

    
1921
            val = RadioSettingValueList(
1922
                                TDR_LIST,
1923
                                TDR_LIST[s.tdr])
1924
            val.set_mutable(True)
1925
            cf.append(RadioSetting("tdr", "TDR", val))
1926

    
1927
            val = RadioSettingValueList(
1928
                                ACTIVE_AREA_LIST,
1929
                                ACTIVE_AREA_LIST[s.act_area])
1930
            val.set_mutable(True)
1931
            cf.append(RadioSetting("act_area", "Active Receiver(BAND)", val))
1932

    
1933
        return cf
1934

    
1935
    def _fm_tab(self):
1936
        """FM Broadcast channels
1937
        """
1938
        def apply_fm(setting, obj):
1939
            f = freq2short(setting.value, 76000000, 108000000)
1940
            obj.fm_freq = f
1941

    
1942
        fm = RadioSettingGroup("fm_chans", "FM Broadcast")
1943
        for ch in range(0, 20):
1944
            chan = self._memobj.fm_chans[ch]
1945
            freq = RadioSettingValueString(0, 20,
1946
                                           short2freq(chan.fm_freq))
1947
            rs = RadioSetting("fm_%d" % (ch + 1),
1948
                              "FM Channel %d" % (ch + 1), freq)
1949
            rs.set_apply_callback(apply_fm, chan)
1950
            fm.append(rs)
1951
        return fm
1952

    
1953
    def _scan_grp(self):
1954
        """Scan groups
1955
        """
1956
        def apply_name(setting, obj):
1957
            name = str2name(setting.value, 8, '\0', '\0')
1958
            obj.name = name
1959

    
1960
        def apply_start(setting, obj):
1961
            """Do a callback to deal with RadioSettingInteger limitation
1962
            on memory address resolution
1963
            """
1964
            obj.scan_st = int(setting.value)
1965

    
1966
        def apply_end(setting, obj):
1967
            """Do a callback to deal with RadioSettingInteger limitation
1968
            on memory address resolution
1969
            """
1970
            obj.scan_end = int(setting.value)
1971

    
1972
        sgrp = self._memobj.scn_grps
1973
        scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups")
1974
        for i in range(0, 10):
1975
            s_grp = sgrp.addrs[i]
1976
            s_name = sgrp.names[i]
1977
            rs_name = RadioSettingValueString(0, 8,
1978
                                              name2str(s_name.name))
1979
            rs = RadioSetting("scn_grps.names[%i].name" % i,
1980
                              "Group %i Name" % (i + 1), rs_name)
1981
            rs.set_apply_callback(apply_name, s_name)
1982
            scan.append(rs)
1983
            rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st)
1984
            rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i,
1985
                              "Starting Channel", rs_st)
1986
            rs.set_apply_callback(apply_start, s_grp)
1987
            scan.append(rs)
1988
            rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end)
1989
            rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i,
1990
                              "Last Channel", rs_end)
1991
            rs.set_apply_callback(apply_end, s_grp)
1992
            scan.append(rs)
1993
        return scan
1994

    
1995
    def _callid_grp(self):
1996
        """Caller IDs to be recognized by radio
1997
        This really should be a table in the UI
1998
        """
1999
        def apply_callid(setting, obj):
2000
            c = str2callid(setting.value)
2001
            obj.cid = c
2002

    
2003
        def apply_name(setting, obj):
2004
            name = str2name(setting.value, 6, '\0', '\xff')
2005
            obj.name = name
2006

    
2007
        cid = RadioSettingGroup("callids", "Caller IDs")
2008
        for i in range(0, 20):
2009
            callid = self._memobj.call_ids[i]
2010
            name = self._memobj.cid_names[i]
2011
            c_name = RadioSettingValueString(0, 6, name2str(name.name))
2012
            rs = RadioSetting("cid_names[%i].name" % i,
2013
                              "Caller ID %i Name" % (i + 1), c_name)
2014
            rs.set_apply_callback(apply_name, name)
2015
            cid.append(rs)
2016
            c_id = RadioSettingValueString(0, 6,
2017
                                           self.callid2str(callid.cid),
2018
                                           False)
2019
            rs = RadioSetting("call_ids[%i].cid" % i,
2020
                              "Caller ID Code", c_id)
2021
            rs.set_apply_callback(apply_callid, callid)
2022
            cid.append(rs)
2023
        return cid
2024

    
2025
    def _band_tab(self, area, band):
2026
        """ Build a band tab inside a VFO/Area
2027
        """
2028
        def apply_freq(setting, lo, hi, obj):
2029
            f = freq2int(setting.value, lo, hi)
2030
            obj.freq = f/10
2031

    
2032
        def apply_offset(setting, obj):
2033
            f = freq2int(setting.value, 0, 5000000)
2034
            obj.offset = f/10
2035

    
2036
        def apply_enc(setting, obj):
2037
            t = tone2short(setting.value)
2038
            obj.encqt = t
2039

    
2040
        def apply_dec(setting, obj):
2041
            t = tone2short(setting.value)
2042
            obj.decqt = t
2043

    
2044
        if area == "a":
2045
            if band == 150:
2046
                c = self._memobj.vfo_a.band_150
2047
                lo = 108000000
2048
                hi = 180000000
2049
            elif band == 200:
2050
                c = self._memobj.vfo_a.band_200
2051
                lo = 230000000
2052
                hi = 250000000
2053
            elif band == 300:
2054
                c = self._memobj.vfo_a.band_300
2055
                lo = 350000000
2056
                hi = 400000000
2057
            elif band == 450:
2058
                c = self._memobj.vfo_a.band_450
2059
                lo = 400000000
2060
                hi = 512000000
2061
            else:   # 700
2062
                c = self._memobj.vfo_a.band_700
2063
                lo = 700000000
2064
                hi = 985000000
2065
        else:  # area 'b'
2066
            if band == 150:
2067
                c = self._memobj.vfo_b.band_150
2068
                lo = 136000000
2069
                hi = 180000000
2070
            else:  # 450
2071
                c = self._memobj.vfo_b.band_450
2072
                lo = 400000000
2073
                hi = 512000000
2074

    
2075
        prefix = "vfo_%s.band_%d" % (area, band)
2076
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2077
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2078
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2079
        rs.set_apply_callback(apply_freq, lo, hi, c)
2080
        bf.append(rs)
2081

    
2082
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2083
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2084
        rs.set_apply_callback(apply_offset, c)
2085
        bf.append(rs)
2086

    
2087
        rs = RadioSetting(prefix + ".encqt",
2088
                          "Encode QT (Menu 17,19)",
2089
                          RadioSettingValueList(TONE_LIST,
2090
                                                short2tone(c.encqt)))
2091
        rs.set_apply_callback(apply_enc, c)
2092
        bf.append(rs)
2093

    
2094
        rs = RadioSetting(prefix + ".decqt",
2095
                          "Decode QT (Menu 16,18)",
2096
                          RadioSettingValueList(TONE_LIST,
2097
                                                short2tone(c.decqt)))
2098
        rs.set_apply_callback(apply_dec, c)
2099
        bf.append(rs)
2100

    
2101
        bf.append(RadioSetting(prefix + ".qt",
2102
                               "Mute Mode (Menu 21)",
2103
                               RadioSettingValueList(SPMUTE_LIST,
2104
                                                     SPMUTE_LIST[c.qt])))
2105
        bf.append(RadioSetting(prefix + ".scan",
2106
                               "Scan this (Menu 48)",
2107
                               RadioSettingValueBoolean(c.scan)))
2108
        bf.append(RadioSetting(prefix + ".pwr",
2109
                               "Power (Menu 5)",
2110
                               RadioSettingValueList(
2111
                                   POWER_LIST, POWER_LIST[c.pwr])))
2112
        bf.append(RadioSetting(prefix + ".mod",
2113
                               "AM Modulation (Menu 54)",
2114
                               RadioSettingValueBoolean(c.mod)))
2115
        bf.append(RadioSetting(prefix + ".fm_dev",
2116
                               "FM Deviation (Menu 4)",
2117
                               RadioSettingValueList(
2118
                                   BANDWIDTH_LIST,
2119
                                   BANDWIDTH_LIST[c.fm_dev])))
2120
        bf.append(
2121
            RadioSetting(prefix + ".shift",
2122
                         "Frequency Shift (Menu 6)",
2123
                         RadioSettingValueList(OFFSET_LIST,
2124
                                               OFFSET_LIST[c.shift])))
2125
        return bf
2126

    
2127
    def _area_tab(self, area):
2128
        """Build a VFO tab
2129
        """
2130
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2131
            f = freq2short(setting.value, scan_lo, scan_hi)
2132
            obj.scan_st = f
2133

    
2134
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2135
            f = freq2short(setting.value, scan_lo, scan_hi)
2136
            obj.scan_end = f
2137

    
2138
        if area == "a":
2139
            desc = "Area A Settings"
2140
            c = self._memobj.a_conf
2141
            scan_lo = 108000000
2142
            scan_hi = 985000000
2143
            scan_rng = self._memobj.settings.a
2144
            band_list = (150, 200, 300, 450, 700)
2145
        else:
2146
            desc = "Area B Settings"
2147
            c = self._memobj.b_conf
2148
            scan_lo = 136000000
2149
            scan_hi = 512000000
2150
            scan_rng = self._memobj.settings.b
2151
            band_list = (150, 450)
2152

    
2153
        prefix = "%s_conf" % area
2154
        af = RadioSettingGroup(prefix, desc)
2155
        af.append(
2156
            RadioSetting(prefix + ".w_mode",
2157
                         "Workmode",
2158
                         RadioSettingValueList(
2159
                             WORKMODE_LIST,
2160
                             WORKMODE_LIST[c.w_mode])))
2161
        af.append(RadioSetting(prefix + ".w_chan",
2162
                               "Channel",
2163
                               RadioSettingValueInteger(1, 999,
2164
                                                        c.w_chan)))
2165
        af.append(
2166
            RadioSetting(prefix + ".scan_grp",
2167
                         "Scan Group (Menu 49)",
2168
                         RadioSettingValueList(
2169
                             SCANGRP_LIST,
2170
                             SCANGRP_LIST[c.scan_grp])))
2171
        af.append(RadioSetting(prefix + ".bcl",
2172
                               "Busy Channel Lock-out (Menu 15)",
2173
                               RadioSettingValueBoolean(c.bcl)))
2174
        af.append(
2175
            RadioSetting(prefix + ".sql",
2176
                         "Squelch Level (Menu 8)",
2177
                         RadioSettingValueList(LIST_0_9,
2178
                                               LIST_0_9[c.sql])))
2179
        af.append(
2180
            RadioSetting(prefix + ".cset",
2181
                         "Call ID Group (Menu 52)",
2182
                         RadioSettingValueList(LIST_1_20,
2183
                                               LIST_1_20[c.cset])))
2184
        af.append(
2185
            RadioSetting(prefix + ".step",
2186
                         "Frequency Step (Menu 3)",
2187
                         RadioSettingValueList(
2188
                             self._step_list, self._step_list[c.step])))
2189
        af.append(
2190
            RadioSetting(prefix + ".scan_mode",
2191
                         "Scan Mode (Menu 20)",
2192
                         RadioSettingValueList(
2193
                             SCANMODE_LIST,
2194
                             SCANMODE_LIST[c.scan_mode])))
2195
        af.append(
2196
            RadioSetting(prefix + ".scan_range",
2197
                         "Scan Range (Menu 50)",
2198
                         RadioSettingValueList(
2199
                             SCANRANGE_LIST,
2200
                             SCANRANGE_LIST[c.scan_range])))
2201
        st = RadioSettingValueString(0, 15,
2202
                                     short2freq(scan_rng.scan_st))
2203
        rs = RadioSetting("settings.%s.scan_st" % area,
2204
                          "Frequency Scan Start", st)
2205
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2206
        af.append(rs)
2207

    
2208
        end = RadioSettingValueString(0, 15,
2209
                                      short2freq(scan_rng.scan_end))
2210
        rs = RadioSetting("settings.%s.scan_end" % area,
2211
                          "Frequency Scan End", end)
2212
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2213
                              scan_rng)
2214
        af.append(rs)
2215
        # Each area has its own set of bands
2216
        for band in (band_list):
2217
            af.append(self._band_tab(area, band))
2218
        return af
2219

    
2220
    def _key_tab(self):
2221
        """Build radio key/button menu
2222
        """
2223
        s = self._memobj.settings
2224
        if self.MODEL == "KG-UV9PX":
2225
            pfkey1 = PF1KEY_LIST
2226
            pfkey2 = PF2KEY_LIST
2227
            pfkey3 = PF3KEY_LIST2
2228
        elif (self.MODEL == "KG-UV9GX" or
2229
               self.MODEL == "KG-UV9G Pro"):
2230
            pfkey1 = PF1KEY_LIST9GX
2231
            pfkey2 = PF2KEY_LIST9GX
2232
            pfkey3 = PF3KEY_LIST9GX
2233
        else:
2234
            pfkey1 = PF1KEY_LIST
2235
            pfkey2 = PF2KEY_LIST
2236
            pfkey3 = PF3KEY_LIST
2237

    
2238
        kf = RadioSettingGroup("key_grp", "Key Settings")
2239

    
2240
        kf.append(RadioSetting("settings.pf1",
2241
                               "PF1 Key function (Menu 55)",
2242
                               RadioSettingValueList(
2243
                                   pfkey1,
2244
                                   pfkey1[s.pf1])))
2245
        kf.append(RadioSetting("settings.pf2",
2246
                               "PF2 Key function (Menu 56)",
2247
                               RadioSettingValueList(
2248
                                   pfkey2,
2249
                                   pfkey2[s.pf2])))
2250

    
2251
        kf.append(RadioSetting("settings.pf3",
2252
                               "PF3 Key function (Menu 57)",
2253
                               RadioSettingValueList(
2254
                                   pfkey3,
2255
                                   pfkey3[s.pf3])))
2256
        return kf
2257

    
2258
    def _fl_tab(self):
2259
        """Build the frequency limits tab
2260
        """
2261

    
2262
        # The stop limits in the factory KG-UV9D Mate memory image are 1MHz
2263
        # higher than the published specs. The settings panel will crash if
2264
        # it encounters a value outside of these ranges.
2265
        hard_limits = {
2266
            "band_150": (108000000, 181000000),
2267
            "band_450": (400000000, 513000000),
2268
            "band_300": (350000000, 401000000),
2269
            "band_700": (700000000, 987000000),
2270
            "band_200": (230000000, 251000000)
2271
        }
2272

    
2273
        def apply_freq_start(setting, low, high, obj):
2274
            f = freq2short(setting.value, low, high)
2275
            obj.start = f
2276

    
2277
        def apply_freq_stop(setting, low, high, obj):
2278
            """Sets the stop limit to 1MHz below the input value"""
2279

    
2280
            # The firmware has an off-by-1MHz error with stop limits.
2281
            # If you set the stop limit to 1480 (148MHz), you can still tune
2282
            # up to 148.99MHz. To compensate for this,
2283
            # we subtract 10 increments of 100MHz before storing the value.
2284
            f = freq2short(setting.value, low, high) - 10
2285
            obj.stop = f
2286

    
2287
        fl = RadioSettingGroup("freq_limit_grp", "Frequency Limits")
2288

    
2289
        rx = self._memobj.rx_freq_limits
2290
        tx = self._memobj.tx_freq_limits
2291

    
2292
        for rx_band in rx.items():
2293
            name, limits = rx_band
2294

    
2295
            start_freq = RadioSettingValueString(1,
2296
                                                 20,
2297
                                                 short2freq(limits.start))
2298
            start_rs = RadioSetting("rx_start_" + name,
2299
                                    name + " Receive Start",
2300
                                    start_freq)
2301
            start_rs.set_apply_callback(apply_freq_start,
2302
                                        hard_limits[name][0],
2303
                                        hard_limits[name][1],
2304
                                        limits)
2305
            fl.append(start_rs)
2306

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

    
2321
        for tx_band in tx.items():
2322
            name, limits = tx_band
2323

    
2324
            start_freq = RadioSettingValueString(1,
2325
                                                 20,
2326
                                                 short2freq(limits.start))
2327
            start_rs = RadioSetting("tx_start_" + name,
2328
                                    name + " Transmit Start",
2329
                                    start_freq)
2330
            start_rs.set_apply_callback(apply_freq_start,
2331
                                        hard_limits[name][0],
2332
                                        hard_limits[name][1], limits)
2333
            fl.append(start_rs)
2334

    
2335
            # Add 10 increments of 100MHz before displaying to compensate for
2336
            # the firmware off-by-1MHz problem.
2337
            stop_freq = RadioSettingValueString(1,
2338
                                                20,
2339
                                                short2freq(limits.stop + 10))
2340
            stop_rs = RadioSetting("tx_stop_" + name,
2341
                                   name + " Transmit Stop",
2342
                                   stop_freq)
2343
            stop_rs.set_apply_callback(apply_freq_stop,
2344
                                       hard_limits[name][0],
2345
                                       hard_limits[name][1],
2346
                                       limits)
2347
            fl.append(stop_rs)
2348

    
2349
        return fl
2350

    
2351
    def _get_settings(self):
2352
        """Build the radio configuration settings menus
2353
        """
2354

    
2355
        core_grp = self._core_tab()
2356
        fm_grp = self._fm_tab()
2357
        area_a_grp = self._area_tab("a")
2358
        area_b_grp = self._area_tab("b")
2359
        key_grp = self._key_tab()
2360
        scan_grp = self._scan_grp()
2361
        callid_grp = self._callid_grp()
2362
        admin_grp = self._admin_tab()
2363
        rpt_grp = self._repeater_tab()
2364
        freq_limit_grp = self._fl_tab()
2365

    
2366
        core_grp.append(key_grp)
2367
        core_grp.append(admin_grp)
2368
        core_grp.append(rpt_grp)
2369
        core_grp.append(freq_limit_grp)
2370
        group = RadioSettings(core_grp,
2371
                              area_a_grp,
2372
                              area_b_grp,
2373
                              fm_grp,
2374
                              scan_grp,
2375
                              callid_grp
2376
                              )
2377
        return group
2378

    
2379
    def get_settings(self):
2380
        """ Public build out linkage between radio settings and UI
2381
        """
2382
        try:
2383
            return self._get_settings()
2384
        except Exception:
2385
            import traceback
2386
            LOG.error("Failed to parse settings: %s",
2387
                      traceback.format_exc())
2388
            return None
2389

    
2390
    def _is_freq(self, element):
2391
        """This is a hack to smoke out whether we need to do
2392
        frequency translations for otherwise innocent u16s and u32s
2393
        """
2394
        return "rxfreq" in element.get_name() or \
2395
               "txfreq" in element.get_name() or \
2396
               "scan_st" in element.get_name() or \
2397
               "scan_end" in element.get_name() or \
2398
               "offset" in element.get_name() or \
2399
               "fm_stop" in element.get_name()
2400

    
2401
    def _is_limit(self, element):
2402
        return "lower_limit" in element.get_name() or\
2403
               "upper_limit" in element.get_name()
2404

    
2405
    def set_settings(self, settings):
2406
        """ Public update radio settings via UI callback
2407
        A lot of this should be in common code....
2408
        """
2409

    
2410
        for element in settings:
2411
            if not isinstance(element, RadioSetting):
2412
                LOG.debug("set_settings: not instance %s" %
2413
                          element.get_name())
2414
                self.set_settings(element)
2415
                continue
2416
            else:
2417
                try:
2418
                    if "." in element.get_name():
2419
                        bits = element.get_name().split(".")
2420
                        obj = self._memobj
2421
                        for bit in bits[:-1]:
2422
                            # decode an array index
2423
                            if "[" in bit and "]" in bit:
2424
                                bit, index = bit.split("[", 1)
2425
                                index, junk = index.split("]", 1)
2426
                                index = int(index)
2427
                                obj = getattr(obj, bit)[index]
2428
                            else:
2429
                                obj = getattr(obj, bit)
2430
                        setting = bits[-1]
2431
                    else:
2432
                        obj = self._memobj.settings
2433
                        setting = element.get_name()
2434

    
2435
                    if element.has_apply_callback():
2436
                        LOG.debug("Using apply callback")
2437
                        element.run_apply_callback()
2438
                    else:
2439
                        LOG.debug("Setting %s = %s" %
2440
                                  (setting, element.value))
2441
                        if self._is_freq(element):
2442
                            setattr(obj, setting, int(element.value)/10)
2443
                        elif self._is_limit(element):
2444
                            setattr(obj, setting, int(element.value)*10)
2445
                        else:
2446
                            setattr(obj, setting, element.value)
2447
                except Exception as e:
2448
                    LOG.debug("set_settings: Exception with %s" %
2449
                              element.get_name())
2450
                    raise
2451

    
2452
    def callid2str(self, cid):
2453
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2454
        One digit (binary) per byte, terminated with '0xc'
2455
        """
2456

    
2457
        bin2ascii = " 1234567890"
2458
        cidstr = ""
2459
        for i in range(0, 6):
2460
            b = cid[i].get_value()
2461
            if b == 0xc:  # the cid EOL
2462
                break
2463
            if b == 0 or b > 0xa:
2464
                raise InvalidValueError(
2465
                    "Caller ID code has illegal byte 0x%x" % b)
2466
            cidstr += bin2ascii[b]
2467
        return cidstr
2468

    
2469

    
2470
@directory.register
2471
class KGUV9PXRadio(KGUV9DPlusRadio):
2472

    
2473
    """Wouxun KG-UV9PX"""
2474
    VENDOR = "Wouxun"
2475
    MODEL = "KG-UV9PX"
2476
    _model = b"KG-UV9D"
2477
    _rev = b"02"  # default rev for the radio I know about...
2478
    _file_ident = b"kg-uv9px"
2479
    NEEDS_COMPAT_SERIAL = False
2480
    _valid_steps = STEPS
2481
    _step_list = STEP_LIST
2482

    
2483
    @classmethod
2484
    def match_model(cls, filedata, filename):
2485
        # This model is only ever matched via metadata
2486
        return False
2487

    
2488
    def process_mmap(self):
2489
        if self._rev != b"02" and self._rev != b"00":
2490
            # new revision found - log it and assume same map and proceed
2491
            LOG.debug("Unrecognized model variation (%s) Using default Map" %
2492
                      self._rev)
2493
        self._memobj = bitwise.parse(_MEM_FORMAT_9PX, self._mmap)
2494

    
2495
    def get_features(self):
2496
        """ Public get_features
2497
            Return the features of this radio once we have identified
2498
            it and gotten its bits
2499
            """
2500
        rf = chirp_common.RadioFeatures()
2501
        rf.has_settings = True
2502
        rf.has_ctone = True
2503
        rf.has_rx_dtcs = True
2504
        rf.has_cross = True
2505
        rf.has_tuning_step = False
2506
        rf.has_bank = False
2507
        rf.can_odd_split = True
2508
        rf.valid_skips = ["", "S"]
2509
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
2510
        rf.valid_cross_modes = [
2511
            "Tone->Tone",
2512
            "Tone->DTCS",
2513
            "DTCS->Tone",
2514
            "DTCS->",
2515
            "->Tone",
2516
            "->DTCS",
2517
            "DTCS->DTCS",
2518
        ]
2519
        rf.valid_modes = ["FM", "NFM", "AM"]
2520
        rf.valid_power_levels = self.POWER_LEVELS
2521
        rf.valid_name_length = 8
2522
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
2523
        rf.valid_bands = [(108000000, 135997500),  # Aircraft  AM
2524
                          (136000000, 180997500),  # supports 2m
2525
                          (219000000, 250997500),
2526
                          (350000000, 399997500),
2527
                          (400000000, 512997500),  # supports 70cm
2528
                          (700000000, 986997500)]
2529
        rf.valid_characters = chirp_common.CHARSET_ASCII
2530
        rf.valid_tuning_steps = STEPS
2531
        rf.memory_bounds = (1, 999)  # 999 memories
2532
        return rf
2533

    
2534
    def callid2str(self, cid):
2535
        """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
2536
        One digit (binary) per byte, terminated with '0xc'
2537
        """
2538

    
2539
        bin2ascii = " 1234567890"
2540
        cidstr = ""
2541
        for i in range(0, 6):
2542
            b = cid[i].get_value()
2543
            # 9PX factory reset CID use 0x00 for 0 digit - instead of 0x0a
2544
            # remap 0x00 to 0x0a
2545
            if b == 0x00:
2546
                b = 0x0a
2547
            if b == 0xc or b == 0xf0:  # the cid EOL
2548
                break
2549
            if b > 0xa:
2550
                raise InvalidValueError(
2551
                    "Caller ID code has illegal byte 0x%x" % b)
2552
            cidstr += bin2ascii[b]
2553
        return cidstr
2554

    
2555
    def _get_settings(self):
2556
        """Build the radio configuration settings menus
2557
        """
2558

    
2559
        core_grp = self._core_tab()
2560
        fm_grp = self._fm_tab()
2561
        area_a_grp = self._area_tab("a")
2562
        area_b_grp = self._area_tab("b")
2563
        key_grp = self._key_tab()
2564
        scan_grp = self._scan_grp()
2565
        callid_grp = self._callid_grp()
2566
        admin_grp = self._admin_tab()
2567
        rpt_grp = self._repeater_tab()
2568
        freq_limit_grp = self._fl_tab()
2569
        core_grp.append(key_grp)
2570
        core_grp.append(admin_grp)
2571
        core_grp.append(rpt_grp)
2572
        group = RadioSettings(core_grp,
2573
                              area_a_grp,
2574
                              area_b_grp,
2575
                              fm_grp,
2576
                              scan_grp,
2577
                              callid_grp,
2578
                              freq_limit_grp,)
2579
        return group
2580

    
2581
    def _area_tab(self, area):
2582
        """Build a VFO tab
2583
        """
2584
        def apply_scan_st(setting, scan_lo, scan_hi, obj):
2585
            f = freq2short(setting.value, scan_lo, scan_hi)
2586
            obj.scan_st = f
2587

    
2588
        def apply_scan_end(setting, scan_lo, scan_hi, obj):
2589
            f = freq2short(setting.value, scan_lo, scan_hi)
2590
            obj.scan_end = f
2591

    
2592
        if area == "a":
2593
            desc = "Receiver A Settings"
2594
            c = self._memobj.a_conf
2595
            scan_lo = 108000000
2596
            scan_hi = 985997500
2597
            scan_rng = self._memobj.settings.a
2598
            band_list = (150, 200, 300, 450, 700)
2599
        else:
2600
            desc = "Receiver B Settings"
2601
            c = self._memobj.b_conf
2602
            scan_lo = 136000000
2603
            scan_hi = 512997500
2604
            scan_rng = self._memobj.settings.b
2605
            band_list = (150, 450)
2606

    
2607
        prefix = "%s_conf" % area
2608
        af = RadioSettingGroup(prefix, desc)
2609
        af.append(
2610
            RadioSetting(prefix + ".w_mode",
2611
                         "Workmode",
2612
                         RadioSettingValueList(
2613
                             WORKMODE_LIST,
2614
                             WORKMODE_LIST[c.w_mode])))
2615
        af.append(RadioSetting(prefix + ".w_chan",
2616
                               "Channel",
2617
                               RadioSettingValueInteger(1, 999,
2618
                                                        c.w_chan)))
2619
        af.append(
2620
            RadioSetting(prefix + ".scan_grp",
2621
                         "Scan Group (Menu 49)",
2622
                         RadioSettingValueList(
2623
                             SCANGRP_LIST,
2624
                             SCANGRP_LIST[c.scan_grp])))
2625
        af.append(RadioSetting(prefix + ".bcl",
2626
                               "Busy Channel Lock-out (Menu 15)",
2627
                               RadioSettingValueBoolean(c.bcl)))
2628
        af.append(
2629
            RadioSetting(prefix + ".sql",
2630
                         "Squelch Level (Menu 8)",
2631
                         RadioSettingValueList(LIST_0_9,
2632
                                               LIST_0_9[c.sql])))
2633
        af.append(
2634
            RadioSetting(prefix + ".cset",
2635
                         "Call ID Group (Menu 52)",
2636
                         RadioSettingValueList(LIST_1_20,
2637
                                               LIST_1_20[c.cset])))
2638
        af.append(
2639
            RadioSetting(prefix + ".step",
2640
                         "Frequency Step (Menu 3)",
2641
                         RadioSettingValueList(
2642
                             self._step_list, self._step_list[c.step])))
2643
        af.append(
2644
            RadioSetting(prefix + ".scan_mode",
2645
                         "Scan Mode (Menu 20)",
2646
                         RadioSettingValueList(
2647
                             SCANMODE_LIST,
2648
                             SCANMODE_LIST[c.scan_mode])))
2649
        af.append(
2650
            RadioSetting(prefix + ".scan_range",
2651
                         "Scan Range (Menu 50)",
2652
                         RadioSettingValueList(
2653
                             SCANRANGE_LIST,
2654
                             SCANRANGE_LIST[c.scan_range])))
2655
        st = RadioSettingValueString(0, 15,
2656
                                     short2freq(scan_rng.scan_st))
2657
        rs = RadioSetting("settings.%s.scan_st" % area,
2658
                          "Frequency Scan Start", st)
2659
        rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng)
2660
        af.append(rs)
2661

    
2662
        end = RadioSettingValueString(0, 15,
2663
                                      short2freq(scan_rng.scan_end))
2664
        rs = RadioSetting("settings.%s.scan_end" % area,
2665
                          "Frequency Scan End", end)
2666
        rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi,
2667
                              scan_rng)
2668
        af.append(rs)
2669
        # Each area has its own set of bands
2670
        for band in (band_list):
2671
            af.append(self._band_tab(area, band))
2672
        return af
2673

    
2674
    def _band_tab(self, area, band):
2675
        """ Build a band tab inside a VFO/Area
2676
        """
2677
        def apply_freq(setting, lo, hi, obj):
2678
            f = freq2int(setting.value, lo, hi)
2679
            obj.freq = f/10
2680

    
2681
        def apply_offset(setting, obj):
2682
            f = freq2int(setting.value, 0, 5000000)
2683
            obj.offset = f/10
2684

    
2685
        def apply_enc(setting, obj):
2686
            t = tone2short(setting.value)
2687
            obj.encqt = t
2688

    
2689
        def apply_dec(setting, obj):
2690
            t = tone2short(setting.value)
2691
            obj.decqt = t
2692

    
2693
        if area == "a":
2694
            if band == 150:
2695
                c = self._memobj.vfo_a.band_150
2696
                lo = 108000000
2697
                hi = 180997500
2698
            elif band == 200:
2699
                c = self._memobj.vfo_a.band_200
2700
                lo = 219000000
2701
                hi = 250997500
2702
            elif band == 300:
2703
                c = self._memobj.vfo_a.band_300
2704
                lo = 350000000
2705
                hi = 399997500
2706
            elif band == 450:
2707
                c = self._memobj.vfo_a.band_450
2708
                lo = 400000000
2709
                hi = 512997500
2710
            else:   # 700
2711
                c = self._memobj.vfo_a.band_700
2712
                lo = 700000000
2713
                hi = 986997500
2714
        else:  # area 'b'
2715
            if band == 150:
2716
                c = self._memobj.vfo_b.band_150
2717
                lo = 136000000
2718
                hi = 180997500
2719
            else:  # 450
2720
                c = self._memobj.vfo_b.band_450
2721
                lo = 400000000
2722
                hi = 512997500
2723

    
2724
        prefix = "vfo_%s.band_%d" % (area, band)
2725
        bf = RadioSettingGroup(prefix, "%dMHz Band" % band)
2726
        freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10))
2727
        rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq)
2728
        rs.set_apply_callback(apply_freq, lo, hi, c)
2729
        bf.append(rs)
2730

    
2731
        off = RadioSettingValueString(0, 15, int2freq(c.offset * 10))
2732
        rs = RadioSetting(prefix + ".offset", "Tx Offset (Menu 28)", off)
2733
        rs.set_apply_callback(apply_offset, c)
2734
        bf.append(rs)
2735

    
2736
        rs = RadioSetting(prefix + ".encqt",
2737
                          "Encode QT (Menu 17,19)",
2738
                          RadioSettingValueList(TONE_LIST,
2739
                                                short2tone(c.encqt)))
2740
        rs.set_apply_callback(apply_enc, c)
2741
        bf.append(rs)
2742

    
2743
        rs = RadioSetting(prefix + ".decqt",
2744
                          "Decode QT (Menu 16,18)",
2745
                          RadioSettingValueList(TONE_LIST,
2746
                                                short2tone(c.decqt)))
2747
        rs.set_apply_callback(apply_dec, c)
2748
        bf.append(rs)
2749

    
2750
        bf.append(RadioSetting(prefix + ".qt",
2751
                               "Mute Mode (Menu 21)",
2752
                               RadioSettingValueList(SPMUTE_LIST,
2753
                                                     SPMUTE_LIST[c.qt])))
2754
        bf.append(RadioSetting(prefix + ".scan",
2755
                               "Scan this (Menu 48)",
2756
                               RadioSettingValueBoolean(c.scan)))
2757
        bf.append(RadioSetting(prefix + ".pwr",
2758
                               "Power (Menu 5)",
2759
                               RadioSettingValueList(
2760
                                   POWER_LIST, POWER_LIST[c.pwr])))
2761
        bf.append(RadioSetting(prefix + ".mod",
2762
                               "AM Modulation (Menu 54)",
2763
                               RadioSettingValueBoolean(c.mod)))
2764
        bf.append(RadioSetting(prefix + ".fm_dev",
2765
                               "FM Deviation (Menu 4)",
2766
                               RadioSettingValueList(
2767
                                   BANDWIDTH_LIST,
2768
                                   BANDWIDTH_LIST[c.fm_dev])))
2769
        bf.append(
2770
            RadioSetting(prefix + ".shift",
2771
                         "Frequency Shift (Menu 6)",
2772
                         RadioSettingValueList(OFFSET_LIST,
2773
                                               OFFSET_LIST[c.shift])))
2774
        return bf
2775

    
2776
    def _fl_tab(self):
2777

    
2778
        freq_limit_grp = RadioSettingGroup("limits",
2779
                                           "Freq Limits")
2780
        limgrp = freq_limit_grp
2781

    
2782
        l = self._memobj.limits
2783

    
2784
        if self.MODEL == "KG-UV9PX":
2785
            val = RadioSettingValueInteger(136, 180,
2786
                                           (l.lim_150M_Txlower_limit) / 10.0)
2787
            rs = RadioSetting("limits.lim_150M_Txlower_limit",
2788
                              "150M Tx Lower Limit (MHz)",
2789
                              RadioSettingValueInteger(136, 180, val))
2790
            limgrp.append(rs)
2791

    
2792
            val = RadioSettingValueInteger(136, 180,
2793
                                           (l.lim_150M_Txupper_limit) / 10.0)
2794
            rs = RadioSetting("limits.lim_150M_Txupper_limit",
2795
                              "150M Tx Upper Limit (MHz + 0.9975)",
2796
                              RadioSettingValueInteger(136, 180, val))
2797
            limgrp.append(rs)
2798

    
2799
            val = RadioSettingValueInteger(400, 512,
2800
                                           (l.lim_450M_Txlower_limit) / 10.0)
2801
            rs = RadioSetting("limits.lim_450M_Txlower_limit",
2802
                              "450M Tx Lower Limit (MHz)",
2803
                              RadioSettingValueInteger(400, 512, val))
2804
            limgrp.append(rs)
2805

    
2806
            val = RadioSettingValueInteger(400, 512,
2807
                                           (l.lim_450M_Txupper_limit) / 10.0)
2808
            rs = RadioSetting("limits.lim_450M_Txupper_limit",
2809
                              "450M Tx Upper Limit (MHz + 0.9975)",
2810
                              RadioSettingValueInteger(400, 512, val))
2811
            limgrp.append(rs)
2812

    
2813
        val = RadioSettingValueInteger(108, 180,
2814
                                       (l.lim_150M_area_a_rxlower_limit) /
2815
                                       10.0)
2816
        rs = RadioSetting("limits.lim_150M_area_a_rxlower_limit",
2817
                          "Rcvr A 150M Rx Lower Limit (MHz)",
2818
                          RadioSettingValueInteger(108, 180,
2819
                                                   val))
2820
        limgrp.append(rs)
2821

    
2822
        val = RadioSettingValueInteger(108, 180,
2823
                                       (l.lim_150M_area_a_rxupper_limit) /
2824
                                       10.0)
2825
        rs = RadioSetting("limits.lim_150M_area_a_rxupper_limit",
2826
                          "Rcvr A 150M Rx Upper Limit (MHz + 0.9975)",
2827
                          RadioSettingValueInteger(108, 180,
2828
                                                   val))
2829
        limgrp.append(rs)
2830

    
2831
        val = RadioSettingValueInteger(136, 180,
2832
                                       (l.lim_150M_area_b_rxlower_limit) /
2833
                                       10.0)
2834
        rs = RadioSetting("limits.lim_150M_area_b_rxlower_limit",
2835
                          "Rcvr B 150M Rx Lower Limit (MHz)",
2836
                          RadioSettingValueInteger(136, 180,
2837
                                                   val))
2838
        limgrp.append(rs)
2839

    
2840
        val = RadioSettingValueInteger(136, 180,
2841
                                       (l.lim_150M_area_b_rxupper_limit) /
2842
                                       10.0)
2843
        rs = RadioSetting("limits.lim_150M_area_b_rxupper_limit",
2844
                          "Rcvr B 150M Rx Upper Limit (MHz + 0.9975)",
2845
                          RadioSettingValueInteger(136, 180,
2846
                                                   val))
2847
        limgrp.append(rs)
2848

    
2849
        val = RadioSettingValueInteger(400, 512,
2850
                                       (l.lim_450M_rxlower_limit) / 10.0)
2851
        rs = RadioSetting("limits.lim_450M_rxlower_limit",
2852
                          "450M Rx Lower Limit (MHz)",
2853
                          RadioSettingValueInteger(400, 512,
2854
                                                   val))
2855
        limgrp.append(rs)
2856

    
2857
        val = RadioSettingValueInteger(400, 512,
2858
                                       (l.lim_450M_rxupper_limit) / 10.0)
2859
        rs = RadioSetting("limits.lim_450M_rxupper_limit",
2860
                          "450M Rx Upper Limit (MHz + 0.9975)",
2861
                          RadioSettingValueInteger(400, 512,
2862
                                                   val))
2863
        limgrp.append(rs)
2864

    
2865
        val = RadioSettingValueInteger(350, 399,
2866
                                       (l.lim_300M_rxlower_limit) / 10.0)
2867
        rs = RadioSetting("limits.lim_300M_rxlower_limit",
2868
                          "300M Rx Lower Limit (MHz)",
2869
                          RadioSettingValueInteger(350, 399,
2870
                                                   val))
2871
        limgrp.append(rs)
2872

    
2873
        val = RadioSettingValueInteger(350, 399,
2874
                                       (l.lim_300M_rxupper_limit) / 10.0)
2875
        rs = RadioSetting("limits.lim_300M_rxupper_limit",
2876
                          "300M Rx Upper Limit (MHz + 0.9975)",
2877
                          RadioSettingValueInteger(350, 399,
2878
                                                   val))
2879
        limgrp.append(rs)
2880
        val = RadioSettingValueInteger(700, 986,
2881
                                       (l.lim_800M_rxlower_limit) / 10.0)
2882
        rs = RadioSetting("limits.lim_800M_rxlower_limit",
2883
                          "800M Rx Lower Limit (MHz)",
2884
                          RadioSettingValueInteger(700, 986,
2885
                                                   val))
2886
        limgrp.append(rs)
2887

    
2888
        val = RadioSettingValueInteger(700, 986,
2889
                                       (l.lim_800M_rxupper_limit) / 10.0)
2890
        rs = RadioSetting("limits.lim_800M_rxupper_limit",
2891
                          "800M Rx Upper Limit (MHz + 0.9975)",
2892
                          RadioSettingValueInteger(700, 986,
2893
                                                   val))
2894
        limgrp.append(rs)
2895

    
2896
        val = RadioSettingValueInteger(219, 250,
2897
                                       (l.lim_210M_rxlower_limit) / 10.0)
2898
        rs = RadioSetting("limits.lim_210M_rxlower_limit",
2899
                          "210M Rx Lower Limit (MHz)",
2900
                          RadioSettingValueInteger(219, 250,
2901
                                                   val))
2902
        limgrp.append(rs)
2903

    
2904
        val = RadioSettingValueInteger(219, 250,
2905
                                       (l.lim_210M_rxupper_limit) / 10.0)
2906
        rs = RadioSetting("limits.lim_210M_rxupper_limit",
2907
                          "210M Rx Upper Limit (MHz + 0.9975)",
2908
                          RadioSettingValueInteger(219, 250,
2909
                                                   val))
2910
        limgrp.append(rs)
2911

    
2912
        return limgrp
2913

    
2914

    
2915
@directory.register
2916
class KGUV9GXRadio(KGUV9PXRadio):
2917

    
2918
    """Wouxun KG-UV9GX"""
2919
    VENDOR = "Wouxun"
2920
    MODEL = "KG-UV9GX"
2921
    _model = b"KG-UV9D"
2922
    _rev = b"02"  # default rev for the radio I know about...
2923
    NEEDS_COMPAT_SERIAL = False
2924
    _valid_steps = STEPS
2925
    _step_list = STEP_LIST
2926

    
2927
    @classmethod
2928
    def match_model(cls, filedata, filename):
2929
        # This model is only ever matched via metadata
2930
        return False
2931

    
2932

    
2933
@directory.register
2934
class KGUV9KRadio(KGUV9DPlusRadio):
2935

    
2936
    """Wouxun KG-UV9K"""
2937
    VENDOR = "Wouxun"
2938
    MODEL = "KG-UV9K"
2939
    _model = b"KG-UV9D"
2940
    _file_ident = b"kg-uv9k"
2941
    _rev = b"02"  # default rev for the radio I know about...
2942
    NEEDS_COMPAT_SERIAL = False
2943
    _step_list = STEP_LIST_9K
2944
    _valid_steps = STEPS_9K
2945

    
2946
    @classmethod
2947
    def match_model(cls, filedata, filename):
2948
        # This model is only ever matched via metadata
2949
        return False
2950

    
2951

    
2952
@directory.register
2953
class KGUV9GProRadio(KGUV9DPlusRadio):
2954

    
2955
    """Wouxun KG-UV9G Pro"""
2956
    VENDOR = "Wouxun"
2957
    MODEL = "KG-UV9G Pro"
2958
    _model = b"KG-UV9D"
2959
    _file_ident = b"kg-uv9gpro"
2960
    _rev = b"02"  # default rev for the radio I know about...
2961
    NEEDS_COMPAT_SERIAL = False
2962
    _step_list = STEP_LIST
2963
    _valid_steps = STEPS
2964

    
2965
    @classmethod
2966
    def match_model(cls, filedata, filename):
2967
        # This model is only ever matched via metadata
2968
        return False
(2-2/2)