Project

General

Profile

Bug #10787 » retevis_rb15_full_band_2.py

Jim Unroe, 08/14/2023 08:07 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
     unknown2:1;
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, 520000000)]
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" * 8 + "\x00" * 5 + "\xFF" * 3)
541
            return
542

    
543
        _mem.rxfreq = mem.freq / 10
544

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

    
557
        _mem.scan = mem.skip != "S"
558
        _mem.isnarrow = mem.mode == "NFM"
559

    
560
        self._set_tone(mem, _mem)
561

    
562
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
563

    
564
        for setting in mem.extra:
565
            setattr(_mem, setting.get_name(), setting.value)
566

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

    
574
        voice = RadioSetting("voice", "Language", RadioSettingValueList(
575
                             LIST_VOICE, LIST_VOICE[_settings.voice]))
576
        basic.append(voice)
577

    
578
        beep = RadioSetting("beep", "Key Beep",
579
                            RadioSettingValueBoolean(_settings.beep))
580
        basic.append(beep)
581

    
582
        volume = RadioSetting("volume", "Volume Level",
583
                              RadioSettingValueInteger(
584
                                  0, 7, _settings.volume))
585
        basic.append(volume)
586

    
587
        save = RadioSetting("save", "Battery Save",
588
                            RadioSettingValueList(
589
                                LIST_SAVE, LIST_SAVE[_settings.save]))
590
        basic.append(save)
591

    
592
        backlight = RadioSetting("backlight", "Backlight",
593
                                 RadioSettingValueList(
594
                                     LIST_BACKLIGHT,
595
                                     LIST_BACKLIGHT[_settings.backlight]))
596
        basic.append(backlight)
597

    
598
        vibrate = RadioSetting("vibrate", "Vibrate",
599
                               RadioSettingValueBoolean(_settings.vibrate))
600
        basic.append(vibrate)
601

    
602
        autolock = RadioSetting("autolock", "Auto Lock",
603
                                RadioSettingValueBoolean(_settings.autolock))
604
        basic.append(autolock)
605

    
606
        calltone = RadioSetting("calltone", "Call Tone",
607
                                RadioSettingValueInteger(
608
                                    1, 10, _settings.calltone))
609
        basic.append(calltone)
610

    
611
        roger = RadioSetting("roger", "Roger Tone",
612
                             RadioSettingValueList(
613
                                 LIST_ROGER, LIST_ROGER[_settings.roger]))
614
        basic.append(roger)
615

    
616
        squelch = RadioSetting("squelch", "Squelch Level",
617
                               RadioSettingValueInteger(
618
                                   0, 10, _settings.squelch))
619
        basic.append(squelch)
620

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

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

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

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

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

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

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

    
687
        # VOX Settings
688
        vox = RadioSetting("vox", "VOX",
689
                           RadioSettingValueBoolean(_settings.vox))
690
        voxset.append(vox)
691

    
692
        voxl = RadioSetting("voxl", "VOX Level",
693
                            RadioSettingValueInteger(
694
                                0, 10, _settings.voxl))
695
        voxset.append(voxl)
696

    
697
        voxd = RadioSetting("voxd", "VOX Delay (seconde)",
698
                            RadioSettingValueList(
699
                                LIST_VOXD, LIST_VOXD[_settings.voxd]))
700
        voxset.append(voxd)
701

    
702
        return top
703

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

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

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

    
737

    
738
@directory.register
739
class RB15Radio(RB15RadioBase):
740
    """RETEVIS RB15"""
741
    VENDOR = "Retevis"
742
    MODEL = "RB15"
743

    
744
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
745
                    chirp_common.PowerLevel("Low", watts=0.50)]
746

    
747
    _ranges = [
748
               (0x0150, 0x07A0),
749
              ]
750
    _memsize = 0x07A0
751

    
752
    _upper = 99
753
    _frs = False  # sold as FRS radio but supports full band TX/RX
754

    
755

    
756
@directory.register
757
class RB615RadioBase(RB15RadioBase):
758
    """RETEVIS RB615"""
759
    VENDOR = "Retevis"
760
    MODEL = "RB615"
761

    
762
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
763
                    chirp_common.PowerLevel("Low", watts=0.50)]
764

    
765
    _ranges = [
766
               (0x0150, 0x07A0),
767
              ]
768
    _memsize = 0x07A0
769

    
770
    _upper = 99
771
    _pmr = False  # sold as PMR radio but supports full band TX/RX
(9-9/27)