Project

General

Profile

Bug #10787 » retevis_rb15_full_band_4.py

Jim Unroe, 08/15/2023 11:33 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.rxfreq = mem.freq / 10
546

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

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

    
562
        self._set_tone(mem, _mem)
563

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
704
        return top
705

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

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

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

    
739

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

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

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

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

    
757

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

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

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

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