Project

General

Profile

New Model #10458 » mml_jc_8810-v0.8.py

add my fingerprint - Juan Manuel Puñal Bargueño, 04/01/2023 12:58 AM

 
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[256];
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:4,        // 9005
77
     tot:4;           //      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_900c:5,   // 900C
90
     pttlt:3;         //      PTT Delay
91
  u8 unused_900d:7,   // 900D
92
     mdfa:1;          //      Channel_A Display
93
  u8 unused_9003:7,   // 900E
94
     mdfb:1;          //      Channel_B Display
95
  u8 unknown_900f;    // 900F
96
  u8 unused_9010:4,   // 9010
97
     autolk:4;        //      Key Auto Lock
98
  u8 unused_9011:6,   // 9011
99
     almode:2;        //      Alarm Mode
100
  u8 unused_9012:7,   // 9012
101
     alarmsound:1;    //      Alarm Sound
102
  u8 unused_9013:6,   // 9013
103
     dualtx:2;        //      Dual TX
104
  u8 unused_9014:7,   // 9014
105
     ste:1;           //      Tail Noise Clear
106
  u8 unused_9015:4,   // 9015
107
     rpste:4;         //      RPT Noise Clear
108
  u8 unused_9016:4,   // 9016
109
     rptrl:4;         //      RPT Noise Delay
110
  u8 unused_9017:7,   // 9017
111
     roger:1;         //      Roger
112
  u8 unknown_9018;    // 9018
113
  u8 unused_9019:7,   // 9019
114
     fmradio:1;       //      FM Radio
115
  u8 unused_901a1:3,  // 901A
116
     vfomrb:1,        //      WorkMode B
117
     unused_901a2:3,  //
118
     vfomra:1;        //      WorkMode A
119
  u8 unused_901b:7,   // 901B
120
     kblock:1;        //      KB Lock
121
  u8 unused_901c:7,   // 901C
122
     ponmsg:1;        //      Power On Msg
123
  u8 unknown_901d;    // 901D
124
  u8 unused_901e:6,   // 901E
125
     tone:2;          //      Pilot
126
  u8 unknown_901f;    // 901F
127
  u8 unused_9020:4,   // 9020
128
     voxd:4;          //      VOX Delay
129
  u8 unused_9021:4,   // 9021
130
     menuquit:4;      //      Menu Auto Quit
131
  u8 unused_9022:7,   // 9022
132
     tailcode:1;      //      Tail Code
133
  u8 unknown_9023;    // 9023
134
  u8 unknown_9024;    // 9024
135
  u8 unknown_9025;    // 9025
136
  u8 unknown_9026;    // 9026
137
  u8 unknown_9027;    // 9027
138
  u8 unknown_9028;    // 9028
139
  u8 unused_9029:6,   // 9029
140
     qtsave:2;        //      QT Save Type
141
  u8 ani;             // 902A ANI
142
  u8 skey2_sp;        // 902B Skey2 Short
143
  u8 skey2_lp;        // 902C Skey2 Long
144
  u8 skey3_sp;        // 902D Skey3 Short
145
  u8 topkey_sp;       // 902E Top Key (RT-470L?)
146
  u8 unused_902f:7,   // 902F
147
     rxendtail:1;     //      Tone Rx End
148
} settings;
149

    
150
#seekto 0xA020;
151
struct {
152
  u8 code[5];         //      5-character DTMF Encoder Groups
153
  u8 unused[11];
154
} pttid[15];
155

    
156
#seekto 0xA006;
157
struct {
158
  u8 unused_a006:4,   // A006
159
     pttid:4;         //      PTT-ID
160
  u8 unused_a007:5,   // A007
161
     dtmfon:3;        //      DTMF Speed (on time)
162
  u8 unused_a008:5,   // A008
163
     dtmfoff:3;       //      DTMF Speed (off time)
164
} dtmf;
165

    
166
#seekto 0xB000;
167
struct {
168
  u8 code[3];         //      3-character ANI Code Groups
169
} anicodes[60];
170

    
171
#seekto 0xB0C0;
172
struct {
173
  char name[6];       //      6-character ANI Code Group Names
174
  u8 unused[10];
175
} aninames[30];
176
"""
177

    
178

    
179
CMD_ACK = b"\x06"
180

    
181
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
182

    
183
DTMF_CHARS = "0123456789 *#ABCD"
184

    
185
TXPOWER_HIGH = 0x00
186
TXPOWER_LOW = 0x01
187
TXPOWER_MID = 0x02
188

    
189
ABR_LIST = ["Off", "5 seconds", "10 seconds", "15 seconds", "20 seconds",
190
            "30 seconds", "1 minute", "2 minutes", "3 minutes"]
191
ALMODE_LIST = ["Site", "Tone", "Code"]
192
AUTOLK_LIST = ABR_LIST[:4]
193
DTMFSPEED_LIST = ["50 ms", "100 ms", "200 ms", "300 ms", "500 ms"]
194
DTMFST_LIST = ["Off", "KeyBoard Side Tone", "ANI Side Tone", "KB ST + ANI ST"]
195
DUALTX_LIST = ["Off", "A", "B"]
196
ENCRYPT_LIST = ["Off", "DCP1", "DCP2", "DCP3"]
197
LANGUAGE_LIST = ["English", "Chinese"]
198
MDF_LIST = ["Channel + Name", "Channel + Frequency"]
199
MENUQUIT_LIST = ["%s seconds" % x for x in range(5, 55, 5)] + ["60 seconds"]
200
OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
201
PONMSG_LIST = ["Logo", "Voltage"]
202
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
203
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
204
PTTLT_LIST = ["None", "100 seconds"] + \
205
             ["%s seconds" % x for x in range(200, 1200, 200)]
206
QTSAVE_LIST = ["All", "TX", "RX"]
207
RPSTE_LIST = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)]
208
SAVE_LIST = ["Off", "Normal", "Super", "Deep"]
209
SCREV_LIST = ["Time (TO)", "Carrier (CO)", "Search (SE)"]
210
TAILCODE_LIST = ["55 Hz", "62.5 Hz"]
211
TONERXEND_LIST = ["Off", "MDC-1200"]
212
TONE_LIST = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
213
TOT_LIST = ["Off"] + ["%s seconds" % x for x in range(30, 270, 30)]
214
VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
215
VOXD_LIST = ["%s seconds" % str(x / 10) for x in range(5, 21)]
216
WORKMODE_LIST = ["VFO Mode", "Channel Mode"]
217

    
218
SKEY_CHOICES = ["FM Radio", "Flashlight", "TX Power Level",
219
                "NOAA Weather", "Scan", "Search"]
220
SKEY_VALUES = [0x07, 0x08, 0x0A, 0x0C, 0x1C, 0x1D]
221

    
222
SKEY2SP_CHOICES = ["PTT B"] + SKEY_CHOICES
223
SKEY2SP_VALUES = [0x01] + SKEY_VALUES
224

    
225
SKEY3LP_CHOICES = ["Monitor"]
226
SKEY3LP_VALUES = [0xFF]
227

    
228
TOPKEYLP_CHOICES = ["Alarm"]
229
TOPKEYLP_VALUES = [0xFF]
230

    
231

    
232
SETTING_LISTS = {
233
    "abr": ABR_LIST,
234
    "almod": ALMODE_LIST,
235
    "autolk": AUTOLK_LIST,
236
    "dtmfoff": DTMFSPEED_LIST,
237
    "dtmfon": DTMFSPEED_LIST,
238
    "dtmfst": DTMFST_LIST,
239
    "dualtx": DUALTX_LIST,
240
    "encrypt": ENCRYPT_LIST,
241
    "language": LANGUAGE_LIST,
242
    "mdfa": MDF_LIST,
243
    "mdfb": MDF_LIST,
244
    "menuquit": MENUQUIT_LIST,
245
    "ponmsg": PONMSG_LIST,
246
    "pttid": PTTID_LIST,
247
    "pttlt": PTTLT_LIST,
248
    "qtsave": QTSAVE_LIST,
249
    "rpste": RPSTE_LIST,
250
    "rptrl": RPSTE_LIST,
251
    "rxendtail": TONERXEND_LIST,
252
    "save": SAVE_LIST,
253
    "scode": PTTIDCODE_LIST,
254
    "screv": SCREV_LIST,
255
    "tailcode": TAILCODE_LIST,
256
    "tone": TONE_LIST,
257
    "tot": TOT_LIST,
258
    "vox": OFF1TO9_LIST,
259
    "voxd": VOXD_LIST,
260
    "workmode": WORKMODE_LIST
261
    }
262

    
263

    
264
def _enter_programming_mode(radio):
265
    serial = radio.pipe
266

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

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

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

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

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

    
296

    
297
def _exit_programming_mode(radio):
298
    serial = radio.pipe
299
    try:
300
        serial.write(b"E")
301
    except errors.RadioError:
302
        raise errors.RadioError("Radio refused to exit programming mode")
303

    
304

    
305
def _read_block(radio, block_addr, block_size):
306
    serial = radio.pipe
307

    
308
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
309
    expectedresponse = b"R" + cmd[1:]
310
    LOG.debug("Reading block %04x..." % (block_addr))
311

    
312
    try:
313
        serial.write(cmd)
314
        response = serial.read(4 + block_size)
315
        if response[:4] != expectedresponse:
316
            raise Exception("Error reading block %04x." % (block_addr))
317

    
318
        block_data = response[4:]
319
    except errors.RadioError:
320
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
321

    
322
    return block_data
323

    
324

    
325
def _write_block(radio, block_addr, block_size):
326
    serial = radio.pipe
327

    
328
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
329
    data = radio.get_mmap()[block_addr:block_addr + block_size]
330

    
331
    LOG.debug("Writing Data:")
332
    LOG.debug(util.hexprint(cmd + data))
333

    
334
    try:
335
        serial.write(cmd + data)
336
        if serial.read(1) != CMD_ACK:
337
            raise Exception("No ACK")
338
    except errors.RadioError:
339
        raise errors.RadioError("Failed to send block "
340
                                "to radio at %04x" % block_addr)
341

    
342

    
343
def do_download(radio):
344
    LOG.debug("download")
345
    _enter_programming_mode(radio)
346

    
347
    data = b""
348

    
349
    status = chirp_common.Status()
350
    status.msg = "Cloning from radio"
351

    
352
    status.cur = 0
353
    status.max = radio._memsize
354

    
355
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
356
        status.cur = addr + radio.BLOCK_SIZE
357
        radio.status_fn(status)
358

    
359
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
360
        data += block
361

    
362
        LOG.debug("Address: %04x" % addr)
363
        LOG.debug(util.hexprint(block))
364

    
365
    _exit_programming_mode(radio)
366

    
367
    return memmap.MemoryMapBytes(data)
368

    
369

    
370
def do_upload(radio):
371
    status = chirp_common.Status()
372
    status.msg = "Uploading to radio"
373

    
374
    _enter_programming_mode(radio)
375

    
376
    status.cur = 0
377
    status.max = radio._memsize
378

    
379
    for start_addr, end_addr in radio._ranges:
380
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
381
            status.cur = addr + radio.BLOCK_SIZE_UP
382
            radio.status_fn(status)
383
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
384

    
385
    _exit_programming_mode(radio)
386

    
387

    
388
def _split(rf, f1, f2):
389
    """Returns False if the two freqs are in the same band (no split)
390
    or True otherwise"""
391

    
392
    # determine if the two freqs are in the same band
393
    for low, high in rf.valid_bands:
394
        if f1 >= low and f1 <= high and \
395
                f2 >= low and f2 <= high:
396
            # if the two freqs are on the same Band this is not a split
397
            return False
398

    
399
    # if you get here is because the freq pairs are split
400
    return True
401

    
402

    
403
class JC8810base(chirp_common.CloneModeRadio):
404
    """MML JC-8810"""
405
    VENDOR = "MML"
406
    MODEL = "JC-8810base"
407
    BAUD_RATE = 57600
408
    NEEDS_COMPAT_SERIAL = False
409
    BLOCK_SIZE = 0x40
410
    BLOCK_SIZE_UP = 0x40
411

    
412
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=10.00),
413
                    chirp_common.PowerLevel("M", watts=8.00),
414
                    chirp_common.PowerLevel("L", watts=4.00)]
415

    
416
    _magic = b"PROGRAMJC81U"
417
    _fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
418
                    b"\x00\x00\x00\x42\x00\x20\xF0\x04"]
419

    
420
    _ranges = [
421
               (0x0000, 0x2000),
422
               (0x8000, 0x8040),
423
               (0x9000, 0x9040),
424
               (0xA000, 0xA140),
425
               (0xB000, 0xB300)
426
              ]
427
    _memsize = 0xB300
428
    _valid_chars = chirp_common.CHARSET_ALPHANUMERIC + \
429
        "`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?"
430

    
431
    def get_features(self):
432
        rf = chirp_common.RadioFeatures()
433
        rf.has_settings = True
434
        rf.has_bank = False
435
        rf.has_ctone = True
436
        rf.has_cross = True
437
        rf.has_rx_dtcs = True
438
        rf.has_tuning_step = False
439
        rf.can_odd_split = True
440
        rf.has_name = True
441
        rf.valid_name_length = 12
442
        rf.valid_characters = self._valid_chars
443
        rf.valid_skips = ["", "S"]
444
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
445
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
446
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
447
        rf.valid_power_levels = self.POWER_LEVELS
448
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
449
        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 KHz.
450
        rf.valid_dtcs_codes = DTCS
451
        rf.memory_bounds = (1, 256)
452
        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
453
        rf.valid_bands = [(108000000, 136000000),
454
                          (136000000, 180000000),
455
                          (200000000, 260000000),
456
                          (330000000, 400000000),
457
                          (400000000, 520000000)]
458
        return rf
459

    
460
    def process_mmap(self):
461
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
462

    
463
    def sync_in(self):
464
        """Download from radio"""
465
        try:
466
            data = do_download(self)
467
        except errors.RadioError:
468
            # Pass through any real errors we raise
469
            raise
470
        except Exception:
471
            # If anything unexpected happens, make sure we raise
472
            # a RadioError and log the problem
473
            LOG.exception('Unexpected error during download')
474
            raise errors.RadioError('Unexpected error communicating '
475
                                    'with the radio')
476
        self._mmap = data
477
        self.process_mmap()
478

    
479
    def sync_out(self):
480
        """Upload to radio"""
481
        try:
482
            do_upload(self)
483
        except Exception:
484
            # If anything unexpected happens, make sure we raise
485
            # a RadioError and log the problem
486
            LOG.exception('Unexpected error during upload')
487
            raise errors.RadioError('Unexpected error communicating '
488
                                    'with the radio')
489

    
490
    def _is_txinh(self, _mem):
491
        raw_tx = ""
492
        for i in range(0, 4):
493
            raw_tx += _mem.txfreq[i].get_raw()
494
        return raw_tx == "\xFF\xFF\xFF\xFF"
495

    
496
    def get_memory(self, number):
497
        """Get the mem representation from the radio image"""
498
        _mem = self._memobj.memory[number - 1]
499

    
500
        # Create a high-level memory object to return to the UI
501
        mem = chirp_common.Memory()
502

    
503
        # Memory number
504
        mem.number = number
505

    
506
        if _mem.get_raw()[0] == "\xff":
507
            mem.empty = True
508
            return mem
509

    
510
        # Freq and offset
511
        mem.freq = int(_mem.rxfreq) * 10
512
        # tx freq can be blank
513
        if _mem.get_raw()[4] == "\xFF":
514
            # TX freq not set
515
            mem.offset = 0
516
            mem.duplex = "off"
517
        else:
518
            # TX freq set
519
            offset = (int(_mem.txfreq) * 10) - mem.freq
520
            if offset != 0:
521
                if _split(self.get_features(), mem.freq, int(
522
                          _mem.txfreq) * 10):
523
                    mem.duplex = "split"
524
                    mem.offset = int(_mem.txfreq) * 10
525
                elif offset < 0:
526
                    mem.offset = abs(offset)
527
                    mem.duplex = "-"
528
                elif offset > 0:
529
                    mem.offset = offset
530
                    mem.duplex = "+"
531
            else:
532
                mem.offset = 0
533

    
534
        for char in _mem.name:
535
            if str(char) == "\xFF":
536
                char = " "  # may have 0xFF mid-name
537
            mem.name += str(char)
538
        mem.name = mem.name.rstrip()
539

    
540
        dtcs_pol = ["N", "N"]
541

    
542
        if _mem.txtone in [0, 0xFFFF]:
543
            txmode = ""
544
        elif _mem.txtone >= 0x0258:
545
            txmode = "Tone"
546
            mem.rtone = int(_mem.txtone) / 10.0
547
        elif _mem.txtone <= 0x0258:
548
            txmode = "DTCS"
549
            if _mem.txtone > 0x69:
550
                index = _mem.txtone - 0x6A
551
                dtcs_pol[0] = "R"
552
            else:
553
                index = _mem.txtone - 1
554
            mem.dtcs = DTCS[index]
555
        else:
556
            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
557

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

    
574
        if txmode == "Tone" and not rxmode:
575
            mem.tmode = "Tone"
576
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
577
            mem.tmode = "TSQL"
578
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
579
            mem.tmode = "DTCS"
580
        elif rxmode or txmode:
581
            mem.tmode = "Cross"
582
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
583

    
584
        mem.dtcs_polarity = "".join(dtcs_pol)
585

    
586
        if not _mem.scan:
587
            mem.skip = "S"
588

    
589
        _levels = self.POWER_LEVELS
590
        if _mem.txpower == TXPOWER_HIGH:
591
            mem.power = _levels[0]
592
        elif _mem.txpower == TXPOWER_MID:
593
            mem.power = _levels[1]
594
        elif _mem.txpower == TXPOWER_LOW:
595
            mem.power = _levels[2]
596
        else:
597
            LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
598
                      (mem.name, _mem.txpower))
599

    
600
        mem.mode = _mem.narrow and "NFM" or "FM"
601

    
602
        mem.extra = RadioSettingGroup("Extra", "extra")
603

    
604
        # BCL (Busy Channel Lockout)
605
        rs = RadioSettingValueBoolean(_mem.bcl)
606
        rset = RadioSetting("bcl", "BCL", rs)
607
        mem.extra.append(rset)
608

    
609
        # PTT-ID
610
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.pttid])
611
        rset = RadioSetting("pttid", "PTT ID", rs)
612
        mem.extra.append(rset)
613

    
614
        # Signal (DTMF Encoder Group #)
615
        rs = RadioSettingValueList(PTTIDCODE_LIST, PTTIDCODE_LIST[_mem.scode])
616
        rset = RadioSetting("scode", "PTT ID Code", rs)
617
        mem.extra.append(rset)
618

    
619
        # # Encrypt
620
        # rs = RadioSettingValueList(ENCRYPT_LIST, ENCRYPT_LIST[_mem.encrypt])
621
        # rset = RadioSetting("encrypt", "Encrypt", rs)
622
        # mem.extra.append(rset)
623

    
624
        # # Learning
625
        # rs = RadioSettingValueBoolean(_mem.learning)
626
        # rset = RadioSetting("learning", "Learning", rs)
627
        # mem.extra.append(rset)
628

    
629
        # # CODE
630
        # rs = RadioSettingValueInteger(0, 999999, _mem.code)
631
        # rset = RadioSetting("code", "Code", rs)
632
        # mem.extra.append(rset)
633

    
634
        # # ANI
635
        # rs = RadioSettingValueBoolean(_mem.ani)
636
        # rset = RadioSetting("ani", "ANI", rs)
637
        # mem.extra.append(rset)
638

    
639
        return mem
640

    
641
    def set_memory(self, mem):
642
        _mem = self._memobj.memory[mem.number - 1]
643

    
644
        if mem.empty:
645
            _mem.set_raw("\xff" * 32)
646
            return
647

    
648
        _mem.set_raw("\x00" * 16 + "\xFF" * 16)
649

    
650
        _mem.rxfreq = mem.freq / 10
651

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

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

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

    
700
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
701
            _mem.txtone += 0x69
702
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
703
            _mem.rxtone += 0x69
704

    
705
        _mem.scan = mem.skip != "S"
706
        _mem.narrow = mem.mode == "NFM"
707

    
708
        _levels = self.POWER_LEVELS
709
        if mem.power is None:
710
            _mem.txpower = TXPOWER_HIGH
711
        elif mem.power == _levels[0]:
712
            _mem.txpower = TXPOWER_HIGH
713
        elif mem.power == _levels[1]:
714
            _mem.txpower = TXPOWER_MID
715
        elif mem.power == _levels[2]:
716
            _mem.txpower = TXPOWER_LOW
717
        else:
718
            LOG.error('%s: set_mem: unhandled power level: %s' %
719
                      (mem.name, mem.power))
720

    
721
        for setting in mem.extra:
722
            if setting.get_name() == "scramble_type":
723
                setattr(_mem, setting.get_name(), int(setting.value) + 8)
724
                setattr(_mem, "scramble_type2", int(setting.value) + 8)
725
            else:
726
                setattr(_mem, setting.get_name(), setting.value)
727

    
728
    def get_settings(self):
729
        _dtmf = self._memobj.dtmf
730
        _settings = self._memobj.settings
731
        basic = RadioSettingGroup("basic", "Basic Settings")
732
        group = RadioSettings(basic)
733

    
734
        # Menu 12: TOT
735
        rs = RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot])
736
        rset = RadioSetting("tot", "Time Out Timer", rs)
737
        basic.append(rset)
738

    
739
        # Menu 00: SQL
740
        rs = RadioSettingValueInteger(0, 9, _settings.sql)
741
        rset = RadioSetting("sql", "Squelch Level", rs)
742
        basic.append(rset)
743

    
744
        # Menu 13: VOX
745
        rs = RadioSettingValueList(OFF1TO9_LIST, OFF1TO9_LIST[_settings.vox])
746
        rset = RadioSetting("vox", "VOX", rs)
747
        basic.append(rset)
748

    
749
        # Menu 39: VOX DELAY
750
        rs = RadioSettingValueList(VOXD_LIST, VOXD_LIST[_settings.voxd])
751
        rset = RadioSetting("voxd", "VOX Delay", rs)
752
        basic.append(rset)
753

    
754
        # Menu 15: VOICE
755
        rs = RadioSettingValueBoolean(_settings.voice)
756
        rset = RadioSetting("voice", "Voice Prompts", rs)
757
        basic.append(rset)
758

    
759
        # Menu 17: LANGUAGE
760
        rs = RadioSettingValueList(LANGUAGE_LIST,
761
                                   LANGUAGE_LIST[_settings.language])
762
        rset = RadioSetting("language", "Voice", rs)
763
        basic.append(rset)
764

    
765
        # Menu 23: ABR
766
        rs = RadioSettingValueList(ABR_LIST, ABR_LIST[_settings.abr])
767
        rset = RadioSetting("abr", "Auto BackLight", rs)
768
        basic.append(rset)
769

    
770
        # Work Mode A
771
        rs = RadioSettingValueList(WORKMODE_LIST,
772
                                   WORKMODE_LIST[_settings.vfomra])
773
        rset = RadioSetting("vfomra", "Work Mode A", rs)
774
        basic.append(rset)
775

    
776
        # Work Mode B
777
        rs = RadioSettingValueList(WORKMODE_LIST,
778
                                   WORKMODE_LIST[_settings.vfomrb])
779
        rset = RadioSetting("vfomrb", "Work Mode B", rs)
780
        basic.append(rset)
781

    
782
        # Menu 19: SC-REV
783
        rs = RadioSettingValueList(SCREV_LIST, SCREV_LIST[_settings.screv])
784
        rset = RadioSetting("screv", "Scan Resume Method", rs)
785
        basic.append(rset)
786

    
787
        # Menu 10: SAVE
788
        rs = RadioSettingValueList(SAVE_LIST,
789
                                   SAVE_LIST[_settings.save])
790
        rset = RadioSetting("save", "Battery Save Mode", rs)
791
        basic.append(rset)
792

    
793
        # Menu 42: MDF-A
794
        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfa])
795
        rset = RadioSetting("mdfa", "Memory Display Format A", rs)
796
        basic.append(rset)
797

    
798
        # Menu 43: MDF-B
799
        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfb])
800
        rset = RadioSetting("mdfb", "Memory Display Format B", rs)
801
        basic.append(rset)
802

    
803
        # Menu 33: DTMFST (DTMF ST)
804
        rs = RadioSettingValueList(DTMFST_LIST, DTMFST_LIST[_settings.dtmfst])
805
        rset = RadioSetting("dtmfst", "DTMF Side Tone", rs)
806
        basic.append(rset)
807

    
808
        # Menu 37: PTT-LT
809
        rs = RadioSettingValueList(PTTLT_LIST, PTTLT_LIST[_settings.pttlt])
810
        rset = RadioSetting("pttlt", "PTT Delay", rs)
811
        basic.append(rset)
812

    
813
        rs = RadioSettingValueInteger(1, 60, _settings.ani + 1)
814
        rset = RadioSetting("ani", "ANI", rs)
815
        basic.append(rset)
816

    
817
        def apply_skey2sp_listvalue(setting, obj):
818
            LOG.debug("Setting value: " + str(setting.value) + " from list")
819
            val = str(setting.value)
820
            index = SKEY2SP_CHOICES.index(val)
821
            val = SKEY2SP_VALUES[index]
822
            obj.set_value(val)
823

    
824
        # Menu 20: PF2
825
        if _settings.skey2_sp in SKEY2SP_VALUES:
826
            idx = SKEY2SP_VALUES.index(_settings.skey2_sp)
827
        else:
828
            idx = SKEY2SP_VALUES.index(0x07)  # default FM
829
        rs = RadioSettingValueList(SKEY2SP_CHOICES, SKEY2SP_CHOICES[idx])
830
        rset = RadioSetting("skey2_sp", "PF2 Key (Short Press)", rs)
831
        rset.set_apply_callback(apply_skey2sp_listvalue, _settings.skey2_sp)
832
        basic.append(rset)
833

    
834
        def apply_skey_listvalue(setting, obj):
835
            LOG.debug("Setting value: " + str(setting.value) + " from list")
836
            val = str(setting.value)
837
            index = SKEY_CHOICES.index(val)
838
            val = SKEY_VALUES[index]
839
            obj.set_value(val)
840

    
841
        # Menu 21: PF2 LONG PRESS
842
        if _settings.skey2_lp in SKEY_VALUES:
843
            idx = SKEY_VALUES.index(_settings.skey2_lp)
844
        else:
845
            idx = SKEY_VALUES.index(0x1D)  # default Search
846
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
847
        rset = RadioSetting("skey2_lp", "PF2 Key (Long Press)", rs)
848
        rset.set_apply_callback(apply_skey_listvalue, _settings.skey2_lp)
849
        basic.append(rset)
850

    
851
        # Menu 22: PF3
852
        if _settings.skey3_sp in SKEY_VALUES:
853
            idx = SKEY_VALUES.index(_settings.skey3_sp)
854
        else:
855
            idx = SKEY_VALUES.index(0x08)  # default Flashlight
856
        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
857
        rset = RadioSetting("skey3_sp", "PF3 Key (Short Press)", rs)
858
        rset.set_apply_callback(apply_skey_listvalue, _settings.skey3_sp)
859
        basic.append(rset)
860

    
861
        # Mneu 36: TONE
862
        rs = RadioSettingValueList(TONE_LIST, TONE_LIST[_settings.tone])
863
        rset = RadioSetting("tone", "Tone-burst Frequency", rs)
864
        basic.append(rset)
865

    
866
        # Mneu 29: POWER ON MSG
867
        rs = RadioSettingValueList(PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])
868
        rset = RadioSetting("ponmsg", "Power On Message", rs)
869
        basic.append(rset)
870

    
871
        rs = RadioSettingValueList(TAILCODE_LIST,
872
                                   TAILCODE_LIST[_settings.tailcode])
873
        rset = RadioSetting("tailcode", "Tail Code", rs)
874
        basic.append(rset)
875

    
876
        # Menu 46: STE
877
        rs = RadioSettingValueBoolean(_settings.ste)
878
        rset = RadioSetting("ste", "Squelch Tail Eliminate (HT to HT)", rs)
879
        basic.append(rset)
880

    
881
        # Menu 40: RP-STE
882
        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rpste])
883
        rset = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)", rs)
884
        basic.append(rset)
885

    
886
        # Menu 41: RPT-RL
887
        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rptrl])
888
        rset = RadioSetting("rptrl", "STE Repeater Delay", rs)
889
        basic.append(rset)
890

    
891
        # Menu 38: MENU EXIT TIME
892
        rs = RadioSettingValueList(MENUQUIT_LIST,
893
                                   MENUQUIT_LIST[_settings.menuquit])
894
        rset = RadioSetting("menuquit", "Menu Auto Quit", rs)
895
        basic.append(rset)
896

    
897
        # Menu 34: AUTOLOCK
898
        rs = RadioSettingValueList(AUTOLK_LIST, AUTOLK_LIST[_settings.autolk])
899
        rset = RadioSetting("autolk", "Key Auto Lock", rs)
900
        basic.append(rset)
901

    
902
        # Menu 28: CDCSS SAVE MODE
903
        rs = RadioSettingValueList(QTSAVE_LIST, QTSAVE_LIST[_settings.qtsave])
904
        rset = RadioSetting("qtsave", "QT Save Type", rs)
905
        basic.append(rset)
906

    
907
        # Menu 45: TX-A/B
908
        rs = RadioSettingValueList(DUALTX_LIST, DUALTX_LIST[_settings.dualtx])
909
        rset = RadioSetting("dualtx", "Dual TX", rs)
910
        basic.append(rset)
911

    
912
        # Menu 47: AL-MODE
913
        rs = RadioSettingValueList(ALMODE_LIST, ALMODE_LIST[_settings.almode])
914
        rset = RadioSetting("almode", "Alarm Mode", rs)
915
        basic.append(rset)
916

    
917
        # Menu 11: ROGER
918
        rs = RadioSettingValueBoolean(_settings.roger)
919
        rset = RadioSetting("roger", "Roger", rs)
920
        basic.append(rset)
921

    
922
        rs = RadioSettingValueBoolean(_settings.alarmsound)
923
        rset = RadioSetting("alarmsound", "Alarm Sound", rs)
924
        basic.append(rset)
925

    
926
        # Menu 44: TDR
927
        rs = RadioSettingValueBoolean(_settings.tdr)
928
        rset = RadioSetting("tdr", "TDR", rs)
929
        basic.append(rset)
930

    
931
        rs = RadioSettingValueBoolean(not _settings.fmradio)
932
        rset = RadioSetting("fmradio", "FM Radio", rs)
933
        basic.append(rset)
934

    
935
        rs = RadioSettingValueBoolean(_settings.kblock)
936
        rset = RadioSetting("kblock", "KB Lock", rs)
937
        basic.append(rset)
938

    
939
        # Menu 16: BEEP PROMPT
940
        rs = RadioSettingValueBoolean(_settings.beep)
941
        rset = RadioSetting("beep", "Beep", rs)
942
        basic.append(rset)
943

    
944
        # Menu 48: RX END TAIL
945
        rs = RadioSettingValueList(TONERXEND_LIST,
946
                                   TONERXEND_LIST[_settings.rxendtail])
947
        rset = RadioSetting("rxendtail", "Tone RX End", rs)
948
        basic.append(rset)
949

    
950
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
951
        group.append(dtmf)
952

    
953
        def apply_code(setting, obj, length):
954
            code = []
955
            for j in range(0, length):
956
                try:
957
                    code.append(DTMF_CHARS.index(str(setting.value)[j]))
958
                except IndexError:
959
                    code.append(0xFF)
960
            obj.code = code
961

    
962
        for i in range(0, 15):
963
            _codeobj = self._memobj.pttid[i].code
964
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
965
            rs = RadioSettingValueString(0, 5, _code, False)
966
            rs.set_charset(DTMF_CHARS)
967
            rset = RadioSetting("pttid/%i.code" % i,
968
                                "PTT-ID Code %i" % (i + 1), rs)
969
            rset.set_apply_callback(apply_code, self._memobj.pttid[i], 5)
970
            dtmf.append(rset)
971

    
972
        rs = RadioSettingValueList(DTMFSPEED_LIST,
973
                                   DTMFSPEED_LIST[_dtmf.dtmfon])
974
        rset = RadioSetting("dtmf.dtmfon", "DTMF Speed (on)", rs)
975
        dtmf.append(rset)
976

    
977
        rs = RadioSettingValueList(DTMFSPEED_LIST,
978
                                   DTMFSPEED_LIST[_dtmf.dtmfoff])
979
        rset = RadioSetting("dtmf.dtmfoff", "DTMF Speed (off)", rs)
980
        dtmf.append(rset)
981

    
982
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_dtmf.pttid])
983
        rset = RadioSetting("dtmf.pttid", "PTT ID", rs)
984
        dtmf.append(rset)
985

    
986
        ani = RadioSettingGroup("ani", "ANI Code List Settings")
987
        group.append(ani)
988

    
989
        def _filter(name):
990
            filtered = ""
991
            for char in str(name):
992
                if char in self._valid_chars:
993
                    filtered += char
994
                else:
995
                    filtered += " "
996
            return filtered
997

    
998
        for i in range(0, 30):
999
            _codeobj = self._memobj.anicodes[i].code
1000
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1001
            rs = RadioSettingValueString(0, 3, _code, False)
1002
            rs.set_charset(DTMF_CHARS)
1003
            rset = RadioSetting("anicodes/%i.code" % i,
1004
                                "ANI Code %i" % (i + 1), rs)
1005
            rset.set_apply_callback(apply_code, self._memobj.anicodes[i], 3)
1006
            ani.append(rset)
1007

    
1008
            _nameobj = "NUM.%i" % (i + 1)
1009
            rs = RadioSettingValueString(0, 6, _nameobj)
1010
            rs.set_mutable(False)
1011
            rset = RadioSetting("aninames/%i.code" % i,
1012
                                "ANI Code %i Name" % (i + 1), rs)
1013
            ani.append(rset)
1014

    
1015
        for i in range(0, 30):
1016
            _codeobj = self._memobj.anicodes[i + 30].code
1017
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
1018
            rs = RadioSettingValueString(0, 3, _code, False)
1019
            rs.set_charset(DTMF_CHARS)
1020
            rset = RadioSetting("anicodes/%i.code" % (i + 30),
1021
                                "ANI Code %i" % (i + 31), rs)
1022
            rset.set_apply_callback(apply_code,
1023
                                    self._memobj.anicodes[i + 30], 3)
1024
            ani.append(rset)
1025

    
1026
            _nameobj = self._memobj.aninames[i].name
1027
            rs = RadioSettingValueString(0, 6, _filter(_nameobj))
1028
            rset = RadioSetting("aninames/%i.name" % i,
1029
                                "ANI Code %i Name" % (i + 31), rs)
1030
            ani.append(rset)
1031

    
1032
        return group
1033

    
1034
    def set_settings(self, settings):
1035
        _settings = self._memobj.settings
1036
        for element in settings:
1037
            if not isinstance(element, RadioSetting):
1038
                self.set_settings(element)
1039
                continue
1040
            else:
1041
                try:
1042
                    name = element.get_name()
1043
                    if "." in name:
1044
                        bits = name.split(".")
1045
                        obj = self._memobj
1046
                        for bit in bits[:-1]:
1047
                            if "/" in bit:
1048
                                bit, index = bit.split("/", 1)
1049
                                index = int(index)
1050
                                obj = getattr(obj, bit)[index]
1051
                            else:
1052
                                obj = getattr(obj, bit)
1053
                        setting = bits[-1]
1054
                    else:
1055
                        obj = _settings
1056
                        setting = element.get_name()
1057

    
1058
                    if element.has_apply_callback():
1059
                        LOG.debug("Using apply callback")
1060
                        element.run_apply_callback()
1061
                    elif setting == "ani":
1062
                        setattr(obj, setting, int(element.value) - 1)
1063
                    elif setting == "fmradio":
1064
                        setattr(obj, setting, not int(element.value))
1065
                    elif element.value.get_mutable():
1066
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1067
                        setattr(obj, setting, element.value)
1068
                except Exception as e:
1069
                    LOG.debug(element.get_name(), e)
1070
                    raise
1071

    
1072
    @classmethod
1073
    def match_model(cls, filedata, filename):
1074
        # This radio has always been post-metadata, so never do
1075
        # old-school detection
1076
        return False
1077

    
1078

    
1079
@directory.register
1080
class RT470Radio(JC8810base):
1081
    """Radtel RT-470"""
1082
    VENDOR = "Radtel"
1083
    MODEL = "RT-470"
1084

    
1085

    
1086
@directory.register
1087
class RT470LRadio(JC8810base):
1088
    """Radtel RT-470L"""
1089
    VENDOR = "Radtel"
1090
    MODEL = "RT-470L"
1091

    
1092
    _fingerprint = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04",
1093
                    b"\x00\x00\x00\x20\x00\x20\xCC\x04"]
1094

    
1095
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
1096
                    chirp_common.PowerLevel("M", watts=4.00),
1097
                    chirp_common.PowerLevel("L", watts=2.00)]
(17-17/54)