Project

General

Profile

New Model #9821 » radtel_t18 - t20_frs.py

choose Baofeng T-20FRS - Jim Unroe, 12/15/2023 01:32 PM

 
1
# Copyright 2021 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
    RadioSettingValueInteger, RadioSettingValueList, \
23
    RadioSettingValueBoolean, RadioSettings
24

    
25
LOG = logging.getLogger(__name__)
26

    
27
MEM_FORMAT = """
28
#seekto 0x0010;
29
struct {
30
    lbcd rxfreq[4];
31
    lbcd txfreq[4];
32
    lbcd rxtone[2];
33
    lbcd txtone[2];
34
    u8 speccode:1,       // Retevis RB87 PTT ID
35
       compander:1,
36
       scramble:1,
37
       skip:1,
38
       highpower:1,
39
       narrow:1,
40
       unknown2:1,
41
       bcl:1;
42
    u8 unknown3[3];
43
} memory[%d];
44
#seekto 0x02B0;
45
struct {
46
    u8 voicesw;      // Voice SW            +
47
    u8 voiceselect;  // Voice Select
48
    u8 scan;         // Scan                +
49
    u8 vox;          // VOX                 +
50
    u8 voxgain;      // Vox Gain            +
51
    u8 voxnotxonrx;  // Rx Disable Vox      +
52
    u8 hivoltnotx;   // High Vol Inhibit TX +
53
    u8 lovoltnotx;   // Low Vol Inhibit TX  +
54
    u8 rxemergency;  // RX Emergency
55
} settings2;
56
#seekto 0x03C0;
57
struct {
58
    u8 codesw:1,         // Retevis RB29 code switch
59
       scanmode:1,
60
       vox:1,            // Retevis RB19 VOX
61
       speccode:1,
62
       voiceprompt:2,
63
       batterysaver:1,
64
       beep:1;           // Retevis RB87 vox
65
    u8 squelchlevel;
66
    u8 sidekey2;         // Retevis RT22S setting
67
                         // Retevis RB85 sidekey 1 short
68
                         // Retevis RB19 sidekey 2 long
69
                         // Retevis RT47 sidekey 1 long
70
                         // Retevis RB87 sidekey 1 long
71
    u8 timeouttimer;
72
    u8 voxlevel;
73
    u8 sidekey2S;
74
    u8 unused;           // Selected channel
75
    u8 voxdelay;
76
    u8 sidekey1L;
77
    u8 sidekey2L;
78
    u8 unused2[3];
79
    u8 unknown3:4,
80
       unknown4:1,
81
       unknown5:2,
82
       power10w:1;       // Retevis RT85 power 10w on/off
83
                         // Retevis RT75 stop TX with low voltage
84
} settings;
85
"""
86

    
87
MEM_FORMAT_RB18 = """
88
#seekto 0x0000;
89
struct {
90
    lbcd rxfreq[4];
91
    lbcd txfreq[4];
92
    lbcd rxtone[2];
93
    lbcd txtone[2];
94
    u8 jumpcode:1,
95
       unknown1:2,
96
       skip:1,
97
       highpower:1,
98
       narrow:1,
99
       unknown2:1,
100
       bcl:1;
101
    u8 unknown3[3];
102
} memory[%d];
103
#seekto 0x0630;
104
struct {
105
    u8 unk630:7,
106
       voice:1;
107
    u8 unk631:7,
108
       language:1;
109
    u8 unk632:7,
110
       scan:1;
111
    u8 unk633:7,
112
       vox:1;
113
    u8 unk634:5,
114
       vox_level:3;
115
    u8 unk635;
116
    u8 unk636:7,
117
       lovoltnotx:1;
118
    u8 unk637:7,
119
       hivoltnotx:1;
120
    u8 unknown2[8];
121
    u8 unk640:5,
122
       rogerbeep:1,
123
       batterysaver:1,
124
       beep:1;
125
    u8 squelchlevel;
126
    u8 unk642;
127
    u8 timeouttimer;
128
    u8 unk644:7,
129
       tail:1;
130
    u8 channel;
131
} settings;
132
"""
133

    
134
MEM_FORMAT_T20FRS = """
135
// #seekto 0x0000;
136
struct {
137
    lbcd rxfreq[4];
138
    lbcd txfreq[4];
139
    lbcd rxtone[2];
140
    lbcd txtone[2];
141
    u8 jumpcode:1,
142
       unknown1:2,
143
       skip:1,
144
       highpower:1,
145
       narrow:1,
146
       unknown2:1,
147
       bcl:1;
148
    u8 unknown3[3];
149
} memory[%d];
150
#seekto 0x02B0;
151
struct {
152
    u8 voicesw;      // Voice SW            +
153
    u8 voiceselect;  // Voice Select
154
    u8 scan;         // Scan                +
155
    u8 vox;          // VOX                 +
156
    u8 voxgain;      // Vox Gain            +
157
    u8 voxnotxonrx;  // Rx Disable Vox      +
158
    u8 hivoltnotx;   // High Vol Inhibit TX +
159
    u8 lovoltnotx;   // Low Vol Inhibit TX  +
160
    u8 rxemergency;  // RX Emergency
161
} settings2;
162
#seekto 0x02C0;
163
struct {
164
    u8 unk:6,
165
       batterysaver:1,
166
       beep:1;
167
    u8 squelchlevel;
168
    u8 sidekey2;
169
    u8 timeouttimer;
170
} settings;
171
"""
172

    
173
CMD_ACK = b"\x06"
174

    
175
VOICE_LIST = ["Off", "Chinese", "English"]
176
VOICE_LIST2 = ["English", "Chinese"]
177
VOICE_LIST3 = ["Off", "English", "Chinese"]
178
VOICE_LIST4 = ["Chinese", "English"]
179
TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds",
180
                     "120 seconds", "150 seconds", "180 seconds",
181
                     "210 seconds", "240 seconds", "270 seconds",
182
                     "300 seconds"]
183
SCANMODE_LIST = ["Carrier", "Time"]
184
VOXLEVEL_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
185
VOXDELAY_LIST = ["0.5 seconds", "1.0 seconds", "1.5 seconds",
186
                 "2.0 seconds", "2.5 seconds", "3.0 seconds"]
187
SIDEKEY19_LIST = ["Off", "Scan", "Emergency Alarm"]
188
SIDEKEY2_LIST = SIDEKEY19_LIST + ["Display Battery"]
189
SIDEKEY29_LIST = ["Off", "Scan", "VOX", "Busy Lock", "Emergency Alarm"]
190

    
191
SIDEKEY85SHORT_LIST = ["Off",
192
                       "Noise Cansellation On",
193
                       "Continuous Monitor",
194
                       "High/Low Power",
195
                       "Emergency Alarm",
196
                       "Show Battery",
197
                       "Scan",
198
                       "VOX",
199
                       "Busy Channel Lock"]
200
SIDEKEY85LONG_LIST = ["Off",
201
                      "Noise Cansellation On",
202
                      "Continuous Monitor",
203
                      "Monitor Momentary",
204
                      "High/Low Power",
205
                      "Emergency Alarm",
206
                      "Show Battery",
207
                      "Scan",
208
                      "VOX",
209
                      "Busy Channel Lock"]
210
SPECCODE_LIST = ["SpeCode 1", "SpeCode 2"]
211
SIDEKEY75_LIST = ["Off",
212
                  "Monitor Momentary",
213
                  "Scan",
214
                  "VOX",
215
                  "Monitor",
216
                  "Announciation"]
217
SIDEKEY47_LIST = ["Monitor Momentary",
218
                  "Channel Lock",
219
                  "Scan",
220
                  "VOX"]
221
SIDEKEYV8A_LIST = ["Off",
222
                   "Monitor",
223
                   "High/Low Power",
224
                   "Alarm"]
225
SIDEKEY87_LIST = ["Scan", "Emergency Alarm"]
226

    
227
FRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
228
              462687500, 462712500]
229
FRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
230
              467687500, 467712500]
231
FRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
232
              462675000, 462700000, 462725000]
233
FRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3
234

    
235
FRS16_FREQS = [462562500, 462587500, 462612500, 462637500,
236
               462662500, 462625000, 462725000, 462687500,
237
               462712500, 462550000, 462575000, 462600000,
238
               462650000, 462675000, 462700000, 462725000]
239

    
240
GMRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3 * 2
241

    
242
MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
243

    
244
PMR_FREQS1 = [446006250, 446018750, 446031250, 446043750, 446056250,
245
              446068750, 446081250, 446093750]
246
PMR_FREQS2 = [446106250, 446118750, 446131250, 446143750, 446156250,
247
              446168750, 446181250, 446193750]
248
PMR_FREQS = PMR_FREQS1 + PMR_FREQS2
249

    
250

    
251
def _t18_enter_programming_mode(radio):
252
    serial = radio.pipe
253

    
254
    _magic = b"\x02" + radio._magic
255

    
256
    try:
257
        serial.write(_magic)
258
        if radio._echo:
259
            chew = serial.read(len(_magic))  # Chew the echo
260
        ack = serial.read(1)
261
    except:
262
        raise errors.RadioError("Error communicating with radio")
263

    
264
    if not ack:
265
        raise errors.RadioError("No response from radio")
266
    elif ack != CMD_ACK:
267
        raise errors.RadioError("Radio refused to enter programming mode")
268

    
269
    try:
270
        serial.write(b"\x02")
271
        if radio._echo:
272
            serial.read(1)  # Chew the echo
273
        ident = serial.read(8)
274
    except:
275
        raise errors.RadioError("Error communicating with radio")
276

    
277
    # check if ident is OK
278
    for fp in radio._fingerprint:
279
        if ident.startswith(fp):
280
            break
281
    else:
282
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
283
        raise errors.RadioError("Radio identification failed.")
284

    
285
    try:
286
        serial.write(CMD_ACK)
287
        if radio._echo:
288
            serial.read(1)  # Chew the echo
289
        ack = serial.read(1)
290
    except:
291
        raise errors.RadioError("Error communicating with radio")
292

    
293
    if radio.MODEL == "RT647":
294
        if ack != b"\xF0":
295
            raise errors.RadioError("Radio refused to enter programming mode")
296
    else:
297
        if ack != CMD_ACK:
298
            raise errors.RadioError("Radio refused to enter programming mode")
299

    
300

    
301
def _t18_exit_programming_mode(radio):
302
    serial = radio.pipe
303
    try:
304
        serial.write(radio.CMD_EXIT)
305
        if radio._echo:
306
            chew = serial.read(1)  # Chew the echo
307
    except:
308
        raise errors.RadioError("Radio refused to exit programming mode")
309

    
310

    
311
def _t18_read_block(radio, block_addr, block_size):
312
    serial = radio.pipe
313

    
314
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
315
    expectedresponse = b"W" + cmd[1:]
316
    LOG.debug("Reading block %04x..." % (block_addr))
317

    
318
    try:
319
        serial.write(cmd)
320
        if radio._echo:
321
            serial.read(4)  # Chew the echo
322
        response = serial.read(4 + block_size)
323
        if response[:4] != expectedresponse:
324
            raise Exception("Error reading block %04x." % (block_addr))
325

    
326
        block_data = response[4:]
327

    
328
        if radio.ACK_BLOCK:
329
            serial.write(CMD_ACK)
330
            if radio._echo:
331
                serial.read(1)  # Chew the echo
332
            ack = serial.read(1)
333
    except:
334
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
335

    
336
    if radio.ACK_BLOCK:
337
        if ack != CMD_ACK:
338
            raise Exception("No ACK reading block %04x." % (block_addr))
339

    
340
    return block_data
341

    
342

    
343
def _t18_write_block(radio, block_addr, block_size):
344
    serial = radio.pipe
345

    
346
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
347
    data = radio.get_mmap()[block_addr:block_addr + block_size]
348

    
349
    LOG.debug("Writing Data:")
350
    LOG.debug(util.hexprint(cmd + data))
351

    
352
    try:
353
        serial.write(cmd + data)
354
        if radio._echo:
355
            serial.read(4 + len(data))  # Chew the echo
356
        if serial.read(1) != CMD_ACK:
357
            raise Exception("No ACK")
358
    except:
359
        raise errors.RadioError("Failed to send block "
360
                                "to radio at %04x" % block_addr)
361

    
362

    
363
def do_download(radio):
364
    LOG.debug("download")
365
    _t18_enter_programming_mode(radio)
366

    
367
    data = b""
368

    
369
    status = chirp_common.Status()
370
    status.msg = "Cloning from radio"
371

    
372
    status.cur = 0
373
    status.max = radio._memsize
374

    
375
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
376
        status.cur = addr + radio.BLOCK_SIZE
377
        radio.status_fn(status)
378

    
379
        block = _t18_read_block(radio, addr, radio.BLOCK_SIZE)
380
        data += block
381

    
382
        LOG.debug("Address: %04x" % addr)
383
        LOG.debug(util.hexprint(block))
384

    
385
    _t18_exit_programming_mode(radio)
386

    
387
    return memmap.MemoryMapBytes(data)
388

    
389

    
390
def do_upload(radio):
391
    status = chirp_common.Status()
392
    status.msg = "Uploading to radio"
393

    
394
    _t18_enter_programming_mode(radio)
395

    
396
    status.cur = 0
397
    status.max = radio._memsize
398

    
399
    for start_addr, end_addr in radio._ranges:
400
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
401
            status.cur = addr + radio.BLOCK_SIZE
402
            radio.status_fn(status)
403
            _t18_write_block(radio, addr, radio.BLOCK_SIZE)
404

    
405
    _t18_exit_programming_mode(radio)
406

    
407

    
408
def model_match(cls, data):
409
    """Match the opened/downloaded image to the correct version"""
410

    
411
    if len(data) == cls._memsize:
412
        rid = data[0x03D0:0x03D8]
413
        return b"P558" in rid
414
    else:
415
        return False
416

    
417

    
418
@directory.register
419
class T18Radio(chirp_common.CloneModeRadio):
420
    """radtel T18"""
421
    VENDOR = "Radtel"
422
    MODEL = "T18"
423
    BAUD_RATE = 9600
424
    NEEDS_COMPAT_SERIAL = False
425
    BLOCK_SIZE = 0x08
426
    CMD_EXIT = b"b"
427
    ACK_BLOCK = True
428

    
429
    VALID_BANDS = [(400000000, 470000000)]
430

    
431
    _magic = b"1ROGRAM"
432
    _fingerprint = [b"SMP558" + b"\x00\x00"]
433
    _upper = 16
434
    _mem_params = (_upper  # number of channels
435
                   )
436
    _frs = _frs16 = _murs = _pmr = _gmrs = False
437
    _echo = False
438

    
439
    _ranges = [
440
        (0x0000, 0x03F0),
441
    ]
442
    _memsize = 0x03F0
443

    
444
    def get_features(self):
445
        rf = chirp_common.RadioFeatures()
446
        rf.has_settings = True
447
        rf.valid_modes = ["NFM", "FM"]  # 12.5 kHz, 25 kHz.
448
        rf.valid_skips = ["", "S"]
449
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
450
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
451
        if self.MODEL != "T18" and \
452
                self.MODEL != "RB618" and \
453
                self.MODEL != "RT647":
454
            rf.valid_power_levels = self.POWER_LEVELS
455
        rf.can_odd_split = True
456
        rf.has_rx_dtcs = True
457
        rf.has_ctone = True
458
        rf.has_cross = True
459
        rf.valid_cross_modes = [
460
            "Tone->Tone",
461
            "DTCS->",
462
            "->DTCS",
463
            "Tone->DTCS",
464
            "DTCS->Tone",
465
            "->Tone",
466
            "DTCS->DTCS"]
467
        rf.has_tuning_step = False
468
        rf.has_bank = False
469
        rf.has_name = False
470
        rf.memory_bounds = (1, self._upper)
471
        rf.valid_bands = self.VALID_BANDS
472
        rf.valid_tuning_steps = chirp_common.TUNING_STEPS
473

    
474
        return rf
475

    
476
    def process_mmap(self):
477
        self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
478

    
479
    def sync_in(self):
480
        self._mmap = do_download(self)
481
        self.process_mmap()
482

    
483
    def sync_out(self):
484
        do_upload(self)
485

    
486
    def get_raw_memory(self, number):
487
        return repr(self._memobj.memory[number - 1])
488

    
489
    def _decode_tone(self, val):
490
        val = int(val)
491
        if val == 16665:
492
            return '', None, None
493
        elif val >= 12000:
494
            return 'DTCS', val - 12000, 'R'
495
        elif val >= 8000:
496
            return 'DTCS', val - 8000, 'N'
497
        else:
498
            return 'Tone', val / 10.0, None
499

    
500
    def _encode_tone(self, memval, mode, value, pol):
501
        if mode == '':
502
            memval[0].set_raw(0xFF)
503
            memval[1].set_raw(0xFF)
504
        elif mode == 'Tone':
505
            memval.set_value(int(value * 10))
506
        elif mode == 'DTCS':
507
            flag = 0x80 if pol == 'N' else 0xC0
508
            memval.set_value(value)
509
            memval[1].set_bits(flag)
510
        else:
511
            raise Exception("Internal error: invalid mode `%s'" % mode)
512

    
513
    def get_memory(self, number):
514
        _mem = self._memobj.memory[number - 1]
515

    
516
        mem = chirp_common.Memory()
517

    
518
        mem.number = number
519
        mem.freq = int(_mem.rxfreq) * 10
520

    
521
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
522
        if mem.freq == 0:
523
            mem.empty = True
524
            return mem
525

    
526
        if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
527
            mem.freq = 0
528
            mem.empty = True
529
            return mem
530

    
531
        if _mem.txfreq.get_raw() == b"\xFF\xFF\xFF\xFF":
532
            mem.duplex = "off"
533
            mem.offset = 0
534
        elif int(_mem.rxfreq) == int(_mem.txfreq):
535
            mem.duplex = ""
536
            mem.offset = 0
537
        else:
538
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
539
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
540

    
541
        mem.mode = not _mem.narrow and "FM" or "NFM"
542

    
543
        mem.skip = _mem.skip and "S" or ""
544

    
545
        txtone = self._decode_tone(_mem.txtone)
546
        rxtone = self._decode_tone(_mem.rxtone)
547
        chirp_common.split_tone_decode(mem, txtone, rxtone)
548

    
549
        if self.MODEL != "T18" and self.MODEL != "RB618":
550
            mem.power = self.POWER_LEVELS[1 - _mem.highpower]
551

    
552
        mem.extra = RadioSettingGroup("Extra", "extra")
553
        rs = RadioSetting("bcl", "Busy Channel Lockout",
554
                          RadioSettingValueBoolean(not _mem.bcl))
555
        mem.extra.append(rs)
556
        if self.MODEL != "RB18" and self.MODEL != "RB618" and \
557
                self.MODEL != "FRS-B1" and self.MODEL != "BF-V8A" and \
558
                self.MODEL != "RB29" and self.MODEL != "RB629" and \
559
                self.MODEL != "BF-T20FRS":
560
            if self.MODEL not in ["RB87",
561
                                  "RT47V"]:
562
                rs = RadioSetting("scramble", "Scramble",
563
                                  RadioSettingValueBoolean(not _mem.scramble))
564
                mem.extra.append(rs)
565
            rs = RadioSetting("compander", "Compander",
566
                              RadioSettingValueBoolean(not _mem.compander))
567
            mem.extra.append(rs)
568
            if self.MODEL != "RT47V" and self.MODEL != "T8" and \
569
                    self.MODEL != "RB17" and self.MODEL != "RB617" and \
570
                    self.MODEL != "RB75" and self.MODEL != "RB19P":
571
                if self.MODEL == "RB87":
572
                    rs = RadioSettingValueBoolean(not _mem.speccode)
573
                    rset = RadioSetting("speccode", "PTT-ID", rs)
574
                    mem.extra.append(rset)
575
                else:
576
                    rs = RadioSettingValueBoolean(not _mem.speccode)
577
                    rset = RadioSetting("speccode", "Spec Code", rs)
578
                    mem.extra.append(rset)
579

    
580
        immutable = []
581

    
582
        if self._frs:
583
            if mem.freq in FRS_FREQS:
584
                if mem.number >= 1 and mem.number <= 22:
585
                    FRS_FREQ = FRS_FREQS[mem.number - 1]
586
                    mem.freq = FRS_FREQ
587
                mem.duplex = ''
588
                mem.offset = 0
589
                mem.mode = "NFM"
590
                if mem.number >= 8 and mem.number <= 14:
591
                    mem.power = self.POWER_LEVELS[1]
592
                    immutable = ["empty", "freq", "duplex", "offset", "mode",
593
                                 "power"]
594
                else:
595
                    immutable = ["empty", "freq", "duplex", "offset", "mode"]
596
        elif self._frs16:
597
            if mem.freq in FRS16_FREQS:
598
                if mem.number >= 1 and mem.number <= 16:
599
                    FRS_FREQ = FRS16_FREQS[mem.number - 1]
600
                    mem.freq = FRS_FREQ
601
                mem.duplex = ''
602
                mem.offset = 0
603
                mem.mode = "NFM"
604
                immutable = ["empty", "freq", "duplex", "offset", "mode"]
605
        elif self._murs:
606
            if mem.freq in MURS_FREQS:
607
                if mem.number >= 1 and mem.number <= 5:
608
                    MURS_FREQ = MURS_FREQS[mem.number - 1]
609
                    mem.freq = MURS_FREQ
610
                mem.duplex = ''
611
                mem.offset = 0
612
                if mem.number <= 3:
613
                    mem.mode = "NFM"
614
                    immutable = ["empty", "freq", "duplex", "offset", "mode"]
615
                else:
616
                    immutable = ["empty", "freq", "duplex", "offset"]
617
        elif self._pmr:
618
            if mem.freq in PMR_FREQS:
619
                if mem.number >= 1 and mem.number <= 16:
620
                    PMR_FREQ = PMR_FREQS[mem.number - 1]
621
                    mem.freq = PMR_FREQ
622
                mem.duplex = ''
623
                mem.offset = 0
624
                mem.mode = "NFM"
625
                mem.power = self.POWER_LEVELS[1]
626
                immutable = ["empty", "freq", "duplex", "offset", "mode",
627
                             "power"]
628
        elif self._gmrs:
629
            if mem.freq in GMRS_FREQS:
630
                if mem.number >= 1 and mem.number <= 30:
631
                    GMRS_FREQ = GMRS_FREQS[mem.number - 1]
632
                    mem.freq = GMRS_FREQ
633
                    immutable = ["freq"]
634
                if mem.number >= 1 and mem.number <= 7:
635
                    mem.duplex = ''
636
                    mem.offset = 0
637
                    immutable += ["duplex", "offset"]
638
                elif mem.number >= 8 and mem.number <= 14:
639
                    mem.duplex = ''
640
                    mem.offset = 0
641
                    mem.mode = "NFM"
642
                    mem.power = self.POWER_LEVELS[1]
643
                    immutable += ["duplex", "offset", "mode", "power"]
644
                elif mem.number >= 15 and mem.number <= 22:
645
                    mem.duplex = ''
646
                    mem.offset = 0
647
                    immutable += ["duplex", "offset"]
648
                elif mem.number >= 23 and mem.number <= 30:
649
                    mem.duplex = '+'
650
                    mem.offset = 5000000
651
                    immutable += ["duplex", "offset"]
652

    
653
        mem.immutable = immutable
654

    
655
        return mem
656

    
657
    def set_memory(self, mem):
658
        # Get a low-level memory object mapped to the image
659
        _mem = self._memobj.memory[mem.number - 1]
660

    
661
        if mem.empty:
662
            _mem.set_raw("\xFF" * (_mem.size() // 8))
663

    
664
            return
665

    
666
        if self.MODEL == "BF-V8A" or self.MODEL == "BF-T20FRS":
667
            _mem.set_raw("\x00" * 12 + "\x09\xFF\xFF\xFF")
668
        else:
669
            _mem.set_raw("\x00" * 12 + "\xF9\xFF\xFF\xFF")
670

    
671
        _mem.rxfreq = mem.freq / 10
672

    
673
        if mem.duplex == "off":
674
            for i in range(0, 4):
675
                _mem.txfreq[i].set_raw("\xFF")
676
        elif mem.duplex == "split":
677
            _mem.txfreq = mem.offset / 10
678
        elif mem.duplex == "+":
679
            _mem.txfreq = (mem.freq + mem.offset) / 10
680
        elif mem.duplex == "-":
681
            _mem.txfreq = (mem.freq - mem.offset) / 10
682
        else:
683
            _mem.txfreq = mem.freq / 10
684

    
685
        txtone, rxtone = chirp_common.split_tone_encode(mem)
686
        self._encode_tone(_mem.txtone, *txtone)
687
        self._encode_tone(_mem.rxtone, *rxtone)
688

    
689
        if self.MODEL != "T18" and self.MODEL != "RB18":
690
            _mem.highpower = mem.power == self.POWER_LEVELS[0]
691

    
692
        _mem.narrow = 'N' in mem.mode
693
        _mem.skip = mem.skip == "S"
694

    
695
        for setting in mem.extra:
696
            # NOTE: Only three settings right now, all are inverted
697
            setattr(_mem, setting.get_name(), not int(setting.value))
698

    
699
    def get_settings(self):
700
        _settings = self._memobj.settings
701
        if self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
702
                self.MODEL == "BF-T20FRS":
703
            _settings2 = self._memobj.settings2
704
        basic = RadioSettingGroup("basic", "Basic Settings")
705
        top = RadioSettings(basic)
706

    
707
        rs = RadioSetting("squelchlevel", "Squelch level",
708
                          RadioSettingValueInteger(
709
                              0, 9, _settings.squelchlevel))
710
        basic.append(rs)
711

    
712
        rs = RadioSetting("timeouttimer", "Timeout timer",
713
                          RadioSettingValueList(
714
                              TIMEOUTTIMER_LIST,
715
                              TIMEOUTTIMER_LIST[
716
                                  _settings.timeouttimer]))
717
        basic.append(rs)
718

    
719
        if self.MODEL == "RB18" or self.MODEL == "RB618":
720
            rs = RadioSetting("scan", "Scan",
721
                              RadioSettingValueBoolean(_settings.scan))
722
            basic.append(rs)
723
        elif self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
724
                self.MODEL == "BF-T20FRS":
725
            rs = RadioSetting("settings2.scan", "Scan",
726
                              RadioSettingValueBoolean(_settings2.scan))
727
            basic.append(rs)
728
        else:
729
            rs = RadioSetting("scanmode", "Scan mode",
730
                              RadioSettingValueList(
731
                                  SCANMODE_LIST,
732
                                  SCANMODE_LIST[_settings.scanmode]))
733
            basic.append(rs)
734

    
735
        if self.MODEL == "RT20":
736
            rs = RadioSetting("voiceprompt", "Voice prompts",
737
                              RadioSettingValueList(
738
                                  VOICE_LIST4,
739
                                  VOICE_LIST4[_settings.voiceprompt]))
740
            basic.append(rs)
741
        elif self.MODEL == "RT22S":
742
            rs = RadioSetting("voiceprompt", "Voice prompts",
743
                              RadioSettingValueBoolean(_settings.voiceprompt))
744
            basic.append(rs)
745
        elif self.MODEL == "RB18" or self.MODEL == "RB618":
746
            rs = RadioSetting("voice", "Voice prompts",
747
                              RadioSettingValueBoolean(_settings.voice))
748
            basic.append(rs)
749
        elif self.MODEL == "FRS-B1" or self.MODEL == "BF-V8A" or \
750
                self.MODEL == "BF-T20FRS":
751
            rs = RadioSetting("settings2.voicesw", "Voice prompts",
752
                              RadioSettingValueBoolean(_settings2.voicesw))
753
            basic.append(rs)
754
        elif self.MODEL == "RT15":
755
            rs = RadioSetting("voiceprompt", "Voice prompts",
756
                              RadioSettingValueList(
757
                                  VOICE_LIST3,
758
                                  VOICE_LIST3[_settings.voiceprompt]))
759
            basic.append(rs)
760
        else:
761
            if self.MODEL != "RB87":
762
                rs = RadioSetting("voiceprompt", "Voice prompts",
763
                                  RadioSettingValueList(
764
                                      VOICE_LIST,
765
                                      VOICE_LIST[_settings.voiceprompt]))
766
                basic.append(rs)
767

    
768
        rs = RadioSetting("batterysaver", "Battery saver",
769
                          RadioSettingValueBoolean(_settings.batterysaver))
770
        basic.append(rs)
771

    
772
        if self.MODEL not in ["RB29",
773
                              "RB75",
774
                              "RB87",
775
                              "RB629",
776
                              "RT15"
777
                              ]:
778
            rs = RadioSetting("beep", "Beep",
779
                              RadioSettingValueBoolean(_settings.beep))
780
            basic.append(rs)
781

    
782
        if self.MODEL == "RB19" or self.MODEL == "RB19P" \
783
                or self.MODEL == "RB619":
784
            rs = RadioSetting("vox", "VOX",
785
                              RadioSettingValueBoolean(_settings.vox))
786
            basic.append(rs)
787

    
788
        if self.MODEL == "RB87":
789
            rs = RadioSetting("beep", "VOX",
790
                              RadioSettingValueBoolean(_settings.beep))
791
            basic.append(rs)
792

    
793
        if self.MODEL != "RB18" and self.MODEL != "RB618" \
794
                and self.MODEL != "FRS-B1" \
795
                and self.MODEL != "BF-V8A" \
796
                and self.MODEL != "BF-T20FRS":
797
            rs = RadioSetting("voxlevel", "Vox level",
798
                              RadioSettingValueList(
799
                                  VOXLEVEL_LIST,
800
                                  VOXLEVEL_LIST[_settings.voxlevel]))
801
            basic.append(rs)
802

    
803
            rs = RadioSetting("voxdelay", "VOX delay",
804
                              RadioSettingValueList(
805
                                  VOXDELAY_LIST,
806
                                  VOXDELAY_LIST[_settings.voxdelay]))
807
            basic.append(rs)
808

    
809
        if self.MODEL == "RT22S":
810
            rs = RadioSetting("sidekey2", "Side Key 2(Long)",
811
                              RadioSettingValueList(
812
                                  SIDEKEY2_LIST,
813
                                  SIDEKEY2_LIST[_settings.sidekey2]))
814
            basic.append(rs)
815

    
816
        if self.MODEL == "RB18" or self.MODEL == "RB618":
817
            rs = RadioSetting("language", "Language",
818
                              RadioSettingValueList(
819
                                  VOICE_LIST2,
820
                                  VOICE_LIST2[_settings.language]))
821
            basic.append(rs)
822

    
823
            rs = RadioSetting("tail", "Tail",
824
                              RadioSettingValueBoolean(_settings.tail))
825
            basic.append(rs)
826

    
827
            rs = RadioSetting("hivoltnotx", "High voltage no TX",
828
                              RadioSettingValueBoolean(_settings.hivoltnotx))
829
            basic.append(rs)
830

    
831
            rs = RadioSetting("lovoltnotx", "Low voltage no TX",
832
                              RadioSettingValueBoolean(_settings.lovoltnotx))
833
            basic.append(rs)
834

    
835
            rs = RadioSetting("vox", "VOX",
836
                              RadioSettingValueBoolean(_settings.vox))
837
            basic.append(rs)
838

    
839
            if _settings.vox_level > 4:
840
                val = 1
841
            else:
842
                val = _settings.vox_level + 1
843
            rs = RadioSetting("vox_level", "VOX level",
844
                              RadioSettingValueInteger(1, 5, val))
845
            basic.append(rs)
846

    
847
            rs = RadioSetting("rogerbeep", "Roger beep",
848
                              RadioSettingValueBoolean(_settings.rogerbeep))
849
            basic.append(rs)
850

    
851
        if self.MODEL == "RB85":
852
            rs = RadioSetting("speccode", "SpecCode Select",
853
                              RadioSettingValueList(
854
                                  SPECCODE_LIST,
855
                                  SPECCODE_LIST[_settings.speccode]))
856
            basic.append(rs)
857

    
858
            rs = RadioSetting("sidekey2", "Side Key 1(Short)",
859
                              RadioSettingValueList(
860
                                  SIDEKEY85SHORT_LIST,
861
                                  SIDEKEY85SHORT_LIST[_settings.sidekey2]))
862
            basic.append(rs)
863

    
864
            rs = RadioSetting("sidekey1L", "Side Key 1(Long)",
865
                              RadioSettingValueList(
866
                                  SIDEKEY85LONG_LIST,
867
                                  SIDEKEY85LONG_LIST[_settings.sidekey1L]))
868
            basic.append(rs)
869

    
870
            rs = RadioSetting("sidekey2S", "Side Key 2(Short)",
871
                              RadioSettingValueList(
872
                                  SIDEKEY85SHORT_LIST,
873
                                  SIDEKEY85SHORT_LIST[_settings.sidekey2S]))
874
            basic.append(rs)
875

    
876
            rs = RadioSetting("sidekey2L", "Side Key 2(Long)",
877
                              RadioSettingValueList(
878
                                  SIDEKEY85LONG_LIST,
879
                                  SIDEKEY85LONG_LIST[_settings.sidekey2L]))
880
            basic.append(rs)
881

    
882
            rs = RadioSetting("power10w", "Power 10W",
883
                              RadioSettingValueBoolean(_settings.power10w))
884
            basic.append(rs)
885

    
886
        if self.MODEL == "RB75":
887
            rs = RadioSetting("sidekey2", "Side Key 1(Short)",
888
                              RadioSettingValueList(
889
                                  SIDEKEY75_LIST,
890
                                  SIDEKEY75_LIST[_settings.sidekey2]))
891
            basic.append(rs)
892

    
893
            rs = RadioSetting("sidekey1L", "Side Key 1(Long)",
894
                              RadioSettingValueList(
895
                                  SIDEKEY75_LIST,
896
                                  SIDEKEY75_LIST[_settings.sidekey1L]))
897
            basic.append(rs)
898

    
899
            rs = RadioSetting("sidekey2S", "Side Key 2(Short)",
900
                              RadioSettingValueList(
901
                                  SIDEKEY75_LIST,
902
                                  SIDEKEY75_LIST[_settings.sidekey2S]))
903
            basic.append(rs)
904

    
905
            rs = RadioSetting("sidekey2L", "Side Key 2(Long)",
906
                              RadioSettingValueList(
907
                                  SIDEKEY75_LIST,
908
                                  SIDEKEY75_LIST[_settings.sidekey2L]))
909
            basic.append(rs)
910

    
911
            rs = RadioSetting("power10w", "Low Voltage Stop TX",
912
                              RadioSettingValueBoolean(_settings.power10w))
913
            basic.append(rs)
914

    
915
        if self.MODEL == "RB87":
916
            rs = RadioSetting("sidekey2", "Side Key 1(Long)",
917
                              RadioSettingValueList(
918
                                  SIDEKEY87_LIST,
919
                                  SIDEKEY87_LIST[_settings.sidekey2]))
920
            basic.append(rs)
921

    
922
        if self.MODEL == "FRS-B1":
923
            rs = RadioSetting("settings2.hivoltnotx",
924
                              "High Voltage Inhibit TX",
925
                              RadioSettingValueBoolean(_settings2.hivoltnotx))
926
            basic.append(rs)
927

    
928
            rs = RadioSetting("settings2.lovoltnotx", "Low Voltage Inhibit TX",
929
                              RadioSettingValueBoolean(_settings2.lovoltnotx))
930
            basic.append(rs)
931

    
932
            rs = RadioSetting("settings2.vox", "Vox",
933
                              RadioSettingValueBoolean(_settings2.vox))
934
            basic.append(rs)
935

    
936
            rs = RadioSetting("settings2.voxnotxonrx", "Rx Disable VOX",
937
                              RadioSettingValueBoolean(_settings2.voxnotxonrx))
938
            basic.append(rs)
939

    
940
            rs = RadioSetting("settings2.voxgain", "Vox Gain",
941
                              RadioSettingValueInteger(
942
                                  1, 5, _settings2.voxgain))
943
            basic.append(rs)
944

    
945
        if self.MODEL == "RB19" or self.MODEL == "RB19P" \
946
                or self.MODEL == "RB619":
947
            rs = RadioSetting("sidekey2", "Left Navigation Button(Long)",
948
                              RadioSettingValueList(
949
                                  SIDEKEY19_LIST,
950
                                  SIDEKEY19_LIST[_settings.sidekey2]))
951
            basic.append(rs)
952

    
953
        if self.MODEL == "RT47" or self.MODEL == "RT47V" or \
954
                self.MODEL == "RT647":
955
            rs = RadioSetting("sidekey2", "Side Key 1(Long)",
956
                              RadioSettingValueList(
957
                                  SIDEKEY47_LIST,
958
                                  SIDEKEY47_LIST[_settings.sidekey2]))
959
            basic.append(rs)
960

    
961
            rs = RadioSetting("sidekey2S", "Side Key 2(Long)",
962
                              RadioSettingValueList(
963
                                  SIDEKEY47_LIST,
964
                                  SIDEKEY47_LIST[_settings.sidekey2S]))
965
            basic.append(rs)
966

    
967
        if self.MODEL == "BF-V8A" or self.MODEL == "BF-T20FRS":
968
            rs = RadioSetting("sidekey2", "Side key",
969
                              RadioSettingValueList(
970
                                  SIDEKEYV8A_LIST,
971
                                  SIDEKEYV8A_LIST[_settings.sidekey2]))
972
            basic.append(rs)
973

    
974
            rs = RadioSetting("settings2.rxemergency", "RX emergency",
975
                              RadioSettingValueBoolean(_settings2.rxemergency))
976
            basic.append(rs)
977

    
978
            rs = RadioSetting("settings2.voiceselect", "Language",
979
                              RadioSettingValueList(
980
                                  VOICE_LIST2,
981
                                  VOICE_LIST2[_settings2.voiceselect]))
982
            basic.append(rs)
983

    
984
            rs = RadioSetting("settings2.hivoltnotx",
985
                              "High voltage inhibit TX",
986
                              RadioSettingValueBoolean(_settings2.hivoltnotx))
987
            basic.append(rs)
988

    
989
            rs = RadioSetting("settings2.lovoltnotx", "Low voltage inhibit TX",
990
                              RadioSettingValueBoolean(_settings2.lovoltnotx))
991
            basic.append(rs)
992

    
993
            rs = RadioSetting("settings2.vox", "VOX",
994
                              RadioSettingValueBoolean(_settings2.vox))
995
            basic.append(rs)
996

    
997
            rs = RadioSetting("settings2.voxnotxonrx", "RX disable VOX",
998
                              RadioSettingValueBoolean(_settings2.voxnotxonrx))
999
            basic.append(rs)
1000

    
1001
            rs = RadioSetting("settings2.voxgain", "Vox Gain",
1002
                              RadioSettingValueInteger(
1003
                                  1, 5, _settings2.voxgain))
1004
            basic.append(rs)
1005

    
1006
        if self.MODEL == "RB29" or self.MODEL == "RB629":
1007
            rs = RadioSetting("codesw", "Code Switch",
1008
                              RadioSettingValueBoolean(_settings.codesw))
1009
            basic.append(rs)
1010

    
1011
            rs = RadioSetting("sidekey2S", "Side Key(Short)",
1012
                              RadioSettingValueList(
1013
                                  SIDEKEY29_LIST,
1014
                                  SIDEKEY29_LIST[_settings.sidekey2S]))
1015
            basic.append(rs)
1016

    
1017
            rs = RadioSetting("sidekey2L", "Side Key(Long)",
1018
                              RadioSettingValueList(
1019
                                  SIDEKEY29_LIST,
1020
                                  SIDEKEY29_LIST[_settings.sidekey2L]))
1021
            basic.append(rs)
1022

    
1023
        return top
1024

    
1025
    def set_settings(self, settings):
1026
        for element in settings:
1027
            if not isinstance(element, RadioSetting):
1028
                self.set_settings(element)
1029
                continue
1030
            else:
1031
                try:
1032
                    if "." in element.get_name():
1033
                        bits = element.get_name().split(".")
1034
                        obj = self._memobj
1035
                        for bit in bits[:-1]:
1036
                            obj = getattr(obj, bit)
1037
                        setting = bits[-1]
1038
                    else:
1039
                        obj = self._memobj.settings
1040
                        setting = element.get_name()
1041

    
1042
                    if element.has_apply_callback():
1043
                        LOG.debug("Using apply callback")
1044
                        element.run_apply_callback()
1045
                    elif setting == "vox_level":
1046
                        setattr(obj, setting, int(element.value) - 1)
1047
                    else:
1048
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1049
                        setattr(obj, setting, element.value)
1050
                except Exception:
1051
                    LOG.debug(element.get_name())
1052
                    raise
1053

    
1054
    @classmethod
1055
    def match_model(cls, filedata, filename):
1056
        if cls.MODEL == "T18":
1057
            match_size = False
1058
            match_model = False
1059

    
1060
            # testing the file data size
1061
            if len(filedata) == cls._memsize:
1062
                match_size = True
1063

    
1064
            # testing the model fingerprint
1065
            match_model = model_match(cls, filedata)
1066

    
1067
            if match_size and match_model:
1068
                return True
1069
            else:
1070
                return False
1071
        else:
1072
            # Radios that have always been post-metadata, so never do
1073
            # old-school detection
1074
            return False
1075

    
1076

    
1077
@directory.register
1078
class RT20Radio(T18Radio):
1079
    """RETEVIS RT20"""
1080
    VENDOR = "Retevis"
1081
    MODEL = "RT20"
1082
    ACK_BLOCK = True
1083
    BLOCK_SIZE = 0x08
1084

    
1085
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1086
                    chirp_common.PowerLevel("Low",  watts=0.50)]
1087

    
1088
    _magic = b"8AOGRAM"
1089
    _fingerprint = [b"SMP558" + b"\x02"]
1090
    _upper = 16
1091
    _mem_params = (_upper  # number of channels
1092
                   )
1093

    
1094

    
1095
@directory.register
1096
class RT22SRadio(T18Radio):
1097
    """RETEVIS RT22S"""
1098
    VENDOR = "Retevis"
1099
    MODEL = "RT22S"
1100
    ACK_BLOCK = False
1101

    
1102
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1103
                    chirp_common.PowerLevel("Low",  watts=0.50)]
1104

    
1105
    _magic = b"9COGRAM"
1106
    _fingerprint = [b"SMP558" + b"\x02"]
1107
    _upper = 22
1108
    _mem_params = (_upper  # number of channels
1109
                   )
1110
    _frs = True
1111
    _pmr = False
1112

    
1113

    
1114
@directory.register
1115
class RB18Radio(T18Radio):
1116
    """RETEVIS RB18"""
1117
    VENDOR = "Retevis"
1118
    MODEL = "RB18"
1119
    BLOCK_SIZE = 0x10
1120
    CMD_EXIT = b"E"
1121

    
1122
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1123
                    chirp_common.PowerLevel("Low",  watts=0.50)]
1124

    
1125
    _magic = b"PROGRAL"
1126
    _fingerprint = [b"P3107" + b"\xF7"]
1127
    _upper = 22
1128
    _mem_params = (_upper  # number of channels
1129
                   )
1130
    _frs = True
1131
    _pmr = False
1132

    
1133
    _ranges = [
1134
        (0x0000, 0x0660),
1135
    ]
1136
    _memsize = 0x0660
1137

    
1138
    def process_mmap(self):
1139
        self._memobj = bitwise.parse(MEM_FORMAT_RB18 %
1140
                                     self._mem_params, self._mmap)
1141

    
1142
    @classmethod
1143
    def match_model(cls, filedata, filename):
1144
        # This radio has always been post-metadata, so never do
1145
        # old-school detection
1146
        return False
1147

    
1148

    
1149
@directory.register
1150
class RB618Radio(RB18Radio):
1151
    """RETEVIS RB618"""
1152
    VENDOR = "Retevis"
1153
    MODEL = "RB618"
1154

    
1155
    _upper = 16
1156
    _mem_params = (_upper  # number of channels
1157
                   )
1158
    _frs = False
1159
    _pmr = True
1160

    
1161

    
1162
@directory.register
1163
class RT68Radio(T18Radio):
1164
    """RETEVIS RT68"""
1165
    VENDOR = "Retevis"
1166
    MODEL = "RT68"
1167
    ACK_BLOCK = False
1168
    CMD_EXIT = b""
1169

    
1170
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1171
                    chirp_common.PowerLevel("Low",  watts=0.50)]
1172

    
1173
    _magic = b"83OGRAM"
1174
    _fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00"]
1175
    _upper = 16
1176
    _mem_params = (_upper  # number of channels
1177
                   )
1178
    _frs16 = True
1179
    _pmr = False
1180

    
1181
    @classmethod
1182
    def match_model(cls, filedata, filename):
1183
        # This radio has always been post-metadata, so never do
1184
        # old-school detection
1185
        return False
1186

    
1187

    
1188
@directory.register
1189
class RT668Radio(RT68Radio):
1190
    """RETEVIS RT668"""
1191
    VENDOR = "Retevis"
1192
    MODEL = "RT668"
1193

    
1194
    _frs16 = False
1195
    _pmr = True
1196

    
1197

    
1198
@directory.register
1199
class RB17Radio(RT68Radio):
1200
    """RETEVIS RB17"""
1201
    VENDOR = "Retevis"
1202
    MODEL = "RB17"
1203

    
1204
    _magic = b"A5OGRAM"
1205
    _fingerprint = [b"\x53\x00\x00\x00\x00\x00\x00\x00"]
1206

    
1207
    _frs16 = True
1208
    _pmr = False
1209
    _murs = False
1210

    
1211

    
1212
@directory.register
1213
class RB617Radio(RB17Radio):
1214
    """RETEVIS RB617"""
1215
    VENDOR = "Retevis"
1216
    MODEL = "RB617"
1217

    
1218
    _frs16 = False
1219
    _pmr = True
1220
    _murs = False
1221

    
1222

    
1223
@directory.register
1224
class RB17VRadio(RB17Radio):
1225
    """RETEVIS RB17V"""
1226
    VENDOR = "Retevis"
1227
    MODEL = "RB17V"
1228

    
1229
    VALID_BANDS = [(136000000, 174000000)]
1230

    
1231
    _upper = 5
1232

    
1233
    _frs16 = False
1234
    _pmr = False
1235
    _murs = True
1236

    
1237

    
1238
@directory.register
1239
class RB85Radio(T18Radio):
1240
    """Retevis RB85"""
1241
    VENDOR = "Retevis"
1242
    MODEL = "RB85"
1243
    ACK_BLOCK = False
1244

    
1245
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
1246
                    chirp_common.PowerLevel("Low", watts=5.00)]
1247

    
1248
    _magic = b"H19GRAM"
1249
    _fingerprint = [b"SMP558" + b"\x02"]
1250

    
1251

    
1252
@directory.register
1253
class RB75Radio(T18Radio):
1254
    """Retevis RB75"""
1255
    VENDOR = "Retevis"
1256
    MODEL = "RB75"
1257
    ACK_BLOCK = False
1258

    
1259
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
1260
                    chirp_common.PowerLevel("Low", watts=0.50)]
1261

    
1262
    _magic = b"KVOGRAM"
1263
    _fingerprint = [b"SMP558" + b"\x00"]
1264
    _upper = 30
1265
    _mem_params = (_upper  # number of channels
1266
                   )
1267
    _gmrs = False  # sold as GMRS radio but supports full band TX/RX
1268

    
1269

    
1270
@directory.register
1271
class FRSB1Radio(T18Radio):
1272
    """BTECH FRS-B1"""
1273
    VENDOR = "BTECH"
1274
    MODEL = "FRS-B1"
1275
    ACK_BLOCK = True
1276

    
1277
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1278
                    chirp_common.PowerLevel("Low", watts=0.50)]
1279

    
1280
    _magic = b"PROGRAM"
1281
    _fingerprint = [b"P3107" + b"\xF7\x00"]
1282
    _upper = 22
1283
    _mem_params = (_upper  # number of channels
1284
                   )
1285
    _frs = True
1286

    
1287

    
1288
@directory.register
1289
class RB19Radio(T18Radio):
1290
    """Retevis RB19"""
1291
    VENDOR = "Retevis"
1292
    MODEL = "RB19"
1293
    ACK_BLOCK = False
1294

    
1295
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1296
                    chirp_common.PowerLevel("Low", watts=0.50)]
1297

    
1298
    _magic = b"9COGRAM"
1299
    _fingerprint = [b"SMP558" + b"\x02"]
1300
    _upper = 22
1301
    _mem_params = (_upper  # number of channels
1302
                   )
1303
    _frs = True
1304

    
1305

    
1306
@directory.register
1307
class RB19PRadio(T18Radio):
1308
    """Retevis RB19P"""
1309
    VENDOR = "Retevis"
1310
    MODEL = "RB19P"
1311
    ACK_BLOCK = False
1312

    
1313
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=3.00),
1314
                    chirp_common.PowerLevel("Low", watts=0.50)]
1315

    
1316
    _magic = b"70OGRAM"
1317
    _fingerprint = [b"SMP558" + b"\x02"]
1318
    _upper = 30
1319
    _mem_params = (_upper  # number of channels
1320
                   )
1321
    _gmrs = True
1322

    
1323

    
1324
@directory.register
1325
class RB619Radio(T18Radio):
1326
    """Retevis RB619"""
1327
    VENDOR = "Retevis"
1328
    MODEL = "RB619"
1329
    ACK_BLOCK = False
1330

    
1331
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.500),
1332
                    chirp_common.PowerLevel("Low", watts=0.499)]
1333

    
1334
    _magic = b"9COGRAM"
1335
    _fingerprint = [b"SMP558" + b"\x02"]
1336
    _upper = 16
1337
    _mem_params = (_upper  # number of channels
1338
                   )
1339
    _pmr = True
1340

    
1341

    
1342
@directory.register
1343
class RT47Radio(T18Radio):
1344
    """Retevis RT47"""
1345
    VENDOR = "Retevis"
1346
    MODEL = "RT47"
1347
    ACK_BLOCK = False
1348

    
1349
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
1350
                    chirp_common.PowerLevel("Low", watts=0.500)]
1351

    
1352
    _magic = b"47OGRAM"
1353
    _fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00"]
1354
    _upper = 16
1355
    _mem_params = (_upper  # number of channels
1356
                   )
1357
    _frs16 = True
1358
    _echo = True
1359

    
1360

    
1361
@directory.register
1362
class RT47VRadio(RT47Radio):
1363
    """Retevis RT47V"""
1364
    VENDOR = "Retevis"
1365
    MODEL = "RT47V"
1366

    
1367
    VALID_BANDS = [(136000000, 174000000)]
1368

    
1369
    _upper = 5
1370
    _mem_params = (_upper  # number of channels
1371
                   )
1372
    _frs16 = False
1373
    _murs = True
1374

    
1375

    
1376
@directory.register
1377
class RT647Radio(RT47Radio):
1378
    """Retevis RT647"""
1379
    VENDOR = "Retevis"
1380
    MODEL = "RT647"
1381

    
1382
    _frs16 = False
1383
    _pmr = True
1384

    
1385

    
1386
@directory.register
1387
class BFV8ARadio(T18Radio):
1388
    """Baofeng BF-V8A"""
1389
    VENDOR = "Baofeng"
1390
    MODEL = "BF-V8A"
1391
    ACK_BLOCK = True
1392

    
1393
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
1394
                    chirp_common.PowerLevel("Low", watts=0.500)]
1395

    
1396
    _magic = b"PROGRAM"
1397
    _fingerprint = [b"P3107" + b"\xF7\x00\x00"]
1398
    _upper = 16
1399
    _mem_params = (_upper  # number of channels
1400
                   )
1401
    _echo = False
1402

    
1403

    
1404
@directory.register
1405
class RB29Radio(T18Radio):
1406
    """Retevis RB29"""
1407
    VENDOR = "Retevis"
1408
    MODEL = "RB29"
1409
    ACK_BLOCK = False
1410

    
1411
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1412
                    chirp_common.PowerLevel("Low", watts=0.50)]
1413

    
1414
    _magic = b"S19GRAM"
1415
    _fingerprint = [b"SMP558" + b"\x02"]
1416
    _upper = 16
1417
    _mem_params = (_upper  # number of channels
1418
                   )
1419
    _frs16 = True
1420

    
1421

    
1422
@directory.register
1423
class RB629Radio(RB29Radio):
1424
    """Retevis RB29"""
1425
    VENDOR = "Retevis"
1426
    MODEL = "RB629"
1427

    
1428
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.500),
1429
                    chirp_common.PowerLevel("Low", watts=0.499)]
1430

    
1431
    _frs16 = False
1432
    _pmr = True
1433

    
1434

    
1435
@directory.register
1436
class RT15Radio(T18Radio):
1437
    """RETEVIS RT15"""
1438
    VENDOR = "Retevis"
1439
    MODEL = "RT15"
1440
    ACK_BLOCK = False
1441
    CMD_EXIT = b"b"
1442

    
1443
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
1444
                    chirp_common.PowerLevel("Low",  watts=0.50)]
1445

    
1446
    _magic = b"KAOGRAM"
1447
    _fingerprint = [b"\x06\x00\x00\x00\x00\x00\x00\x00",
1448
                    b"\x06\x03\xE8\x08\xFF\xFF\xFF\xFF"]
1449
    _upper = 16
1450
    _mem_params = (_upper  # number of channels
1451
                   )
1452
    _frs16 = False  # sold as FRS radio but supports full band TX/RX
1453

    
1454
    @classmethod
1455
    def match_model(cls, filedata, filename):
1456
        # This radio has always been post-metadata, so never do
1457
        # old-school detection
1458
        return False
1459

    
1460

    
1461
@directory.register
1462
class RB87Radio(T18Radio):
1463
    """RETEVIS RB87"""
1464
    VENDOR = "Retevis"
1465
    MODEL = "RB87"
1466
    ACK_BLOCK = False
1467
    CMD_EXIT = b""
1468
    ACK_BLOCK = False
1469

    
1470
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
1471
                    chirp_common.PowerLevel("Low", watts=0.50)]
1472

    
1473
    _magic = b"C8OGRAN"
1474
    _fingerprint = [b"SMP558"]
1475
    _upper = 30
1476
    _mem_params = (_upper  # number of channels
1477
                   )
1478
    _gmrs = True
1479

    
1480

    
1481
@directory.register
1482
class BFT20FRSRadio(T18Radio):
1483
    """Baofeng BF-T20FRS"""
1484
    VENDOR = "Baofeng"
1485
    MODEL = "BF-T20FRS"
1486
    ACK_BLOCK = True
1487

    
1488
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.000),
1489
                    chirp_common.PowerLevel("Low", watts=0.500)]
1490

    
1491
    _magic = b"PROGRAM"
1492
    _fingerprint = [b"P3107" + b"\xF7\x00\x00"]
1493
    _upper = 22
1494
    _mem_params = (_upper  # number of channels
1495
                   )
1496
    _frs = True
1497

    
1498
    _ranges = [
1499
        (0x0000, 0x0160),
1500
        (0x02B0, 0x02D0),
1501
    ]
1502
    _memsize = 0x03F0
1503

    
1504
    def process_mmap(self):
1505
        self._memobj = bitwise.parse(MEM_FORMAT_T20FRS %
1506
                                     self._mem_params, self._mmap)
(2-2/2)