Project

General

Profile

New Model #10871 ยป retevis_ra87_v0.1.py

1st draft - per-channel settings only - Jim Unroe, 01/11/2024 03:12 PM

 
1
# Copyright 2024 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.settings import RadioSetting, RadioSettingGroup, \
22
    RadioSettingValueList, \
23
    RadioSettingValueBoolean
24

    
25
LOG = logging.getLogger(__name__)
26

    
27
MEM_FORMAT = """
28
struct mem {
29
    bbcd rxfreq[5];       // RX Frequency                            // 0-4
30
    u8   step:4,          // STEP                                    // 5
31
         unk1:2,
32
         duplex:2;        // Duplex 0: Simplex, 1: Plus, 2: Minus
33
    u8   unk2:3,                                                     // 6
34
         reverse:1,       // Reverse
35
         unk3:4;
36

    
37
    ul16 rxdtcs_pol:1,                                               // 7-8
38
         unk4:1,
39
         is_rxdigtone:1,
40
         unk5:1,
41
         rxtone:12;
42
    ul16 txdtcs_pol:1,                                               // 9-A
43
         unk6:1,
44
         is_txdigtone:1,
45
         unk7:1,
46
         txtone:12;
47

    
48
    u8   unknown3;                                                   // B
49
    bbcd offset[2];       // Offset 00.05 - 69.95 MHz                // C-D
50
    u8   unknown4;                                                   // E
51
    u8   unk8:7,                                                     // F
52
         narrow:1;        // FM Narrow
53

    
54
    u8   unk9:3,          //                                         // 0
55
         beatshift:1,     // Beat Shift
56
         unk10:4;
57
    bbcd txfreq[4];                                                  // 1-4
58
    u8   unk11:4,                                                     // 5
59
         txstep:4;        // TX STEP
60
    u8   unk12:1,                                                    // 6
61
         txpower:3,       // Power
62
         unk13:4;
63
    u8   unknown5;                                                   // 7
64
    u8   compand:1,       // Compand                                 // 8
65
         scramble:3,      // Scramble
66
         unk14:4;
67
    char name[6];         // Name                                    // 9-E
68
    u8   hide:1,          // Channel Hide 0: Show, 1: Hide           // F
69
         unk15:6,
70
         skip:1;          // Lockout
71
};
72

    
73
// #seekto 0x0000;
74
struct mem left_memory[100];
75

    
76
#seekto 0x0D40;
77
struct mem right_memory[100];
78

    
79
#seekto 0x1CC0;
80
struct {
81
    u8 unknown_1cc0[7];   // 0x1CC0-0x1CC6
82
    u8 unk1:4,            // 0x1CC7
83
       sql:4;             //        Squelch
84
    u8 unknown_1cc8[35];  // 0x1CC8-0x1CFA
85
    u8 unk2:4,            // 0x1CEB
86
       wfclr:4;           //        Background Color - Wait
87
    u8 unk3:4,            // 0x1CEC
88
       rxclr:4;           //        Background Color - RX
89
    u8 unk4:4,            // 0x1CED
90
       txclr:4;           //        Background Color - TX
91
} settings;
92
"""
93

    
94
CMD_ACK = b"\x06"
95

    
96
TXPOWER_LOW = 0x00
97
TXPOWER_LOW2 = 0x01
98
TXPOWER_LOW3 = 0x02
99
TXPOWER_MID = 0x03
100
TXPOWER_HIGH = 0x04
101

    
102
DUPLEX_NOSPLIT = 0x00
103
DUPLEX_POSSPLIT = 0x01
104
DUPLEX_NEGSPLIT = 0x02
105

    
106
DUPLEX = ["", "+", "-"]
107
TUNING_STEPS = [5., 6.25, 10., 12.5, 15., 20., 25., 30., 50., 100.]
108

    
109
SCRAMBLE_LIST = ["Off", "Freq 1", "Freq 2", "Freq 3", "Freq 4", "Freq 5",
110
                 "Freq 6", "User"]
111
SQUELCH_LIST = ["Off", "S0", "S1", "S2", "S3", "S4", "S5", "S6", "S7"]
112

    
113

    
114
def _enter_programming_mode_download(radio):
115
    serial = radio.pipe
116

    
117
    _magic = radio._magic
118

    
119
    try:
120
        serial.write(_magic)
121
        if radio._echo:
122
            serial.read(len(_magic))  # Chew the echo
123
        ack = serial.read(1)
124
    except Exception:
125
        raise errors.RadioError("Error communicating with radio")
126

    
127
    if not ack:
128
        raise errors.RadioError("No response from radio")
129
    elif ack != CMD_ACK:
130
        raise errors.RadioError("Radio refused to enter programming mode")
131

    
132
    try:
133
        serial.write(b"\x02")
134
        if radio._echo:
135
            serial.read(1)  # Chew the echo
136
        ident = serial.read(8)
137
    except Exception:
138
        raise errors.RadioError("Error communicating with radio")
139

    
140
    # check if ident is OK
141
    for fp in radio._fingerprint:
142
        if ident.startswith(fp):
143
            break
144
    else:
145
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
146
        raise errors.RadioError("Radio identification failed.")
147

    
148
    try:
149
        serial.write(CMD_ACK)
150
        if radio._echo:
151
            serial.read(1)  # Chew the echo
152
        ack = serial.read(1)
153
    except Exception:
154
        raise errors.RadioError("Error communicating with radio")
155

    
156
    # check if ident is OK
157
    for fp in radio._fingerprint:
158
        if ident.startswith(fp):
159
            break
160
    else:
161
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
162
        raise errors.RadioError("Radio identification failed.")
163

    
164
    try:
165
        serial.write(CMD_ACK)
166
        serial.read(1)  # Chew the echo
167
        ack = serial.read(1)
168
    except Exception:
169
        raise errors.RadioError("Error communicating with radio")
170

    
171
    if ack != CMD_ACK:
172
        raise errors.RadioError("Radio refused to enter programming mode")
173

    
174

    
175
def _enter_programming_mode_upload(radio):
176
    serial = radio.pipe
177

    
178
    _magic = radio._magic
179

    
180
    try:
181
        serial.write(_magic)
182
        if radio._echo:
183
            serial.read(len(_magic))  # Chew the echo
184
        ack = serial.read(1)
185
    except Exception:
186
        raise errors.RadioError("Error communicating with radio")
187

    
188
    if not ack:
189
        raise errors.RadioError("No response from radio")
190
    elif ack != CMD_ACK:
191
        raise errors.RadioError("Radio refused to enter programming mode")
192

    
193
    try:
194
        serial.write(b"\x52\x1F\x05\x01")
195
        if radio._echo:
196
            serial.read(4)  # Chew the echo
197
        ident = serial.read(5)
198
    except Exception:
199
        raise errors.RadioError("Error communicating with radio")
200

    
201
    if ident != b"\x57\x1F\x05\x01\xA5":
202
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
203
        raise errors.RadioError("Radio identification failed.")
204

    
205
    try:
206
        serial.write(CMD_ACK)
207
        if radio._echo:
208
            serial.read(1)  # Chew the echo
209
        ack = serial.read(1)
210
    except Exception:
211
        raise errors.RadioError("Error communicating with radio")
212

    
213
    if ack != CMD_ACK:
214
        raise errors.RadioError("Radio refused to enter programming mode")
215

    
216

    
217
def _exit_programming_mode(radio):
218
    serial = radio.pipe
219
    try:
220
        serial.write(radio.CMD_EXIT)
221
        if radio._echo:
222
            serial.read(7)  # Chew the echo
223
    except Exception:
224
        raise errors.RadioError("Radio refused to exit programming mode")
225

    
226

    
227
def _read_block(radio, block_addr, block_size):
228
    serial = radio.pipe
229

    
230
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
231
    expectedresponse = b"W" + cmd[1:]
232
    LOG.debug("Reading block %04x..." % (block_addr))
233

    
234
    try:
235
        serial.write(cmd)
236
        if radio._echo:
237
            serial.read(4)  # Chew the echo
238
        response = serial.read(4 + block_size)
239
        if response[:4] != expectedresponse:
240
            raise Exception("Error reading block %04x." % (block_addr))
241

    
242
        block_data = response[4:]
243

    
244
    except Exception:
245
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
246

    
247
    return block_data
248

    
249

    
250
def _write_block(radio, block_addr, block_size):
251
    serial = radio.pipe
252

    
253
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
254
    data = radio.get_mmap()[block_addr:block_addr + block_size]
255

    
256
    LOG.debug("Writing Data:")
257
    LOG.debug(util.hexprint(cmd + data))
258

    
259
    try:
260
        serial.write(cmd + data)
261
        if radio._echo:
262
            serial.read(4 + len(data))  # Chew the echo
263
        if serial.read(1) != CMD_ACK:
264
            raise Exception("No ACK")
265
    except Exception:
266
        raise errors.RadioError("Failed to send block "
267
                                "to radio at %04x" % block_addr)
268

    
269

    
270
def do_download(radio):
271
    LOG.debug("download")
272
    _enter_programming_mode_download(radio)
273

    
274
    data = b""
275

    
276
    status = chirp_common.Status()
277
    status.msg = "Cloning from radio"
278

    
279
    status.cur = 0
280
    status.max = radio._memsize
281

    
282
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
283
        status.cur = addr + radio.BLOCK_SIZE
284
        radio.status_fn(status)
285

    
286
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
287
        data += block
288

    
289
        LOG.debug("Address: %04x" % addr)
290
        LOG.debug(util.hexprint(block))
291

    
292
    return data
293

    
294

    
295
def do_upload(radio):
296
    status = chirp_common.Status()
297
    status.msg = "Uploading to radio"
298

    
299
    _enter_programming_mode_upload(radio)
300

    
301
    status.cur = 0
302
    status.max = radio._memsize
303

    
304
    for start_addr, end_addr in radio._ranges:
305
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
306
            status.cur = addr + radio.BLOCK_SIZE
307
            radio.status_fn(status)
308
            _write_block(radio, addr, radio.BLOCK_SIZE)
309

    
310
    _exit_programming_mode(radio)
311

    
312

    
313
class RA87StyleRadio(chirp_common.CloneModeRadio):
314
    """Retevis RA87"""
315
    VENDOR = "Retevis"
316
    NEEDS_COMPAT_SERIAL = False
317
    BAUD_RATE = 9600
318
    BLOCK_SIZE = 0x40
319
    CMD_EXIT = b"EZ" + b"\xA5" + b"2#E" + b"\xF2"
320
    NAME_LENGTH = 6
321

    
322
    VALID_BANDS = [(400000000, 480000000)]
323

    
324
    _magic = b"PROGRAM"
325
    _fingerprint = [b"\xFF\xFF\xFF\xFF\xFF\xA5\x2C\xFF"]
326
    _upper = 99
327
    _gmrs = True
328
    _echo = True
329

    
330
    _ranges = [
331
        (0x0000, 0x2000),
332
    ]
333
    _memsize = 0x2000
334

    
335
    def get_features(self):
336
        rf = chirp_common.RadioFeatures()
337
        rf.can_odd_split = True
338
        rf.has_bank = False
339
        rf.has_ctone = True
340
        rf.has_cross = True
341
        rf.has_name = True
342
        rf.has_sub_devices = self.VARIANT == ""
343
        rf.has_tuning_step = False
344
        rf.has_rx_dtcs = True
345
        rf.has_settings = False
346
        rf.memory_bounds = (0, self._upper)
347
        rf.valid_bands = self.VALID_BANDS
348
        rf.valid_cross_modes = [
349
            "Tone->Tone",
350
            "DTCS->",
351
            "->DTCS",
352
            "Tone->DTCS",
353
            "DTCS->Tone",
354
            "->Tone",
355
            "DTCS->DTCS"]
356
        rf.valid_duplexes = DUPLEX + ["split"]
357
        rf.valid_power_levels = self.POWER_LEVELS
358
        rf.valid_modes = ["NFM", "FM"]  # 12.5 kHz, 25 kHz.
359
        rf.valid_name_length = self.NAME_LENGTH
360
        rf.valid_skips = ["", "S"]
361
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
362
        rf.valid_tuning_steps = TUNING_STEPS
363
        return rf
364

    
365
    def get_sub_devices(self):
366
        return [RA87RadioLeft(self._mmap), RA87RadioRight(self._mmap)]
367

    
368
    def process_mmap(self):
369
        """Process the mem map into the mem object"""
370
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
371

    
372
    def sync_in(self):
373
        try:
374
            data = do_download(self)
375
            self._mmap = memmap.MemoryMapBytes(data)
376
        except errors.RadioError:
377
            raise
378
        except Exception as e:
379
            LOG.exception('General failure')
380
            raise errors.RadioError('Failed to download from radio: %s' % e)
381
        finally:
382
            _exit_programming_mode(self)
383
        self.process_mmap()
384

    
385
    def sync_out(self):
386
        try:
387
            do_upload(self)
388
        except errors.RadioError:
389
            raise
390
        except Exception as e:
391
            LOG.exception('General failure')
392
            raise errors.RadioError('Failed to upload to radio: %s' % e)
393
        finally:
394
            _exit_programming_mode(self)
395

    
396
    def get_raw_memory(self, number):
397
        return repr(self._memobj.memory[number - 1])
398

    
399
    def _get_dcs(self, val):
400
        return int(str(val)[2:-16])
401

    
402
    def _set_dcs(self, val):
403
        return int(str(val), 16)
404

    
405
    def _memory_obj(self, suffix=""):
406
        return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
407

    
408
    def get_memory(self, number):
409
        _mem = self._memory_obj()[number]
410

    
411
        mem = chirp_common.Memory()
412

    
413
        mem.number = number
414

    
415
        if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF\xFF":
416
            mem.freq = 0
417
            mem.empty = True
418
            return mem
419

    
420
        mem.freq = int(_mem.rxfreq)
421

    
422
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
423
        if mem.freq == 0:
424
            mem.empty = True
425
            return mem
426

    
427
        if int(_mem.txfreq) != 0:  # DUPLEX_ODDSPLIT
428
            mem.duplex = "split"
429
            mem.offset = int(_mem.txfreq) * 10
430
        elif _mem.duplex == DUPLEX_POSSPLIT:
431
            mem.duplex = '+'
432
            mem.offset = int(_mem.offset) * 1000
433
        elif _mem.duplex == DUPLEX_NEGSPLIT:
434
            mem.duplex = '-'
435
            mem.offset = int(_mem.offset) * 1000
436
        elif _mem.duplex == DUPLEX_NOSPLIT:
437
            mem.duplex = ''
438
            mem.offset = 0
439
        else:
440
            LOG.error('%s: get_mem: unhandled duplex: %02x' %
441
                      (mem.name, _mem.duplex))
442

    
443
        mem.tuning_step = TUNING_STEPS[_mem.step]
444

    
445
        mem.mode = not _mem.narrow and "FM" or "NFM"
446

    
447
        mem.skip = _mem.skip and "S" or ""
448

    
449
        mem.name = str(_mem.name).strip("\xFF")
450

    
451
        dtcs_pol = ["N", "N"]
452

    
453
        if _mem.rxtone == 0xFFF:
454
            rxmode = ""
455
        elif _mem.rxtone == 0x800 and _mem.is_rxdigtone == 0:
456
            rxmode = ""
457
        elif _mem.is_rxdigtone == 0:
458
            # CTCSS
459
            rxmode = "Tone"
460
            mem.ctone = int(_mem.rxtone) / 10.0
461
        else:
462
            # Digital
463
            rxmode = "DTCS"
464
            mem.rx_dtcs = self._get_dcs(_mem.rxtone)
465
            if _mem.rxdtcs_pol == 1:
466
                dtcs_pol[1] = "R"
467

    
468
        if _mem.txtone == 0xFFF:
469
            txmode = ""
470
        elif _mem.txtone == 0x08 and _mem.is_txdigtone == 0:
471
            txmode = ""
472
        elif _mem.is_txdigtone == 0:
473
            # CTCSS
474
            txmode = "Tone"
475
            mem.rtone = int(_mem.txtone) / 10.0
476
        else:
477
            # Digital
478
            txmode = "DTCS"
479
            mem.dtcs = self._get_dcs(_mem.txtone)
480
            if _mem.txdtcs_pol == 1:
481
                dtcs_pol[0] = "R"
482

    
483
        if txmode == "Tone" and not rxmode:
484
            mem.tmode = "Tone"
485
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
486
            mem.tmode = "TSQL"
487
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
488
            mem.tmode = "DTCS"
489
        elif rxmode or txmode:
490
            mem.tmode = "Cross"
491
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
492

    
493
        mem.dtcs_polarity = "".join(dtcs_pol)
494

    
495
        _levels = self.POWER_LEVELS
496
        if _mem.txpower == TXPOWER_HIGH:
497
            mem.power = _levels[4]
498
        elif _mem.txpower == TXPOWER_MID:
499
            mem.power = _levels[3]
500
        elif _mem.txpower == TXPOWER_LOW3:
501
            mem.power = _levels[2]
502
        elif _mem.txpower == TXPOWER_LOW2:
503
            mem.power = _levels[1]
504
        elif _mem.txpower == TXPOWER_LOW:
505
            mem.power = _levels[0]
506
        else:
507
            LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
508
                      (mem.name, _mem.txpower))
509

    
510
        mem.extra = RadioSettingGroup("Extra", "extra")
511
        rs = RadioSetting("beatshift", "Beat Shift",
512
                          RadioSettingValueBoolean(_mem.beatshift))
513
        mem.extra.append(rs)
514
        rs = RadioSetting("compand", "Compand",
515
                          RadioSettingValueBoolean(_mem.compand))
516
        mem.extra.append(rs)
517
        scramble_options = ['Off', 'Freq 1', 'Freq 2', 'Freq 3', 'Freq 4',
518
                            'Freq 5', 'Freq 6', 'User']
519
        scramble_option = scramble_options[_mem.scramble]
520
        rs = RadioSetting("scramble", "Scramble",
521
                          RadioSettingValueList(scramble_options,
522
                                                scramble_option))
523
        mem.extra.append(rs)
524
        rs = RadioSetting("hide", "Hide Channel",
525
                          RadioSettingValueBoolean(_mem.hide))
526
        mem.extra.append(rs)
527
        rs = RadioSetting("reverse", "Reverse",
528
                          RadioSettingValueBoolean(_mem.reverse))
529
        mem.extra.append(rs)
530

    
531
        return mem
532

    
533
    def set_memory(self, mem):
534
        # Get a low-level memory object mapped to the image
535
        _mem = self._memory_obj()[mem.number]
536

    
537
        if mem.empty:
538
            _mem.set_raw(b"\xFF" * 31 + b"\x80")
539

    
540
            return
541

    
542
        _mem.set_raw(b"\x00" * 25 + b"\xFF" * 6 + b"\x00")
543

    
544
        _mem.rxfreq = mem.freq
545

    
546
        if mem.duplex == 'split':
547
            _mem.txfreq = mem.offset / 10
548
        elif mem.duplex == '+':
549
            _mem.duplex = DUPLEX_POSSPLIT
550
            _mem.offset = mem.offset / 1000
551
        elif mem.duplex == '-':
552
            _mem.duplex = DUPLEX_NEGSPLIT
553
            _mem.offset = mem.offset / 1000
554
        elif mem.duplex == '':
555
            _mem.duplex = DUPLEX_NOSPLIT
556
        else:
557
            LOG.error('%s: set_mem: unhandled duplex: %s' %
558
                      (mem.name, mem.duplex))
559

    
560
        rxmode = ""
561
        txmode = ""
562

    
563
        if mem.tmode == "Tone":
564
            txmode = "Tone"
565
        elif mem.tmode == "TSQL":
566
            rxmode = "Tone"
567
            txmode = "TSQL"
568
        elif mem.tmode == "DTCS":
569
            rxmode = "DTCSSQL"
570
            txmode = "DTCS"
571
        elif mem.tmode == "Cross":
572
            txmode, rxmode = mem.cross_mode.split("->", 1)
573

    
574
        if rxmode == "":
575
            _mem.rxdtcs_pol = 0
576
            _mem.is_rxdigtone = 0
577
            _mem.rxtone = 0x800
578
        elif rxmode == "Tone":
579
            _mem.rxdtcs_pol = 0
580
            _mem.is_rxdigtone = 0
581
            _mem.rxtone = int(mem.ctone * 10)
582
        elif rxmode == "DTCSSQL":
583
            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
584
            _mem.is_rxdigtone = 1
585
            _mem.rxtone = self._set_dcs(mem.dtcs)
586
        elif rxmode == "DTCS":
587
            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
588
            _mem.is_rxdigtone = 1
589
            _mem.rxtone = self._set_dcs(mem.rx_dtcs)
590

    
591
        if txmode == "":
592
            _mem.txdtcs_pol = 0
593
            _mem.is_txdigtone = 0
594
            _mem.txtone = 0x08
595
        elif txmode == "Tone":
596
            _mem.txdtcs_pol = 0
597
            _mem.is_txdigtone = 0
598
            _mem.txtone = int(mem.rtone * 10)
599
        elif txmode == "TSQL":
600
            _mem.txdtcs_pol = 0
601
            _mem.is_txdigtone = 0
602
            _mem.txtone = int(mem.ctone * 10)
603
        elif txmode == "DTCS":
604
            _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
605
            _mem.is_txdigtone = 1
606
            _mem.txtone = self._set_dcs(mem.dtcs)
607

    
608
        # name TAG of the channel
609
        _mem.name = mem.name.rstrip().ljust(6, "\xFF")
610

    
611
        _levels = self.POWER_LEVELS
612
        if mem.power is None:
613
            _mem.txpower = TXPOWER_LOW
614
        elif mem.power == _levels[0]:
615
            _mem.txpower = TXPOWER_LOW
616
        elif mem.power == _levels[1]:
617
            _mem.txpower = TXPOWER_LOW2
618
        elif mem.power == _levels[2]:
619
            _mem.txpower = TXPOWER_LOW3
620
        elif mem.power == _levels[3]:
621
            _mem.txpower = TXPOWER_MID
622
        elif mem.power == _levels[4]:
623
            _mem.txpower = TXPOWER_HIGH
624
        else:
625
            LOG.error('%s: set_mem: unhandled power level: %s' %
626
                      (mem.name, mem.power))
627

    
628
        _mem.narrow = 'N' in mem.mode
629
        _mem.skip = mem.skip == "S"
630
        _mem.step = TUNING_STEPS.index(mem.tuning_step)
631

    
632
        for setting in mem.extra:
633
            setattr(_mem, setting.get_name(), int(setting.value))
634

    
635
    @classmethod
636
    def match_model(cls, filedata, filename):
637
        # This radio has always been post-metadata, so never do
638
        # old-school detection
639
        return False
640

    
641

    
642
@directory.register
643
class RA87Radio(RA87StyleRadio):
644
    """Retevis RA87"""
645
    MODEL = "RA87"
646

    
647
    POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
648
                    chirp_common.PowerLevel("Low2", watts=10.00),
649
                    chirp_common.PowerLevel("Low3", watts=15.00),
650
                    chirp_common.PowerLevel("Mid", watts=20.00),
651
                    chirp_common.PowerLevel("High", watts=40.00)]
652

    
653

    
654
class RA87RadioLeft(RA87Radio):
655
    """Retevis RA87 Left VFO subdevice"""
656
    VARIANT = "Left"
657
    _vfo = "left"
658

    
659

    
660
class RA87RadioRight(RA87Radio):
661
    """Retevis RA87 Right VFO subdevice"""
662
    VARIANT = "Right"
663
    _vfo = "right"
    (1-1/1)