Project

General

Profile

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

Test driver. Download & upload. - Jim Unroe, 03/30/2023 12:34 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];       //     Code
60
  u8 unknown6;        //
61
  char name[12];      // 12-character Alpha Tag
62
} memory[256];
63
"""
64

    
65

    
66
CMD_ACK = b"\x06"
67

    
68
DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
69

    
70
DTMF_CHARS = "0123456789 *#ABCD"
71

    
72
TXPOWER_HIGH = 0x00
73
TXPOWER_LOW = 0x01
74
TXPOWER_MID = 0x02
75

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

    
80

    
81
def _enter_programming_mode(radio):
82
    serial = radio.pipe
83

    
84
    exito = False
85
    for i in range(0, 5):
86
        serial.write(radio._magic)
87
        ack = serial.read(1)
88

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

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

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

    
109
    if ident not in radio._fingerprint:
110
        LOG.debug(util.hexprint(ident))
111
        raise errors.RadioError("Radio returned unknown identification string")
112

    
113

    
114
def _exit_programming_mode(radio):
115
    serial = radio.pipe
116
    try:
117
        serial.write(b"E")
118
    except errors.RadioError:
119
        raise errors.RadioError("Radio refused to exit programming mode")
120

    
121

    
122
def _read_block(radio, block_addr, block_size):
123
    serial = radio.pipe
124

    
125
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
126
    expectedresponse = b"R" + cmd[1:]
127
    LOG.debug("Reading block %04x..." % (block_addr))
128

    
129
    try:
130
        serial.write(cmd)
131
        response = serial.read(4 + block_size)
132
        if response[:4] != expectedresponse:
133
            raise Exception("Error reading block %04x." % (block_addr))
134

    
135
        block_data = response[4:]
136
    except errors.RadioError:
137
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
138

    
139
    return block_data
140

    
141

    
142
def _write_block(radio, block_addr, block_size):
143
    serial = radio.pipe
144

    
145
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
146
    data = radio.get_mmap()[block_addr:block_addr + block_size]
147

    
148
    LOG.debug("Writing Data:")
149
    LOG.debug(util.hexprint(cmd + data))
150

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

    
159

    
160
def do_download(radio):
161
    LOG.debug("download")
162
    _enter_programming_mode(radio)
163

    
164
    data = b""
165

    
166
    status = chirp_common.Status()
167
    status.msg = "Cloning from radio"
168

    
169
    status.cur = 0
170
    status.max = radio._memsize
171

    
172
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
173
        status.cur = addr + radio.BLOCK_SIZE
174
        radio.status_fn(status)
175

    
176
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
177
        data += block
178

    
179
        LOG.debug("Address: %04x" % addr)
180
        LOG.debug(util.hexprint(block))
181

    
182
    _exit_programming_mode(radio)
183

    
184
    return memmap.MemoryMapBytes(data)
185

    
186

    
187
def do_upload(radio):
188
    status = chirp_common.Status()
189
    status.msg = "Uploading to radio"
190

    
191
    _enter_programming_mode(radio)
192

    
193
    status.cur = 0
194
    status.max = radio._memsize
195

    
196
    for start_addr, end_addr in radio._ranges:
197
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
198
            status.cur = addr + radio.BLOCK_SIZE_UP
199
            radio.status_fn(status)
200
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
201

    
202
    _exit_programming_mode(radio)
203

    
204

    
205
class JC8810base(chirp_common.CloneModeRadio):
206
    """MML JC-8810"""
207
    VENDOR = "MML"
208
    MODEL = "JC-8810base"
209
    BAUD_RATE = 57600
210
    NEEDS_COMPAT_SERIAL = False
211
    BLOCK_SIZE = 0x40
212
    BLOCK_SIZE_UP = 0x40
213

    
214
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=10.00),
215
                    chirp_common.PowerLevel("M", watts=8.00),
216
                    chirp_common.PowerLevel("L", watts=4.00)]
217

    
218
    _magic = b"PROGRAMJC81U"
219
    _fingerprint = [b"\x00\x00\x00\x26\x00\x20\xD8\x04",
220
                    b"\x00\x00\x00\x42\x00\x20\xF0\x04"]
221

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

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

    
262
    def process_mmap(self):
263
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
264

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

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

    
292
    def _is_txinh(self, _mem):
293
        raw_tx = ""
294
        for i in range(0, 4):
295
            raw_tx += _mem.txfreq[i].get_raw()
296
        return raw_tx == "\xFF\xFF\xFF\xFF"
297

    
298
    def get_memory(self, number):
299
        _mem = self._memobj.memory[number - 1]
300

    
301
        mem = chirp_common.Memory()
302
        mem.number = number
303

    
304
        if _mem.get_raw()[0] == "\xff":
305
            mem.empty = True
306
            return mem
307

    
308
        mem.freq = int(_mem.rxfreq) * 10
309

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

    
325
        for char in _mem.name:
326
            if str(char) == "\xFF":
327
                char = " "  # may have 0xFF mid-name
328
            mem.name += str(char)
329
        mem.name = mem.name.rstrip()
330

    
331
        dtcs_pol = ["N", "N"]
332

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

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

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

    
375
        mem.dtcs_polarity = "".join(dtcs_pol)
376

    
377
        if not _mem.scan:
378
            mem.skip = "S"
379

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

    
391
        mem.mode = _mem.narrow and "NFM" or "FM"
392

    
393
        mem.extra = RadioSettingGroup("Extra", "extra")
394

    
395
        # BCL (Busy Channel Lockout)
396
        rs = RadioSettingValueBoolean(_mem.bcl)
397
        rset = RadioSetting("bcl", "BCL", rs)
398
        mem.extra.append(rset)
399

    
400
        # PTT-ID
401
        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.pttid])
402
        rset = RadioSetting("pttid", "PTT ID", rs)
403
        mem.extra.append(rset)
404

    
405
        # Signal (DTMF Encoder Group #)
406
        rs = RadioSettingValueList(PTTIDCODE_LIST, PTTIDCODE_LIST[_mem.scode])
407
        rset = RadioSetting("scode", "PTT ID Code", rs)
408
        mem.extra.append(rset)
409

    
410
        # # Encrypt
411
        # rs = RadioSettingValueList(ENCRYPT_LIST, ENCRYPT_LIST[_mem.encrypt])
412
        # rset = RadioSetting("encrypt", "Encrypt", rs)
413
        # mem.extra.append(rset)
414

    
415
        # # Learning
416
        # rs = RadioSettingValueBoolean(_mem.learning)
417
        # rset = RadioSetting("learning", "Learning", rs)
418
        # mem.extra.append(rset)
419

    
420
        # # CODE
421
        # rs = RadioSettingValueInteger(0, 999999, _mem.code)
422
        # rset = RadioSetting("code", "Code", rs)
423
        # mem.extra.append(rset)
424

    
425
        # # ANI
426
        # rs = RadioSettingValueBoolean(_mem.ani)
427
        # rset = RadioSetting("ani", "ANI", rs)
428
        # mem.extra.append(rset)
429

    
430
        return mem
431

    
432
    def set_memory(self, mem):
433
        _mem = self._memobj.memory[mem.number - 1]
434

    
435
        if mem.empty:
436
            _mem.set_raw("\xff" * 32)
437
            return
438

    
439
        _mem.set_raw("\x00" * 16 + "\xFF" * 16)
440

    
441
        _mem.rxfreq = mem.freq / 10
442

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

    
455
        _namelength = self.get_features().valid_name_length
456
        for i in range(_namelength):
457
            try:
458
                _mem.name[i] = mem.name[i]
459
            except IndexError:
460
                _mem.name[i] = "\xFF"
461

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

    
491
        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
492
            _mem.txtone += 0x69
493
        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
494
            _mem.rxtone += 0x69
495

    
496
        _mem.scan = mem.skip != "S"
497
        _mem.narrow = mem.mode == "NFM"
498

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

    
512
        for setting in mem.extra:
513
            if setting.get_name() == "scramble_type":
514
                setattr(_mem, setting.get_name(), int(setting.value) + 8)
515
                setattr(_mem, "scramble_type2", int(setting.value) + 8)
516
            else:
517
                setattr(_mem, setting.get_name(), setting.value)
518

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

    
543
                    if element.has_apply_callback():
544
                        LOG.debug("Using apply callback")
545
                        element.run_apply_callback()
546
                    elif setting == "fmradio":
547
                        setattr(obj, setting, not int(element.value))
548
                    elif element.value.get_mutable():
549
                        LOG.debug("Setting %s = %s" % (setting, element.value))
550
                        setattr(obj, setting, element.value)
551
                except Exception as e:
552
                    LOG.debug(element.get_name(), e)
553
                    raise
554

    
555
    @classmethod
556
    def match_model(cls, filedata, filename):
557
        # This radio has always been post-metadata, so never do
558
        # old-school detection
559
        return False
560

    
561

    
562
@directory.register
563
class RT470Radio(JC8810base):
564
    """Radtel RT-470"""
565
    VENDOR = "Radtel"
566
    MODEL = "RT-470"
567

    
568

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

    
575
    _fingerprint = [b"\x00\x00\x00\xfe\x00\x20\xAC\x04"]
576

    
577
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
578
                    chirp_common.PowerLevel("M", watts=4.00),
579
                    chirp_common.PowerLevel("L", watts=2.00)]
(13-13/54)