Project

General

Profile

Bug #11238 » uvk5_k5_4.00.py

Jim Unroe, 03/12/2024 01:37 PM

 
1
# Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski <sq5bpf@lipkowski.org>
2
#
3
# based on template.py Copyright 2012 Dan Smith <dsmith@danplanet.com>
4
#
5
#
6
# This is a preliminary version of a driver for the UV-K5
7
# It is based on my reverse engineering effort described here:
8
# https://github.com/sq5bpf/uvk5-reverse-engineering
9
#
10
# Warning: this driver is experimental, it may brick your radio,
11
# eat your lunch and mess up your configuration.
12
#
13
#
14
# This program is free software: you can redistribute it and/or modify
15
# it under the terms of the GNU General Public License as published by
16
# the Free Software Foundation, either version 2 of the License, or
17
# (at your option) any later version.
18
#
19
# This program is distributed in the hope that it will be useful,
20
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
# GNU General Public License for more details.
23
#
24
# You should have received a copy of the GNU General Public License
25
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
26

    
27

    
28
import struct
29
import logging
30

    
31
from chirp import chirp_common, directory, bitwise, memmap, errors, util
32
from chirp.settings import RadioSetting, RadioSettingGroup, \
33
    RadioSettingValueBoolean, RadioSettingValueList, \
34
    RadioSettingValueInteger, RadioSettingValueString, \
35
    RadioSettings
36

    
37
LOG = logging.getLogger(__name__)
38

    
39
# Show the obfuscated version of commands. Not needed normally, but
40
# might be useful for someone who is debugging a similar radio
41
DEBUG_SHOW_OBFUSCATED_COMMANDS = False
42

    
43
# Show the memory being written/received. Not needed normally, because
44
# this is the same information as in the packet hexdumps, but
45
# might be useful for someone debugging some obscure memory issue
46
DEBUG_SHOW_MEMORY_ACTIONS = False
47

    
48
MEM_FORMAT = """
49
#seekto 0x0000;
50
struct {
51
  ul32 freq;
52
  ul32 offset;
53
  u8 rxcode;
54
  u8 txcode;
55

    
56
  u8 txcodeflag:4,
57
     rxcodeflag:4;
58

    
59
  //u8 flags1;
60
  u8 flags1_unknown7:1,
61
  flags1_unknown6:1,
62
  flags1_unknown5:1,
63
  enable_am:1,
64
  flags1_unknown3:1,
65
  is_in_scanlist:1,
66
  shift:2;
67

    
68
  //u8 flags2;
69
  u8 flags2_unknown7:1,
70
  flags2_unknown6:1,
71
  flags2_unknown5:1,
72
  bclo:1,
73
  txpower:2,
74
  bandwidth:1,
75
  freq_reverse:1;
76

    
77
  //u8 dtmf_flags;
78
  u8 dtmf_flags_unknown7:1,
79
  dtmf_flags_unknown6:1,
80
  dtmf_flags_unknown5:1,
81
  dtmf_flags_unknown4:1,
82
  dtmf_flags_unknown3:1,
83
  dtmf_pttid:2,
84
  dtmf_decode:1;
85

    
86

    
87
  u8 step;
88
  u8 scrambler;
89
} channel[214];
90

    
91
#seekto 0xd60;
92
struct {
93
u8 is_scanlist1:1,
94
is_scanlist2:1,
95
compander:2,
96
is_free:1,
97
band:3;
98
} channel_attributes[200];
99

    
100
#seekto 0xe40;
101
ul16 fmfreq[20];
102

    
103
#seekto 0xe70;
104
u8 call_channel;
105
u8 squelch;
106
u8 max_talk_time;
107
u8 noaa_autoscan;
108
u8 key_lock;
109
u8 vox_switch;
110
u8 vox_level;
111
u8 mic_gain;
112
u8 unknown3;
113
u8 channel_display_mode;
114
u8 crossband;
115
u8 battery_save;
116
u8 dual_watch;
117
u8 backlight_auto_mode;
118
u8 tail_note_elimination;
119
u8 vfo_open;
120

    
121
#seekto 0xe90;
122
u8 beep_control;
123
u8 key1_shortpress_action;
124
u8 key1_longpress_action;
125
u8 key2_shortpress_action;
126
u8 key2_longpress_action;
127
u8 scan_resume_mode;
128
u8 auto_keypad_lock;
129
u8 power_on_dispmode;
130
u8 password[4];
131

    
132
#seekto 0xea0;
133
u8 keypad_tone;
134
u8 language;
135

    
136
#seekto 0xea8;
137
u8 alarm_mode;
138
u8 reminding_of_end_talk;
139
u8 repeater_tail_elimination;
140

    
141
#seekto 0xeb0;
142
char logo_line1[16];
143
char logo_line2[16];
144

    
145
#seekto 0xed0;
146
struct {
147
u8 side_tone;
148
char separate_code;
149
char group_call_code;
150
u8 decode_response;
151
u8 auto_reset_time;
152
u8 preload_time;
153
u8 first_code_persist_time;
154
u8 hash_persist_time;
155
u8 code_persist_time;
156
u8 code_interval_time;
157
u8 permit_remote_kill;
158
} dtmf_settings;
159

    
160
#seekto 0xee0;
161
struct {
162
char dtmf_local_code[3];
163
char unused1[5];
164
char kill_code[5];
165
char unused2[3];
166
char revive_code[5];
167
char unused3[3];
168
char dtmf_up_code[16];
169
char dtmf_down_code[16];
170
} dtmf_settings_numbers;
171

    
172
#seekto 0xf18;
173
u8 scanlist_default;
174
u8 scanlist1_priority_scan;
175
u8 scanlist1_priority_ch1;
176
u8 scanlist1_priority_ch2;
177
u8 scanlist2_priority_scan;
178
u8 scanlist2_priority_ch1;
179
u8 scanlist2_priority_ch2;
180
u8 scanlist_unknown_0xff;
181

    
182

    
183
#seekto 0xf40;
184
struct {
185
u8 flock;
186
u8 tx350;
187
u8 killed;
188
u8 tx200;
189
u8 tx500;
190
u8 en350;
191
u8 enscramble;
192
} lock;
193

    
194
#seekto 0xf50;
195
struct {
196
char name[16];
197
} channelname[200];
198

    
199
#seekto 0x1c00;
200
struct {
201
char name[8];
202
char number[3];
203
char unused_00[5];
204
} dtmfcontact[16];
205

    
206
#seekto 0x1ed0;
207
struct {
208
struct {
209
    u8 start;
210
    u8 mid;
211
    u8 end;
212
} low;
213
struct {
214
    u8 start;
215
    u8 mid;
216
    u8 end;
217
} medium;
218
struct {
219
    u8 start;
220
    u8 mid;
221
    u8 end;
222
} high;
223
u8 unused_00[7];
224
} perbandpowersettings[7];
225

    
226
#seekto 0x1f40;
227
ul16 battery_level[6];
228
"""
229
# bits that we will save from the channel structure (mostly unknown)
230
SAVE_MASK_0A = 0b11001100
231
SAVE_MASK_0B = 0b11101100
232
SAVE_MASK_0C = 0b11100000
233
SAVE_MASK_0D = 0b11111000
234
SAVE_MASK_0E = 0b11110001
235
SAVE_MASK_0F = 0b11110000
236

    
237
# flags1
238
FLAGS1_OFFSET_NONE = 0b00
239
FLAGS1_OFFSET_MINUS = 0b10
240
FLAGS1_OFFSET_PLUS = 0b01
241

    
242
POWER_HIGH = 0b10
243
POWER_MEDIUM = 0b01
244
POWER_LOW = 0b00
245

    
246
# power
247
UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low",  watts=1.50),
248
                     chirp_common.PowerLevel("Med",  watts=3.00),
249
                     chirp_common.PowerLevel("High", watts=5.00),
250
                     ]
251

    
252
# scrambler
253
SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
254

    
255
# channel display mode
256
CHANNELDISP_LIST = ["Frequency", "Channel No", "Channel Name"]
257
# battery save
258
BATSAVE_LIST = ["OFF", "1:1", "1:2", "1:3", "1:4"]
259

    
260
# Backlight auto mode
261
BACKLIGHT_LIST = ["Off", "1s", "2s", "3s", "4s", "5s"]
262

    
263
# Crossband receiving/transmitting
264
CROSSBAND_LIST = ["Off", "Band A", "Band B"]
265
DUALWATCH_LIST = CROSSBAND_LIST
266

    
267
# ctcss/dcs codes
268
TMODES = ["", "Tone", "DTCS", "DTCS"]
269
TONE_NONE = 0
270
TONE_CTCSS = 1
271
TONE_DCS = 2
272
TONE_RDCS = 3
273

    
274

    
275
CTCSS_TONES = [
276
    67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4,
277
    88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9,
278
    114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2,
279
    151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
280
    177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
281
    203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8,
282
    250.3, 254.1
283
]
284

    
285
# lifted from ft4.py
286
DTCS_CODES = [
287
    23,  25,  26,  31,  32,  36,  43,  47,  51,  53,  54,
288
    65,  71,  72,  73,  74,  114, 115, 116, 122, 125, 131,
289
    132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
290
    205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
291
    255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
292
    331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
293
    413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
294
    465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
295
    612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
296
    731, 732, 734, 743, 754
297
]
298

    
299
FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"]
300

    
301
SCANRESUME_LIST = ["TO: Resume after 5 seconds",
302
                   "CO: Resume after signal disappears",
303
                   "SE: Stop scanning after receiving a signal"]
304

    
305
WELCOME_LIST = ["Full Screen", "Welcome Info", "Voltage"]
306
KEYPADTONE_LIST = ["Off", "Chinese", "English"]
307
LANGUAGE_LIST = ["Chinese", "English"]
308
ALARMMODE_LIST = ["SITE", "TONE"]
309
REMENDOFTALK_LIST = ["Off", "ROGER", "MDC"]
310
RTE_LIST = ["Off", "100ms", "200ms", "300ms", "400ms",
311
            "500ms", "600ms", "700ms", "800ms", "900ms"]
312

    
313
MEM_SIZE = 0x2000  # size of all memory
314
PROG_SIZE = 0x1d00  # size of the memory that we will write
315
MEM_BLOCK = 0x80  # largest block of memory that we can reliably write
316

    
317
# fm radio supported frequencies
318
FMMIN = 76.0
319
FMMAX = 108.0
320

    
321
# bands supported by the UV-K5
322
BANDS = {
323
        0: [50.0, 76.0],
324
        1: [108.0, 135.9999],
325
        2: [136.0, 199.9990],
326
        3: [200.0, 299.9999],
327
        4: [350.0, 399.9999],
328
        5: [400.0, 469.9999],
329
        6: [470.0, 600.0]
330
        }
331

    
332
# for radios with modified firmware:
333
BANDS_NOLIMITS = {
334
        0: [18.0, 76.0],
335
        1: [108.0, 135.9999],
336
        2: [136.0, 199.9990],
337
        3: [200.0, 299.9999],
338
        4: [350.0, 399.9999],
339
        5: [400.0, 469.9999],
340
        6: [470.0, 1300.0]
341
        }
342

    
343
SPECIALS = {
344
        "F1(50M-76M)A": 200,
345
        "F1(50M-76M)B": 201,
346
        "F2(108M-136M)A": 202,
347
        "F2(108M-136M)B": 203,
348
        "F3(136M-174M)A": 204,
349
        "F3(136M-174M)B": 205,
350
        "F4(174M-350M)A": 206,
351
        "F4(174M-350M)B": 207,
352
        "F5(350M-400M)A": 208,
353
        "F5(350M-400M)B": 209,
354
        "F6(400M-470M)A": 210,
355
        "F6(400M-470M)B": 211,
356
        "F7(470M-600M)A": 212,
357
        "F7(470M-600M)B": 213
358
        }
359

    
360
VFO_CHANNEL_NAMES = ["F1(50M-76M)A", "F1(50M-76M)B",
361
                     "F2(108M-136M)A", "F2(108M-136M)B",
362
                     "F3(136M-174M)A", "F3(136M-174M)B",
363
                     "F4(174M-350M)A", "F4(174M-350M)B",
364
                     "F5(350M-400M)A", "F5(350M-400M)B",
365
                     "F6(400M-470M)A", "F6(400M-470M)B",
366
                     "F7(470M-600M)A", "F7(470M-600M)B"]
367

    
368
SCANLIST_LIST = ["None", "1", "2", "1+2"]
369

    
370
DTMF_CHARS = "0123456789ABCD*# "
371
DTMF_CHARS_ID = "0123456789ABCDabcd"
372
DTMF_CHARS_KILL = "0123456789ABCDabcd"
373
DTMF_CHARS_UPDOWN = "0123456789ABCDabcd#* "
374
DTMF_CODE_CHARS = "ABCD*# "
375
DTMF_DECODE_RESPONSE_LIST = ["None", "Ring", "Reply", "Both"]
376

    
377
KEYACTIONS_LIST = ["None", "Flashlight on/off", "Power select",
378
                   "Monitor", "Scan on/off", "VOX on/off",
379
                   "Alarm on/off", "FM radio on/off", "Transmit 1750 Hz"]
380

    
381

    
382
def xorarr(data: bytes):
383
    """the communication is obfuscated using this fine mechanism"""
384
    tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128]
385
    ret = b""
386
    idx = 0
387
    for byte in data:
388
        ret += bytes([byte ^ tbl[idx]])
389
        idx = (idx+1) % len(tbl)
390
    return ret
391

    
392

    
393
def calculate_crc16_xmodem(data: bytes):
394
    """
395
    if this crc was used for communication to AND from the radio, then it
396
    would be a measure to increase reliability.
397
    but it's only used towards the radio, so it's for further obfuscation
398
    """
399
    poly = 0x1021
400
    crc = 0x0
401
    for byte in data:
402
        crc = crc ^ (byte << 8)
403
        for _ in range(8):
404
            crc = crc << 1
405
            if crc & 0x10000:
406
                crc = (crc ^ poly) & 0xFFFF
407
    return crc & 0xFFFF
408

    
409

    
410
def _send_command(serport, data: bytes):
411
    """Send a command to UV-K5 radio"""
412
    LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s",
413
              len(data), util.hexprint(data))
414

    
415
    crc = calculate_crc16_xmodem(data)
416
    data2 = data + struct.pack("<H", crc)
417

    
418
    command = struct.pack(">HBB", 0xabcd, len(data), 0) + \
419
        xorarr(data2) + \
420
        struct.pack(">H", 0xdcba)
421
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
422
        LOG.debug("Sending command (obfuscated):\n%s", util.hexprint(command))
423
    try:
424
        result = serport.write(command)
425
    except Exception as e:
426
        raise errors.RadioError("Error writing data to radio") from e
427
    return result
428

    
429

    
430
def _receive_reply(serport):
431
    header = serport.read(4)
432
    if len(header) != 4:
433
        LOG.warning("Header short read: [%s] len=%i",
434
                    util.hexprint(header), len(header))
435
        raise errors.RadioError("Header short read")
436
    if header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00:
437
        LOG.warning("Bad response header: %s len=%i",
438
                    util.hexprint(header), len(header))
439
        raise errors.RadioError("Bad response header")
440

    
441
    cmd = serport.read(int(header[2]))
442
    if len(cmd) != int(header[2]):
443
        LOG.warning("Body short read: [%s] len=%i",
444
                    util.hexprint(cmd), len(cmd))
445
        raise errors.RadioError("Command body short read")
446

    
447
    footer = serport.read(4)
448

    
449
    if len(footer) != 4:
450
        LOG.warning("Footer short read: [%s] len=%i",
451
                    util.hexprint(footer), len(footer))
452
        raise errors.RadioError("Footer short read")
453

    
454
    if footer[2] != 0xDC or footer[3] != 0xBA:
455
        LOG.debug("Reply before bad response footer (obfuscated)"
456
                  "len=0x%4.4x:\n%s", len(cmd), util.hexprint(cmd))
457
        LOG.warning("Bad response footer: %s len=%i",
458
                    util.hexprint(footer), len(footer))
459
        raise errors.RadioError("Bad response footer")
460

    
461
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
462
        LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s",
463
                  len(cmd), util.hexprint(cmd))
464

    
465
    cmd2 = xorarr(cmd)
466

    
467
    LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s",
468
              len(cmd2), util.hexprint(cmd2))
469

    
470
    return cmd2
471

    
472

    
473
def _getstring(data: bytes, begin, maxlen):
474
    tmplen = min(maxlen+1, len(data))
475
    ss = [data[i] for i in range(begin, tmplen)]
476
    key = 0
477
    for key, val in enumerate(ss):
478
        if val < ord(' ') or val > ord('~'):
479
            return ''.join(chr(x) for x in ss[0:key])
480
    return ''
481

    
482

    
483
def _sayhello(serport):
484
    hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64"
485

    
486
    tries = 5
487
    while True:
488
        LOG.debug("Sending hello packet")
489
        _send_command(serport, hellopacket)
490
        rep = _receive_reply(serport)
491
        if rep:
492
            break
493
        tries -= 1
494
        if tries == 0:
495
            LOG.warning("Failed to initialise radio")
496
            raise errors.RadioError("Failed to initialize radio")
497
    if rep.startswith(b'\x18\x05'):
498
        raise errors.RadioError("Radio is in programming mode, "
499
                                "restart radio into normal mode")
500
    firmware = _getstring(rep, 4, 24)
501

    
502
    LOG.info("Found firmware: %s", firmware)
503
    return firmware
504

    
505

    
506
def _readmem(serport, offset, length):
507
    LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x", offset, length)
508

    
509
    readmem = b"\x1b\x05\x08\x00" + \
510
        struct.pack("<HBB", offset, length, 0) + \
511
        b"\x6a\x39\x57\x64"
512
    _send_command(serport, readmem)
513
    rep = _receive_reply(serport)
514
    if DEBUG_SHOW_MEMORY_ACTIONS:
515
        LOG.debug("readmem Received data len=0x%4.4x:\n%s",
516
                  len(rep), util.hexprint(rep))
517
    return rep[8:]
518

    
519

    
520
def _writemem(serport, data, offset):
521
    LOG.debug("Sending writemem offset=0x%4.4x len=0x%4.4x",
522
              offset, len(data))
523

    
524
    if DEBUG_SHOW_MEMORY_ACTIONS:
525
        LOG.debug("writemem sent data offset=0x%4.4x len=0x%4.4x:\n%s",
526
                  offset, len(data), util.hexprint(data))
527

    
528
    dlen = len(data)
529
    writemem = b"\x1d\x05" + \
530
        struct.pack("<BBHBB", dlen+8, 0, offset, dlen, 1) + \
531
        b"\x6a\x39\x57\x64"+data
532

    
533
    _send_command(serport, writemem)
534
    rep = _receive_reply(serport)
535

    
536
    LOG.debug("writemem Received data: %s len=%i",
537
              util.hexprint(rep), len(rep))
538

    
539
    if (rep[0] == 0x1e and
540
       rep[4] == (offset & 0xff) and
541
       rep[5] == (offset >> 8) & 0xff):
542
        return True
543

    
544
    LOG.warning("Bad data from writemem")
545
    raise errors.RadioError("Bad response to writemem")
546

    
547

    
548
def _resetradio(serport):
549
    resetpacket = b"\xdd\x05\x00\x00"
550
    _send_command(serport, resetpacket)
551

    
552

    
553
def do_download(radio):
554
    """download eeprom from radio"""
555
    serport = radio.pipe
556
    serport.timeout = 0.5
557
    status = chirp_common.Status()
558
    status.cur = 0
559
    status.max = MEM_SIZE
560
    status.msg = "Downloading from radio"
561
    radio.status_fn(status)
562

    
563
    eeprom = b""
564
    f = _sayhello(serport)
565
    if not f:
566
        raise errors.RadioError('Unable to determine firmware version')
567

    
568
    if not radio.k5_approve_firmware(f):
569
        raise errors.RadioError(
570
            'Firmware version is not supported by this driver')
571

    
572
    radio.metadata = {'uvk5_firmware': f}
573

    
574
    addr = 0
575
    while addr < MEM_SIZE:
576
        data = _readmem(serport, addr, MEM_BLOCK)
577
        status.cur = addr
578
        radio.status_fn(status)
579

    
580
        if data and len(data) == MEM_BLOCK:
581
            eeprom += data
582
            addr += MEM_BLOCK
583
        else:
584
            raise errors.RadioError("Memory download incomplete")
585

    
586
    return memmap.MemoryMapBytes(eeprom)
587

    
588

    
589
def do_upload(radio):
590
    """upload configuration to radio eeprom"""
591
    serport = radio.pipe
592
    serport.timeout = 0.5
593
    status = chirp_common.Status()
594
    status.cur = 0
595
    status.msg = "Uploading to radio"
596

    
597
    if radio._upload_calibration:
598
        status.max = MEM_SIZE - radio._cal_start
599
        start_addr = radio._cal_start
600
        stop_addr = MEM_SIZE
601
    else:
602
        status.max = PROG_SIZE
603
        start_addr = 0
604
        stop_addr = PROG_SIZE
605

    
606
    radio.status_fn(status)
607

    
608
    f = _sayhello(serport)
609
    if not f:
610
        raise errors.RadioError('Unable to determine firmware version')
611

    
612
    if not radio.k5_approve_firmware(f):
613
        raise errors.RadioError(
614
            'Firmware version is not supported by this driver')
615
    LOG.info('Uploading image from firmware %r to radio with %r',
616
             radio.metadata.get('uvk5_firmware', 'unknown'), f)
617
    addr = start_addr
618
    while addr < stop_addr:
619
        dat = radio.get_mmap()[addr:addr+MEM_BLOCK]
620
        _writemem(serport, dat, addr)
621
        status.cur = addr - start_addr
622
        radio.status_fn(status)
623
        if dat:
624
            addr += MEM_BLOCK
625
        else:
626
            raise errors.RadioError("Memory upload incomplete")
627
    status.msg = "Uploaded OK"
628

    
629
    _resetradio(serport)
630

    
631
    return True
632

    
633

    
634
def _find_band(nolimits, hz):
635
    mhz = hz/1000000.0
636
    if nolimits:
637
        B = BANDS_NOLIMITS
638
    else:
639
        B = BANDS
640

    
641
    # currently the hacked firmware sets band=1 below 50 MHz
642
    if nolimits and mhz < 50.0:
643
        return 1
644

    
645
    for a in B:
646
        if mhz >= B[a][0] and mhz <= B[a][1]:
647
            return a
648
    return False
649

    
650

    
651
class UVK5RadioBase(chirp_common.CloneModeRadio):
652
    """Quansheng UV-K5"""
653
    VENDOR = "Quansheng"
654
    MODEL = "UV-K5"
655
    BAUD_RATE = 38400
656
    NEEDS_COMPAT_SERIAL = False
657
    _cal_start = 0
658
    _expanded_limits = False
659
    _upload_calibration = False
660
    _pttid_list = ["off", "BOT", "EOT", "BOTH"]
661
    _steps = [1.0, 2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 8.33]
662

    
663
    @classmethod
664
    def k5_approve_firmware(cls, firmware):
665
        # All subclasses must implement this
666
        raise NotImplementedError()
667

    
668
    def get_prompts(x=None):
669
        rp = chirp_common.RadioPrompts()
670
        rp.experimental = _(
671
            'This is an experimental driver for the Quansheng UV-K5. '
672
            'It may harm your radio, or worse. Use at your own risk.\n\n'
673
            'Before attempting to do any changes please download '
674
            'the memory image from the radio with chirp '
675
            'and keep it. This can be later used to recover the '
676
            'original settings. \n\n'
677
            'some details are not yet implemented')
678
        rp.pre_download = _(
679
            "1. Turn radio on.\n"
680
            "2. Connect cable to mic/spkr connector.\n"
681
            "3. Make sure connector is firmly connected.\n"
682
            "4. Click OK to download image from device.\n\n"
683
            "It will may not work if you turn on the radio "
684
            "with the cable already attached\n")
685
        rp.pre_upload = _(
686
            "1. Turn radio on.\n"
687
            "2. Connect cable to mic/spkr connector.\n"
688
            "3. Make sure connector is firmly connected.\n"
689
            "4. Click OK to upload the image to device.\n\n"
690
            "It will may not work if you turn on the radio "
691
            "with the cable already attached")
692
        return rp
693

    
694
    # Return information about this radio's features, including
695
    # how many memories it has, what bands it supports, etc
696
    def get_features(self):
697
        rf = chirp_common.RadioFeatures()
698
        rf.has_bank = False
699
        rf.valid_dtcs_codes = DTCS_CODES
700
        rf.has_rx_dtcs = True
701
        rf.has_ctone = True
702
        rf.has_settings = True
703
        rf.has_comment = False
704
        rf.valid_name_length = 10
705
        rf.valid_power_levels = UVK5_POWER_LEVELS
706
        rf.valid_special_chans = list(SPECIALS.keys())
707
        rf.valid_duplexes = ["", "-", "+", "off"]
708

    
709
        # hack so we can input any frequency,
710
        # the 0.1 and 0.01 steps don't work unfortunately
711
        rf.valid_tuning_steps = list(self._steps)
712

    
713
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
714
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
715
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
716

    
717
        rf.valid_characters = chirp_common.CHARSET_ASCII
718
        rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
719
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
720

    
721
        rf.valid_skips = [""]
722

    
723
        # This radio supports memories 1-200, 201-214 are the VFO memories
724
        rf.memory_bounds = (1, 200)
725

    
726
        rf.valid_bands = []
727
        for a in BANDS_NOLIMITS:
728
            rf.valid_bands.append(
729
                    (int(BANDS_NOLIMITS[a][0]*1000000),
730
                     int(BANDS_NOLIMITS[a][1]*1000000)))
731
        return rf
732

    
733
    # Do a download of the radio from the serial port
734
    def sync_in(self):
735
        self._mmap = do_download(self)
736
        self.process_mmap()
737

    
738
    # Do an upload of the radio to the serial port
739
    def sync_out(self):
740
        do_upload(self)
741

    
742
    def _check_firmware_at_load(self):
743
        firmware = self.metadata.get('uvk5_firmware')
744
        if not firmware:
745
            LOG.warning(_('This image is missing firmware information. '
746
                          'It may have been generated with an old or '
747
                          'modified version of CHIRP. It is advised that '
748
                          'you download a fresh image from your radio and '
749
                          'use that going forward for the best safety and '
750
                          'compatibility.'))
751
        elif not self.k5_approve_firmware(self.metadata['uvk5_firmware']):
752
            raise errors.RadioError(
753
                'Image firmware is %r but is not supported by '
754
                'this driver' % firmware)
755

    
756
    # Convert the raw byte array into a memory object structure
757
    def process_mmap(self):
758
        self._check_firmware_at_load()
759
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
760

    
761
    # Return a raw representation of the memory object, which
762
    # is very helpful for development
763
    def get_raw_memory(self, number):
764
        return repr(self._memobj.channel[number-1])
765

    
766
    def _find_band(self, hz):
767
        return _find_band(self._expanded_limits, hz)
768

    
769
    def validate_memory(self, mem):
770
        msgs = super().validate_memory(mem)
771

    
772
        if mem.duplex == 'off':
773
            return msgs
774

    
775
        # find tx frequency
776
        if mem.duplex == '-':
777
            txfreq = mem.freq - mem.offset
778
        elif mem.duplex == '+':
779
            txfreq = mem.freq + mem.offset
780
        else:
781
            txfreq = mem.freq
782

    
783
        # find band
784
        band = self._find_band(txfreq)
785
        if band is False:
786
            msg = "Transmit frequency %.4f MHz is not supported by this radio"\
787
                   % (txfreq/1000000.0)
788
            msgs.append(chirp_common.ValidationError(msg))
789

    
790
        band = self._find_band(mem.freq)
791
        if band is False:
792
            msg = "The frequency %.4f MHz is not supported by this radio" \
793
                   % (mem.freq/1000000.0)
794
            msgs.append(chirp_common.ValidationError(msg))
795

    
796
        return msgs
797

    
798
    def _set_tone(self, mem, _mem):
799
        ((txmode, txtone, txpol),
800
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
801

    
802
        if txmode == "Tone":
803
            txtoval = CTCSS_TONES.index(txtone)
804
            txmoval = 0b01
805
        elif txmode == "DTCS":
806
            txmoval = txpol == "R" and 0b11 or 0b10
807
            txtoval = DTCS_CODES.index(txtone)
808
        else:
809
            txmoval = 0
810
            txtoval = 0
811

    
812
        if rxmode == "Tone":
813
            rxtoval = CTCSS_TONES.index(rxtone)
814
            rxmoval = 0b01
815
        elif rxmode == "DTCS":
816
            rxmoval = rxpol == "R" and 0b11 or 0b10
817
            rxtoval = DTCS_CODES.index(rxtone)
818
        else:
819
            rxmoval = 0
820
            rxtoval = 0
821

    
822
        _mem.rxcodeflag = rxmoval
823
        _mem.txcodeflag = txmoval
824
        _mem.rxcode = rxtoval
825
        _mem.txcode = txtoval
826

    
827
    def _get_tone(self, mem, _mem):
828
        rxtype = _mem.rxcodeflag
829
        txtype = _mem.txcodeflag
830
        rx_tmode = TMODES[rxtype]
831
        tx_tmode = TMODES[txtype]
832

    
833
        rx_tone = tx_tone = None
834

    
835
        if tx_tmode == "Tone":
836
            if _mem.txcode < len(CTCSS_TONES):
837
                tx_tone = CTCSS_TONES[_mem.txcode]
838
            else:
839
                tx_tone = 0
840
                tx_tmode = ""
841
        elif tx_tmode == "DTCS":
842
            if _mem.txcode < len(DTCS_CODES):
843
                tx_tone = DTCS_CODES[_mem.txcode]
844
            else:
845
                tx_tone = 0
846
                tx_tmode = ""
847

    
848
        if rx_tmode == "Tone":
849
            if _mem.rxcode < len(CTCSS_TONES):
850
                rx_tone = CTCSS_TONES[_mem.rxcode]
851
            else:
852
                rx_tone = 0
853
                rx_tmode = ""
854
        elif rx_tmode == "DTCS":
855
            if _mem.rxcode < len(DTCS_CODES):
856
                rx_tone = DTCS_CODES[_mem.rxcode]
857
            else:
858
                rx_tone = 0
859
                rx_tmode = ""
860

    
861
        tx_pol = txtype == 0x03 and "R" or "N"
862
        rx_pol = rxtype == 0x03 and "R" or "N"
863

    
864
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
865
                                       (rx_tmode, rx_tone, rx_pol))
866

    
867
    def _get_mem_extra(self, mem, _mem):
868
        tmpscn = SCANLIST_LIST[0]
869

    
870
        # We'll also look at the channel attributes if a memory has them
871
        if mem.number <= 200:
872
            _mem3 = self._memobj.channel_attributes[mem.number - 1]
873
            # free memory bit
874
            if _mem3.is_free > 0:
875
                mem.empty = True
876
            # scanlists
877
            if _mem3.is_scanlist1 > 0 and _mem3.is_scanlist2 > 0:
878
                tmpscn = SCANLIST_LIST[3]  # "1+2"
879
            elif _mem3.is_scanlist1 > 0:
880
                tmpscn = SCANLIST_LIST[1]  # "1"
881
            elif _mem3.is_scanlist2 > 0:
882
                tmpscn = SCANLIST_LIST[2]  # "2"
883

    
884
        mem.extra = RadioSettingGroup("Extra", "extra")
885

    
886
        # BCLO
887
        is_bclo = bool(_mem.bclo > 0)
888
        rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo))
889
        mem.extra.append(rs)
890

    
891
        # Frequency reverse - whatever that means, don't see it in the manual
892
        is_frev = bool(_mem.freq_reverse > 0)
893
        rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev))
894
        mem.extra.append(rs)
895

    
896
        # PTTID
897
        try:
898
            pttid = self._pttid_list[_mem.dtmf_pttid]
899
        except IndexError:
900
            pttid = 0
901
        rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(
902
            self._pttid_list, pttid))
903
        mem.extra.append(rs)
904

    
905
        # DTMF DECODE
906
        is_dtmf = bool(_mem.dtmf_decode > 0)
907
        rs = RadioSetting("dtmfdecode", _("DTMF decode"),
908
                          RadioSettingValueBoolean(is_dtmf))
909
        mem.extra.append(rs)
910

    
911
        # Scrambler
912
        if _mem.scrambler & 0x0f < len(SCRAMBLER_LIST):
913
            enc = _mem.scrambler & 0x0f
914
        else:
915
            enc = 0
916

    
917
        rs = RadioSetting("scrambler", _("Scrambler"), RadioSettingValueList(
918
            SCRAMBLER_LIST, SCRAMBLER_LIST[enc]))
919
        mem.extra.append(rs)
920

    
921
        rs = RadioSetting("scanlists", _("Scanlists"), RadioSettingValueList(
922
            SCANLIST_LIST, tmpscn))
923
        mem.extra.append(rs)
924

    
925
    def _get_mem_mode(self, _mem):
926
        if _mem.enable_am > 0:
927
            if _mem.bandwidth > 0:
928
                return "NAM"
929
            else:
930
                return "AM"
931
        else:
932
            if _mem.bandwidth > 0:
933
                return "NFM"
934
            else:
935
                return "FM"
936

    
937
    def _get_specials(self):
938
        return dict(SPECIALS)
939

    
940
    # Extract a high-level memory object from the low-level memory map
941
    # This is called to populate a memory in the UI
942
    def get_memory(self, number2):
943

    
944
        mem = chirp_common.Memory()
945

    
946
        if isinstance(number2, str):
947
            number = self._get_specials()[number2]
948
            mem.extd_number = number2
949
        else:
950
            number = number2 - 1
951

    
952
        mem.number = number + 1
953

    
954
        _mem = self._memobj.channel[number]
955

    
956
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
957
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
958
            mem.empty = True
959

    
960
        self._get_mem_extra(mem, _mem)
961

    
962
        if mem.empty:
963
            return mem
964

    
965
        if number > 199:
966
            mem.immutable = ["name", "scanlists"]
967
        else:
968
            _mem2 = self._memobj.channelname[number]
969
            for char in _mem2.name:
970
                if str(char) == "\xFF" or str(char) == "\x00":
971
                    break
972
                mem.name += str(char)
973
            mem.name = mem.name.rstrip()
974

    
975
        # Convert your low-level frequency to Hertz
976
        mem.freq = int(_mem.freq)*10
977
        mem.offset = int(_mem.offset)*10
978

    
979
        if (mem.offset == 0):
980
            mem.duplex = ''
981
        else:
982
            if _mem.shift == FLAGS1_OFFSET_MINUS:
983
                if _mem.freq == _mem.offset:
984
                    # fake tx disable by setting tx to 0 MHz
985
                    mem.duplex = 'off'
986
                    mem.offset = 0
987
                else:
988
                    mem.duplex = '-'
989
            elif _mem.shift == FLAGS1_OFFSET_PLUS:
990
                mem.duplex = '+'
991
            else:
992
                mem.duplex = ''
993

    
994
        # tone data
995
        self._get_tone(mem, _mem)
996

    
997
        mem.mode = self._get_mem_mode(_mem)
998

    
999
        # tuning step
1000
        try:
1001
            mem.tuning_step = self._steps[_mem.step]
1002
        except IndexError:
1003
            mem.tuning_step = 2.5
1004

    
1005
        # power
1006
        if _mem.txpower == POWER_HIGH:
1007
            mem.power = UVK5_POWER_LEVELS[2]
1008
        elif _mem.txpower == POWER_MEDIUM:
1009
            mem.power = UVK5_POWER_LEVELS[1]
1010
        else:
1011
            mem.power = UVK5_POWER_LEVELS[0]
1012

    
1013
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
1014
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
1015
            mem.empty = True
1016
        else:
1017
            mem.empty = False
1018

    
1019
        return mem
1020

    
1021
    def set_settings(self, settings):
1022
        _mem = self._memobj
1023
        for element in settings:
1024
            if not isinstance(element, RadioSetting):
1025
                self.set_settings(element)
1026
                continue
1027

    
1028
            # basic settings
1029

    
1030
            # call channel
1031
            if element.get_name() == "call_channel":
1032
                _mem.call_channel = int(element.value)-1
1033

    
1034
            # squelch
1035
            if element.get_name() == "squelch":
1036
                _mem.squelch = int(element.value)
1037
            # TOT
1038
            if element.get_name() == "tot":
1039
                _mem.max_talk_time = int(element.value)
1040

    
1041
            # NOAA autoscan
1042
            if element.get_name() == "noaa_autoscan":
1043
                _mem.noaa_autoscan = element.value and 1 or 0
1044

    
1045
            # VOX switch
1046
            if element.get_name() == "vox_switch":
1047
                _mem.vox_switch = element.value and 1 or 0
1048

    
1049
            # vox level
1050
            if element.get_name() == "vox_level":
1051
                _mem.vox_level = int(element.value)-1
1052

    
1053
            # mic gain
1054
            if element.get_name() == "mic_gain":
1055
                _mem.mic_gain = int(element.value)
1056

    
1057
            # Channel display mode
1058
            if element.get_name() == "channel_display_mode":
1059
                _mem.channel_display_mode = CHANNELDISP_LIST.index(
1060
                    str(element.value))
1061

    
1062
            # Crossband receiving/transmitting
1063
            if element.get_name() == "crossband":
1064
                _mem.crossband = CROSSBAND_LIST.index(str(element.value))
1065

    
1066
            # Battery Save
1067
            if element.get_name() == "battery_save":
1068
                _mem.battery_save = BATSAVE_LIST.index(str(element.value))
1069
            # Dual Watch
1070
            if element.get_name() == "dualwatch":
1071
                _mem.dual_watch = DUALWATCH_LIST.index(str(element.value))
1072

    
1073
            # Backlight auto mode
1074
            if element.get_name() == "backlight_auto_mode":
1075
                _mem.backlight_auto_mode = \
1076
                        BACKLIGHT_LIST.index(str(element.value))
1077

    
1078
            # Tail tone elimination
1079
            if element.get_name() == "tail_note_elimination":
1080
                _mem.tail_note_elimination = element.value and 1 or 0
1081

    
1082
            # VFO Open
1083
            if element.get_name() == "vfo_open":
1084
                _mem.vfo_open = element.value and 1 or 0
1085

    
1086
            # Beep control
1087
            if element.get_name() == "beep_control":
1088
                _mem.beep_control = element.value and 1 or 0
1089

    
1090
            # Scan resume mode
1091
            if element.get_name() == "scan_resume_mode":
1092
                _mem.scan_resume_mode = SCANRESUME_LIST.index(
1093
                    str(element.value))
1094

    
1095
            # Keypad lock
1096
            if element.get_name() == "key_lock":
1097
                _mem.key_lock = element.value and 1 or 0
1098

    
1099
            # Auto keypad lock
1100
            if element.get_name() == "auto_keypad_lock":
1101
                _mem.auto_keypad_lock = element.value and 1 or 0
1102

    
1103
            # Power on display mode
1104
            if element.get_name() == "welcome_mode":
1105
                _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value))
1106

    
1107
            # Keypad Tone
1108
            if element.get_name() == "keypad_tone":
1109
                _mem.keypad_tone = KEYPADTONE_LIST.index(str(element.value))
1110

    
1111
            # Language
1112
            if element.get_name() == "language":
1113
                _mem.language = LANGUAGE_LIST.index(str(element.value))
1114

    
1115
            # Alarm mode
1116
            if element.get_name() == "alarm_mode":
1117
                _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value))
1118

    
1119
            # Reminding of end of talk
1120
            if element.get_name() == "reminding_of_end_talk":
1121
                _mem.reminding_of_end_talk = REMENDOFTALK_LIST.index(
1122
                    str(element.value))
1123

    
1124
            # Repeater tail tone elimination
1125
            if element.get_name() == "repeater_tail_elimination":
1126
                _mem.repeater_tail_elimination = RTE_LIST.index(
1127
                    str(element.value))
1128

    
1129
            # Logo string 1
1130
            if element.get_name() == "logo1":
1131
                b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
1132
                _mem.logo_line1 = b[0:12]+"\x00\xff\xff\xff"
1133

    
1134
            # Logo string 2
1135
            if element.get_name() == "logo2":
1136
                b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
1137
                _mem.logo_line2 = b[0:12]+"\x00\xff\xff\xff"
1138

    
1139
            # unlock settings
1140

    
1141
            # FLOCK
1142
            if element.get_name() == "flock":
1143
                _mem.lock.flock = FLOCK_LIST.index(str(element.value))
1144

    
1145
            # 350TX
1146
            if element.get_name() == "tx350":
1147
                _mem.lock.tx350 = element.value and 1 or 0
1148

    
1149
            # 200TX
1150
            if element.get_name() == "tx200":
1151
                _mem.lock.tx200 = element.value and 1 or 0
1152

    
1153
            # 500TX
1154
            if element.get_name() == "tx500":
1155
                _mem.lock.tx500 = element.value and 1 or 0
1156

    
1157
            # 350EN
1158
            if element.get_name() == "en350":
1159
                _mem.lock.en350 = element.value and 1 or 0
1160

    
1161
            # SCREN
1162
            if element.get_name() == "enscramble":
1163
                _mem.lock.enscramble = element.value and 1 or 0
1164

    
1165
            # KILLED
1166
            if element.get_name() == "killed":
1167
                _mem.lock.killed = element.value and 1 or 0
1168

    
1169
            # fm radio
1170
            for i in range(1, 21):
1171
                freqname = "FM_" + str(i)
1172
                if element.get_name() == freqname:
1173
                    val = str(element.value).strip()
1174
                    try:
1175
                        val2 = int(float(val)*10)
1176
                    except Exception:
1177
                        val2 = 0xffff
1178

    
1179
                    if val2 < FMMIN*10 or val2 > FMMAX*10:
1180
                        val2 = 0xffff
1181
#                        raise errors.InvalidValueError(
1182
#                                "FM radio frequency should be a value "
1183
#                                "in the range %.1f - %.1f" % (FMMIN , FMMAX))
1184
                    _mem.fmfreq[i-1] = val2
1185

    
1186
            # dtmf settings
1187
            if element.get_name() == "dtmf_side_tone":
1188
                _mem.dtmf_settings.side_tone = \
1189
                        element.value and 1 or 0
1190

    
1191
            if element.get_name() == "dtmf_separate_code":
1192
                _mem.dtmf_settings.separate_code = str(element.value)
1193

    
1194
            if element.get_name() == "dtmf_group_call_code":
1195
                _mem.dtmf_settings.group_call_code = element.value
1196

    
1197
            if element.get_name() == "dtmf_decode_response":
1198
                _mem.dtmf_settings.decode_response = \
1199
                        DTMF_DECODE_RESPONSE_LIST.index(str(element.value))
1200

    
1201
            if element.get_name() == "dtmf_auto_reset_time":
1202
                _mem.dtmf_settings.auto_reset_time = \
1203
                        int(int(element.value)/10)
1204

    
1205
            if element.get_name() == "dtmf_preload_time":
1206
                _mem.dtmf_settings.preload_time = \
1207
                        int(int(element.value)/10)
1208

    
1209
            if element.get_name() == "dtmf_first_code_persist_time":
1210
                _mem.dtmf_settings.first_code_persist_time = \
1211
                        int(int(element.value)/10)
1212

    
1213
            if element.get_name() == "dtmf_hash_persist_time":
1214
                _mem.dtmf_settings.hash_persist_time = \
1215
                        int(int(element.value)/10)
1216

    
1217
            if element.get_name() == "dtmf_code_persist_time":
1218
                _mem.dtmf_settings.code_persist_time = \
1219
                        int(int(element.value)/10)
1220

    
1221
            if element.get_name() == "dtmf_code_interval_time":
1222
                _mem.dtmf_settings.code_interval_time = \
1223
                        int(int(element.value)/10)
1224

    
1225
            if element.get_name() == "dtmf_permit_remote_kill":
1226
                _mem.dtmf_settings.permit_remote_kill = \
1227
                        element.value and 1 or 0
1228

    
1229
            if element.get_name() == "dtmf_dtmf_local_code":
1230
                k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3
1231
                _mem.dtmf_settings_numbers.dtmf_local_code = k[0:3]
1232

    
1233
            if element.get_name() == "dtmf_dtmf_up_code":
1234
                k = str(element.value).strip("\x20\xff\x00") + "\x00"*16
1235
                _mem.dtmf_settings_numbers.dtmf_up_code = k[0:16]
1236

    
1237
            if element.get_name() == "dtmf_dtmf_down_code":
1238
                k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16
1239
                _mem.dtmf_settings_numbers.dtmf_down_code = k[0:16]
1240

    
1241
            if element.get_name() == "dtmf_kill_code":
1242
                k = str(element.value).strip("\x20\xff\x00") + "\x00"*5
1243
                _mem.dtmf_settings_numbers.kill_code = k[0:5]
1244

    
1245
            if element.get_name() == "dtmf_revive_code":
1246
                k = str(element.value).strip("\x20\xff\x00") + "\x00"*5
1247
                _mem.dtmf_settings_numbers.revive_code = k[0:5]
1248

    
1249
            # dtmf contacts
1250
            for i in range(1, 17):
1251
                varname = "DTMF_" + str(i)
1252
                if element.get_name() == varname:
1253
                    k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*8
1254
                    _mem.dtmfcontact[i-1].name = k[0:8]
1255

    
1256
                varnumname = "DTMFNUM_" + str(i)
1257
                if element.get_name() == varnumname:
1258
                    k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3
1259
                    _mem.dtmfcontact[i-1].number = k[0:3]
1260

    
1261
            # scanlist stuff
1262
            if element.get_name() == "scanlist_default":
1263
                val = (int(element.value) == 2) and 1 or 0
1264
                _mem.scanlist_default = val
1265

    
1266
            if element.get_name() == "scanlist1_priority_scan":
1267
                _mem.scanlist1_priority_scan = \
1268
                        element.value and 1 or 0
1269

    
1270
            if element.get_name() == "scanlist2_priority_scan":
1271
                _mem.scanlist2_priority_scan = \
1272
                        element.value and 1 or 0
1273

    
1274
            if element.get_name() == "scanlist1_priority_ch1" or \
1275
                    element.get_name() == "scanlist1_priority_ch2" or \
1276
                    element.get_name() == "scanlist2_priority_ch1" or \
1277
                    element.get_name() == "scanlist2_priority_ch2":
1278

    
1279
                val = int(element.value)
1280

    
1281
                if val > 200 or val < 1:
1282
                    val = 0xff
1283
                else:
1284
                    val -= 1
1285

    
1286
                if element.get_name() == "scanlist1_priority_ch1":
1287
                    _mem.scanlist1_priority_ch1 = val
1288
                if element.get_name() == "scanlist1_priority_ch2":
1289
                    _mem.scanlist1_priority_ch2 = val
1290
                if element.get_name() == "scanlist2_priority_ch1":
1291
                    _mem.scanlist2_priority_ch1 = val
1292
                if element.get_name() == "scanlist2_priority_ch2":
1293
                    _mem.scanlist2_priority_ch2 = val
1294

    
1295
            if element.get_name() == "key1_shortpress_action":
1296
                _mem.key1_shortpress_action = KEYACTIONS_LIST.index(
1297
                        str(element.value))
1298

    
1299
            if element.get_name() == "key1_longpress_action":
1300
                _mem.key1_longpress_action = KEYACTIONS_LIST.index(
1301
                        str(element.value))
1302

    
1303
            if element.get_name() == "key2_shortpress_action":
1304
                _mem.key2_shortpress_action = KEYACTIONS_LIST.index(
1305
                        str(element.value))
1306

    
1307
            if element.get_name() == "key2_longpress_action":
1308
                _mem.key2_longpress_action = KEYACTIONS_LIST.index(
1309
                        str(element.value))
1310

    
1311
            if element.get_name() == "nolimits":
1312
                LOG.warning("User expanded band limits")
1313
                self._expanded_limits = bool(element.value)
1314

    
1315
    def get_settings(self):
1316
        _mem = self._memobj
1317
        basic = RadioSettingGroup("basic", "Basic Settings")
1318
        keya = RadioSettingGroup("keya", "Programmable keys")
1319
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
1320
        dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts")
1321
        scanl = RadioSettingGroup("scn", "Scan Lists")
1322
        unlock = RadioSettingGroup("unlock", "Unlock Settings")
1323
        fmradio = RadioSettingGroup("fmradio", _("FM Radio"))
1324

    
1325
        roinfo = RadioSettingGroup("roinfo", _("Driver information"))
1326

    
1327
        top = RadioSettings(
1328
                basic, keya, dtmf, dtmfc, scanl, unlock, fmradio, roinfo)
1329

    
1330
        # Programmable keys
1331
        tmpval = int(_mem.key1_shortpress_action)
1332
        if tmpval >= len(KEYACTIONS_LIST):
1333
            tmpval = 0
1334
        rs = RadioSetting("key1_shortpress_action", "Side key 1 short press",
1335
                          RadioSettingValueList(
1336
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1337
        keya.append(rs)
1338

    
1339
        tmpval = int(_mem.key1_longpress_action)
1340
        if tmpval >= len(KEYACTIONS_LIST):
1341
            tmpval = 0
1342
        rs = RadioSetting("key1_longpress_action", "Side key 1 long press",
1343
                          RadioSettingValueList(
1344
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1345
        keya.append(rs)
1346

    
1347
        tmpval = int(_mem.key2_shortpress_action)
1348
        if tmpval >= len(KEYACTIONS_LIST):
1349
            tmpval = 0
1350
        rs = RadioSetting("key2_shortpress_action", "Side key 2 short press",
1351
                          RadioSettingValueList(
1352
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1353
        keya.append(rs)
1354

    
1355
        tmpval = int(_mem.key2_longpress_action)
1356
        if tmpval >= len(KEYACTIONS_LIST):
1357
            tmpval = 0
1358
        rs = RadioSetting("key2_longpress_action", "Side key 2 long press",
1359
                          RadioSettingValueList(
1360
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1361
        keya.append(rs)
1362

    
1363
        # DTMF settings
1364
        tmppr = bool(_mem.dtmf_settings.side_tone > 0)
1365
        rs = RadioSetting(
1366
                "dtmf_side_tone",
1367
                "DTMF Sidetone",
1368
                RadioSettingValueBoolean(tmppr))
1369
        dtmf.append(rs)
1370

    
1371
        tmpval = str(_mem.dtmf_settings.separate_code)
1372
        if tmpval not in DTMF_CODE_CHARS:
1373
            tmpval = '*'
1374
        val = RadioSettingValueString(1, 1, tmpval)
1375
        val.set_charset(DTMF_CODE_CHARS)
1376
        rs = RadioSetting("dtmf_separate_code", "Separate Code", val)
1377
        dtmf.append(rs)
1378

    
1379
        tmpval = str(_mem.dtmf_settings.group_call_code)
1380
        if tmpval not in DTMF_CODE_CHARS:
1381
            tmpval = '#'
1382
        val = RadioSettingValueString(1, 1, tmpval)
1383
        val.set_charset(DTMF_CODE_CHARS)
1384
        rs = RadioSetting("dtmf_group_call_code", "Group Call Code", val)
1385
        dtmf.append(rs)
1386

    
1387
        tmpval = _mem.dtmf_settings.decode_response
1388
        if tmpval >= len(DTMF_DECODE_RESPONSE_LIST):
1389
            tmpval = 0
1390
        rs = RadioSetting("dtmf_decode_response", "Decode Response",
1391
                          RadioSettingValueList(
1392
                              DTMF_DECODE_RESPONSE_LIST,
1393
                              DTMF_DECODE_RESPONSE_LIST[tmpval]))
1394
        dtmf.append(rs)
1395

    
1396
        tmpval = _mem.dtmf_settings.auto_reset_time
1397
        if tmpval > 60 or tmpval < 5:
1398
            tmpval = 5
1399
        rs = RadioSetting("dtmf_auto_reset_time",
1400
                          "Auto reset time (s)",
1401
                          RadioSettingValueInteger(5, 60, tmpval))
1402
        dtmf.append(rs)
1403

    
1404
        tmpval = int(_mem.dtmf_settings.preload_time)
1405
        if tmpval > 100 or tmpval < 3:
1406
            tmpval = 30
1407
        tmpval *= 10
1408
        rs = RadioSetting("dtmf_preload_time",
1409
                          "Pre-load time (ms)",
1410
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1411
        dtmf.append(rs)
1412

    
1413
        tmpval = int(_mem.dtmf_settings.first_code_persist_time)
1414
        if tmpval > 100 or tmpval < 3:
1415
            tmpval = 30
1416
        tmpval *= 10
1417
        rs = RadioSetting("dtmf_first_code_persist_time",
1418
                          "First code persist time (ms)",
1419
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1420
        dtmf.append(rs)
1421

    
1422
        tmpval = int(_mem.dtmf_settings.hash_persist_time)
1423
        if tmpval > 100 or tmpval < 3:
1424
            tmpval = 30
1425
        tmpval *= 10
1426
        rs = RadioSetting("dtmf_hash_persist_time",
1427
                          "#/* persist time (ms)",
1428
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1429
        dtmf.append(rs)
1430

    
1431
        tmpval = int(_mem.dtmf_settings.code_persist_time)
1432
        if tmpval > 100 or tmpval < 3:
1433
            tmpval = 30
1434
        tmpval *= 10
1435
        rs = RadioSetting("dtmf_code_persist_time",
1436
                          "Code persist time (ms)",
1437
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1438
        dtmf.append(rs)
1439

    
1440
        tmpval = int(_mem.dtmf_settings.code_interval_time)
1441
        if tmpval > 100 or tmpval < 3:
1442
            tmpval = 30
1443
        tmpval *= 10
1444
        rs = RadioSetting("dtmf_code_interval_time",
1445
                          "Code interval time (ms)",
1446
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1447
        dtmf.append(rs)
1448

    
1449
        tmpval = bool(_mem.dtmf_settings.permit_remote_kill > 0)
1450
        rs = RadioSetting(
1451
                "dtmf_permit_remote_kill",
1452
                "Permit remote kill",
1453
                RadioSettingValueBoolean(tmpval))
1454
        dtmf.append(rs)
1455

    
1456
        tmpval = str(_mem.dtmf_settings_numbers.dtmf_local_code).upper().strip(
1457
                "\x00\xff\x20")
1458
        for i in tmpval:
1459
            if i in DTMF_CHARS_ID:
1460
                continue
1461
            else:
1462
                tmpval = "103"
1463
                break
1464
        val = RadioSettingValueString(3, 3, tmpval)
1465
        val.set_charset(DTMF_CHARS_ID)
1466
        rs = RadioSetting("dtmf_dtmf_local_code",
1467
                          "Local code (3 chars 0-9 ABCD)", val)
1468
        dtmf.append(rs)
1469

    
1470
        tmpval = str(_mem.dtmf_settings_numbers.dtmf_up_code).upper().strip(
1471
                "\x00\xff\x20")
1472
        for i in tmpval:
1473
            if i in DTMF_CHARS_UPDOWN or i == "":
1474
                continue
1475
            else:
1476
                tmpval = "123"
1477
                break
1478
        val = RadioSettingValueString(1, 16, tmpval)
1479
        val.set_charset(DTMF_CHARS_UPDOWN)
1480
        rs = RadioSetting("dtmf_dtmf_up_code",
1481
                          "Up code (1-16 chars 0-9 ABCD*#)", val)
1482
        dtmf.append(rs)
1483

    
1484
        tmpval = str(_mem.dtmf_settings_numbers.dtmf_down_code).upper().strip(
1485
                "\x00\xff\x20")
1486
        for i in tmpval:
1487
            if i in DTMF_CHARS_UPDOWN:
1488
                continue
1489
            else:
1490
                tmpval = "456"
1491
                break
1492
        val = RadioSettingValueString(1, 16, tmpval)
1493
        val.set_charset(DTMF_CHARS_UPDOWN)
1494
        rs = RadioSetting("dtmf_dtmf_down_code",
1495
                          "Down code (1-16 chars 0-9 ABCD*#)", val)
1496
        dtmf.append(rs)
1497

    
1498
        tmpval = str(_mem.dtmf_settings_numbers.kill_code).upper().strip(
1499
                "\x00\xff\x20")
1500
        for i in tmpval:
1501
            if i in DTMF_CHARS_KILL:
1502
                continue
1503
            else:
1504
                tmpval = "77777"
1505
                break
1506
        if not len(tmpval) == 5:
1507
            tmpval = "77777"
1508
        val = RadioSettingValueString(5, 5, tmpval)
1509
        val.set_charset(DTMF_CHARS_KILL)
1510
        rs = RadioSetting("dtmf_kill_code",
1511
                          "Kill code (5 chars 0-9 ABCD)", val)
1512
        dtmf.append(rs)
1513

    
1514
        tmpval = str(_mem.dtmf_settings_numbers.revive_code).upper().strip(
1515
                "\x00\xff\x20")
1516
        for i in tmpval:
1517
            if i in DTMF_CHARS_KILL:
1518
                continue
1519
            else:
1520
                tmpval = "88888"
1521
                break
1522
        if not len(tmpval) == 5:
1523
            tmpval = "88888"
1524
        val = RadioSettingValueString(5, 5, tmpval)
1525
        val.set_charset(DTMF_CHARS_KILL)
1526
        rs = RadioSetting("dtmf_revive_code",
1527
                          "Revive code (5 chars 0-9 ABCD)", val)
1528
        dtmf.append(rs)
1529

    
1530
        for i in range(1, 17):
1531
            varname = "DTMF_"+str(i)
1532
            varnumname = "DTMFNUM_"+str(i)
1533
            vardescr = "DTMF Contact "+str(i)+" name"
1534
            varinumdescr = "DTMF Contact "+str(i)+" number"
1535

    
1536
            cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff")
1537
            cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff")
1538

    
1539
            val = RadioSettingValueString(0, 8, cntn)
1540
            rs = RadioSetting(varname, vardescr, val)
1541
            dtmfc.append(rs)
1542

    
1543
            val = RadioSettingValueString(0, 3, cntnum)
1544
            val.set_charset(DTMF_CHARS)
1545
            rs = RadioSetting(varnumname, varinumdescr, val)
1546
            dtmfc.append(rs)
1547
            rs.set_doc("DTMF Contacts are 3 codes (valid: 0-9 * # ABCD), "
1548
                       "or an empty string")
1549

    
1550
        # scanlists
1551
        if _mem.scanlist_default == 1:
1552
            tmpsc = 2
1553
        else:
1554
            tmpsc = 1
1555
        rs = RadioSetting("scanlist_default",
1556
                          "Default scanlist",
1557
                          RadioSettingValueInteger(1, 2, tmpsc))
1558
        scanl.append(rs)
1559

    
1560
        tmppr = bool((_mem.scanlist1_priority_scan & 1) > 0)
1561
        rs = RadioSetting(
1562
                "scanlist1_priority_scan",
1563
                "Scanlist 1 priority channel scan",
1564
                RadioSettingValueBoolean(tmppr))
1565
        scanl.append(rs)
1566

    
1567
        tmpch = _mem.scanlist1_priority_ch1 + 1
1568
        if tmpch > 200:
1569
            tmpch = 0
1570
        rs = RadioSetting("scanlist1_priority_ch1",
1571
                          "Scanlist 1 priority channel 1 (0 - off)",
1572
                          RadioSettingValueInteger(0, 200, tmpch))
1573
        scanl.append(rs)
1574

    
1575
        tmpch = _mem.scanlist1_priority_ch2 + 1
1576
        if tmpch > 200:
1577
            tmpch = 0
1578
        rs = RadioSetting("scanlist1_priority_ch2",
1579
                          "Scanlist 1 priority channel 2 (0 - off)",
1580
                          RadioSettingValueInteger(0, 200, tmpch))
1581
        scanl.append(rs)
1582

    
1583
        tmppr = bool((_mem.scanlist2_priority_scan & 1) > 0)
1584
        rs = RadioSetting(
1585
                "scanlist2_priority_scan",
1586
                "Scanlist 2 priority channel scan",
1587
                RadioSettingValueBoolean(tmppr))
1588
        scanl.append(rs)
1589

    
1590
        tmpch = _mem.scanlist2_priority_ch1 + 1
1591
        if tmpch > 200:
1592
            tmpch = 0
1593
        rs = RadioSetting("scanlist2_priority_ch1",
1594
                          "Scanlist 2 priority channel 1 (0 - off)",
1595
                          RadioSettingValueInteger(0, 200, tmpch))
1596
        scanl.append(rs)
1597

    
1598
        tmpch = _mem.scanlist2_priority_ch2 + 1
1599
        if tmpch > 200:
1600
            tmpch = 0
1601
        rs = RadioSetting("scanlist2_priority_ch2",
1602
                          "Scanlist 2 priority channel 2 (0 - off)",
1603
                          RadioSettingValueInteger(0, 200, tmpch))
1604
        scanl.append(rs)
1605

    
1606
        # basic settings
1607

    
1608
        # call channel
1609
        tmpc = _mem.call_channel+1
1610
        if tmpc > 200:
1611
            tmpc = 1
1612
        rs = RadioSetting("call_channel", "One key call channel",
1613
                          RadioSettingValueInteger(1, 200, tmpc))
1614
        basic.append(rs)
1615

    
1616
        # squelch
1617
        tmpsq = _mem.squelch
1618
        if tmpsq > 9:
1619
            tmpsq = 1
1620
        rs = RadioSetting("squelch", "Squelch",
1621
                          RadioSettingValueInteger(0, 9, tmpsq))
1622
        basic.append(rs)
1623

    
1624
        # TOT
1625
        tmptot = _mem.max_talk_time
1626
        if tmptot > 10:
1627
            tmptot = 10
1628
        rs = RadioSetting(
1629
                "tot",
1630
                "Max talk time [min]",
1631
                RadioSettingValueInteger(0, 10, tmptot))
1632
        basic.append(rs)
1633

    
1634
        # NOAA autoscan
1635
        rs = RadioSetting(
1636
                "noaa_autoscan",
1637
                "NOAA Autoscan", RadioSettingValueBoolean(
1638
                    bool(_mem.noaa_autoscan > 0)))
1639
        basic.append(rs)
1640

    
1641
        # VOX switch
1642
        rs = RadioSetting(
1643
                "vox_switch",
1644
                "VOX enabled", RadioSettingValueBoolean(
1645
                    bool(_mem.vox_switch > 0)))
1646
        basic.append(rs)
1647

    
1648
        # VOX Level
1649
        tmpvox = _mem.vox_level+1
1650
        if tmpvox > 10:
1651
            tmpvox = 10
1652
        rs = RadioSetting("vox_level", "VOX Level",
1653
                          RadioSettingValueInteger(1, 10, tmpvox))
1654
        basic.append(rs)
1655

    
1656
        # Mic gain
1657
        tmpmicgain = _mem.mic_gain
1658
        if tmpmicgain > 4:
1659
            tmpmicgain = 4
1660
        rs = RadioSetting("mic_gain", "Mic Gain",
1661
                          RadioSettingValueInteger(0, 4, tmpmicgain))
1662
        basic.append(rs)
1663

    
1664
        # Channel display mode
1665
        tmpchdispmode = _mem.channel_display_mode
1666
        if tmpchdispmode >= len(CHANNELDISP_LIST):
1667
            tmpchdispmode = 0
1668
        rs = RadioSetting(
1669
                "channel_display_mode",
1670
                "Channel display mode",
1671
                RadioSettingValueList(
1672
                    CHANNELDISP_LIST,
1673
                    CHANNELDISP_LIST[tmpchdispmode]))
1674
        basic.append(rs)
1675

    
1676
        # Crossband receiving/transmitting
1677
        tmpcross = _mem.crossband
1678
        if tmpcross >= len(CROSSBAND_LIST):
1679
            tmpcross = 0
1680
        rs = RadioSetting(
1681
                "crossband",
1682
                "Cross-band receiving/transmitting",
1683
                RadioSettingValueList(
1684
                    CROSSBAND_LIST,
1685
                    CROSSBAND_LIST[tmpcross]))
1686
        basic.append(rs)
1687

    
1688
        # Battery save
1689
        tmpbatsave = _mem.battery_save
1690
        if tmpbatsave >= len(BATSAVE_LIST):
1691
            tmpbatsave = BATSAVE_LIST.index("1:4")
1692
        rs = RadioSetting(
1693
                "battery_save",
1694
                "Battery Save",
1695
                RadioSettingValueList(
1696
                    BATSAVE_LIST,
1697
                    BATSAVE_LIST[tmpbatsave]))
1698
        basic.append(rs)
1699

    
1700
        # Dual watch
1701
        tmpdual = _mem.dual_watch
1702
        if tmpdual >= len(DUALWATCH_LIST):
1703
            tmpdual = 0
1704
        rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList(
1705
            DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
1706
        basic.append(rs)
1707

    
1708
        # Backlight auto mode
1709
        tmpback = _mem.backlight_auto_mode
1710
        if tmpback >= len(BACKLIGHT_LIST):
1711
            tmpback = 0
1712
        rs = RadioSetting("backlight_auto_mode",
1713
                          "Backlight auto mode",
1714
                          RadioSettingValueList(
1715
                              BACKLIGHT_LIST,
1716
                              BACKLIGHT_LIST[tmpback]))
1717
        basic.append(rs)
1718

    
1719
        # Tail tone elimination
1720
        rs = RadioSetting(
1721
                "tail_note_elimination",
1722
                "Tail tone elimination",
1723
                RadioSettingValueBoolean(
1724
                    bool(_mem.tail_note_elimination > 0)))
1725
        basic.append(rs)
1726

    
1727
        # VFO open
1728
        rs = RadioSetting("vfo_open", "VFO open",
1729
                          RadioSettingValueBoolean(bool(_mem.vfo_open > 0)))
1730
        basic.append(rs)
1731

    
1732
        # Beep control
1733
        rs = RadioSetting(
1734
                "beep_control",
1735
                "Beep control",
1736
                RadioSettingValueBoolean(bool(_mem.beep_control > 0)))
1737
        basic.append(rs)
1738

    
1739
        # Scan resume mode
1740
        tmpscanres = _mem.scan_resume_mode
1741
        if tmpscanres >= len(SCANRESUME_LIST):
1742
            tmpscanres = 0
1743
        rs = RadioSetting(
1744
                "scan_resume_mode",
1745
                "Scan resume mode",
1746
                RadioSettingValueList(
1747
                    SCANRESUME_LIST,
1748
                    SCANRESUME_LIST[tmpscanres]))
1749
        basic.append(rs)
1750

    
1751
        # Keypad locked
1752
        rs = RadioSetting(
1753
                "key_lock",
1754
                "Keypad lock",
1755
                RadioSettingValueBoolean(bool(_mem.key_lock > 0)))
1756
        basic.append(rs)
1757

    
1758
        # Auto keypad lock
1759
        rs = RadioSetting(
1760
                "auto_keypad_lock",
1761
                "Auto keypad lock",
1762
                RadioSettingValueBoolean(bool(_mem.auto_keypad_lock > 0)))
1763
        basic.append(rs)
1764

    
1765
        # Power on display mode
1766
        tmpdispmode = _mem.power_on_dispmode
1767
        if tmpdispmode >= len(WELCOME_LIST):
1768
            tmpdispmode = 0
1769
        rs = RadioSetting(
1770
                "welcome_mode",
1771
                "Power on display mode",
1772
                RadioSettingValueList(
1773
                    WELCOME_LIST,
1774
                    WELCOME_LIST[tmpdispmode]))
1775
        basic.append(rs)
1776

    
1777
        # Keypad Tone
1778
        tmpkeypadtone = _mem.keypad_tone
1779
        if tmpkeypadtone >= len(KEYPADTONE_LIST):
1780
            tmpkeypadtone = 0
1781
        rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList(
1782
            KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone]))
1783
        basic.append(rs)
1784

    
1785
        # Language
1786
        tmplanguage = _mem.language
1787
        if tmplanguage >= len(LANGUAGE_LIST):
1788
            tmplanguage = 0
1789
        rs = RadioSetting("language", "Language", RadioSettingValueList(
1790
            LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage]))
1791
        basic.append(rs)
1792

    
1793
        # Alarm mode
1794
        tmpalarmmode = _mem.alarm_mode
1795
        if tmpalarmmode >= len(ALARMMODE_LIST):
1796
            tmpalarmmode = 0
1797
        rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList(
1798
            ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode]))
1799
        basic.append(rs)
1800

    
1801
        # Reminding of end of talk
1802
        tmpalarmmode = _mem.reminding_of_end_talk
1803
        if tmpalarmmode >= len(REMENDOFTALK_LIST):
1804
            tmpalarmmode = 0
1805
        rs = RadioSetting(
1806
                "reminding_of_end_talk",
1807
                "Reminding of end of talk",
1808
                RadioSettingValueList(
1809
                    REMENDOFTALK_LIST,
1810
                    REMENDOFTALK_LIST[tmpalarmmode]))
1811
        basic.append(rs)
1812

    
1813
        # Repeater tail tone elimination
1814
        tmprte = _mem.repeater_tail_elimination
1815
        if tmprte >= len(RTE_LIST):
1816
            tmprte = 0
1817
        rs = RadioSetting(
1818
                "repeater_tail_elimination",
1819
                "Repeater tail tone elimination",
1820
                RadioSettingValueList(RTE_LIST, RTE_LIST[tmprte]))
1821
        basic.append(rs)
1822

    
1823
        # Logo string 1
1824
        logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00"
1825
        logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12)
1826
        rs = RadioSetting("logo1", _("Logo string 1 (12 characters)"),
1827
                          RadioSettingValueString(0, 12, logo1))
1828
        basic.append(rs)
1829

    
1830
        # Logo string 2
1831
        logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00"
1832
        logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12)
1833
        rs = RadioSetting("logo2", _("Logo string 2 (12 characters)"),
1834
                          RadioSettingValueString(0, 12, logo2))
1835
        basic.append(rs)
1836

    
1837
        # FM radio
1838
        for i in range(1, 21):
1839
            freqname = "FM_"+str(i)
1840
            fmfreq = _mem.fmfreq[i-1]/10.0
1841
            if fmfreq < FMMIN or fmfreq > FMMAX:
1842
                rs = RadioSetting(freqname, freqname,
1843
                                  RadioSettingValueString(0, 5, ""))
1844
            else:
1845
                rs = RadioSetting(freqname, freqname,
1846
                                  RadioSettingValueString(0, 5, str(fmfreq)))
1847

    
1848
            fmradio.append(rs)
1849

    
1850
        # unlock settings
1851

    
1852
        # F-LOCK
1853
        tmpflock = _mem.lock.flock
1854
        if tmpflock >= len(FLOCK_LIST):
1855
            tmpflock = 0
1856
        rs = RadioSetting(
1857
            "flock", "F-LOCK",
1858
            RadioSettingValueList(FLOCK_LIST, FLOCK_LIST[tmpflock]))
1859
        unlock.append(rs)
1860

    
1861
        # 350TX
1862
        rs = RadioSetting("tx350", "350TX - unlock 350-400 MHz TX",
1863
                          RadioSettingValueBoolean(
1864
                              bool(_mem.lock.tx350 > 0)))
1865
        unlock.append(rs)
1866

    
1867
        # Killed
1868
        rs = RadioSetting("Killed", "KILLED Device was disabled (via DTMF)",
1869
                          RadioSettingValueBoolean(
1870
                              bool(_mem.lock.killed > 0)))
1871
        unlock.append(rs)
1872

    
1873
        # 200TX
1874
        rs = RadioSetting("tx200", "200TX - unlock 174-350 MHz TX",
1875
                          RadioSettingValueBoolean(
1876
                              bool(_mem.lock.tx200 > 0)))
1877
        unlock.append(rs)
1878

    
1879
        # 500TX
1880
        rs = RadioSetting("tx500", "500TX - unlock 500-600 MHz TX",
1881
                          RadioSettingValueBoolean(
1882
                              bool(_mem.lock.tx500 > 0)))
1883
        unlock.append(rs)
1884

    
1885
        # 350EN
1886
        rs = RadioSetting("en350", "350EN - unlock 350-400 MHz RX",
1887
                          RadioSettingValueBoolean(
1888
                              bool(_mem.lock.en350 > 0)))
1889
        unlock.append(rs)
1890

    
1891
        # SCREEN
1892
        rs = RadioSetting("scrambler", "SCREN - scrambler enable",
1893
                          RadioSettingValueBoolean(
1894
                              bool(_mem.lock.enscramble > 0)))
1895
        unlock.append(rs)
1896

    
1897
        # readonly info
1898
        # Firmware
1899
        firmware = self.metadata.get('uvk5_firmware', 'UNKNOWN')
1900

    
1901
        val = RadioSettingValueString(0, 128, firmware)
1902
        val.set_mutable(False)
1903
        rs = RadioSetting("fw_ver", "Firmware Version", val)
1904
        roinfo.append(rs)
1905

    
1906
        # No limits version for hacked firmware
1907
        val = RadioSettingValueBoolean(self._expanded_limits)
1908
        rs = RadioSetting("nolimits", "Limits disabled for modified firmware",
1909
                          val)
1910
        rs.set_warning(_(
1911
            'This should only be enabled if you are using modified firmware '
1912
            'that supports wider frequency coverage. Enabling this will cause '
1913
            'CHIRP not to enforce OEM restrictions and may lead to undefined '
1914
            'or unregulated behavior. Use at your own risk!'),
1915
            safe_value=False)
1916
        roinfo.append(rs)
1917

    
1918
        return top
1919

    
1920
    def _set_mem_mode(self, _mem, mode):
1921
        if mode == "NFM":
1922
            _mem.bandwidth = 1
1923
            _mem.enable_am = 0
1924
        elif mode == "FM":
1925
            _mem.bandwidth = 0
1926
            _mem.enable_am = 0
1927
        elif mode == "NAM":
1928
            _mem.bandwidth = 1
1929
            _mem.enable_am = 1
1930
        elif mode == "AM":
1931
            _mem.bandwidth = 0
1932
            _mem.enable_am = 1
1933

    
1934
    # Store details about a high-level memory to the memory map
1935
    # This is called when a user edits a memory in the UI
1936
    def set_memory(self, mem):
1937
        number = mem.number-1
1938

    
1939
        # Get a low-level memory object mapped to the image
1940
        _mem = self._memobj.channel[number]
1941
        _mem4 = self._memobj
1942
        # empty memory
1943
        if mem.empty:
1944
            _mem.set_raw(b"\xFF" * 16)
1945
            if number < 200:
1946
                _mem2 = self._memobj.channelname[number]
1947
                _mem2.set_raw(b"\xFF" * 16)
1948
                _mem4.channel_attributes[number].is_scanlist1 = 0
1949
                _mem4.channel_attributes[number].is_scanlist2 = 0
1950
                # Compander in other models, not supported here
1951
                _mem4.channel_attributes[number].compander = 0
1952
                _mem4.channel_attributes[number].is_free = 1
1953
                _mem4.channel_attributes[number].band = 0x7
1954
            return mem
1955

    
1956
        # clean the channel memory, restore some bits if it was used before
1957
        if _mem.get_raw(asbytes=False)[0] == "\xff":
1958
            # this was an empty memory
1959
            _mem.set_raw(b"\x00" * 16)
1960
        else:
1961
            # this memory wasn't empty, save some bits that we don't know the
1962
            # meaning of, or that we don't support yet
1963
            prev_0a = _mem.get_raw()[0x0a] & SAVE_MASK_0A
1964
            prev_0b = _mem.get_raw()[0x0b] & SAVE_MASK_0B
1965
            prev_0c = _mem.get_raw()[0x0c] & SAVE_MASK_0C
1966
            prev_0d = _mem.get_raw()[0x0d] & SAVE_MASK_0D
1967
            prev_0e = _mem.get_raw()[0x0e] & SAVE_MASK_0E
1968
            prev_0f = _mem.get_raw()[0x0f] & SAVE_MASK_0F
1969
            _mem.set_raw(b"\x00" * 10 +
1970
                         bytes([prev_0a, prev_0b, prev_0c,
1971
                                prev_0d, prev_0e, prev_0f]))
1972

    
1973
        if number < 200:
1974
            _mem4.channel_attributes[number].is_scanlist1 = 0
1975
            _mem4.channel_attributes[number].is_scanlist2 = 0
1976
            _mem4.channel_attributes[number].compander = 0
1977
            _mem4.channel_attributes[number].is_free = 1
1978
            _mem4.channel_attributes[number].band = 0x7
1979

    
1980
        # find band
1981
        band = _find_band(self, mem.freq)
1982

    
1983
        self._set_mem_mode(_mem, mem.mode)
1984

    
1985
        # frequency/offset
1986
        _mem.freq = mem.freq/10
1987
        _mem.offset = mem.offset/10
1988

    
1989
        if mem.duplex == "":
1990
            _mem.offset = 0
1991
            _mem.shift = 0
1992
        elif mem.duplex == '-':
1993
            _mem.shift = FLAGS1_OFFSET_MINUS
1994
        elif mem.duplex == '+':
1995
            _mem.shift = FLAGS1_OFFSET_PLUS
1996
        elif mem.duplex == 'off':
1997
            # we fake tx disable by setting the tx freq to 0 MHz
1998
            _mem.shift = FLAGS1_OFFSET_MINUS
1999
            _mem.offset = _mem.freq
2000

    
2001
        # set band
2002
        if number < 200:
2003
            _mem4.channel_attributes[number].is_free = 0
2004
            _mem4.channel_attributes[number].band = band
2005

    
2006
        # channels >200 are the 14 VFO chanells and don't have names
2007
        if number < 200:
2008
            _mem2 = self._memobj.channelname[number]
2009
            tag = mem.name.ljust(10) + "\x00"*6
2010
            _mem2.name = tag  # Store the alpha tag
2011

    
2012
        # tone data
2013
        self._set_tone(mem, _mem)
2014

    
2015
        # step
2016
        _mem.step = self._steps.index(mem.tuning_step)
2017

    
2018
        # tx power
2019
        if str(mem.power) == str(UVK5_POWER_LEVELS[2]):
2020
            _mem.txpower = POWER_HIGH
2021
        elif str(mem.power) == str(UVK5_POWER_LEVELS[1]):
2022
            _mem.txpower = POWER_MEDIUM
2023
        else:
2024
            _mem.txpower = POWER_LOW
2025

    
2026
        for setting in mem.extra:
2027
            sname = setting.get_name()
2028
            svalue = setting.value.get_value()
2029

    
2030
            if sname == "bclo":
2031
                _mem.bclo = svalue and 1 or 0
2032

    
2033
            if sname == "pttid":
2034
                _mem.dtmf_pttid = self._pttid_list.index(svalue)
2035

    
2036
            if sname == "frev":
2037
                _mem.freq_reverse = svalue and 1 or 0
2038

    
2039
            if sname == "dtmfdecode":
2040
                _mem.dtmf_decode = svalue and 1 or 0
2041

    
2042
            if sname == "scrambler":
2043
                _mem.scrambler = (
2044
                    _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
2045

    
2046
            if number < 200 and sname == "scanlists":
2047
                if svalue == "1":
2048
                    _mem4.channel_attributes[number].is_scanlist1 = 1
2049
                    _mem4.channel_attributes[number].is_scanlist2 = 0
2050
                elif svalue == "2":
2051
                    _mem4.channel_attributes[number].is_scanlist1 = 0
2052
                    _mem4.channel_attributes[number].is_scanlist2 = 1
2053
                elif svalue == "1+2":
2054
                    _mem4.channel_attributes[number].is_scanlist1 = 1
2055
                    _mem4.channel_attributes[number].is_scanlist2 = 1
2056
                else:
2057
                    _mem4.channel_attributes[number].is_scanlist1 = 0
2058
                    _mem4.channel_attributes[number].is_scanlist2 = 0
2059

    
2060
        return mem
2061

    
2062

    
2063
@directory.register
2064
class UVK5Radio(UVK5RadioBase):
2065
    @classmethod
2066
    def k5_approve_firmware(cls, firmware):
2067
        approved_prefixes = ('k5_2.01.', 'app_2.01.', '2.01.',
2068
                             '1o11', '4.00.', 'k5_4.00.')
2069
        return any(firmware.startswith(x) for x in approved_prefixes)
2070

    
2071
    @classmethod
2072
    def detect_from_serial(cls, pipe):
2073
        firmware = _sayhello(pipe)
2074
        for rclass in [UVK5Radio] + cls.detected_models():
2075
            if rclass.k5_approve_firmware(firmware):
2076
                return rclass
2077
        raise errors.RadioError('Firmware %r not supported' % firmware)
2078

    
2079

    
2080
@directory.register
2081
class RA79Radio(UVK5Radio):
2082
    """Retevis RA79"""
2083
    VENDOR = "Retevis"
2084
    MODEL = "RA79"
(4-4/4)