Project

General

Profile

New Model #10478 » uvk5.py

uvk5.py chirp driver for UV-K5, version 20230613 - Jacek Lipkowski SQ5BPF, 06/13/2023 02:16 AM

 
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. Before even attempting
12
# to use it save a memory image from the radio using k5prog:
13
# https://github.com/sq5bpf/k5prog
14
#
15
#
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation, either version 2 of the License, or
19
# (at your option) any later version.
20
#
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
# GNU General Public License for more details.
25
#
26
# You should have received a copy of the GNU General Public License
27
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
28

    
29

    
30
import struct
31
import logging
32

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

    
39
LOG = logging.getLogger(__name__)
40

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

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

    
50
DRIVER_VERSION = "Quansheng UV-K5 driver v20230613 (c) Jacek Lipkowski SQ5BPF"
51
PRINT_CONSOLE = False
52

    
53
MEM_FORMAT = """
54
#seekto 0x0000;
55
struct {
56
  ul32 freq;
57
  ul32 offset;
58
  u8 rxcode;
59
  u8 txcode;
60
  u8 code_flag;
61
  u8 flags1;
62
  u8 flags2;
63
  u8 dtmf_flags;
64
  u8 step;
65
  u8 scrambler;
66
} channel[214];
67

    
68
#seekto 0xd60;
69
struct {
70
u8 is_scanlist1:1,
71
is_scanlist2:1,
72
unknown1:1,
73
unknown2:1,
74
is_free:1,
75
band:3;
76
} channel_attributes[200];
77

    
78
#seekto 0xe40;
79
ul16 fmfreq[20];
80

    
81
#seekto 0xe70;
82
u8 call_channel;
83
u8 squelch;
84
u8 max_talk_time;
85
u8 noaa_autoscan;
86
u8 unknown1;
87
u8 unknown2;
88
u8 vox_level;
89
u8 mic_gain;
90
u8 unknown3;
91
u8 channel_display_mode;
92
u8 crossband;
93
u8 battery_save;
94
u8 dual_watch;
95
u8 tail_note_elimination;
96
u8 vfo_open;
97

    
98
#seekto 0xe90;
99
u8 beep_control;
100
#seekto 0xe95;
101
u8 scan_resume_mode;
102
u8 auto_keypad_lock;
103
u8 power_on_dispmode;
104
u8 password[4];
105

    
106
#seekto 0xea0;
107
u8 keypad_tone;
108
u8 language;
109

    
110
#seekto 0xea8;
111
u8 alarm_mode;
112
u8 reminding_of_end_talk;
113
u8 repeater_tail_elimination;
114

    
115
#seekto 0xeb0;
116
char logo_line1[16];
117
char logo_line2[16];
118

    
119
#seekto 0xf18;
120
u8 scanlist_default;
121
u8 scanlist1_priority_scan;
122
u8 scanlist1_priority_ch1;
123
u8 scanlist1_priority_ch2;
124
u8 scanlist2_priority_scan;
125
u8 scanlist2_priority_ch1;
126
u8 scanlist2_priority_ch2;
127
u8 scanlist_unknown_0xff;
128

    
129

    
130
#seekto 0xf40;
131
u8 int_flock;
132
u8 int_350tx;
133
u8 int_unknown1;
134
u8 int_200tx;
135
u8 int_500tx;
136
u8 int_350en;
137
u8 int_screen;
138

    
139
#seekto 0xf50;
140
struct {
141
char name[16];
142
} channelname[200];
143

    
144
"""
145
# bits that we will save from the channel structure (mostly unknown)
146
SAVE_MASK_0A = 0b11001100
147
SAVE_MASK_0B = 0b11101100
148
SAVE_MASK_0C = 0b11100000
149
SAVE_MASK_0D = 0b11111000
150
SAVE_MASK_0E = 0b11110001
151
SAVE_MASK_0F = 0b11110000
152

    
153
# flags1
154
FLAGS1_OFFSET_MASK = 0b11
155
FLAGS1_OFFSET_NONE = 0b00
156
FLAGS1_OFFSET_MINUS = 0b10
157
FLAGS1_OFFSET_PLUS = 0b01
158

    
159
FLAGS1_ISSCANLIST = 0b100
160
FLAGS1_ISAM = 0b10000
161

    
162
# flags2
163
FLAGS2_BCLO = 0b10000
164
FLAGS2_POWER_MASK = 0b1100
165
FLAGS2_POWER_HIGH = 0b1000
166
FLAGS2_POWER_MEDIUM = 0b0100
167
FLAGS2_POWER_LOW = 0b0000
168
FLAGS2_BANDWIDTH = 0b10
169
FLAGS2_REVERSE = 0b1
170

    
171
# dtmf_flags
172
PTTID_LIST = ["off", "BOT", "EOT", "BOTH"]
173
FLAGS_DTMF_PTTID_MASK = 0b110  # PTTID: 00-disabled, 01-BOT, 10-EOT, 11-BOTH
174
FLAGS_DTMF_PTTID_DISABLED = 0b000
175
FLAGS_DTMF_PTTID_BOT = 0b010
176
FLAGS_DTMF_PTTID_EOT = 0b100
177
FLAGS_DTMF_PTTID_BOTH = 0b110
178
FLAGS_DTMF_DECODE = 0b1
179

    
180
# power
181
UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low",  watts=1.50),
182
                     chirp_common.PowerLevel("Med",  watts=3.00),
183
                     chirp_common.PowerLevel("High", watts=5.00),
184
                     ]
185

    
186
# scrambler
187
SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
188

    
189
# channel display mode
190
CHANNELDISP_LIST = ["Frequency", "Channel No", "Channel Name"]
191
# battery save
192
BATSAVE_LIST = ["OFF", "1:1", "1:2", "1:3", "1:4"]
193

    
194
# Crossband receiving/transmitting
195
CROSSBAND_LIST = ["Off", "Band A", "Band B"]
196
DUALWATCH_LIST = CROSSBAND_LIST
197

    
198
# steps
199
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 8.33]
200

    
201
# ctcss/dcs codes
202
TMODES = ["", "Tone", "DTCS", "DTCS"]
203
TONE_NONE = 0
204
TONE_CTCSS = 1
205
TONE_DCS = 2
206
TONE_RDCS = 3
207

    
208

    
209
CTCSS_TONES = [
210
    67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4,
211
    88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9,
212
    114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2,
213
    151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
214
    177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
215
    203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8,
216
    250.3, 254.1
217
]
218

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

    
233
FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"]
234
SCANRESUME_LIST = ["TO: Resume after 5 seconds",
235
                   "CO: Resume after signal dissapears",
236
                   "SE: Stop scanning after receiving a signal"]
237
WELCOME_LIST = ["Full Screen", "Welcome Info", "Voltage"]
238
KEYPADTONE_LIST = ["Off", "Chinese", "English"]
239
LANGUAGE_LIST = ["Chinese", "English"]
240
ALARMMODE_LIST = ["SITE", "TONE"]
241
REMENDOFTALK_LIST = ["Off", "ROGER", "MDC"]
242
RTE_LIST = ["Off", "100ms", "200ms", "300ms", "400ms",
243
            "500ms", "600ms", "700ms", "800ms", "900ms"]
244

    
245
MEM_SIZE = 0x2000  # size of all memory
246
PROG_SIZE = 0x1d00  # size of the memory that we will write
247
MEM_BLOCK = 0x80  # largest block of memory that we can reliably write
248

    
249
# fm radio supported frequencies
250
FMMIN = 76.0
251
FMMAX = 108.0
252

    
253
# bands supported by the UV-K5
254
BANDS = {
255
        0: [50.0, 76.0],
256
        1: [108.0, 135.9999],
257
        2: [136.0, 199.9990],
258
        3: [200.0, 299.9999],
259
        4: [350.0, 399.9999],
260
        5: [400.0, 469.9999],
261
        6: [470.0, 600.0]
262
        }
263

    
264
# for radios with modified firmware:
265
BANDS_NOLIMITS = {
266
        0: [18.0, 76.0],
267
        1: [108.0, 135.9999],
268
        2: [136.0, 199.9990],
269
        3: [200.0, 299.9999],
270
        4: [350.0, 399.9999],
271
        5: [400.0, 469.9999],
272
        6: [470.0, 1300.0]
273
        }
274
BANDMASK = 0b1111
275

    
276
VFO_CHANNEL_NAMES = ["F1(50M-76M)A", "F1(50M-76M)B",
277
                     "F2(108M-136M)A", "F2(108M-136M)B",
278
                     "F3(136M-174M)A", "F3(136M-174M)B",
279
                     "F4(174M-350M)A", "F4(174M-350M)B",
280
                     "F5(350M-400M)A", "F5(350M-400M)B",
281
                     "F6(400M-470M)A", "F6(400M-470M)B",
282
                     "F7(470M-600M)A", "F7(470M-600M)B"]
283

    
284
SCANLIST_LIST = ["None", "1", "2", "1+2"]
285

    
286

    
287
# the communication is obfuscated using this fine mechanism
288
def xorarr(data: bytes):
289
    tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128]
290
    x = b""
291
    r = 0
292
    for byte in data:
293
        x += bytes([byte ^ tbl[r]])
294
        r = (r+1) % len(tbl)
295
    return x
296

    
297

    
298
# if this crc was used for communication to AND from the radio, then it
299
# would be a measure to increase reliability.
300
# but it's only used towards the radio, so it's for further obfuscation
301
def calculate_crc16_xmodem(data: bytes):
302
    poly = 0x1021
303
    crc = 0x0
304
    for byte in data:
305
        crc = crc ^ (byte << 8)
306
        for i in range(8):
307
            crc = crc << 1
308
            if (crc & 0x10000):
309
                crc = (crc ^ poly) & 0xFFFF
310
    return crc & 0xFFFF
311

    
312

    
313
def _send_command(serport, data: bytes):
314
    """Send a command to UV-K5 radio"""
315
    LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s" %
316
              (len(data), util.hexprint(data)))
317

    
318
    crc = calculate_crc16_xmodem(data)
319
    data2 = data + struct.pack("<H", crc)
320

    
321
    command = struct.pack(">HBB", 0xabcd, len(data), 0) + \
322
        xorarr(data2) + \
323
        struct.pack(">H", 0xdcba)
324
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
325
        LOG.debug("Sending command (obfuscated):\n%s" % util.hexprint(command))
326
    try:
327
        result = serport.write(command)
328
    except Exception:
329
        raise errors.RadioError("Error writing data to radio")
330
    return result
331

    
332

    
333
def _receive_reply(serport):
334
    header = serport.read(4)
335
    if len(header) != 4:
336
        LOG.warning("Header short read: [%s] len=%i" %
337
                    (util.hexprint(header), len(header)))
338
        raise errors.RadioError("Header short read")
339
    if header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00:
340
        LOG.warning("Bad response header: %s len=%i" %
341
                    (util.hexprint(header), len(header)))
342
        raise errors.RadioError("Bad response header")
343

    
344
        return False
345

    
346
    cmd = serport.read(int(header[2]))
347
    if len(cmd) != int(header[2]):
348
        LOG.warning("Body short read: [%s] len=%i" %
349
                    (util.hexprint(cmd), len(cmd)))
350
        raise errors.RadioError("Command body short read")
351

    
352
    footer = serport.read(4)
353

    
354
    if len(footer) != 4:
355
        LOG.warning("Footer short read: [%s] len=%i" %
356
                    (util.hexprint(footer), len(footer)))
357
        raise errors.RadioError("Footer short read")
358

    
359
    if footer[2] != 0xDC or footer[3] != 0xBA:
360
        LOG.debug(
361
                "Reply before bad response footer (obfuscated)"
362
                "len=0x%4.4x:\n%s" % (len(cmd), util.hexprint(cmd)))
363
        LOG.warning("Bad response footer: %s len=%i" %
364
                    (util.hexprint(footer), len(footer)))
365
        raise errors.RadioError("Bad response footer")
366
        return False
367

    
368
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
369
        LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s" %
370
                  (len(cmd), util.hexprint(cmd)))
371

    
372
    cmd2 = xorarr(cmd)
373

    
374
    LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s" %
375
              (len(cmd2), util.hexprint(cmd2)))
376

    
377
    return cmd2
378

    
379

    
380
def _getstring(data: bytes, begin, maxlen):
381
    tmplen = min(maxlen+1, len(data))
382
    s = [data[i] for i in range(begin, tmplen)]
383
    for key, val in enumerate(s):
384
        if val < ord(' ') or val > ord('~'):
385
            break
386
    return ''.join(chr(x) for x in s[0:key])
387

    
388

    
389
def _sayhello(serport):
390
    hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64"
391

    
392
    tries = 5
393
    while (True):
394
        LOG.debug("Sending hello packet")
395
        _send_command(serport, hellopacket)
396
        o = _receive_reply(serport)
397
        if (o):
398
            break
399
        tries -= 1
400
        if tries == 0:
401
            LOG.warning("Failed to initialise radio")
402
            raise errors.RadioError("Failed to initialize radio")
403
            return False
404
    firmware = _getstring(o, 4, 16)
405
    LOG.info("Found firmware: %s" % firmware)
406
    return firmware
407

    
408

    
409
def _readmem(serport, offset, length):
410
    LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x" % (offset, length))
411

    
412
    readmem = b"\x1b\x05\x08\x00" + \
413
        struct.pack("<HBB", offset, length, 0) + \
414
        b"\x6a\x39\x57\x64"
415
    _send_command(serport, readmem)
416
    o = _receive_reply(serport)
417
    if DEBUG_SHOW_MEMORY_ACTIONS:
418
        LOG.debug("readmem Received data len=0x%4.4x:\n%s" %
419
                  (len(o), util.hexprint(o)))
420
    return o[8:]
421

    
422

    
423
def _writemem(serport, data, offset):
424
    LOG.debug("Sending writemem offset=0x%4.4x len=0x%4.4x" %
425
              (offset, len(data)))
426

    
427
    if DEBUG_SHOW_MEMORY_ACTIONS:
428
        LOG.debug("writemem sent data offset=0x%4.4x len=0x%4.4x:\n%s" %
429
                  (offset, len(data), util.hexprint(data)))
430

    
431
    dlen = len(data)
432
    writemem = b"\x1d\x05" + \
433
        struct.pack("<BBHBB", dlen+8, 0, offset, dlen, 1) + \
434
        b"\x6a\x39\x57\x64"+data
435

    
436
    _send_command(serport, writemem)
437
    o = _receive_reply(serport)
438

    
439
    LOG.debug("writemem Received data: %s len=%i" % (util.hexprint(o), len(o)))
440

    
441
    if (o[0] == 0x1e
442
            and
443
            o[4] == (offset & 0xff)
444
            and
445
            o[5] == (offset >> 8) & 0xff):
446
        return True
447
    else:
448
        LOG.warning("Bad data from writemem")
449
        raise errors.RadioError("Bad response to writemem")
450
    return False
451

    
452

    
453
def _resetradio(serport):
454
    resetpacket = b"\xdd\x05\x00\x00"
455
    _send_command(serport, resetpacket)
456

    
457

    
458
def do_download(radio):
459
    serport = radio.pipe
460
    serport.timeout = 0.5
461
    status = chirp_common.Status()
462
    status.cur = 0
463
    status.max = MEM_SIZE
464
    status.msg = "Downloading from radio"
465
    radio.status_fn(status)
466

    
467
    eeprom = b""
468
    f = _sayhello(serport)
469
    if f:
470
        radio.FIRMWARE_VERSION = f
471
    else:
472
        return False
473

    
474
    addr = 0
475
    while addr < MEM_SIZE:
476
        o = _readmem(serport, addr, MEM_BLOCK)
477
        status.cur = addr
478
        radio.status_fn(status)
479

    
480
        if o and len(o) == MEM_BLOCK:
481
            eeprom += o
482
            addr += MEM_BLOCK
483
        else:
484
            raise errors.RadioError("Memory download incomplete")
485

    
486
    return memmap.MemoryMapBytes(eeprom)
487

    
488

    
489
def do_upload(radio):
490
    serport = radio.pipe
491
    serport.timeout = 0.5
492
    status = chirp_common.Status()
493
    status.cur = 0
494
    status.max = PROG_SIZE
495
    status.msg = "Uploading to radio"
496
    radio.status_fn(status)
497

    
498
    f = _sayhello(serport)
499
    if f:
500
        radio.FIRMWARE_VERSION = f
501
    else:
502
        return False
503

    
504
    addr = 0
505
    while addr < PROG_SIZE:
506
        o = radio.get_mmap()[addr:addr+MEM_BLOCK]
507
        _writemem(serport, o, addr)
508
        status.cur = addr
509
        radio.status_fn(status)
510
        if o:
511
            addr += MEM_BLOCK
512
        else:
513
            raise errors.RadioError("Memory upload incomplete")
514
    status.msg = "Uploaded OK"
515

    
516
    _resetradio(serport)
517

    
518
    return True
519

    
520

    
521
def _find_band(self, hz):
522
    mhz = hz/1000000.0
523
    if self.FIRMWARE_NOLIMITS:
524
        B = BANDS_NOLIMITS
525
    else:
526
        B = BANDS
527
    for a in B:
528
        if mhz >= B[a][0] and mhz <= B[a][1]:
529
            return a
530
    return False
531

    
532

    
533
@directory.register
534
class UVK5Radio(chirp_common.CloneModeRadio):
535
    """Quansheng UV-K5"""
536
    VENDOR = "Quansheng"
537
    MODEL = "UV-K5"
538
    BAUD_RATE = 38400
539
    NEEDS_COMPAT_SERIAL = False
540
    FIRMWARE_VERSION = ""
541
    FIRMWARE_NOLIMITS = False
542

    
543
    def get_prompts(x=None):
544
        rp = chirp_common.RadioPrompts()
545
        rp.experimental = \
546
            ('This is an experimental driver for the Quanscheng UV-K5. '
547
             'It may harm your radio, or worse. Use at your own risk.\n\n'
548
             'Before attempting to do any changes please download'
549
             'the memory image from the radio with chirp or k5prog '
550
             'and keep it. This can be later used to recover the '
551
             'original settings. \n\n'
552
             'DTMF settings and other details are not yet implemented')
553
        rp.pre_download = _(
554
            "1. Turn radio on.\n"
555
            "2. Connect cable to mic/spkr connector.\n"
556
            "3. Make sure connector is firmly connected.\n"
557
            "4. Click OK to download image from device.\n\n"
558
            "It will may not work if you turn o the radio "
559
            "with the cable already attached\n")
560
        rp.pre_upload = _(
561
            "1. Turn radio on.\n"
562
            "2. Connect cable to mic/spkr connector.\n"
563
            "3. Make sure connector is firmly connected.\n"
564
            "4. Click OK to upload the image to device.\n\n"
565
            "It will may not work if you turn o the radio "
566
            "with the cable already attached")
567
        return rp
568

    
569
    # Return information about this radio's features, including
570
    # how many memories it has, what bands it supports, etc
571
    def get_features(self):
572
        rf = chirp_common.RadioFeatures()
573
        rf.has_bank = False
574
        rf.valid_dtcs_codes = DTCS_CODES
575
        rf.has_rx_dtcs = True
576
        rf.has_ctone = True
577
        rf.has_settings = True
578
        rf.has_comment = False
579
        rf.valid_name_length = 16
580
        rf.valid_power_levels = UVK5_POWER_LEVELS
581

    
582
        # hack so we can input any frequency,
583
        # the 0.1 and 0.01 steps don't work unfortunately
584
        rf.valid_tuning_steps = [0.01, 0.1, 1.0] + STEPS
585

    
586
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
587
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
588
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
589

    
590
        rf.valid_characters = chirp_common.CHARSET_ASCII
591
        rf.valid_modes = ["FM", "NFM", "AM", "NAM"]
592
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
593

    
594
        rf.valid_skips = [""]
595

    
596
        # This radio supports memories 1-200, 201-214 are the VFO memories
597
        rf.memory_bounds = (1, 214)
598

    
599
        # This is what the BK4819 chip supports
600
        # Will leave it in a comment, might be useful someday
601
        # rf.valid_bands = [(18000000,  620000000),
602
        #                  (840000000, 1300000000)
603
        #                  ]
604
        rf.valid_bands = []
605
        for a in BANDS:
606
            rf.valid_bands.append(
607
                    (int(BANDS[a][0]*1000000), int(BANDS[a][1]*1000000)))
608
        return rf
609

    
610
    # Do a download of the radio from the serial port
611
    def sync_in(self):
612
        self._mmap = do_download(self)
613
        self.process_mmap()
614

    
615
    # Do an upload of the radio to the serial port
616
    def sync_out(self):
617
        do_upload(self)
618

    
619
    # Convert the raw byte array into a memory object structure
620
    def process_mmap(self):
621
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
622

    
623
    # Return a raw representation of the memory object, which
624
    # is very helpful for development
625
    def get_raw_memory(self, number):
626
        return repr(self._memobj.channel[number-1])
627

    
628
    def validate_memory(self, mem):
629
        msgs = super().validate_memory(mem)
630
        return msgs
631

    
632
    def _set_tone(self, mem, _mem):
633
        ((txmode, txtone, txpol),
634
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
635

    
636
        if txmode == "Tone":
637
            txtoval = CTCSS_TONES.index(txtone)
638
            txmoval = 0b01
639
        elif txmode == "DTCS":
640
            txmoval = txpol == "R" and 0b11 or 0b10
641
            txtoval = DTCS_CODES.index(txtone)
642
        else:
643
            txmoval = 0
644
            txtoval = 0
645

    
646
        if rxmode == "Tone":
647
            rxtoval = CTCSS_TONES.index(rxtone)
648
            rxmoval = 0b01
649
        elif rxmode == "DTCS":
650
            rxmoval = rxpol == "R" and 0b11 or 0b10
651
            rxtoval = DTCS_CODES.index(rxtone)
652
        else:
653
            rxmoval = 0
654
            rxtoval = 0
655

    
656
        _mem.code_flag = (_mem.code_flag & 0b11001100) | (
657
            txmoval << 4) | rxmoval
658
        _mem.rxcode = rxtoval
659
        _mem.txcode = txtoval
660

    
661
    def _get_tone(self, mem, _mem):
662
        rxtype = _mem.code_flag & 0x03
663
        txtype = (_mem.code_flag >> 4) & 0x03
664
        rx_tmode = TMODES[rxtype]
665
        tx_tmode = TMODES[txtype]
666

    
667
        rx_tone = tx_tone = None
668

    
669
        if tx_tmode == "Tone":
670
            if _mem.txcode < len(CTCSS_TONES):
671
                tx_tone = CTCSS_TONES[_mem.txcode]
672
            else:
673
                tx_tone = 0
674
                tx_tmode = ""
675
        elif tx_tmode == "DTCS":
676
            if _mem.txcode < len(DTCS_CODES):
677
                tx_tone = DTCS_CODES[_mem.txcode]
678
            else:
679
                tx_tone = 0
680
                tx_tmode = ""
681

    
682
        if rx_tmode == "Tone":
683
            if _mem.rxcode < len(CTCSS_TONES):
684
                rx_tone = CTCSS_TONES[_mem.rxcode]
685
            else:
686
                rx_tone = 0
687
                rx_tmode = ""
688
        elif rx_tmode == "DTCS":
689
            if _mem.rxcode < len(DTCS_CODES):
690
                rx_tone = DTCS_CODES[_mem.rxcode]
691
            else:
692
                rx_tone = 0
693
                rx_tmode = ""
694

    
695
        tx_pol = txtype == 0x03 and "R" or "N"
696
        rx_pol = rxtype == 0x03 and "R" or "N"
697

    
698
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
699
                                       (rx_tmode, rx_tone, rx_pol))
700

    
701
    # Extract a high-level memory object from the low-level memory map
702
    # This is called to populate a memory in the UI
703
    def get_memory(self, number2):
704
        number = number2-1  # in the radio memories start with 0
705

    
706
        mem = chirp_common.Memory()
707

    
708
        # cutting and pasting configs from different radios
709
        # might try to set channel 0
710
        if number2 == 0:
711
            LOG.warning("Attempt to get channel 0")
712
            return mem
713

    
714
        _mem = self._memobj.channel[number]
715

    
716
        tmpcomment = ""
717

    
718
        mem.number = number2
719

    
720
        is_empty = False
721
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
722
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
723
            is_empty = True
724

    
725
        tmpscn = SCANLIST_LIST[0]
726

    
727
        # We'll also look at the channel attributes if a memory has them
728
        if number < 200:
729
            _mem3 = self._memobj.channel_attributes[number]
730
            # free memory bit
731
            if _mem3.is_free > 0:
732
                is_empty = True
733
            # scanlists
734
            if _mem3.is_scanlist1 > 0 and _mem3.is_scanlist2 > 0:
735
                tmpscn = SCANLIST_LIST[3]  # "1+2"
736
            elif _mem3.is_scanlist1 > 0:
737
                tmpscn = SCANLIST_LIST[1]  # "1"
738
            elif _mem3.is_scanlist2 > 0:
739
                tmpscn = SCANLIST_LIST[2]  # "2"
740

    
741
        if is_empty:
742
            mem.empty = True
743
            # set some sane defaults:
744
            mem.power = UVK5_POWER_LEVELS[2]
745
            mem.extra = RadioSettingGroup("Extra", "extra")
746
            rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(False))
747
            mem.extra.append(rs)
748
            rs = RadioSetting("frev", "FreqRev",
749
                              RadioSettingValueBoolean(False))
750
            mem.extra.append(rs)
751
            rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(
752
                PTTID_LIST, PTTID_LIST[0]))
753
            mem.extra.append(rs)
754
            rs = RadioSetting("dtmfdecode", "DTMF decode",
755
                              RadioSettingValueBoolean(False))
756
            mem.extra.append(rs)
757
            rs = RadioSetting("scrambler", "Scrambler", RadioSettingValueList(
758
                SCRAMBLER_LIST, SCRAMBLER_LIST[0]))
759
            mem.extra.append(rs)
760

    
761
            rs = RadioSetting("scanlists", "Scanlists", RadioSettingValueList(
762
                SCANLIST_LIST, SCANLIST_LIST[0]))
763
            mem.extra.append(rs)
764

    
765
            # actually the step and duplex are overwritten by chirp based on
766
            # bandplan. they are here to document sane defaults for IARU r1
767
            # mem.tuning_step = 25.0
768
            # mem.duplex = "off"
769

    
770
            return mem
771

    
772
        if number > 199:
773
            mem.name = VFO_CHANNEL_NAMES[number-200]
774
            mem.immutable = ["name", "scanlists"]
775
        else:
776
            _mem2 = self._memobj.channelname[number]
777
            for char in _mem2.name:
778
                if str(char) == "\xFF" or str(char) == "\x00":
779
                    break
780
                mem.name += str(char)
781
            mem.name = mem.name.rstrip()
782

    
783
        # Convert your low-level frequency to Hertz
784
        mem.freq = int(_mem.freq)*10
785
        mem.offset = int(_mem.offset)*10
786

    
787
        if (mem.offset == 0):
788
            mem.duplex = ''
789
        else:
790
            if (_mem.flags1 & FLAGS1_OFFSET_MASK) == FLAGS1_OFFSET_MINUS:
791
                mem.duplex = '-'
792
            elif (_mem.flags1 & FLAGS1_OFFSET_MASK) == FLAGS1_OFFSET_PLUS:
793
                mem.duplex = '+'
794
            else:
795
                mem.duplex = ''
796

    
797
        # tone data
798
        self._get_tone(mem, _mem)
799

    
800
        # mode
801
        if (_mem.flags1 & FLAGS1_ISAM) > 0:
802
            if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0:
803
                mem.mode = "NAM"
804
            else:
805
                mem.mode = "AM"
806
        else:
807
            if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0:
808
                mem.mode = "NFM"
809
            else:
810
                mem.mode = "FM"
811

    
812
        # tuning step
813
        tstep = _mem.step & 0x7
814
        if tstep < len(STEPS):
815
            mem.tuning_step = STEPS[tstep]
816
        else:
817
            mem.tuning_step = 2.5
818

    
819
        # power
820
        if (_mem.flags2 & FLAGS2_POWER_MASK) == FLAGS2_POWER_HIGH:
821
            mem.power = UVK5_POWER_LEVELS[2]
822
        elif (_mem.flags2 & FLAGS2_POWER_MASK) == FLAGS2_POWER_MEDIUM:
823
            mem.power = UVK5_POWER_LEVELS[1]
824
        else:
825
            mem.power = UVK5_POWER_LEVELS[0]
826

    
827
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
828
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
829
            mem.empty = True
830
        else:
831
            mem.empty = False
832

    
833
        mem.extra = RadioSettingGroup("Extra", "extra")
834

    
835
        # BCLO
836
        is_bclo = bool(_mem.flags2 & FLAGS2_BCLO > 0)
837
        rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo))
838
        mem.extra.append(rs)
839
        tmpcomment += "BCLO:"+(is_bclo and "ON" or "off")+" "
840

    
841
        # Frequency reverse - whatever that means, don't see it in the manual
842
        is_frev = bool(_mem.flags2 & FLAGS2_REVERSE > 0)
843
        rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev))
844
        mem.extra.append(rs)
845
        tmpcomment += "FreqReverse:"+(is_frev and "ON" or "off")+" "
846

    
847
        # PTTID
848
        pttid = (_mem.dtmf_flags & FLAGS_DTMF_PTTID_MASK) >> 1
849
        rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(
850
            PTTID_LIST, PTTID_LIST[pttid]))
851
        mem.extra.append(rs)
852
        tmpcomment += "PTTid:"+PTTID_LIST[pttid]+" "
853

    
854
        # DTMF DECODE
855
        is_dtmf = bool(_mem.dtmf_flags & FLAGS_DTMF_DECODE > 0)
856
        rs = RadioSetting("dtmfdecode", "DTMF decode",
857
                          RadioSettingValueBoolean(is_dtmf))
858
        mem.extra.append(rs)
859
        tmpcomment += "DTMFdecode:"+(is_dtmf and "ON" or "off")+" "
860

    
861
        # Scrambler
862
        if _mem.scrambler & 0x0f < len(SCRAMBLER_LIST):
863
            enc = _mem.scrambler & 0x0f
864
        else:
865
            enc = 0
866

    
867
        rs = RadioSetting("scrambler", "Scrambler", RadioSettingValueList(
868
            SCRAMBLER_LIST, SCRAMBLER_LIST[enc]))
869
        mem.extra.append(rs)
870
        tmpcomment += "Scrambler:"+SCRAMBLER_LIST[enc]+" "
871

    
872
        # scanlists
873
        pttid = (_mem.dtmf_flags & FLAGS_DTMF_PTTID_MASK) >> 1
874
        rs = RadioSetting("scanlists", "Scanlists", RadioSettingValueList(
875
            SCANLIST_LIST, tmpscn))
876
        mem.extra.append(rs)
877

    
878
        return mem
879

    
880
    def set_settings(self, settings):
881
        _mem = self._memobj
882
        for element in settings:
883
            if not isinstance(element, RadioSetting):
884
                self.set_settings(element)
885
                continue
886

    
887
            # basic settings
888

    
889
            # call channel
890
            if element.get_name() == "call_channel":
891
                _mem.call_channel = int(element.value)-1
892

    
893
            # squelch
894
            if element.get_name() == "squelch":
895
                _mem.squelch = int(element.value)
896
            # TOT
897
            if element.get_name() == "tot":
898
                _mem.max_talk_time = int(element.value)
899
            # NOAA autoscan
900
            if element.get_name() == "noaa_autoscan":
901
                _mem.noaa_autoscan = element.value and 1 or 0
902

    
903
            # vox level
904
            if element.get_name() == "vox_level":
905
                _mem.vox_level = int(element.value)-1
906

    
907
            # mic gain
908
            if element.get_name() == "mic_gain":
909
                _mem.mic_gain = int(element.value)
910

    
911
            # Channel display mode
912
            if element.get_name() == "channel_display_mode":
913
                _mem.channel_display_mode = CHANNELDISP_LIST.index(
914
                    str(element.value))
915

    
916
            # Crossband receiving/transmitting
917
            if element.get_name() == "crossband":
918
                _mem.crossband = CROSSBAND_LIST.index(str(element.value))
919

    
920
            # Battery Save
921
            if element.get_name() == "battery_save":
922
                _mem.battery_save = BATSAVE_LIST.index(str(element.value))
923
            # Dual Watch
924
            if element.get_name() == "dualwatch":
925
                _mem.dual_watch = DUALWATCH_LIST.index(str(element.value))
926

    
927
            # Tail tone elimination
928
            if element.get_name() == "tail_note_elimination":
929
                _mem.tail_note_elimination = element.value and 1 or 0
930

    
931
            # VFO Open
932
            if element.get_name() == "vfo_open":
933
                _mem.vfo_open = element.value and 1 or 0
934

    
935
            # Beep control
936
            if element.get_name() == "beep_control":
937
                _mem.beep_control = element.value and 1 or 0
938

    
939
            # Scan resume mode
940
            if element.get_name() == "scan_resume_mode":
941
                _mem.scan_resume_mode = SCANRESUME_LIST.index(
942
                    str(element.value))
943

    
944
            # Auto keypad lock
945
            if element.get_name() == "auto_keypad_lock":
946
                _mem.auto_keypad_lock = element.value and 1 or 0
947

    
948
            # Power on display mode
949
            if element.get_name() == "welcome_mode":
950
                _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value))
951

    
952
            # Keypad Tone
953
            if element.get_name() == "keypad_tone":
954
                _mem.keypad_tone = KEYPADTONE_LIST.index(str(element.value))
955

    
956
            # Language
957
            if element.get_name() == "language":
958
                _mem.language = LANGUAGE_LIST.index(str(element.value))
959

    
960
            # Alarm mode
961
            if element.get_name() == "alarm_mode":
962
                _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value))
963

    
964
            # Reminding of end of talk
965
            if element.get_name() == "reminding_of_end_talk":
966
                _mem.reminding_of_end_talk = REMENDOFTALK_LIST.index(
967
                    str(element.value))
968

    
969
            # Repeater tail tone elimination
970
            if element.get_name() == "repeater_tail_elimination":
971
                _mem.repeater_tail_elimination = RTE_LIST.index(
972
                    str(element.value))
973

    
974
            # Logo string 1
975
            if element.get_name() == "logo1":
976
                b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
977
                _mem.logo_line1 = b[0:12]+"\xff\xff\xff\xff"
978

    
979
            # Logo string 2
980
            if element.get_name() == "logo2":
981
                b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
982
                _mem.logo_line2 = b[0:12]+"\xff\xff\xff\xff"
983

    
984
            # unlock settings
985

    
986
            # FLOCK
987
            if element.get_name() == "flock":
988
                _mem.int_flock = FLOCK_LIST.index(str(element.value))
989

    
990
            # 350TX
991
            if element.get_name() == "350tx":
992
                _mem.int_350tx = element.value and 1 or 0
993

    
994
            # UNKNOWN1
995
            if element.get_name() == "unknown1":
996
                _mem.int_unknown1 = element.value and 1 or 0
997

    
998
            # 200TX
999
            if element.get_name() == "200tx":
1000
                _mem.int_200tx = element.value and 1 or 0
1001

    
1002
            # 500TX
1003
            if element.get_name() == "500tx":
1004
                _mem.int_500tx = element.value and 1 or 0
1005

    
1006
            # 350EN
1007
            if element.get_name() == "350en":
1008
                _mem.int_350en = element.value and 1 or 0
1009

    
1010
            # fm radio
1011
            for i in range(1, 21):
1012
                freqname = "FM_" + str(i)
1013
                if element.get_name() == freqname:
1014
                    val = str(element.value).strip()
1015
                    try:
1016
                        val2 = int(float(val)*10)
1017
                    except Exception:
1018
                        val2 = 0xffff
1019

    
1020
                    if val2 < FMMIN*10 or val2 > FMMAX*10:
1021
                        val2 = 0xffff
1022
#                        raise errors.InvalidValueError(
1023
#                                "FM radio frequency should be a value "
1024
#                                "in the range %.1f - %.1f" % (FMMIN , FMMAX))
1025
                    _mem.fmfreq[i-1] = val2
1026

    
1027
            # scanlist stuff
1028
            if element.get_name() == "scanlist_default":
1029
                val = (int(element.value) == 2) and 1 or 0
1030
                _mem.scanlist_default = val
1031

    
1032
            if element.get_name() == "scanlist1_priority_scan":
1033
                _mem.scanlist1_priority_scan = \
1034
                        element.value and 1 or 0
1035

    
1036
            if element.get_name() == "scanlist2_priority_scan":
1037
                _mem.scanlist2_priority_scan = \
1038
                        element.value and 1 or 0
1039

    
1040
            if element.get_name() == "scanlist1_priority_ch1" or \
1041
                    element.get_name() == "scanlist1_priority_ch2" or \
1042
                    element.get_name() == "scanlist2_priority_ch1" or \
1043
                    element.get_name() == "scanlist2_priority_ch2":
1044

    
1045
                val = int(element.value)
1046

    
1047
                if val > 200 or val < 1:
1048
                    val = 0xff
1049
                else:
1050
                    val -= 1
1051

    
1052
                if element.get_name() == "scanlist1_priority_ch1":
1053
                    _mem.scanlist1_priority_ch1 = val
1054
                if element.get_name() == "scanlist1_priority_ch2":
1055
                    _mem.scanlist1_priority_ch2 = val
1056
                if element.get_name() == "scanlist2_priority_ch1":
1057
                    _mem.scanlist2_priority_ch1 = val
1058
                if element.get_name() == "scanlist2_priority_ch2":
1059
                    _mem.scanlist2_priority_ch2 = val
1060

    
1061
    def get_settings(self):
1062
        _mem = self._memobj
1063
        basic = RadioSettingGroup("basic", "Basic Settings")
1064
        scanl = RadioSettingGroup("scn", "Scan Lists")
1065
        unlock = RadioSettingGroup("unlock", "Unlock Settings")
1066
        fmradio = RadioSettingGroup("fmradio", "FM Radio")
1067

    
1068
        roinfo = RadioSettingGroup("roinfo", "Driver information")
1069

    
1070
        top = RadioSettings(basic, scanl, unlock, fmradio, roinfo)
1071

    
1072
        if _mem.scanlist_default == 1:
1073
            tmpsc = 2
1074
        else:
1075
            tmpsc = 1
1076
        rs = RadioSetting("scanlist_default",
1077
                          "Default scanlist",
1078
                          RadioSettingValueInteger(1, 2, tmpsc))
1079
        scanl.append(rs)
1080

    
1081
        tmppr = bool((_mem.scanlist1_priority_scan & 1) > 0)
1082
        rs = RadioSetting(
1083
                "scanlist1_priority_scan",
1084
                "Scanlist 1 priority channel scan",
1085
                RadioSettingValueBoolean(tmppr))
1086
        scanl.append(rs)
1087

    
1088
        tmpch = _mem.scanlist1_priority_ch1 + 1
1089
        if tmpch > 200:
1090
            tmpch = 0
1091
        rs = RadioSetting("scanlist1_priority_ch1",
1092
                          "Scanlist 1 priority channel 1 (0 - off)",
1093
                          RadioSettingValueInteger(0, 200, tmpch))
1094
        scanl.append(rs)
1095

    
1096
        tmpch = _mem.scanlist1_priority_ch2 + 1
1097
        if tmpch > 200:
1098
            tmpch = 0
1099
        rs = RadioSetting("scanlist1_priority_ch2",
1100
                          "Scanlist 1 priority channel 2 (0 - off)",
1101
                          RadioSettingValueInteger(0, 200, tmpch))
1102
        scanl.append(rs)
1103

    
1104
        tmppr = bool((_mem.scanlist2_priority_scan & 1) > 0)
1105
        rs = RadioSetting(
1106
                "scanlist2_priority_scan",
1107
                "Scanlist 2 priority channel scan",
1108
                RadioSettingValueBoolean(tmppr))
1109
        scanl.append(rs)
1110

    
1111
        tmpch = _mem.scanlist2_priority_ch1 + 1
1112
        if tmpch > 200:
1113
            tmpch = 0
1114
        rs = RadioSetting("scanlist2_priority_ch1",
1115
                          "Scanlist 2 priority channel 1 (0 - off)",
1116
                          RadioSettingValueInteger(0, 200, tmpch))
1117
        scanl.append(rs)
1118

    
1119
        tmpch = _mem.scanlist2_priority_ch2 + 1
1120
        if tmpch > 200:
1121
            tmpch = 0
1122
        rs = RadioSetting("scanlist2_priority_ch2",
1123
                          "Scanlist 2 priority channel 2 (0 - off)",
1124
                          RadioSettingValueInteger(0, 200, tmpch))
1125
        scanl.append(rs)
1126

    
1127
        # basic settings
1128

    
1129
        # call channel
1130
        tmpc = _mem.call_channel+1
1131
        if tmpc > 200:
1132
            tmpc = 1
1133
        rs = RadioSetting("call_channel", "One key call channel",
1134
                          RadioSettingValueInteger(1, 200, tmpc))
1135
        basic.append(rs)
1136

    
1137
        # squelch
1138
        tmpsq = _mem.squelch
1139
        if tmpsq > 9:
1140
            tmpsq = 1
1141
        rs = RadioSetting("squelch", "Squelch",
1142
                          RadioSettingValueInteger(0, 9, tmpsq))
1143
        basic.append(rs)
1144

    
1145
        # TOT
1146
        tmptot = _mem.max_talk_time
1147
        if tmptot > 10:
1148
            tmptot = 10
1149
        rs = RadioSetting(
1150
                "tot",
1151
                "Max talk time [min]",
1152
                RadioSettingValueInteger(0, 10, tmptot))
1153
        basic.append(rs)
1154

    
1155
        # NOAA autoscan
1156
        rs = RadioSetting(
1157
                "noaa_autoscan",
1158
                "NOAA Autoscan", RadioSettingValueBoolean(
1159
                    bool(_mem.noaa_autoscan > 0)))
1160
        basic.append(rs)
1161

    
1162
        # VOX Level
1163
        tmpvox = _mem.vox_level+1
1164
        if tmpvox > 10:
1165
            tmpvox = 10
1166
        rs = RadioSetting("vox_level", "VOX Level",
1167
                          RadioSettingValueInteger(1, 10, tmpvox))
1168
        basic.append(rs)
1169

    
1170
        # Mic gain
1171
        tmpmicgain = _mem.mic_gain
1172
        if tmpmicgain > 4:
1173
            tmpmicgain = 4
1174
        rs = RadioSetting("mic_gain", "Mic Gain",
1175
                          RadioSettingValueInteger(0, 4, tmpmicgain))
1176
        basic.append(rs)
1177

    
1178
        # Channel display mode
1179
        tmpchdispmode = _mem.channel_display_mode
1180
        if tmpchdispmode >= len(CHANNELDISP_LIST):
1181
            tmpchdispmode = 0
1182
        rs = RadioSetting(
1183
                "channel_display_mode",
1184
                "Channel display mode",
1185
                RadioSettingValueList(
1186
                    CHANNELDISP_LIST,
1187
                    CHANNELDISP_LIST[tmpchdispmode]))
1188
        basic.append(rs)
1189

    
1190
        # Crossband receiving/transmitting
1191
        tmpcross = _mem.crossband
1192
        if tmpcross >= len(CROSSBAND_LIST):
1193
            tmpcross = 0
1194
        rs = RadioSetting(
1195
                "crossband",
1196
                "Cross-band receiving/transmitting",
1197
                RadioSettingValueList(
1198
                    CROSSBAND_LIST,
1199
                    CROSSBAND_LIST[tmpcross]))
1200
        basic.append(rs)
1201

    
1202
        # Battery save
1203
        tmpbatsave = _mem.battery_save
1204
        if tmpbatsave >= len(BATSAVE_LIST):
1205
            tmpbatsave = BATSAVE_LIST.index("1:4")
1206
        rs = RadioSetting(
1207
                "battery_save",
1208
                "Battery Save",
1209
                RadioSettingValueList(
1210
                    BATSAVE_LIST,
1211
                    BATSAVE_LIST[tmpbatsave]))
1212
        basic.append(rs)
1213

    
1214
        # Dual watch
1215
        tmpdual = _mem.dual_watch
1216
        if tmpdual >= len(DUALWATCH_LIST):
1217
            tmpdual = 0
1218
        rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList(
1219
            DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
1220
        basic.append(rs)
1221

    
1222
        # Tail tone elimination
1223
        rs = RadioSetting(
1224
                "tail_note_elimination",
1225
                "Tail tone elimination",
1226
                RadioSettingValueBoolean(
1227
                    bool(_mem.tail_note_elimination > 0)))
1228
        basic.append(rs)
1229

    
1230
        # VFO open
1231
        rs = RadioSetting("vfo_open", "VFO open",
1232
                          RadioSettingValueBoolean(bool(_mem.vfo_open > 0)))
1233
        basic.append(rs)
1234

    
1235
        # Beep control
1236
        rs = RadioSetting(
1237
                "beep_control",
1238
                "Beep control",
1239
                RadioSettingValueBoolean(bool(_mem.beep_control > 0)))
1240
        basic.append(rs)
1241

    
1242
        # Scan resume mode
1243
        tmpscanres = _mem.scan_resume_mode
1244
        if tmpscanres >= len(SCANRESUME_LIST):
1245
            tmpscanres = 0
1246
        rs = RadioSetting(
1247
                "scan_resume_mode",
1248
                "Scan resume mode",
1249
                RadioSettingValueList(
1250
                    SCANRESUME_LIST,
1251
                    SCANRESUME_LIST[tmpscanres]))
1252
        basic.append(rs)
1253

    
1254
        # Auto keypad lock
1255
        rs = RadioSetting(
1256
                "auto_keypad_lock",
1257
                "Auto keypad lock",
1258
                RadioSettingValueBoolean(bool(_mem.auto_keypad_lock > 0)))
1259
        basic.append(rs)
1260

    
1261
        # Power on display mode
1262
        tmpdispmode = _mem.power_on_dispmode
1263
        if tmpdispmode >= len(WELCOME_LIST):
1264
            tmpdispmode = 0
1265
        rs = RadioSetting(
1266
                "welcome_mode",
1267
                "Power on display mode",
1268
                RadioSettingValueList(
1269
                    WELCOME_LIST,
1270
                    WELCOME_LIST[tmpdispmode]))
1271
        basic.append(rs)
1272

    
1273
        # Keypad Tone
1274
        tmpkeypadtone = _mem.keypad_tone
1275
        if tmpkeypadtone >= len(KEYPADTONE_LIST):
1276
            tmpkeypadtone = 0
1277
        rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList(
1278
            KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone]))
1279
        basic.append(rs)
1280

    
1281
        # Language
1282
        tmplanguage = _mem.language
1283
        if tmplanguage >= len(LANGUAGE_LIST):
1284
            tmplanguage = 0
1285
        rs = RadioSetting("language", "Language", RadioSettingValueList(
1286
            LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage]))
1287
        basic.append(rs)
1288

    
1289
        # Alarm mode
1290
        tmpalarmmode = _mem.alarm_mode
1291
        if tmpalarmmode >= len(ALARMMODE_LIST):
1292
            tmpalarmmode = 0
1293
        rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList(
1294
            ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode]))
1295
        basic.append(rs)
1296

    
1297
        # Reminding of end of talk
1298
        tmpalarmmode = _mem.reminding_of_end_talk
1299
        if tmpalarmmode >= len(REMENDOFTALK_LIST):
1300
            tmpalarmmode = 0
1301
        rs = RadioSetting(
1302
                "reminding_of_end_talk",
1303
                "Reminding of end of talk",
1304
                RadioSettingValueList(
1305
                    REMENDOFTALK_LIST,
1306
                    REMENDOFTALK_LIST[tmpalarmmode]))
1307
        basic.append(rs)
1308

    
1309
        # Repeater tail tone elimination
1310
        tmprte = _mem.repeater_tail_elimination
1311
        if tmprte >= len(RTE_LIST):
1312
            tmprte = 0
1313
        rs = RadioSetting(
1314
                "repeater_tail_elimination",
1315
                "Repeater tail tone elimination",
1316
                RadioSettingValueList(RTE_LIST, RTE_LIST[tmprte]))
1317
        basic.append(rs)
1318

    
1319
        # Logo string 1
1320
        logo1 = str(_mem.logo_line1).strip("\x20\x00\xff")  # +"\x20"*12
1321
        logo1 = logo1[0:12]
1322
        rs = RadioSetting("logo1", "Logo string 1 (12 characters)",
1323
                          RadioSettingValueString(0, 12, logo1))
1324
        basic.append(rs)
1325

    
1326
        # Logo string 2
1327
        logo2 = str(_mem.logo_line2).strip("\x20\x00\xff")  # +"\x20"*12
1328
        logo2 = logo2[0:12]
1329
        rs = RadioSetting("logo2", "Logo string 2 (12 characters)",
1330
                          RadioSettingValueString(0, 12, logo2))
1331
        basic.append(rs)
1332

    
1333
        for i in range(1, 21):
1334
            freqname = "FM_"+str(i)
1335
            fmfreq = _mem.fmfreq[i-1]/10.0
1336
            if fmfreq < FMMIN or fmfreq > FMMAX:
1337
                rs = RadioSetting(freqname, freqname,
1338
                                  RadioSettingValueString(0, 5, ""))
1339
            else:
1340
                rs = RadioSetting(freqname, freqname,
1341
                                  RadioSettingValueString(0, 5, str(fmfreq)))
1342

    
1343
            fmradio.append(rs)
1344

    
1345
        # unlock settings
1346

    
1347
        # F-LOCK
1348
        tmpflock = _mem.int_flock
1349
        if tmpflock >= len(FLOCK_LIST):
1350
            tmpflock = 0
1351
        rs = RadioSetting(
1352
            "flock", "F-LOCK",
1353
            RadioSettingValueList(FLOCK_LIST, FLOCK_LIST[tmpflock]))
1354
        unlock.append(rs)
1355

    
1356
        # 350TX
1357
        rs = RadioSetting("350tx", "350TX", RadioSettingValueBoolean(
1358
            bool(_mem.int_350tx > 0)))
1359
        unlock.append(rs)
1360

    
1361
        # unknown1
1362
        rs = RadioSetting("unknown11", "UNKNOWN1",
1363
                          RadioSettingValueBoolean(
1364
                              bool(_mem.int_unknown1 > 0)))
1365
        unlock.append(rs)
1366

    
1367
        # 200TX
1368
        rs = RadioSetting("200tx", "200TX", RadioSettingValueBoolean(
1369
            bool(_mem.int_200tx > 0)))
1370
        unlock.append(rs)
1371

    
1372
        # 500TX
1373
        rs = RadioSetting("500tx", "500TX", RadioSettingValueBoolean(
1374
            bool(_mem.int_500tx > 0)))
1375
        unlock.append(rs)
1376

    
1377
        # 350EN
1378
        rs = RadioSetting("350en", "350EN", RadioSettingValueBoolean(
1379
            bool(_mem.int_350en > 0)))
1380
        unlock.append(rs)
1381

    
1382
        # SCREEN
1383
        rs = RadioSetting("screen", "SCREEN", RadioSettingValueBoolean(
1384
            bool(_mem.int_screen > 0)))
1385
        unlock.append(rs)
1386

    
1387
        # readonly info
1388
        # Firmware
1389
        if self.FIRMWARE_VERSION == "":
1390
            firmware = "To get the firmware version please download"
1391
            "the image from the radio first"
1392
        else:
1393
            firmware = self.FIRMWARE_VERSION
1394

    
1395
        val = RadioSettingValueString(0, 128, firmware)
1396
        val.set_mutable(False)
1397
        rs = RadioSetting("fw_ver", "Firmware Version", val)
1398
        roinfo.append(rs)
1399

    
1400
        # Driver version
1401
        val = RadioSettingValueString(0, 128, DRIVER_VERSION)
1402
        val.set_mutable(False)
1403
        rs = RadioSetting("driver_ver", "Driver version", val)
1404
        roinfo.append(rs)
1405

    
1406
        # No limits version for hacked firmware
1407
        val = RadioSettingValueBoolean(self.FIRMWARE_NOLIMITS)
1408
        val.set_mutable(False)
1409
        rs = RadioSetting("nolimits", "Limits disabled for modified firmware",
1410
                          val)
1411
        roinfo.append(rs)
1412

    
1413
        return top
1414

    
1415
    # Store details about a high-level memory to the memory map
1416
    # This is called when a user edits a memory in the UI
1417
    def set_memory(self, mem):
1418
        number = mem.number-1
1419

    
1420
        # Get a low-level memory object mapped to the image
1421
        _mem = self._memobj.channel[number]
1422
        _mem4 = self._memobj
1423
        # empty memory
1424
        if mem.empty:
1425
            _mem.set_raw("\xFF" * 16)
1426
            if number < 200:
1427
                _mem2 = self._memobj.channelname[number]
1428
                _mem2.set_raw("\xFF" * 16)
1429
                _mem4.channel_attributes[number].is_scanlist1 = 0
1430
                _mem4.channel_attributes[number].is_scanlist2 = 0
1431
                _mem4.channel_attributes[number].unknown1 = 0
1432
                _mem4.channel_attributes[number].unknown2 = 0
1433
                _mem4.channel_attributes[number].is_free = 1
1434
                _mem4.channel_attributes[number].band = 0x7
1435
            return mem
1436

    
1437
        # clean the channel memory, restore some bits if it was used before
1438
        if _mem.get_raw()[0] == "\xff":
1439
            # this was an empty memory
1440
            _mem.set_raw("\x00" * 16)
1441
        else:
1442
            # this memory was't empty, save some bits that we don't know the
1443
            # meaning of, or that we don't support yet
1444
            prev_0a = ord(_mem.get_raw()[0x0a]) & SAVE_MASK_0A
1445
            prev_0b = ord(_mem.get_raw()[0x0b]) & SAVE_MASK_0B
1446
            prev_0c = ord(_mem.get_raw()[0x0c]) & SAVE_MASK_0C
1447
            prev_0d = ord(_mem.get_raw()[0x0d]) & SAVE_MASK_0D
1448
            prev_0e = ord(_mem.get_raw()[0x0e]) & SAVE_MASK_0E
1449
            prev_0f = ord(_mem.get_raw()[0x0f]) & SAVE_MASK_0F
1450
            _mem.set_raw("\x00" * 10 +
1451
                         chr(prev_0a) + chr(prev_0b) + chr(prev_0c) +
1452
                         chr(prev_0d) + chr(prev_0e) + chr(prev_0f))
1453

    
1454
        if number < 200:
1455
            _mem4.channel_attributes[number].is_scanlist1 = 0
1456
            _mem4.channel_attributes[number].is_scanlist2 = 0
1457
            _mem4.channel_attributes[number].unknown1 = 0
1458
            _mem4.channel_attributes[number].unknown2 = 0
1459
            _mem4.channel_attributes[number].is_free = 1
1460
            _mem4.channel_attributes[number].band = 0x7
1461

    
1462
        # find tx frequency
1463
        if mem.duplex == '-':
1464
            txfreq = mem.freq - mem.offset
1465
        elif mem.duplex == '+':
1466
            txfreq = mem.freq + mem.offset
1467
        else:
1468
            txfreq = mem.freq
1469

    
1470
        # find band
1471
        band = _find_band(self, txfreq)
1472
        if band is False:
1473
            raise errors.RadioError(
1474
                    "Transmit frequency %.4fMHz is not supported by this radio"
1475
                    % txfreq/1000000.0)
1476

    
1477
        band = _find_band(self, mem.freq)
1478
        if band is False:
1479
            return mem
1480

    
1481
        # mode
1482
        if mem.mode == "NFM":
1483
            _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
1484
            _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
1485
        elif mem.mode == "FM":
1486
            _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1487
            _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
1488
        elif mem.mode == "NAM":
1489
            _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
1490
            _mem.flags1 = _mem.flags1 | FLAGS1_ISAM
1491
        elif mem.mode == "AM":
1492
            _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1493
            _mem.flags1 = _mem.flags1 | FLAGS1_ISAM
1494

    
1495
        # frequency/offset
1496
        _mem.freq = mem.freq/10
1497
        _mem.offset = mem.offset/10
1498

    
1499
        if mem.duplex == "off" or mem.duplex == "":
1500
            _mem.offset = 0
1501
            _mem.flags1 = _mem.flags1 & ~FLAGS1_OFFSET_MASK
1502
        elif mem.duplex == '-':
1503
            _mem.flags1 = (
1504
                    _mem.flags1 & ~FLAGS1_OFFSET_MASK) | FLAGS1_OFFSET_MINUS
1505
        elif mem.duplex == '+':
1506
            _mem.flags1 = (
1507
                    _mem.flags1 & ~FLAGS1_OFFSET_MASK) | FLAGS1_OFFSET_PLUS
1508

    
1509
        # set band
1510
        if number < 200:
1511
            _mem4.channel_attributes[number].is_free = 0
1512
            _mem4.channel_attributes[number].band = band
1513

    
1514
        # channels >200 are the 14 VFO chanells and don't have names
1515
        if number < 200:
1516
            _mem2 = self._memobj.channelname[number]
1517
            tag = mem.name.ljust(16)[:16]
1518
            _mem2.name = tag  # Store the alpha tag
1519

    
1520
        # tone data
1521
        self._set_tone(mem, _mem)
1522

    
1523
        # step
1524
        _mem.step = STEPS.index(mem.tuning_step)
1525

    
1526
        # tx power
1527
        if str(mem.power) == str(UVK5_POWER_LEVELS[2]):
1528
            _mem.flags2 = (
1529
                _mem.flags2 & ~FLAGS2_POWER_MASK) | FLAGS2_POWER_HIGH
1530
        elif str(mem.power) == str(UVK5_POWER_LEVELS[1]):
1531
            _mem.flags2 = (
1532
                _mem.flags2 & ~FLAGS2_POWER_MASK) | FLAGS2_POWER_MEDIUM
1533
        else:
1534
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK)
1535

    
1536
        for setting in mem.extra:
1537
            sname = setting.get_name()
1538
            svalue = setting.value.get_value()
1539

    
1540
            if sname == "bclo":
1541
                if svalue:
1542
                    _mem.flags2 = _mem.flags2 | FLAGS2_BCLO
1543
                else:
1544
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_BCLO
1545

    
1546
            if sname == "pttid":
1547
                _mem.dtmf_flags = (
1548
                        (_mem.dtmf_flags & ~FLAGS_DTMF_PTTID_MASK)
1549
                        | (PTTID_LIST.index(svalue) << 1))
1550

    
1551
            if sname == "frev":
1552
                if svalue:
1553
                    _mem.flags2 = _mem.flags2 | FLAGS2_REVERSE
1554
                else:
1555
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_REVERSE
1556

    
1557
            if sname == "dtmfdecode":
1558
                if svalue:
1559
                    _mem.dtmf_flags = _mem.dtmf_flags | FLAGS_DTMF_DECODE
1560
                else:
1561
                    _mem.dtmf_flags = _mem.dtmf_flags & ~FLAGS_DTMF_DECODE
1562

    
1563
            if sname == "scrambler":
1564
                _mem.scrambler = (
1565
                    _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
1566

    
1567
            if number < 200 and sname == "scanlists":
1568
                if svalue == "1":
1569
                    _mem4.channel_attributes[number].is_scanlist1 = 1
1570
                    _mem4.channel_attributes[number].is_scanlist2 = 0
1571
                elif svalue == "2":
1572
                    _mem4.channel_attributes[number].is_scanlist1 = 0
1573
                    _mem4.channel_attributes[number].is_scanlist2 = 1
1574
                elif svalue == "1+2":
1575
                    _mem4.channel_attributes[number].is_scanlist1 = 1
1576
                    _mem4.channel_attributes[number].is_scanlist2 = 1
1577
                else:
1578
                    _mem4.channel_attributes[number].is_scanlist1 = 0
1579
                    _mem4.channel_attributes[number].is_scanlist2 = 0
1580

    
1581
        return mem
1582

    
1583

    
1584
@directory.register
1585
class UVK5Radio_nolimit(UVK5Radio):
1586
    VENDOR = "Quansheng"
1587
    MODEL = "UV-K5 (modified firmware)"
1588
    VARIANT = "nolimits"
1589
    FIRMWARE_NOLIMITS = True
1590

    
1591
    def get_features(self):
1592
        rf = UVK5Radio.get_features(self)
1593
        # This is what the BK4819 chip supports
1594
        rf.valid_bands = [(18000000,  620000000),
1595
                          (840000000, 1300000000)
1596
                          ]
1597
        return rf
(39-39/47)