Project

General

Profile

Bug #10787 » retevis_rb15_full_band_6.py

Jim Unroe, 08/15/2023 12:38 PM

 
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
        _mem.unknown1 = False
545

    
546
        _mem.rxfreq = mem.freq / 10
547

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

    
560
        _mem.scan = mem.skip != "S"
561
        _mem.isnarrow = mem.mode == "NFM"
562

    
563
        self._set_tone(mem, _mem)
564

    
565
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
566

    
567
        for setting in mem.extra:
568
            setattr(_mem, setting.get_name(), setting.value)
569

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

    
577
        voice = RadioSetting("voice", "Language", RadioSettingValueList(
578
                             LIST_VOICE, LIST_VOICE[_settings.voice]))
579
        basic.append(voice)
580

    
581
        beep = RadioSetting("beep", "Key Beep",
582
                            RadioSettingValueBoolean(_settings.beep))
583
        basic.append(beep)
584

    
585
        volume = RadioSetting("volume", "Volume Level",
586
                              RadioSettingValueInteger(
587
                                  0, 7, _settings.volume))
588
        basic.append(volume)
589

    
590
        save = RadioSetting("save", "Battery Save",
591
                            RadioSettingValueList(
592
                                LIST_SAVE, LIST_SAVE[_settings.save]))
593
        basic.append(save)
594

    
595
        backlight = RadioSetting("backlight", "Backlight",
596
                                 RadioSettingValueList(
597
                                     LIST_BACKLIGHT,
598
                                     LIST_BACKLIGHT[_settings.backlight]))
599
        basic.append(backlight)
600

    
601
        vibrate = RadioSetting("vibrate", "Vibrate",
602
                               RadioSettingValueBoolean(_settings.vibrate))
603
        basic.append(vibrate)
604

    
605
        autolock = RadioSetting("autolock", "Auto Lock",
606
                                RadioSettingValueBoolean(_settings.autolock))
607
        basic.append(autolock)
608

    
609
        calltone = RadioSetting("calltone", "Call Tone",
610
                                RadioSettingValueInteger(
611
                                    1, 10, _settings.calltone))
612
        basic.append(calltone)
613

    
614
        roger = RadioSetting("roger", "Roger Tone",
615
                             RadioSettingValueList(
616
                                 LIST_ROGER, LIST_ROGER[_settings.roger]))
617
        basic.append(roger)
618

    
619
        squelch = RadioSetting("squelch", "Squelch Level",
620
                               RadioSettingValueInteger(
621
                                   0, 10, _settings.squelch))
622
        basic.append(squelch)
623

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

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

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

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

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

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

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

    
690
        # VOX Settings
691
        vox = RadioSetting("vox", "VOX",
692
                           RadioSettingValueBoolean(_settings.vox))
693
        voxset.append(vox)
694

    
695
        voxl = RadioSetting("voxl", "VOX Level",
696
                            RadioSettingValueInteger(
697
                                0, 10, _settings.voxl))
698
        voxset.append(voxl)
699

    
700
        voxd = RadioSetting("voxd", "VOX Delay (seconde)",
701
                            RadioSettingValueList(
702
                                LIST_VOXD, LIST_VOXD[_settings.voxd]))
703
        voxset.append(voxd)
704

    
705
        return top
706

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

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

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

    
740

    
741
@directory.register
742
class RB15Radio(RB15RadioBase):
743
    """RETEVIS RB15"""
744
    VENDOR = "Retevis"
745
    MODEL = "RB15"
746

    
747
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
748
                    chirp_common.PowerLevel("Low", watts=0.50)]
749

    
750
    _ranges = [
751
               (0x0150, 0x07A0),
752
              ]
753
    _memsize = 0x07A0
754

    
755
    _upper = 99
756
    _frs = False  # sold as FRS radio but supports full band TX/RX
757

    
758

    
759
@directory.register
760
class RB615RadioBase(RB15RadioBase):
761
    """RETEVIS RB615"""
762
    VENDOR = "Retevis"
763
    MODEL = "RB615"
764

    
765
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
766
                    chirp_common.PowerLevel("Low", watts=0.50)]
767

    
768
    _ranges = [
769
               (0x0150, 0x07A0),
770
              ]
771
    _memsize = 0x07A0
772

    
773
    _upper = 99
774
    _pmr = False  # sold as PMR radio but supports full band TX/RX
(23-23/27)