Project

General

Profile

New Model #9793 » baofeng_uv17Pro.py

88598f92 pro - Dan Smith, 02/09/2024 06:45 PM

 
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
20
from chirp import chirp_common, directory, memmap
21
from chirp import bitwise
22
from chirp.settings import RadioSetting, \
23
    RadioSettingValueBoolean, RadioSettingValueList, \
24
    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

    
35
DTMF_CHARS = "0123456789 *#ABCD"
36
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
37

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

    
66

    
67
def model_match(cls, data):
68
    """Match the opened image to the correct version"""
69
    return data[cls.MEM_TOTAL:] == bytes(cls.MODEL, 'utf-8')
70

    
71

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

    
96

    
97
def _do_ident(radio):
98
    # Flush input buffer
99
    baofeng_common._clean_buffer(radio)
100

    
101
    # Ident radio
102
    magic = radio._magic
103
    baofeng_common._rawsend(radio, magic)
104
    ack = baofeng_common._rawrecv(radio, radio._magic_response_length)
105

    
106
    if not ack.startswith(radio._fingerprint):
107
        if ack:
108
            LOG.debug(repr(ack))
109
        raise errors.RadioError("Radio did not respond as expected (A)")
110

    
111
    return True
112

    
113

    
114
def _download(radio):
115
    """Get the memory map"""
116

    
117
    # Put radio in program mode and identify it
118
    _do_ident(radio)
119
    for index in range(len(radio._magics)):
120
        baofeng_common._rawsend(radio, radio._magics[index])
121
        baofeng_common._rawrecv(radio, radio._magicResponseLengths[index])
122

    
123
    data = b""
124

    
125
    # UI progress
126
    status = chirp_common.Status()
127
    status.cur = 0
128
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
129
    status.msg = "Cloning from radio..."
130
    radio.status_fn(status)
131

    
132
    for i in range(len(radio.MEM_SIZES)):
133
        MEM_SIZE = radio.MEM_SIZES[i]
134
        MEM_START = radio.MEM_STARTS[i]
135
        for addr in range(MEM_START, MEM_START + MEM_SIZE,
136
                          radio.BLOCK_SIZE):
137
            frame = radio._make_read_frame(addr, radio.BLOCK_SIZE)
138
            # DEBUG
139
            LOG.debug("Frame=" + util.hexprint(frame))
140

    
141
            # Sending the read request
142
            baofeng_common._rawsend(radio, frame)
143

    
144
            # Now we read data
145
            d = baofeng_common._rawrecv(radio, radio.BLOCK_SIZE + 4)
146

    
147
            LOG.debug("Response Data= " + util.hexprint(d))
148
            d = _crypt(1, d[4:])
149

    
150
            # Aggregate the data
151
            data += d
152

    
153
            # UI Update
154
            status.cur = len(data) // radio.BLOCK_SIZE
155
            status.msg = "Cloning from radio..."
156
            radio.status_fn(status)
157
    return data
158

    
159

    
160
def _upload(radio):
161
    # Put radio in program mode and identify it
162
    _do_ident(radio)
163
    for index in range(len(radio._magics)):
164
        baofeng_common._rawsend(radio, radio._magics[index])
165
        baofeng_common._rawrecv(radio, radio._magicResponseLengths[index])
166

    
167
    data = b""
168

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

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

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

    
191
            # Sending the read request
192
            baofeng_common._rawsend(radio, frame)
193

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

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

    
206

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

    
214
    MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
215
    MEM_SIZES = [0x8040, 0x0040, 0x02C0, 0x0040]
216

    
217
    MEM_TOTAL = 0x8380
218
    BLOCK_SIZE = 0x40
219
    BAUD_RATE = 115200
220

    
221
    download_function = _download
222
    upload_function = _upload
223

    
224
    _gmrs = False
225
    _bw_shift = False
226
    _has_support_for_banknames = False
227

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

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

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

    
283
    CHANNELS = 1000
284

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

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

    
330
    #seekto 0x8000;
331
    struct {
332
      struct vfo_entry a;
333
      struct vfo_entry b;
334
    } vfo;
335

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

    
389
    #seekto 0x8080;
390
    struct {
391
      u8 code[5];
392
      u8 unknown[1];
393
      u8 unused1:6,
394
         aniid:2;
395
      u8 dtmfon;
396
      u8 dtmfoff;
397
    } ani;
398

    
399
    #seekto 0x80A0;
400
    struct {
401
      u8 code[5];
402
      u8 name[10];
403
      u8 unused;
404
    } pttid[20];
405

    
406
    #seekto 0x8280;
407
    struct {
408
      char name[16];
409
    } bank_name[10];
410
    """
411

    
412
    def _make_read_frame(self, addr, length):
413
        """Pack the info in the header format"""
414
        frame = self._make_frame(b"\x52", addr, length)
415
        # Return the data
416
        return frame
417

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

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

    
450
    def process_mmap(self):
451
        """Process the mem map into the mem object"""
452
        # make lines shorter for style check.
453
        self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
454

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

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

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

    
485
    def get_settings_common_basic(self, basic, _mem):
486
        if _mem.settings.squelch >= len(self.SQUELCH_LIST):
487
            val = 0x00
488
        else:
489
            val = _mem.settings.squelch
490
        rs = RadioSetting("settings.squelch", "Squelch",
491
                          RadioSettingValueList(
492
                              self.SQUELCH_LIST, self.SQUELCH_LIST[val]))
493
        basic.append(rs)
494

    
495
        if _mem.settings.tot >= len(self.LIST_TIMEOUT):
496
            val = 0x03
497
        else:
498
            val = _mem.settings.tot
499
        rs = RadioSetting("settings.tot", "Timeout Timer",
500
                          RadioSettingValueList(
501
                              self.LIST_TIMEOUT, self.LIST_TIMEOUT[val]))
502
        basic.append(rs)
503

    
504
        rs = RadioSetting("settings.dualstandby", "Dual Watch",
505
                          RadioSettingValueBoolean(_mem.settings.dualstandby))
506
        basic.append(rs)
507

    
508
        if _mem.settings.powerondistype >= len(self.LIST_POWERON_DISPLAY_TYPE):
509
            val = 0x00
510
        else:
511
            rs = RadioSetting("settings.powerondistype",
512
                              "Power On Display Type",
513
                              RadioSettingValueList(
514
                                  self.LIST_POWERON_DISPLAY_TYPE,
515
                                  self.LIST_POWERON_DISPLAY_TYPE[
516
                                      _mem.settings.powerondistype]))
517
            basic.append(rs)
518

    
519
        if _mem.settings.voice >= len(self.LIST_VOICE):
520
            val = 0x01
521
        else:
522
            val = _mem.settings.voice
523
        rs = RadioSetting("settings.voice", "Voice Prompt",
524
                          RadioSettingValueList(
525
                              self.LIST_VOICE, self.LIST_VOICE[val]))
526
        basic.append(rs)
527

    
528
        rs = RadioSetting("settings.voicesw", "Enable Voice",
529
                          RadioSettingValueBoolean(_mem.settings.voicesw))
530
        basic.append(rs)
531

    
532
        if _mem.settings.backlight >= len(self.LIST_BACKLIGHT_TIMER):
533
            val = 0x00
534
        else:
535
            val = _mem.settings.backlight
536
        rs = RadioSetting("settings.backlight", "Backlight Timer",
537
                          RadioSettingValueList(
538
                              self.LIST_BACKLIGHT_TIMER,
539
                              self.LIST_BACKLIGHT_TIMER[val]))
540
        basic.append(rs)
541

    
542
        rs = RadioSetting("settings.autolock", "Key Auto Lock",
543
                          RadioSettingValueBoolean(_mem.settings.autolock))
544
        basic.append(rs)
545

    
546
        rs = RadioSetting("settings.beep", "Beep",
547
                          RadioSettingValueList(
548
                              LIST_BEEP, LIST_BEEP[_mem.settings.beep]))
549
        basic.append(rs)
550

    
551
        rs = RadioSetting("settings.roger", "Roger",
552
                          RadioSettingValueBoolean(_mem.settings.roger))
553
        basic.append(rs)
554

    
555
        rs = RadioSetting("settings.chadistype", "Channel A display type",
556
                          RadioSettingValueList(
557
                              self.LIST_MODE,
558
                              self.LIST_MODE[_mem.settings.chadistype]))
559
        basic.append(rs)
560

    
561
        rs = RadioSetting("settings.chbdistype", "Channel B display type",
562
                          RadioSettingValueList(
563
                              self.LIST_MODE,
564
                              self.LIST_MODE[_mem.settings.chbdistype]))
565
        basic.append(rs)
566

    
567
    def get_settings(self):
568
        """Translate the bit in the mem_struct into settings in the UI"""
569
        _mem = self._memobj
570
        basic = RadioSettingGroup("basic", "Basic Settings")
571
        bank = RadioSettingGroup("bank", "Bank names")
572
        dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
573
        top = RadioSettings(basic, bank, dtmfe)
574

    
575
        self.get_settings_common_basic(basic, _mem)
576

    
577
        rs = RadioSetting("settings.savemode", "Save Mode",
578
                          RadioSettingValueBoolean(_mem.settings.savemode))
579
        basic.append(rs)
580

    
581
        rs = RadioSetting("settings.totalarm", "Timeout Timer Alarm",
582
                          RadioSettingValueList(
583
                              LIST_TIMEOUT_ALARM,
584
                              LIST_TIMEOUT_ALARM[_mem.settings.totalarm]))
585
        basic.append(rs)
586

    
587
        if self._has_pilot_tone:
588
            rs = RadioSetting("settings.tone", "Pilot Tone",
589
                              RadioSettingValueList(
590
                                LIST_PILOT_TONE,
591
                                LIST_PILOT_TONE[_mem.settings.tone]))
592
            basic.append(rs)
593

    
594
        rs = RadioSetting("settings.sidetone", "Side Tone",
595
                          RadioSettingValueList(
596
                              LIST_SIDE_TONE,
597
                              LIST_SIDE_TONE[_mem.settings.sidetone]))
598
        basic.append(rs)
599

    
600
        rs = RadioSetting("settings.tailclear", "Tail Clear",
601
                          RadioSettingValueBoolean(_mem.settings.tailclear))
602
        basic.append(rs)
603

    
604
        rs = RadioSetting("settings.scanmode", "Scan Mode",
605
                          RadioSettingValueList(
606
                              LIST_SCANMODE,
607
                              LIST_SCANMODE[_mem.settings.scanmode]))
608
        basic.append(rs)
609

    
610
        rs = RadioSetting("settings.alarmmode", "Alarm Mode",
611
                          RadioSettingValueList(
612
                              LIST_ALARMMODE,
613
                              LIST_ALARMMODE[_mem.settings.alarmmode]))
614
        basic.append(rs)
615

    
616
        rs = RadioSetting("settings.alarmtone", "Sound Alarm",
617
                          RadioSettingValueBoolean(_mem.settings.alarmtone))
618
        basic.append(rs)
619

    
620
        rs = RadioSetting("settings.keylock", "Key Lock",
621
                          RadioSettingValueBoolean(_mem.settings.keylock))
622
        basic.append(rs)
623

    
624
        rs = RadioSetting("settings.menuquittime", "Menu Quit Timer",
625
                          RadioSettingValueList(
626
                              LIST_MENU_QUIT_TIME,
627
                              LIST_MENU_QUIT_TIME[
628
                                  _mem.settings.menuquittime]))
629
        basic.append(rs)
630

    
631
        if self._has_send_id_delay:
632
            rs = RadioSetting("settings.pttdly", "Send ID Delay",
633
                              RadioSettingValueList(
634
                                LIST_ID_DELAY,
635
                                LIST_ID_DELAY[_mem.settings.pttdly]))
636
            basic.append(rs)
637

    
638
        rs = RadioSetting("settings.ctsdcsscantype", "QT Save Mode",
639
                          RadioSettingValueList(
640
                              LIST_QT_SAVEMODE,
641
                              LIST_QT_SAVEMODE[_mem.settings.ctsdcsscantype]))
642
        basic.append(rs)
643

    
644
        def getKey2shortIndex(value):
645
            key_to_index = {0x07: 0,
646
                            0x1C: 1,
647
                            0x1D: 2,
648
                            0x2D: 3}
649
            return key_to_index.get(int(value), 0)
650

    
651
        def apply_Key2short(setting, obj):
652
            val = str(setting.value)
653
            key_to_index = {'FM': 0x07,
654
                            'Scan': 0x1C,
655
                            'Search': 0x1D,
656
                            'Vox': 0x2D}
657
            obj.key2short = key_to_index.get(val, 0x07)
658

    
659
        if self._has_skey2_short:
660
            rs = RadioSetting("settings.key2short", "Skey2 Short",
661
                              RadioSettingValueList(
662
                                LIST_SKEY2_SHORT,
663
                                LIST_SKEY2_SHORT[
664
                                    getKey2shortIndex(
665
                                        _mem.settings.key2short)]))
666
            rs.set_apply_callback(apply_Key2short, _mem.settings)
667
            basic.append(rs)
668

    
669
        rs = RadioSetting("settings.chaworkmode", "Channel A work mode",
670
                          RadioSettingValueList(
671
                              LIST_WORKMODE,
672
                              LIST_WORKMODE[_mem.settings.chaworkmode]))
673
        basic.append(rs)
674

    
675
        rs = RadioSetting("settings.chbworkmode", "Channel B work mode",
676
                          RadioSettingValueList(
677
                              LIST_WORKMODE,
678
                              LIST_WORKMODE[_mem.settings.chbworkmode]))
679
        basic.append(rs)
680

    
681
        rs = RadioSetting("settings.rpttailclear", "Rpt Tail Clear",
682
                          RadioSettingValueList(
683
                              LIST_RPT_TAIL_CLEAR,
684
                              LIST_RPT_TAIL_CLEAR[
685
                                  _mem.settings.rpttailclear]))
686
        basic.append(rs)
687

    
688
        rs = RadioSetting("settings.rpttaildet", "Rpt Tail Delay",
689
                          RadioSettingValueList(
690
                              LIST_RPT_TAIL_CLEAR,
691
                              LIST_RPT_TAIL_CLEAR[_mem.settings.rpttaildet]))
692
        basic.append(rs)
693

    
694
        rs = RadioSetting("settings.rstmenu", "Enable Menu Rst",
695
                          RadioSettingValueBoolean(_mem.settings.rstmenu))
696
        basic.append(rs)
697

    
698
        if self._has_voxsw:
699
            rs = RadioSetting("settings.voxsw", "Vox Switch",
700
                              RadioSettingValueBoolean(_mem.settings.voxsw))
701
            basic.append(rs)
702

    
703
            rs = RadioSetting("settings.vox", "Vox Level",
704
                              RadioSettingValueList(
705
                                LIST_VOX_LEVEL_ALT,
706
                                LIST_VOX_LEVEL_ALT[_mem.settings.vox]))
707
            basic.append(rs)
708
        else:
709
            rs = RadioSetting("settings.vox", "Vox Level",
710
                              RadioSettingValueList(
711
                                LIST_VOX_LEVEL,
712
                                LIST_VOX_LEVEL[_mem.settings.vox]))
713
            basic.append(rs)
714

    
715
        rs = RadioSetting("settings.voxdlytime", "Vox Delay Time",
716
                          RadioSettingValueList(
717
                              LIST_VOX_DELAY_TIME,
718
                              LIST_VOX_DELAY_TIME[_mem.settings.voxdlytime]))
719
        basic.append(rs)
720

    
721
        if self._has_gps:
722
            rs = RadioSetting("settings.gpsw", "GPS On",
723
                              RadioSettingValueBoolean(_mem.settings.gpsw))
724
            basic.append(rs)
725

    
726
            rs = RadioSetting("settings.gpsmode", "GPS Mode",
727
                              RadioSettingValueList(
728
                                LIST_GPS_MODE,
729
                                LIST_GPS_MODE[_mem.settings.gpsmode]))
730
            basic.append(rs)
731

    
732
            rs = RadioSetting("settings.gpstimezone", "GPS Timezone",
733
                              RadioSettingValueList(
734
                                LIST_GPS_TIMEZONE,
735
                                LIST_GPS_TIMEZONE[_mem.settings.gpstimezone]))
736
            basic.append(rs)
737

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

    
742
        def _filterName(name):
743
            fname = b""
744
            for char in name:
745
                if ord(str(char)) == 255:
746
                    break
747
                fname += int(char).to_bytes(1, 'big')
748
            return fname.decode('gb2312')
749

    
750
        def apply_bankname(setting, obj):
751
            name = str(setting.value).encode('gb2312')[:16]
752
            obj.name = name
753

    
754
        if self._has_support_for_banknames:
755
            for i in range(0, 10):
756
                _nameobj = self._memobj.bank_name[i]
757
                rs = RadioSetting("bank_name/%i.name" % i,
758
                                  "Bank name %i" % (i + 1),
759
                                  RadioSettingValueString(
760
                                      0, 16, _filterName(_nameobj.name), True,
761
                                      CHARSET_GB2312))
762
                rs.set_apply_callback(apply_bankname, _nameobj)
763
                bank.append(rs)
764

    
765
        self.get_settings_common_dtmf(dtmfe)
766

    
767
        if _mem.ani.dtmfon > 0xC3:
768
            val = 0x03
769
        else:
770
            val = _mem.ani.dtmfon
771
        rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
772
                          RadioSettingValueList(LIST_DTMFSPEED,
773
                                                LIST_DTMFSPEED[val]))
774
        dtmfe.append(rs)
775

    
776
        if _mem.ani.dtmfoff > 0xC3:
777
            val = 0x03
778
        else:
779
            val = _mem.ani.dtmfoff
780
        rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
781
                          RadioSettingValueList(LIST_DTMFSPEED,
782
                                                LIST_DTMFSPEED[val]))
783
        dtmfe.append(rs)
784

    
785
        if self._has_when_to_send_aniid:
786
            rs = RadioSetting("ani.aniid", "When to send ANI ID",
787
                              RadioSettingValueList(LIST_PTTID,
788
                                                    LIST_PTTID[
789
                                                        _mem.ani.aniid]))
790
            dtmfe.append(rs)
791

    
792
        if _mem.settings.hangup >= len(LIST_HANGUPTIME):
793
            val = 0
794
        else:
795
            val = _mem.settings.hangup
796
        rs = RadioSetting("settings.hangup", "Hang-up time",
797
                          RadioSettingValueList(LIST_HANGUPTIME,
798
                                                LIST_HANGUPTIME[
799
                                                    val]))
800
        dtmfe.append(rs)
801

    
802
        return top
803

    
804
    def sync_in(self):
805
        """Download from radio"""
806
        try:
807
            data = self.download_function()
808
        except errors.RadioError:
809
            # Pass through any real errors we raise
810
            raise
811
        except Exception:
812
            # If anything unexpected happens, make sure we raise
813
            # a RadioError and log the problem
814
            LOG.exception('Unexpected error during download')
815
            raise errors.RadioError('Unexpected error communicating '
816
                                    'with the radio')
817
        self._mmap = memmap.MemoryMapBytes(data)
818

    
819
        self.process_mmap()
820

    
821
    def sync_out(self):
822
        """Upload to radio"""
823
        try:
824
            self.upload_function()
825
        except errors.RadioError:
826
            raise
827
        except Exception:
828
            # If anything unexpected happens, make sure we raise
829
            # a RadioError and log the problem
830
            LOG.exception('Unexpected error during upload')
831
            raise errors.RadioError('Unexpected error communicating '
832
                                    'with the radio')
833

    
834
    def get_features(self):
835
        """Get the radio's features"""
836

    
837
        rf = chirp_common.RadioFeatures()
838
        rf.has_settings = True
839
        rf.has_bank = False
840
        rf.has_tuning_step = False
841
        rf.can_odd_split = True
842
        rf.has_name = True
843
        rf.has_offset = True
844
        rf.has_mode = True
845
        rf.has_dtcs = True
846
        rf.has_rx_dtcs = True
847
        rf.has_dtcs_polarity = True
848
        rf.has_ctone = True
849
        rf.has_cross = True
850
        rf.valid_modes = self.MODES
851
        rf.valid_characters = self.VALID_CHARS
852
        rf.valid_name_length = self.LENGTH_NAME
853
        if self._gmrs:
854
            rf.valid_duplexes = ["", "+", "off"]
855
        else:
856
            rf.valid_duplexes = ["", "-", "+", "split", "off"]
857
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
858
        rf.valid_cross_modes = [
859
            "Tone->Tone",
860
            "DTCS->",
861
            "->DTCS",
862
            "Tone->DTCS",
863
            "DTCS->Tone",
864
            "->Tone",
865
            "DTCS->DTCS"]
866
        rf.valid_skips = self.SKIP_VALUES
867
        rf.valid_dtcs_codes = self.DTCS_CODES
868
        rf.memory_bounds = (1, self.CHANNELS)
869
        rf.valid_power_levels = self.POWER_LEVELS
870
        rf.valid_bands = self.VALID_BANDS
871
        rf.valid_tuning_steps = STEPS
872

    
873
        return rf
874

    
875
    def decode_tone(self, val):
876
        mode = ""
877
        pol = "N"
878
        if val in [0, 0xFFFF]:
879
            xval = 0
880
        elif val >= 0x0258:
881
            mode = "Tone"
882
            xval = int(val) / 10.0
883
        elif val <= 0x0258:
884
            mode = "DTCS"
885
            if val > 0x69:
886
                index = val - 0x6A
887
                pol = "R"
888
            else:
889
                index = val - 1
890
            xval = self.DTCS_CODES[index]
891
        else:
892
            LOG.warn("Bug: tone is %04x" % val)
893
        return mode, xval, pol
894

    
895
    def encode_tone(self, memtone, mode, tone, pol):
896
        if mode == "Tone":
897
            memtone.set_value(int(tone * 10))
898
        elif mode == "TSQL":
899
            memtone.set_value(int(tone * 10))
900
        elif mode == "DTCS":
901
            if pol == 'R':
902
                memtone.set_value(self.DTCS_CODES.index(tone) + 1 + 0x69)
903
            else:
904
                memtone.set_value(self.DTCS_CODES.index(tone) + 1)
905
        else:
906
            memtone.set_value(0)
907

    
908
    def split_txfreq(self, _mem, freq):
909
        if self._is_txinh(_mem):
910
            # TX freq not set
911
            duplex = "off"
912
            offset = 0
913
        else:
914
            offset = (int(_mem.txfreq) * 10) - freq
915
            if offset != 0:
916
                if baofeng_common._split(self.get_features(), freq, int(
917
                          _mem.txfreq) * 10):
918
                    duplex = "split"
919
                    offset = int(_mem.txfreq) * 10
920
                elif offset < 0:
921
                    offset = abs(offset)
922
                    duplex = "-"
923
                elif offset > 0:
924
                    duplex = "+"
925
            else:
926
                duplex = ""
927
                offset = 0
928
        return offset, duplex
929

    
930
    def get_memory_common(self, _mem, name, mem):
931
        if _mem.get_raw()[0] == 255:
932
            mem.empty = True
933
            return mem
934

    
935
        mem.freq = int(_mem.rxfreq) * 10
936

    
937
        # TX freq set
938
        mem.offset, mem.duplex = self.split_txfreq(_mem, mem.freq)
939

    
940
        txtone = self.decode_tone(_mem.txtone)
941
        rxtone = self.decode_tone(_mem.rxtone)
942
        chirp_common.split_tone_decode(mem, txtone, rxtone)
943

    
944
        if not _mem.scan:
945
            mem.skip = "S"
946

    
947
        levels = self.POWER_LEVELS
948
        try:
949
            mem.power = levels[_mem.lowpower]
950
        except IndexError:
951
            mem.power = levels[0]
952

    
953
        mem.mode = _mem.wide and "NFM" or "FM"
954

    
955
        mem.extra = RadioSettingGroup("Extra", "extra")
956

    
957
        rs = RadioSetting("bcl", "BCL",
958
                          RadioSettingValueBoolean(_mem.bcl))
959
        mem.extra.append(rs)
960

    
961
        rs = RadioSetting("pttid", "PTT ID",
962
                          RadioSettingValueList(self.PTTID_LIST,
963
                                                self.PTTID_LIST[
964
                                                    _mem.pttid]))
965
        mem.extra.append(rs)
966

    
967
        scode = (_mem.scode - self._scode_offset) % len(self.SCODE_LIST)
968
        rs = RadioSetting("scode", "S-CODE",
969
                          RadioSettingValueList(self.SCODE_LIST,
970
                                                self.SCODE_LIST[
971
                                                    scode]))
972
        mem.extra.append(rs)
973

    
974
        mem.name = str(name).replace('\xFF', ' ').replace('\x00', ' ').rstrip()
975

    
976
    def get_raw_memory(self, number):
977
        return self._memobj.memory[number - 1]
978

    
979
    def get_memory(self, number):
980
        _mem = self.get_raw_memory(number)
981

    
982
        mem = chirp_common.Memory()
983
        mem.number = number
984

    
985
        self.get_memory_common(_mem, _mem.name, mem)
986

    
987
        return mem
988

    
989
    def unsplit_txfreq(self, mem):
990
        _mem = self.get_raw_memory(mem.number)
991
        if mem.duplex == "off":
992
            for i in range(0, 4):
993
                _mem.txfreq[i].set_raw(b"\xFF")
994
        elif mem.duplex == "split":
995
            _mem.txfreq = mem.offset / 10
996
        elif mem.duplex == "+":
997
            _mem.txfreq = (mem.freq + mem.offset) / 10
998
        elif mem.duplex == "-":
999
            _mem.txfreq = (mem.freq - mem.offset) / 10
1000
        else:
1001
            _mem.txfreq = mem.freq / 10
1002

    
1003
    def set_memory_common(self, mem, _mem):
1004
        _mem.rxfreq = mem.freq / 10
1005
        self.unsplit_txfreq(mem)
1006

    
1007
        ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
1008
            chirp_common.split_tone_encode(mem)
1009
        self.encode_tone(_mem.txtone, txmode, txtone, txpol)
1010
        self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol)
1011

    
1012
        _mem.scan = mem.skip != "S"
1013
        _mem.wide = mem.mode == "NFM"
1014

    
1015
        if mem.power:
1016
            _mem.lowpower = self.POWER_LEVELS.index(mem.power)
1017
        else:
1018
            _mem.lowpower = 0
1019

    
1020
        # extra settings
1021
        if len(mem.extra) > 0:
1022
            # there are setting, parse
1023
            for setting in mem.extra:
1024
                if setting.get_name() == "scode":
1025
                    setattr(_mem, setting.get_name(), str(int(setting.value) +
1026
                                                          self._scode_offset))
1027
                else:
1028
                    setattr(_mem, setting.get_name(), setting.value)
1029
        else:
1030
            # there are no extra settings, load defaults
1031
            _mem.bcl = 0
1032
            _mem.pttid = 0
1033
            _mem.scode = self._scode_offset
1034

    
1035
    def set_memory(self, mem):
1036
        _mem = self.get_raw_memory(mem.number)
1037

    
1038
        _mem.set_raw(b"\x00"*16 + b"\xff" * 16)
1039

    
1040
        if mem.empty:
1041
            _mem.set_raw(b"\xff" * 32)
1042
            return
1043

    
1044
        _namelength = self.get_features().valid_name_length
1045
        _mem.name = mem.name.ljust(_namelength, '\xFF')
1046

    
1047
        self.set_memory_common(mem, _mem)
1048

    
1049

    
1050
@directory.register
1051
class UV17ProGPS(UV17Pro):
1052
    VENDOR = "Baofeng"
1053
    MODEL = "UV-17ProGPS"
1054
    _has_support_for_banknames = True
1055
    _magic = MSTRING_UV17PROGPS
1056
    _magics = [b"\x46", b"\x4d", b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01" +
1057
               b"\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09" +
1058
               b"\x10\x04\x00"]
1059
    _magicResponseLengths = [16, 7, 1]
1060
    _has_when_to_send_aniid = False
1061
    _vfoscan = True
1062
    _has_gps = True
1063
    _has_voxsw = True
1064
    _has_pilot_tone = True
1065
    _has_send_id_delay = True
1066
    _has_skey2_short = True
1067
    VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
1068
                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
(7-7/12)