Project

General

Profile

Bug #11165 » uvk5.py

Dan Smith, 02/13/2024 02:48 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
    @classmethod
669
    def detect_from_serial(cls, pipe):
670
        firmware = _sayhello(pipe)
671
        for rclass in [UVK5Radio] + cls.DETECTED_MODELS:
672
            if rclass.k5_approve_firmware(firmware):
673
                return rclass
674
        raise errors.RadioError('Firmware %r not supported' % firmware)
675

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

    
702
    # Return information about this radio's features, including
703
    # how many memories it has, what bands it supports, etc
704
    def get_features(self):
705
        rf = chirp_common.RadioFeatures()
706
        rf.has_bank = False
707
        rf.valid_dtcs_codes = DTCS_CODES
708
        rf.has_rx_dtcs = True
709
        rf.has_ctone = True
710
        rf.has_settings = True
711
        rf.has_comment = False
712
        rf.valid_name_length = 10
713
        rf.valid_power_levels = UVK5_POWER_LEVELS
714
        rf.valid_special_chans = list(SPECIALS.keys())
715
        rf.valid_duplexes = ["", "-", "+", "off"]
716

    
717
        # hack so we can input any frequency,
718
        # the 0.1 and 0.01 steps don't work unfortunately
719
        rf.valid_tuning_steps = list(self._steps)
720

    
721
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
722
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
723
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
724

    
725
        rf.valid_characters = chirp_common.CHARSET_ASCII
726
        rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
727
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
728

    
729
        rf.valid_skips = [""]
730

    
731
        # This radio supports memories 1-200, 201-214 are the VFO memories
732
        rf.memory_bounds = (1, 200)
733

    
734
        rf.valid_bands = []
735
        for a in BANDS_NOLIMITS:
736
            rf.valid_bands.append(
737
                    (int(BANDS_NOLIMITS[a][0]*1000000),
738
                     int(BANDS_NOLIMITS[a][1]*1000000)))
739
        return rf
740

    
741
    # Do a download of the radio from the serial port
742
    def sync_in(self):
743
        self._mmap = do_download(self)
744
        self.process_mmap()
745

    
746
    # Do an upload of the radio to the serial port
747
    def sync_out(self):
748
        do_upload(self)
749

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

    
764
    # Convert the raw byte array into a memory object structure
765
    def process_mmap(self):
766
        self._check_firmware_at_load()
767
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
768

    
769
    # Return a raw representation of the memory object, which
770
    # is very helpful for development
771
    def get_raw_memory(self, number):
772
        return repr(self._memobj.channel[number-1])
773

    
774
    def validate_memory(self, mem):
775
        msgs = super().validate_memory(mem)
776

    
777
        if mem.duplex == 'off':
778
            return msgs
779

    
780
        # find tx frequency
781
        if mem.duplex == '-':
782
            txfreq = mem.freq - mem.offset
783
        elif mem.duplex == '+':
784
            txfreq = mem.freq + mem.offset
785
        else:
786
            txfreq = mem.freq
787

    
788
        # find band
789
        band = _find_band(self._expanded_limits, txfreq)
790
        if band is False:
791
            msg = "Transmit frequency %.4f MHz is not supported by this radio"\
792
                   % (txfreq/1000000.0)
793
            msgs.append(chirp_common.ValidationError(msg))
794

    
795
        band = _find_band(self._expanded_limits, mem.freq)
796
        if band is False:
797
            msg = "The frequency %.4f MHz is not supported by this radio" \
798
                   % (mem.freq/1000000.0)
799
            msgs.append(chirp_common.ValidationError(msg))
800

    
801
        return msgs
802

    
803
    def _set_tone(self, mem, _mem):
804
        ((txmode, txtone, txpol),
805
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
806

    
807
        if txmode == "Tone":
808
            txtoval = CTCSS_TONES.index(txtone)
809
            txmoval = 0b01
810
        elif txmode == "DTCS":
811
            txmoval = txpol == "R" and 0b11 or 0b10
812
            txtoval = DTCS_CODES.index(txtone)
813
        else:
814
            txmoval = 0
815
            txtoval = 0
816

    
817
        if rxmode == "Tone":
818
            rxtoval = CTCSS_TONES.index(rxtone)
819
            rxmoval = 0b01
820
        elif rxmode == "DTCS":
821
            rxmoval = rxpol == "R" and 0b11 or 0b10
822
            rxtoval = DTCS_CODES.index(rxtone)
823
        else:
824
            rxmoval = 0
825
            rxtoval = 0
826

    
827
        _mem.rxcodeflag = rxmoval
828
        _mem.txcodeflag = txmoval
829
        _mem.rxcode = rxtoval
830
        _mem.txcode = txtoval
831

    
832
    def _get_tone(self, mem, _mem):
833
        rxtype = _mem.rxcodeflag
834
        txtype = _mem.txcodeflag
835
        rx_tmode = TMODES[rxtype]
836
        tx_tmode = TMODES[txtype]
837

    
838
        rx_tone = tx_tone = None
839

    
840
        if tx_tmode == "Tone":
841
            if _mem.txcode < len(CTCSS_TONES):
842
                tx_tone = CTCSS_TONES[_mem.txcode]
843
            else:
844
                tx_tone = 0
845
                tx_tmode = ""
846
        elif tx_tmode == "DTCS":
847
            if _mem.txcode < len(DTCS_CODES):
848
                tx_tone = DTCS_CODES[_mem.txcode]
849
            else:
850
                tx_tone = 0
851
                tx_tmode = ""
852

    
853
        if rx_tmode == "Tone":
854
            if _mem.rxcode < len(CTCSS_TONES):
855
                rx_tone = CTCSS_TONES[_mem.rxcode]
856
            else:
857
                rx_tone = 0
858
                rx_tmode = ""
859
        elif rx_tmode == "DTCS":
860
            if _mem.rxcode < len(DTCS_CODES):
861
                rx_tone = DTCS_CODES[_mem.rxcode]
862
            else:
863
                rx_tone = 0
864
                rx_tmode = ""
865

    
866
        tx_pol = txtype == 0x03 and "R" or "N"
867
        rx_pol = rxtype == 0x03 and "R" or "N"
868

    
869
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
870
                                       (rx_tmode, rx_tone, rx_pol))
871

    
872
    def _get_mem_extra(self, mem, _mem):
873
        tmpscn = SCANLIST_LIST[0]
874

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

    
889
        mem.extra = RadioSettingGroup("Extra", "extra")
890

    
891
        # BCLO
892
        is_bclo = bool(_mem.bclo > 0)
893
        rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo))
894
        mem.extra.append(rs)
895

    
896
        # Frequency reverse - whatever that means, don't see it in the manual
897
        is_frev = bool(_mem.freq_reverse > 0)
898
        rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev))
899
        mem.extra.append(rs)
900

    
901
        # PTTID
902
        try:
903
            pttid = self._pttid_list[_mem.dtmf_pttid]
904
        except IndexError:
905
            pttid = 0
906
        rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(
907
            self._pttid_list, pttid))
908
        mem.extra.append(rs)
909

    
910
        # DTMF DECODE
911
        is_dtmf = bool(_mem.dtmf_decode > 0)
912
        rs = RadioSetting("dtmfdecode", _("DTMF decode"),
913
                          RadioSettingValueBoolean(is_dtmf))
914
        mem.extra.append(rs)
915

    
916
        # Scrambler
917
        if _mem.scrambler & 0x0f < len(SCRAMBLER_LIST):
918
            enc = _mem.scrambler & 0x0f
919
        else:
920
            enc = 0
921

    
922
        rs = RadioSetting("scrambler", _("Scrambler"), RadioSettingValueList(
923
            SCRAMBLER_LIST, SCRAMBLER_LIST[enc]))
924
        mem.extra.append(rs)
925

    
926
        rs = RadioSetting("scanlists", _("Scanlists"), RadioSettingValueList(
927
            SCANLIST_LIST, tmpscn))
928
        mem.extra.append(rs)
929

    
930
    def _get_mem_mode(self, _mem):
931
        if _mem.enable_am > 0:
932
            if _mem.bandwidth > 0:
933
                return "NAM"
934
            else:
935
                return "AM"
936
        else:
937
            if _mem.bandwidth > 0:
938
                return "NFM"
939
            else:
940
                return "FM"
941

    
942
    def _get_specials(self):
943
        return dict(SPECIALS)
944

    
945
    # Extract a high-level memory object from the low-level memory map
946
    # This is called to populate a memory in the UI
947
    def get_memory(self, number2):
948

    
949
        mem = chirp_common.Memory()
950

    
951
        if isinstance(number2, str):
952
            number = self._get_specials()[number2]
953
            mem.extd_number = number2
954
        else:
955
            number = number2 - 1
956

    
957
        mem.number = number + 1
958

    
959
        _mem = self._memobj.channel[number]
960

    
961
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
962
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
963
            mem.empty = True
964

    
965
        self._get_mem_extra(mem, _mem)
966

    
967
        if mem.empty:
968
            return mem
969

    
970
        if number > 199:
971
            mem.immutable = ["name", "scanlists"]
972
        else:
973
            _mem2 = self._memobj.channelname[number]
974
            for char in _mem2.name:
975
                if str(char) == "\xFF" or str(char) == "\x00":
976
                    break
977
                mem.name += str(char)
978
            mem.name = mem.name.rstrip()
979

    
980
        # Convert your low-level frequency to Hertz
981
        mem.freq = int(_mem.freq)*10
982
        mem.offset = int(_mem.offset)*10
983

    
984
        if (mem.offset == 0):
985
            mem.duplex = ''
986
        else:
987
            if _mem.shift == FLAGS1_OFFSET_MINUS:
988
                if _mem.freq == _mem.offset:
989
                    # fake tx disable by setting tx to 0 MHz
990
                    mem.duplex = 'off'
991
                    mem.offset = 0
992
                else:
993
                    mem.duplex = '-'
994
            elif _mem.shift == FLAGS1_OFFSET_PLUS:
995
                mem.duplex = '+'
996
            else:
997
                mem.duplex = ''
998

    
999
        # tone data
1000
        self._get_tone(mem, _mem)
1001

    
1002
        mem.mode = self._get_mem_mode(_mem)
1003

    
1004
        # tuning step
1005
        try:
1006
            mem.tuning_step = self._steps[_mem.step]
1007
        except IndexError:
1008
            mem.tuning_step = 2.5
1009

    
1010
        # power
1011
        if _mem.txpower == POWER_HIGH:
1012
            mem.power = UVK5_POWER_LEVELS[2]
1013
        elif _mem.txpower == POWER_MEDIUM:
1014
            mem.power = UVK5_POWER_LEVELS[1]
1015
        else:
1016
            mem.power = UVK5_POWER_LEVELS[0]
1017

    
1018
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
1019
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
1020
            mem.empty = True
1021
        else:
1022
            mem.empty = False
1023

    
1024
        return mem
1025

    
1026
    def set_settings(self, settings):
1027
        _mem = self._memobj
1028
        for element in settings:
1029
            if not isinstance(element, RadioSetting):
1030
                self.set_settings(element)
1031
                continue
1032

    
1033
            # basic settings
1034

    
1035
            # call channel
1036
            if element.get_name() == "call_channel":
1037
                _mem.call_channel = int(element.value)-1
1038

    
1039
            # squelch
1040
            if element.get_name() == "squelch":
1041
                _mem.squelch = int(element.value)
1042
            # TOT
1043
            if element.get_name() == "tot":
1044
                _mem.max_talk_time = int(element.value)
1045

    
1046
            # NOAA autoscan
1047
            if element.get_name() == "noaa_autoscan":
1048
                _mem.noaa_autoscan = element.value and 1 or 0
1049

    
1050
            # VOX switch
1051
            if element.get_name() == "vox_switch":
1052
                _mem.vox_switch = element.value and 1 or 0
1053

    
1054
            # vox level
1055
            if element.get_name() == "vox_level":
1056
                _mem.vox_level = int(element.value)-1
1057

    
1058
            # mic gain
1059
            if element.get_name() == "mic_gain":
1060
                _mem.mic_gain = int(element.value)
1061

    
1062
            # Channel display mode
1063
            if element.get_name() == "channel_display_mode":
1064
                _mem.channel_display_mode = CHANNELDISP_LIST.index(
1065
                    str(element.value))
1066

    
1067
            # Crossband receiving/transmitting
1068
            if element.get_name() == "crossband":
1069
                _mem.crossband = CROSSBAND_LIST.index(str(element.value))
1070

    
1071
            # Battery Save
1072
            if element.get_name() == "battery_save":
1073
                _mem.battery_save = BATSAVE_LIST.index(str(element.value))
1074
            # Dual Watch
1075
            if element.get_name() == "dualwatch":
1076
                _mem.dual_watch = DUALWATCH_LIST.index(str(element.value))
1077

    
1078
            # Backlight auto mode
1079
            if element.get_name() == "backlight_auto_mode":
1080
                _mem.backlight_auto_mode = \
1081
                        BACKLIGHT_LIST.index(str(element.value))
1082

    
1083
            # Tail tone elimination
1084
            if element.get_name() == "tail_note_elimination":
1085
                _mem.tail_note_elimination = element.value and 1 or 0
1086

    
1087
            # VFO Open
1088
            if element.get_name() == "vfo_open":
1089
                _mem.vfo_open = element.value and 1 or 0
1090

    
1091
            # Beep control
1092
            if element.get_name() == "beep_control":
1093
                _mem.beep_control = element.value and 1 or 0
1094

    
1095
            # Scan resume mode
1096
            if element.get_name() == "scan_resume_mode":
1097
                _mem.scan_resume_mode = SCANRESUME_LIST.index(
1098
                    str(element.value))
1099

    
1100
            # Keypad lock
1101
            if element.get_name() == "key_lock":
1102
                _mem.key_lock = element.value and 1 or 0
1103

    
1104
            # Auto keypad lock
1105
            if element.get_name() == "auto_keypad_lock":
1106
                _mem.auto_keypad_lock = element.value and 1 or 0
1107

    
1108
            # Power on display mode
1109
            if element.get_name() == "welcome_mode":
1110
                _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value))
1111

    
1112
            # Keypad Tone
1113
            if element.get_name() == "keypad_tone":
1114
                _mem.keypad_tone = KEYPADTONE_LIST.index(str(element.value))
1115

    
1116
            # Language
1117
            if element.get_name() == "language":
1118
                _mem.language = LANGUAGE_LIST.index(str(element.value))
1119

    
1120
            # Alarm mode
1121
            if element.get_name() == "alarm_mode":
1122
                _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value))
1123

    
1124
            # Reminding of end of talk
1125
            if element.get_name() == "reminding_of_end_talk":
1126
                _mem.reminding_of_end_talk = REMENDOFTALK_LIST.index(
1127
                    str(element.value))
1128

    
1129
            # Repeater tail tone elimination
1130
            if element.get_name() == "repeater_tail_elimination":
1131
                _mem.repeater_tail_elimination = RTE_LIST.index(
1132
                    str(element.value))
1133

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

    
1139
            # Logo string 2
1140
            if element.get_name() == "logo2":
1141
                b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
1142
                _mem.logo_line2 = b[0:12]+"\x00\xff\xff\xff"
1143

    
1144
            # unlock settings
1145

    
1146
            # FLOCK
1147
            if element.get_name() == "flock":
1148
                _mem.lock.flock = FLOCK_LIST.index(str(element.value))
1149

    
1150
            # 350TX
1151
            if element.get_name() == "tx350":
1152
                _mem.lock.tx350 = element.value and 1 or 0
1153

    
1154
            # 200TX
1155
            if element.get_name() == "tx200":
1156
                _mem.lock.tx200 = element.value and 1 or 0
1157

    
1158
            # 500TX
1159
            if element.get_name() == "tx500":
1160
                _mem.lock.tx500 = element.value and 1 or 0
1161

    
1162
            # 350EN
1163
            if element.get_name() == "en350":
1164
                _mem.lock.en350 = element.value and 1 or 0
1165

    
1166
            # SCREN
1167
            if element.get_name() == "enscramble":
1168
                _mem.lock.enscramble = element.value and 1 or 0
1169

    
1170
            # KILLED
1171
            if element.get_name() == "killed":
1172
                _mem.lock.killed = element.value and 1 or 0
1173

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

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

    
1191
            # dtmf settings
1192
            if element.get_name() == "dtmf_side_tone":
1193
                _mem.dtmf_settings.side_tone = \
1194
                        element.value and 1 or 0
1195

    
1196
            if element.get_name() == "dtmf_separate_code":
1197
                _mem.dtmf_settings.separate_code = str(element.value)
1198

    
1199
            if element.get_name() == "dtmf_group_call_code":
1200
                _mem.dtmf_settings.group_call_code = element.value
1201

    
1202
            if element.get_name() == "dtmf_decode_response":
1203
                _mem.dtmf_settings.decode_response = \
1204
                        DTMF_DECODE_RESPONSE_LIST.index(str(element.value))
1205

    
1206
            if element.get_name() == "dtmf_auto_reset_time":
1207
                _mem.dtmf_settings.auto_reset_time = \
1208
                        int(int(element.value)/10)
1209

    
1210
            if element.get_name() == "dtmf_preload_time":
1211
                _mem.dtmf_settings.preload_time = \
1212
                        int(int(element.value)/10)
1213

    
1214
            if element.get_name() == "dtmf_first_code_persist_time":
1215
                _mem.dtmf_settings.first_code_persist_time = \
1216
                        int(int(element.value)/10)
1217

    
1218
            if element.get_name() == "dtmf_hash_persist_time":
1219
                _mem.dtmf_settings.hash_persist_time = \
1220
                        int(int(element.value)/10)
1221

    
1222
            if element.get_name() == "dtmf_code_persist_time":
1223
                _mem.dtmf_settings.code_persist_time = \
1224
                        int(int(element.value)/10)
1225

    
1226
            if element.get_name() == "dtmf_code_interval_time":
1227
                _mem.dtmf_settings.code_interval_time = \
1228
                        int(int(element.value)/10)
1229

    
1230
            if element.get_name() == "dtmf_permit_remote_kill":
1231
                _mem.dtmf_settings.permit_remote_kill = \
1232
                        element.value and 1 or 0
1233

    
1234
            if element.get_name() == "dtmf_dtmf_local_code":
1235
                k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3
1236
                _mem.dtmf_settings_numbers.dtmf_local_code = k[0:3]
1237

    
1238
            if element.get_name() == "dtmf_dtmf_up_code":
1239
                k = str(element.value).strip("\x20\xff\x00") + "\x00"*16
1240
                _mem.dtmf_settings_numbers.dtmf_up_code = k[0:16]
1241

    
1242
            if element.get_name() == "dtmf_dtmf_down_code":
1243
                k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16
1244
                _mem.dtmf_settings_numbers.dtmf_down_code = k[0:16]
1245

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

    
1250
            if element.get_name() == "dtmf_revive_code":
1251
                k = str(element.value).strip("\x20\xff\x00") + "\x00"*5
1252
                _mem.dtmf_settings_numbers.revive_code = k[0:5]
1253

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

    
1261
                varnumname = "DTMFNUM_" + str(i)
1262
                if element.get_name() == varnumname:
1263
                    k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3
1264
                    _mem.dtmfcontact[i-1].number = k[0:3]
1265

    
1266
            # scanlist stuff
1267
            if element.get_name() == "scanlist_default":
1268
                val = (int(element.value) == 2) and 1 or 0
1269
                _mem.scanlist_default = val
1270

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

    
1275
            if element.get_name() == "scanlist2_priority_scan":
1276
                _mem.scanlist2_priority_scan = \
1277
                        element.value and 1 or 0
1278

    
1279
            if element.get_name() == "scanlist1_priority_ch1" or \
1280
                    element.get_name() == "scanlist1_priority_ch2" or \
1281
                    element.get_name() == "scanlist2_priority_ch1" or \
1282
                    element.get_name() == "scanlist2_priority_ch2":
1283

    
1284
                val = int(element.value)
1285

    
1286
                if val > 200 or val < 1:
1287
                    val = 0xff
1288
                else:
1289
                    val -= 1
1290

    
1291
                if element.get_name() == "scanlist1_priority_ch1":
1292
                    _mem.scanlist1_priority_ch1 = val
1293
                if element.get_name() == "scanlist1_priority_ch2":
1294
                    _mem.scanlist1_priority_ch2 = val
1295
                if element.get_name() == "scanlist2_priority_ch1":
1296
                    _mem.scanlist2_priority_ch1 = val
1297
                if element.get_name() == "scanlist2_priority_ch2":
1298
                    _mem.scanlist2_priority_ch2 = val
1299

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

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

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

    
1312
            if element.get_name() == "key2_longpress_action":
1313
                _mem.key2_longpress_action = KEYACTIONS_LIST.index(
1314
                        str(element.value))
1315

    
1316
            if element.get_name() == "nolimits":
1317
                LOG.warning("User expanded band limits")
1318
                self._expanded_limits = bool(element.value)
1319

    
1320
    def get_settings(self):
1321
        _mem = self._memobj
1322
        basic = RadioSettingGroup("basic", "Basic Settings")
1323
        keya = RadioSettingGroup("keya", "Programmable keys")
1324
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
1325
        dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts")
1326
        scanl = RadioSettingGroup("scn", "Scan Lists")
1327
        unlock = RadioSettingGroup("unlock", "Unlock Settings")
1328
        fmradio = RadioSettingGroup("fmradio", _("FM Radio"))
1329

    
1330
        roinfo = RadioSettingGroup("roinfo", _("Driver information"))
1331

    
1332
        top = RadioSettings(
1333
                basic, keya, dtmf, dtmfc, scanl, unlock, fmradio, roinfo)
1334

    
1335
        # Programmable keys
1336
        tmpval = int(_mem.key1_shortpress_action)
1337
        if tmpval >= len(KEYACTIONS_LIST):
1338
            tmpval = 0
1339
        rs = RadioSetting("key1_shortpress_action", "Side key 1 short press",
1340
                          RadioSettingValueList(
1341
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1342
        keya.append(rs)
1343

    
1344
        tmpval = int(_mem.key1_longpress_action)
1345
        if tmpval >= len(KEYACTIONS_LIST):
1346
            tmpval = 0
1347
        rs = RadioSetting("key1_longpress_action", "Side key 1 long press",
1348
                          RadioSettingValueList(
1349
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1350
        keya.append(rs)
1351

    
1352
        tmpval = int(_mem.key2_shortpress_action)
1353
        if tmpval >= len(KEYACTIONS_LIST):
1354
            tmpval = 0
1355
        rs = RadioSetting("key2_shortpress_action", "Side key 2 short press",
1356
                          RadioSettingValueList(
1357
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1358
        keya.append(rs)
1359

    
1360
        tmpval = int(_mem.key2_longpress_action)
1361
        if tmpval >= len(KEYACTIONS_LIST):
1362
            tmpval = 0
1363
        rs = RadioSetting("key2_longpress_action", "Side key 2 long press",
1364
                          RadioSettingValueList(
1365
                              KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval]))
1366
        keya.append(rs)
1367

    
1368
        # DTMF settings
1369
        tmppr = bool(_mem.dtmf_settings.side_tone > 0)
1370
        rs = RadioSetting(
1371
                "dtmf_side_tone",
1372
                "DTMF Sidetone",
1373
                RadioSettingValueBoolean(tmppr))
1374
        dtmf.append(rs)
1375

    
1376
        tmpval = str(_mem.dtmf_settings.separate_code)
1377
        if tmpval not in DTMF_CODE_CHARS:
1378
            tmpval = '*'
1379
        val = RadioSettingValueString(1, 1, tmpval)
1380
        val.set_charset(DTMF_CODE_CHARS)
1381
        rs = RadioSetting("dtmf_separate_code", "Separate Code", val)
1382
        dtmf.append(rs)
1383

    
1384
        tmpval = str(_mem.dtmf_settings.group_call_code)
1385
        if tmpval not in DTMF_CODE_CHARS:
1386
            tmpval = '#'
1387
        val = RadioSettingValueString(1, 1, tmpval)
1388
        val.set_charset(DTMF_CODE_CHARS)
1389
        rs = RadioSetting("dtmf_group_call_code", "Group Call Code", val)
1390
        dtmf.append(rs)
1391

    
1392
        tmpval = _mem.dtmf_settings.decode_response
1393
        if tmpval >= len(DTMF_DECODE_RESPONSE_LIST):
1394
            tmpval = 0
1395
        rs = RadioSetting("dtmf_decode_response", "Decode Response",
1396
                          RadioSettingValueList(
1397
                              DTMF_DECODE_RESPONSE_LIST,
1398
                              DTMF_DECODE_RESPONSE_LIST[tmpval]))
1399
        dtmf.append(rs)
1400

    
1401
        tmpval = _mem.dtmf_settings.auto_reset_time
1402
        if tmpval > 60 or tmpval < 5:
1403
            tmpval = 5
1404
        rs = RadioSetting("dtmf_auto_reset_time",
1405
                          "Auto reset time (s)",
1406
                          RadioSettingValueInteger(5, 60, tmpval))
1407
        dtmf.append(rs)
1408

    
1409
        tmpval = int(_mem.dtmf_settings.preload_time)
1410
        if tmpval > 100 or tmpval < 3:
1411
            tmpval = 30
1412
        tmpval *= 10
1413
        rs = RadioSetting("dtmf_preload_time",
1414
                          "Pre-load time (ms)",
1415
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1416
        dtmf.append(rs)
1417

    
1418
        tmpval = int(_mem.dtmf_settings.first_code_persist_time)
1419
        if tmpval > 100 or tmpval < 3:
1420
            tmpval = 30
1421
        tmpval *= 10
1422
        rs = RadioSetting("dtmf_first_code_persist_time",
1423
                          "First code persist time (ms)",
1424
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1425
        dtmf.append(rs)
1426

    
1427
        tmpval = int(_mem.dtmf_settings.hash_persist_time)
1428
        if tmpval > 100 or tmpval < 3:
1429
            tmpval = 30
1430
        tmpval *= 10
1431
        rs = RadioSetting("dtmf_hash_persist_time",
1432
                          "#/* persist time (ms)",
1433
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1434
        dtmf.append(rs)
1435

    
1436
        tmpval = int(_mem.dtmf_settings.code_persist_time)
1437
        if tmpval > 100 or tmpval < 3:
1438
            tmpval = 30
1439
        tmpval *= 10
1440
        rs = RadioSetting("dtmf_code_persist_time",
1441
                          "Code persist time (ms)",
1442
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1443
        dtmf.append(rs)
1444

    
1445
        tmpval = int(_mem.dtmf_settings.code_interval_time)
1446
        if tmpval > 100 or tmpval < 3:
1447
            tmpval = 30
1448
        tmpval *= 10
1449
        rs = RadioSetting("dtmf_code_interval_time",
1450
                          "Code interval time (ms)",
1451
                          RadioSettingValueInteger(30, 1000, tmpval, 10))
1452
        dtmf.append(rs)
1453

    
1454
        tmpval = bool(_mem.dtmf_settings.permit_remote_kill > 0)
1455
        rs = RadioSetting(
1456
                "dtmf_permit_remote_kill",
1457
                "Permit remote kill",
1458
                RadioSettingValueBoolean(tmpval))
1459
        dtmf.append(rs)
1460

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

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

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

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

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

    
1535
        for i in range(1, 17):
1536
            varname = "DTMF_"+str(i)
1537
            varnumname = "DTMFNUM_"+str(i)
1538
            vardescr = "DTMF Contact "+str(i)+" name"
1539
            varinumdescr = "DTMF Contact "+str(i)+" number"
1540

    
1541
            cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff")
1542
            cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff")
1543

    
1544
            val = RadioSettingValueString(0, 8, cntn)
1545
            rs = RadioSetting(varname, vardescr, val)
1546
            dtmfc.append(rs)
1547

    
1548
            val = RadioSettingValueString(0, 3, cntnum)
1549
            val.set_charset(DTMF_CHARS)
1550
            rs = RadioSetting(varnumname, varinumdescr, val)
1551
            dtmfc.append(rs)
1552
            rs.set_doc("DTMF Contacts are 3 codes (valid: 0-9 * # ABCD), "
1553
                       "or an empty string")
1554

    
1555
        # scanlists
1556
        if _mem.scanlist_default == 1:
1557
            tmpsc = 2
1558
        else:
1559
            tmpsc = 1
1560
        rs = RadioSetting("scanlist_default",
1561
                          "Default scanlist",
1562
                          RadioSettingValueInteger(1, 2, tmpsc))
1563
        scanl.append(rs)
1564

    
1565
        tmppr = bool((_mem.scanlist1_priority_scan & 1) > 0)
1566
        rs = RadioSetting(
1567
                "scanlist1_priority_scan",
1568
                "Scanlist 1 priority channel scan",
1569
                RadioSettingValueBoolean(tmppr))
1570
        scanl.append(rs)
1571

    
1572
        tmpch = _mem.scanlist1_priority_ch1 + 1
1573
        if tmpch > 200:
1574
            tmpch = 0
1575
        rs = RadioSetting("scanlist1_priority_ch1",
1576
                          "Scanlist 1 priority channel 1 (0 - off)",
1577
                          RadioSettingValueInteger(0, 200, tmpch))
1578
        scanl.append(rs)
1579

    
1580
        tmpch = _mem.scanlist1_priority_ch2 + 1
1581
        if tmpch > 200:
1582
            tmpch = 0
1583
        rs = RadioSetting("scanlist1_priority_ch2",
1584
                          "Scanlist 1 priority channel 2 (0 - off)",
1585
                          RadioSettingValueInteger(0, 200, tmpch))
1586
        scanl.append(rs)
1587

    
1588
        tmppr = bool((_mem.scanlist2_priority_scan & 1) > 0)
1589
        rs = RadioSetting(
1590
                "scanlist2_priority_scan",
1591
                "Scanlist 2 priority channel scan",
1592
                RadioSettingValueBoolean(tmppr))
1593
        scanl.append(rs)
1594

    
1595
        tmpch = _mem.scanlist2_priority_ch1 + 1
1596
        if tmpch > 200:
1597
            tmpch = 0
1598
        rs = RadioSetting("scanlist2_priority_ch1",
1599
                          "Scanlist 2 priority channel 1 (0 - off)",
1600
                          RadioSettingValueInteger(0, 200, tmpch))
1601
        scanl.append(rs)
1602

    
1603
        tmpch = _mem.scanlist2_priority_ch2 + 1
1604
        if tmpch > 200:
1605
            tmpch = 0
1606
        rs = RadioSetting("scanlist2_priority_ch2",
1607
                          "Scanlist 2 priority channel 2 (0 - off)",
1608
                          RadioSettingValueInteger(0, 200, tmpch))
1609
        scanl.append(rs)
1610

    
1611
        # basic settings
1612

    
1613
        # call channel
1614
        tmpc = _mem.call_channel+1
1615
        if tmpc > 200:
1616
            tmpc = 1
1617
        rs = RadioSetting("call_channel", "One key call channel",
1618
                          RadioSettingValueInteger(1, 200, tmpc))
1619
        basic.append(rs)
1620

    
1621
        # squelch
1622
        tmpsq = _mem.squelch
1623
        if tmpsq > 9:
1624
            tmpsq = 1
1625
        rs = RadioSetting("squelch", "Squelch",
1626
                          RadioSettingValueInteger(0, 9, tmpsq))
1627
        basic.append(rs)
1628

    
1629
        # TOT
1630
        tmptot = _mem.max_talk_time
1631
        if tmptot > 10:
1632
            tmptot = 10
1633
        rs = RadioSetting(
1634
                "tot",
1635
                "Max talk time [min]",
1636
                RadioSettingValueInteger(0, 10, tmptot))
1637
        basic.append(rs)
1638

    
1639
        # NOAA autoscan
1640
        rs = RadioSetting(
1641
                "noaa_autoscan",
1642
                "NOAA Autoscan", RadioSettingValueBoolean(
1643
                    bool(_mem.noaa_autoscan > 0)))
1644
        basic.append(rs)
1645

    
1646
        # VOX switch
1647
        rs = RadioSetting(
1648
                "vox_switch",
1649
                "VOX enabled", RadioSettingValueBoolean(
1650
                    bool(_mem.vox_switch > 0)))
1651
        basic.append(rs)
1652

    
1653
        # VOX Level
1654
        tmpvox = _mem.vox_level+1
1655
        if tmpvox > 10:
1656
            tmpvox = 10
1657
        rs = RadioSetting("vox_level", "VOX Level",
1658
                          RadioSettingValueInteger(1, 10, tmpvox))
1659
        basic.append(rs)
1660

    
1661
        # Mic gain
1662
        tmpmicgain = _mem.mic_gain
1663
        if tmpmicgain > 4:
1664
            tmpmicgain = 4
1665
        rs = RadioSetting("mic_gain", "Mic Gain",
1666
                          RadioSettingValueInteger(0, 4, tmpmicgain))
1667
        basic.append(rs)
1668

    
1669
        # Channel display mode
1670
        tmpchdispmode = _mem.channel_display_mode
1671
        if tmpchdispmode >= len(CHANNELDISP_LIST):
1672
            tmpchdispmode = 0
1673
        rs = RadioSetting(
1674
                "channel_display_mode",
1675
                "Channel display mode",
1676
                RadioSettingValueList(
1677
                    CHANNELDISP_LIST,
1678
                    CHANNELDISP_LIST[tmpchdispmode]))
1679
        basic.append(rs)
1680

    
1681
        # Crossband receiving/transmitting
1682
        tmpcross = _mem.crossband
1683
        if tmpcross >= len(CROSSBAND_LIST):
1684
            tmpcross = 0
1685
        rs = RadioSetting(
1686
                "crossband",
1687
                "Cross-band receiving/transmitting",
1688
                RadioSettingValueList(
1689
                    CROSSBAND_LIST,
1690
                    CROSSBAND_LIST[tmpcross]))
1691
        basic.append(rs)
1692

    
1693
        # Battery save
1694
        tmpbatsave = _mem.battery_save
1695
        if tmpbatsave >= len(BATSAVE_LIST):
1696
            tmpbatsave = BATSAVE_LIST.index("1:4")
1697
        rs = RadioSetting(
1698
                "battery_save",
1699
                "Battery Save",
1700
                RadioSettingValueList(
1701
                    BATSAVE_LIST,
1702
                    BATSAVE_LIST[tmpbatsave]))
1703
        basic.append(rs)
1704

    
1705
        # Dual watch
1706
        tmpdual = _mem.dual_watch
1707
        if tmpdual >= len(DUALWATCH_LIST):
1708
            tmpdual = 0
1709
        rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList(
1710
            DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
1711
        basic.append(rs)
1712

    
1713
        # Backlight auto mode
1714
        tmpback = _mem.backlight_auto_mode
1715
        if tmpback >= len(BACKLIGHT_LIST):
1716
            tmpback = 0
1717
        rs = RadioSetting("backlight_auto_mode",
1718
                          "Backlight auto mode",
1719
                          RadioSettingValueList(
1720
                              BACKLIGHT_LIST,
1721
                              BACKLIGHT_LIST[tmpback]))
1722
        basic.append(rs)
1723

    
1724
        # Tail tone elimination
1725
        rs = RadioSetting(
1726
                "tail_note_elimination",
1727
                "Tail tone elimination",
1728
                RadioSettingValueBoolean(
1729
                    bool(_mem.tail_note_elimination > 0)))
1730
        basic.append(rs)
1731

    
1732
        # VFO open
1733
        rs = RadioSetting("vfo_open", "VFO open",
1734
                          RadioSettingValueBoolean(bool(_mem.vfo_open > 0)))
1735
        basic.append(rs)
1736

    
1737
        # Beep control
1738
        rs = RadioSetting(
1739
                "beep_control",
1740
                "Beep control",
1741
                RadioSettingValueBoolean(bool(_mem.beep_control > 0)))
1742
        basic.append(rs)
1743

    
1744
        # Scan resume mode
1745
        tmpscanres = _mem.scan_resume_mode
1746
        if tmpscanres >= len(SCANRESUME_LIST):
1747
            tmpscanres = 0
1748
        rs = RadioSetting(
1749
                "scan_resume_mode",
1750
                "Scan resume mode",
1751
                RadioSettingValueList(
1752
                    SCANRESUME_LIST,
1753
                    SCANRESUME_LIST[tmpscanres]))
1754
        basic.append(rs)
1755

    
1756
        # Keypad locked
1757
        rs = RadioSetting(
1758
                "key_lock",
1759
                "Keypad lock",
1760
                RadioSettingValueBoolean(bool(_mem.key_lock > 0)))
1761
        basic.append(rs)
1762

    
1763
        # Auto keypad lock
1764
        rs = RadioSetting(
1765
                "auto_keypad_lock",
1766
                "Auto keypad lock",
1767
                RadioSettingValueBoolean(bool(_mem.auto_keypad_lock > 0)))
1768
        basic.append(rs)
1769

    
1770
        # Power on display mode
1771
        tmpdispmode = _mem.power_on_dispmode
1772
        if tmpdispmode >= len(WELCOME_LIST):
1773
            tmpdispmode = 0
1774
        rs = RadioSetting(
1775
                "welcome_mode",
1776
                "Power on display mode",
1777
                RadioSettingValueList(
1778
                    WELCOME_LIST,
1779
                    WELCOME_LIST[tmpdispmode]))
1780
        basic.append(rs)
1781

    
1782
        # Keypad Tone
1783
        tmpkeypadtone = _mem.keypad_tone
1784
        if tmpkeypadtone >= len(KEYPADTONE_LIST):
1785
            tmpkeypadtone = 0
1786
        rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList(
1787
            KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone]))
1788
        basic.append(rs)
1789

    
1790
        # Language
1791
        tmplanguage = _mem.language
1792
        if tmplanguage >= len(LANGUAGE_LIST):
1793
            tmplanguage = 0
1794
        rs = RadioSetting("language", "Language", RadioSettingValueList(
1795
            LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage]))
1796
        basic.append(rs)
1797

    
1798
        # Alarm mode
1799
        tmpalarmmode = _mem.alarm_mode
1800
        if tmpalarmmode >= len(ALARMMODE_LIST):
1801
            tmpalarmmode = 0
1802
        rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList(
1803
            ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode]))
1804
        basic.append(rs)
1805

    
1806
        # Reminding of end of talk
1807
        tmpalarmmode = _mem.reminding_of_end_talk
1808
        if tmpalarmmode >= len(REMENDOFTALK_LIST):
1809
            tmpalarmmode = 0
1810
        rs = RadioSetting(
1811
                "reminding_of_end_talk",
1812
                "Reminding of end of talk",
1813
                RadioSettingValueList(
1814
                    REMENDOFTALK_LIST,
1815
                    REMENDOFTALK_LIST[tmpalarmmode]))
1816
        basic.append(rs)
1817

    
1818
        # Repeater tail tone elimination
1819
        tmprte = _mem.repeater_tail_elimination
1820
        if tmprte >= len(RTE_LIST):
1821
            tmprte = 0
1822
        rs = RadioSetting(
1823
                "repeater_tail_elimination",
1824
                "Repeater tail tone elimination",
1825
                RadioSettingValueList(RTE_LIST, RTE_LIST[tmprte]))
1826
        basic.append(rs)
1827

    
1828
        # Logo string 1
1829
        logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00"
1830
        logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12)
1831
        rs = RadioSetting("logo1", _("Logo string 1 (12 characters)"),
1832
                          RadioSettingValueString(0, 12, logo1))
1833
        basic.append(rs)
1834

    
1835
        # Logo string 2
1836
        logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00"
1837
        logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12)
1838
        rs = RadioSetting("logo2", _("Logo string 2 (12 characters)"),
1839
                          RadioSettingValueString(0, 12, logo2))
1840
        basic.append(rs)
1841

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

    
1853
            fmradio.append(rs)
1854

    
1855
        # unlock settings
1856

    
1857
        # F-LOCK
1858
        tmpflock = _mem.lock.flock
1859
        if tmpflock >= len(FLOCK_LIST):
1860
            tmpflock = 0
1861
        rs = RadioSetting(
1862
            "flock", "F-LOCK",
1863
            RadioSettingValueList(FLOCK_LIST, FLOCK_LIST[tmpflock]))
1864
        unlock.append(rs)
1865

    
1866
        # 350TX
1867
        rs = RadioSetting("tx350", "350TX - unlock 350-400 MHz TX",
1868
                          RadioSettingValueBoolean(
1869
                              bool(_mem.lock.tx350 > 0)))
1870
        unlock.append(rs)
1871

    
1872
        # Killed
1873
        rs = RadioSetting("Killed", "KILLED Device was disabled (via DTMF)",
1874
                          RadioSettingValueBoolean(
1875
                              bool(_mem.lock.killed > 0)))
1876
        unlock.append(rs)
1877

    
1878
        # 200TX
1879
        rs = RadioSetting("tx200", "200TX - unlock 174-350 MHz TX",
1880
                          RadioSettingValueBoolean(
1881
                              bool(_mem.lock.tx200 > 0)))
1882
        unlock.append(rs)
1883

    
1884
        # 500TX
1885
        rs = RadioSetting("tx500", "500TX - unlock 500-600 MHz TX",
1886
                          RadioSettingValueBoolean(
1887
                              bool(_mem.lock.tx500 > 0)))
1888
        unlock.append(rs)
1889

    
1890
        # 350EN
1891
        rs = RadioSetting("en350", "350EN - unlock 350-400 MHz RX",
1892
                          RadioSettingValueBoolean(
1893
                              bool(_mem.lock.en350 > 0)))
1894
        unlock.append(rs)
1895

    
1896
        # SCREEN
1897
        rs = RadioSetting("scrambler", "SCREN - scrambler enable",
1898
                          RadioSettingValueBoolean(
1899
                              bool(_mem.lock.enscramble > 0)))
1900
        unlock.append(rs)
1901

    
1902
        # readonly info
1903
        # Firmware
1904
        firmware = self.metadata.get('uvk5_firmware', 'UNKNOWN')
1905

    
1906
        val = RadioSettingValueString(0, 128, firmware)
1907
        val.set_mutable(False)
1908
        rs = RadioSetting("fw_ver", "Firmware Version", val)
1909
        roinfo.append(rs)
1910

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

    
1923
        return top
1924

    
1925
    def _set_mem_mode(self, _mem, mode):
1926
        if mode == "NFM":
1927
            _mem.bandwidth = 1
1928
            _mem.enable_am = 0
1929
        elif mode == "FM":
1930
            _mem.bandwidth = 0
1931
            _mem.enable_am = 0
1932
        elif mode == "NAM":
1933
            _mem.bandwidth = 1
1934
            _mem.enable_am = 1
1935
        elif mode == "AM":
1936
            _mem.bandwidth = 0
1937
            _mem.enable_am = 1
1938

    
1939
    # Store details about a high-level memory to the memory map
1940
    # This is called when a user edits a memory in the UI
1941
    def set_memory(self, mem):
1942
        number = mem.number-1
1943

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

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

    
1978
        if number < 200:
1979
            _mem4.channel_attributes[number].is_scanlist1 = 0
1980
            _mem4.channel_attributes[number].is_scanlist2 = 0
1981
            _mem4.channel_attributes[number].compander = 0
1982
            _mem4.channel_attributes[number].is_free = 1
1983
            _mem4.channel_attributes[number].band = 0x7
1984

    
1985
        # find band
1986
        band = _find_band(self, mem.freq)
1987

    
1988
        self._set_mem_mode(_mem, mem.mode)
1989

    
1990
        # frequency/offset
1991
        _mem.freq = mem.freq/10
1992
        _mem.offset = mem.offset/10
1993

    
1994
        if mem.duplex == "":
1995
            _mem.offset = 0
1996
            _mem.shift = 0
1997
        elif mem.duplex == '-':
1998
            _mem.shift = FLAGS1_OFFSET_MINUS
1999
        elif mem.duplex == '+':
2000
            _mem.shift = FLAGS1_OFFSET_PLUS
2001
        elif mem.duplex == 'off':
2002
            # we fake tx disable by setting the tx freq to 0 MHz
2003
            _mem.shift = FLAGS1_OFFSET_MINUS
2004
            _mem.offset = _mem.freq
2005

    
2006
        # set band
2007
        if number < 200:
2008
            _mem4.channel_attributes[number].is_free = 0
2009
            _mem4.channel_attributes[number].band = band
2010

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

    
2017
        # tone data
2018
        self._set_tone(mem, _mem)
2019

    
2020
        # step
2021
        _mem.step = self._steps.index(mem.tuning_step)
2022

    
2023
        # tx power
2024
        if str(mem.power) == str(UVK5_POWER_LEVELS[2]):
2025
            _mem.txpower = POWER_HIGH
2026
        elif str(mem.power) == str(UVK5_POWER_LEVELS[1]):
2027
            _mem.txpower = POWER_MEDIUM
2028
        else:
2029
            _mem.txpower = POWER_LOW
2030

    
2031
        for setting in mem.extra:
2032
            sname = setting.get_name()
2033
            svalue = setting.value.get_value()
2034

    
2035
            if sname == "bclo":
2036
                _mem.bclo = svalue and 1 or 0
2037

    
2038
            if sname == "pttid":
2039
                _mem.dtmf_pttid = self._pttid_list.index(svalue)
2040

    
2041
            if sname == "frev":
2042
                _mem.freq_reverse = svalue and 1 or 0
2043

    
2044
            if sname == "dtmfdecode":
2045
                _mem.dtmf_decode = svalue and 1 or 0
2046

    
2047
            if sname == "scrambler":
2048
                _mem.scrambler = (
2049
                    _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
2050

    
2051
            if number < 200 and sname == "scanlists":
2052
                if svalue == "1":
2053
                    _mem4.channel_attributes[number].is_scanlist1 = 1
2054
                    _mem4.channel_attributes[number].is_scanlist2 = 0
2055
                elif svalue == "2":
2056
                    _mem4.channel_attributes[number].is_scanlist1 = 0
2057
                    _mem4.channel_attributes[number].is_scanlist2 = 1
2058
                elif svalue == "1+2":
2059
                    _mem4.channel_attributes[number].is_scanlist1 = 1
2060
                    _mem4.channel_attributes[number].is_scanlist2 = 1
2061
                else:
2062
                    _mem4.channel_attributes[number].is_scanlist1 = 0
2063
                    _mem4.channel_attributes[number].is_scanlist2 = 0
2064

    
2065
        return mem
2066

    
2067

    
2068
@directory.register
2069
class UVK5Radio(UVK5RadioBase):
2070
    @classmethod
2071
    def k5_approve_firmware(cls, firmware):
2072
        approved_prefixes = ('k5_2.01.', 'app_2.01.', '2.01.',
2073
                             '1o11', '4.00.')
2074
        return any(firmware.startswith(x) for x in approved_prefixes)
2075

    
2076

    
2077
@directory.register
2078
class RA79Radio(UVK5Radio):
2079
    """Retevis RA79"""
2080
    VENDOR = "Retevis"
2081
    MODEL = "RA79"
(1-1/4)