Project

General

Profile

Bug #10787 » retevis_rb15_full_band_5.py

M B, 08/15/2023 11:52 AM

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

    
16
import struct
17
import logging
18

    
19
from chirp import chirp_common, directory, memmap
20
from chirp import bitwise, errors, util
21
from chirp import bandplan_na
22
from chirp.settings import RadioSetting, RadioSettingGroup, \
23
    RadioSettingValueInteger, RadioSettingValueList, \
24
    RadioSettingValueBoolean, RadioSettings
25

    
26
LOG = logging.getLogger(__name__)
27

    
28
MEM_FORMAT = """
29
struct memory {
30
  u32 rxfreq;                                // 00-03
31
  u16 decQT;                                 // 04-05
32
  u32 txfreq;                                // 06-09
33
  u16 encQT;                                 // 0a-0b
34
  u8 lowpower:1,  // Power Level             // 0c
35
     unknown1:1,
36
     isnarrow:1,  // Bandwidth
37
     bcl:2,       // Busy Channel Lockout
38
     scan:1,      // Scan Add
39
     encode:1,    // Encode
40
     isunused:1;  // Is Unused
41
  u8 unknown3[3];                            // 0d-0f
42
};
43

    
44
#seekto 0x0170;
45
struct memory channels[99];
46

    
47
#seekto 0x0162;
48
struct {
49
  u8 unknown_1:1,           // 0x0162
50
     voice:2,               //               Voice Prompt
51
     beep:1,                //               Beep Switch
52
     unknown_2:1,
53
     vox:1,                 //               VOX
54
     autolock:1,            //               Auto Lock
55
     vibrate:1;             //               Vibrate Switch
56
  u8 squelch:4,             // 0x0163        SQ Level
57
     unknown_3:1,
58
     volume:3;              //               Volume Level
59
  u8 voxl:4,                // 0x0164        VOX Level
60
     voxd:4;                //               VOX Delay
61
  u8 unknown_5:1,           // 0x0165
62
     save:3,                //               Power Save
63
     calltone:4;            //               Call Tone
64
  u8 unknown_6:4,           // 0x0166
65
     roger:2,               //               Roger Tone
66
     backlight:2;           //               Backlight Set
67
  u16 tot;                  // 0x0167-0x0168 Time-out Timer
68
  u8 unknown_7[3];          // 0x0169-0x016B
69
  u8 skeyul;                // 0x016C        Side Key Up Long
70
  u8 skeyus;                // 0x016D        Side Key Up Short
71
  u8 skeydl;                // 0x016E        Side Key Down Long
72
  u8 skeyds;                // 0x016F        Side Key Down Short
73
} settings;
74
"""
75

    
76
CMD_ACK = b"\x06"
77

    
78
RB15_DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
79

    
80
LIST_BACKLIGHT = ["Off", "On", "Auto"]
81
LIST_BCL = ["None", "Carrier", "QT/DQT Match"]
82
LIST_ROGER = ["Off", "Start", "End", "Start and End"]
83
LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4", "1:5"]
84
_STEP_LIST = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
85
LIST_VOICE = ["Off", "Chinese", "English"]
86
LIST_VOXD = ["0.0", "0.5", "1.0", "1.5", "2.0", "2.5", "3.0", "3.5", "4.0",
87
             "4.5", "5.0S"]
88

    
89
SKEY_CHOICES = ["None", "Scan", "Monitor", "VOX On/Off",
90
                "Local Alarm", "Remote Alarm", "Backlight On/Off", "Call Tone"]
91
SKEY_VALUES = [0x00, 0x01, 0x03, 0x04, 0x09, 0x0A, 0x13, 0x14]
92

    
93
TOT_CHOICES = ["Off", "15", "30", "45", "60", "75", "90", "105", "120",
94
               "135", "150", "165", "180", "195", "210", "225", "240",
95
               "255", "270", "285", "300", "315", "330", "345", "360",
96
               "375", "390", "405", "420", "435", "450", "465", "480",
97
               "495", "510", "525", "540", "555", "570", "585", "600"
98
               ]
99
TOT_VALUES = [0x00, 0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78,
100
              0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0,
101
              0xFF, 0x10E, 0x11D, 0x12C, 0x13B, 0x14A, 0x159, 0x168,
102
              0x177, 0x186, 0x195, 0x1A4, 0x1B3, 0x1C2, 0x1D1, 0x1E0,
103
              0x1EF, 0x1FE, 0x20D, 0x21C, 0x22B, 0x23A, 0x249, 0x258
104
              ]
105

    
106

    
107
def _checksum(data):
108
    cs = 0
109
    for byte in data:
110
        cs += byte
111
    return cs % 256
112

    
113

    
114
def tone2short(t):
115
    """Convert a string tone or DCS to an encoded u16
116
    """
117
    tone = str(t)
118
    if tone == "----":
119
        u16tone = 0x0000
120
    elif tone[0] == 'D':  # This is a DCS code
121
        c = tone[1: -1]
122
        code = int(c, 8)
123
        if tone[-1] == 'I':
124
            code |= 0x4000
125
        u16tone = code | 0x8000
126
    else:  # This is an analog CTCSS
127
        u16tone = int(tone[0:-2]+tone[-1]) & 0xffff  # strip the '.'
128
    return u16tone
129

    
130

    
131
def short2tone(tone):
132
    """ Map a binary CTCSS/DCS to a string name for the tone
133
    """
134
    if tone == 0 or tone == 0xffff:
135
        ret = "----"
136
    else:
137
        code = tone & 0x3fff
138
        if tone & 0x4000:      # This is a DCS
139
            ret = "D%0.3oN" % code
140
        elif tone & 0x8000:  # This is an inverse code
141
            ret = "D%0.3oI" % code
142
        else:   # Just plain old analog CTCSS
143
            ret = "%4.1f" % (code / 10.0)
144
    return ret
145

    
146

    
147
def _rb15_enter_programming_mode(radio):
148
    serial = radio.pipe
149

    
150
    # lengthen the timeout here as these radios are resetting due to timeout
151
    radio.pipe.timeout = 0.75
152

    
153
    exito = False
154
    for i in range(0, 5):
155
        serial.write(radio.magic)
156
        ack = serial.read(1)
157

    
158
        try:
159
            if ack == CMD_ACK:
160
                exito = True
161
                break
162
        except:
163
            LOG.debug("Attempt #%s, failed, trying again" % i)
164
            pass
165

    
166
    # return timeout to default value
167
    radio.pipe.timeout = 0.25
168

    
169
    # check if we had EXITO
170
    if exito is False:
171
        msg = "The radio did not accept program mode after five tries.\n"
172
        msg += "Check you interface cable and power cycle your radio."
173
        raise errors.RadioError(msg)
174

    
175

    
176
def _rb15_exit_programming_mode(radio):
177
    serial = radio.pipe
178
    try:
179
        serial.write(b"21" + b"\x05\xEE" + b"V")
180
    except:
181
        raise errors.RadioError("Radio refused to exit programming mode")
182

    
183

    
184
def _rb15_read_block(radio, block_addr, block_size):
185
    serial = radio.pipe
186

    
187
    cmd = struct.pack(">BH", ord(b'R'), block_addr)
188

    
189
    ccs = bytes([_checksum(cmd)])
190

    
191
    expectedresponse = b"R" + cmd[1:]
192

    
193
    cmd = cmd + ccs
194

    
195
    LOG.debug("Reading block %04x..." % (block_addr))
196

    
197
    try:
198
        serial.write(cmd)
199
        response = serial.read(3 + block_size + 1)
200

    
201
        cs = bytes([_checksum(response[:-1])])
202

    
203
        if response[:3] != expectedresponse:
204
            raise Exception("Error reading block %04x." % (block_addr))
205

    
206
        chunk = response[3:]
207

    
208
        if chunk[-1:] != cs:
209
            raise Exception("Block failed checksum!")
210

    
211
        block_data = chunk[:-1]
212
    except:
213
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
214

    
215
    return block_data
216

    
217

    
218
def _rb15_write_block(radio, block_addr, block_size):
219
    serial = radio.pipe
220

    
221
    cmd = struct.pack(">BH", ord(b'W'), block_addr)
222
    data = radio.get_mmap()[block_addr:block_addr + block_size]
223

    
224
    cs = bytes([_checksum(cmd + data)])
225
    data += cs
226

    
227
    LOG.debug("Writing Data:")
228
    LOG.debug(util.hexprint(cmd + data))
229

    
230
    try:
231
        serial.write(cmd + data)
232
        if serial.read(1) != CMD_ACK:
233
            raise Exception("No ACK")
234
    except:
235
        raise errors.RadioError("Failed to send block "
236
                                "to radio at %04x" % block_addr)
237

    
238

    
239
def do_download(radio):
240
    LOG.debug("download")
241
    _rb15_enter_programming_mode(radio)
242

    
243
    data = b""
244

    
245
    status = chirp_common.Status()
246
    status.msg = "Cloning from radio"
247

    
248
    status.cur = 0
249
    status.max = radio._memsize
250

    
251
    for addr in range(0x0000, radio._memsize, radio.BLOCK_SIZE):
252
        status.cur = addr + radio.BLOCK_SIZE
253
        radio.status_fn(status)
254

    
255
        block = _rb15_read_block(radio, addr, radio.BLOCK_SIZE)
256
        data += block
257

    
258
        LOG.debug("Address: %04x" % addr)
259
        LOG.debug(util.hexprint(block))
260

    
261
    _rb15_exit_programming_mode(radio)
262

    
263
    return memmap.MemoryMapBytes(data)
264

    
265

    
266
def do_upload(radio):
267
    status = chirp_common.Status()
268
    status.msg = "Uploading to radio"
269

    
270
    _rb15_enter_programming_mode(radio)
271

    
272
    status.cur = 0
273
    status.max = radio._memsize
274

    
275
    for start_addr, end_addr in radio._ranges:
276
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
277
            status.cur = addr + radio.BLOCK_SIZE
278
            radio.status_fn(status)
279
            _rb15_write_block(radio, addr, radio.BLOCK_SIZE)
280

    
281
    _rb15_exit_programming_mode(radio)
282

    
283

    
284
def _split(rf, f1, f2):
285
    """Returns False if the two freqs are in the same band (no split)
286
    or True otherwise"""
287

    
288
    # determine if the two freqs are in the same band
289
    for low, high in rf.valid_bands:
290
        if f1 >= low and f1 <= high and \
291
                f2 >= low and f2 <= high:
292
            # if the two freqs are on the same Band this is not a split
293
            return False
294

    
295
    # if you get here is because the freq pairs are split
296
    return True
297

    
298

    
299
class RB15RadioBase(chirp_common.CloneModeRadio):
300
    """RETEVIS RB15 BASE"""
301
    VENDOR = "Retevis"
302
    BAUD_RATE = 9600
303
    NEEDS_COMPAT_SERIAL = False
304

    
305
    BLOCK_SIZE = 0x10
306
    magic = b"21" + b"\x05\x10" + b"x"
307

    
308
    VALID_BANDS = [(400000000, 480000000)]
309

    
310
    _ranges = [
311
               (0x0150, 0x07A0),
312
              ]
313
    _memsize = 0x07A0
314

    
315
    _frs = _pmr = False
316

    
317
    def get_features(self):
318
        rf = chirp_common.RadioFeatures()
319
        rf.has_settings = True
320
        rf.has_bank = False
321
        rf.has_ctone = True
322
        rf.has_cross = True
323
        rf.has_rx_dtcs = True
324
        rf.has_tuning_step = False
325
        rf.can_odd_split = True
326
        rf.has_name = False
327
        rf.valid_skips = ["", "S"]
328
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
329
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
330
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
331
        rf.valid_power_levels = self.POWER_LEVELS
332
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
333
        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 kHz.
334
        rf.valid_dtcs_codes = RB15_DTCS
335
        rf.memory_bounds = (1, self._upper)
336
        rf.valid_tuning_steps = _STEP_LIST
337
        rf.valid_bands = self.VALID_BANDS
338

    
339
        return rf
340

    
341
    def process_mmap(self):
342
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
343

    
344
    def sync_in(self):
345
        """Download from radio"""
346
        try:
347
            data = do_download(self)
348
        except errors.RadioError:
349
            # Pass through any real errors we raise
350
            raise
351
        except:
352
            # If anything unexpected happens, make sure we raise
353
            # a RadioError and log the problem
354
            LOG.exception('Unexpected error during download')
355
            raise errors.RadioError('Unexpected error communicating '
356
                                    'with the radio')
357
        self._mmap = data
358
        self.process_mmap()
359

    
360
    def sync_out(self):
361
        """Upload to radio"""
362
        try:
363
            do_upload(self)
364
        except:
365
            # If anything unexpected happens, make sure we raise
366
            # a RadioError and log the problem
367
            LOG.exception('Unexpected error during upload')
368
            raise errors.RadioError('Unexpected error communicating '
369
                                    'with the radio')
370

    
371
    def get_raw_memory(self, number):
372
        return repr(self._memobj.memory[number - 1])
373

    
374
    def _get_tone(self, _mem, mem):
375
        """Decode both the encode and decode CTSS/DCS codes from
376
        the memory channel and stuff them into the UI
377
        memory channel row.
378
        """
379
        txtone = short2tone(_mem.encQT)
380
        rxtone = short2tone(_mem.decQT)
381
        pt = "N"
382
        pr = "N"
383

    
384
        if txtone == "----":
385
            txmode = ""
386
        elif txtone[0] == "D":
387
            mem.dtcs = int(txtone[1:4])
388
            if txtone[4] == "I":
389
                pt = "R"
390
            txmode = "DTCS"
391
        else:
392
            mem.rtone = float(txtone)
393
            txmode = "Tone"
394

    
395
        if rxtone == "----":
396
            rxmode = ""
397
        elif rxtone[0] == "D":
398
            mem.rx_dtcs = int(rxtone[1:4])
399
            if rxtone[4] == "I":
400
                pr = "R"
401
            rxmode = "DTCS"
402
        else:
403
            mem.ctone = float(rxtone)
404
            rxmode = "Tone"
405

    
406
        if txmode == "Tone" and len(rxmode) == 0:
407
            mem.tmode = "Tone"
408
        elif (txmode == rxmode and txmode == "Tone" and
409
              mem.rtone == mem.ctone):
410
            mem.tmode = "TSQL"
411
        elif (txmode == rxmode and txmode == "DTCS" and
412
              mem.dtcs == mem.rx_dtcs):
413
            mem.tmode = "DTCS"
414
        elif (len(rxmode) + len(txmode)) > 0:
415
            mem.tmode = "Cross"
416
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
417

    
418
        mem.dtcs_polarity = pt + pr
419

    
420
        LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" %
421
                  (txmode, _mem.encQT, rxmode, _mem.decQT))
422

    
423
    def _set_tone(self, mem, _mem):
424
        """Update the memory channel block CTCC/DCS tones
425
        from the UI fields
426
        """
427
        def _set_dcs(code, pol):
428
            val = int("%i" % code, 8) | 0x4000
429
            if pol == "R":
430
                val = int("%i" % code, 8) | 0x8000
431
            return val
432

    
433
        rx_mode = tx_mode = None
434
        rxtone = txtone = 0x0000
435

    
436
        if mem.tmode == "Tone":
437
            tx_mode = "Tone"
438
            txtone = int(mem.rtone * 10)
439
        elif mem.tmode == "TSQL":
440
            rx_mode = tx_mode = "Tone"
441
            rxtone = txtone = int(mem.ctone * 10)
442
        elif mem.tmode == "DTCS":
443
            tx_mode = rx_mode = "DTCS"
444
            txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
445
            rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1])
446
        elif mem.tmode == "Cross":
447
            tx_mode, rx_mode = mem.cross_mode.split("->")
448
            if tx_mode == "DTCS":
449
                txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0])
450
            elif tx_mode == "Tone":
451
                txtone = int(mem.rtone * 10)
452
            if rx_mode == "DTCS":
453
                rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1])
454
            elif rx_mode == "Tone":
455
                rxtone = int(mem.ctone * 10)
456

    
457
        _mem.decQT = rxtone
458
        _mem.encQT = txtone
459

    
460
        LOG.debug("Set TX %s (%i) RX %s (%i)" %
461
                  (tx_mode, _mem.encQT, rx_mode, _mem.decQT))
462

    
463
    def get_memory(self, number):
464
        mem = chirp_common.Memory()
465
        _mem = self._memobj.channels[number - 1]
466
        mem.number = number
467

    
468
        mem.freq = int(_mem.rxfreq) * 10
469

    
470
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
471
        if mem.freq == 0:
472
            mem.empty = True
473
            return mem
474

    
475
        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
476
            mem.freq = 0
477
            mem.empty = True
478
            return mem
479

    
480
        if _mem.get_raw() == ("\xFF" * 16):
481
            LOG.debug("Initializing empty memory")
482
            _mem.set_raw("\x00" * 16)
483

    
484
        # Freq and offset
485
        mem.freq = int(_mem.rxfreq) * 10
486
        # tx freq can be blank
487
        if _mem.get_raw()[4] == "\xFF":
488
            # TX freq not set
489
            mem.offset = 0
490
            mem.duplex = "off"
491
        else:
492
            # TX freq set
493
            offset = (int(_mem.txfreq) * 10) - mem.freq
494
            if offset != 0:
495
                if _split(self.get_features(), mem.freq, int(
496
                          _mem.txfreq) * 10):
497
                    mem.duplex = "split"
498
                    mem.offset = int(_mem.txfreq) * 10
499
                elif offset < 0:
500
                    mem.offset = abs(offset)
501
                    mem.duplex = "-"
502
                elif offset > 0:
503
                    mem.offset = offset
504
                    mem.duplex = "+"
505
            else:
506
                mem.offset = 0
507

    
508
        mem.mode = _mem.isnarrow and "NFM" or "FM"
509

    
510
        self._get_tone(_mem, mem)
511

    
512
        mem.power = self.POWER_LEVELS[_mem.lowpower]
513

    
514
        if not _mem.scan:
515
            mem.skip = "S"
516

    
517
        mem.extra = RadioSettingGroup("Extra", "extra")
518

    
519
        if _mem.bcl > 0x02:
520
            val = 0
521
        else:
522
            val = _mem.bcl
523
        rs = RadioSetting("bcl", "BCL",
524
                          RadioSettingValueList(
525
                              LIST_BCL, LIST_BCL[val]))
526
        mem.extra.append(rs)
527

    
528
        rs = RadioSetting("encode", "Encode",
529
                          RadioSettingValueBoolean(_mem.encode))
530
        mem.extra.append(rs)
531

    
532
        return mem
533

    
534
    def set_memory(self, mem):
535
        LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
536
        _mem = self._memobj.channels[mem.number - 1]
537

    
538
        # if empty memory
539
        if mem.empty:
540
            _mem.set_raw("\xFF" * 16)
541
            return
542

    
543
        _mem.isunused = False
544

    
545
        _mem.unknown1 = False
546

    
547
        _mem.encode = False
548

    
549
        _mem.rxfreq = mem.freq / 10
550

    
551
        if mem.duplex == "off":
552
            for i in range(0, 4):
553
                _mem.txfreq[i].set_raw("\xFF")
554
        elif mem.duplex == "split":
555
            _mem.txfreq = mem.offset / 10
556
        elif mem.duplex == "+":
557
            _mem.txfreq = (mem.freq + mem.offset) / 10
558
        elif mem.duplex == "-":
559
            _mem.txfreq = (mem.freq - mem.offset) / 10
560
        else:
561
            _mem.txfreq = mem.freq / 10
562

    
563
        _mem.scan = mem.skip != "S"
564
        _mem.isnarrow = mem.mode == "NFM"
565

    
566
        self._set_tone(mem, _mem)
567

    
568
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
569

    
570
        for setting in mem.extra:
571
            setattr(_mem, setting.get_name(), setting.value)
572

    
573
    def get_settings(self):
574
        _settings = self._memobj.settings
575
        basic = RadioSettingGroup("basic", "Basic Settings")
576
        sidekey = RadioSettingGroup("sidekey", "Side Key Settings")
577
        voxset = RadioSettingGroup("vox", "VOX Settings")
578
        top = RadioSettings(basic, sidekey, voxset)
579

    
580
        voice = RadioSetting("voice", "Language", RadioSettingValueList(
581
                             LIST_VOICE, LIST_VOICE[_settings.voice]))
582
        basic.append(voice)
583

    
584
        beep = RadioSetting("beep", "Key Beep",
585
                            RadioSettingValueBoolean(_settings.beep))
586
        basic.append(beep)
587

    
588
        volume = RadioSetting("volume", "Volume Level",
589
                              RadioSettingValueInteger(
590
                                  0, 7, _settings.volume))
591
        basic.append(volume)
592

    
593
        save = RadioSetting("save", "Battery Save",
594
                            RadioSettingValueList(
595
                                LIST_SAVE, LIST_SAVE[_settings.save]))
596
        basic.append(save)
597

    
598
        backlight = RadioSetting("backlight", "Backlight",
599
                                 RadioSettingValueList(
600
                                     LIST_BACKLIGHT,
601
                                     LIST_BACKLIGHT[_settings.backlight]))
602
        basic.append(backlight)
603

    
604
        vibrate = RadioSetting("vibrate", "Vibrate",
605
                               RadioSettingValueBoolean(_settings.vibrate))
606
        basic.append(vibrate)
607

    
608
        autolock = RadioSetting("autolock", "Auto Lock",
609
                                RadioSettingValueBoolean(_settings.autolock))
610
        basic.append(autolock)
611

    
612
        calltone = RadioSetting("calltone", "Call Tone",
613
                                RadioSettingValueInteger(
614
                                    1, 10, _settings.calltone))
615
        basic.append(calltone)
616

    
617
        roger = RadioSetting("roger", "Roger Tone",
618
                             RadioSettingValueList(
619
                                 LIST_ROGER, LIST_ROGER[_settings.roger]))
620
        basic.append(roger)
621

    
622
        squelch = RadioSetting("squelch", "Squelch Level",
623
                               RadioSettingValueInteger(
624
                                   0, 10, _settings.squelch))
625
        basic.append(squelch)
626

    
627
        def apply_tot_listvalue(setting, obj):
628
            LOG.debug("Setting value: " + str(
629
                      setting.value) + " from list")
630
            val = str(setting.value)
631
            index = TOT_CHOICES.index(val)
632
            val = TOT_VALUES[index]
633
            obj.set_value(val)
634

    
635
        if _settings.tot in TOT_VALUES:
636
            idx = TOT_VALUES.index(_settings.tot)
637
        else:
638
            idx = TOT_VALUES.index(0x78)
639
        rs = RadioSettingValueList(TOT_CHOICES, TOT_CHOICES[idx])
640
        rset = RadioSetting("tot", "Time-out Timer", rs)
641
        rset.set_apply_callback(apply_tot_listvalue, _settings.tot)
642
        basic.append(rset)
643

    
644
        # Side Key Settings
645
        def apply_skey_listvalue(setting, obj):
646
            LOG.debug("Setting value: " + str(
647
                      setting.value) + " from list")
648
            val = str(setting.value)
649
            index = SKEY_CHOICES.index(val)
650
            val = SKEY_VALUES[index]
651
            obj.set_value(val)
652

    
653
        # Side Key (Upper) - Short Press
654
        if _settings.skeyus in SKEY_VALUES:
655
            idx = SKEY_VALUES.index(_settings.skeyus)
656
        else:
657
            idx = SKEY_VALUES.index(0x01)
658
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
659
        rset = RadioSetting("skeyus", "Side Key(upper) - Short Press", rs)
660
        rset.set_apply_callback(apply_skey_listvalue, _settings.skeyus)
661
        sidekey.append(rset)
662

    
663
        # Side Key (Upper) - Long Press
664
        if _settings.skeyul in SKEY_VALUES:
665
            idx = SKEY_VALUES.index(_settings.skeyul)
666
        else:
667
            idx = SKEY_VALUES.index(0x04)
668
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
669
        rset = RadioSetting("skeyul", "Side Key(upper) - Long Press", rs)
670
        rset.set_apply_callback(apply_skey_listvalue, _settings.skeyul)
671
        sidekey.append(rset)
672

    
673
        # Side Key (Lower) - Short Press
674
        if _settings.skeyds in SKEY_VALUES:
675
            idx = SKEY_VALUES.index(_settings.skeyds)
676
        else:
677
            idx = SKEY_VALUES.index(0x03)
678
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
679
        rset = RadioSetting("skeyds", "Side Key(lower) - Short Press", rs)
680
        rset.set_apply_callback(apply_skey_listvalue, _settings.skeyds)
681
        sidekey.append(rset)
682

    
683
        # Side Key (Lower) - Long Press
684
        if _settings.skeyul in SKEY_VALUES:
685
            idx = SKEY_VALUES.index(_settings.skeydl)
686
        else:
687
            idx = SKEY_VALUES.index(0x14)
688
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
689
        rset = RadioSetting("skeydl", "Side Key(lower) - Long Press", rs)
690
        rset.set_apply_callback(apply_skey_listvalue, _settings.skeydl)
691
        sidekey.append(rset)
692

    
693
        # VOX Settings
694
        vox = RadioSetting("vox", "VOX",
695
                           RadioSettingValueBoolean(_settings.vox))
696
        voxset.append(vox)
697

    
698
        voxl = RadioSetting("voxl", "VOX Level",
699
                            RadioSettingValueInteger(
700
                                0, 10, _settings.voxl))
701
        voxset.append(voxl)
702

    
703
        voxd = RadioSetting("voxd", "VOX Delay (seconde)",
704
                            RadioSettingValueList(
705
                                LIST_VOXD, LIST_VOXD[_settings.voxd]))
706
        voxset.append(voxd)
707

    
708
        return top
709

    
710
    def set_settings(self, settings):
711
        for element in settings:
712
            if not isinstance(element, RadioSetting):
713
                self.set_settings(element)
714
                continue
715
            else:
716
                try:
717
                    if "." in element.get_name():
718
                        bits = element.get_name().split(".")
719
                        obj = self._memobj
720
                        for bit in bits[:-1]:
721
                            obj = getattr(obj, bit)
722
                        setting = bits[-1]
723
                    else:
724
                        obj = self._memobj.settings
725
                        setting = element.get_name()
726

    
727
                    if element.has_apply_callback():
728
                        LOG.debug("Using apply callback")
729
                        element.run_apply_callback()
730
                    elif element.value.get_mutable():
731
                        LOG.debug("Setting %s = %s" % (setting, element.value))
732
                        setattr(obj, setting, element.value)
733
                except Exception:
734
                    LOG.debug(element.get_name())
735
                    raise
736

    
737
    @classmethod
738
    def match_model(cls, filedata, filename):
739
        # This radio has always been post-metadata, so never do
740
        # old-school detection
741
        return False
742

    
743

    
744
@directory.register
745
class RB15Radio(RB15RadioBase):
746
    """RETEVIS RB15"""
747
    VENDOR = "Retevis"
748
    MODEL = "RB15"
749

    
750
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
751
                    chirp_common.PowerLevel("Low", watts=0.50)]
752

    
753
    _ranges = [
754
               (0x0150, 0x07A0),
755
              ]
756
    _memsize = 0x07A0
757

    
758
    _upper = 99
759
    _frs = False  # sold as FRS radio but supports full band TX/RX
760

    
761

    
762
@directory.register
763
class RB615RadioBase(RB15RadioBase):
764
    """RETEVIS RB615"""
765
    VENDOR = "Retevis"
766
    MODEL = "RB615"
767

    
768
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
769
                    chirp_common.PowerLevel("Low", watts=0.50)]
770

    
771
    _ranges = [
772
               (0x0150, 0x07A0),
773
              ]
774
    _memsize = 0x07A0
775

    
776
    _upper = 99
777
    _pmr = False  # sold as PMR radio but supports full band TX/RX
(19-19/27)