Project

General

Profile

New Model #10648 » baofeng_uv17Pro.py

Latest driver staged for pull request into chirp - Sander van der Wel, 11/17/2023 04:49 AM

 
1
# Copyright 2023:
2
# * Sander van der Wel, <svdwel@icloud.com>
3
# this software is a modified version of the boafeng driver by:
4
# * Jim Unroe KC9HI, <rock.unroe@gmail.com>
5
#
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18

    
19
import logging
20

    
21
from chirp import chirp_common, directory, memmap
22
from chirp import bitwise
23
from chirp.settings import RadioSettingGroup, RadioSetting, \
24
    RadioSettingValueBoolean, RadioSettingValueList, \
25
    RadioSettingValueString, RadioSettingValueInteger, \
26
    RadioSettings, \
27
    InvalidValueError
28
import time
29
import struct
30
from chirp import errors, util
31

    
32
LOG = logging.getLogger(__name__)
33

    
34
# #### MAGICS #########################################################
35

    
36
# Baofeng UV-17L magic string
37
MSTRING_UV17L = b"PROGRAMBFNORMALU"
38
MSTRING_UV17PROGPS = b"PROGRAMCOLORPROU"
39

    
40
DTMF_CHARS = "0123456789 *#ABCD"
41
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
42

    
43
LIST_AB = ["A", "B"]
44
LIST_ALMOD = ["Site", "Tone", "Code"]
45
LIST_BANDWIDTH = ["Wide", "Narrow"]
46
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
47
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 500]]
48
LIST_HANGUPTIME = ["%s s" % x for x in [3, 4, 5, 6, 7, 8, 9, 10]]
49
LIST_SIDE_TONE = ["Off", "Side Key", "ID", "Side + ID"]
50
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
51
LIST_MODE = ["Name", "Frequency", "Channel Number"]
52
LIST_OFF1TO5 = ["Off"] + list("12345")
53
LIST_OFF1TO9 = LIST_OFF1TO5 + list("6789")
54
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
55
LIST_OFFAB = ["Off"] + LIST_AB
56
LIST_RESUME = ["TO", "CO", "SE"]
57
LIST_PONMSG = ["Full", "Message"]
58
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
59
LIST_SCODE = ["%s" % x for x in range(1, 21)]
60
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
61
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
62
LIST_SHIFTD = ["Off", "+", "-"]
63
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
64
LIST_STEP = [str(x) for x in STEPS]
65
LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 195, 15)]
66
LIST_TIMEOUT_ALARM = ["Off"] + ["%s sec" % x for x in range(1, 11)]
67
LIST_PILOT_TONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
68
LIST_TXPOWER = ["High", "Low"]
69
LIST_VOICE = ["Off", "English", "Chinese"]
70
LIST_WORKMODE = ["Frequency", "Channel"]
71
LIST_POWERON_DISPLAY_TYPE = ["LOGO", "BATT voltage"]
72
LIST_BEEP = ["Off", "Beep", "Voice", "Both"]
73
LIST_LANGUAGE = ["English", "Chinese"]
74
LIST_SCANMODE = ["Time", "Carrier", "Search"]
75
LIST_ALARMMODE = ["Local", "Send Tone", "Send Code"]
76
LIST_MENU_QUIT_TIME = ["%s sec" % x for x in range(5, 55, 5)] + ["60 sec"]
77
LIST_BACKLIGHT_TIMER = ["Always On"] + ["%s sec" % x for x in range(5, 25, 5)]
78
LIST_ID_DELAY = ["%s ms" % x for x in range(100, 3100, 100)]
79
LIST_QT_SAVEMODE = ["Both", "RX", "TX"]
80
LIST_SKEY2_SHORT = ["FM", "Scan", "Search", "Vox"]
81
LIST_RPT_TAIL_CLEAR = ["%s ms" % x for x in range(0, 1100, 100)]
82
LIST_VOX_DELAY_TIME = ["%s ms" % x for x in range(500, 2100, 100)]
83
LIST_VOX_LEVEL = ["Off"] + ["%s" % x for x in range(1, 10, 1)]
84
LIST_VOX_LEVEL_ALT = ["%s" % x for x in range(1, 10, 1)]
85
LIST_GPS_MODE = ["GPS", "Beidou", "GPS + Beidou"]
86
LIST_GPS_TIMEZONE = ["%s" % x for x in range(-12, 13, 1)]
87

    
88
TXP_CHOICES = ["High", "Low"]
89
TXP_VALUES = [0x00, 0x02]
90

    
91
STIMEOUT = 1.5
92

    
93

    
94
def model_match(cls, data):
95
    """Match the opened image to the correct version"""
96
    return data[cls.MEM_TOTAL:] == bytes(cls.MODEL, 'utf-8')
97

    
98

    
99
def _crypt(symbolIndex, buffer):
100
    # Some weird encryption is used. From the table below, we only use "CO 7".
101
    tblEncrySymbol = [b"BHT ", b"CO 7", b"A ES", b" EIY", b"M PQ",
102
                      b"XN Y", b"RVB ", b" HQP", b"W RC", b"MS N",
103
                      b" SAT", b"K DH", b"ZO R", b"C SL", b"6RB ",
104
                      b" JCG", b"PN V", b"J PK", b"EK L", b"I LZ"]
105
    tblEncrySymbols = tblEncrySymbol[symbolIndex]
106
    decBuffer = b""
107
    index1 = 0
108
    for index2 in range(len(buffer)):
109
        boolEncryptChar = ((tblEncrySymbols[index1] != 32) &
110
                           (buffer[index2] != 0) &
111
                           (buffer[index2] != 255) &
112
                           (buffer[index2] != tblEncrySymbols[index1]) &
113
                           (buffer[index2] != (tblEncrySymbols[index1] ^ 255))
114
                           )
115
        if (boolEncryptChar):
116
            decByte = buffer[index2] ^ tblEncrySymbols[index1]
117
            decBuffer += decByte.to_bytes(1, 'big')
118
        else:
119
            decBuffer += buffer[index2].to_bytes(1, 'big')
120
        index1 = (index1 + 1) % 4
121
    return decBuffer
122

    
123

    
124
def _clean_buffer(radio):
125
    radio.pipe.timeout = 0.005
126
    junk = radio.pipe.read(256)
127
    radio.pipe.timeout = STIMEOUT
128
    if junk:
129
        LOG.debug("Got %i bytes of junk before starting" % len(junk))
130

    
131

    
132
def _rawrecv(radio, amount):
133
    """Raw read from the radio device"""
134
    data = ""
135
    try:
136
        data = radio.pipe.read(amount)
137
    except Exception:
138
        msg = "Generic error reading data from radio; check your cable."
139
        raise errors.RadioError(msg)
140

    
141
    if len(data) != amount:
142
        msg = "Error reading data from radio: not the amount of data we want."
143
        raise errors.RadioError(msg)
144

    
145
    return data
146

    
147

    
148
def _rawsend(radio, data):
149
    """Raw send to the radio device"""
150
    try:
151
        radio.pipe.write(data)
152
    except Exception:
153
        raise errors.RadioError("Error sending data to radio")
154

    
155

    
156
def _make_read_frame(addr, length):
157
    """Pack the info in the header format"""
158
    frame = _make_frame(b"\x52", addr, length)
159
    # Return the data
160

    
161
    return frame
162

    
163

    
164
def _make_frame(cmd, addr, length, data=""):
165
    """Pack the info in the header format"""
166
    frame = cmd+struct.pack(">i", addr)[2:]+struct.pack("b", length)
167
    # add the data if set
168
    if len(data) != 0:
169
        frame += data
170
    # return the data
171
    return frame
172

    
173

    
174
def _recv(radio, addr, length):
175
    """Get data from the radio """
176
    # read 4 bytes of header
177
    hdr = _rawrecv(radio, 4)
178

    
179
    # read data
180
    data = _rawrecv(radio, length)
181

    
182
    # DEBUG
183
    LOG.info("Response:")
184
    LOG.debug(util.hexprint(hdr + data))
185

    
186
    c, a, ln = struct.unpack(">BHB", hdr)
187
    if a != addr or ln != length or c != ord("X"):
188
        LOG.error("Invalid answer for block 0x%04x:" % addr)
189
        LOG.debug("CMD: %s  ADDR: %04x  SIZE: %02x" % (c, a, ln))
190
        raise errors.RadioError("Unknown response from the radio")
191

    
192
    return data
193

    
194

    
195
def _get_radio_firmware_version(radio):
196
    # There is a problem in new radios where a different firmware version is
197
    # received when directly reading a single block as compared to what is
198
    # received when reading sequential blocks. This causes a mismatch between
199
    # the image firmware version and the radio firmware version when uploading
200
    # an image that came from the same radio. The workaround is to read 1 or
201
    # more consecutive blocks prior to reading the block with the firmware
202
    # version.
203
    #
204
    # Read 2 consecutive blocks to get the radio firmware version.
205
    for addr in range(0x1E80, 0x1F00, radio._recv_block_size):
206
        frame = _make_frame("S", addr, radio._recv_block_size)
207

    
208
        # sending the read request
209
        _rawsend(radio, frame)
210

    
211
        if radio._ack_block and addr != 0x1E80:
212
            ack = _rawrecv(radio, 1)
213
            if ack != b"\x06":
214
                raise errors.RadioError(
215
                    "Radio refused to send block 0x%04x" % addr)
216

    
217
        # now we read
218
        block = _recv(radio, addr, radio._recv_block_size)
219

    
220
        _rawsend(radio, b"\x06")
221
        time.sleep(0.05)
222

    
223
    # get firmware version from the last block read
224
    version = block[48:64]
225
    return version
226

    
227

    
228
def _image_ident_from_data(data, start, stop):
229
    return data[start:stop]
230

    
231

    
232
def _get_image_firmware_version(radio):
233
    return _image_ident_from_data(radio.get_mmap(), radio._fw_ver_start,
234
                                  radio._fw_ver_start + 0x10)
235

    
236

    
237
def _sendmagic(radio, magic, response):
238
    _rawsend(radio, magic)
239
    ack = _rawrecv(radio, len(response))
240
    if ack != response:
241
        if ack:
242
            LOG.debug(repr(ack))
243
        raise errors.RadioError("Radio did not respond to enter read mode")
244

    
245

    
246
def _do_ident(radio):
247
    """Put the radio in PROGRAM mode & identify it"""
248
    radio.pipe.baudrate = radio.BAUDRATE
249
    radio.pipe.parity = "N"
250
    radio.pipe.timeout = STIMEOUT
251

    
252
    # Flush input buffer
253
    _clean_buffer(radio)
254

    
255
    # Ident radio
256
    magic = radio._magic
257
    _rawsend(radio, magic)
258
    ack = _rawrecv(radio, radio._magic_response_length)
259

    
260
    if not ack.startswith(radio._fingerprint):
261
        if ack:
262
            LOG.debug(repr(ack))
263
        raise errors.RadioError("Radio did not respond as expected (A)")
264

    
265
    return True
266

    
267

    
268
def _ident_radio(radio):
269
    for magic in radio._magic:
270
        error = None
271
        try:
272
            data = _do_ident(radio, magic)
273
            return data
274
        except errors.RadioError as e:
275
            print(e)
276
            error = e
277
            time.sleep(2)
278
    if error:
279
        raise error
280
    raise errors.RadioError("Radio did not respond")
281

    
282

    
283
def _download(radio):
284
    """Get the memory map"""
285

    
286
    # Put radio in program mode and identify it
287
    _do_ident(radio)
288
    for index in range(len(radio._magics)):
289
        _rawsend(radio, radio._magics[index])
290
        _rawrecv(radio, radio._magicResponseLengths[index])
291

    
292
    data = b""
293

    
294
    # UI progress
295
    status = chirp_common.Status()
296
    status.cur = 0
297
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
298
    status.msg = "Cloning from radio..."
299
    radio.status_fn(status)
300

    
301
    for i in range(len(radio.MEM_SIZES)):
302
        MEM_SIZE = radio.MEM_SIZES[i]
303
        MEM_START = radio.MEM_STARTS[i]
304
        for addr in range(MEM_START, MEM_START + MEM_SIZE, radio.BLOCK_SIZE):
305
            frame = _make_read_frame(addr, radio.BLOCK_SIZE)
306
            # DEBUG
307
            LOG.debug("Frame=" + util.hexprint(frame))
308

    
309
            # Sending the read request
310
            _rawsend(radio, frame)
311

    
312
            # Now we read data
313
            d = _rawrecv(radio, radio.BLOCK_SIZE + 4)
314

    
315
            LOG.debug("Response Data= " + util.hexprint(d))
316
            d = _crypt(1, d[4:])
317

    
318
            # Aggregate the data
319
            data += d
320

    
321
            # UI Update
322
            status.cur = len(data) // radio.BLOCK_SIZE
323
            status.msg = "Cloning from radio..."
324
            radio.status_fn(status)
325
    data += bytes(radio.MODEL, 'utf-8')
326
    return data
327

    
328

    
329
def _upload(radio):
330
    # Put radio in program mode and identify it
331
    _do_ident(radio)
332
    for index in range(len(radio._magics)):
333
        _rawsend(radio, radio._magics[index])
334
        _rawrecv(radio, radio._magicResponseLengths[index])
335

    
336
    data = b""
337

    
338
    # UI progress
339
    status = chirp_common.Status()
340
    status.cur = 0
341
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
342
    status.msg = "Cloning from radio..."
343
    radio.status_fn(status)
344

    
345
    data_addr = 0x00
346
    for i in range(len(radio.MEM_SIZES)):
347
        MEM_SIZE = radio.MEM_SIZES[i]
348
        MEM_START = radio.MEM_STARTS[i]
349
        for addr in range(MEM_START, MEM_START + MEM_SIZE, radio.BLOCK_SIZE):
350
            data = radio.get_mmap()[data_addr:data_addr + radio.BLOCK_SIZE]
351
            data = _crypt(1, data)
352
            data_addr += radio.BLOCK_SIZE
353

    
354
            frame = _make_frame(b"W", addr, radio.BLOCK_SIZE, data)
355
            # DEBUG
356
            LOG.debug("Frame=" + util.hexprint(frame))
357

    
358
            # Sending the read request
359
            _rawsend(radio, frame)
360

    
361
            # receiving the response
362
            ack = _rawrecv(radio, 1)
363
            if ack != b"\x06":
364
                msg = "Bad ack writing block 0x%04x" % addr
365
                raise errors.RadioError(msg)
366

    
367
            # UI Update
368
            status.cur = data_addr // radio.BLOCK_SIZE
369
            status.msg = "Cloning to radio..."
370
            radio.status_fn(status)
371
    data += bytes(radio.MODEL, 'utf-8')
372
    return data
373

    
374

    
375
def _split(rf, f1, f2):
376
    """Returns False if the two freqs are in the same band (no split)
377
    or True otherwise"""
378

    
379
    # determine if the two freqs are in the same band
380
    for low, high in rf.valid_bands:
381
        if f1 >= low and f1 <= high and \
382
                f2 >= low and f2 <= high:
383
            # if the two freqs are on the same Band this is not a split
384
            return False
385

    
386
    # if you get here is because the freq pairs are split
387
    return True
388

    
389

    
390
@directory.register
391
class UV17Pro(chirp_common.CloneModeRadio,
392
              chirp_common.ExperimentalRadio):
393
    """Baofeng UV-17Pro"""
394
    VENDOR = "Baofeng"
395
    MODEL = "UV-17Pro"
396
    NEEDS_COMPAT_SERIAL = False
397

    
398
    MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
399
    MEM_SIZES = [0x8040, 0x0040, 0x02C0, 0x0040]
400

    
401
    MEM_TOTAL = 0x8380
402
    BLOCK_SIZE = 0x40
403
    STIMEOUT = 2
404
    BAUDRATE = 115200
405

    
406
    _gmrs = False
407
    _bw_shift = False
408
    _support_banknames = False
409

    
410
    _tri_band = True
411
    _fileid = []
412
    _magic = MSTRING_UV17L
413
    _magic_response_length = 1
414
    _fingerprint = b"\x06"
415
    _magics = [b"\x46", b"\x4d",
416
               b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01\x01\x04\x11\x08\x05" +
417
               b"\x0D\x0D\x01\x11\x0F\x09\x12\x09\x10\x04\x00"]
418
    _magicResponseLengths = [16, 15, 1]
419
    _fw_ver_start = 0x1EF0
420
    _recv_block_size = 0x40
421
    _mem_size = MEM_TOTAL
422
    _ack_block = True
423
    _aniid = True
424
    _vfoscan = False
425
    _has_gps = False
426
    _voxsw = False
427
    _pilot_tone = False
428
    _send_id_delay = False
429
    _skey2_short = False
430

    
431
    MODES = ["NFM", "FM"]
432
    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
433
        "!@#$%^&*()+-=[]:\";'<>?,./"
434
    LENGTH_NAME = 12
435
    SKIP_VALUES = ["", "S"]
436
    DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
437
    RXTX_CODES = ('Off', )
438
    for code in chirp_common.TONES:
439
        RXTX_CODES = (RXTX_CODES + (str(code), ))
440
    for code in DTCS_CODES:
441
        RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'N', ))
442
    for code in DTCS_CODES:
443
        RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'I', ))
444
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
445
                    chirp_common.PowerLevel("Low",  watts=1.00)]
446
    _airband = (108000000, 136000000)
447
    _vhf_range = (136000000, 174000000)
448
    _vhf2_range = (200000000, 260000000)
449
    _uhf_range = (400000000, 520000000)
450
    _uhf2_range = (350000000, 390000000)
451

    
452
    VALID_BANDS = [_vhf_range, _vhf2_range,
453
                   _uhf_range]
454
    PTTID_LIST = LIST_PTTID
455
    SCODE_LIST = LIST_SCODE
456

    
457
    MEM_FORMAT = """
458
    #seekto 0x0000;
459
    struct {
460
      lbcd rxfreq[4];
461
      lbcd txfreq[4];
462
      ul16 rxtone;
463
      ul16 txtone;
464
      u8 scode:8;
465
      u8 pttid:8;
466
      u8 lowpower:8;
467
      u8 unknown1:1,
468
         wide:1,
469
         sqmode:2,
470
         bcl:1,
471
         scan:1,
472
         unknown2:1,
473
         fhss:1;
474
      u8 unknown3:8;
475
      u8 unknown4:8;
476
      u8 unknown5:8;
477
      u8 unknown6:8;
478
      char name[12];
479
    } memory[1000];
480

    
481
    #seekto 0x8080;
482
    struct {
483
      u8 code[5];
484
      u8 unknown[1];
485
      u8 unused1:6,
486
         aniid:2;
487
      u8 dtmfon;
488
      u8 dtmfoff;
489
    } ani;
490

    
491
    #seekto 0x80A0;
492
    struct {
493
      u8 code[5];
494
      u8 name[10];
495
      u8 unused:8;
496
    } pttid[20];
497

    
498
    #seekto 0x8280;
499
    struct {
500
      char name1[12];
501
      u32 unknown1;
502
      char name2[12];
503
      u32 unknown2;
504
      char name3[12];
505
      u32 unknown3;
506
      char name4[12];
507
      u32 unknown4;
508
      char name5[12];
509
      u32 unknown5;
510
      char name6[12];
511
      u32 unknown6;
512
      char name7[12];
513
      u32 unknown7;
514
      char name8[12];
515
      u32 unknown8;
516
      char name9[12];
517
      u32 unknown9;
518
      char name10[12];
519
      u32 unknown10;
520
    } bank_names;
521

    
522
    struct vfo {
523
      u8 freq[8];
524
      ul16 rxtone;
525
      ul16 txtone;
526
      u8 unknown0;
527
      u8 bcl;
528
      u8 sftd:3,
529
         scode:5;
530
      u8 unknown1;
531
      u8 lowpower;
532
      u8 unknown2:1,
533
         wide:1,
534
         unknown3:5,
535
         fhss:1;
536
      u8 unknown4;
537
      u8 step;
538
      u8 offset[6];
539
      u8 unknown5[2];
540
      u8 sqmode;
541
      u8 unknown6[3];
542
    };
543

    
544
    #seekto 0x8000;
545
    struct {
546
      struct vfo a;
547
      struct vfo b;
548
    } vfo;
549

    
550
    #seekto 0x8040;
551
    struct {
552
      u8 squelch;
553
      u8 savemode;
554
      u8 vox;
555
      u8 backlight;
556
      u8 dualstandby;
557
      u8 tot;
558
      u8 beep;
559
      u8 voicesw;
560
      u8 voice;
561
      u8 sidetone;
562
      u8 scanmode;
563
      u8 pttid;
564
      u8 pttdly;
565
      u8 chadistype;
566
      u8 chbdistype;
567
      u8 bcl;
568
      u8 autolock;
569
      u8 alarmmode;
570
      u8 alarmtone;
571
      u8 unknown1;
572
      u8 tailclear;
573
      u8 rpttailclear;
574
      u8 rpttaildet;
575
      u8 roger;
576
      u8 unknown2;
577
      u8 fmenable;
578
      u8 chaworkmode:4,
579
         chbworkmode:4;
580
      u8 keylock;
581
      u8 powerondistype;
582
      u8 tone;
583
      u8 unknown4[2];
584
      u8 voxdlytime;
585
      u8 menuquittime;
586
      u8 unknown5[6];
587
      u8 totalarm;
588
      u8 unknown6[2];
589
      u8 ctsdcsscantype;
590
      ul16 vfoscanmin;
591
      ul16 vfoscanmax;
592
      u8 gpsw;
593
      u8 gpsmode;
594
      u8 unknown7[2];
595
      u8 key2short;
596
      u8 unknown8[2];
597
      u8 rstmenu;
598
      u8 unknown9;
599
      u8 hangup;
600
      u8 voxsw;
601
      u8 gpstimezone;
602
    } settings;
603
    """
604

    
605
    @classmethod
606
    def get_prompts(cls):
607
        rp = chirp_common.RadioPrompts()
608
        rp.experimental = \
609
            ('This driver is a beta version.\n'
610
             '\n'
611
             'Please save an unedited copy of your first successful\n'
612
             'download to a CHIRP Radio Images(*.img) file.'
613
             )
614
        rp.pre_download = _(
615
            "Follow these instructions to download your info:\n"
616
            "1 - Turn off your radio\n"
617
            "2 - Connect your interface cable\n"
618
            "3 - Turn on your radio\n"
619
            "4 - Do the download of your radio data\n")
620
        rp.pre_upload = _(
621
            "Follow this instructions to upload your info:\n"
622
            "1 - Turn off your radio\n"
623
            "2 - Connect your interface cable\n"
624
            "3 - Turn on your radio\n"
625
            "4 - Do the upload of your radio data\n")
626
        return rp
627

    
628
    def process_mmap(self):
629
        """Process the mem map into the mem object"""
630
        # make lines shorter for style check.
631
        check1 = (len(self._mmap) == self.MEM_TOTAL)
632
        check2 = (len(self._mmap) == self.MEM_TOTAL + len(self.MODEL))
633
        if (check1 or check2):
634
            self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
635
        else:
636
            raise errors.ImageDetectFailed('Image length mismatch.\n'
637
                                           'Try reloading the configuration'
638
                                           ' from the radio.')
639

    
640
    def get_settings(self):
641
        """Translate the bit in the mem_struct into settings in the UI"""
642
        _mem = self._memobj
643
        basic = RadioSettingGroup("basic", "Basic Settings")
644
        bank = RadioSettingGroup("bank", "Bank names")
645
        work = RadioSettingGroup("work", "VFO Mode Settings")
646
        dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
647
        top = RadioSettings(basic, bank, work, dtmfe)
648

    
649
        if _mem.settings.squelch > 0x05:
650
            val = 0x00
651
        else:
652
            val = _mem.settings.squelch
653
        rs = RadioSetting("settings.squelch", "Squelch",
654
                          RadioSettingValueList(
655
                              LIST_OFF1TO5, LIST_OFF1TO5[val]))
656
        basic.append(rs)
657

    
658
        rs = RadioSetting("settings.tot", "Timeout Timer",
659
                          RadioSettingValueList(
660
                              LIST_TIMEOUT, LIST_TIMEOUT[_mem.settings.tot]))
661
        basic.append(rs)
662

    
663
        rs = RadioSetting("settings.savemode", "Save Mode",
664
                          RadioSettingValueBoolean(_mem.settings.savemode))
665
        basic.append(rs)
666

    
667
        rs = RadioSetting("settings.totalarm", "Timeout Timer Alarm",
668
                          RadioSettingValueList(
669
                              LIST_TIMEOUT_ALARM,
670
                              LIST_TIMEOUT_ALARM[_mem.settings.totalarm]))
671
        basic.append(rs)
672

    
673
        rs = RadioSetting("settings.dualstandby", "Dual Watch",
674
                          RadioSettingValueBoolean(_mem.settings.dualstandby))
675
        basic.append(rs)
676

    
677
        if self._pilot_tone:
678
            rs = RadioSetting("settings.tone", "Pilot Tone",
679
                              RadioSettingValueList(
680
                                LIST_PILOT_TONE,
681
                                LIST_PILOT_TONE[_mem.settings.tone]))
682
            basic.append(rs)
683

    
684
        rs = RadioSetting("settings.sidetone", "Side Tone",
685
                          RadioSettingValueList(
686
                              LIST_SIDE_TONE,
687
                              LIST_SIDE_TONE[_mem.settings.sidetone]))
688
        basic.append(rs)
689

    
690
        rs = RadioSetting("settings.tailclear", "Tail Clear",
691
                          RadioSettingValueBoolean(_mem.settings.tailclear))
692
        basic.append(rs)
693

    
694
        rs = RadioSetting("settings.powerondistype", "Power On Display Type",
695
                          RadioSettingValueList(
696
                              LIST_POWERON_DISPLAY_TYPE,
697
                              LIST_POWERON_DISPLAY_TYPE[
698
                                  _mem.settings.powerondistype]))
699
        basic.append(rs)
700

    
701
        rs = RadioSetting("settings.beep", "Beep",
702
                          RadioSettingValueList(
703
                              LIST_BEEP, LIST_BEEP[_mem.settings.beep]))
704
        basic.append(rs)
705

    
706
        rs = RadioSetting("settings.roger", "Roger",
707
                          RadioSettingValueBoolean(_mem.settings.roger))
708
        basic.append(rs)
709

    
710
        rs = RadioSetting("settings.voice", "Language",
711
                          RadioSettingValueList(
712
                              LIST_LANGUAGE,
713
                              LIST_LANGUAGE[_mem.settings.voice]))
714
        basic.append(rs)
715

    
716
        rs = RadioSetting("settings.voicesw", "Enable Voice",
717
                          RadioSettingValueBoolean(_mem.settings.voicesw))
718
        basic.append(rs)
719

    
720
        rs = RadioSetting("settings.scanmode", "Scan Mode",
721
                          RadioSettingValueList(
722
                              LIST_SCANMODE,
723
                              LIST_SCANMODE[_mem.settings.scanmode]))
724
        basic.append(rs)
725

    
726
        rs = RadioSetting("settings.alarmmode", "Alarm Mode",
727
                          RadioSettingValueList(
728
                              LIST_ALARMMODE,
729
                              LIST_ALARMMODE[_mem.settings.alarmmode]))
730
        basic.append(rs)
731

    
732
        rs = RadioSetting("settings.alarmtone", "Sound Alarm",
733
                          RadioSettingValueBoolean(_mem.settings.alarmtone))
734
        basic.append(rs)
735

    
736
        rs = RadioSetting("settings.keylock", "Key Lock",
737
                          RadioSettingValueBoolean(_mem.settings.keylock))
738
        basic.append(rs)
739

    
740
        rs = RadioSetting("settings.fmenable", "Disable FM radio",
741
                          RadioSettingValueBoolean(_mem.settings.fmenable))
742
        basic.append(rs)
743

    
744
        rs = RadioSetting("settings.autolock", "Key Auto Lock",
745
                          RadioSettingValueBoolean(_mem.settings.autolock))
746
        basic.append(rs)
747

    
748
        rs = RadioSetting("settings.menuquittime", "Menu Quit Timer",
749
                          RadioSettingValueList(
750
                              LIST_MENU_QUIT_TIME,
751
                              LIST_MENU_QUIT_TIME[
752
                                  _mem.settings.menuquittime]))
753
        basic.append(rs)
754

    
755
        rs = RadioSetting("settings.backlight", "Backlight Timer",
756
                          RadioSettingValueList(
757
                              LIST_BACKLIGHT_TIMER,
758
                              LIST_BACKLIGHT_TIMER[_mem.settings.backlight]))
759
        basic.append(rs)
760

    
761
        if self._send_id_delay:
762
            rs = RadioSetting("settings.pttdly", "Send ID Delay",
763
                              RadioSettingValueList(
764
                                LIST_ID_DELAY,
765
                                LIST_ID_DELAY[_mem.settings.pttdly]))
766
            basic.append(rs)
767

    
768
        rs = RadioSetting("settings.ctsdcsscantype", "QT Save Mode",
769
                          RadioSettingValueList(
770
                              LIST_QT_SAVEMODE,
771
                              LIST_QT_SAVEMODE[_mem.settings.ctsdcsscantype]))
772
        basic.append(rs)
773

    
774
        def getKey2shortIndex(value):
775
            if value == 0x07:
776
                return 0
777
            if value == 0x1C:
778
                return 1
779
            if value == 0x1D:
780
                return 2
781
            if value == 0x2D:
782
                return 3
783
            return 0
784

    
785
        def apply_Key2short(setting, obj):
786
            val = str(setting.value)
787
            if val == "FM":
788
                obj.key2short = 0x07
789
            if val == "Scan":
790
                obj.key2short = 0x1C
791
            if val == "Search":
792
                obj.key2short = 0x1D
793
            if val == "Vox":
794
                obj.key2short = 0x2D
795

    
796
        if self._skey2_short:
797
            rs = RadioSetting("settings.key2short", "Skey2 Short",
798
                              RadioSettingValueList(
799
                                LIST_SKEY2_SHORT,
800
                                LIST_SKEY2_SHORT[
801
                                    getKey2shortIndex(
802
                                        _mem.settings.key2short)]))
803
            rs.set_apply_callback(apply_Key2short, _mem.settings)
804
            basic.append(rs)
805

    
806
        rs = RadioSetting("settings.chadistype", "Channel A display type",
807
                          RadioSettingValueList(
808
                              LIST_MODE, LIST_MODE[_mem.settings.chadistype]))
809
        basic.append(rs)
810

    
811
        rs = RadioSetting("settings.chaworkmode", "Channel A work mode",
812
                          RadioSettingValueList(
813
                              LIST_WORKMODE,
814
                              LIST_WORKMODE[_mem.settings.chaworkmode]))
815
        basic.append(rs)
816

    
817
        rs = RadioSetting("settings.chbdistype", "Channel B display type",
818
                          RadioSettingValueList(
819
                              LIST_MODE, LIST_MODE[_mem.settings.chbdistype]))
820
        basic.append(rs)
821

    
822
        rs = RadioSetting("settings.chbworkmode", "Channel B work mode",
823
                          RadioSettingValueList(
824
                              LIST_WORKMODE,
825
                              LIST_WORKMODE[_mem.settings.chbworkmode]))
826
        basic.append(rs)
827

    
828
        rs = RadioSetting("settings.rpttailclear", "Rpt Tail Clear",
829
                          RadioSettingValueList(
830
                              LIST_RPT_TAIL_CLEAR,
831
                              LIST_RPT_TAIL_CLEAR[
832
                                  _mem.settings.rpttailclear]))
833
        basic.append(rs)
834

    
835
        rs = RadioSetting("settings.rpttaildet", "Rpt Tail Delay",
836
                          RadioSettingValueList(
837
                              LIST_RPT_TAIL_CLEAR,
838
                              LIST_RPT_TAIL_CLEAR[_mem.settings.rpttaildet]))
839
        basic.append(rs)
840

    
841
        rs = RadioSetting("settings.rstmenu", "Enable Menu Rst",
842
                          RadioSettingValueBoolean(_mem.settings.rstmenu))
843
        basic.append(rs)
844

    
845
        if self._voxsw:
846
            rs = RadioSetting("settings.voxsw", "Vox Switch",
847
                              RadioSettingValueBoolean(_mem.settings.voxsw))
848
            basic.append(rs)
849

    
850
            rs = RadioSetting("settings.vox", "Vox Level",
851
                              RadioSettingValueList(
852
                                LIST_VOX_LEVEL_ALT,
853
                                LIST_VOX_LEVEL_ALT[_mem.settings.vox]))
854
            basic.append(rs)
855
        else:
856
            rs = RadioSetting("settings.vox", "Vox Level",
857
                              RadioSettingValueList(
858
                                LIST_VOX_LEVEL,
859
                                LIST_VOX_LEVEL[_mem.settings.vox]))
860
            basic.append(rs)
861

    
862
        rs = RadioSetting("settings.voxdlytime", "Vox Delay Time",
863
                          RadioSettingValueList(
864
                              LIST_VOX_DELAY_TIME,
865
                              LIST_VOX_DELAY_TIME[_mem.settings.voxdlytime]))
866
        basic.append(rs)
867

    
868
        if self._has_gps:
869
            rs = RadioSetting("settings.gpsw", "GPS On",
870
                              RadioSettingValueBoolean(_mem.settings.gpsw))
871
            basic.append(rs)
872

    
873
            rs = RadioSetting("settings.gpsmode", "GPS Mode",
874
                              RadioSettingValueList(
875
                                LIST_GPS_MODE,
876
                                LIST_GPS_MODE[_mem.settings.gpsmode]))
877
            basic.append(rs)
878

    
879
            rs = RadioSetting("settings.gpstimezone", "GPS Timezone",
880
                              RadioSettingValueList(
881
                                LIST_GPS_TIMEZONE,
882
                                LIST_GPS_TIMEZONE[_mem.settings.gpstimezone]))
883
            basic.append(rs)
884

    
885
        def _filterName(name, zone):
886
            fname = ""
887
            charset = chirp_common.CHARSET_ASCII
888
            for char in name:
889
                if ord(str(char)) == 255:
890
                    break
891
                if str(char) not in charset:
892
                    char = "X"
893
                fname += str(char)
894
            if fname == "XXXXXX":
895
                fname = "ZONE" + zone
896
            return fname
897

    
898
        if self._support_banknames:
899
            _zone = "01"
900
            _msg = _mem.bank_names
901
            rs = RadioSetting("bank_names.name1", "Bank name 1",
902
                              RadioSettingValueString(
903
                                0, 12, _filterName(_msg.name1, _zone)))
904
            bank.append(rs)
905

    
906
            _zone = "02"
907
            _msg = _mem.bank_names
908
            rs = RadioSetting("bank_names.name2", "Bank name 2",
909
                              RadioSettingValueString(
910
                                0, 12, _filterName(_msg.name2, _zone)))
911

    
912
            bank.append(rs)
913
            _zone = "03"
914
            _msg = _mem.bank_names
915
            rs = RadioSetting("bank_names.name3", "Bank name 3",
916
                              RadioSettingValueString(
917
                                0, 12, _filterName(_msg.name3, _zone)))
918
            bank.append(rs)
919

    
920
            _zone = "04"
921
            _msg = _mem.bank_names
922
            rs = RadioSetting("bank_names.name4", "Bank name 4",
923
                              RadioSettingValueString(
924
                                0, 12, _filterName(_msg.name4, _zone)))
925
            bank.append(rs)
926

    
927
            _zone = "05"
928
            _msg = _mem.bank_names
929
            rs = RadioSetting("bank_names.name5", "Bank name 5",
930
                              RadioSettingValueString(
931
                                0, 12, _filterName(_msg.name5, _zone)))
932
            bank.append(rs)
933

    
934
            _zone = "06"
935
            _msg = _mem.bank_names
936
            rs = RadioSetting("bank_names.name6", "Bank name 6",
937
                              RadioSettingValueString(
938
                                0, 12, _filterName(_msg.name6, _zone)))
939
            bank.append(rs)
940

    
941
            _zone = "07"
942
            _msg = _mem.bank_names
943
            rs = RadioSetting("bank_names.name7", "Bank name 7",
944
                              RadioSettingValueString(
945
                                0, 12, _filterName(_msg.name7, _zone)))
946
            bank.append(rs)
947

    
948
            _zone = "08"
949
            _msg = _mem.bank_names
950
            rs = RadioSetting("bank_names.name8", "Bank name 8",
951
                              RadioSettingValueString(
952
                                0, 12, _filterName(_msg.name8, _zone)))
953
            bank.append(rs)
954

    
955
            _zone = "09"
956
            _msg = _mem.bank_names
957
            rs = RadioSetting("bank_names.name9", "Bank name 9",
958
                              RadioSettingValueString(
959
                                0, 12, _filterName(_msg.name9, _zone)))
960
            bank.append(rs)
961

    
962
            _zone = "10"
963
            _msg = _mem.bank_names
964
            rs = RadioSetting("bank_names.name10", "Bank name 10",
965
                              RadioSettingValueString(
966
                                0, 12, _filterName(_msg.name10, _zone)))
967
            bank.append(rs)
968

    
969
        _codeobj = self._memobj.ani.code
970
        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
971
        val = RadioSettingValueString(0, 5, _code, False)
972
        val.set_charset(DTMF_CHARS)
973
        rs = RadioSetting("ani.code", "ANI Code", val)
974

    
975
        # DTMF settings
976
        def apply_code(setting, obj, length):
977
            code = []
978
            for j in range(0, length):
979
                try:
980
                    code.append(DTMF_CHARS.index(str(setting.value)[j]))
981
                except IndexError:
982
                    code.append(0xFF)
983
            obj.code = code
984

    
985
        for i in range(0, 20):
986
            _codeobj = self._memobj.pttid[i].code
987
            _code = "".join([
988
                DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
989
            val = RadioSettingValueString(0, 5, _code, False)
990
            val.set_charset(DTMF_CHARS)
991
            pttid = RadioSetting("pttid/%i.code" % i,
992
                                 "Signal Code %i" % (i + 1), val)
993
            pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
994
            dtmfe.append(pttid)
995

    
996
        if _mem.ani.dtmfon > 0xC3:
997
            val = 0x03
998
        else:
999
            val = _mem.ani.dtmfon
1000
        rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
1001
                          RadioSettingValueList(LIST_DTMFSPEED,
1002
                                                LIST_DTMFSPEED[val]))
1003
        dtmfe.append(rs)
1004

    
1005
        if _mem.ani.dtmfoff > 0xC3:
1006
            val = 0x03
1007
        else:
1008
            val = _mem.ani.dtmfoff
1009
        rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
1010
                          RadioSettingValueList(LIST_DTMFSPEED,
1011
                                                LIST_DTMFSPEED[val]))
1012
        dtmfe.append(rs)
1013

    
1014
        _codeobj = self._memobj.ani.code
1015
        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1016
        val = RadioSettingValueString(0, 5, _code, False)
1017
        val.set_charset(DTMF_CHARS)
1018
        rs = RadioSetting("ani.code", "ANI Code", val)
1019
        rs.set_apply_callback(apply_code, self._memobj.ani, 5)
1020
        dtmfe.append(rs)
1021

    
1022
        if self._aniid:
1023
            rs = RadioSetting("ani.aniid", "When to send ANI ID",
1024
                              RadioSettingValueList(LIST_PTTID,
1025
                                                    LIST_PTTID[
1026
                                                        _mem.ani.aniid]))
1027
            dtmfe.append(rs)
1028

    
1029
        rs = RadioSetting("settings.hangup", "Hang-up time",
1030
                          RadioSettingValueList(LIST_HANGUPTIME,
1031
                                                LIST_HANGUPTIME[
1032
                                                    _mem.settings.hangup]))
1033
        dtmfe.append(rs)
1034

    
1035
        def convert_bytes_to_freq(bytes):
1036
            real_freq = 0
1037
            for byte in bytes:
1038
                real_freq = (real_freq * 10) + byte
1039
            return chirp_common.format_freq(real_freq * 10)
1040

    
1041
        def my_validate(value):
1042
            value = chirp_common.parse_freq(value)
1043
            freqOk = False
1044
            for band in self.VALID_BANDS:
1045
                if value > band[0] and value < band[1]:
1046
                    freqOk = True
1047
            if not freqOk:
1048
                raise InvalidValueError("Invalid frequency!")
1049
            return chirp_common.format_freq(value)
1050

    
1051
        def apply_freq(setting, obj):
1052
            value = chirp_common.parse_freq(str(setting.value)) / 10
1053
            for i in range(7, -1, -1):
1054
                obj.freq[i] = value % 10
1055
                value /= 10
1056

    
1057
        val1a = RadioSettingValueString(0, 10,
1058
                                        convert_bytes_to_freq(
1059
                                            _mem.vfo.a.freq))
1060
        val1a.set_validate_callback(my_validate)
1061
        rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a)
1062
        rs.set_apply_callback(apply_freq, _mem.vfo.a)
1063
        work.append(rs)
1064

    
1065
        rs = RadioSetting("vfo.a.sftd", "VFO A Offset dir",
1066
                          RadioSettingValueList(
1067
                              LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.a.sftd]))
1068
        work.append(rs)
1069

    
1070
        def convert_bytes_to_offset(bytes):
1071
            real_offset = 0
1072
            for byte in bytes:
1073
                real_offset = (real_offset * 10) + byte
1074
            return chirp_common.format_freq(real_offset * 1000)
1075

    
1076
        def apply_offset(setting, obj):
1077
            value = chirp_common.parse_freq(str(setting.value)) / 1000
1078
            for i in range(5, -1, -1):
1079
                obj.offset[i] = value % 10
1080
                value /= 10
1081

    
1082
        val1a = RadioSettingValueString(
1083
                    0, 10, convert_bytes_to_offset(_mem.vfo.a.offset))
1084
        rs = RadioSetting("vfo.a.offset",
1085
                          "VFO A Offset", val1a)
1086
        rs.set_apply_callback(apply_offset, _mem.vfo.a)
1087
        work.append(rs)
1088

    
1089
        def apply_txpower_listvalue(setting, obj):
1090
            LOG.debug("Setting value: " + str(
1091
                      setting.value) + " from list")
1092
            val = str(setting.value)
1093
            index = TXP_CHOICES.index(val)
1094
            val = TXP_VALUES[index]
1095
            obj.set_value(val)
1096

    
1097
        rs = RadioSetting("vfo.a.lowpower", "VFO A Power",
1098
                          RadioSettingValueList(
1099
                                LIST_TXPOWER,
1100
                                LIST_TXPOWER[
1101
                                    min(_mem.vfo.a.lowpower, 0x02)]
1102
                                            ))
1103
        work.append(rs)
1104

    
1105
        rs = RadioSetting("vfo.a.wide", "VFO A Bandwidth",
1106
                          RadioSettingValueList(
1107
                              LIST_BANDWIDTH,
1108
                              LIST_BANDWIDTH[_mem.vfo.a.wide]))
1109
        work.append(rs)
1110

    
1111
        rs = RadioSetting("vfo.a.scode", "VFO A S-CODE",
1112
                          RadioSettingValueList(
1113
                              LIST_SCODE,
1114
                              LIST_SCODE[_mem.vfo.a.scode]))
1115
        work.append(rs)
1116

    
1117
        rs = RadioSetting("vfo.a.step", "VFO A Tuning Step",
1118
                          RadioSettingValueList(
1119
                              LIST_STEP, LIST_STEP[_mem.vfo.a.step]))
1120
        work.append(rs)
1121

    
1122
        rs = RadioSetting("vfo.a.fhss", "VFO A FHSS",
1123
                          RadioSettingValueBoolean(_mem.vfo.a.fhss))
1124
        work.append(rs)
1125

    
1126
        def getToneIndex(tone):
1127
            if tone in [0, 0xFFFF]:
1128
                index = 0
1129
            elif tone >= 0x0258:
1130
                index = self.RXTX_CODES.index(str(tone / 10.0))
1131
            elif tone <= 0x0258:
1132
                index = 50 + tone
1133
                if tone > 0x69:
1134
                    index = tone - 0x6A + 156
1135
            return self.RXTX_CODES[index]
1136

    
1137
        def apply_rxtone(setting, obj):
1138
            index = self.RXTX_CODES.index(str(setting.value))
1139
            if index > 156:
1140
                obj.rxtone = index - 156 + 0x6A
1141
            elif index > 50:
1142
                obj.rxtone = index - 50
1143
            elif index == 0:
1144
                obj.rxtone = 0
1145
            else:
1146
                obj.rxtone = int(float(setting.value)*10)
1147

    
1148
        def apply_txtone(setting, obj):
1149
            index = self.RXTX_CODES.index(str(setting.value))
1150
            if index > 156:
1151
                obj.txtone = index - 156 + 0x6A
1152
            elif index > 50:
1153
                obj.txtone = index - 50
1154
            elif index == 0:
1155
                obj.txtone = 0
1156
            else:
1157
                obj.txtone = int(float(setting.value)*10)
1158

    
1159
        rs = RadioSetting("vfo.a.rxtone", "VFA A RX QT/DQT",
1160
                          RadioSettingValueList(self.RXTX_CODES,
1161
                                                getToneIndex(
1162
                                                    _mem.vfo.a.rxtone)))
1163
        rs.set_apply_callback(apply_rxtone, _mem.vfo.a)
1164
        work.append(rs)
1165

    
1166
        rs = RadioSetting("vfo.a.txtone", "VFA A TX QT/DQT",
1167
                          RadioSettingValueList(self.RXTX_CODES,
1168
                                                getToneIndex(
1169
                                                    _mem.vfo.a.txtone)))
1170
        rs.set_apply_callback(apply_txtone, _mem.vfo.a)
1171
        work.append(rs)
1172

    
1173
        val1b = RadioSettingValueString(0, 10,
1174
                                        convert_bytes_to_freq(
1175
                                            _mem.vfo.b.freq))
1176
        val1b.set_validate_callback(my_validate)
1177
        rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b)
1178
        rs.set_apply_callback(apply_freq, _mem.vfo.b)
1179
        work.append(rs)
1180

    
1181
        rs = RadioSetting("vfo.b.sftd", "VFO B Offset dir",
1182
                          RadioSettingValueList(
1183
                              LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.b.sftd]))
1184
        work.append(rs)
1185

    
1186
        val1b = RadioSettingValueString(
1187
                    0, 10, convert_bytes_to_offset(_mem.vfo.b.offset))
1188
        rs = RadioSetting("vfo.b.offset",
1189
                          "VFO B Offset", val1b)
1190
        rs.set_apply_callback(apply_offset, _mem.vfo.b)
1191
        work.append(rs)
1192

    
1193
        rs = RadioSetting("vfo.b.lowpower", "VFO B Power",
1194
                          RadioSettingValueList(
1195
                                LIST_TXPOWER,
1196
                                LIST_TXPOWER[min(_mem.vfo.b.lowpower, 0x02)]
1197
                                            ))
1198
        work.append(rs)
1199

    
1200
        rs = RadioSetting("vfo.b.wide", "VFO B Bandwidth",
1201
                          RadioSettingValueList(
1202
                              LIST_BANDWIDTH,
1203
                              LIST_BANDWIDTH[_mem.vfo.b.wide]))
1204
        work.append(rs)
1205

    
1206
        rs = RadioSetting("vfo.b.scode", "VFO B S-CODE",
1207
                          RadioSettingValueList(
1208
                              LIST_SCODE,
1209
                              LIST_SCODE[_mem.vfo.b.scode]))
1210
        work.append(rs)
1211

    
1212
        rs = RadioSetting("vfo.b.step", "VFO B Tuning Step",
1213
                          RadioSettingValueList(
1214
                              LIST_STEP, LIST_STEP[_mem.vfo.b.step]))
1215
        work.append(rs)
1216

    
1217
        rs = RadioSetting("vfo.b.fhss", "VFO B FHSS",
1218
                          RadioSettingValueBoolean(_mem.vfo.b.fhss))
1219
        work.append(rs)
1220

    
1221
        rs = RadioSetting("vfo.b.rxtone", "VFA B RX QT/DQT",
1222
                          RadioSettingValueList(self.RXTX_CODES,
1223
                                                getToneIndex(
1224
                                                    _mem.vfo.b.rxtone)))
1225
        rs.set_apply_callback(apply_rxtone, _mem.vfo.b)
1226
        work.append(rs)
1227

    
1228
        rs = RadioSetting("vfo.b.txtone", "VFA B TX QT/DQT",
1229
                          RadioSettingValueList(self.RXTX_CODES,
1230
                                                getToneIndex(
1231
                                                    _mem.vfo.b.txtone)))
1232
        rs.set_apply_callback(apply_txtone, _mem.vfo.b)
1233
        work.append(rs)
1234

    
1235
        if self._vfoscan:
1236
            def scan_validate(value):
1237
                freqOk = False
1238
                for band in self.VALID_BANDS:
1239
                    minBand = (band[0]/1000000)
1240
                    maxBand = (band[1]/1000000)
1241
                    if value >= minBand and value <= maxBand:
1242
                        freqOk = True
1243
                if not freqOk:
1244
                    raise InvalidValueError("Invalid frequency!")
1245
                return value
1246

    
1247
            scanMin = RadioSettingValueInteger(0, 800,
1248
                                               _mem.settings.vfoscanmin)
1249
            scanMin.set_validate_callback(scan_validate)
1250
            rs = RadioSetting("settings.vfoscanmin", "VFO scan range minimum",
1251
                              scanMin)
1252
            work.append(rs)
1253

    
1254
            scanMax = RadioSettingValueInteger(0, 800,
1255
                                               _mem.settings.vfoscanmax)
1256
            scanMax.set_validate_callback(scan_validate)
1257
            rs = RadioSetting("settings.vfoscanmax", "VFO scan range maximum",
1258
                              scanMax)
1259
            work.append(rs)
1260

    
1261
        rs = RadioSetting("settings.bcl", "BCL",
1262
                          RadioSettingValueBoolean(_mem.settings.bcl))
1263
        work.append(rs)
1264

    
1265
        rs = RadioSetting("settings.pttid", "PTT ID",
1266
                          RadioSettingValueList(self.PTTID_LIST,
1267
                                                self.PTTID_LIST[
1268
                                                    _mem.settings.pttid]))
1269
        work.append(rs)
1270

    
1271
        return top
1272

    
1273
    def sync_in(self):
1274
        """Download from radio"""
1275
        try:
1276
            data = _download(self)
1277
        except errors.RadioError:
1278
            # Pass through any real errors we raise
1279
            raise
1280
        except Exception:
1281
            # If anything unexpected happens, make sure we raise
1282
            # a RadioError and log the problem
1283
            LOG.exception('Unexpected error during download')
1284
            raise errors.RadioError('Unexpected error communicating '
1285
                                    'with the radio')
1286
        self._mmap = memmap.MemoryMapBytes(data)
1287

    
1288
        self.process_mmap()
1289

    
1290
    def sync_out(self):
1291
        """Upload to radio"""
1292
        try:
1293
            _upload(self)
1294
        except errors.RadioError:
1295
            raise
1296
        except Exception:
1297
            # If anything unexpected happens, make sure we raise
1298
            # a RadioError and log the problem
1299
            LOG.exception('Unexpected error during upload')
1300
            raise errors.RadioError('Unexpected error communicating '
1301
                                    'with the radio')
1302

    
1303
    def get_features(self):
1304
        """Get the radio's features"""
1305

    
1306
        rf = chirp_common.RadioFeatures()
1307
        rf.has_settings = True
1308
        rf.has_bank = False
1309
        rf.has_tuning_step = False
1310
        rf.can_odd_split = True
1311
        rf.has_name = True
1312
        rf.has_offset = True
1313
        rf.has_mode = True
1314
        rf.has_dtcs = True
1315
        rf.has_rx_dtcs = True
1316
        rf.has_dtcs_polarity = True
1317
        rf.has_ctone = True
1318
        rf.has_cross = True
1319
        rf.valid_modes = self.MODES
1320
        rf.valid_characters = self.VALID_CHARS
1321
        rf.valid_name_length = self.LENGTH_NAME
1322
        if self._gmrs:
1323
            rf.valid_duplexes = ["", "+", "off"]
1324
        else:
1325
            rf.valid_duplexes = ["", "-", "+", "split", "off"]
1326
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
1327
        rf.valid_cross_modes = [
1328
            "Tone->Tone",
1329
            "DTCS->",
1330
            "->DTCS",
1331
            "Tone->DTCS",
1332
            "DTCS->Tone",
1333
            "->Tone",
1334
            "DTCS->DTCS"]
1335
        rf.valid_skips = self.SKIP_VALUES
1336
        rf.valid_dtcs_codes = self.DTCS_CODES
1337
        rf.memory_bounds = (0, 999)
1338
        rf.valid_power_levels = self.POWER_LEVELS
1339
        rf.valid_bands = self.VALID_BANDS
1340
        rf.valid_tuning_steps = STEPS
1341

    
1342
        return rf
1343

    
1344
    def _is_txinh(self, _mem):
1345
        raw_tx = b""
1346
        for i in range(0, 4):
1347
            raw_tx += _mem.txfreq[i].get_raw()
1348
        return raw_tx == b"\xFF\xFF\xFF\xFF"
1349

    
1350
    def get_memory(self, number):
1351
        _mem = self._memobj.memory[number]
1352

    
1353
        mem = chirp_common.Memory()
1354
        mem.number = number
1355

    
1356
        if _mem.get_raw()[0] == 255:
1357
            mem.empty = True
1358
            return mem
1359

    
1360
        mem.freq = int(_mem.rxfreq) * 10
1361

    
1362
        if self._is_txinh(_mem):
1363
            # TX freq not set
1364
            mem.duplex = "off"
1365
            mem.offset = 0
1366
        else:
1367
            # TX freq set
1368
            offset = (int(_mem.txfreq) * 10) - mem.freq
1369
            if offset != 0:
1370
                if _split(self.get_features(), mem.freq, int(
1371
                          _mem.txfreq) * 10):
1372
                    mem.duplex = "split"
1373
                    mem.offset = int(_mem.txfreq) * 10
1374
                elif offset < 0:
1375
                    mem.offset = abs(offset)
1376
                    mem.duplex = "-"
1377
                elif offset > 0:
1378
                    mem.offset = offset
1379
                    mem.duplex = "+"
1380
            else:
1381
                mem.offset = 0
1382

    
1383
        for char in _mem.name:
1384
            if str(char) == "\xFF":
1385
                char = " "  # The OEM software may have 0xFF mid-name
1386
            mem.name += str(char)
1387
        mem.name = mem.name.rstrip()
1388

    
1389
        if not _mem.scan:
1390
            mem.skip = "S"
1391

    
1392
        levels = self.POWER_LEVELS
1393
        try:
1394
            mem.power = levels[_mem.lowpower]
1395
        except IndexError:
1396
            mem.power = levels[0]
1397

    
1398
        mem.mode = _mem.wide and "NFM" or "FM"
1399

    
1400
        dtcs_pol = ["N", "N"]
1401

    
1402
        if _mem.txtone in [0, 0xFFFF]:
1403
            txmode = ""
1404
        elif _mem.txtone >= 0x0258:
1405
            txmode = "Tone"
1406
            mem.rtone = int(_mem.txtone) / 10.0
1407
        elif _mem.txtone <= 0x0258:
1408
            txmode = "DTCS"
1409
            if _mem.txtone > 0x69:
1410
                index = _mem.txtone - 0x6A
1411
                dtcs_pol[0] = "R"
1412
            else:
1413
                index = _mem.txtone - 1
1414
            mem.dtcs = self.DTCS_CODES[index]
1415
        else:
1416
            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
1417

    
1418
        if _mem.rxtone in [0, 0xFFFF]:
1419
            rxmode = ""
1420
        elif _mem.rxtone >= 0x0258:
1421
            rxmode = "Tone"
1422
            mem.ctone = int(_mem.rxtone) / 10.0
1423
        elif _mem.rxtone <= 0x0258:
1424
            rxmode = "DTCS"
1425
            if _mem.rxtone >= 0x6A:
1426
                index = _mem.rxtone - 0x6A
1427
                dtcs_pol[1] = "R"
1428
            else:
1429
                index = _mem.rxtone - 1
1430
            mem.rx_dtcs = self.DTCS_CODES[index]
1431
        else:
1432
            LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
1433

    
1434
        if txmode == "Tone" and not rxmode:
1435
            mem.tmode = "Tone"
1436
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
1437
            mem.tmode = "TSQL"
1438
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
1439
            mem.tmode = "DTCS"
1440
        elif rxmode or txmode:
1441
            mem.tmode = "Cross"
1442
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1443

    
1444
        mem.dtcs_polarity = "".join(dtcs_pol)
1445

    
1446
        mem.extra = RadioSettingGroup("Extra", "extra")
1447

    
1448
        rs = RadioSetting("bcl", "BCL",
1449
                          RadioSettingValueBoolean(_mem.bcl))
1450
        mem.extra.append(rs)
1451

    
1452
        if (_mem.pttid != 0xFF):
1453
            rs = RadioSetting("pttid", "PTT ID",
1454
                              RadioSettingValueList(self.PTTID_LIST,
1455
                                                    self.PTTID_LIST[
1456
                                                        _mem.pttid]))
1457
            mem.extra.append(rs)
1458

    
1459
        if (_mem.scode != 0xFF):
1460
            rs = RadioSetting("scode", "S-CODE",
1461
                              RadioSettingValueList(self.SCODE_LIST,
1462
                                                    self.SCODE_LIST[
1463
                                                        _mem.scode]))
1464
            mem.extra.append(rs)
1465

    
1466
        return mem
1467

    
1468
    def set_memory(self, mem):
1469
        _mem = self._memobj.memory[mem.number]
1470

    
1471
        _mem.set_raw("\x00"*16 + "\xff" * 16)
1472

    
1473
        if mem.empty:
1474
            _mem.set_raw("\xff" * 32)
1475
            return
1476

    
1477
        _mem.rxfreq = mem.freq / 10
1478

    
1479
        if mem.duplex == "off":
1480
            for i in range(0, 4):
1481
                _mem.txfreq[i].set_raw("\xFF")
1482
        elif mem.duplex == "split":
1483
            _mem.txfreq = mem.offset / 10
1484
        elif mem.duplex == "+":
1485
            _mem.txfreq = (mem.freq + mem.offset) / 10
1486
        elif mem.duplex == "-":
1487
            _mem.txfreq = (mem.freq - mem.offset) / 10
1488
        else:
1489
            _mem.txfreq = mem.freq / 10
1490

    
1491
        _namelength = self.get_features().valid_name_length
1492
        for i in range(_namelength):
1493
            try:
1494
                _mem.name[i] = mem.name[i]
1495
            except IndexError:
1496
                _mem.name[i] = "\xFF"
1497

    
1498
        rxmode = txmode = ""
1499
        if mem.tmode == "Tone":
1500
            _mem.txtone = int(mem.rtone * 10)
1501
            _mem.rxtone = 0
1502
        elif mem.tmode == "TSQL":
1503
            _mem.txtone = int(mem.ctone * 10)
1504
            _mem.rxtone = int(mem.ctone * 10)
1505
        elif mem.tmode == "DTCS":
1506
            rxmode = txmode = "DTCS"
1507
            _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
1508
            _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
1509
        elif mem.tmode == "Cross":
1510
            txmode, rxmode = mem.cross_mode.split("->", 1)
1511
            if txmode == "Tone":
1512
                _mem.txtone = int(mem.rtone * 10)
1513
            elif txmode == "DTCS":
1514
                _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
1515
            else:
1516
                _mem.txtone = 0
1517
            if rxmode == "Tone":
1518
                _mem.rxtone = int(mem.ctone * 10)
1519
            elif rxmode == "DTCS":
1520
                _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
1521
            else:
1522
                _mem.rxtone = 0
1523
        else:
1524
            _mem.rxtone = 0
1525
            _mem.txtone = 0
1526

    
1527
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
1528
            _mem.txtone += 0x69
1529
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
1530
            _mem.rxtone += 0x69
1531

    
1532
        _mem.scan = mem.skip != "S"
1533
        _mem.wide = mem.mode == "NFM"
1534

    
1535
        if mem.power:
1536
            _mem.lowpower = self.POWER_LEVELS.index(mem.power)
1537
        else:
1538
            _mem.lowpower = 0
1539

    
1540
        # extra settings
1541
        if len(mem.extra) > 0:
1542
            # there are setting, parse
1543
            for setting in mem.extra:
1544
                if setting.get_name() == "scode":
1545
                    setattr(_mem, setting.get_name(), str(int(setting.value)))
1546
                else:
1547
                    setattr(_mem, setting.get_name(), setting.value)
1548
        else:
1549
            # there are no extra settings, load defaults
1550
            _mem.bcl = 0
1551
            _mem.pttid = 0
1552
            _mem.scode = 0
1553

    
1554
    def set_settings(self, settings):
1555
        _settings = self._memobj.settings
1556
        for element in settings:
1557
            if not isinstance(element, RadioSetting):
1558
                if element.get_name() == "fm_preset":
1559
                    self._set_fm_preset(element)
1560
                else:
1561
                    self.set_settings(element)
1562
                    continue
1563
            else:
1564
                try:
1565
                    name = element.get_name()
1566
                    if "." in name:
1567
                        bits = name.split(".")
1568
                        obj = self._memobj
1569
                        for bit in bits[:-1]:
1570
                            if "/" in bit:
1571
                                bit, index = bit.split("/", 1)
1572
                                index = int(index)
1573
                                obj = getattr(obj, bit)[index]
1574
                            else:
1575
                                obj = getattr(obj, bit)
1576
                        setting = bits[-1]
1577
                    else:
1578
                        obj = _settings
1579
                        setting = element.get_name()
1580

    
1581
                    if element.has_apply_callback():
1582
                        LOG.debug("Using apply callback")
1583
                        element.run_apply_callback()
1584
                    elif element.value.get_mutable():
1585
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1586
                        setattr(obj, setting, element.value)
1587
                except Exception:
1588
                    LOG.debug(element.get_name())
1589
                    raise
1590

    
1591
    def _set_fm_preset(self, settings):
1592
        for element in settings:
1593
            try:
1594
                val = element.value
1595
                if self._memobj.fm_presets <= 108.0 * 10 - 650:
1596
                    value = int(val.get_value() * 10 - 650)
1597
                else:
1598
                    value = int(val.get_value() * 10)
1599
                LOG.debug("Setting fm_presets = %s" % (value))
1600
                if self._bw_shift:
1601
                    value = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
1602
                self._memobj.fm_presets = value
1603
            except Exception:
1604
                LOG.debug(element.get_name())
1605
                raise
1606

    
1607

    
1608
@directory.register
1609
class UV17ProGPS(UV17Pro):
1610
    VENDOR = "Baofeng"
1611
    MODEL = "UV-17ProGPS"
1612
    _support_banknames = True
1613
    _magic = MSTRING_UV17PROGPS
1614
    _magics = [b"\x46", b"\x4d", b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01" +
1615
               b"\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09" +
1616
               b"\x10\x04\x00"]
1617
    _magicResponseLengths = [16, 7, 1]
1618
    _aniid = False
1619
    _vfoscan = True
1620
    _has_gps = True
1621
    _voxsw = True
1622
    _pilot_tone = True
1623
    _send_id_delay = True
1624
    _skey2_short = True
1625
    VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
1626
                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
1627

    
1628

    
1629
@directory.register
1630
class UV17L(UV17Pro):
1631
    VENDOR = "Baofeng"
1632
    MODEL = "UV-17L"
(16-16/17)