Project

General

Profile

New Model #11006 » baofeng_uv17Pro - GM-5RH.py

Jim Unroe, 03/18/2024 05:17 AM

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

    
17
import logging
18

    
19
from chirp.drivers import baofeng_common as bfc
20
from chirp import chirp_common, directory, memmap
21
from chirp import bitwise
22
from chirp.settings import RadioSetting, \
23
    RadioSettingValueBoolean, RadioSettingValueList, \
24
    InvalidValueError, RadioSettingValueString, \
25
    RadioSettings, RadioSettingGroup
26
import struct
27
from chirp import errors, util
28

    
29
LOG = logging.getLogger(__name__)
30

    
31
# Baofeng UV-17L magic string
32
MSTRING_UV17L = b"PROGRAMBFNORMALU"
33
MSTRING_UV17PROGPS = b"PROGRAMCOLORPROU"
34
# Baofeng GM-5RH magic string
35
MSTRING_GM5RH = b"PROGRAMBFGMRS05U"
36

    
37
DTMF_CHARS = "0123456789 *#ABCD"
38
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
39
LIST_STEPS = ["2.5", "5.0", "6.25", "10.0", "12.5", "20.0", "25.0", "50.0"]
40

    
41
LIST_AB = ["A", "B"]
42
LIST_BANDWIDTH = ["Wide", "Narrow"]
43
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 500]]
44
LIST_HANGUPTIME = ["%s s" % x for x in [3, 4, 5, 6, 7, 8, 9, 10]]
45
LIST_SIDE_TONE = ["Off", "Side Key", "ID", "Side + ID"]
46
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
47
LIST_TIMEOUT_ALARM = ["Off"] + ["%s sec" % x for x in range(1, 11)]
48
LIST_PILOT_TONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
49
LIST_VOICE = ["Off", "English", "Chinese"]
50
LIST_WORKMODE = ["Frequency", "Channel"]
51
LIST_BEEP = ["Off", "Beep", "Voice", "Both"]
52
LIST_SCANMODE = ["Time", "Carrier", "Search"]
53
LIST_ALARMMODE = ["Local", "Send Tone", "Send Code"]
54
LIST_MENU_QUIT_TIME = ["%s sec" % x for x in range(5, 55, 5)] + ["60 sec"]
55
LIST_ID_DELAY = ["%s ms" % x for x in range(100, 3100, 100)]
56
LIST_QT_SAVEMODE = ["Both", "RX", "TX"]
57
LIST_SKEY2_SHORT = ["FM", "Scan", "Search", "Vox"]
58
LIST_RPT_TAIL_CLEAR = ["%s ms" % x for x in range(0, 1100, 100)]
59
LIST_VOX_DELAY_TIME = ["%s ms" % x for x in range(500, 2100, 100)]
60
LIST_VOX_LEVEL = ["Off"] + ["%s" % x for x in range(1, 10, 1)]
61
LIST_VOX_LEVEL_ALT = ["%s" % x for x in range(1, 10, 1)]
62
LIST_GPS_MODE = ["GPS", "Beidou", "GPS + Beidou"]
63
LIST_GPS_TIMEZONE = ["%s" % x for x in range(-12, 13, 1)]
64
LIST_SHIFTS = ["Off", "+", "-"]
65
CHARSET_GB2312 = chirp_common.CHARSET_ASCII
66
for x in range(0xB0, 0xD7):
67
    for y in range(0xA1, 0xFF):
68
        CHARSET_GB2312 += bytes([x, y]).decode('gb2312')
69

    
70

    
71
def model_match(cls, data):
72
    """Match the opened image to the correct version"""
73
    return data[cls.MEM_TOTAL:] == bytes(cls.MODEL, 'utf-8')
74

    
75

    
76
def _crypt(symbol_index, buffer):
77
    # Some weird encryption is used. From the table below, we only use "CO 7".
78
    tblEncrySymbol = [b"BHT ", b"CO 7", b"A ES", b" EIY", b"M PQ",
79
                      b"XN Y", b"RVB ", b" HQP", b"W RC", b"MS N",
80
                      b" SAT", b"K DH", b"ZO R", b"C SL", b"6RB ",
81
                      b" JCG", b"PN V", b"J PK", b"EK L", b"I LZ"]
82
    tbl_encrypt_symbols = tblEncrySymbol[symbol_index]
83
    dec_buffer = b""
84
    index1 = 0
85
    for index2 in range(len(buffer)):
86
        bool_encrypt_char = ((tbl_encrypt_symbols[index1] != 32) and
87
                             (buffer[index2] != 0) and
88
                             (buffer[index2] != 255) and
89
                             (buffer[index2] != tbl_encrypt_symbols[index1])
90
                             and (buffer[index2] !=
91
                                  (tbl_encrypt_symbols[index1] ^ 255)))
92
        if (bool_encrypt_char):
93
            dec_byte = buffer[index2] ^ tbl_encrypt_symbols[index1]
94
            dec_buffer += struct.pack('>B', dec_byte)
95
        else:
96
            dec_buffer += struct.pack('>B', buffer[index2])
97
        index1 = (index1 + 1) % 4
98
    return dec_buffer
99

    
100

    
101
def _sendmagic(radio, magic, response_len):
102
    bfc._rawsend(radio, magic)
103
    return bfc._rawrecv(radio, response_len)
104

    
105

    
106
def _do_ident(radio):
107
    # Flush input buffer
108
    bfc._clean_buffer(radio)
109

    
110
    # Ident radio
111
    ack = _sendmagic(radio, radio._magic, len(radio._fingerprint))
112

    
113
    if not ack.startswith(radio._fingerprint):
114
        if ack:
115
            LOG.debug(repr(ack))
116
        raise errors.RadioError("Radio did not respond as expected (A)")
117

    
118
    for magic, resplen in radio._magics:
119
        _sendmagic(radio, magic, resplen)
120

    
121
    return True
122

    
123

    
124
def _download(radio):
125
    """Get the memory map"""
126

    
127
    # Put radio in program mode and identify it
128
    _do_ident(radio)
129
    data = b""
130

    
131
    # UI progress
132
    status = chirp_common.Status()
133
    status.cur = 0
134
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
135
    status.msg = "Cloning from radio..."
136
    radio.status_fn(status)
137

    
138
    for i in range(len(radio.MEM_SIZES)):
139
        MEM_SIZE = radio.MEM_SIZES[i]
140
        MEM_START = radio.MEM_STARTS[i]
141
        for addr in range(MEM_START, MEM_START + MEM_SIZE,
142
                          radio.BLOCK_SIZE):
143
            frame = radio._make_read_frame(addr, radio.BLOCK_SIZE)
144
            # DEBUG
145
            LOG.debug("Frame=" + util.hexprint(frame))
146

    
147
            # Sending the read request
148
            bfc._rawsend(radio, frame)
149

    
150
            # Now we read data
151
            d = bfc._rawrecv(radio, radio.BLOCK_SIZE + 4)
152

    
153
            LOG.debug("Response Data= " + util.hexprint(d))
154
            d = _crypt(1, d[4:])
155

    
156
            # Aggregate the data
157
            data += d
158

    
159
            # UI Update
160
            status.cur = len(data) // radio.BLOCK_SIZE
161
            status.msg = "Cloning from radio..."
162
            radio.status_fn(status)
163
    return data
164

    
165

    
166
def _upload(radio):
167
    # Put radio in program mode and identify it
168
    _do_ident(radio)
169
    data = b""
170

    
171
    # UI progress
172
    status = chirp_common.Status()
173
    status.cur = 0
174
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
175
    status.msg = "Cloning from radio..."
176
    radio.status_fn(status)
177

    
178
    data_addr = 0x00
179
    radio_mem = radio.get_mmap()
180
    for i in range(len(radio.MEM_SIZES)):
181
        MEM_SIZE = radio.MEM_SIZES[i]
182
        MEM_START = radio.MEM_STARTS[i]
183
        for addr in range(MEM_START, MEM_START + MEM_SIZE,
184
                          radio.BLOCK_SIZE):
185
            data = radio_mem[data_addr:data_addr + radio.BLOCK_SIZE]
186
            data = _crypt(1, data)
187
            data_addr += radio.BLOCK_SIZE
188

    
189
            frame = radio._make_frame(b"W", addr, radio.BLOCK_SIZE, data)
190
            # DEBUG
191
            LOG.debug("Frame=" + util.hexprint(frame))
192

    
193
            # Sending the read request
194
            bfc._rawsend(radio, frame)
195

    
196
            # receiving the response
197
            ack = bfc._rawrecv(radio, 1)
198
            if ack != b"\x06":
199
                msg = "Bad ack writing block 0x%04x" % addr
200
                raise errors.RadioError(msg)
201

    
202
            # UI Update
203
            status.cur = data_addr // radio.BLOCK_SIZE
204
            status.msg = "Cloning to radio..."
205
            radio.status_fn(status)
206
    return data
207

    
208

    
209
@directory.register
210
class UV17Pro(bfc.BaofengCommonHT):
211
    """Baofeng UV-17Pro"""
212
    VENDOR = "Baofeng"
213
    MODEL = "UV-17Pro"
214
    NEEDS_COMPAT_SERIAL = False
215

    
216
    MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
217
    MEM_SIZES = [0x8040, 0x0040, 0x02C0, 0x0040]
218

    
219
    MEM_TOTAL = 0x8380
220
    BLOCK_SIZE = 0x40
221
    BAUD_RATE = 115200
222

    
223
    download_function = _download
224
    upload_function = _upload
225

    
226
    _gmrs = False
227
    _bw_shift = False
228
    _has_support_for_banknames = False
229
    _has_workmode_support = True
230
    _has_savemode = True
231

    
232
    _tri_band = True
233
    _fileid = []
234
    _magic = MSTRING_UV17L
235
    _fingerprint = b"\x06"
236
    _magics = [(b"\x46", 16),
237
               (b"\x4d", 15),
238
               (b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01\x01\x04\x11\x08\x05" +
239
                b"\x0D\x0D\x01\x11\x0F\x09\x12\x09\x10\x04\x00", 1)]
240
    _fw_ver_start = 0x1EF0
241
    _recv_block_size = 0x40
242
    _mem_size = MEM_TOTAL
243
    _ack_block = True
244
    _has_when_to_send_aniid = True
245
    _vfoscan = False
246
    _has_gps = False
247
    _has_voxsw = False
248
    _has_pilot_tone = False
249
    _has_send_id_delay = False
250
    _has_skey2_short = False
251
    _scode_offset = 0
252

    
253
    MODES = ["NFM", "FM"]
254
    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
255
        "!@#$%^&*()+-=[]:\";'<>?,./"
256
    LENGTH_NAME = 12
257
    SKIP_VALUES = ["", "S"]
258
    DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
259
    RXTX_CODES = ('Off', )
260
    LIST_PW_SAVEMODE = ["Off", "On"]
261
    for code in chirp_common.TONES:
262
        RXTX_CODES = (RXTX_CODES + (str(code), ))
263
    for code in DTCS_CODES:
264
        RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'N', ))
265
    for code in DTCS_CODES:
266
        RXTX_CODES = (RXTX_CODES + ('D' + str(code) + 'I', ))
267
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
268
                    chirp_common.PowerLevel("Low",  watts=1.00)]
269
    _airband = (108000000, 136000000)
270
    _vhf_range = (136000000, 174000000)
271
    _vhf2_range = (200000000, 260000000)
272
    _uhf_range = (400000000, 520000000)
273
    _uhf2_range = (350000000, 390000000)
274

    
275
    VALID_BANDS = [_vhf_range, _vhf2_range,
276
                   _uhf_range]
277
    PTTID_LIST = LIST_PTTID
278
    SCODE_LIST = ["%s" % x for x in range(1, 21)]
279
    SQUELCH_LIST = ["Off"] + list("12345")
280
    LIST_POWERON_DISPLAY_TYPE = ["LOGO", "BATT voltage"]
281
    LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 195, 15)]
282
    LIST_VOICE = ["English", "Chinese"]
283
    LIST_BACKLIGHT_TIMER = ["Always On"] + ["%s sec"
284
                                            % x for x in range(5, 25, 5)]
285
    LIST_MODE = ["Name", "Frequency", "Channel Number"]
286

    
287
    CHANNELS = 1000
288

    
289
    MEM_FORMAT = """
290
    struct {
291
      lbcd rxfreq[4];
292
      lbcd txfreq[4];
293
      ul16 rxtone;
294
      ul16 txtone;
295
      u8 scode;
296
      u8 pttid;
297
      u8 lowpower;
298
      u8 unknown1:1,
299
         wide:1,
300
         sqmode:2,
301
         bcl:1,
302
         scan:1,
303
         unknown2:1,
304
         fhss:1;
305
      u8 unknown3;
306
      u8 unknown4;
307
      u8 unknown5;
308
      u8 unknown6;
309
      char name[12];
310
    } memory[1000];
311

    
312
    struct vfo_entry {
313
      u8 freq[8];
314
      ul16 rxtone;
315
      ul16 txtone;
316
      u8 unknown0;
317
      u8 bcl;
318
      u8 sftd:3,
319
         scode:5;
320
      u8 unknown1;
321
      u8 lowpower;
322
      u8 unknown2:1,
323
         wide:1,
324
         unknown3:5,
325
         fhss:1;
326
      u8 unknown4;
327
      u8 step;
328
      u8 offset[6];
329
      u8 unknown5[2];
330
      u8 sqmode;
331
      u8 unknown6[3];
332
      };
333

    
334
    #seekto 0x8000;
335
    struct {
336
      struct vfo_entry a;
337
      struct vfo_entry b;
338
    } vfo;
339

    
340
    struct {
341
      u8 squelch;
342
      u8 savemode;
343
      u8 vox;
344
      u8 backlight;
345
      u8 dualstandby;
346
      u8 tot;
347
      u8 beep;
348
      u8 voicesw;
349
      u8 voice;
350
      u8 sidetone;
351
      u8 scanmode;
352
      u8 pttid;
353
      u8 pttdly;
354
      u8 chadistype;
355
      u8 chbdistype;
356
      u8 bcl;
357
      u8 autolock;
358
      u8 alarmmode;
359
      u8 alarmtone;
360
      u8 unknown1;
361
      u8 tailclear;
362
      u8 rpttailclear;
363
      u8 rpttaildet;
364
      u8 roger;
365
      u8 unknown2;
366
      u8 fmenable;
367
      u8 chbworkmode:4,
368
         chaworkmode:4;
369
      u8 keylock;
370
      u8 powerondistype;
371
      u8 tone;
372
      u8 unknown4[2];
373
      u8 voxdlytime;
374
      u8 menuquittime;
375
      u8 unknown5[6];
376
      u8 totalarm;
377
      u8 unknown6[2];
378
      u8 ctsdcsscantype;
379
      ul16 vfoscanmin;
380
      ul16 vfoscanmax;
381
      u8 gpsw;
382
      u8 gpsmode;
383
      u8 unknown7[2];
384
      u8 key2short;
385
      u8 unknown8[2];
386
      u8 rstmenu;
387
      u8 unknown9;
388
      u8 hangup;
389
      u8 voxsw;
390
      u8 gpstimezone;
391
    } settings;
392

    
393
    #seekto 0x8080;
394
    struct {
395
      u8 code[5];
396
      u8 unknown[1];
397
      u8 unused1:6,
398
         aniid:2;
399
      u8 dtmfon;
400
      u8 dtmfoff;
401
    } ani;
402

    
403
    #seekto 0x80A0;
404
    struct {
405
      u8 code[5];
406
      u8 name[10];
407
      u8 unused;
408
    } pttid[20];
409

    
410
    #seekto 0x8280;
411
    struct {
412
      char name[16];
413
    } bank_name[10];
414
    """
415

    
416
    def _make_read_frame(self, addr, length):
417
        """Pack the info in the header format"""
418
        frame = self._make_frame(b"\x52", addr, length)
419
        # Return the data
420
        return frame
421

    
422
    def _make_frame(self, cmd, addr, length, data=""):
423
        """Pack the info in the header format"""
424
        frame = cmd + struct.pack(">i", addr)[2:] + struct.pack("b", length)
425
        # add the data if set
426
        if len(data) != 0:
427
            frame += data
428
        # return the data
429
        return frame
430

    
431
    @classmethod
432
    def get_prompts(cls):
433
        rp = chirp_common.RadioPrompts()
434
        rp.experimental = \
435
            ('This driver is a beta version.\n'
436
             '\n'
437
             'Please save an unedited copy of your first successful\n'
438
             'download to a CHIRP Radio Images(*.img) file.'
439
             )
440
        rp.pre_download = _(
441
            "Follow these instructions to download your info:\n"
442
            "1 - Turn off your radio\n"
443
            "2 - Connect your interface cable\n"
444
            "3 - Turn on your radio\n"
445
            "4 - Do the download of your radio data\n")
446
        rp.pre_upload = _(
447
            "Follow this instructions to upload your info:\n"
448
            "1 - Turn off your radio\n"
449
            "2 - Connect your interface cable\n"
450
            "3 - Turn on your radio\n"
451
            "4 - Do the upload of your radio data\n")
452
        return rp
453

    
454
    def process_mmap(self):
455
        """Process the mem map into the mem object"""
456
        # make lines shorter for style check.
457
        self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
458

    
459
    # DTMF settings
460
    def apply_code(self, setting, obj, length):
461
        code = []
462
        for j in range(0, length):
463
            try:
464
                code.append(DTMF_CHARS.index(str(setting.value)[j]))
465
            except IndexError:
466
                code.append(0xFF)
467
        obj.code = code
468

    
469
    def get_settings_common_dtmf(self, dtmfe, _mem):
470
        for i in range(0, len(self.SCODE_LIST)):
471
            _codeobj = self._memobj.pttid[i].code
472
            _code = "".join([
473
                DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
474
            val = RadioSettingValueString(0, 5, _code, False)
475
            val.set_charset(DTMF_CHARS)
476
            pttid = RadioSetting("pttid/%i.code" % i,
477
                                 "Signal Code %i" % (i + 1), val)
478
            pttid.set_apply_callback(self.apply_code, self._memobj.pttid[i], 5)
479
            dtmfe.append(pttid)
480

    
481
        _codeobj = self._memobj.ani.code
482
        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
483
        val = RadioSettingValueString(0, 5, _code, False)
484
        val.set_charset(DTMF_CHARS)
485
        rs = RadioSetting("ani.code", "ANI Code", val)
486
        rs.set_apply_callback(self.apply_code, self._memobj.ani, 5)
487
        dtmfe.append(rs)
488

    
489
    def get_settings_pro_dtmf(self, dtmfe, _mem):
490
        if _mem.ani.dtmfon > 0xC3:
491
            val = 0x03
492
        else:
493
            val = _mem.ani.dtmfon
494
        rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
495
                          RadioSettingValueList(LIST_DTMFSPEED,
496
                                                LIST_DTMFSPEED[val]))
497
        dtmfe.append(rs)
498

    
499
        if _mem.ani.dtmfoff > 0xC3:
500
            val = 0x03
501
        else:
502
            val = _mem.ani.dtmfoff
503
        rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
504
                          RadioSettingValueList(LIST_DTMFSPEED,
505
                                                LIST_DTMFSPEED[val]))
506
        dtmfe.append(rs)
507

    
508
        if self._has_when_to_send_aniid:
509
            rs = RadioSetting("ani.aniid", "When to send ANI ID",
510
                              RadioSettingValueList(LIST_PTTID,
511
                                                    LIST_PTTID[
512
                                                        _mem.ani.aniid]))
513
            dtmfe.append(rs)
514

    
515
        if _mem.settings.hangup >= len(LIST_HANGUPTIME):
516
            val = 0
517
        else:
518
            val = _mem.settings.hangup
519
        rs = RadioSetting("settings.hangup", "Hang-up time",
520
                          RadioSettingValueList(LIST_HANGUPTIME,
521
                                                LIST_HANGUPTIME[
522
                                                    val]))
523
        dtmfe.append(rs)
524

    
525
    def get_settings_common_basic(self, basic, _mem):
526
        if _mem.settings.squelch >= len(self.SQUELCH_LIST):
527
            val = 0x00
528
        else:
529
            val = _mem.settings.squelch
530
        rs = RadioSetting("settings.squelch", "Squelch",
531
                          RadioSettingValueList(
532
                              self.SQUELCH_LIST, self.SQUELCH_LIST[val]))
533
        basic.append(rs)
534

    
535
        if _mem.settings.tot >= len(self.LIST_TIMEOUT):
536
            val = 0x03
537
        else:
538
            val = _mem.settings.tot
539
        rs = RadioSetting("settings.tot", "Timeout Timer",
540
                          RadioSettingValueList(
541
                              self.LIST_TIMEOUT, self.LIST_TIMEOUT[val]))
542
        basic.append(rs)
543

    
544
        rs = RadioSetting("settings.dualstandby", "Dual Watch",
545
                          RadioSettingValueBoolean(_mem.settings.dualstandby))
546
        basic.append(rs)
547

    
548
        if _mem.settings.powerondistype >= len(self.LIST_POWERON_DISPLAY_TYPE):
549
            val = 0x00
550
        else:
551
            rs = RadioSetting("settings.powerondistype",
552
                              "Power On Display Type",
553
                              RadioSettingValueList(
554
                                  self.LIST_POWERON_DISPLAY_TYPE,
555
                                  self.LIST_POWERON_DISPLAY_TYPE[
556
                                      _mem.settings.powerondistype]))
557
            basic.append(rs)
558

    
559
        if _mem.settings.voice >= len(self.LIST_VOICE):
560
            val = 0x01
561
        else:
562
            val = _mem.settings.voice
563
        rs = RadioSetting("settings.voice", "Voice Prompt",
564
                          RadioSettingValueList(
565
                              self.LIST_VOICE, self.LIST_VOICE[val]))
566
        basic.append(rs)
567

    
568
        rs = RadioSetting("settings.voicesw", "Enable Voice",
569
                          RadioSettingValueBoolean(_mem.settings.voicesw))
570
        basic.append(rs)
571

    
572
        if _mem.settings.backlight >= len(self.LIST_BACKLIGHT_TIMER):
573
            val = 0x00
574
        else:
575
            val = _mem.settings.backlight
576
        rs = RadioSetting("settings.backlight", "Backlight Timer",
577
                          RadioSettingValueList(
578
                              self.LIST_BACKLIGHT_TIMER,
579
                              self.LIST_BACKLIGHT_TIMER[val]))
580
        basic.append(rs)
581

    
582
        rs = RadioSetting("settings.autolock", "Key Auto Lock",
583
                          RadioSettingValueBoolean(_mem.settings.autolock))
584
        basic.append(rs)
585

    
586
        rs = RadioSetting("settings.beep", "Beep",
587
                          RadioSettingValueList(
588
                              LIST_BEEP, LIST_BEEP[_mem.settings.beep]))
589
        basic.append(rs)
590

    
591
        rs = RadioSetting("settings.roger", "Roger",
592
                          RadioSettingValueBoolean(_mem.settings.roger))
593
        basic.append(rs)
594

    
595
        rs = RadioSetting("settings.chadistype", "Channel A display type",
596
                          RadioSettingValueList(
597
                              self.LIST_MODE,
598
                              self.LIST_MODE[_mem.settings.chadistype]))
599
        basic.append(rs)
600

    
601
        rs = RadioSetting("settings.chbdistype", "Channel B display type",
602
                          RadioSettingValueList(
603
                              self.LIST_MODE,
604
                              self.LIST_MODE[_mem.settings.chbdistype]))
605
        basic.append(rs)
606

    
607
    def get_settings_pro_basic(self, basic, _mem):
608

    
609
        if _mem.settings.savemode > len(self.LIST_PW_SAVEMODE):
610
            val = 0x01  # assume values out of range are some form of "On"
611
        else:
612
            val = _mem.settings.savemode
613
        rs = RadioSetting("settings.savemode", "Save Mode",
614
                          RadioSettingValueList(self.LIST_PW_SAVEMODE,
615
                                                self.LIST_PW_SAVEMODE[val]))
616
        basic.append(rs)
617

    
618
        rs = RadioSetting("settings.totalarm", "Timeout Timer Alarm",
619
                          RadioSettingValueList(
620
                              LIST_TIMEOUT_ALARM,
621
                              LIST_TIMEOUT_ALARM[_mem.settings.totalarm]))
622
        basic.append(rs)
623

    
624
        if self._has_pilot_tone:
625
            rs = RadioSetting("settings.tone", "Pilot Tone",
626
                              RadioSettingValueList(
627
                                LIST_PILOT_TONE,
628
                                LIST_PILOT_TONE[_mem.settings.tone]))
629
            basic.append(rs)
630

    
631
        rs = RadioSetting("settings.sidetone", "Side Tone",
632
                          RadioSettingValueList(
633
                              LIST_SIDE_TONE,
634
                              LIST_SIDE_TONE[_mem.settings.sidetone]))
635
        basic.append(rs)
636

    
637
        rs = RadioSetting("settings.tailclear", "Tail Clear",
638
                          RadioSettingValueBoolean(_mem.settings.tailclear))
639
        basic.append(rs)
640

    
641
        rs = RadioSetting("settings.scanmode", "Scan Mode",
642
                          RadioSettingValueList(
643
                              LIST_SCANMODE,
644
                              LIST_SCANMODE[_mem.settings.scanmode]))
645
        basic.append(rs)
646

    
647
        rs = RadioSetting("settings.alarmmode", "Alarm Mode",
648
                          RadioSettingValueList(
649
                              LIST_ALARMMODE,
650
                              LIST_ALARMMODE[_mem.settings.alarmmode]))
651
        basic.append(rs)
652

    
653
        rs = RadioSetting("settings.alarmtone", "Sound Alarm",
654
                          RadioSettingValueBoolean(_mem.settings.alarmtone))
655
        basic.append(rs)
656

    
657
        rs = RadioSetting("settings.keylock", "Key Lock",
658
                          RadioSettingValueBoolean(_mem.settings.keylock))
659
        basic.append(rs)
660

    
661
        rs = RadioSetting("settings.menuquittime", "Menu Quit Timer",
662
                          RadioSettingValueList(
663
                              LIST_MENU_QUIT_TIME,
664
                              LIST_MENU_QUIT_TIME[
665
                                  _mem.settings.menuquittime]))
666
        basic.append(rs)
667

    
668
        if self._has_send_id_delay:
669
            rs = RadioSetting("settings.pttdly", "Send ID Delay",
670
                              RadioSettingValueList(
671
                                LIST_ID_DELAY,
672
                                LIST_ID_DELAY[_mem.settings.pttdly]))
673
            basic.append(rs)
674

    
675
        rs = RadioSetting("settings.ctsdcsscantype", "QT Save Mode",
676
                          RadioSettingValueList(
677
                              LIST_QT_SAVEMODE,
678
                              LIST_QT_SAVEMODE[_mem.settings.ctsdcsscantype]))
679
        basic.append(rs)
680

    
681
        def getKey2shortIndex(value):
682
            key_to_index = {0x07: 0,
683
                            0x1C: 1,
684
                            0x1D: 2,
685
                            0x2D: 3}
686
            return key_to_index.get(int(value), 0)
687

    
688
        def apply_Key2short(setting, obj):
689
            val = str(setting.value)
690
            key_to_index = {'FM': 0x07,
691
                            'Scan': 0x1C,
692
                            'Search': 0x1D,
693
                            'Vox': 0x2D}
694
            obj.key2short = key_to_index.get(val, 0x07)
695

    
696
        if self._has_skey2_short:
697
            rs = RadioSetting("settings.key2short", "Skey2 Short",
698
                              RadioSettingValueList(
699
                                LIST_SKEY2_SHORT,
700
                                LIST_SKEY2_SHORT[
701
                                    getKey2shortIndex(
702
                                        _mem.settings.key2short)]))
703
            rs.set_apply_callback(apply_Key2short, _mem.settings)
704
            basic.append(rs)
705

    
706
        rs = RadioSetting("settings.chaworkmode", "Channel A work mode",
707
                          RadioSettingValueList(
708
                              LIST_WORKMODE,
709
                              LIST_WORKMODE[_mem.settings.chaworkmode]))
710
        basic.append(rs)
711

    
712
        rs = RadioSetting("settings.chbworkmode", "Channel B work mode",
713
                          RadioSettingValueList(
714
                              LIST_WORKMODE,
715
                              LIST_WORKMODE[_mem.settings.chbworkmode]))
716
        basic.append(rs)
717

    
718
        rs = RadioSetting("settings.rpttailclear", "Rpt Tail Clear",
719
                          RadioSettingValueList(
720
                              LIST_RPT_TAIL_CLEAR,
721
                              LIST_RPT_TAIL_CLEAR[
722
                                  _mem.settings.rpttailclear]))
723
        basic.append(rs)
724

    
725
        rs = RadioSetting("settings.rpttaildet", "Rpt Tail Delay",
726
                          RadioSettingValueList(
727
                              LIST_RPT_TAIL_CLEAR,
728
                              LIST_RPT_TAIL_CLEAR[_mem.settings.rpttaildet]))
729
        basic.append(rs)
730

    
731
        rs = RadioSetting("settings.rstmenu", "Enable Menu Rst",
732
                          RadioSettingValueBoolean(_mem.settings.rstmenu))
733
        basic.append(rs)
734

    
735
        if self._has_voxsw:
736
            rs = RadioSetting("settings.voxsw", "Vox Switch",
737
                              RadioSettingValueBoolean(_mem.settings.voxsw))
738
            basic.append(rs)
739

    
740
            rs = RadioSetting("settings.vox", "Vox Level",
741
                              RadioSettingValueList(
742
                                LIST_VOX_LEVEL_ALT,
743
                                LIST_VOX_LEVEL_ALT[_mem.settings.vox]))
744
            basic.append(rs)
745
        else:
746
            rs = RadioSetting("settings.vox", "Vox Level",
747
                              RadioSettingValueList(
748
                                LIST_VOX_LEVEL,
749
                                LIST_VOX_LEVEL[_mem.settings.vox]))
750
            basic.append(rs)
751

    
752
        rs = RadioSetting("settings.voxdlytime", "Vox Delay Time",
753
                          RadioSettingValueList(
754
                              LIST_VOX_DELAY_TIME,
755
                              LIST_VOX_DELAY_TIME[_mem.settings.voxdlytime]))
756
        basic.append(rs)
757

    
758
        if self._has_gps:
759
            rs = RadioSetting("settings.gpsw", "GPS On",
760
                              RadioSettingValueBoolean(_mem.settings.gpsw))
761
            basic.append(rs)
762

    
763
            rs = RadioSetting("settings.gpsmode", "GPS Mode",
764
                              RadioSettingValueList(
765
                                LIST_GPS_MODE,
766
                                LIST_GPS_MODE[_mem.settings.gpsmode]))
767
            basic.append(rs)
768

    
769
            rs = RadioSetting("settings.gpstimezone", "GPS Timezone",
770
                              RadioSettingValueList(
771
                                LIST_GPS_TIMEZONE,
772
                                LIST_GPS_TIMEZONE[_mem.settings.gpstimezone]))
773
            basic.append(rs)
774

    
775
        rs = RadioSetting("settings.fmenable", "Disable FM radio",
776
                          RadioSettingValueBoolean(_mem.settings.fmenable))
777
        basic.append(rs)
778

    
779
    def get_settings_common_workmode(self, workmode, _mem):
780

    
781
        vfoA = RadioSettingGroup("vfoA", "VFO A")
782
        vfoB = RadioSettingGroup("vfoB", "VFO B")
783

    
784
        def my_validate(value):
785
            value = chirp_common.parse_freq(value)
786
            for band in self.VALID_BANDS:
787
                if value > band[0] and value < band[1]:
788
                    return chirp_common.format_freq(value)
789
            msg = ("{0} is not in a valid band.".format(value))
790
            raise InvalidValueError(msg)
791

    
792
        def apply_freq(setting, obj):
793
            value = chirp_common.parse_freq(str(setting.value)) / 10
794
            for i in range(7, -1, -1):
795
                obj.freq[i] = value % 10
796
                value /= 10
797

    
798
        freqA = RadioSettingValueString(0, 10,
799
                                        bfc.bcd_decode_freq(_mem.vfo.a.freq))
800
        freqA.set_validate_callback(my_validate)
801
        rs = RadioSetting("vfo.a.freq", "Frequency", freqA)
802
        rs.set_apply_callback(apply_freq, _mem.vfo.a)
803
        vfoA.append(rs)
804

    
805
        freqB = RadioSettingValueString(0, 10,
806
                                        bfc.bcd_decode_freq(_mem.vfo.b.freq))
807
        freqB.set_validate_callback(my_validate)
808
        rs = RadioSetting("vfo.b.freq", "Frequency", freqB)
809
        rs.set_apply_callback(apply_freq, _mem.vfo.b)
810
        vfoB.append(rs)
811

    
812
        if _mem.vfo.a.sftd >= len(LIST_SHIFTS):
813
            val = 0
814
        else:
815
            val = _mem.vfo.a.sftd
816
        rs = RadioSetting("vfo.a.sftd", "Shift",
817
                          RadioSettingValueList(LIST_SHIFTS, LIST_SHIFTS[val]))
818
        vfoA.append(rs)
819

    
820
        if _mem.vfo.b.sftd >= len(LIST_SHIFTS):
821
            val = 0
822
        else:
823
            val = _mem.vfo.b.sftd
824
        rs = RadioSetting("vfo.b.sftd", "Shift",
825
                          RadioSettingValueList(LIST_SHIFTS, LIST_SHIFTS[val]))
826
        vfoB.append(rs)
827

    
828
        def convert_bytes_to_offset(bytes):
829
            real_offset = 0
830
            for byte in bytes:
831
                real_offset = (real_offset * 10) + byte
832
            return chirp_common.format_freq(real_offset * 1000)
833

    
834
        def apply_offset(setting, obj):
835
            value = chirp_common.parse_freq(str(setting.value)) / 1000
836
            for i in range(5, -1, -1):
837
                obj.offset[i] = value % 10
838
                value /= 10
839

    
840
        offA = RadioSettingValueString(
841
                0, 10, convert_bytes_to_offset(_mem.vfo.a.offset))
842
        rs = RadioSetting("vfo.a.offset",
843
                          "Offset (0.0-999.999)", offA)
844
        rs.set_apply_callback(apply_offset, _mem.vfo.a)
845
        vfoA.append(rs)
846

    
847
        offB = RadioSettingValueString(
848
                0, 10, convert_bytes_to_offset(_mem.vfo.b.offset))
849
        rs = RadioSetting("vfo.b.offset",
850
                          "Offset (0.0-999.999)", offB)
851
        rs.set_apply_callback(apply_offset, _mem.vfo.b)
852
        vfoB.append(rs)
853

    
854
        POWER_LEVELS = [str(x) for x in self.POWER_LEVELS]
855
        if _mem.vfo.a.lowpower >= len(POWER_LEVELS):
856
            val = 0
857
        else:
858
            val = _mem.vfo.a.lowpower
859
        rs = RadioSetting("vfo.a.lowpower", "Power",
860
                          RadioSettingValueList(POWER_LEVELS,
861
                                                POWER_LEVELS[val]))
862
        vfoA.append(rs)
863

    
864
        if _mem.vfo.b.lowpower >= len(POWER_LEVELS):
865
            val = 0
866
        else:
867
            val = _mem.vfo.b.lowpower
868
        rs = RadioSetting("vfo.b.lowpower", "Power",
869
                          RadioSettingValueList(POWER_LEVELS,
870
                                                POWER_LEVELS[val]))
871
        vfoB.append(rs)
872

    
873
        if _mem.vfo.a.wide >= len(LIST_BANDWIDTH):
874
            val = 0
875
        else:
876
            val = _mem.vfo.a.wide
877
        rs = RadioSetting("vfo.a.wide", "Bandwidth",
878
                          RadioSettingValueList(LIST_BANDWIDTH,
879
                                                LIST_BANDWIDTH[val]))
880
        vfoA.append(rs)
881

    
882
        if _mem.vfo.b.wide >= len(LIST_BANDWIDTH):
883
            val = 0
884
        else:
885
            val = _mem.vfo.b.wide
886
        rs = RadioSetting("vfo.b.wide", "Bandwidth",
887
                          RadioSettingValueList(LIST_BANDWIDTH,
888
                                                LIST_BANDWIDTH[val]))
889
        vfoB.append(rs)
890

    
891
        if _mem.vfo.a.scode >= len(self.SCODE_LIST):
892
            val = 0
893
        else:
894
            val = _mem.vfo.a.scode
895
        rs = RadioSetting("vfo.a.scode", "Signal Code",
896
                          RadioSettingValueList(self.SCODE_LIST,
897
                                                self.SCODE_LIST[val]))
898
        vfoA.append(rs)
899

    
900
        if _mem.vfo.b.scode >= len(self.SCODE_LIST):
901
            val = 0
902
        else:
903
            val = _mem.vfo.b.scode
904
        rs = RadioSetting("vfo.b.scode", "Signal Code",
905
                          RadioSettingValueList(self.SCODE_LIST,
906
                                                self.SCODE_LIST[val]))
907
        vfoB.append(rs)
908

    
909
        if _mem.vfo.a.step >= len(STEPS):
910
            val = 0
911
        else:
912
            val = _mem.vfo.a.step
913
        rs = RadioSetting("vfo.a.step", "Tuning Step",
914
                          RadioSettingValueList(LIST_STEPS, LIST_STEPS[val]))
915
        vfoA.append(rs)
916

    
917
        if _mem.vfo.b.step >= len(STEPS):
918
            val = 0
919
        else:
920
            val = _mem.vfo.b.step
921
        rs = RadioSetting("vfo.b.step", "Tuning Step",
922
                          RadioSettingValueList(LIST_STEPS, LIST_STEPS[val]))
923
        vfoB.append(rs)
924

    
925
        workmode.append(vfoA)
926
        workmode.append(vfoB)
927

    
928
    def get_settings_common_bank(self, bank, _mem):
929

    
930
        def _filterName(name):
931
            fname = b""
932
            for char in name:
933
                if ord(str(char)) == 255:
934
                    break
935
                fname += int(char).to_bytes(1, 'big')
936
            return fname.decode('gb2312').strip()
937

    
938
        def apply_bankname(setting, obj):
939
            name = str(setting.value).encode('gb2312')[:16].ljust(16, b"\xff")
940
            obj.name = name
941

    
942
        if self._has_support_for_banknames:
943
            for i in range(0, 10):
944
                _nameobj = self._memobj.bank_name[i]
945
                rs = RadioSetting("bank_name/%i.name" % i,
946
                                  "Bank name %i" % (i + 1),
947
                                  RadioSettingValueString(
948
                                      0, 16, _filterName(_nameobj.name), False,
949
                                      CHARSET_GB2312))
950
                rs.set_apply_callback(apply_bankname, _nameobj)
951
                bank.append(rs)
952

    
953
    def get_settings(self):
954
        """Translate the bit in the mem_struct into settings in the UI"""
955
        _mem = self._memobj
956
        supported = []
957

    
958
        basic = RadioSettingGroup("basic", "Basic Settings")
959
        self.get_settings_common_basic(basic, _mem)
960
        self.get_settings_pro_basic(basic, _mem)
961
        supported.append(basic)  # add basic menu
962

    
963
        if self._has_workmode_support:
964
            workmode = RadioSettingGroup("workmode", "Work Mode Settings")
965
            self.get_settings_common_workmode(workmode, _mem)
966
            supported.append(workmode)  # add workmode menu if supported
967

    
968
        dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
969
        self.get_settings_common_dtmf(dtmfe, _mem)
970
        self.get_settings_pro_dtmf(dtmfe, _mem)
971
        supported.append(dtmfe)  # add dtmfe menu
972

    
973
        if self._has_support_for_banknames:
974
            bank = RadioSettingGroup("bank", "Bank names")
975
            self.get_settings_common_bank(bank, _mem)
976
            supported.append(bank)  # add bank menu if supported
977

    
978
        top = RadioSettings(*tuple(supported))
979
        return top
980

    
981
    def set_settings(self, settings):
982
        _settings = self._memobj.settings
983
        for element in settings:
984
            if not isinstance(element, RadioSetting):
985
                self.set_settings(element)
986
                continue
987
            else:
988
                try:
989
                    name = element.get_name()
990
                    if "." in name:
991
                        bits = name.split(".")
992
                        obj = self._memobj
993
                        for bit in bits[:-1]:
994
                            if "/" in bit:
995
                                bit, index = bit.split("/", 1)
996
                                index = int(index)
997
                                obj = getattr(obj, bit)[index]
998
                            else:
999
                                obj = getattr(obj, bit)
1000
                        setting = bits[-1]
1001
                    else:
1002
                        obj = _settings
1003
                        setting = element.get_name()
1004

    
1005
                    if element.has_apply_callback():
1006
                        LOG.debug("Using apply callback")
1007
                        element.run_apply_callback()
1008
                    elif element.value.get_mutable():
1009
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1010
                        setattr(obj, setting, element.value)
1011
                except Exception:
1012
                    LOG.debug(element.get_name())
1013
                    raise
1014

    
1015
    def sync_in(self):
1016
        """Download from radio"""
1017
        try:
1018
            data = self.download_function()
1019
        except errors.RadioError:
1020
            # Pass through any real errors we raise
1021
            raise
1022
        except Exception:
1023
            # If anything unexpected happens, make sure we raise
1024
            # a RadioError and log the problem
1025
            LOG.exception('Unexpected error during download')
1026
            raise errors.RadioError('Unexpected error communicating '
1027
                                    'with the radio')
1028
        self._mmap = memmap.MemoryMapBytes(data)
1029

    
1030
        self.process_mmap()
1031

    
1032
    def sync_out(self):
1033
        """Upload to radio"""
1034
        try:
1035
            self.upload_function()
1036
        except errors.RadioError:
1037
            raise
1038
        except Exception:
1039
            # If anything unexpected happens, make sure we raise
1040
            # a RadioError and log the problem
1041
            LOG.exception('Unexpected error during upload')
1042
            raise errors.RadioError('Unexpected error communicating '
1043
                                    'with the radio')
1044

    
1045
    def get_features(self):
1046
        """Get the radio's features"""
1047

    
1048
        rf = chirp_common.RadioFeatures()
1049
        rf.has_settings = True
1050
        rf.has_bank = False
1051
        rf.has_tuning_step = False
1052
        rf.can_odd_split = True
1053
        rf.has_name = True
1054
        rf.has_offset = True
1055
        rf.has_mode = True
1056
        rf.has_dtcs = True
1057
        rf.has_rx_dtcs = True
1058
        rf.has_dtcs_polarity = True
1059
        rf.has_ctone = True
1060
        rf.has_cross = True
1061
        rf.valid_modes = self.MODES
1062
        rf.valid_characters = self.VALID_CHARS
1063
        rf.valid_name_length = self.LENGTH_NAME
1064
        if self._gmrs:
1065
            rf.valid_duplexes = ["", "+", "off"]
1066
        else:
1067
            rf.valid_duplexes = ["", "-", "+", "split", "off"]
1068
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
1069
        rf.valid_cross_modes = [
1070
            "Tone->Tone",
1071
            "DTCS->",
1072
            "->DTCS",
1073
            "Tone->DTCS",
1074
            "DTCS->Tone",
1075
            "->Tone",
1076
            "DTCS->DTCS"]
1077
        rf.valid_skips = self.SKIP_VALUES
1078
        rf.valid_dtcs_codes = self.DTCS_CODES
1079
        rf.memory_bounds = (1, self.CHANNELS)
1080
        rf.valid_power_levels = self.POWER_LEVELS
1081
        rf.valid_bands = self.VALID_BANDS
1082
        rf.valid_tuning_steps = STEPS
1083

    
1084
        return rf
1085

    
1086
    def decode_tone(self, val):
1087
        mode = ""
1088
        pol = "N"
1089
        if val in [0, 0xFFFF]:
1090
            xval = 0
1091
        elif val >= 0x0258:
1092
            mode = "Tone"
1093
            xval = int(val) / 10.0
1094
        elif val <= 0x0258:
1095
            mode = "DTCS"
1096
            if val > 0x69:
1097
                index = val - 0x6A
1098
                pol = "R"
1099
            else:
1100
                index = val - 1
1101
            xval = self.DTCS_CODES[index]
1102
        else:
1103
            LOG.warn("Bug: tone is %04x" % val)
1104
        return mode, xval, pol
1105

    
1106
    def encode_tone(self, memtone, mode, tone, pol):
1107
        if mode == "Tone":
1108
            memtone.set_value(int(tone * 10))
1109
        elif mode == "TSQL":
1110
            memtone.set_value(int(tone * 10))
1111
        elif mode == "DTCS":
1112
            if pol == 'R':
1113
                memtone.set_value(self.DTCS_CODES.index(tone) + 1 + 0x69)
1114
            else:
1115
                memtone.set_value(self.DTCS_CODES.index(tone) + 1)
1116
        else:
1117
            memtone.set_value(0)
1118

    
1119
    def split_txfreq(self, _mem, freq):
1120
        if self._is_txinh(_mem):
1121
            # TX freq not set
1122
            duplex = "off"
1123
            offset = 0
1124
        else:
1125
            offset = (int(_mem.txfreq) * 10) - freq
1126
            if offset != 0:
1127
                if bfc._split(self.get_features(), freq, int(
1128
                          _mem.txfreq) * 10):
1129
                    duplex = "split"
1130
                    offset = int(_mem.txfreq) * 10
1131
                elif offset < 0:
1132
                    offset = abs(offset)
1133
                    duplex = "-"
1134
                elif offset > 0:
1135
                    duplex = "+"
1136
            else:
1137
                duplex = ""
1138
                offset = 0
1139
        return offset, duplex
1140

    
1141
    def get_memory_common(self, _mem, name, mem):
1142
        if _mem.get_raw()[0] == 255:
1143
            mem.empty = True
1144
            return mem
1145

    
1146
        mem.freq = int(_mem.rxfreq) * 10
1147

    
1148
        # TX freq set
1149
        mem.offset, mem.duplex = self.split_txfreq(_mem, mem.freq)
1150

    
1151
        txtone = self.decode_tone(_mem.txtone)
1152
        rxtone = self.decode_tone(_mem.rxtone)
1153
        chirp_common.split_tone_decode(mem, txtone, rxtone)
1154

    
1155
        if not _mem.scan:
1156
            mem.skip = "S"
1157

    
1158
        levels = self.POWER_LEVELS
1159
        try:
1160
            mem.power = levels[_mem.lowpower]
1161
        except IndexError:
1162
            mem.power = levels[0]
1163

    
1164
        mem.mode = _mem.wide and self.MODES[0] or self.MODES[1]
1165
        if (mem.freq >= self._airband[0] and mem.freq <= self._airband[1]):
1166
            # NOTE: AM is not in valid_modes because you can't arbitrarily
1167
            # enable it on this radio. However, we can expose it as immutable
1168
            # which will display properly in the UI and not allow the user
1169
            # to change those channels to FM.
1170
            mem.mode = "AM"
1171
            mem.immutable = ['mode']
1172

    
1173
        mem.extra = RadioSettingGroup("Extra", "extra")
1174

    
1175
        rs = RadioSetting("bcl", "BCL",
1176
                          RadioSettingValueBoolean(_mem.bcl))
1177
        mem.extra.append(rs)
1178

    
1179
        rs = RadioSetting("pttid", "PTT ID",
1180
                          RadioSettingValueList(self.PTTID_LIST,
1181
                                                self.PTTID_LIST[
1182
                                                    _mem.pttid]))
1183
        mem.extra.append(rs)
1184

    
1185
        scode = (_mem.scode - self._scode_offset) % len(self.SCODE_LIST)
1186
        rs = RadioSetting("scode", "S-CODE",
1187
                          RadioSettingValueList(self.SCODE_LIST,
1188
                                                self.SCODE_LIST[
1189
                                                    scode]))
1190
        mem.extra.append(rs)
1191

    
1192
        mem.name = str(name).replace('\xFF', ' ').replace('\x00', ' ').rstrip()
1193

    
1194
    def get_raw_memory(self, number):
1195
        return self._memobj.memory[number - 1]
1196

    
1197
    def get_memory(self, number):
1198
        _mem = self.get_raw_memory(number)
1199

    
1200
        mem = chirp_common.Memory()
1201
        mem.number = number
1202

    
1203
        self.get_memory_common(_mem, _mem.name, mem)
1204

    
1205
        return mem
1206

    
1207
    def unsplit_txfreq(self, mem):
1208
        _mem = self.get_raw_memory(mem.number)
1209
        if mem.duplex == "off":
1210
            for i in range(0, 4):
1211
                _mem.txfreq[i].set_raw(b"\xFF")
1212
        elif mem.duplex == "split":
1213
            _mem.txfreq = mem.offset / 10
1214
        elif mem.duplex == "+":
1215
            _mem.txfreq = (mem.freq + mem.offset) / 10
1216
        elif mem.duplex == "-":
1217
            _mem.txfreq = (mem.freq - mem.offset) / 10
1218
        else:
1219
            _mem.txfreq = mem.freq / 10
1220

    
1221
    def set_memory_common(self, mem, _mem):
1222
        _mem.rxfreq = mem.freq / 10
1223
        self.unsplit_txfreq(mem)
1224

    
1225
        ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
1226
            chirp_common.split_tone_encode(mem)
1227
        self.encode_tone(_mem.txtone, txmode, txtone, txpol)
1228
        self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol)
1229

    
1230
        _mem.scan = mem.skip != "S"
1231
        _mem.wide = mem.mode == self.MODES[0]
1232

    
1233
        if mem.power:
1234
            _mem.lowpower = self.POWER_LEVELS.index(mem.power)
1235
        else:
1236
            _mem.lowpower = 0
1237

    
1238
        # extra settings
1239
        if len(mem.extra) > 0:
1240
            # there are setting, parse
1241
            for setting in mem.extra:
1242
                if setting.get_name() == "scode":
1243
                    setattr(_mem, setting.get_name(), str(int(setting.value) +
1244
                                                          self._scode_offset))
1245
                else:
1246
                    setattr(_mem, setting.get_name(), setting.value)
1247
        else:
1248
            # there are no extra settings, load defaults
1249
            _mem.bcl = 0
1250
            _mem.pttid = 0
1251
            _mem.scode = self._scode_offset
1252

    
1253
    def set_memory(self, mem):
1254
        _mem = self.get_raw_memory(mem.number)
1255

    
1256
        _mem.set_raw(b"\x00"*16 + b"\xff" * 16)
1257

    
1258
        if mem.empty:
1259
            _mem.set_raw(b"\xff" * 32)
1260
            return
1261

    
1262
        _namelength = self.get_features().valid_name_length
1263
        _mem.name = mem.name.ljust(_namelength, '\xFF')
1264

    
1265
        self.set_memory_common(mem, _mem)
1266

    
1267

    
1268
@directory.register
1269
class UV17ProGPS(UV17Pro):
1270
    VENDOR = "Baofeng"
1271
    MODEL = "UV-17ProGPS"
1272

    
1273
    _has_support_for_banknames = True
1274
    _has_workmode_support = True
1275
    _magic = MSTRING_UV17PROGPS
1276
    _magics = [(b"\x46", 16),
1277
               (b"\x4d", 7),
1278
               (b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01" +
1279
                b"\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09" +
1280
                b"\x10\x04\x00", 1)]
1281
    _has_when_to_send_aniid = False
1282
    _vfoscan = True
1283
    _has_gps = True
1284
    _has_voxsw = True
1285
    _has_pilot_tone = True
1286
    _has_send_id_delay = True
1287
    _has_skey2_short = True
1288
    VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
1289
                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
1290

    
1291
    def check_set_memory_immutable_policy(self, existing, new):
1292
        if (self._airband[0] <= new.freq <= self._airband[1] and
1293
                new.mode == 'AM'):
1294
            # This is valid, so mark mode as immutable so it doesn't get
1295
            # blocked, and let the radio override it during set.
1296
            new.immutable.append('mode')
1297
            existing.immutable = []
1298
        elif existing.mode == 'AM' and new.mode in self.MODES:
1299
            # If we're going from a forced-AM channel to some valid one,
1300
            # clear immutable so we allow the change.
1301
            try:
1302
                existing.immutable.remove('mode')
1303
            except ValueError:
1304
                pass
1305
        super().check_set_memory_immutable_policy(existing, new)
1306

    
1307

    
1308
@directory.register
1309
class BF5RM(UV17Pro):
1310
    VENDOR = "Baofeng"
1311
    MODEL = "5RM"
1312

    
1313
    VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
1314
                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
1315
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
1316
                    chirp_common.PowerLevel("Low", watts=1.00),
1317
                    chirp_common.PowerLevel("Medium", watts=5.00)]
1318
    SCODE_LIST = ["%s" % x for x in range(1, 16)]
1319
    LIST_PW_SAVEMODE = ["Off", "1:1", "1:2", "1:4"]
1320
    _has_workmode_support = True
1321

    
1322
    def check_set_memory_immutable_policy(self, existing, new):
1323
        if (self._airband[0] <= new.freq <= self._airband[1] and
1324
                new.mode == 'AM'):
1325
            # This is valid, so mark mode as immutable so it doesn't get
1326
            # blocked, and let the radio override it during set.
1327
            new.immutable.append('mode')
1328
            existing.immutable = []
1329
        elif existing.mode == 'AM' and new.mode in self.MODES:
1330
            # If we're going from a forced-AM channel to some valid one,
1331
            # clear immutable so we allow the change.
1332
            try:
1333
                existing.immutable.remove('mode')
1334
            except ValueError:
1335
                pass
1336
        super().check_set_memory_immutable_policy(existing, new)
1337

    
1338

    
1339
@directory.register
1340
class GM5RH(UV17Pro):
1341
    VENDOR = "Baofeng"
1342
    MODEL = "GM-5RH"
1343

    
1344
    VALID_BANDS = [UV17Pro._vhf_range, UV17Pro._vhf2_range, UV17Pro._uhf_range]
1345
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
1346
                    chirp_common.PowerLevel("Low", watts=0.50),
1347
                    chirp_common.PowerLevel("Medium", watts=3.00)]
1348
    SCODE_LIST = ["%s" % x for x in range(1, 16)]
1349
    LIST_PW_SAVEMODE = ["Off", "1:1", "2:1", "3:1", "4:1"]
1350
    _has_workmode_support = True
1351

    
1352
    _magic = MSTRING_GM5RH
(3-3/9)