Project

General

Profile

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

Test driver. Download only. Adds fingerprint for RT-470L. - Jim Unroe, 03/29/2023 01:12 PM

 
1
# Copyright 2021-2022 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 os
18
import struct
19
import time
20

    
21
from chirp import (
22
    bitwise,
23
    chirp_common,
24
    directory,
25
    errors,
26
    memmap,
27
    util,
28
)
29
from chirp.settings import (
30
    RadioSetting,
31
    RadioSettingGroup,
32
    RadioSettings,
33
    RadioSettingValueBoolean,
34
    RadioSettingValueFloat,
35
    RadioSettingValueInteger,
36
    RadioSettingValueList,
37
    RadioSettingValueString,
38
)
39

    
40
LOG = logging.getLogger(__name__)
41

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

    
68

    
69
CMD_ACK = b"\x06"
70

    
71
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
72

    
73
DTMF_CHARS = "0123456789 *#ABCD"
74

    
75
TXPOWER_HIGH = 0x00
76
TXPOWER_LOW = 0x01
77
TXPOWER_MID = 0x02
78

    
79
ENCRYPT_LIST = ["Off", "DCP1", "DCP2", "DCP3"]
80
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
81
PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
82

    
83

    
84
def _enter_programming_mode(radio):
85
    serial = radio.pipe
86

    
87
    exito = False
88
    for i in range(0, 5):
89
        serial.write(radio._magic)
90
        ack = serial.read(1)
91

    
92
        try:
93
            if ack == CMD_ACK:
94
                exito = True
95
                break
96
        except errors.RadioError:
97
            LOG.debug("Attempt #%s, failed, trying again" % i)
98
            pass
99

    
100
    # check if we had EXITO
101
    if exito is False:
102
        msg = "The radio did not accept program mode after five tries.\n"
103
        msg += "Check you interface cable and power cycle your radio."
104
        raise errors.RadioError(msg)
105

    
106
    try:
107
        serial.write(b"F")
108
        ident = serial.read(8)
109
    except errors.RadioError:
110
        raise errors.RadioError("Error communicating with radio")
111

    
112
    #if not ident == radio._fingerprint:
113
    if not ident in radio._fingerprint:
114
        LOG.debug(util.hexprint(ident))
115
        raise errors.RadioError("Radio returned unknown identification string")
116

    
117

    
118
def _exit_programming_mode(radio):
119
    serial = radio.pipe
120
    try:
121
        serial.write(b"E")
122
    except errors.RadioError:
123
        raise errors.RadioError("Radio refused to exit programming mode")
124

    
125

    
126
def _read_block(radio, block_addr, block_size):
127
    serial = radio.pipe
128

    
129
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
130
    expectedresponse = b"R" + cmd[1:]
131
    LOG.debug("Reading block %04x..." % (block_addr))
132

    
133
    try:
134
        serial.write(cmd)
135
        response = serial.read(4 + block_size)
136
        if response[:4] != expectedresponse:
137
            raise Exception("Error reading block %04x." % (block_addr))
138

    
139
        block_data = response[4:]
140
    except errors.RadioError:
141
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
142

    
143
    return block_data
144

    
145

    
146
def _write_block(radio, block_addr, block_size):
147
    serial = radio.pipe
148

    
149
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
150
    data = radio.get_mmap()[block_addr:block_addr + block_size]
151

    
152
    LOG.debug("Writing Data:")
153
    LOG.debug(util.hexprint(cmd + data))
154

    
155
    try:
156
        serial.write(cmd + data)
157
        if serial.read(1) != CMD_ACK:
158
            raise Exception("No ACK")
159
    except errors.RadioError:
160
        raise errors.RadioError("Failed to send block "
161
                                "to radio at %04x" % block_addr)
162

    
163

    
164
def do_download(radio):
165
    LOG.debug("download")
166
    _enter_programming_mode(radio)
167

    
168
    data = b""
169

    
170
    status = chirp_common.Status()
171
    status.msg = "Cloning from radio"
172

    
173
    status.cur = 0
174
    status.max = radio._memsize
175

    
176
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
177
        status.cur = addr + radio.BLOCK_SIZE
178
        radio.status_fn(status)
179

    
180
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
181
        data += block
182

    
183
        LOG.debug("Address: %04x" % addr)
184
        LOG.debug(util.hexprint(block))
185

    
186
    _exit_programming_mode(radio)
187

    
188
    return memmap.MemoryMapBytes(data)
189

    
190

    
191
def do_upload(radio):
192
    status = chirp_common.Status()
193
    status.msg = "Uploading to radio"
194

    
195
    _enter_programming_mode(radio)
196

    
197
    status.cur = 0
198
    status.max = radio._memsize
199

    
200
    for start_addr, end_addr in radio._ranges:
201
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
202
            status.cur = addr + radio.BLOCK_SIZE_UP
203
            radio.status_fn(status)
204
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
205

    
206
    _exit_programming_mode(radio)
207

    
208

    
209
class JC8810base(chirp_common.CloneModeRadio):
210
    """MML JC-8810"""
211
    VENDOR = "MML"
212
    MODEL = "JC-8810base"
213
    BAUD_RATE = 57600
214
    NEEDS_COMPAT_SERIAL = False
215
    BLOCK_SIZE = 0x40
216
    BLOCK_SIZE_UP = 0x40
217

    
218
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=10.00),
219
                    chirp_common.PowerLevel("M", watts=8.00),
220
                    chirp_common.PowerLevel("L", watts=4.00)]
221

    
222
    _magic = b"PROGRAMJC81U"
223
    _fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
224
                    b"\x00\x00\x00\x42\x00\x20\xF0\x04"]
225

    
226
    _ranges = [
227
               (0x0000, 0x2000),
228
               (0x8000, 0x8040),
229
               (0x9000, 0x9040),
230
               (0xA000, 0xA140),
231
               (0xB000, 0xB300)
232
              ]
233
    _memsize = 0xB300
234
    _valid_chars = chirp_common.CHARSET_ALPHANUMERIC + \
235
        "`~!@#$%^&*()-=_+[]\\{}|;':\",./<>?"
236

    
237
    def get_features(self):
238
        rf = chirp_common.RadioFeatures()
239
        rf.has_settings = False
240
        rf.has_bank = False
241
        rf.has_ctone = True
242
        rf.has_cross = True
243
        rf.has_rx_dtcs = True
244
        rf.has_tuning_step = False
245
        rf.can_odd_split = True
246
        rf.has_name = True
247
        rf.valid_name_length = 12
248
        rf.valid_characters = self._valid_chars
249
        rf.valid_skips = ["", "S"]
250
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
251
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
252
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
253
        rf.valid_power_levels = self.POWER_LEVELS
254
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
255
        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 KHz.
256
        rf.valid_dtcs_codes = DTCS
257
        rf.memory_bounds = (1, 256)
258
        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
259
        rf.valid_bands = [(108000000, 136000000),
260
                          (136000000, 174000000),
261
                          (220000000, 260000000),
262
                          (350000000, 390000000),
263
                          (400000000, 520000000)]
264
        return rf
265

    
266
    def process_mmap(self):
267
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
268

    
269
    def sync_in(self):
270
        """Download from radio"""
271
        try:
272
            data = do_download(self)
273
        except errors.RadioError:
274
            # Pass through any real errors we raise
275
            raise
276
        except Exception:
277
            # If anything unexpected happens, make sure we raise
278
            # a RadioError and log the problem
279
            LOG.exception('Unexpected error during download')
280
            raise errors.RadioError('Unexpected error communicating '
281
                                    'with the radio')
282
        self._mmap = data
283
        self.process_mmap()
284

    
285
    def sync_out(self):
286
        """Upload to radio"""
287
        try:
288
            do_upload(self)
289
        except Exception:
290
            # If anything unexpected happens, make sure we raise
291
            # a RadioError and log the problem
292
            LOG.exception('Unexpected error during upload')
293
            raise errors.RadioError('Unexpected error communicating '
294
                                    'with the radio')
295

    
296
    def _is_txinh(self, _mem):
297
        raw_tx = ""
298
        for i in range(0, 4):
299
            raw_tx += _mem.txfreq[i].get_raw()
300
        return raw_tx == "\xFF\xFF\xFF\xFF"
301

    
302
    def get_memory(self, number):
303
        _mem = self._memobj.memory[number - 1]
304

    
305
        mem = chirp_common.Memory()
306
        mem.number = number
307

    
308
        if _mem.get_raw()[0] == "\xff":
309
            mem.empty = True
310
            return mem
311

    
312
        mem.freq = int(_mem.rxfreq) * 10
313

    
314
        if self._is_txinh(_mem):
315
            # TX freq not set
316
            mem.duplex = "off"
317
            mem.offset = 0
318
        else:
319
            # TX freq set
320
            offset = (int(_mem.txfreq) * 10) - mem.freq
321
            if offset != 0:
322
                if offset > 0:
323
                    mem.duplex = "+"
324
                    mem.offset = 5000000
325
            else:
326
                mem.duplex = ""
327
                mem.offset = 0
328

    
329
        for char in _mem.name:
330
            if str(char) == "\xFF":
331
                char = " "  # may have 0xFF mid-name
332
            mem.name += str(char)
333
        mem.name = mem.name.rstrip()
334

    
335
        dtcs_pol = ["N", "N"]
336

    
337
        if _mem.txtone in [0, 0xFFFF]:
338
            txmode = ""
339
        elif _mem.txtone >= 0x0258:
340
            txmode = "Tone"
341
            mem.rtone = int(_mem.txtone) / 10.0
342
        elif _mem.txtone <= 0x0258:
343
            txmode = "DTCS"
344
            if _mem.txtone > 0x69:
345
                index = _mem.txtone - 0x6A
346
                dtcs_pol[0] = "R"
347
            else:
348
                index = _mem.txtone - 1
349
            mem.dtcs = DTCS[index]
350
        else:
351
            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
352

    
353
        if _mem.rxtone in [0, 0xFFFF]:
354
            rxmode = ""
355
        elif _mem.rxtone >= 0x0258:
356
            rxmode = "Tone"
357
            mem.ctone = int(_mem.rxtone) / 10.0
358
        elif _mem.rxtone <= 0x0258:
359
            rxmode = "DTCS"
360
            if _mem.rxtone >= 0x6A:
361
                index = _mem.rxtone - 0x6A
362
                dtcs_pol[1] = "R"
363
            else:
364
                index = _mem.rxtone - 1
365
            mem.rx_dtcs = DTCS[index]
366
        else:
367
            LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
368

    
369
        if txmode == "Tone" and not rxmode:
370
            mem.tmode = "Tone"
371
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
372
            mem.tmode = "TSQL"
373
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
374
            mem.tmode = "DTCS"
375
        elif rxmode or txmode:
376
            mem.tmode = "Cross"
377
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
378

    
379
        mem.dtcs_polarity = "".join(dtcs_pol)
380

    
381
        if not _mem.scan:
382
            mem.skip = "S"
383

    
384
        _levels = self.POWER_LEVELS
385
        if _mem.txpower == TXPOWER_HIGH:
386
            mem.power = _levels[0]
387
        elif _mem.txpower == TXPOWER_MID:
388
            mem.power = _levels[1]
389
        elif _mem.txpower == TXPOWER_LOW:
390
            mem.power = _levels[2]
391
        else:
392
            LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
393
                      (mem.name, _mem.txpower))
394

    
395
        mem.mode = _mem.narrow and "NFM" or "FM"
396

    
397
        mem.extra = RadioSettingGroup("Extra", "extra")
398

    
399
        # BCL (Busy Channel Lockout)
400
        rs = RadioSettingValueBoolean(_mem.bcl)
401
        rset = RadioSetting("bcl", "BCL", rs)
402
        mem.extra.append(rset)
403

    
404
        # PTT-ID
405
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.pttid])
406
        rset = RadioSetting("pttid", "PTT ID", rs)
407
        mem.extra.append(rset)
408

    
409
        # Signal (DTMF Encoder Group #)
410
        rs = RadioSettingValueList(PTTIDCODE_LIST, PTTIDCODE_LIST[_mem.scode])
411
        rset = RadioSetting("scode", "PTT ID Code", rs)
412
        mem.extra.append(rset)
413

    
414
        # # Encrypt
415
        # rs = RadioSettingValueList(ENCRYPT_LIST, ENCRYPT_LIST[_mem.encrypt])
416
        # rset = RadioSetting("encrypt", "Encrypt", rs)
417
        # mem.extra.append(rset)
418

    
419
        # # Learning
420
        # rs = RadioSettingValueBoolean(_mem.learning)
421
        # rset = RadioSetting("learning", "Learning", rs)
422
        # mem.extra.append(rset)
423

    
424
        # # CODE
425
        # rs = RadioSettingValueInteger(0, 999999, _mem.code)
426
        # rset = RadioSetting("code", "Code", rs)
427
        # mem.extra.append(rset)
428

    
429
        # # ANI
430
        # rs = RadioSettingValueBoolean(_mem.ani)
431
        # rset = RadioSetting("ani", "ANI", rs)
432
        # mem.extra.append(rset)
433

    
434
        return mem
435

    
436
    def set_memory(self, mem):
437
        _mem = self._memobj.memory[mem.number - 1]
438

    
439
        if mem.empty:
440
            _mem.set_raw("\xff" * 32)
441
            return
442

    
443
        _mem.set_raw("\x00" * 16 + "\xFF" * 16)
444

    
445
        _mem.rxfreq = mem.freq / 10
446

    
447
        if mem.duplex == "off":
448
            for i in range(0, 4):
449
                _mem.txfreq[i].set_raw("\xFF")
450
        elif mem.duplex == "split":
451
            _mem.txfreq = mem.offset / 10
452
        elif mem.duplex == "+":
453
            _mem.txfreq = (mem.freq + mem.offset) / 10
454
        elif mem.duplex == "-":
455
            _mem.txfreq = (mem.freq - mem.offset) / 10
456
        else:
457
            _mem.txfreq = mem.freq / 10
458

    
459
        _namelength = self.get_features().valid_name_length
460
        for i in range(_namelength):
461
            try:
462
                _mem.name[i] = mem.name[i]
463
            except IndexError:
464
                _mem.name[i] = "\xFF"
465

    
466
        rxmode = txmode = ""
467
        if mem.tmode == "Tone":
468
            _mem.txtone = int(mem.rtone * 10)
469
            _mem.rxtone = 0
470
        elif mem.tmode == "TSQL":
471
            _mem.txtone = int(mem.ctone * 10)
472
            _mem.rxtone = int(mem.ctone * 10)
473
        elif mem.tmode == "DTCS":
474
            rxmode = txmode = "DTCS"
475
            _mem.txtone = DTCS.index(mem.dtcs) + 1
476
            _mem.rxtone = DTCS.index(mem.dtcs) + 1
477
        elif mem.tmode == "Cross":
478
            txmode, rxmode = mem.cross_mode.split("->", 1)
479
            if txmode == "Tone":
480
                _mem.txtone = int(mem.rtone * 10)
481
            elif txmode == "DTCS":
482
                _mem.txtone = DTCS.index(mem.dtcs) + 1
483
            else:
484
                _mem.txtone = 0
485
            if rxmode == "Tone":
486
                _mem.rxtone = int(mem.ctone * 10)
487
            elif rxmode == "DTCS":
488
                _mem.rxtone = DTCS.index(mem.rx_dtcs) + 1
489
            else:
490
                _mem.rxtone = 0
491
        else:
492
            _mem.rxtone = 0
493
            _mem.txtone = 0
494

    
495
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
496
            _mem.txtone += 0x69
497
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
498
            _mem.rxtone += 0x69
499

    
500
        _mem.scan = mem.skip != "S"
501
        _mem.narrow = mem.mode == "NFM"
502

    
503
        _levels = self.POWER_LEVELS
504
        if mem.power is None:
505
            _mem.txpower = TXPOWER_HIGH
506
        elif mem.power == _levels[0]:
507
            _mem.txpower = TXPOWER_HIGH
508
        elif mem.power == _levels[1]:
509
            _mem.txpower = TXPOWER_MID
510
        elif mem.power == _levels[2]:
511
            _mem.txpower = TXPOWER_LOW
512
        else:
513
            LOG.error('%s: set_mem: unhandled power level: %s' %
514
                      (mem.name, mem.power))
515

    
516
        for setting in mem.extra:
517
            if setting.get_name() == "scramble_type":
518
                setattr(_mem, setting.get_name(), int(setting.value) + 8)
519
                setattr(_mem, "scramble_type2", int(setting.value) + 8)
520
            else:
521
                setattr(_mem, setting.get_name(), setting.value)
522

    
523
    def set_settings(self, settings):
524
        _settings = self._memobj.settings
525
        for element in settings:
526
            if not isinstance(element, RadioSetting):
527
                self.set_settings(element)
528
                continue
529
            else:
530
                try:
531
                    name = element.get_name()
532
                    if "." in name:
533
                        bits = name.split(".")
534
                        obj = self._memobj
535
                        for bit in bits[:-1]:
536
                            if "/" in bit:
537
                                bit, index = bit.split("/", 1)
538
                                index = int(index)
539
                                obj = getattr(obj, bit)[index]
540
                            else:
541
                                obj = getattr(obj, bit)
542
                        setting = bits[-1]
543
                    else:
544
                        obj = _settings
545
                        setting = element.get_name()
546

    
547
                    if element.has_apply_callback():
548
                        LOG.debug("Using apply callback")
549
                        element.run_apply_callback()
550
                    elif setting == "fmradio":
551
                        setattr(obj, setting, not int(element.value))
552
                    elif setting == "tot":
553
                        setattr(obj, setting, int(element.value) + 1)
554
                    elif element.value.get_mutable():
555
                        LOG.debug("Setting %s = %s" % (setting, element.value))
556
                        setattr(obj, setting, element.value)
557
                except Exception as e:
558
                    LOG.debug(element.get_name(), e)
559
                    raise
560

    
561
    @classmethod
562
    def match_model(cls, filedata, filename):
563
        # This radio has always been post-metadata, so never do
564
        # old-school detection
565
        return False
566

    
567

    
568
@directory.register
569
class RT470Radio(JC8810base):
570
    """Radtel RT-470"""
571
    VENDOR = "Radtel"
572
    MODEL = "RT-470"
573

    
574

    
575
@directory.register
576
class RT470LRadio(JC8810base):
577
    """Radtel RT-470L"""
578
    VENDOR = "Radtel"
579
    MODEL = "RT-470L"
580

    
581
    _fingerprint = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04"]
582

    
583
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
584
                    chirp_common.PowerLevel("M", watts=4.00),
585
                    chirp_common.PowerLevel("L", watts=2.00)]
(7-7/54)