Project

General

Profile

Bug #10915 » mml_jc8810_rt470x-v2.1.py

Jim Unroe, 11/07/2023 07:11 PM

 
1
# Copyright 2023 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 logging
17
import struct
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
#seekto 0x0000;
41
struct {
42
  lbcd rxfreq[4];     // 0-3
43
  lbcd txfreq[4];     // 4-7
44
  ul16 rxtone;        // 8-9
45
  ul16 txtone;        // A-B
46
  u8 unknown1:4,      // C
47
     scode:4;         //     Signaling
48
  u8 unknown2:6,      // D
49
     pttid:2;         //     PTT-ID
50
  u8 unknown3:6,      // E
51
     txpower:2;       //     Power Level  0 = H, 1 = L, 2 = M
52
  u8 unknown4:1,      // F
53
     narrow:1,        //     Bandwidth  0 = Wide, 1 = Narrow
54
     encrypt:2,       //     Encrypt
55
     bcl:1,           //     BCL
56
     scan:1,          //     Scan  0 = Skip, 1 = Scan
57
     unknown5:1,      //
58
     learning:1;      //     Learning
59
  lbcd code[3];       // 0-2 Code
60
  u8 unknown6;        // 3
61
  char name[12];      // 4-F 12-character Alpha Tag
62
} memory[%d];
63

    
64
#seekto 0x9000;
65
struct {
66
  u8 unused:4,        // 9000
67
     sql:4;           //      Squelch Level
68
  u8 unused_9001:6,   // 9001
69
     save:2;          //      Battery Save
70
  u8 unused_9002:4,   // 9002
71
     vox:4;           //      VOX Level
72
  u8 unused_9003:4,   // 9003
73
     abr:4;           //      Auto BackLight
74
  u8 unused_9004:7,   // 9004
75
     tdr:1;           //      TDR
76
  u8 unused:5,        // 9005
77
     tot:3;           //      Time-out Timer
78
  u8 unused_9006:7,   // 9006
79
     beep:1;          //      Beep
80
  u8 unused_9007:7,   // 9007
81
     voice:1;         //      Voice Prompt
82
  u8 unused_9008:7,   // 9008
83
     language:1;      //      Language
84
  u8 unused_9009:6,   // 9009
85
     dtmfst:2;        //      DTMF ST
86
  u8 unused_900a:6,   // 900A
87
     screv:2;         //      Scan Mode
88
  // u8 unknown_900b;    // 900B
89
  u8 unused_900B:4,   // 900B
90
     pttid:4;         //      PTT-ID
91
  u8 unused_900c:5,   // 900C
92
     pttlt:3;         //      PTT Delay
93
  u8 unused_900d:6,   // 900D
94
     mdfa:2;          //      Channel_A Display
95
  u8 unused_9003:6,   // 900E
96
     mdfb:2;          //      Channel_B Display
97
  u8 unknown_900f;    // 900F
98
  u8 unused_9010:4,   // 9010
99
     autolk:4;        //      Key Auto Lock
100
  u8 unused_9011:6,   // 9011
101
     almode:2;        //      Alarm Mode
102
  u8 unused_9012:7,   // 9012
103
     alarmsound:1;    //      Alarm Sound
104
  u8 unused_9013:6,   // 9013
105
     dualtx:2;        //      Dual TX
106
  u8 unused_9014:7,   // 9014
107
     ste:1;           //      Tail Noise Clear
108
  u8 unused_9015:4,   // 9015
109
     rpste:4;         //      RPT Noise Clear
110
  u8 unused_9016:4,   // 9016
111
     rptrl:4;         //      RPT Noise Delay
112
  u8 unused_9017:6,   // 9017
113
     roger:2;         //      Roger
114
  u8 unknown_9018;    // 9018
115
  u8 unused_9019:7,   // 9019
116
     fmradio:1;       //      FM Radio
117
  u8 unused_901a1:3,  // 901A
118
     vfomrb:1,        //      WorkMode B
119
     unused_901a2:3,  //
120
     vfomra:1;        //      WorkMode A
121
  u8 unused_901b:7,   // 901B
122
     kblock:1;        //      KB Lock
123
  u8 unused_901c:7,   // 901C
124
     ponmsg:1;        //      Power On Msg
125
  u8 unknown_901d;    // 901D
126
  u8 unused_901e:6,   // 901E
127
     tone:2;          //      Pilot
128
  u8 unknown_901f;    // 901F
129
  u8 unused_9020:4,   // 9020
130
     voxd:4;          //      VOX Delay
131
  u8 unused_9021:4,   // 9021
132
     menuquit:4;      //      Menu Auto Quit
133
  u8 unused_9022:7,   // 9022
134
     tailcode:1;      //      Tail Code (RT-470L)
135
  u8 unknown_9023;    // 9023
136
  u8 unknown_9024;    // 9024
137
  u8 unknown_9025;    // 9025
138
  u8 unknown_9026;    // 9026
139
  u8 unknown_9027;    // 9027
140
  u8 unknown_9028;    // 9028
141
  u8 unused_9029:6,   // 9029
142
     qtsave:2;        //      QT Save Type
143
  u8 ani;             // 902A ANI
144
  u8 skey2_sp;        // 902B Skey2 Short
145
  u8 skey2_lp;        // 902C Skey2 Long
146
  u8 skey3_sp;        // 902D Skey3 Short
147
  u8 topkey_sp;       // 902E Top Key (RT-470L)
148
  u8 unused_902f:6,   // 902F
149
     rxendtail:2;     //      RX END TAIL (RT-470)
150
                      //      TAIL PHASE (A36plus)
151
  u8 skey3_lp;        // 9030 Skey3 Long (RT-470L)
152
                      //      RX END TAIL (A36plus)
153
} settings;
154

    
155
#seekto 0xA020;
156
struct {
157
  u8 code[5];         //      5-character DTMF Encoder Groups
158
  u8 unused[11];
159
} pttid[15];
160

    
161
#seekto 0xA006;
162
struct {
163
  // u8 unused_a006:4,   // A006
164
  //    pttid:4;         //      PTT-ID
165
  u8 unknown_A006;    // A006
166
  u8 unused_a007:5,   // A007
167
     dtmfon:3;        //      DTMF Speed (on time)
168
  u8 unused_a008:5,   // A008
169
     dtmfoff:3;       //      DTMF Speed (off time)
170
} dtmf;
171

    
172
#seekto 0xB000;
173
struct {
174
  u8 code[3];         //      3-character ANI Code Groups
175
} anicodes[60];
176

    
177
#seekto 0xB0C0;
178
struct {
179
  char name[10];      //      10-character ANI Code Group Names
180
  u8 unused[6];
181
} aninames[%d];
182

    
183
#seekto 0xB200;
184
struct {
185
  char name[10];      //      10-character Custom CH Names (Talkpod A36plus)
186
  u8 unused[6];
187
} customnames[30];
188

    
189
"""
190

    
191

    
192
CMD_ACK = b"\x06"
193

    
194
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
195

    
196
DTMF_CHARS = "0123456789 *#ABCD"
197

    
198
TXPOWER_HIGH = 0x00
199
TXPOWER_LOW = 0x01
200
TXPOWER_MID = 0x02
201

    
202
ABR_LIST = ["On", "5 seconds", "10 seconds", "15 seconds", "20 seconds",
203
            "30 seconds", "1 minute", "2 minutes", "3 minutes"]
204
ALMODE_LIST = ["Site", "Tone", "Code"]
205
AUTOLK_LIST = ["Off"] + ABR_LIST[1:4]
206
DTMFSPEED_LIST = ["50 ms", "100 ms", "200 ms", "300 ms", "500 ms"]
207
DTMFST_LIST = ["Off", "KeyBoard Side Tone", "ANI Side Tone", "KB ST + ANI ST"]
208
DUALTX_LIST = ["Off", "A", "B"]
209
ENCRYPT_LIST = ["Off", "DCP1", "DCP2", "DCP3"]
210
LANGUAGE_LIST = ["English", "Chinese"]
211
MDF_LIST = ["Name", "Frequency", "Channel"]
212
MENUQUIT_LIST = ["%s seconds" % x for x in range(5, 55, 5)] + ["60 seconds"]
213
OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
214
PONMSG_LIST = ["Logo", "Voltage"]
215
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
216
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
217
PTTLT_LIST = ["None", "100 ms"] + \
218
             ["%s ms" % x for x in range(200, 1200, 200)]
219
QTSAVE_LIST = ["All", "RX", "TX"]
220
ROGER_LIST = ["OFF", "BEEP", "TONE1200"]
221
RPSTE_LIST = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
222
SAVE_LIST = ["Off", "Normal", "Super", "Deep"]
223
SCREV_LIST = ["Time (TO)", "Carrier (CO)", "Search (SE)"]
224
TAILCODE_LIST = ["55 Hz", "62.5 Hz"]
225
TAILPHASE_LIST = ["None", "120 Shift", "180 Shift", "240 Shift"]
226
TONERXEND_LIST = ["Off", "MDC-1200"]
227
TONE_LIST = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
228
TOT_LIST = ["Off", "30 seconds", "60 seconds", "120 seconds", "240 seconds",
229
            "480 seconds"]
230
VOXD_LIST = ["%s seconds" % str(x / 10) for x in range(5, 21)]
231
WORKMODE_LIST = ["VFO Mode", "Channel Mode"]
232

    
233
ALL_SKEY_CHOICES = ["OFF",
234
                    "FM Radio",
235
                    "TX Power Level",
236
                    "Scan",
237
                    "Search",
238
                    "Flashlight",
239
                    "NOAA Weather",
240
                    "Monitor",
241
                    "PTT B",
242
                    "SOS",
243
                    "DTMF",
244
                    "REVERSE",
245
                    "REMOTE Scan"]
246

    
247
ALL_SKEY_VALUES = [0xFF,
248
                   0x07,
249
                   0x0A,
250
                   0x1C,
251
                   0x1D,
252
                   0x08,
253
                   0x0C,
254
                   0x05,
255
                   0x01,
256
                   0x03,
257
                   0x2A,
258
                   0x2D,
259
                   0x23]
260

    
261

    
262
def _enter_programming_mode(radio):
263
    serial = radio.pipe
264

    
265
    exito = False
266
    for i in range(0, 5):
267
        serial.write(radio._magic)
268
        ack = serial.read(1)
269

    
270
        try:
271
            if ack == CMD_ACK:
272
                exito = True
273
                break
274
        except Exception:
275
            LOG.debug("Attempt #%s, failed, trying again" % i)
276
            pass
277

    
278
    # check if we had EXITO
279
    if exito is False:
280
        msg = "The radio did not accept program mode after five tries.\n"
281
        msg += "Check you interface cable and power cycle your radio."
282
        raise errors.RadioError(msg)
283

    
284
    try:
285
        serial.write(b"F")
286
        ident = serial.read(8)
287
    except Exception:
288
        raise errors.RadioError("Error communicating with radio")
289

    
290
    if ident not in radio._fingerprint:
291
        LOG.debug(util.hexprint(ident))
292
        raise errors.RadioError("Radio returned unknown identification string")
293

    
294
    if radio.MODEL == "RT-470X":
295
        if ident in radio._fingerprint_pcb1:
296
            LOG.info("Radtel RT-470X - original pcb")
297
            radio.RT470X_ORIG = True
298
        elif ident in radio._fingerprint_pcb2:
299
            LOG.info("Radtel RT-470X - pcb2")
300
            radio.RT470X_ORIG = False
301

    
302

    
303
def _exit_programming_mode(radio):
304
    serial = radio.pipe
305
    try:
306
        serial.write(b"E")
307
    except Exception:
308
        raise errors.RadioError("Radio refused to exit programming mode")
309

    
310

    
311
def _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"R" + cmd[1:]
316
    LOG.debug("Reading block %04x..." % (block_addr))
317

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

    
324
        block_data = response[4:]
325
    except Exception:
326
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
327

    
328
    return block_data
329

    
330

    
331
def _write_block(radio, block_addr, block_size):
332
    serial = radio.pipe
333

    
334
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
335
    data = radio.get_mmap()[block_addr:block_addr + block_size]
336

    
337
    LOG.debug("Writing Data:")
338
    LOG.debug(util.hexprint(cmd + data))
339

    
340
    try:
341
        serial.write(cmd + data)
342
        if serial.read(1) != CMD_ACK:
343
            raise Exception("No ACK")
344
    except Exception:
345
        raise errors.RadioError("Failed to send block "
346
                                "to radio at %04x" % block_addr)
347

    
348

    
349
def do_download(radio):
350
    LOG.debug("download")
351
    _enter_programming_mode(radio)
352

    
353
    data = b""
354

    
355
    status = chirp_common.Status()
356
    status.msg = "Cloning from radio"
357

    
358
    status.cur = 0
359
    status.max = radio._memsize
360

    
361
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
362
        status.cur = addr + radio.BLOCK_SIZE
363
        radio.status_fn(status)
364

    
365
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
366
        data += block
367

    
368
        LOG.debug("Address: %04x" % addr)
369
        LOG.debug(util.hexprint(block))
370

    
371
    _exit_programming_mode(radio)
372

    
373
    return memmap.MemoryMapBytes(data)
374

    
375

    
376
def do_upload(radio):
377
    status = chirp_common.Status()
378
    status.msg = "Uploading to radio"
379

    
380
    _enter_programming_mode(radio)
381

    
382
    status.cur = 0
383
    status.max = radio._memsize
384

    
385
    for start_addr, end_addr in radio._ranges:
386
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
387
            status.cur = addr + radio.BLOCK_SIZE_UP
388
            radio.status_fn(status)
389
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
390

    
391
    _exit_programming_mode(radio)
392

    
393

    
394
def _split(rf, f1, f2):
395
    """Returns False if the two freqs are in the same band (no split)
396
    or True otherwise"""
397

    
398
    # determine if the two freqs are in the same band
399
    for low, high in rf.valid_bands:
400
        if f1 >= low and f1 <= high and \
401
                f2 >= low and f2 <= high:
402
            # if the two freqs are on the same Band this is not a split
403
            return False
404

    
405
    # if you get here is because the freq pairs are split
406
    return True
407

    
408

    
409
class JC8810base(chirp_common.CloneModeRadio):
410
    """MML JC-8810"""
411
    VENDOR = "MML"
412
    MODEL = "JC-8810base"
413
    BAUD_RATE = 57600
414
    NEEDS_COMPAT_SERIAL = False
415
    BLOCK_SIZE = 0x40
416
    BLOCK_SIZE_UP = 0x40
417

    
418
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=10.00),
419
                    chirp_common.PowerLevel("M", watts=8.00),
420
                    chirp_common.PowerLevel("L", watts=4.00)]
421

    
422
    VALID_BANDS = [(108000000, 136000000),
423
                   (136000000, 180000000),
424
                   (200000000, 260000000),
425
                   (330000000, 400000000),
426
                   (400000000, 520000000)]
427

    
428
    _magic = b"PROGRAMJC81U"
429
    _fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
430
                    b"\x00\x00\x00\x42\x00\x20\xF0\x04",
431
                    b"\x00\x00\x00\x4A\x00\x20\xF8\x04"]
432

    
433
    _ranges = [
434
               (0x0000, 0x2000),
435
               (0x8000, 0x8040),
436
               (0x9000, 0x9040),
437
               (0xA000, 0xA140),
438
               (0xB000, 0xB300)
439
              ]
440
    _memsize = 0xB300
441
    _upper = 256
442
    _aninames = 30
443
    _mem_params = (_upper,  # number of channels
444
                   _aninames,  # number of aninames
445
                   )
446
    _valid_chars = chirp_common.CHARSET_ALPHANUMERIC + \
447
        "`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?"
448

    
449
    def get_features(self):
450
        rf = chirp_common.RadioFeatures()
451
        rf.has_settings = True
452
        rf.has_bank = False
453
        rf.has_ctone = True
454
        rf.has_cross = True
455
        rf.has_rx_dtcs = True
456
        rf.has_tuning_step = False
457
        rf.can_odd_split = True
458
        rf.has_name = True
459
        rf.valid_name_length = 12
460
        rf.valid_characters = self._valid_chars
461
        rf.valid_skips = ["", "S"]
462
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
463
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
464
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
465
        rf.valid_power_levels = self.POWER_LEVELS
466
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
467
        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 kHz.
468
        rf.valid_dtcs_codes = DTCS
469
        rf.memory_bounds = (1, self._upper)
470
        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
471
        rf.valid_bands = self.VALID_BANDS
472
        return rf
473

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

    
477
    def sync_in(self):
478
        """Download from radio"""
479
        try:
480
            data = do_download(self)
481
        except errors.RadioError:
482
            # Pass through any real errors we raise
483
            raise
484
        except Exception:
485
            # If anything unexpected happens, make sure we raise
486
            # a RadioError and log the problem
487
            LOG.exception('Unexpected error during download')
488
            raise errors.RadioError('Unexpected error communicating '
489
                                    'with the radio')
490
        self._mmap = data
491
        self.process_mmap()
492

    
493
    def sync_out(self):
494
        """Upload to radio"""
495
        try:
496
            do_upload(self)
497
        except Exception:
498
            # If anything unexpected happens, make sure we raise
499
            # a RadioError and log the problem
500
            LOG.exception('Unexpected error during upload')
501
            raise errors.RadioError('Unexpected error communicating '
502
                                    'with the radio')
503

    
504
    def _is_txinh(self, _mem):
505
        raw_tx = ""
506
        for i in range(0, 4):
507
            raw_tx += _mem.txfreq[i].get_raw()
508
        return raw_tx == "\xFF\xFF\xFF\xFF"
509

    
510
    def get_memory(self, number):
511
        """Get the mem representation from the radio image"""
512
        _mem = self._memobj.memory[number - 1]
513

    
514
        # Create a high-level memory object to return to the UI
515
        mem = chirp_common.Memory()
516

    
517
        # Memory number
518
        mem.number = number
519

    
520
        if _mem.get_raw()[0] == "\xff":
521
            mem.empty = True
522
            return mem
523

    
524
        # Freq and offset
525
        mem.freq = int(_mem.rxfreq) * 10
526
        # tx freq can be blank
527
        if _mem.get_raw()[4] == "\xFF":
528
            # TX freq not set
529
            mem.offset = 0
530
            mem.duplex = "off"
531
        else:
532
            # TX freq set
533
            offset = (int(_mem.txfreq) * 10) - mem.freq
534
            if offset != 0:
535
                if _split(self.get_features(), mem.freq, int(
536
                          _mem.txfreq) * 10):
537
                    mem.duplex = "split"
538
                    mem.offset = int(_mem.txfreq) * 10
539
                elif offset < 0:
540
                    mem.offset = abs(offset)
541
                    mem.duplex = "-"
542
                elif offset > 0:
543
                    mem.offset = offset
544
                    mem.duplex = "+"
545
            else:
546
                mem.offset = 0
547

    
548
        for char in _mem.name:
549
            if str(char) == "\xFF":
550
                char = " "  # may have 0xFF mid-name
551
            mem.name += str(char)
552
        mem.name = mem.name.rstrip()
553

    
554
        dtcs_pol = ["N", "N"]
555

    
556
        if _mem.txtone in [0, 0xFFFF]:
557
            txmode = ""
558
        elif _mem.txtone >= 0x0258:
559
            txmode = "Tone"
560
            mem.rtone = int(_mem.txtone) / 10.0
561
        elif _mem.txtone <= 0x0258:
562
            txmode = "DTCS"
563
            if _mem.txtone > 0x69:
564
                index = _mem.txtone - 0x6A
565
                dtcs_pol[0] = "R"
566
            else:
567
                index = _mem.txtone - 1
568
            mem.dtcs = DTCS[index]
569
        else:
570
            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
571

    
572
        if _mem.rxtone in [0, 0xFFFF]:
573
            rxmode = ""
574
        elif _mem.rxtone >= 0x0258:
575
            rxmode = "Tone"
576
            mem.ctone = int(_mem.rxtone) / 10.0
577
        elif _mem.rxtone <= 0x0258:
578
            rxmode = "DTCS"
579
            if _mem.rxtone >= 0x6A:
580
                index = _mem.rxtone - 0x6A
581
                dtcs_pol[1] = "R"
582
            else:
583
                index = _mem.rxtone - 1
584
            mem.rx_dtcs = DTCS[index]
585
        else:
586
            LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
587

    
588
        if txmode == "Tone" and not rxmode:
589
            mem.tmode = "Tone"
590
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
591
            mem.tmode = "TSQL"
592
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
593
            mem.tmode = "DTCS"
594
        elif rxmode or txmode:
595
            mem.tmode = "Cross"
596
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
597

    
598
        mem.dtcs_polarity = "".join(dtcs_pol)
599

    
600
        if not _mem.scan:
601
            mem.skip = "S"
602

    
603
        _levels = self.POWER_LEVELS
604
        if self.MODEL in ["A36plus", "UV-A37", "AR-730"]:
605
            if _mem.txpower == TXPOWER_HIGH:
606
                mem.power = _levels[0]
607
            elif _mem.txpower == TXPOWER_LOW:
608
                mem.power = _levels[1]
609
            else:
610
                LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
611
                          (mem.name, _mem.txpower))
612
        else:
613
            if _mem.txpower == TXPOWER_HIGH:
614
                mem.power = _levels[0]
615
            elif _mem.txpower == TXPOWER_MID:
616
                mem.power = _levels[1]
617
            elif _mem.txpower == TXPOWER_LOW:
618
                mem.power = _levels[2]
619
            else:
620
                LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
621
                          (mem.name, _mem.txpower))
622

    
623
        mem.mode = _mem.narrow and "NFM" or "FM"
624

    
625
        mem.extra = RadioSettingGroup("Extra", "extra")
626

    
627
        # BCL (Busy Channel Lockout)
628
        rs = RadioSettingValueBoolean(_mem.bcl)
629
        rset = RadioSetting("bcl", "BCL", rs)
630
        mem.extra.append(rset)
631

    
632
        # PTT-ID
633
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.pttid])
634
        rset = RadioSetting("pttid", "PTT ID", rs)
635
        mem.extra.append(rset)
636

    
637
        # Signal (DTMF Encoder Group #)
638
        rs = RadioSettingValueList(PTTIDCODE_LIST, PTTIDCODE_LIST[_mem.scode])
639
        rset = RadioSetting("scode", "PTT ID Code", rs)
640
        mem.extra.append(rset)
641

    
642
        return mem
643

    
644
    def set_memory(self, mem):
645
        _mem = self._memobj.memory[mem.number - 1]
646

    
647
        if mem.empty:
648
            _mem.set_raw("\xff" * 32)
649
            return
650

    
651
        _mem.set_raw("\x00" * 16 + "\xFF" * 16)
652

    
653
        _mem.rxfreq = mem.freq / 10
654

    
655
        if mem.duplex == "off":
656
            for i in range(0, 4):
657
                _mem.txfreq[i].set_raw("\xFF")
658
        elif mem.duplex == "split":
659
            _mem.txfreq = mem.offset / 10
660
        elif mem.duplex == "+":
661
            _mem.txfreq = (mem.freq + mem.offset) / 10
662
        elif mem.duplex == "-":
663
            _mem.txfreq = (mem.freq - mem.offset) / 10
664
        else:
665
            _mem.txfreq = mem.freq / 10
666

    
667
        _namelength = self.get_features().valid_name_length
668
        for i in range(_namelength):
669
            try:
670
                _mem.name[i] = mem.name[i]
671
            except IndexError:
672
                _mem.name[i] = "\xFF"
673

    
674
        rxmode = txmode = ""
675
        if mem.tmode == "Tone":
676
            _mem.txtone = int(mem.rtone * 10)
677
            _mem.rxtone = 0
678
        elif mem.tmode == "TSQL":
679
            _mem.txtone = int(mem.ctone * 10)
680
            _mem.rxtone = int(mem.ctone * 10)
681
        elif mem.tmode == "DTCS":
682
            rxmode = txmode = "DTCS"
683
            _mem.txtone = DTCS.index(mem.dtcs) + 1
684
            _mem.rxtone = DTCS.index(mem.dtcs) + 1
685
        elif mem.tmode == "Cross":
686
            txmode, rxmode = mem.cross_mode.split("->", 1)
687
            if txmode == "Tone":
688
                _mem.txtone = int(mem.rtone * 10)
689
            elif txmode == "DTCS":
690
                _mem.txtone = DTCS.index(mem.dtcs) + 1
691
            else:
692
                _mem.txtone = 0
693
            if rxmode == "Tone":
694
                _mem.rxtone = int(mem.ctone * 10)
695
            elif rxmode == "DTCS":
696
                _mem.rxtone = DTCS.index(mem.rx_dtcs) + 1
697
            else:
698
                _mem.rxtone = 0
699
        else:
700
            _mem.rxtone = 0
701
            _mem.txtone = 0
702

    
703
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
704
            _mem.txtone += 0x69
705
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
706
            _mem.rxtone += 0x69
707

    
708
        _mem.scan = mem.skip != "S"
709
        _mem.narrow = mem.mode == "NFM"
710

    
711
        _levels = self.POWER_LEVELS
712
        if self.MODEL in ["A36plus", "UV-A37", "AR-730"]:
713
            if mem.power is None:
714
                _mem.txpower = TXPOWER_HIGH
715
            elif mem.power == _levels[0]:
716
                _mem.txpower = TXPOWER_HIGH
717
            elif mem.power == _levels[1]:
718
                _mem.txpower = TXPOWER_LOW
719
            else:
720
                LOG.error('%s: set_mem: unhandled power level: %s' %
721
                          (mem.name, mem.power))
722
        else:
723
            if mem.power is None:
724
                _mem.txpower = TXPOWER_HIGH
725
            elif mem.power == _levels[0]:
726
                _mem.txpower = TXPOWER_HIGH
727
            elif mem.power == _levels[1]:
728
                _mem.txpower = TXPOWER_MID
729
            elif mem.power == _levels[2]:
730
                _mem.txpower = TXPOWER_LOW
731
            else:
732
                LOG.error('%s: set_mem: unhandled power level: %s' %
733
                          (mem.name, mem.power))
734

    
735
        for setting in mem.extra:
736
            if setting.get_name() == "scramble_type":
737
                setattr(_mem, setting.get_name(), int(setting.value) + 8)
738
                setattr(_mem, "scramble_type2", int(setting.value) + 8)
739
            else:
740
                setattr(_mem, setting.get_name(), setting.value)
741

    
742
    def get_settings(self):
743
        _dtmf = self._memobj.dtmf
744
        _settings = self._memobj.settings
745
        basic = RadioSettingGroup("basic", "Basic Settings")
746
        group = RadioSettings(basic)
747

    
748
        # Menu 12: TOT
749
        rs = RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot])
750
        rset = RadioSetting("tot", "Time Out Timer", rs)
751
        basic.append(rset)
752

    
753
        # Menu 00: SQL
754
        rs = RadioSettingValueInteger(0, 9, _settings.sql)
755
        rset = RadioSetting("sql", "Squelch Level", rs)
756
        basic.append(rset)
757

    
758
        # Menu 13: VOX
759
        rs = RadioSettingValueList(OFF1TO9_LIST, OFF1TO9_LIST[_settings.vox])
760
        rset = RadioSetting("vox", "VOX", rs)
761
        basic.append(rset)
762

    
763
        # Menu 39: VOX DELAY
764
        rs = RadioSettingValueList(VOXD_LIST, VOXD_LIST[_settings.voxd])
765
        rset = RadioSetting("voxd", "VOX Delay", rs)
766
        basic.append(rset)
767

    
768
        # Menu 15: VOICE
769
        rs = RadioSettingValueBoolean(_settings.voice)
770
        rset = RadioSetting("voice", "Voice Prompts", rs)
771
        basic.append(rset)
772

    
773
        if self.MODEL not in ["A36plus", "UV-A37", "AR-730"]:
774
            # Menu 17: LANGUAGE
775
            rs = RadioSettingValueList(LANGUAGE_LIST,
776
                                       LANGUAGE_LIST[_settings.language])
777
            rset = RadioSetting("language", "Voice", rs)
778
            basic.append(rset)
779

    
780
        # Menu 23: ABR
781
        rs = RadioSettingValueList(ABR_LIST, ABR_LIST[_settings.abr])
782
        rset = RadioSetting("abr", "Auto BackLight", rs)
783
        basic.append(rset)
784

    
785
        # Work Mode A
786
        rs = RadioSettingValueList(WORKMODE_LIST,
787
                                   WORKMODE_LIST[_settings.vfomra])
788
        rset = RadioSetting("vfomra", "Work Mode A", rs)
789
        basic.append(rset)
790

    
791
        # Work Mode B
792
        rs = RadioSettingValueList(WORKMODE_LIST,
793
                                   WORKMODE_LIST[_settings.vfomrb])
794
        rset = RadioSetting("vfomrb", "Work Mode B", rs)
795
        basic.append(rset)
796

    
797
        # Menu 19: SC-REV
798
        rs = RadioSettingValueList(SCREV_LIST, SCREV_LIST[_settings.screv])
799
        rset = RadioSetting("screv", "Scan Resume Method", rs)
800
        basic.append(rset)
801

    
802
        # Menu 10: SAVE
803
        rs = RadioSettingValueList(SAVE_LIST,
804
                                   SAVE_LIST[_settings.save])
805
        rset = RadioSetting("save", "Battery Save Mode", rs)
806
        basic.append(rset)
807

    
808
        # Menu 42: MDF-A
809
        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfa])
810
        rset = RadioSetting("mdfa", "Memory Display Format A", rs)
811
        basic.append(rset)
812

    
813
        # Menu 43: MDF-B
814
        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfb])
815
        rset = RadioSetting("mdfb", "Memory Display Format B", rs)
816
        basic.append(rset)
817

    
818
        # Menu 33: DTMFST (DTMF ST)
819
        rs = RadioSettingValueList(DTMFST_LIST, DTMFST_LIST[_settings.dtmfst])
820
        rset = RadioSetting("dtmfst", "DTMF Side Tone", rs)
821
        basic.append(rset)
822

    
823
        # Menu 37: PTT-LT
824
        rs = RadioSettingValueList(PTTLT_LIST, PTTLT_LIST[_settings.pttlt])
825
        rset = RadioSetting("pttlt", "PTT Delay", rs)
826
        basic.append(rset)
827

    
828
        rs = RadioSettingValueInteger(1, 60, _settings.ani + 1)
829
        rset = RadioSetting("ani", "ANI", rs)
830
        basic.append(rset)
831

    
832
        # Menu 20: PF2
833
        def apply_skey2s_listvalue(setting, obj):
834
            LOG.debug("Setting value: " + str(setting.value) + " from list")
835
            val = str(setting.value)
836
            index = SKEY2S_CHOICES.index(val)
837
            val = SKEY2S_VALUES[index]
838
            obj.set_value(val)
839

    
840
        if self.MODEL in ["RT-470"]:
841
            unwanted = [0, 7, 9, 10, 11, 12]
842
        elif self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
843
            unwanted = [9, 10, 11, 12]
844
        elif self.MODEL in ["UV-A37", "AR-730"]:
845
            unwanted = [0, 5, 7, 9, 10, 11, 12]
846
        elif self.MODEL in ["A36plus"]:
847
            unwanted = [0, 5, 7, 9, 10, 11]
848
        else:
849
            unwanted = []
850
        SKEY2S_CHOICES = ALL_SKEY_CHOICES.copy()
851
        SKEY2S_VALUES = ALL_SKEY_VALUES.copy()
852
        for ele in sorted(unwanted, reverse=True):
853
            del SKEY2S_CHOICES[ele]
854
            del SKEY2S_VALUES[ele]
855

    
856
        if _settings.skey2_sp in SKEY2S_VALUES:
857
            idx = SKEY2S_VALUES.index(_settings.skey2_sp)
858
        else:
859
            idx = SKEY2S_VALUES.index(0x07)  # default FM
860
        rs = RadioSettingValueList(SKEY2S_CHOICES, SKEY2S_CHOICES[idx])
861
        rset = RadioSetting("skey2_sp", "PF2 Key (Short Press)", rs)
862
        rset.set_apply_callback(apply_skey2s_listvalue, _settings.skey2_sp)
863
        basic.append(rset)
864

    
865
        # Menu 21: PF2 LONG PRESS
866
        def apply_skey2l_listvalue(setting, obj):
867
            LOG.debug("Setting value: " + str(setting.value) + " from list")
868
            val = str(setting.value)
869
            index = SKEY2L_CHOICES.index(val)
870
            val = SKEY2L_VALUES[index]
871
            obj.set_value(val)
872

    
873
        if self.MODEL in ["RT-470"]:
874
            unwanted = [0, 7, 8, 9, 10, 11, 12]
875
        elif self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
876
            unwanted = [8, 9, 10, 11, 12]
877
        elif self.MODEL in ["UV-A37", "AR-730"]:
878
            unwanted = [0, 5, 7, 8, 10, 11, 12]
879
        elif self.MODEL in ["A36plus"]:
880
            unwanted = [0, 5, 7, 8, 11, 12]
881
        else:
882
            unwanted = []
883
        SKEY2L_CHOICES = ALL_SKEY_CHOICES.copy()
884
        SKEY2L_VALUES = ALL_SKEY_VALUES.copy()
885
        for ele in sorted(unwanted, reverse=True):
886
            del SKEY2L_CHOICES[ele]
887
            del SKEY2L_VALUES[ele]
888

    
889
        if _settings.skey2_lp in SKEY2L_VALUES:
890
            idx = SKEY2L_VALUES.index(_settings.skey2_lp)
891
        else:
892
            idx = SKEY2L_VALUES.index(0x1D)  # default Search
893
        rs = RadioSettingValueList(SKEY2L_CHOICES, SKEY2L_CHOICES[idx])
894
        rset = RadioSetting("skey2_lp", "PF2 Key (Long Press)", rs)
895
        rset.set_apply_callback(apply_skey2l_listvalue, _settings.skey2_lp)
896
        basic.append(rset)
897

    
898
        # Menu 22: PF3
899
        def apply_skey3s_listvalue(setting, obj):
900
            LOG.debug("Setting value: " + str(setting.value) + " from list")
901
            val = str(setting.value)
902
            index = SKEY3S_CHOICES.index(val)
903
            val = SKEY3S_VALUES[index]
904
            obj.set_value(val)
905

    
906
        if self.MODEL in ["RT-470"]:
907
            unwanted = [0, 7, 8, 9, 10, 11, 12]
908
        elif self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
909
            unwanted = [8, 9, 10, 11, 12]
910
        elif self.MODEL in ["UV-A37", "AR-730"]:
911
            unwanted = [0, 5, 7, 8, 9, 10, 11, 12]
912
        elif self.MODEL in ["A36plus"]:
913
            unwanted = [0, 5, 7, 8, 11]
914
        else:
915
            unwanted = []
916
        SKEY3S_CHOICES = ALL_SKEY_CHOICES.copy()
917
        SKEY3S_VALUES = ALL_SKEY_VALUES.copy()
918
        for ele in sorted(unwanted, reverse=True):
919
            del SKEY3S_CHOICES[ele]
920
            del SKEY3S_VALUES[ele]
921

    
922
        if _settings.skey3_sp in SKEY3S_VALUES:
923
            idx = SKEY3S_VALUES.index(_settings.skey3_sp)
924
        else:
925
            idx = SKEY3S_VALUES.index(0x0C)  # default NOAA
926
        rs = RadioSettingValueList(SKEY3S_CHOICES, SKEY3S_CHOICES[idx])
927
        rset = RadioSetting("skey3_sp", "PF3 Key (Short Press)", rs)
928
        rset.set_apply_callback(apply_skey3s_listvalue, _settings.skey3_sp)
929
        basic.append(rset)
930

    
931
        if self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
932
            # Menu 24: PF3 LONG PRESS (RT-470L)
933
            def apply_skey3l_listvalue(setting, obj):
934
                LOG.debug("Setting value: " + str(setting.value) +
935
                          " from list")
936
                val = str(setting.value)
937
                index = SKEY3L_CHOICES.index(val)
938
                val = SKEY2L_VALUES[index]
939
                obj.set_value(val)
940

    
941
            if self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
942
                unwanted = [8, 9, 10, 11, 12]
943
            else:
944
                unwanted = []
945
            SKEY3L_CHOICES = ALL_SKEY_CHOICES.copy()
946
            SKEY3L_VALUES = ALL_SKEY_VALUES.copy()
947
            for ele in sorted(unwanted, reverse=True):
948
                del SKEY3L_CHOICES[ele]
949
                del SKEY3L_VALUES[ele]
950

    
951
            if _settings.skey3_lp in SKEY3L_VALUES:
952
                idx = SKEY3L_VALUES.index(_settings.skey3_lp)
953
            else:
954
                idx = SKEY3L_VALUES.index(0x1D)  # default SEARCH
955
            rs = RadioSettingValueList(SKEY3L_CHOICES, SKEY3L_CHOICES[idx])
956
            rset = RadioSetting("skey3_lp", "PF3 Key (Long Press)", rs)
957
            rset.set_apply_callback(apply_skey3l_listvalue,
958
                                    _settings.skey3_lp)
959
            basic.append(rset)
960

    
961
        # Menu 25: TOP KEY (RT-470L)
962
        def apply_skeytop_listvalue(setting, obj):
963
            LOG.debug("Setting value: " + str(setting.value) +
964
                      " from list")
965
            val = str(setting.value)
966
            index = SKEYTOP_CHOICES.index(val)
967
            val = SKEYTOP_VALUES[index]
968
            obj.set_value(val)
969

    
970
        if self.MODEL in ["RT-470"]:
971
            # ==========
972
            # Notice to developers:
973
            # The RT-470 v1.22 firmware added 'hidden' support for the
974
            # Top Key (Short Press) feature. RT-470 radios with a firmware
975
            # version prior to v1.22 will not honor the Top Key (Short
976
            # Press) setting in CHIRP.
977
            # ==========
978
            unwanted = [0, 7, 8, 9, 10, 11, 12]
979
        elif self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
980
            unwanted = [8, 9, 10, 11, 12]
981
        elif self.MODEL in ["UV-A37", "AR-730"]:
982
            unwanted = [0, 5, 7, 8, 9, 10, 11, 12]
983
        elif self.MODEL in ["A36plus"]:
984
            unwanted = [0, 5, 7, 8, 11]
985
        else:
986
            unwanted = []
987
        SKEYTOP_CHOICES = ALL_SKEY_CHOICES.copy()
988
        SKEYTOP_VALUES = ALL_SKEY_VALUES.copy()
989
        for ele in sorted(unwanted, reverse=True):
990
            del SKEYTOP_CHOICES[ele]
991
            del SKEYTOP_VALUES[ele]
992

    
993
        if _settings.topkey_sp in SKEYTOP_VALUES:
994
            idx = SKEYTOP_VALUES.index(_settings.topkey_sp)
995
        else:
996
            idx = SKEYTOP_VALUES.index(0x1D)  # default SEARCH
997
        rs = RadioSettingValueList(SKEYTOP_CHOICES, SKEYTOP_CHOICES[idx])
998
        rset = RadioSetting("topkey_sp", "Top Key (Short Press)", rs)
999
        rset.set_apply_callback(apply_skeytop_listvalue,
1000
                                _settings.topkey_sp)
1001
        basic.append(rset)
1002

    
1003
        # Mneu 36: TONE
1004
        rs = RadioSettingValueList(TONE_LIST, TONE_LIST[_settings.tone])
1005
        rset = RadioSetting("tone", "Tone-burst Frequency", rs)
1006
        basic.append(rset)
1007

    
1008
        # Mneu 29: POWER ON MSG
1009
        rs = RadioSettingValueList(PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])
1010
        rset = RadioSetting("ponmsg", "Power On Message", rs)
1011
        basic.append(rset)
1012

    
1013
        if self.MODEL in ["HI-8811", "RT-470L", "RT-470X"]:
1014
            rs = RadioSettingValueList(TAILCODE_LIST,
1015
                                       TAILCODE_LIST[_settings.tailcode])
1016
            rset = RadioSetting("tailcode", "Tail Code", rs)
1017
            basic.append(rset)
1018

    
1019
        # Menu 46: STE
1020
        rs = RadioSettingValueBoolean(_settings.ste)
1021
        rset = RadioSetting("ste", "Squelch Tail Eliminate (HT to HT)", rs)
1022
        basic.append(rset)
1023

    
1024
        # Menu 40: RP-STE
1025
        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rpste])
1026
        rset = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)", rs)
1027
        basic.append(rset)
1028

    
1029
        # Menu 41: RPT-RL
1030
        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rptrl])
1031
        rset = RadioSetting("rptrl", "STE Repeater Delay", rs)
1032
        basic.append(rset)
1033

    
1034
        # Menu 38: MENU EXIT TIME
1035
        rs = RadioSettingValueList(MENUQUIT_LIST,
1036
                                   MENUQUIT_LIST[_settings.menuquit])
1037
        rset = RadioSetting("menuquit", "Menu Auto Quit", rs)
1038
        basic.append(rset)
1039

    
1040
        # Menu 34: AUTOLOCK
1041
        rs = RadioSettingValueList(AUTOLK_LIST, AUTOLK_LIST[_settings.autolk])
1042
        rset = RadioSetting("autolk", "Key Auto Lock", rs)
1043
        basic.append(rset)
1044

    
1045
        # Menu 28: CDCSS SAVE MODE
1046
        rs = RadioSettingValueList(QTSAVE_LIST, QTSAVE_LIST[_settings.qtsave])
1047
        rset = RadioSetting("qtsave", "QT Save Type", rs)
1048
        basic.append(rset)
1049

    
1050
        # Menu 45: TX-A/B
1051
        rs = RadioSettingValueList(DUALTX_LIST, DUALTX_LIST[_settings.dualtx])
1052
        rset = RadioSetting("dualtx", "Dual TX", rs)
1053
        basic.append(rset)
1054

    
1055
        # Menu 47: AL-MODE
1056
        rs = RadioSettingValueList(ALMODE_LIST, ALMODE_LIST[_settings.almode])
1057
        rset = RadioSetting("almode", "Alarm Mode", rs)
1058
        basic.append(rset)
1059

    
1060
        # Menu 11: ROGER
1061
        # ==========
1062
        # Notice to developers:
1063
        # The RT-470 v1.22 firmware expanded the ROGER menu with an additional
1064
        # choice, 'TONE1200'. RT-470 radios with a firmware version prior to
1065
        #  v1.22 will not honor the ROGER menu's 'TONE1200' choice in CHIRP.
1066
        # ==========
1067
        rs = RadioSettingValueList(ROGER_LIST, ROGER_LIST[_settings.roger])
1068
        rset = RadioSetting("roger", "Roger", rs)
1069
        basic.append(rset)
1070

    
1071
        rs = RadioSettingValueBoolean(_settings.alarmsound)
1072
        rset = RadioSetting("alarmsound", "Alarm Sound", rs)
1073
        basic.append(rset)
1074

    
1075
        # Menu 44: TDR
1076
        rs = RadioSettingValueBoolean(_settings.tdr)
1077
        rset = RadioSetting("tdr", "TDR", rs)
1078
        basic.append(rset)
1079

    
1080
        rs = RadioSettingValueBoolean(not _settings.fmradio)
1081
        rset = RadioSetting("fmradio", "FM Radio", rs)
1082
        basic.append(rset)
1083

    
1084
        rs = RadioSettingValueBoolean(_settings.kblock)
1085
        rset = RadioSetting("kblock", "KB Lock", rs)
1086
        basic.append(rset)
1087

    
1088
        # Menu 16: BEEP PROMPT
1089
        rs = RadioSettingValueBoolean(_settings.beep)
1090
        rset = RadioSetting("beep", "Beep", rs)
1091
        basic.append(rset)
1092

    
1093
        if self.MODEL not in ["A36plus", "UV-A37", "AR-730"]:
1094
            # Menu 48: RX END TAIL
1095
            rs = RadioSettingValueList(TONERXEND_LIST,
1096
                                       TONERXEND_LIST[_settings.rxendtail])
1097
            rset = RadioSetting("rxendtail", "Tone RX End", rs)
1098
            basic.append(rset)
1099

    
1100
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
1101
        group.append(dtmf)
1102

    
1103
        def apply_code(setting, obj, length):
1104
            code = []
1105
            for j in range(0, length):
1106
                try:
1107
                    code.append(DTMF_CHARS.index(str(setting.value)[j]))
1108
                except IndexError:
1109
                    code.append(0xFF)
1110
            obj.code = code
1111

    
1112
        for i in range(0, 15):
1113
            _codeobj = self._memobj.pttid[i].code
1114
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1115
            rs = RadioSettingValueString(0, 5, _code, False)
1116
            rs.set_charset(DTMF_CHARS)
1117
            rset = RadioSetting("pttid/%i.code" % i,
1118
                                "PTT-ID Code %i" % (i + 1), rs)
1119
            rset.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
1120
            dtmf.append(rset)
1121

    
1122
        rs = RadioSettingValueList(DTMFSPEED_LIST,
1123
                                   DTMFSPEED_LIST[_dtmf.dtmfon])
1124
        rset = RadioSetting("dtmf.dtmfon", "DTMF Speed (on)", rs)
1125
        dtmf.append(rset)
1126

    
1127
        rs = RadioSettingValueList(DTMFSPEED_LIST,
1128
                                   DTMFSPEED_LIST[_dtmf.dtmfoff])
1129
        rset = RadioSetting("dtmf.dtmfoff", "DTMF Speed (off)", rs)
1130
        dtmf.append(rset)
1131

    
1132
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_settings.pttid])
1133
        rset = RadioSetting("pttid", "PTT ID", rs)
1134
        dtmf.append(rset)
1135

    
1136
        ani = RadioSettingGroup("ani", "ANI Code List Settings")
1137
        group.append(ani)
1138

    
1139
        def _filter(name):
1140
            filtered = ""
1141
            for char in str(name):
1142
                if char in self._valid_chars:
1143
                    filtered += char
1144
                else:
1145
                    filtered += " "
1146
            return filtered
1147

    
1148
        end = 60 - self._aninames  # end of immutable ANI names
1149
        for i in range(0, end):
1150
            _codeobj = self._memobj.anicodes[i].code
1151
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1152
            rs = RadioSettingValueString(0, 3, _code, False)
1153
            rs.set_charset(DTMF_CHARS)
1154
            rset = RadioSetting("anicodes/%i.code" % i,
1155
                                "ANI Code %i (NUM.%i)" % (i + 1, i + 1), rs)
1156
            rset.set_apply_callback(apply_code, self._memobj.anicodes[i], 3)
1157
            ani.append(rset)
1158

    
1159
        for i in range(end, 60):
1160
            _codeobj = self._memobj.anicodes[i].code
1161
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1162
            rs = RadioSettingValueString(0, 3, _code, False)
1163
            rs.set_charset(DTMF_CHARS)
1164
            rset = RadioSetting("anicodes/%i.code" % (i),
1165
                                "ANI Code %i" % (i + 1), rs)
1166
            rset.set_apply_callback(apply_code,
1167
                                    self._memobj.anicodes[i], 3)
1168
            ani.append(rset)
1169

    
1170
            _nameobj = self._memobj.aninames[i - end].name
1171
            rs = RadioSettingValueString(0, 10, _filter(_nameobj))
1172
            rset = RadioSetting("aninames/%i.name" % (i - end),
1173
                                "ANI Code %i Name" % (i + 1), rs)
1174
            ani.append(rset)
1175

    
1176
        if self.MODEL == "A36plus":
1177
            custom = RadioSettingGroup("custom", "Custom Channel Names")
1178
            group.append(custom)
1179

    
1180
            for i in range(0, 30):
1181
                _nameobj = self._memobj.customnames[i].name
1182
                rs = RadioSettingValueString(0, 10, _filter(_nameobj))
1183
                rset = RadioSetting("customnames/%i.name" % i,
1184
                                    "Custom Name %i" % (i + 1), rs)
1185
                custom.append(rset)
1186

    
1187
        if self.MODEL == "A36plus":
1188
            # Menu 21: RX END TAIL
1189
            rs = RadioSettingValueList(TONERXEND_LIST,
1190
                                       TONERXEND_LIST[_settings.skey3_lp])
1191
            rset = RadioSetting("skey3_lp", "RX End Tail", rs)
1192
            basic.append(rset)
1193

    
1194
            # Menu 23: TAIL PHASE
1195
            rs = RadioSettingValueList(TAILPHASE_LIST,
1196
                                       TAILPHASE_LIST[_settings.rxendtail])
1197
            rset = RadioSetting("rxendtail", "Tail Phase", rs)
1198
            basic.append(rset)
1199

    
1200
        return group
1201

    
1202
    def set_settings(self, settings):
1203
        _settings = self._memobj.settings
1204
        for element in settings:
1205
            if not isinstance(element, RadioSetting):
1206
                self.set_settings(element)
1207
                continue
1208
            else:
1209
                try:
1210
                    name = element.get_name()
1211
                    if "." in name:
1212
                        bits = name.split(".")
1213
                        obj = self._memobj
1214
                        for bit in bits[:-1]:
1215
                            if "/" in bit:
1216
                                bit, index = bit.split("/", 1)
1217
                                index = int(index)
1218
                                obj = getattr(obj, bit)[index]
1219
                            else:
1220
                                obj = getattr(obj, bit)
1221
                        setting = bits[-1]
1222
                    else:
1223
                        obj = _settings
1224
                        setting = element.get_name()
1225

    
1226
                    if element.has_apply_callback():
1227
                        LOG.debug("Using apply callback")
1228
                        element.run_apply_callback()
1229
                    elif setting == "ani":
1230
                        setattr(obj, setting, int(element.value) - 1)
1231
                    elif setting == "fmradio":
1232
                        setattr(obj, setting, not int(element.value))
1233
                    elif element.value.get_mutable():
1234
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1235
                        setattr(obj, setting, element.value)
1236
                except Exception as e:
1237
                    LOG.debug(element.get_name(), e)
1238
                    raise
1239

    
1240
    @classmethod
1241
    def match_model(cls, filedata, filename):
1242
        # This radio has always been post-metadata, so never do
1243
        # old-school detection
1244
        return False
1245

    
1246

    
1247
@directory.register
1248
class RT470Radio(JC8810base):
1249
    """Radtel RT-470"""
1250
    VENDOR = "Radtel"
1251
    MODEL = "RT-470"
1252

    
1253
    # ==========
1254
    # Notice to developers:
1255
    # The RT-470 support in this driver is currently based upon v1.25 firmware.
1256
    # ==========
1257

    
1258
    _fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
1259
                    b"\x00\x00\x00\x42\x00\x20\xF0\x04",
1260
                    b"\x00\x00\x00\x4A\x00\x20\xF8\x04",
1261
                    b"\x00\x00\x00\x3A\x00\x20\xE8\x04",  # fw 1.25A
1262
                    ]
1263

    
1264
    VALID_BANDS = [(16000000, 100000000),
1265
                   (100000000, 136000000),
1266
                   (136000000, 200000000),
1267
                   (200000000, 300000000),
1268
                   (300000000, 400000000),
1269
                   (400000000, 560000000),
1270
                   (740000000, 1000000000),
1271
                   ]
1272

    
1273

    
1274
@directory.register
1275
class RT470LRadio(JC8810base):
1276
    """Radtel RT-470L"""
1277
    VENDOR = "Radtel"
1278
    MODEL = "RT-470L"
1279

    
1280
    # ==========
1281
    # Notice to developers:
1282
    # The RT-470 support in this driver is currently based upon v1.17 firmware.
1283
    # ==========
1284

    
1285
    _fingerprint = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04",
1286
                    b"\x00\x00\x00\x20\x00\x20\xCC\x04",
1287
                    b"\x00\x00\x00\x20\x00\x20\x07\x00"]
1288

    
1289
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
1290
                    chirp_common.PowerLevel("M", watts=4.00),
1291
                    chirp_common.PowerLevel("L", watts=2.00)]
1292

    
1293
    VALID_BANDS = [(108000000, 136000000),
1294
                   (136000000, 179000000),
1295
                   (220000000, 260000000),
1296
                   (330000000, 400000000),
1297
                   (400000000, 520000000)]
1298

    
1299

    
1300
@directory.register
1301
class RT470XRadio(RT470LRadio):
1302
    """Radtel RT-470X"""
1303
    VENDOR = "Radtel"
1304
    MODEL = "RT-470X"
1305

    
1306
    # ==========
1307
    # Notice to developers:
1308
    # The RT-470X support in this driver is currently based upon...
1309
    # - v1.18a firmware (original pcb)
1310
    # - v2.10a firmware (pcb2)
1311
    # ==========
1312

    
1313
    # original pcb
1314
    _fingerprint_pcb1 = [b"\x00\x00\x00\x20\x00\x20\xCC\x04",
1315
                         ]
1316
    
1317
    # pcb 2
1318
    _fingerprint_pcb2 = [b"\x00\x00\x00\x2C\x00\x20\xD8\x04",  # fw v2.10A
1319
                         ]
1320

    
1321
    _fingerprint = _fingerprint_pcb1 + _fingerprint_pcb2
1322

    
1323
    RT470X_ORIG = False
1324

    
1325
    VALID_BANDS = [(100000000, 136000000),
1326
                   (136000000, 200000000),
1327
                   (200000000, 300000000),
1328
                   (300000000, 400000000),
1329
                   (400000000, 560000000)]
1330

    
1331

    
1332
@directory.register
1333
class HI8811Radio(RT470LRadio):
1334
    """Hiroyasu HI-8811"""
1335
    VENDOR = "Hiroyasu"
1336
    MODEL = "HI-8811"
1337

    
1338
    # ==========
1339
    # Notice to developers:
1340
    # The HI-8811 support in this driver is currently based upon v1.17
1341
    # firmware.
1342
    # ==========
1343

    
1344

    
1345
@directory.register
1346
class UVA37Radio(JC8810base):
1347
    """Anysecu UV-A37"""
1348
    VENDOR = "Anysecu"
1349
    MODEL = "UV-A37"
1350

    
1351
    # ==========
1352
    # Notice to developers:
1353
    # The UV-A37 support in this driver is currently based upon v1.24
1354
    # firmware.
1355
    # ==========
1356

    
1357
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
1358
                    chirp_common.PowerLevel("L", watts=1.00)]
1359

    
1360
    VALID_BANDS = [(108000000, 136000000),
1361
                   (136000000, 174000000),
1362
                   (200000000, 260000000),
1363
                   (350000000, 390000000),
1364
                   (400000000, 520000000)]
1365

    
1366
    _magic = b"PROGRAMJC37U"
1367
    _fingerprint = [b"\x00\x00\x00\xE4\x00\x20\x94\x04",
1368
                    b"\x00\x00\x00\xE8\x00\x20\x98\x04"]
1369

    
1370
    _ranges = [
1371
               (0x0000, 0x2000),
1372
               (0x8000, 0x8040),
1373
               (0x9000, 0x9040),
1374
               (0xA000, 0xA140),
1375
               (0xB000, 0xB440)
1376
              ]
1377
    _memsize = 0xB440
1378

    
1379

    
1380
@directory.register
1381
class A36plusRadio(JC8810base):
1382
    """Talkpod A36plus"""
1383
    VENDOR = "Talkpod"
1384
    MODEL = "A36plus"
1385

    
1386
    # ==========
1387
    # Notice to developers:
1388
    # The A36plus support in this driver is currently based upon v1.22
1389
    # firmware.
1390
    # ==========
1391

    
1392
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
1393
                    chirp_common.PowerLevel("L", watts=1.00)]
1394

    
1395
    VALID_BANDS = [(108000000, 136000000),
1396
                   (136000000, 180000000),
1397
                   (200000000, 260000000),
1398
                   (350000000, 400000000),
1399
                   (400000000, 520000000),
1400
                   ]
1401

    
1402
    _magic = b"PROGRAMJC37U"
1403
    _fingerprint = [b"\x00\x00\x00\x42\x00\x20\xF0\x04",
1404
                    b"\x00\x00\x00\x5A\x00\x20\x08\x05",  # fw 1.18
1405
                    b"\x00\x00\x00\x9E\x00\x20\x0C\x05",  # fw 1.22
1406
                    ]
1407

    
1408
    _ranges = [
1409
               (0x0000, 0x4000),
1410
               (0x8000, 0x8040),
1411
               (0x9000, 0x9040),
1412
               (0xA000, 0xA140),
1413
               (0xB000, 0xB440)
1414
              ]
1415
    _memsize = 0xB440
1416
    _upper = 512  # fw 1.22 expands from 256 to 512 channels
1417
    _aninames = 10
1418
    _mem_params = (_upper,  # number of channels
1419
                   _aninames,  # number of aninames
1420
                   )
1421

    
1422

    
1423
@directory.register
1424
class AR730Radio(UVA37Radio):
1425
    """Abbree AR730"""
1426
    VENDOR = "Abbree"
1427
    MODEL = "AR-730"
1428

    
1429
    # ==========
1430
    # Notice to developers:
1431
    # The AR-730 support in this driver is currently based upon v1.24
1432
    # firmware.
1433
    # ==========
1434

    
1435
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
1436
                    chirp_common.PowerLevel("L", watts=1.00)]
1437

    
1438
    VALID_BANDS = [(108000000, 136000000),
1439
                   (136000000, 180000000),
1440
                   (200000000, 260000000),
1441
                   (350000000, 390000000),
1442
                   (400000000, 520000000)]
(14-14/17)