Project

General

Profile

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

Test driver. Download only. - Jim Unroe, 03/28/2023 12:10 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
        LOG.debug(util.hexprint(ident))
114
        raise errors.RadioError("Radio returned unknown identification string")
115

    
116

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

    
124

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

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

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

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

    
142
    return block_data
143

    
144

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

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

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

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

    
162

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

    
167
    data = b""
168

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

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

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

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

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

    
185
    _exit_programming_mode(radio)
186

    
187
    return memmap.MemoryMapBytes(data)
188

    
189

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

    
194
    _enter_programming_mode(radio)
195

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

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

    
205
    _exit_programming_mode(radio)
206

    
207

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

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

    
221
    _magic = b"PROGRAMJC81U"
222
    _fingerprint = b"\x00\x00\x00\x26\x00\x20\xD8\x04"
223

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

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

    
264
    def process_mmap(self):
265
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
266

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

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

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

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

    
303
        mem = chirp_common.Memory()
304
        mem.number = number
305

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

    
310
        mem.freq = int(_mem.rxfreq) * 10
311

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

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

    
333
        dtcs_pol = ["N", "N"]
334

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

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

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

    
377
        mem.dtcs_polarity = "".join(dtcs_pol)
378

    
379
        if not _mem.scan:
380
            mem.skip = "S"
381

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

    
393
        mem.mode = _mem.narrow and "NFM" or "FM"
394

    
395
        mem.extra = RadioSettingGroup("Extra", "extra")
396

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

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

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

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

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

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

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

    
432
        return mem
433

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

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

    
441
        _mem.set_raw("\x00" * 16 + "\xFF" * 16)
442

    
443
        _mem.rxfreq = mem.freq / 10
444

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

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

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

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

    
498
        _mem.scan = mem.skip != "S"
499
        _mem.narrow = mem.mode == "NFM"
500

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

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

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

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

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

    
565

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

    
572

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

    
579
    POWER_LEVELS = [chirp_common.PowerLevel("H", watts=5.00),
580
                    chirp_common.PowerLevel("M", watts=4.00),
581
                    chirp_common.PowerLevel("L", watts=2.00)]
(2-2/54)