Project

General

Profile

Bug #10787 » retevis_rb15_full_band.py

Jim Unroe, 08/14/2023 04:48 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
        rs = RadioSetting("bcl", "BCL",
520
                          RadioSettingValueList(
521
                              LIST_BCL, LIST_BCL[_mem.bcl]))
522
        mem.extra.append(rs)
523

    
524
        rs = RadioSetting("encode", "Encode",
525
                          RadioSettingValueBoolean(_mem.encode))
526
        mem.extra.append(rs)
527

    
528
        return mem
529

    
530
    def set_memory(self, mem):
531
        LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
532
        _mem = self._memobj.channels[mem.number - 1]
533

    
534
        # if empty memory
535
        if mem.empty:
536
            _mem.set_raw("\xFF" * 8 + "\x00" * 5 + "\xFF" * 3)
537
            return
538

    
539
        _mem.rxfreq = mem.freq / 10
540

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

    
553
        _mem.scan = mem.skip != "S"
554
        _mem.isnarrow = mem.mode == "NFM"
555

    
556
        self._set_tone(mem, _mem)
557

    
558
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
559

    
560
        for setting in mem.extra:
561
            setattr(_mem, setting.get_name(), setting.value)
562

    
563
    def get_settings(self):
564
        _settings = self._memobj.settings
565
        basic = RadioSettingGroup("basic", "Basic Settings")
566
        sidekey = RadioSettingGroup("sidekey", "Side Key Settings")
567
        voxset = RadioSettingGroup("vox", "VOX Settings")
568
        top = RadioSettings(basic, sidekey, voxset)
569

    
570
        voice = RadioSetting("voice", "Language", RadioSettingValueList(
571
                             LIST_VOICE, LIST_VOICE[_settings.voice]))
572
        basic.append(voice)
573

    
574
        beep = RadioSetting("beep", "Key Beep",
575
                            RadioSettingValueBoolean(_settings.beep))
576
        basic.append(beep)
577

    
578
        volume = RadioSetting("volume", "Volume Level",
579
                              RadioSettingValueInteger(
580
                                  0, 7, _settings.volume))
581
        basic.append(volume)
582

    
583
        save = RadioSetting("save", "Battery Save",
584
                            RadioSettingValueList(
585
                                LIST_SAVE, LIST_SAVE[_settings.save]))
586
        basic.append(save)
587

    
588
        backlight = RadioSetting("backlight", "Backlight",
589
                                 RadioSettingValueList(
590
                                     LIST_BACKLIGHT,
591
                                     LIST_BACKLIGHT[_settings.backlight]))
592
        basic.append(backlight)
593

    
594
        vibrate = RadioSetting("vibrate", "Vibrate",
595
                               RadioSettingValueBoolean(_settings.vibrate))
596
        basic.append(vibrate)
597

    
598
        autolock = RadioSetting("autolock", "Auto Lock",
599
                                RadioSettingValueBoolean(_settings.autolock))
600
        basic.append(autolock)
601

    
602
        calltone = RadioSetting("calltone", "Call Tone",
603
                                RadioSettingValueInteger(
604
                                    1, 10, _settings.calltone))
605
        basic.append(calltone)
606

    
607
        roger = RadioSetting("roger", "Roger Tone",
608
                             RadioSettingValueList(
609
                                 LIST_ROGER, LIST_ROGER[_settings.roger]))
610
        basic.append(roger)
611

    
612
        squelch = RadioSetting("squelch", "Squelch Level",
613
                               RadioSettingValueInteger(
614
                                   0, 10, _settings.squelch))
615
        basic.append(squelch)
616

    
617
        def apply_tot_listvalue(setting, obj):
618
            LOG.debug("Setting value: " + str(
619
                      setting.value) + " from list")
620
            val = str(setting.value)
621
            index = TOT_CHOICES.index(val)
622
            val = TOT_VALUES[index]
623
            obj.set_value(val)
624

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

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

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

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

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

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

    
683
        # VOX Settings
684
        vox = RadioSetting("vox", "VOX",
685
                           RadioSettingValueBoolean(_settings.vox))
686
        voxset.append(vox)
687

    
688
        voxl = RadioSetting("voxl", "VOX Level",
689
                            RadioSettingValueInteger(
690
                                0, 10, _settings.voxl))
691
        voxset.append(voxl)
692

    
693
        voxd = RadioSetting("voxd", "VOX Delay (seconde)",
694
                            RadioSettingValueList(
695
                                LIST_VOXD, LIST_VOXD[_settings.voxd]))
696
        voxset.append(voxd)
697

    
698
        return top
699

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

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

    
727
    @classmethod
728
    def match_model(cls, filedata, filename):
729
        # This radio has always been post-metadata, so never do
730
        # old-school detection
731
        return False
732

    
733

    
734
@directory.register
735
class RB15Radio(RB15RadioBase):
736
    """RETEVIS RB15"""
737
    VENDOR = "Retevis"
738
    MODEL = "RB15"
739

    
740
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
741
                    chirp_common.PowerLevel("Low", watts=0.50)]
742

    
743
    _ranges = [
744
               (0x0150, 0x07A0),
745
              ]
746
    _memsize = 0x07A0
747

    
748
    _upper = 99
749
    _frs = False  # sold as FRS radio but supports full band TX/RX
750

    
751

    
752
@directory.register
753
class RB615RadioBase(RB15RadioBase):
754
    """RETEVIS RB615"""
755
    VENDOR = "Retevis"
756
    MODEL = "RB615"
757

    
758
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
759
                    chirp_common.PowerLevel("Low", watts=0.50)]
760

    
761
    _ranges = [
762
               (0x0150, 0x07A0),
763
              ]
764
    _memsize = 0x07A0
765

    
766
    _upper = 99
767
    _pmr = False  # sold as PMR radio but supports full band TX/RX
(5-5/27)