Project

General

Profile

Bug #11372 » retevis_ra87.py

Jim Unroe, 06/07/2024 10:06 AM

 
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 (
20
    bitwise,
21
    chirp_common,
22
    directory,
23
    errors,
24
    memmap,
25
    util,
26
)
27
from chirp.settings import (
28
    RadioSetting,
29
    RadioSettingGroup,
30
    RadioSettings,
31
    RadioSettingValueBoolean,
32
    RadioSettingValueInteger,
33
    RadioSettingValueList,
34
    RadioSettingValueString,
35
)
36

    
37
LOG = logging.getLogger(__name__)
38

    
39
MEM_FORMAT = """
40
struct mem {
41
    bbcd rxfreq[5];       // RX Frequency                            // 0-4
42
    u8   step:4,          // STEP                                    // 5
43
         unk1:2,
44
         duplex:2;        // Duplex 0: Simplex, 1: Plus, 2: Minus
45
    u8   unk2:3,                                                     // 6
46
         reverse:1,       // Reverse
47
         unk3:4;
48

    
49
    ul16 rxdtcs_pol:1,                                               // 7-8
50
         unk4:1,
51
         is_rxdigtone:1,
52
         unk5:1,
53
         rxtone:12;
54
    ul16 txdtcs_pol:1,                                               // 9-A
55
         unk6:1,
56
         is_txdigtone:1,
57
         unk7:1,
58
         txtone:12;
59

    
60
    u8   unknown1;                                                   // B
61
    bbcd offset[2];       // Offset 00.05 - 69.95 MHz                // C-D
62
    u8   unknown2;                                                   // E
63
    u8   unk8:7,                                                     // F
64
         narrow:1;        // FM Narrow
65

    
66
    u8   unk9:3,          //                                         // 0
67
         beatshift:1,     // Beat Shift
68
         unk10:4;
69
    bbcd txfreq[4];                                                  // 1-4
70
    u8   unk11:4,                                                    // 5
71
         txstep:4;        // TX STEP
72
    u8   unk12:1,                                                    // 6
73
         txpower:3,       // Power
74
         unk13:4;
75
    u8   unknown3;                                                   // 7
76
    u8   compand:1,       // Compand                                 // 8
77
         scramble:3,      // Scramble
78
         unk14:4;
79
    char name[6];         // Name                                    // 9-E
80
    u8   hide:1,          // Channel Hide 0: Show, 1: Hide           // F
81
         unk15:6,
82
         skip:1;          // Lock Out (skip when scanning)
83
};
84

    
85
// #seekto 0x0000;
86
struct mem left_memory[100];
87

    
88
#seekto 0x0D40;
89
struct mem right_memory[100];
90

    
91
#seekto 0x1C10;
92
struct {
93
    char lower[4];        // 0x1C10 Lower Band Limit
94
    char upper[4];        // 0x1C14 Upper Band Limit
95
    char ponmsg[6];       // 0x1C18 Power-On Message
96
    u8   unknown_1c1e[2]; // 0x1C1E
97
    u8   keyl;            // 0x1C20 Radio Key Lock
98
} settings2;
99

    
100
#seekto 0x1C42;
101
struct {
102
    u16 freq1;            // Scramble Freq 1
103
    u16 freq2;            // Scramble Freq 2
104
    u16 freq3;            // Scramble Freq 3
105
    u16 freq4;            // Scramble Freq 4
106
    u16 freq5;            // Scramble Freq 5
107
    u16 freq6;            // Scramble Freq 6
108
    u16 ufreq;            // Scramble User Freq
109
} scramble;
110

    
111
#seekto 0x1CC0;
112
struct {
113
    u8 unk1:7,            // 0x1CC0
114
       ani:1;             //        ANI
115
    u8 unk2:7,            // 0x1CC1
116
       tend:1;            //        Roger Beep
117
    u8 unknown_1cc2[5];   // 0x1CC2-0x1CC6
118
    u8 unk3:4,            // 0x1CC7
119
       sql:4;             //        Squelch
120
    u8 unk4:4,            // 0x1CC8
121
       sqh:4;             //        Squelch Hang Time
122
    u8 unknown_1cc9;      // 0x1CC9
123
    u8 unk5:7,            // 0x1CCA
124
       relay:1;           //        Relay
125
    u8 unk6:6,            // 0x1CCB
126
       scan:2;            //        Scan Resume Method
127
    u8 unknown_1ccc;      // 0x1CCC
128
    u8 unk7:7,           // 0x1CCD
129
       echo:1;            //        Echo
130
    u8 unknown_1cce;      // 0x1CCE
131
    u8 unk8:7,            // 0x1CCF
132
       mdf:1;             //        Memory Display Format
133
    u8 unk9:5,            // 0x1CD0
134
       apo:3;             //        Automatic Power Off
135
    u8 unk10:7,            // 0x1CD1
136
       ck:1;              //        Call Key
137
    u8 unk11:7,            // 0x1CD2
138
       hdl:1;             //        HDL
139
    u8 unk12:6,           // 0x1CC3
140
       tot:2;             //        Time-Out Timer
141
    u8 unk13:7,           // 0x1CD4
142
       bcl:1;             //        Busy Channel Lockout (global)
143
    u8 unknown_1cd5;      // 0x1CD5
144
    u8 unk14:7,           // 0x1CD6
145
       bp:1;              //        Key Beeps
146
    u8 unk15:7,           // 0x1CD7
147
       bs:1;              //        Beat Frequency Offset
148
    u8 unknown_1cd8;      // 0x1CD8
149
    u8 unk16:7,           // 0x1CD9
150
       enc:1;             //        Tuning Control Knob Enable
151
    u8 unknown_1cda;      // 0x1CDA
152
    u8 unk17:7,           // 0x1CDB
153
       spd:1;             //        DTMF Speed
154
    u8 unk18:7,           // 0x1CDC
155
       dth:1;             //        DTMF Hold
156
    u8 unk19:5,           // 0x1CDD
157
       pa:3;              //        DTMF Pause
158
    u8 unk20:7,           // 0x1CDE
159
       dtl:1;             //        DTMF Lock
160
    u8 unk21:7,           // 0x1CDF
161
       dtm:1;             //        DTMF Sidetone
162
    u8 unknown_1ce0[5];   // 0x1CE0-0x1CE4
163
    u8 unk22:7,           // 0x1CE5
164
       mcl:1;             //        Mic Key Lock
165
    u8 unk23:3,           // 0x1CE6
166
       pf1:5;             //        PF Key 1
167
    u8 unk24:3,           // 0x1CE7
168
       pf2:5;             //        PF Key 2
169
    u8 unk25:3,           // 0x1CE8
170
       pf3:5;             //        PF Key 3
171
    u8 unk26:3,           // 0x1CE9
172
       pf4:5;             //        PF Key 4
173
    u8 unk27:6,           // 0x1CEA
174
       llig:2;            //        LCD Light
175
    u8 unk28:4,           // 0x1CEB
176
       wfclr:4;           //        Background Color - Wait
177
    u8 unk29:4,           // 0x1CEC
178
       rxclr:4;           //        Background Color - RX
179
    u8 unk30:4,           // 0x1CED
180
       txclr:4;           //        Background Color - TX
181
    u8 unk31:4,           // 0x1CEE
182
       contr:4;           //        Contrast
183
    u8 unk32:6,           // 0x1CEF
184
       klig:2;            //        Keypad Light
185
    u8 unknown_1cf0[2];   // 0x1CF0-0x1CF1
186
    u8 unk33:7,           // 0x1CF2
187
       dani:1;            //        DTMF Decode ANI
188
    u8 unk34:4,           // 0x1CF3
189
       pttid:4;           //        PTT ID
190
    u8 unknown_1cf4[8];   // 0x1CF4-0x1CFB
191
    u8 unk35:3,           // 0x1CFC
192
       tvol:5;            //        Roger Beep Volume
193
    u8 unk36:7,           // 0x1CFD
194
       tail:1;            //        Squelch Tail Eliminate
195
} settings;
196

    
197
#seekto 0x1D00;
198
struct {
199
    u8 unknown_1d00[6];   // 0x1D00
200
    char idcode[10];      // 0x1D06 ID Code
201
    u8 unk37:4,           // 0x1D10
202
       grpcode:4;         //        Group Code
203
    u8 art;               // 0x1D11 Auto Reset Time
204
    u8 unknown_1d12[3];   // 0x1D12-0x1D14
205
    char stuncode[10];    // 0x1D15 ID Code
206
    u8 unk38:7,           // 0x1D1F
207
       stuntype:1;        //        Stun Type
208
} dtmfd;
209

    
210
#seekto 0x1D30;
211
struct {
212
  char code[16];          // Autodial Memories
213
} dtmf_codes[10];
214

    
215
//#seekto 0x1DD0;
216
struct {
217
    char bot[16];         // 0x1DD0 Beginning of Transmission
218
    char eot[16];         // 0x1DE0 End of Transmission
219
} dtmfe;
220
"""
221

    
222
CMD_ACK = b"\x06"
223

    
224
TXPOWER_LOW = 0x00
225
TXPOWER_LOW2 = 0x01
226
TXPOWER_LOW3 = 0x02
227
TXPOWER_MID = 0x03
228
TXPOWER_HIGH = 0x04
229

    
230
DUPLEX_NOSPLIT = 0x00
231
DUPLEX_POSSPLIT = 0x01
232
DUPLEX_NEGSPLIT = 0x02
233

    
234
VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
235
DUPLEX = ["", "+", "-"]
236
TUNING_STEPS = [5., 6.25, 10., 12.5, 15., 20., 25., 30., 50., 100.]
237

    
238

    
239
def _enter_programming_mode_download(radio):
240
    serial = radio.pipe
241

    
242
    _magic = radio._magic
243

    
244
    try:
245
        serial.write(_magic)
246
        if radio._echo:
247
            serial.read(len(_magic))  # Chew the echo
248
        ack = serial.read(1)
249
    except Exception:
250
        raise errors.RadioError("Error communicating with radio")
251

    
252
    if not ack:
253
        raise errors.RadioError("No response from radio")
254
    elif ack != CMD_ACK:
255
        raise errors.RadioError("Radio refused to enter programming mode")
256

    
257
    try:
258
        serial.write(b"\x02")
259
        if radio._echo:
260
            serial.read(1)  # Chew the echo
261
        ident = serial.read(8)
262
    except Exception:
263
        raise errors.RadioError("Error communicating with radio")
264

    
265
    # check if ident is OK
266
    for fp in radio._fingerprint:
267
        if ident.startswith(fp):
268
            break
269
    else:
270
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
271
        raise errors.RadioError("Radio identification failed.")
272

    
273
    try:
274
        serial.write(CMD_ACK)
275
        if radio._echo:
276
            serial.read(1)  # Chew the echo
277
        ack = serial.read(1)
278
    except Exception:
279
        raise errors.RadioError("Error communicating with radio")
280

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

    
289
    try:
290
        serial.write(CMD_ACK)
291
        serial.read(1)  # Chew the echo
292
        ack = serial.read(1)
293
    except Exception:
294
        raise errors.RadioError("Error communicating with radio")
295

    
296
    if ack != CMD_ACK:
297
        raise errors.RadioError("Radio refused to enter programming mode")
298

    
299

    
300
def _enter_programming_mode_upload(radio):
301
    serial = radio.pipe
302

    
303
    _magic = radio._magic
304

    
305
    try:
306
        serial.write(_magic)
307
        if radio._echo:
308
            serial.read(len(_magic))  # Chew the echo
309
        ack = serial.read(1)
310
    except Exception:
311
        raise errors.RadioError("Error communicating with radio")
312

    
313
    if not ack:
314
        raise errors.RadioError("No response from radio")
315
    elif ack != CMD_ACK:
316
        raise errors.RadioError("Radio refused to enter programming mode")
317

    
318
    try:
319
        serial.write(b"\x52\x1F\x05\x01")
320
        if radio._echo:
321
            serial.read(4)  # Chew the echo
322
        ident = serial.read(5)
323
    except Exception:
324
        raise errors.RadioError("Error communicating with radio")
325

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

    
330
    try:
331
        serial.write(CMD_ACK)
332
        if radio._echo:
333
            serial.read(1)  # Chew the echo
334
        ack = serial.read(1)
335
    except Exception:
336
        raise errors.RadioError("Error communicating with radio")
337

    
338
    if ack != CMD_ACK:
339
        raise errors.RadioError("Radio refused to enter programming mode")
340

    
341

    
342
def _exit_programming_mode(radio):
343
    serial = radio.pipe
344
    try:
345
        serial.write(radio.CMD_EXIT)
346
        if radio._echo:
347
            serial.read(7)  # Chew the echo
348
    except Exception:
349
        raise errors.RadioError("Radio refused to exit programming mode")
350

    
351

    
352
def _read_block(radio, block_addr, block_size):
353
    serial = radio.pipe
354

    
355
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
356
    expectedresponse = b"W" + cmd[1:]
357
    LOG.debug("Reading block %04x..." % (block_addr))
358

    
359
    try:
360
        serial.write(cmd)
361
        if radio._echo:
362
            serial.read(4)  # Chew the echo
363
        response = serial.read(4 + block_size)
364
        if response[:4] != expectedresponse:
365
            raise Exception("Error reading block %04x." % (block_addr))
366

    
367
        block_data = response[4:]
368

    
369
    except Exception:
370
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
371

    
372
    return block_data
373

    
374

    
375
def _write_block(radio, block_addr, block_size):
376
    serial = radio.pipe
377

    
378
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
379
    data = radio.get_mmap()[block_addr:block_addr + block_size]
380

    
381
    LOG.debug("Writing Data:")
382
    LOG.debug(util.hexprint(cmd + data))
383

    
384
    try:
385
        serial.write(cmd + data)
386
        if radio._echo:
387
            serial.read(4 + len(data))  # Chew the echo
388
        if serial.read(1) != CMD_ACK:
389
            raise Exception("No ACK")
390
    except Exception:
391
        raise errors.RadioError("Failed to send block "
392
                                "to radio at %04x" % block_addr)
393

    
394

    
395
def do_download(radio):
396
    LOG.debug("download")
397
    _enter_programming_mode_download(radio)
398

    
399
    data = b""
400

    
401
    status = chirp_common.Status()
402
    status.msg = "Downloading from radio"
403

    
404
    status.cur = 0
405
    status.max = radio._memsize
406

    
407
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
408
        status.cur = addr + radio.BLOCK_SIZE
409
        radio.status_fn(status)
410

    
411
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
412
        data += block
413

    
414
        LOG.debug("Address: %04x" % addr)
415
        LOG.debug(util.hexprint(block))
416

    
417
    return data
418

    
419

    
420
def do_upload(radio):
421
    status = chirp_common.Status()
422
    status.msg = "Uploading to radio"
423

    
424
    _enter_programming_mode_upload(radio)
425

    
426
    status.cur = 0
427
    status.max = radio._memsize
428

    
429
    for start_addr, end_addr in radio._ranges:
430
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE):
431
            status.cur = addr + radio.BLOCK_SIZE
432
            radio.status_fn(status)
433
            _write_block(radio, addr, radio.BLOCK_SIZE)
434

    
435
    _exit_programming_mode(radio)
436

    
437

    
438
class RA87StyleRadio(chirp_common.CloneModeRadio):
439
    """Retevis RA87"""
440
    VENDOR = "Retevis"
441
    NEEDS_COMPAT_SERIAL = False
442
    BAUD_RATE = 9600
443
    BLOCK_SIZE = 0x40
444
    CMD_EXIT = b"EZ" + b"\xA5" + b"2#E" + b"\xF2"
445
    NAME_LENGTH = 6
446

    
447
    VALID_BANDS = [(400000000, 480000000)]
448

    
449
    _magic = b"PROGRAM"
450
    _fingerprint = [b"\xFF\xFF\xFF\xFF\xFF\xA5\x2C\xFF"]
451
    _upper = 99
452
    _gmrs = True
453
    _echo = True
454

    
455
    _ranges = [
456
        (0x0000, 0x2000),
457
    ]
458
    _memsize = 0x2000
459

    
460
    def get_features(self):
461
        rf = chirp_common.RadioFeatures()
462
        rf.can_odd_split = True
463
        rf.has_bank = False
464
        rf.has_ctone = True
465
        rf.has_cross = True
466
        rf.has_name = True
467
        rf.has_sub_devices = self.VARIANT == ""
468
        rf.has_tuning_step = True
469
        rf.has_rx_dtcs = True
470
        rf.has_settings = True
471
        rf.memory_bounds = (0, self._upper)
472
        rf.valid_bands = self.VALID_BANDS
473
        rf.valid_characters = VALID_CHARS
474
        rf.valid_cross_modes = [
475
            "Tone->Tone",
476
            "DTCS->",
477
            "->DTCS",
478
            "Tone->DTCS",
479
            "DTCS->Tone",
480
            "->Tone",
481
            "DTCS->DTCS"]
482
        rf.valid_duplexes = DUPLEX + ["split"]
483
        rf.valid_power_levels = self.POWER_LEVELS
484
        rf.valid_modes = ["NFM", "FM"]  # 12.5 kHz, 25 kHz.
485
        rf.valid_name_length = self.NAME_LENGTH
486
        rf.valid_skips = ["", "S"]
487
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
488
        rf.valid_tuning_steps = TUNING_STEPS
489
        return rf
490

    
491
    def get_sub_devices(self):
492
        return [RA87RadioLeft(self._mmap), RA87RadioRight(self._mmap)]
493

    
494
    def process_mmap(self):
495
        """Process the mem map into the mem object"""
496
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
497

    
498
    def sync_in(self):
499
        try:
500
            data = do_download(self)
501
            self._mmap = memmap.MemoryMapBytes(data)
502
        except errors.RadioError:
503
            raise
504
        except Exception as e:
505
            LOG.exception('General failure')
506
            raise errors.RadioError('Failed to download from radio: %s' % e)
507
        finally:
508
            _exit_programming_mode(self)
509
        self.process_mmap()
510

    
511
    def sync_out(self):
512
        try:
513
            do_upload(self)
514
        except errors.RadioError:
515
            raise
516
        except Exception as e:
517
            LOG.exception('General failure')
518
            raise errors.RadioError('Failed to upload to radio: %s' % e)
519
        finally:
520
            _exit_programming_mode(self)
521

    
522
    def _get_dcs(self, val):
523
        return int(str(val)[2:-16])
524

    
525
    def _set_dcs(self, val):
526
        return int(str(val), 16)
527

    
528
    def _memory_obj(self, suffix=""):
529
        return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix))
530

    
531
    def get_memory(self, number):
532
        _mem = self._memory_obj()[number]
533

    
534
        mem = chirp_common.Memory()
535

    
536
        mem.number = number
537

    
538
        if _mem.rxfreq.get_raw() == b"\xFF\xFF\xFF\xFF\xFF":
539
            mem.freq = 0
540
            mem.empty = True
541
            return mem
542

    
543
        mem.freq = int(_mem.rxfreq)
544

    
545
        # We'll consider any blank (i.e. 0 MHz frequency) to be empty
546
        if mem.freq == 0:
547
            mem.empty = True
548
            return mem
549

    
550
        if int(_mem.txfreq) != 0:  # DUPLEX_ODDSPLIT
551
            mem.duplex = "split"
552
            mem.offset = int(_mem.txfreq) * 10
553
        elif _mem.duplex == DUPLEX_POSSPLIT:
554
            mem.duplex = '+'
555
            mem.offset = int(_mem.offset) * 1000
556
        elif _mem.duplex == DUPLEX_NEGSPLIT:
557
            mem.duplex = '-'
558
            mem.offset = int(_mem.offset) * 1000
559
        elif _mem.duplex == DUPLEX_NOSPLIT:
560
            mem.duplex = ''
561
            mem.offset = 0
562
        else:
563
            LOG.error('%s: get_mem: unhandled duplex: %02x' %
564
                      (mem.name, _mem.duplex))
565

    
566
        mem.tuning_step = TUNING_STEPS[_mem.step]
567

    
568
        mem.mode = not _mem.narrow and "FM" or "NFM"
569

    
570
        mem.skip = _mem.skip and "S" or ""
571

    
572
        mem.name = str(_mem.name).strip("\xFF")
573

    
574
        dtcs_pol = ["N", "N"]
575

    
576
        if _mem.rxtone == 0xFFF:
577
            rxmode = ""
578
        elif _mem.rxtone == 0x800 and _mem.is_rxdigtone == 0:
579
            rxmode = ""
580
        elif _mem.is_rxdigtone == 0:
581
            # CTCSS
582
            rxmode = "Tone"
583
            mem.ctone = int(_mem.rxtone) / 10.0
584
        else:
585
            # Digital
586
            rxmode = "DTCS"
587
            mem.rx_dtcs = self._get_dcs(_mem.rxtone)
588
            if _mem.rxdtcs_pol == 1:
589
                dtcs_pol[1] = "R"
590

    
591
        if _mem.txtone == 0xFFF:
592
            txmode = ""
593
        elif _mem.txtone == 0x08 and _mem.is_txdigtone == 0:
594
            txmode = ""
595
        elif _mem.is_txdigtone == 0:
596
            # CTCSS
597
            txmode = "Tone"
598
            mem.rtone = int(_mem.txtone) / 10.0
599
        else:
600
            # Digital
601
            txmode = "DTCS"
602
            mem.dtcs = self._get_dcs(_mem.txtone)
603
            if _mem.txdtcs_pol == 1:
604
                dtcs_pol[0] = "R"
605

    
606
        if txmode == "Tone" and not rxmode:
607
            mem.tmode = "Tone"
608
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
609
            mem.tmode = "TSQL"
610
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
611
            mem.tmode = "DTCS"
612
        elif rxmode or txmode:
613
            mem.tmode = "Cross"
614
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
615

    
616
        mem.dtcs_polarity = "".join(dtcs_pol)
617

    
618
        _levels = self.POWER_LEVELS
619
        if _mem.txpower == TXPOWER_HIGH:
620
            mem.power = _levels[4]
621
        elif _mem.txpower == TXPOWER_MID:
622
            mem.power = _levels[3]
623
        elif _mem.txpower == TXPOWER_LOW3:
624
            mem.power = _levels[2]
625
        elif _mem.txpower == TXPOWER_LOW2:
626
            mem.power = _levels[1]
627
        elif _mem.txpower == TXPOWER_LOW:
628
            mem.power = _levels[0]
629
        else:
630
            LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
631
                      (mem.name, _mem.txpower))
632

    
633
        mem.extra = RadioSettingGroup("Extra", "extra")
634
        rs = RadioSettingValueBoolean(_mem.beatshift)
635
        rset = RadioSetting("beatshift", "Beat Shift", rs)
636
        mem.extra.append(rset)
637

    
638
        rs = RadioSettingValueBoolean(_mem.compand)
639
        rset = RadioSetting("compand", "Compand", rs)
640
        mem.extra.append(rset)
641

    
642
        options = ['Off', 'Freq 1', 'Freq 2', 'Freq 3',
643
                   'Freq 4', 'Freq 5', 'Freq 6', 'User']
644
        rs = RadioSettingValueList(options, options[_mem.scramble])
645
        rset = RadioSetting("scramble", "Scramble", rs)
646
        mem.extra.append(rset)
647

    
648
        rs = RadioSettingValueBoolean(_mem.hide)
649
        rset = RadioSetting("hide", "Hide Channel", rs)
650
        mem.extra.append(rset)
651

    
652
        rs = RadioSettingValueBoolean(_mem.reverse)
653
        rset = RadioSetting("reverse", "Reverse", rs)
654
        mem.extra.append(rset)
655

    
656
        return mem
657

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

    
662
        if mem.empty:
663
            _mem.set_raw(b"\xFF" * 31 + b"\x80")
664

    
665
            return
666

    
667
        _mem.set_raw(b"\x00" * 25 + b"\xFF" * 6 + b"\x00")
668

    
669
        _mem.rxfreq = mem.freq
670

    
671
        if mem.duplex == 'split':
672
            _mem.txfreq = mem.offset / 10
673
        elif mem.duplex == '+':
674
            _mem.duplex = DUPLEX_POSSPLIT
675
            _mem.offset = mem.offset / 1000
676
        elif mem.duplex == '-':
677
            _mem.duplex = DUPLEX_NEGSPLIT
678
            _mem.offset = mem.offset / 1000
679
        elif mem.duplex == '':
680
            _mem.duplex = DUPLEX_NOSPLIT
681
        else:
682
            LOG.error('%s: set_mem: unhandled duplex: %s' %
683
                      (mem.name, mem.duplex))
684

    
685
        rxmode = ""
686
        txmode = ""
687

    
688
        if mem.tmode == "Tone":
689
            txmode = "Tone"
690
        elif mem.tmode == "TSQL":
691
            rxmode = "Tone"
692
            txmode = "TSQL"
693
        elif mem.tmode == "DTCS":
694
            rxmode = "DTCSSQL"
695
            txmode = "DTCS"
696
        elif mem.tmode == "Cross":
697
            txmode, rxmode = mem.cross_mode.split("->", 1)
698

    
699
        if rxmode == "":
700
            _mem.rxdtcs_pol = 0
701
            _mem.is_rxdigtone = 0
702
            _mem.rxtone = 0x800
703
        elif rxmode == "Tone":
704
            _mem.rxdtcs_pol = 0
705
            _mem.is_rxdigtone = 0
706
            _mem.rxtone = int(mem.ctone * 10)
707
        elif rxmode == "DTCSSQL":
708
            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
709
            _mem.is_rxdigtone = 1
710
            _mem.rxtone = self._set_dcs(mem.dtcs)
711
        elif rxmode == "DTCS":
712
            _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0
713
            _mem.is_rxdigtone = 1
714
            _mem.rxtone = self._set_dcs(mem.rx_dtcs)
715

    
716
        if txmode == "":
717
            _mem.txdtcs_pol = 0
718
            _mem.is_txdigtone = 0
719
            _mem.txtone = 0x08
720
        elif txmode == "Tone":
721
            _mem.txdtcs_pol = 0
722
            _mem.is_txdigtone = 0
723
            _mem.txtone = int(mem.rtone * 10)
724
        elif txmode == "TSQL":
725
            _mem.txdtcs_pol = 0
726
            _mem.is_txdigtone = 0
727
            _mem.txtone = int(mem.ctone * 10)
728
        elif txmode == "DTCS":
729
            _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0
730
            _mem.is_txdigtone = 1
731
            _mem.txtone = self._set_dcs(mem.dtcs)
732

    
733
        # name TAG of the channel
734
        _mem.name = mem.name.rstrip().ljust(6, "\xFF")
735

    
736
        _levels = self.POWER_LEVELS
737
        if mem.power is None:
738
            _mem.txpower = TXPOWER_LOW
739
        elif mem.power == _levels[0]:
740
            _mem.txpower = TXPOWER_LOW
741
        elif mem.power == _levels[1]:
742
            _mem.txpower = TXPOWER_LOW2
743
        elif mem.power == _levels[2]:
744
            _mem.txpower = TXPOWER_LOW3
745
        elif mem.power == _levels[3]:
746
            _mem.txpower = TXPOWER_MID
747
        elif mem.power == _levels[4]:
748
            _mem.txpower = TXPOWER_HIGH
749
        else:
750
            LOG.error('%s: set_mem: unhandled power level: %s' %
751
                      (mem.name, mem.power))
752

    
753
        _mem.narrow = 'N' in mem.mode
754
        _mem.skip = mem.skip == "S"
755
        _mem.step = TUNING_STEPS.index(mem.tuning_step)
756

    
757
        for setting in mem.extra:
758
            setattr(_mem, setting.get_name(), int(setting.value))
759

    
760
    def get_settings(self):
761
        _dtmfe = self._memobj.dtmfe
762
        _dtmfd = self._memobj.dtmfd
763
        _scramble = self._memobj.scramble
764
        _settings = self._memobj.settings
765
        _settings2 = self._memobj.settings2
766
        basic = RadioSettingGroup("basic", "Basic Settings")
767
        pfkey = RadioSettingGroup("pfkey", "PF Key Settings")
768
        scramble = RadioSettingGroup("scramble", "Scramble Settings")
769
        lcd = RadioSettingGroup("lcd", "LCD Settings")
770

    
771
        dtmf_enc = RadioSettingGroup("dtmfenc", "Encode")
772
        dtmf_dec = RadioSettingGroup("dtmfdec", "Decode")
773
        dtmf_autodial = RadioSettingGroup("dtmfautodial", "Auto Dial")
774

    
775
        group_dtmf = RadioSettingGroup("group_dtmf", "DTMF Settings")
776
        group_dtmf.append(dtmf_enc)
777
        group_dtmf.append(dtmf_dec)
778
        group_dtmf.append(dtmf_autodial)
779

    
780
        top = RadioSettings(basic, pfkey, scramble, lcd, group_dtmf)
781

    
782
        # menu 08 - SQL
783
        options = ["Off"] + ["S%s" % x for x in range(0, 8)]
784
        rs = RadioSettingValueList(options, options[_settings.sql])
785
        rset = RadioSetting("sql", "S-Meter Squelch Level", rs)
786
        rset.set_doc("Menu 8 (Off, S1, S2, S3, S4, S5, S6, S7)")
787
        basic.append(rset)
788

    
789
        # menu 09 - SQH
790
        options = ["Off", "125", "250", "500"]
791
        rs = RadioSettingValueList(options, options[_settings.sqh])
792
        rset = RadioSetting("sqh", "Squelch Hang Time [ms]", rs)
793
        rset.set_doc("Menu 9 (Off, 125, 250, 500)")
794
        basic.append(rset)
795

    
796
        # menu 11 - RELAY
797
        rs = RadioSettingValueBoolean(_settings.relay)
798
        rset = RadioSetting("relay", "Relay", rs)
799
        rset.set_doc("Menu 11")
800
        basic.append(rset)
801

    
802
        # menu 12 - SCAN
803
        options = ["Time Operated (TO)", "Carrier Operated (CO)",
804
                   "SEarch (SE)"]
805
        rs = RadioSettingValueList(options, options[_settings.scan])
806
        rset = RadioSetting("scan", "Scan Resume Method", rs)
807
        rset.set_doc("Menu 12")
808
        basic.append(rset)
809

    
810
        # menu 14 - ECHO
811
        options = ["Auto (S/RX)", "Manual (D/RX)"]
812
        rs = RadioSettingValueList(options, options[_settings.echo])
813
        rset = RadioSetting("echo", "Response Mode", rs)
814
        rset.set_doc("Menu 14")
815
        basic.append(rset)
816

    
817
        # menu 16 - MDF
818
        options = ["Name", "Frequency"]
819
        rs = RadioSettingValueList(options, options[_settings.mdf])
820
        rset = RadioSetting("mdf", "Memory Display Format", rs)
821
        rset.set_doc("Menu 16")
822
        basic.append(rset)
823

    
824
        # menu 17 - APO
825
        options = ["Off", "30", "60", "90", "120", "180"]
826
        rs = RadioSettingValueList(options, options[_settings.apo])
827
        rset = RadioSetting("apo", "Automaitc Power Off [min]", rs)
828
        rset.set_doc("Menu 17")
829
        basic.append(rset)
830

    
831
        # menu 18 - CK
832
        options = ["CALL", "1750"]
833
        rs = RadioSettingValueList(options, options[_settings.ck])
834
        rset = RadioSetting("ck", "CALL Key", rs)
835
        rset.set_doc("Menu 18")
836
        basic.append(rset)
837

    
838
        # menu 19 - HDL
839
        rs = RadioSettingValueBoolean(_settings.hdl)
840
        rset = RadioSetting("hdl", "1750 Hz Tone Hold", rs)
841
        rset.set_doc("Menu 19")
842
        basic.append(rset)
843

    
844
        # menu 20 - TOT
845
        options = ["3", "5", "10"]
846
        rs = RadioSettingValueList(options, options[_settings.tot])
847
        rset = RadioSetting("tot", "Time-Out Timer [min]", rs)
848
        rset.set_doc("Menu 20")
849
        basic.append(rset)
850

    
851
        # menu 21 - BCL
852
        rs = RadioSettingValueBoolean(_settings.bcl)
853
        rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
854
        rset.set_doc("Menu 21")
855
        basic.append(rset)
856

    
857
        def _filter(name):
858
            filtered = ""
859
            for char in str(name):
860
                if char in VALID_CHARS:
861
                    filtered += char
862
                else:
863
                    filtered += " "
864
            return filtered
865

    
866
        # menu 22 - P.ON.MSG
867
        name = str(_settings2.ponmsg).strip("\xFF")
868
        rs = RadioSettingValueString(0, 6, _filter(name))
869
        rs.set_charset(VALID_CHARS)
870
        rset = RadioSetting("settings2.ponmsg", "Power On Message", rs)
871
        rset.set_doc("Menu 22")
872
        basic.append(rset)
873

    
874
        # menu 23 - BP
875
        rs = RadioSettingValueBoolean(_settings.bp)
876
        rset = RadioSetting("bp", "Key Beeps", rs)
877
        rset.set_doc("Menu 23")
878
        basic.append(rset)
879

    
880
        # menu 24 - BS
881
        rs = RadioSettingValueBoolean(_settings.bs)
882
        rset = RadioSetting("bs", "Beat Frequency Offset", rs)
883
        rset.set_doc("Menu 24")
884
        basic.append(rset)
885

    
886
        # menu 26 - ENC
887
        rs = RadioSettingValueBoolean(_settings.enc)
888
        rset = RadioSetting("enc", "Tuning Control Enable", rs)
889
        rset.set_doc("Menu 26")
890
        basic.append(rset)
891

    
892
        # menu 38 - MC.L
893
        rs = RadioSettingValueBoolean(_settings.mcl)
894
        rset = RadioSetting("mcl", "Mic Key Lock", rs)
895
        basic.append(rset)
896

    
897
        # menu 51 - ANI
898
        rs = RadioSettingValueBoolean(_settings.ani)
899
        rset = RadioSetting("ani", "ANI", rs)
900
        basic.append(rset)
901

    
902
        # menu 60 - TEND
903
        rs = RadioSettingValueBoolean(_settings.tend)
904
        rset = RadioSetting("tend", "Roger Beep", rs)
905
        basic.append(rset)
906

    
907
        # menu 61 - TVOL
908
        rs = RadioSettingValueInteger(1, 25, _settings.tvol + 1)
909
        rset = RadioSetting("tvol", "Roger Beep Volume", rs)
910
        basic.append(rset)
911

    
912
        # menu 62 - TAIL
913
        rs = RadioSettingValueBoolean(_settings.tail)
914
        rset = RadioSetting("tail", "Squelch Tail Eliminate", rs)
915
        basic.append(rset)
916

    
917
        # Other
918
        rs = RadioSettingValueBoolean(_settings2.keyl)
919
        rset = RadioSetting("settings2.keyl", "Radio Key Lock", rs)
920
        basic.append(rset)
921

    
922
        name = str(_settings2.lower).strip("\xFF")
923
        rs = RadioSettingValueString(0, 4, _filter(name))
924
        rs.set_mutable(False)
925
        rset = RadioSetting("settings2.lower", "Lower Band Limit", rs)
926
        basic.append(rset)
927

    
928
        name = str(_settings2.upper).strip("\xFF")
929
        rs = RadioSettingValueString(0, 4, _filter(name))
930
        rs.set_mutable(False)
931
        rset = RadioSetting("settings2.upper", "Upper Band Limit", rs)
932
        basic.append(rset)
933

    
934
        # PF Key Options
935
        options = ["MONI", "ENTER", "1750", "VFO", "MR", "CALL", "MHZ", "REV",
936
                   "SQL", "M-V", "M.IN", "C IN", "MENU", "SHIFT", "LOW",
937
                   "CONTR", "LOCK", "STEP"]
938
        # menu 39: - PF 1
939
        rs = RadioSettingValueList(options, options[_settings.pf1])
940
        rset = RadioSetting("pf1", "PF Key 1", rs)
941
        pfkey.append(rset)
942

    
943
        # menu 40: - PF 2
944
        rs = RadioSettingValueList(options, options[_settings.pf2])
945
        rset = RadioSetting("pf2", "PF Key 2", rs)
946
        pfkey.append(rset)
947

    
948
        # menu 41: - PF 3
949
        rs = RadioSettingValueList(options, options[_settings.pf3])
950
        rset = RadioSetting("pf3", "PF Key 3", rs)
951
        pfkey.append(rset)
952

    
953
        # menu 42: - PF 4
954
        rs = RadioSettingValueList(options, options[_settings.pf4])
955
        rset = RadioSetting("pf4", "PF Key 4", rs)
956
        pfkey.append(rset)
957

    
958
        # Scramble
959
        tmpval = int(_scramble.freq1)
960
        if tmpval > 3800 or tmpval < 2700:
961
            tmpval = 3000
962
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
963
        rset = RadioSetting("scramble.freq1", "Freq 1", rs)
964
        scramble.append(rset)
965

    
966
        tmpval = int(_scramble.freq2)
967
        if tmpval > 3800 or tmpval < 2700:
968
            tmpval = 3100
969
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
970
        rset = RadioSetting("scramble.freq2", "Freq 2", rs)
971
        scramble.append(rset)
972

    
973
        tmpval = int(_scramble.freq3)
974
        if tmpval > 3800 or tmpval < 2700:
975
            tmpval = 3200
976
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
977
        rset = RadioSetting("scramble.freq3", "Freq 3", rs)
978
        scramble.append(rset)
979

    
980
        tmpval = int(_scramble.freq4)
981
        if tmpval > 3800 or tmpval < 2700:
982
            tmpval = 3300
983
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
984
        rset = RadioSetting("scramble.freq4", "Freq 4", rs)
985
        scramble.append(rset)
986

    
987
        tmpval = int(_scramble.freq5)
988
        if tmpval > 3800 or tmpval < 2700:
989
            tmpval = 3400
990
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
991
        rset = RadioSetting("scramble.freq5", "Freq 5", rs)
992
        scramble.append(rset)
993

    
994
        tmpval = int(_scramble.freq6)
995
        if tmpval > 3800 or tmpval < 2700:
996
            tmpval = 3450
997
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
998
        rset = RadioSetting("scramble.freq6", "Freq 6", rs)
999
        scramble.append(rset)
1000

    
1001
        tmpval = int(_scramble.ufreq)
1002
        if tmpval > 3800 or tmpval < 2700:
1003
            tmpval = 3300
1004
        rs = RadioSettingValueInteger(2700, 3800, tmpval, 10)
1005
        rset = RadioSetting("scramble.ufreq", "User Freq", rs)
1006
        scramble.append(rset)
1007

    
1008
        # LCD Display
1009
        # menu 43 - L.LIG
1010
        options = ["Off", "On", "Auto"]
1011
        rs = RadioSettingValueList(options, options[_settings.llig])
1012
        rset = RadioSetting("llig", "LCD Light", rs)
1013
        lcd.append(rset)
1014

    
1015
        # menu 44 - WF.CLR
1016
        rs = RadioSettingValueInteger(1, 8, _settings.wfclr + 1)
1017
        rset = RadioSetting("wfclr", "Background Color - Standby", rs)
1018
        lcd.append(rset)
1019

    
1020
        # menu 45 - RX.CLR
1021
        rs = RadioSettingValueInteger(1, 8, _settings.rxclr + 1)
1022
        rset = RadioSetting("rxclr", "Background Color - RX", rs)
1023
        lcd.append(rset)
1024

    
1025
        # menu 46 - TX.CLR
1026
        rs = RadioSettingValueInteger(1, 8, _settings.txclr + 1)
1027
        rset = RadioSetting("txclr", "Background Color - TX", rs)
1028
        lcd.append(rset)
1029

    
1030
        # menu 47 - CONTR
1031
        rs = RadioSettingValueInteger(0, 3, _settings.contr)
1032
        rset = RadioSetting("contr", "LCD Contrast", rs)
1033
        lcd.append(rset)
1034

    
1035
        # menu 48 - K.LIG
1036
        options = ["Off", "On", "Auto"]
1037
        rs = RadioSettingValueList(options, options[_settings.klig])
1038
        rset = RadioSetting("klig", "Keypad Light", rs)
1039
        lcd.append(rset)
1040

    
1041
        # DTMF
1042
        LIST_DTMF_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7",
1043
                            "8", "9", "A", "B", "C", "D", "*", "#"]
1044
        LIST_DTMF_VALUES = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
1045
                            0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46]
1046
        CHARSET_DTMF_DIGITS = "0123456789AaBbCcDd#*"
1047
        CHARSET_NUMERIC = "0123456789"
1048

    
1049
        def apply_dmtf_frame(setting, obj, len_obj):
1050
            LOG.debug("Setting DTMF-Code: " + str(setting.value))
1051
            val_string = str(setting.value)
1052
            for i in range(0, len_obj):
1053
                obj[i] = 255
1054
            i = 0
1055
            for current_char in val_string:
1056
                current_char = current_char.upper()
1057
                index = LIST_DTMF_DIGITS.index(current_char)
1058
                obj[i] = LIST_DTMF_VALUES[index]
1059
                i = i + 1
1060

    
1061
        # DTMF - Encode
1062
        # menu 52 - PTTID
1063
        options = ["Off", "BOT", "EOT", "Both"]
1064
        rs = RadioSettingValueList(options, options[_settings.pttid])
1065
        rset = RadioSetting("pttid", "When to send PTT ID", rs)
1066
        dtmf_enc.append(rset)
1067

    
1068
        tmp = (str(_dtmfe.bot)
1069
               .strip("\xFF")
1070
               .replace('E', '*')
1071
               .replace('F', '#')
1072
               )
1073
        rs = RadioSettingValueString(0, 16, tmp, False, CHARSET_DTMF_DIGITS)
1074
        rset = RadioSetting("dtmfe.bot", "BOT PTT-ID", rs)
1075
        rset.set_apply_callback(apply_dmtf_frame, _dtmfe.bot, 16)
1076
        dtmf_enc.append(rset)
1077

    
1078
        tmp = (str(_dtmfe.eot)
1079
               .strip("\xFF")
1080
               .replace('E', '*')
1081
               .replace('F', '#')
1082
               )
1083
        rs = RadioSettingValueString(0, 16, tmp, False, CHARSET_DTMF_DIGITS)
1084
        rset = RadioSetting("dtmfe.eot", "EOT PTT-ID", rs)
1085
        rset.set_apply_callback(apply_dmtf_frame, _dtmfe.eot, 16)
1086
        dtmf_enc.append(rset)
1087

    
1088
        # menu 32 - DT.M
1089
        rs = RadioSettingValueBoolean(_settings.dtm)
1090
        rset = RadioSetting("dtm", "DTMF Sidetone", rs)
1091
        dtmf_enc.append(rset)
1092

    
1093
        # menu 31 - DT.L
1094
        rs = RadioSettingValueBoolean(_settings.dtl)
1095
        rset = RadioSetting("dtl", "DTMF Key Lock", rs)
1096
        rset.set_doc("Menu 31")
1097
        dtmf_enc.append(rset)
1098

    
1099
        # menu 29 - DT.H
1100
        rs = RadioSettingValueBoolean(_settings.dth)
1101
        rset = RadioSetting("dth", "DTMF Hold", rs)
1102
        rset.set_doc("Menu 29")
1103
        dtmf_enc.append(rset)
1104

    
1105
        # menu 30 - PA
1106
        options = ["100", "250", "500", "750", "1000", "1500", "2000"]
1107
        rs = RadioSettingValueList(options, options[_settings.pa])
1108
        rset = RadioSetting("pa", "DTMF Pause [ms]", rs)
1109
        rset.set_doc("Menu 30")
1110
        dtmf_enc.append(rset)
1111

    
1112
        # menu 28 - SPD
1113
        options = ["Fast", "Slow"]
1114
        rs = RadioSettingValueList(options, options[_settings.spd])
1115
        rset = RadioSetting("spd", "DTMF Speed", rs)
1116
        rset.set_doc("Menu 28")
1117
        dtmf_enc.append(rset)
1118

    
1119
        # DTMF - Decode
1120
        tmp = (str(_dtmfd.idcode)
1121
               .strip("\xFF")
1122
               )
1123
        rs = RadioSettingValueString(0, 16, tmp, False, CHARSET_NUMERIC)
1124
        rset = RadioSetting("dtmfd.idcode", "ID Code", rs)
1125
        rset.set_apply_callback(apply_dmtf_frame, _dtmfd.idcode, 10)
1126
        dtmf_dec.append(rset)
1127

    
1128
        #
1129
        options = ["Off", "A", "B", "C", "D", "*", "#"]
1130
        rs = RadioSettingValueList(options, options[_dtmfd.grpcode])
1131
        rset = RadioSetting("dtmfd.grpcode", "Group Code", rs)
1132
        dtmf_dec.append(rset)
1133

    
1134
        #
1135
        options = ["Off"] + ["%s" % x for x in range(1, 251)]
1136
        rs = RadioSettingValueList(options, options[_dtmfd.art])
1137
        rset = RadioSetting("dtmfd.art", "Auto Reset Time[s]", rs)
1138
        dtmf_dec.append(rset)
1139

    
1140
        #
1141
        rs = RadioSettingValueBoolean(_settings.dani)
1142
        rset = RadioSetting("dani", "ANI", rs)
1143
        dtmf_dec.append(rset)
1144

    
1145
        tmp = (str(_dtmfd.stuncode)
1146
               .strip("\xFF")
1147
               )
1148
        rs = RadioSettingValueString(0, 16, tmp, False, CHARSET_NUMERIC)
1149
        rset = RadioSetting("dtmfd.stuncode", "Stun Code", rs)
1150
        rset.set_apply_callback(apply_dmtf_frame, _dtmfd.stuncode, 10)
1151
        dtmf_dec.append(rset)
1152

    
1153
        #
1154
        options = ["TX Inhibit", "TX/RX Inhibit"]
1155
        rs = RadioSettingValueList(options, options[_dtmfd.stuntype])
1156
        rset = RadioSetting("dtmfd.stuntype", "Stun Type", rs)
1157
        dtmf_dec.append(rset)
1158

    
1159
        # DTMF - Autodial Memory
1160
        codes = self._memobj.dtmf_codes
1161
        i = 1
1162
        for dtmfcode in codes:
1163
            tmp = (str(dtmfcode.code)
1164
                   .strip("\xFF")
1165
                   .replace('E', '*')
1166
                   .replace('F', '#')
1167
                   )
1168
            rs = RadioSettingValueString(0, 16, tmp, False,
1169
                                         CHARSET_DTMF_DIGITS)
1170
            rset = RadioSetting("dtmf_code_" + str(i) + "_code",
1171
                                "Code " + str(i-1), rs)
1172
            rset.set_apply_callback(apply_dmtf_frame, dtmfcode.code, 16)
1173
            dtmf_autodial.append(rset)
1174
            i = i + 1
1175

    
1176
        return top
1177

    
1178
    def set_settings(self, settings):
1179
        for element in settings:
1180
            if not isinstance(element, RadioSetting):
1181
                self.set_settings(element)
1182
                continue
1183
            else:
1184
                try:
1185
                    if "." in element.get_name():
1186
                        bits = element.get_name().split(".")
1187
                        obj = self._memobj
1188
                        for bit in bits[:-1]:
1189
                            obj = getattr(obj, bit)
1190
                        setting = bits[-1]
1191
                    else:
1192
                        obj = self._memobj.settings
1193
                        setting = element.get_name()
1194

    
1195
                    if element.has_apply_callback():
1196
                        LOG.debug("Using apply callback")
1197
                        element.run_apply_callback()
1198
                    elif setting == "line":
1199
                        setattr(obj, setting, str(element.value).rstrip(
1200
                            " ").ljust(6, "\xFF"))
1201
                    elif setting == "bot":
1202
                        setattr(obj, setting, str(element.value).rstrip(
1203
                            " ").ljust(16, "\xFF"))
1204
                    elif setting == "eot":
1205
                        setattr(obj, setting, str(element.value).rstrip(
1206
                            " ").ljust(16, "\xFF"))
1207
                    elif setting == "wfclr":
1208
                        setattr(obj, setting, int(element.value) - 1)
1209
                    elif setting == "rxclr":
1210
                        setattr(obj, setting, int(element.value) - 1)
1211
                    elif setting == "txclr":
1212
                        setattr(obj, setting, int(element.value) - 1)
1213
                    elif setting == "tvol":
1214
                        setattr(obj, setting, int(element.value) - 1)
1215
                    else:
1216
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1217
                        setattr(obj, setting, element.value)
1218
                except Exception:
1219
                    LOG.debug(element.get_name())
1220
                    raise
1221

    
1222
    @classmethod
1223
    def match_model(cls, filedata, filename):
1224
        # This radio has always been post-metadata, so never do
1225
        # old-school detection
1226
        return False
1227

    
1228

    
1229
@directory.register
1230
class RA87Radio(RA87StyleRadio):
1231
    """Retevis RA87"""
1232
    MODEL = "RA87"
1233

    
1234
    POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
1235
                    chirp_common.PowerLevel("Low2", watts=10.00),
1236
                    chirp_common.PowerLevel("Low3", watts=15.00),
1237
                    chirp_common.PowerLevel("Mid", watts=20.00),
1238
                    chirp_common.PowerLevel("High", watts=40.00)]
1239

    
1240

    
1241
class RA87RadioLeft(RA87Radio):
1242
    """Retevis RA87 Left VFO subdevice"""
1243
    VARIANT = "Left"
1244
    _vfo = "left"
1245

    
1246

    
1247
class RA87RadioRight(RA87Radio):
1248
    """Retevis RA87 Right VFO subdevice"""
1249
    VARIANT = "Right"
1250
    _vfo = "right"
(2-2/3)