Project

General

Profile

New Model #10648 » baofeng_uv17Pro.py

Added DTMF and VFO settings - Sander van der Wel, 10/06/2023 01:31 PM

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

    
19
import logging
20

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

    
33
LOG = logging.getLogger(__name__)
34

    
35
# #### MAGICS #########################################################
36

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

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

    
44
LIST_AB = ["A", "B"]
45
LIST_ALMOD = ["Site", "Tone", "Code"]
46
LIST_BANDWIDTH = ["Wide", "Narrow"]
47
LIST_COLOR = ["Off", "Blue", "Orange", "Purple"]
48
LIST_DTMFSPEED = ["%s ms" % x for x in [50, 100, 200, 300, 500]]
49
LIST_HANGUPTIME = ["%s s" % x for x in [3, 4, 5, 6, 7, 8, 9, 10]]
50
LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
51
LIST_MODE = ["Channel", "Name", "Frequency"]
52
LIST_OFF1TO9 = ["Off"] + list("123456789")
53
LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"]
54
LIST_OFFAB = ["Off"] + LIST_AB
55
LIST_RESUME = ["TO", "CO", "SE"]
56
LIST_PONMSG = ["Full", "Message"]
57
LIST_PTTID = ["Off", "BOT", "EOT", "Both"]
58
LIST_SCODE = ["%s" % x for x in range(1, 21)]
59
LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)]
60
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"]
61
LIST_SHIFTD = ["Off", "+", "-"]
62
LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
63
LIST_STEP = [str(x) for x in STEPS]
64
LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)]
65
LIST_TXPOWER = ["High", "Low"]
66
LIST_VOICE = ["Off", "English", "Chinese"]
67
LIST_WORKMODE = ["Frequency", "Channel"]
68

    
69
TXP_CHOICES = ["High", "Low"]
70
TXP_VALUES = [0x00, 0x02]
71

    
72
STIMEOUT = 1.5
73

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

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

    
98
def _clean_buffer(radio):
99
    radio.pipe.timeout = 0.005
100
    junk = radio.pipe.read(256)
101
    radio.pipe.timeout = STIMEOUT
102
    if junk:
103
        LOG.debug("Got %i bytes of junk before starting" % len(junk))
104

    
105

    
106
def _rawrecv(radio, amount):
107
    """Raw read from the radio device"""
108
    data = ""
109
    try:
110
        data = radio.pipe.read(amount)
111
    except:
112
        msg = "Generic error reading data from radio; check your cable."
113
        raise errors.RadioError(msg)
114

    
115
    if len(data) != amount:
116
        msg = "Error reading data from radio: not the amount of data we want."
117
        raise errors.RadioError(msg)
118

    
119
    return data
120

    
121

    
122
def _rawsend(radio, data):
123
    """Raw send to the radio device"""
124
    try:
125
        #print(data)
126
        #print(radio)
127
        radio.pipe.write(data)
128
    except:
129
        raise errors.RadioError("Error sending data to radio")
130

    
131
def _make_read_frame(addr, length):
132
    """Pack the info in the header format"""
133
    frame = _make_frame(b"\x52", addr, length)
134
    # Return the data
135

    
136
    return frame
137

    
138
def _make_frame(cmd, addr, length, data=""):
139
    """Pack the info in the header format"""
140
    frame = cmd+struct.pack(">i",addr)[2:]+struct.pack("b", length)
141
    # add the data if set
142
    if len(data) != 0:
143
        frame += data
144
    # return the data
145
    return frame
146

    
147

    
148
def _recv(radio, addr, length):
149
    """Get data from the radio """
150
    # read 4 bytes of header
151
    hdr = _rawrecv(radio, 4)
152

    
153
    # read data
154
    data = _rawrecv(radio, length)
155

    
156
    # DEBUG
157
    LOG.info("Response:")
158
    LOG.debug(util.hexprint(hdr + data))
159

    
160
    c, a, l = struct.unpack(">BHB", hdr)
161
    if a != addr or l != length or c != ord("X"):
162
        LOG.error("Invalid answer for block 0x%04x:" % addr)
163
        LOG.debug("CMD: %s  ADDR: %04x  SIZE: %02x" % (c, a, l))
164
        raise errors.RadioError("Unknown response from the radio")
165

    
166
    return data
167

    
168

    
169
def _get_radio_firmware_version(radio):
170
    # There is a problem in new radios where a different firmware version is
171
    # received when directly reading a single block as compared to what is
172
    # received when reading sequential blocks. This causes a mismatch between
173
    # the image firmware version and the radio firmware version when uploading
174
    # an image that came from the same radio. The workaround is to read 1 or
175
    # more consecutive blocks prior to reading the block with the firmware
176
    # version.
177
    #
178
    # Read 2 consecutive blocks to get the radio firmware version.
179
    for addr in range(0x1E80, 0x1F00, radio._recv_block_size):
180
        frame = _make_frame("S", addr, radio._recv_block_size)
181

    
182
        # sending the read request
183
        _rawsend(radio, frame)
184

    
185
        if radio._ack_block and addr != 0x1E80:
186
            ack = _rawrecv(radio, 1)
187
            if ack != b"\x06":
188
                raise errors.RadioError(
189
                    "Radio refused to send block 0x%04x" % addr)
190

    
191
        # now we read
192
        block = _recv(radio, addr, radio._recv_block_size)
193

    
194
        _rawsend(radio, b"\x06")
195
        time.sleep(0.05)
196

    
197
    # get firmware version from the last block read
198
    version = block[48:64]
199
    return version
200

    
201

    
202
def _image_ident_from_data(data, start, stop):
203
    return data[start:stop]
204

    
205

    
206
def _get_image_firmware_version(radio):
207
    return _image_ident_from_data(radio.get_mmap(), radio._fw_ver_start,
208
                                  radio._fw_ver_start + 0x10)
209

    
210
def _sendmagic(radio, magic, response):
211
    _rawsend(radio, magic)
212
    ack = _rawrecv(radio, len(response))
213
    if ack != response:
214
        if ack:
215
            LOG.debug(repr(ack))
216
        raise errors.RadioError("Radio did not respond to enter read mode")
217
    
218
def _do_ident(radio):
219
    """Put the radio in PROGRAM mode & identify it"""
220
    radio.pipe.baudrate = radio.BAUDRATE
221
    radio.pipe.parity = "N"
222
    radio.pipe.timeout = STIMEOUT
223

    
224
    # Flush input buffer
225
    _clean_buffer(radio)
226

    
227
    # Ident radio
228
    magic = radio._magic
229
    _rawsend(radio, magic)
230
    ack = _rawrecv(radio, radio._magic_response_length)
231

    
232
    if not ack.startswith(radio._fingerprint):
233
        if ack:
234
            LOG.debug(repr(ack))
235
        raise errors.RadioError("Radio did not respond as expected (A)")
236

    
237
    return True
238

    
239

    
240
def _ident_radio(radio):
241
    for magic in radio._magic:
242
        error = None
243
        try:
244
            data = _do_ident(radio, magic)
245
            return data
246
        except errors.RadioError as e:
247
            print(e)
248
            error = e
249
            time.sleep(2)
250
    if error:
251
        raise error
252
    raise errors.RadioError("Radio did not respond")
253

    
254

    
255
def _download(radio):
256
    """Get the memory map"""
257

    
258
    # Put radio in program mode and identify it
259
    _do_ident(radio)
260
    for index in range(len(radio._magics)):
261
        _rawsend(radio, radio._magics[index])
262
        _rawrecv(radio, radio._magicResponseLengths[index])
263

    
264
    data = b""
265

    
266
    # UI progress
267
    status = chirp_common.Status()
268
    status.cur = 0
269
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
270
    status.msg = "Cloning from radio..."
271
    radio.status_fn(status)
272

    
273
    for i in range(len(radio.MEM_SIZES)):
274
        MEM_SIZE = radio.MEM_SIZES[i]
275
        MEM_START = radio.MEM_STARTS[i]
276
        for addr in range(MEM_START, MEM_START + MEM_SIZE, radio.BLOCK_SIZE):
277
            frame = _make_read_frame(addr, radio.BLOCK_SIZE)
278
            # DEBUG
279
            LOG.debug("Frame=" + util.hexprint(frame))
280

    
281
            # Sending the read request
282
            _rawsend(radio, frame)
283

    
284
            # Now we read data
285
            d = _rawrecv(radio, radio.BLOCK_SIZE + 4)
286

    
287
            LOG.debug("Response Data= " + util.hexprint(d))
288
            d = _crypt(1, d[4:])
289

    
290
            # Aggregate the data
291
            data += d
292

    
293
            # UI Update
294
            status.cur = len(data) // radio.BLOCK_SIZE
295
            status.msg = "Cloning from radio..."
296
            radio.status_fn(status)
297
    data += bytes(radio.MODEL, 'utf-8')
298
    return data
299

    
300
def _upload(radio):
301
    # Put radio in program mode and identify it
302
    _do_ident(radio)
303
    for index in range(len(radio._magics)):
304
        _rawsend(radio, radio._magics[index])
305
        _rawrecv(radio, radio._magicResponseLengths[index])
306

    
307
    data = b""
308

    
309
    # UI progress
310
    status = chirp_common.Status()
311
    status.cur = 0
312
    status.max = radio.MEM_TOTAL // radio.BLOCK_SIZE
313
    status.msg = "Cloning from radio..."
314
    radio.status_fn(status)
315

    
316
    data_addr = 0x00
317
    for i in range(len(radio.MEM_SIZES)):
318
        MEM_SIZE = radio.MEM_SIZES[i]
319
        MEM_START = radio.MEM_STARTS[i]
320
        for addr in range(MEM_START, MEM_START + MEM_SIZE, radio.BLOCK_SIZE):
321
            data = radio.get_mmap()[data_addr:data_addr + radio.BLOCK_SIZE]
322
            data = _crypt(1, data)
323
            data_addr += radio.BLOCK_SIZE
324

    
325
            frame = _make_frame(b"W", addr, radio.BLOCK_SIZE, data)
326
            # DEBUG
327
            LOG.debug("Frame=" + util.hexprint(frame))
328
            #print()
329
            #print(hex(data_addr))
330
            #print(util.hexprint(frame))
331

    
332
            # Sending the read request
333
            _rawsend(radio, frame)
334

    
335
            # receiving the response
336
            ack = _rawrecv(radio, 1)
337
            #ack = b"\x06"
338
            if ack != b"\x06":
339
                msg = "Bad ack writing block 0x%04x" % addr
340
                raise errors.RadioError(msg)
341

    
342
            # UI Update
343
            status.cur = data_addr // radio.BLOCK_SIZE
344
            status.msg = "Cloning to radio..."
345
            radio.status_fn(status)
346
    data += bytes(radio.MODEL, 'utf-8')
347
    return data
348

    
349

    
350
def _split(rf, f1, f2):
351
    """Returns False if the two freqs are in the same band (no split)
352
    or True otherwise"""
353

    
354
    # determine if the two freqs are in the same band
355
    for low, high in rf.valid_bands:
356
        if f1 >= low and f1 <= high and \
357
                f2 >= low and f2 <= high:
358
            # if the two freqs are on the same Band this is not a split
359
            return False
360

    
361
    # if you get here is because the freq pairs are split
362
    return True
363

    
364
@directory.register
365
class UV17Pro(chirp_common.CloneModeRadio, 
366
              chirp_common.ExperimentalRadio):
367
    """Baofeng UV-17Pro"""
368
    VENDOR = "Baofeng"
369
    MODEL = "UV-17Pro"
370
    NEEDS_COMPAT_SERIAL = False
371

    
372
    MEM_STARTS = [0x0000, 0x9000, 0xA000, 0xD000]
373
    MEM_SIZES =  [0x8040, 0x0040, 0x02C0, 0x0040]
374

    
375
    MEM_TOTAL = 0x8380
376
    BLOCK_SIZE = 0x40
377
    STIMEOUT = 2
378
    BAUDRATE = 115200
379

    
380
    _gmrs = False
381
    _bw_shift = False
382
    _support_banknames = False
383

    
384
    _tri_band = True
385
    _fileid = []
386
    _magic = MSTRING_UV17L
387
    _magic_response_length = 1
388
    _fingerprint = b"\x06"
389
    _magics = [b"\x46", b"\x4d", b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09\x10\x04\x00"]
390
    _magicResponseLengths = [16, 15, 1]
391
    _fw_ver_start = 0x1EF0
392
    _recv_block_size = 0x40
393
    _mem_size = MEM_TOTAL
394
    _ack_block = True
395
    _aniid = True
396

    
397
    MODES = ["NFM", "FM"]
398
    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
399
        "!@#$%^&*()+-=[]:\";'<>?,./"
400
    LENGTH_NAME = 12
401
    SKIP_VALUES = ["", "S"]
402
    DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
403
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
404
                    chirp_common.PowerLevel("Low",  watts=1.00)]
405
    _airband = (108000000, 136000000)
406
    _vhf_range = (136000000, 174000000)
407
    _vhf2_range = (200000000, 260000000)
408
    _uhf_range = (400000000, 520000000)
409
    _uhf2_range = (350000000, 390000000)
410

    
411
    VALID_BANDS = [_vhf_range, _vhf2_range,
412
                   _uhf_range]
413
    PTTID_LIST = LIST_PTTID
414
    SCODE_LIST = LIST_SCODE
415

    
416
    MEM_FORMAT = """
417
    #seekto 0x0000;
418
    struct {
419
      lbcd rxfreq[4];
420
      lbcd txfreq[4];
421
      ul16 rxtone;
422
      ul16 txtone;
423
      u8 scode:8;
424
      u8 pttid:8;
425
      u8 lowpower:8;
426
      u8 unknown1:1,
427
         wide:1,
428
         sqmode:2,
429
         bcl:1,
430
         scan:1,
431
         unknown2:1,
432
         fhss:1;
433
      u8 unknown3:8;
434
      u8 unknown4:8;
435
      u8 unknown5:8;
436
      u8 unknown6:8;
437
      char name[12];
438
    } memory[1000];
439

    
440
    #seekto 0x8080;
441
    struct {
442
      u8 code[5];
443
      u8 unknown[1];
444
      u8 unused1:6,
445
         aniid:2;
446
      u8 dtmfon;
447
      u8 dtmfoff;
448
    } ani;
449

    
450
    #seekto 0x80A0;
451
    struct {
452
      u8 code[5];
453
      u8 name[10];
454
      u8 unused:8;
455
    } pttid[20];
456

    
457
    
458
    #seekto 0x8280;
459
    struct {
460
      char name1[12];
461
      u32 unknown1;
462
      char name2[12];
463
      u32 unknown2;
464
      char name3[12];
465
      u32 unknown3;
466
      char name4[12];
467
      u32 unknown4;
468
      char name5[12];
469
      u32 unknown5;
470
      char name6[12];
471
      u32 unknown6;
472
      char name7[12];
473
      u32 unknown7;
474
      char name8[12];
475
      u32 unknown8;
476
      char name9[12];
477
      u32 unknown9;
478
      char name10[12];
479
      u32 unknown10;
480
    } bank_names;
481

    
482
    struct vfo {
483
      u8 freq[8];
484
      ul16 rxtone;
485
      ul16 txtone;
486
      u8 unknown0;
487
      u8 bcl;
488
      u8 sftd:3,
489
         scode:5;
490
      u8 unknown1;
491
      u8 lowpower;
492
      u8 unknown2:1, 
493
         wide:1,
494
         unknown3:5,
495
         fhss:1;
496
      u8 unknown4;
497
      u8 step;
498
      u8 offset[6];
499
      u8 unknown5[2];
500
      u8 sqmode;
501
      u8 unknown6[3];
502
    };
503

    
504
    #seekto 0x8000;
505
    struct {
506
      struct vfo a;
507
      struct vfo b;
508
    } vfo;
509

    
510
    #seekto 0x8040;
511
    struct {
512
      char unknown0[11];
513
      u8 pttid;
514
      char unknown02[3];
515
      u8 uknown2:7,
516
         bcl:1;
517
      char unknown3[41];
518
      u8 hangup;
519
      char unknown4[6];
520
    } settings;
521
    """
522

    
523
    @classmethod
524
    def get_prompts(cls):
525
        rp = chirp_common.RadioPrompts()
526
        rp.experimental = \
527
            ('This driver is a beta version.\n'
528
             '\n'
529
             'Please save an unedited copy of your first successful\n'
530
             'download to a CHIRP Radio Images(*.img) file.'
531
             )
532
        rp.pre_download = _(
533
            "Follow these instructions to download your info:\n"
534
            "1 - Turn off your radio\n"
535
            "2 - Connect your interface cable\n"
536
            "3 - Turn on your radio\n"
537
            "4 - Do the download of your radio data\n")
538
        rp.pre_upload = _(
539
            "Follow this instructions to upload your info:\n"
540
            "1 - Turn off your radio\n"
541
            "2 - Connect your interface cable\n"
542
            "3 - Turn on your radio\n"
543
            "4 - Do the upload of your radio data\n")
544
        return rp
545

    
546
    def process_mmap(self):
547
        """Process the mem map into the mem object"""
548
        if (len(self._mmap) == self.MEM_TOTAL) or (len(self._mmap) == self.MEM_TOTAL + len(self.MODEL)):
549
            self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)
550
        else:
551
            raise errors.ImageDetectFailed('Image length mismatch.\nTry reloading the configuration'
552
                                           ' from the radio.')
553

    
554
    def get_settings(self):
555
        """Translate the bit in the mem_struct into settings in the UI"""
556
        _mem = self._memobj
557
        basic = RadioSettingGroup("basic", "Basic Settings")
558
        advanced = RadioSettingGroup("advanced", "Advanced Settings")
559
        other = RadioSettingGroup("other", "Other Settings")
560
        work = RadioSettingGroup("work", "Work Mode Settings")
561
        fm_preset = RadioSettingGroup("fm_preset", "FM Preset")
562
        dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings")
563
        service = RadioSettingGroup("service", "Service Settings")
564
        top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe,
565
                            service)
566
     
567
        def _filterName(name):
568
            fname = ""
569
            for char in name:
570
                if ord(str(char)) == 255:
571
                    break
572
                fname += str(char)
573
            return fname
574
        
575
        if self._support_banknames:
576
            _msg = _mem.bank_names
577
            rs = RadioSetting("bank_names.name1", "Bank name 1",
578
                            RadioSettingValueString(
579
                                0, 12, _filterName(_msg.name1)))
580
            other.append(rs)
581

    
582
            _msg = _mem.bank_names
583
            rs = RadioSetting("bank_names.name2", "Bank name 2",
584
                            RadioSettingValueString(
585
                                0, 12, _filterName(_msg.name2)))
586
            
587
            other.append(rs)
588
            _msg = _mem.bank_names
589
            rs = RadioSetting("bank_names.name3", "Bank name 3",
590
                            RadioSettingValueString(
591
                                0, 12, _filterName(_msg.name3)))
592
            
593
            other.append(rs)
594
            _msg = _mem.bank_names
595
            rs = RadioSetting("bank_names.name4", "Bank name 4",
596
                            RadioSettingValueString(
597
                                0, 12, _filterName(_msg.name4)))
598
            
599
            other.append(rs)
600
            _msg = _mem.bank_names
601
            rs = RadioSetting("bank_names.name5", "Bank name 5",
602
                            RadioSettingValueString(
603
                                0, 12, _filterName(_msg.name5)))
604
            
605
            other.append(rs)
606
            _msg = _mem.bank_names
607
            rs = RadioSetting("bank_names.name6", "Bank name 6",
608
                            RadioSettingValueString(
609
                                0, 12, _filterName(_msg.name6)))
610
            
611
            other.append(rs)
612
            _msg = _mem.bank_names
613
            rs = RadioSetting("bank_names.name7", "Bank name 7",
614
                            RadioSettingValueString(
615
                                0, 12, _filterName(_msg.name7)))
616
            
617
            other.append(rs)
618
            _msg = _mem.bank_names
619
            rs = RadioSetting("bank_names.name8", "Bank name 8",
620
                            RadioSettingValueString(
621
                                0, 12, _filterName(_msg.name8)))
622
            
623
            other.append(rs)
624
            _msg = _mem.bank_names
625
            rs = RadioSetting("bank_names.name9", "Bank name 9",
626
                            RadioSettingValueString(
627
                                0, 12, _filterName(_msg.name9)))
628
            other.append(rs)
629

    
630
            _msg = _mem.bank_names
631
            rs = RadioSetting("bank_names.name10", "Bank name 10",
632
                            RadioSettingValueString(
633
                                0, 12, _filterName(_msg.name10)))
634
            other.append(rs)
635

    
636
        _codeobj = self._memobj.ani.code
637
        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
638
        val = RadioSettingValueString(0, 5, _code, False)
639
        val.set_charset(DTMF_CHARS)
640
        rs = RadioSetting("ani.code", "ANI Code", val)
641

    
642
        # DTMF settings
643
        def apply_code(setting, obj, length):
644
            code = []
645
            for j in range(0, length):
646
                try:
647
                    code.append(DTMF_CHARS.index(str(setting.value)[j]))
648
                except IndexError:
649
                    code.append(0xFF)
650
            obj.code = code
651

    
652
        for i in range(0, 20):
653
            _codeobj = self._memobj.pttid[i].code
654
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
655
            val = RadioSettingValueString(0, 5, _code, False)
656
            val.set_charset(DTMF_CHARS)
657
            pttid = RadioSetting("pttid/%i.code" % i,
658
                                 "Signal Code %i" % (i + 1), val)
659
            pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
660
            dtmfe.append(pttid)
661

    
662
        if _mem.ani.dtmfon > 0xC3:
663
            val = 0x03
664
        else:
665
            val = _mem.ani.dtmfon
666
        rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)",
667
                          RadioSettingValueList(LIST_DTMFSPEED,
668
                                                LIST_DTMFSPEED[val]))
669
        dtmfe.append(rs)
670

    
671
        if _mem.ani.dtmfoff > 0xC3:
672
            val = 0x03
673
        else:
674
            val = _mem.ani.dtmfoff
675
        rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)",
676
                          RadioSettingValueList(LIST_DTMFSPEED,
677
                                                LIST_DTMFSPEED[val]))
678
        dtmfe.append(rs)
679

    
680
        _codeobj = self._memobj.ani.code
681
        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
682
        val = RadioSettingValueString(0, 5, _code, False)
683
        val.set_charset(DTMF_CHARS)
684
        rs = RadioSetting("ani.code", "ANI Code", val)
685
        rs.set_apply_callback(apply_code, self._memobj.ani, 5)
686
        dtmfe.append(rs)
687

    
688
        if self._aniid:
689
            rs = RadioSetting("ani.aniid", "When to send ANI ID",
690
                            RadioSettingValueList(LIST_PTTID,
691
                                                    LIST_PTTID[_mem.ani.aniid]))
692
            dtmfe.append(rs)
693

    
694
        rs = RadioSetting("settings.hangup", "Hang-up time",
695
                        RadioSettingValueList(LIST_HANGUPTIME,
696
                                                LIST_HANGUPTIME[_mem.settings.hangup]))
697
        dtmfe.append(rs)
698

    
699
        def convert_bytes_to_freq(bytes):
700
            real_freq = 0
701
            for byte in bytes:
702
                real_freq = (real_freq * 10) + byte
703
            return chirp_common.format_freq(real_freq * 10)
704

    
705
        def my_validate(value):
706
            value = chirp_common.parse_freq(value)
707
            freqOk = False
708
            for band in self.VALID_BANDS:
709
                if value > band[0] and value < band[1]:
710
                    freqOk = True
711
            if not freqOk:
712
                raise InvalidValueError("Invalid frequency!")
713
            return chirp_common.format_freq(value)
714

    
715
        def apply_freq(setting, obj):
716
            value = chirp_common.parse_freq(str(setting.value)) / 10
717
            for i in range(7, -1, -1):
718
                obj.freq[i] = value % 10
719
                value /= 10
720

    
721
        val1a = RadioSettingValueString(0, 10,
722
                                        convert_bytes_to_freq(_mem.vfo.a.freq))
723
        val1a.set_validate_callback(my_validate)
724
        rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a)
725
        rs.set_apply_callback(apply_freq, _mem.vfo.a)
726
        work.append(rs)
727

    
728
        val1b = RadioSettingValueString(0, 10,
729
                                        convert_bytes_to_freq(_mem.vfo.b.freq))
730
        val1b.set_validate_callback(my_validate)
731
        rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b)
732
        rs.set_apply_callback(apply_freq, _mem.vfo.b)
733
        work.append(rs)
734

    
735
        rs = RadioSetting("vfo.a.sftd", "VFO A Shift",
736
                          RadioSettingValueList(
737
                              LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.a.sftd]))
738
        work.append(rs)
739

    
740
        rs = RadioSetting("vfo.b.sftd", "VFO B Shift",
741
                          RadioSettingValueList(
742
                              LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.b.sftd]))
743
        work.append(rs)
744

    
745

    
746
        def convert_bytes_to_offset(bytes):
747
            real_offset = 0
748
            for byte in bytes:
749
                real_offset = (real_offset * 10) + byte
750
            return chirp_common.format_freq(real_offset * 1000)
751

    
752
        def apply_offset(setting, obj):
753
            value = chirp_common.parse_freq(str(setting.value)) / 1000
754
            for i in range(5, -1, -1):
755
                obj.offset[i] = value % 10
756
                value /= 10
757

    
758
        val1a = RadioSettingValueString(
759
                    0, 10, convert_bytes_to_offset(_mem.vfo.a.offset))
760
        rs = RadioSetting("vfo.a.offset",
761
                          "VFO A Offset", val1a)
762
        rs.set_apply_callback(apply_offset, _mem.vfo.a)
763
        work.append(rs)
764

    
765
        val1b = RadioSettingValueString(
766
                    0, 10, convert_bytes_to_offset(_mem.vfo.b.offset))
767
        rs = RadioSetting("vfo.b.offset",
768
                          "VFO B Offset", val1b)
769
        rs.set_apply_callback(apply_offset, _mem.vfo.b)
770
        work.append(rs)
771

    
772
        def apply_txpower_listvalue(setting, obj):
773
            LOG.debug("Setting value: " + str(
774
                      setting.value) + " from list")
775
            val = str(setting.value)
776
            index = TXP_CHOICES.index(val)
777
            val = TXP_VALUES[index]
778
            obj.set_value(val)
779

    
780
        rs = RadioSetting("vfo.a.lowpower", "VFO A Power",
781
                            RadioSettingValueList(
782
                                LIST_TXPOWER,
783
                                LIST_TXPOWER[min(_mem.vfo.a.lowpower, 0x02)]
784
                                            ))
785
        work.append(rs)
786

    
787
        rs = RadioSetting("vfo.b.lowpower", "VFO B Power",
788
                            RadioSettingValueList(
789
                                LIST_TXPOWER,
790
                                LIST_TXPOWER[min(_mem.vfo.b.lowpower, 0x02)]
791
                                            ))
792
        work.append(rs)
793

    
794
        rs = RadioSetting("vfo.a.wide", "VFO A Bandwidth",
795
                          RadioSettingValueList(
796
                              LIST_BANDWIDTH,
797
                              LIST_BANDWIDTH[_mem.vfo.a.wide]))
798
        work.append(rs)
799

    
800
        rs = RadioSetting("vfo.b.wide", "VFO B Bandwidth",
801
                          RadioSettingValueList(
802
                              LIST_BANDWIDTH,
803
                              LIST_BANDWIDTH[_mem.vfo.b.wide]))
804
        work.append(rs)
805

    
806
        rs = RadioSetting("vfo.a.scode", "VFO A S-CODE",
807
                          RadioSettingValueList(
808
                              LIST_SCODE,
809
                              LIST_SCODE[_mem.vfo.a.scode]))
810
        work.append(rs)
811

    
812
        rs = RadioSetting("vfo.b.scode", "VFO B S-CODE",
813
                          RadioSettingValueList(
814
                              LIST_SCODE,
815
                              LIST_SCODE[_mem.vfo.b.scode]))
816
        work.append(rs)
817

    
818
        rs = RadioSetting("vfo.a.step", "VFO A Tuning Step",
819
                          RadioSettingValueList(
820
                              LIST_STEP, LIST_STEP[_mem.vfo.a.step]))
821
        work.append(rs)
822
        rs = RadioSetting("vfo.b.step", "VFO B Tuning Step",
823
                          RadioSettingValueList(
824
                              LIST_STEP, LIST_STEP[_mem.vfo.b.step]))
825
        work.append(rs)
826

    
827
        rs = RadioSetting("vfo.a.fhss", "VFO A FHSS",
828
                          RadioSettingValueBoolean(_mem.vfo.a.fhss))
829
        work.append(rs)
830

    
831
        rs = RadioSetting("vfo.b.fhss", "VFO B FHSS",
832
                          RadioSettingValueBoolean(_mem.vfo.b.fhss))
833
        work.append(rs)
834

    
835
        rs = RadioSetting("settings.bcl", "BCL",
836
                          RadioSettingValueBoolean(_mem.settings.bcl))
837
        work.append(rs)
838

    
839
        rs = RadioSetting("settings.pttid", "PTT ID",
840
                          RadioSettingValueList(self.PTTID_LIST,
841
                                                self.PTTID_LIST[_mem.settings.pttid]))
842
        work.append(rs)
843

    
844

    
845
        return top
846
    
847

    
848
    
849
        # TODO: implement settings 
850

    
851
    
852
    def sync_in(self):
853
        """Download from radio"""
854
        try:
855
            data = _download(self)
856
        except errors.RadioError:
857
            # Pass through any real errors we raise
858
            raise
859
        except:
860
            # If anything unexpected happens, make sure we raise
861
            # a RadioError and log the problem
862
            LOG.exception('Unexpected error during download')
863
            raise errors.RadioError('Unexpected error communicating '
864
                                    'with the radio')
865
        self._mmap = memmap.MemoryMapBytes(data)
866

    
867
        self.process_mmap()
868

    
869
    def sync_out(self):
870
        """Upload to radio"""
871
        try:
872
            _upload(self)
873
        except errors.RadioError:
874
            raise
875
        except Exception:
876
            # If anything unexpected happens, make sure we raise
877
            # a RadioError and log the problem
878
            LOG.exception('Unexpected error during upload')
879
            raise errors.RadioError('Unexpected error communicating '
880
                                    'with the radio')
881

    
882
    def get_features(self):
883
        """Get the radio's features"""
884

    
885
        rf = chirp_common.RadioFeatures()
886
        rf.has_settings = True
887
        rf.has_bank = False
888
        rf.has_tuning_step = False
889
        rf.can_odd_split = True
890
        rf.has_name = True
891
        rf.has_offset = True
892
        rf.has_mode = True
893
        rf.has_dtcs = True
894
        rf.has_rx_dtcs = True
895
        rf.has_dtcs_polarity = True
896
        rf.has_ctone = True
897
        rf.has_cross = True
898
        rf.valid_modes = self.MODES
899
        rf.valid_characters = self.VALID_CHARS
900
        rf.valid_name_length = self.LENGTH_NAME
901
        if self._gmrs:
902
            rf.valid_duplexes = ["", "+", "off"]
903
        else:
904
            rf.valid_duplexes = ["", "-", "+", "split", "off"]
905
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
906
        rf.valid_cross_modes = [
907
            "Tone->Tone",
908
            "DTCS->",
909
            "->DTCS",
910
            "Tone->DTCS",
911
            "DTCS->Tone",
912
            "->Tone",
913
            "DTCS->DTCS"]
914
        rf.valid_skips = self.SKIP_VALUES
915
        rf.valid_dtcs_codes = self.DTCS_CODES
916
        rf.memory_bounds = (0, 999)
917
        rf.valid_power_levels = self.POWER_LEVELS
918
        rf.valid_bands = self.VALID_BANDS
919
        rf.valid_tuning_steps = STEPS
920

    
921
        return rf
922

    
923
    def _is_txinh(self, _mem):
924
        raw_tx = ""
925
        for i in range(0, 4):
926
            raw_tx += _mem.txfreq[i].get_raw()
927
        return raw_tx == "\xFF\xFF\xFF\xFF"
928

    
929
    def get_memory(self, number):
930
        _mem = self._memobj.memory[number]
931
        #_nam = self._memobj.names[number]
932
        #print(number)
933
        #print(_mem)
934
        #print(_nam)
935

    
936
        mem = chirp_common.Memory()
937
        mem.number = number
938

    
939
        if _mem.get_raw()[0] == "\xff":
940
            mem.empty = True
941
            return mem
942

    
943
        mem.freq = int(_mem.rxfreq) * 10
944

    
945
        if self._is_txinh(_mem):
946
            # TX freq not set
947
            mem.duplex = "off"
948
            mem.offset = 0
949
        else:
950
            # TX freq set
951
            offset = (int(_mem.txfreq) * 10) - mem.freq
952
            if offset != 0:
953
                if _split(self.get_features(), mem.freq, int(
954
                          _mem.txfreq) * 10):
955
                    mem.duplex = "split"
956
                    mem.offset = int(_mem.txfreq) * 10
957
                elif offset < 0:
958
                    mem.offset = abs(offset)
959
                    mem.duplex = "-"
960
                elif offset > 0:
961
                    mem.offset = offset
962
                    mem.duplex = "+"
963
            else:
964
                mem.offset = 0
965
    
966
        for char in _mem.name:
967
            if str(char) == "\xFF":
968
                char = " "  # The OEM software may have 0xFF mid-name
969
            mem.name += str(char)
970
        mem.name = mem.name.rstrip()
971

    
972
        if not _mem.scan:
973
            mem.skip = "S"
974

    
975
        levels = self.POWER_LEVELS
976
        try:
977
            mem.power = levels[_mem.lowpower]
978
        except IndexError:
979
            LOG.error("Radio reported invalid power level %s (in %s)" %
980
                        (_mem.power, levels))
981
            mem.power = levels[0]
982

    
983
        mem.mode = _mem.wide and "NFM" or  "FM"
984

    
985
        dtcs_pol = ["N", "N"]
986

    
987
        if _mem.txtone in [0, 0xFFFF]:
988
            txmode = ""
989
        elif _mem.txtone >= 0x0258:
990
            txmode = "Tone"
991
            mem.rtone = int(_mem.txtone) / 10.0
992
        elif _mem.txtone <= 0x0258:
993
            txmode = "DTCS"
994
            if _mem.txtone > 0x69:
995
                index = _mem.txtone - 0x6A
996
                dtcs_pol[0] = "R"
997
            else:
998
                index = _mem.txtone - 1
999
            mem.dtcs = self.DTCS_CODES[index]
1000
        else:
1001
            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
1002

    
1003
        if _mem.rxtone in [0, 0xFFFF]:
1004
            rxmode = ""
1005
        elif _mem.rxtone >= 0x0258:
1006
            rxmode = "Tone"
1007
            mem.ctone = int(_mem.rxtone) / 10.0
1008
        elif _mem.rxtone <= 0x0258:
1009
            rxmode = "DTCS"
1010
            if _mem.rxtone >= 0x6A:
1011
                index = _mem.rxtone - 0x6A
1012
                dtcs_pol[1] = "R"
1013
            else:
1014
                index = _mem.rxtone - 1
1015
            mem.rx_dtcs = self.DTCS_CODES[index]
1016
        else:
1017
            LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
1018

    
1019
        if txmode == "Tone" and not rxmode:
1020
            mem.tmode = "Tone"
1021
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
1022
            mem.tmode = "TSQL"
1023
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
1024
            mem.tmode = "DTCS"
1025
        elif rxmode or txmode:
1026
            mem.tmode = "Cross"
1027
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
1028

    
1029
        mem.dtcs_polarity = "".join(dtcs_pol)
1030

    
1031
        mem.extra = RadioSettingGroup("Extra", "extra")
1032

    
1033
        rs = RadioSetting("bcl", "BCL",
1034
                          RadioSettingValueBoolean(_mem.bcl))
1035
        mem.extra.append(rs)
1036

    
1037
        rs = RadioSetting("pttid", "PTT ID",
1038
                          RadioSettingValueList(self.PTTID_LIST,
1039
                                                self.PTTID_LIST[_mem.pttid]))
1040
        mem.extra.append(rs)
1041

    
1042
        rs = RadioSetting("scode", "S-CODE",
1043
                          RadioSettingValueList(self.SCODE_LIST,
1044
                                                self.SCODE_LIST[_mem.scode]))
1045
        mem.extra.append(rs)
1046

    
1047
        return mem
1048

    
1049
    def set_memory(self, mem):
1050
        _mem = self._memobj.memory[mem.number]
1051

    
1052
        _mem.set_raw("\x00"*16 + "\xff" * 16)
1053

    
1054
        if mem.empty:
1055
            _mem.set_raw("\xff" * 32)
1056
            return
1057

    
1058
        _mem.rxfreq = mem.freq / 10
1059
        
1060
        if mem.duplex == "off":
1061
            for i in range(0, 4):
1062
                _mem.txfreq[i].set_raw("\xFF")
1063
        elif mem.duplex == "split":
1064
            _mem.txfreq = mem.offset / 10
1065
        elif mem.duplex == "+":
1066
            _mem.txfreq = (mem.freq + mem.offset) / 10
1067
        elif mem.duplex == "-":
1068
            _mem.txfreq = (mem.freq - mem.offset) / 10
1069
        else:
1070
            _mem.txfreq = mem.freq / 10
1071

    
1072
        _namelength = self.get_features().valid_name_length
1073
        for i in range(_namelength):
1074
            try:
1075
                _mem.name[i] = mem.name[i]
1076
            except IndexError:
1077
                _mem.name[i] = "\xFF"
1078

    
1079
        rxmode = txmode = ""
1080
        if mem.tmode == "Tone":
1081
            _mem.txtone = int(mem.rtone * 10)
1082
            _mem.rxtone = 0
1083
        elif mem.tmode == "TSQL":
1084
            _mem.txtone = int(mem.ctone * 10)
1085
            _mem.rxtone = int(mem.ctone * 10)
1086
        elif mem.tmode == "DTCS":
1087
            rxmode = txmode = "DTCS"
1088
            _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
1089
            _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1
1090
        elif mem.tmode == "Cross":
1091
            txmode, rxmode = mem.cross_mode.split("->", 1)
1092
            if txmode == "Tone":
1093
                _mem.txtone = int(mem.rtone * 10)
1094
            elif txmode == "DTCS":
1095
                _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1
1096
            else:
1097
                _mem.txtone = 0
1098
            if rxmode == "Tone":
1099
                _mem.rxtone = int(mem.ctone * 10)
1100
            elif rxmode == "DTCS":
1101
                _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1
1102
            else:
1103
                _mem.rxtone = 0
1104
        else:
1105
            _mem.rxtone = 0
1106
            _mem.txtone = 0
1107

    
1108
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
1109
            _mem.txtone += 0x69
1110
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
1111
            _mem.rxtone += 0x69
1112

    
1113
        _mem.scan = mem.skip != "S"
1114
        _mem.wide = mem.mode == "NFM"
1115

    
1116
        if mem.power:
1117
            _mem.lowpower = self.POWER_LEVELS.index(mem.power)
1118
        else:
1119
            _mem.lowpower = 0
1120

    
1121
        # extra settings
1122
        if len(mem.extra) > 0:
1123
            # there are setting, parse
1124
            for setting in mem.extra:
1125
                if setting.get_name() == "scode":
1126
                    setattr(_mem, setting.get_name(), str(int(setting.value)))
1127
                else:
1128
                    setattr(_mem, setting.get_name(), setting.value)
1129
        else:
1130
            # there are no extra settings, load defaults
1131
            _mem.bcl = 0
1132
            _mem.pttid = 0
1133
            _mem.scode = 0
1134

    
1135
    def set_settings(self, settings):
1136
        _settings = self._memobj.settings
1137
        _mem = self._memobj
1138
        for element in settings:
1139
            if not isinstance(element, RadioSetting):
1140
                if element.get_name() == "fm_preset":
1141
                    self._set_fm_preset(element)
1142
                else:
1143
                    self.set_settings(element)
1144
                    continue
1145
            else:
1146
                try:
1147
                    name = element.get_name()
1148
                    if "." in name:
1149
                        bits = name.split(".")
1150
                        obj = self._memobj
1151
                        for bit in bits[:-1]:
1152
                            if "/" in bit:
1153
                                bit, index = bit.split("/", 1)
1154
                                index = int(index)
1155
                                obj = getattr(obj, bit)[index]
1156
                            else:
1157
                                obj = getattr(obj, bit)
1158
                        setting = bits[-1]
1159
                    else:
1160
                        obj = _settings
1161
                        setting = element.get_name()
1162

    
1163
                    if element.has_apply_callback():
1164
                        LOG.debug("Using apply callback")
1165
                        element.run_apply_callback()
1166
                    elif element.value.get_mutable():
1167
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1168
                        setattr(obj, setting, element.value)
1169
                except Exception:
1170
                    LOG.debug(element.get_name())
1171
                    raise
1172

    
1173
    def _set_fm_preset(self, settings):
1174
        for element in settings:
1175
            try:
1176
                val = element.value
1177
                if self._memobj.fm_presets <= 108.0 * 10 - 650:
1178
                    value = int(val.get_value() * 10 - 650)
1179
                else:
1180
                    value = int(val.get_value() * 10)
1181
                LOG.debug("Setting fm_presets = %s" % (value))
1182
                if self._bw_shift:
1183
                    value = ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)
1184
                self._memobj.fm_presets = value
1185
            except Exception:
1186
                LOG.debug(element.get_name())
1187
                raise
1188

    
1189
    @classmethod
1190
    def match_model(cls, filedata, filename):
1191
        match_size = False
1192
        match_model = False
1193

    
1194
        # testing the file data size
1195
        if len(filedata) in [cls.MEM_TOTAL + len(cls.MODEL)]:
1196
            match_size = True
1197

    
1198
        # testing the firmware model fingerprint
1199
        match_model = model_match(cls, filedata)
1200
        if match_size and match_model:
1201
            return True
1202
        else:
1203
            return False
1204

    
1205

    
1206
@directory.register
1207
class UV17ProGPS(UV17Pro):
1208
    VENDOR = "Baofeng"
1209
    MODEL = "UV-17ProGPS"
1210
    _support_banknames = True
1211
    _magic = MSTRING_UV17PROGPS
1212
    _magics = [b"\x46", b"\x4d", b"\x53\x45\x4E\x44\x21\x05\x0D\x01\x01\x01\x04\x11\x08\x05\x0D\x0D\x01\x11\x0F\x09\x12\x09\x10\x04\x00"]
1213
    _magicResponseLengths = [16, 7, 1]
1214
    _aniid = False
1215
    VALID_BANDS = [UV17Pro._airband, UV17Pro._vhf_range, UV17Pro._vhf2_range,
1216
                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
1217

    
1218
@directory.register
1219
class UV17L(UV17Pro):
1220
    VENDOR = "Baofeng"
1221
    MODEL = "UV-17L"
1222

    
1223

    
(9-9/17)