Project

General

Profile

Bug #7835 » anytone_steps.py

Test driver module for Dave - Jim Unroe, 06/13/2020 05:58 PM

 
1
# Copyright 2013 Dan Smith <dsmith@danplanet.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 3 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 os
17
import struct
18
import time
19
import logging
20

    
21
from chirp import bitwise
22
from chirp import chirp_common
23
from chirp import directory
24
from chirp import errors
25
from chirp import memmap
26
from chirp import util
27
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
28
    RadioSettingValueList, RadioSettingValueString, RadioSettingValueBoolean
29

    
30

    
31
LOG = logging.getLogger(__name__)
32

    
33
_mem_format = """
34
#seekto 0x0100;
35
struct {
36
  u8 even_unknown:2,
37
     even_pskip:1,
38
     even_skip:1,
39
     odd_unknown:2,
40
     odd_pskip:1,
41
     odd_skip:1;
42
} flags[379];
43
"""
44

    
45
mem_format = _mem_format + """
46
struct memory {
47
  bbcd freq[4];
48
  bbcd offset[4];
49
  u8 unknownA:4,
50
     tune_step:4;
51
  u8 rxdcsextra:1,
52
     txdcsextra:1,
53
     rxinv:1,
54
     txinv:1,
55
     channel_width:2,
56
     unknownB:2;
57
  u8 unknown8:3,
58
     is_am:1,
59
     power:2,
60
     duplex:2;
61
  u8 unknown4:4,
62
     rxtmode:2,
63
     txtmode:2;
64
  u8 unknown5:2,
65
     txtone:6;
66
  u8 unknown6:2,
67
     rxtone:6;
68
  u8 txcode;
69
  u8 rxcode;
70
  u8 unknown7[2];
71
  u8 unknown2[5];
72
  char name[7];
73
  u8 unknownZ[2];
74
};
75

    
76
#seekto 0x0030;
77
struct {
78
  char serial[16];
79
} serial_no;
80

    
81
#seekto 0x0050;
82
struct {
83
  char date[16];
84
} version;
85

    
86
#seekto 0x0280;
87
struct {
88
  u8 unknown1:6,
89
     display:2;
90
  u8 unknown2[11];
91
  u8 unknown3:3,
92
     apo:5;
93
  u8 unknown4a[2];
94
  u8 unknown4b:6,
95
     mute:2;
96
  u8 unknown4;
97
  u8 unknown5:5,
98
     beep:1,
99
     unknown6:2;
100
  u8 unknown[334];
101
  char welcome[8];
102
} settings;
103

    
104
#seekto 0x0540;
105
struct memory memblk1[12];
106

    
107
#seekto 0x2000;
108
struct memory memory[758];
109

    
110
#seekto 0x7ec0;
111
struct memory memblk2[10];
112
"""
113

    
114

    
115
class FlagObj(object):
116
    def __init__(self, flagobj, which):
117
        self._flagobj = flagobj
118
        self._which = which
119

    
120
    def _get(self, flag):
121
        return getattr(self._flagobj, "%s_%s" % (self._which, flag))
122

    
123
    def _set(self, flag, value):
124
        return setattr(self._flagobj, "%s_%s" % (self._which, flag), value)
125

    
126
    def get_skip(self):
127
        return self._get("skip")
128

    
129
    def set_skip(self, value):
130
        self._set("skip", value)
131

    
132
    skip = property(get_skip, set_skip)
133

    
134
    def get_pskip(self):
135
        return self._get("pskip")
136

    
137
    def set_pskip(self, value):
138
        self._set("pskip", value)
139

    
140
    pskip = property(get_pskip, set_pskip)
141

    
142
    def set(self):
143
        self._set("unknown", 3)
144
        self._set("skip", 1)
145
        self._set("pskip", 1)
146

    
147
    def clear(self):
148
        self._set("unknown", 0)
149
        self._set("skip", 0)
150
        self._set("pskip", 0)
151

    
152
    def get(self):
153
        return (self._get("unknown") << 2 |
154
                self._get("skip") << 1 |
155
                self._get("pskip"))
156

    
157
    def __repr__(self):
158
        return repr(self._flagobj)
159

    
160

    
161
def _is_loc_used(memobj, loc):
162
    return memobj.flags[loc / 2].get_raw() != "\xFF"
163

    
164

    
165
def _addr_to_loc(addr):
166
    return (addr - 0x2000) / 32
167

    
168

    
169
def _should_send_addr(memobj, addr):
170
    if addr < 0x2000 or addr >= 0x7EC0:
171
        return True
172
    else:
173
        return _is_loc_used(memobj, _addr_to_loc(addr))
174

    
175

    
176
def _echo_write(radio, data):
177
    try:
178
        radio.pipe.write(data)
179
        radio.pipe.read(len(data))
180
    except Exception, e:
181
        LOG.error("Error writing to radio: %s" % e)
182
        raise errors.RadioError("Unable to write to radio")
183

    
184

    
185
def _read(radio, length):
186
    try:
187
        data = radio.pipe.read(length)
188
    except Exception, e:
189
        LOG.error("Error reading from radio: %s" % e)
190
        raise errors.RadioError("Unable to read from radio")
191

    
192
    if len(data) != length:
193
        LOG.error("Short read from radio (%i, expected %i)" %
194
                  (len(data), length))
195
        LOG.debug(util.hexprint(data))
196
        raise errors.RadioError("Short read from radio")
197
    return data
198

    
199
valid_model = ['QX588UV', 'HR-2040', 'DB-50M\x00', 'DB-750X']
200

    
201

    
202
def _ident(radio):
203
    radio.pipe.timeout = 1
204
    _echo_write(radio, "PROGRAM")
205
    response = radio.pipe.read(3)
206
    if response != "QX\x06":
207
        LOG.debug("Response was:\n%s" % util.hexprint(response))
208
        raise errors.RadioError("Unsupported model")
209
    _echo_write(radio, "\x02")
210
    response = radio.pipe.read(16)
211
    LOG.debug(util.hexprint(response))
212
    if response[1:8] not in valid_model:
213
        LOG.debug("Response was:\n%s" % util.hexprint(response))
214
        raise errors.RadioError("Unsupported model")
215

    
216

    
217
def _finish(radio):
218
    endframe = "\x45\x4E\x44"
219
    _echo_write(radio, endframe)
220
    result = radio.pipe.read(1)
221
    if result != "\x06":
222
        LOG.debug("Got:\n%s" % util.hexprint(result))
223
        raise errors.RadioError("Radio did not finish cleanly")
224

    
225

    
226
def _checksum(data):
227
    cs = 0
228
    for byte in data:
229
        cs += ord(byte)
230
    return cs % 256
231

    
232

    
233
def _send(radio, cmd, addr, length, data=None):
234
    frame = struct.pack(">cHb", cmd, addr, length)
235
    if data:
236
        frame += data
237
        frame += chr(_checksum(frame[1:]))
238
        frame += "\x06"
239
    _echo_write(radio, frame)
240
    LOG.debug("Sent:\n%s" % util.hexprint(frame))
241
    if data:
242
        result = radio.pipe.read(1)
243
        if result != "\x06":
244
            LOG.debug("Ack was: %s" % repr(result))
245
            raise errors.RadioError(
246
                "Radio did not accept block at %04x" % addr)
247
        return
248
    result = _read(radio, length + 6)
249
    LOG.debug("Got:\n%s" % util.hexprint(result))
250
    header = result[0:4]
251
    data = result[4:-2]
252
    ack = result[-1]
253
    if ack != "\x06":
254
        LOG.debug("Ack was: %s" % repr(ack))
255
        raise errors.RadioError("Radio NAK'd block at %04x" % addr)
256
    _cmd, _addr, _length = struct.unpack(">cHb", header)
257
    if _addr != addr or _length != _length:
258
        LOG.debug("Expected/Received:")
259
        LOG.debug(" Length: %02x/%02x" % (length, _length))
260
        LOG.debug(" Addr: %04x/%04x" % (addr, _addr))
261
        raise errors.RadioError("Radio send unexpected block")
262
    cs = _checksum(result[1:-2])
263
    if cs != ord(result[-2]):
264
        LOG.debug("Calculated: %02x" % cs)
265
        LOG.debug("Actual:     %02x" % ord(result[-2]))
266
        raise errors.RadioError("Block at 0x%04x failed checksum" % addr)
267
    return data
268

    
269

    
270
def _download(radio):
271
    _ident(radio)
272

    
273
    memobj = None
274

    
275
    data = ""
276
    for start, end in radio._ranges:
277
        for addr in range(start, end, 0x10):
278
            if memobj is not None and not _should_send_addr(memobj, addr):
279
                block = "\xFF" * 0x10
280
            else:
281
                block = _send(radio, 'R', addr, 0x10)
282
            data += block
283

    
284
            status = chirp_common.Status()
285
            status.cur = len(data)
286
            status.max = end
287
            status.msg = "Cloning from radio"
288
            radio.status_fn(status)
289

    
290
            if addr == 0x19F0:
291
                memobj = bitwise.parse(_mem_format, data)
292

    
293
    _finish(radio)
294

    
295
    return memmap.MemoryMap(data)
296

    
297

    
298
def _upload(radio):
299
    _ident(radio)
300

    
301
    for start, end in radio._ranges:
302
        for addr in range(start, end, 0x10):
303
            if addr < 0x0100:
304
                continue
305
            if not _should_send_addr(radio._memobj, addr):
306
                continue
307
            block = radio._mmap[addr:addr + 0x10]
308
            _send(radio, 'W', addr, len(block), block)
309

    
310
            status = chirp_common.Status()
311
            status.cur = addr
312
            status.max = end
313
            status.msg = "Cloning to radio"
314
            radio.status_fn(status)
315

    
316
    _finish(radio)
317

    
318

    
319
TONES = [62.5] + list(chirp_common.TONES)
320
TMODES = ['', 'Tone', 'DTCS', '']
321
DUPLEXES = ['', '-', '+', '']
322
MODES = ["FM", "FM", "NFM"]
323
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50),
324
                chirp_common.PowerLevel("Mid1", watts=25),
325
                chirp_common.PowerLevel("Mid2", watts=10),
326
                chirp_common.PowerLevel("Low", watts=5)]
327

    
328

    
329
@directory.register
330
class AnyTone5888UVRadio(chirp_common.CloneModeRadio,
331
                         chirp_common.ExperimentalRadio):
332
    """AnyTone 5888UV"""
333
    VENDOR = "AnyTone"
334
    MODEL = "5888UV"
335
    BAUD_RATE = 9600
336
    _file_ident = "QX588UV"
337

    
338
    # May try to mirror the OEM behavior later
339
    _ranges = [
340
        (0x0000, 0x8000),
341
        ]
342

    
343
    @classmethod
344
    def get_prompts(cls):
345
        rp = chirp_common.RadioPrompts()
346
        rp.experimental = ("The Anytone driver is currently experimental. "
347
                           "There are no known issues with it, but you should "
348
                           "proceed with caution.")
349
        return rp
350

    
351
    def get_features(self):
352
        rf = chirp_common.RadioFeatures()
353
        rf.has_settings = True
354
        rf.has_bank = False
355
        rf.has_cross = True
356
        rf.valid_tuning_steps = [2.5, 5, 6.25, 10, 12.5, 15, 20, 25, 30, 50]
357
        rf.has_tuning_step = False
358
        rf.has_rx_dtcs = True
359
        rf.valid_skips = ["", "S", "P"]
360
        rf.valid_modes = ["FM", "NFM", "AM"]
361
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
362
        rf.valid_cross_modes = ['Tone->DTCS', 'DTCS->Tone',
363
                                '->Tone', '->DTCS', 'Tone->Tone']
364
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
365
        rf.valid_bands = [(108000000, 500000000)]
366
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
367
        rf.valid_name_length = 7
368
        rf.valid_power_levels = POWER_LEVELS
369
        rf.memory_bounds = (1, 758)
370
        return rf
371

    
372
    def sync_in(self):
373
        self._mmap = _download(self)
374
        self.process_mmap()
375

    
376
    def sync_out(self):
377
        _upload(self)
378

    
379
    def process_mmap(self):
380
        self._memobj = bitwise.parse(mem_format, self._mmap)
381

    
382
    def _get_memobjs(self, number):
383
        number -= 1
384
        _mem = self._memobj.memory[number]
385
        _flg = FlagObj(self._memobj.flags[number / 2],
386
                       number % 2 and "even" or "odd")
387
        return _mem, _flg
388

    
389
    def _get_dcs_index(self, _mem, which):
390
        base = getattr(_mem, '%scode' % which)
391
        extra = getattr(_mem, '%sdcsextra' % which)
392
        return (int(extra) << 8) | int(base)
393

    
394
    def _set_dcs_index(self, _mem, which, index):
395
        base = getattr(_mem, '%scode' % which)
396
        extra = getattr(_mem, '%sdcsextra' % which)
397
        base.set_value(index & 0xFF)
398
        extra.set_value(index >> 8)
399

    
400
    def get_raw_memory(self, number):
401
        _mem, _flg = self._get_memobjs(number)
402
        return repr(_mem) + repr(_flg)
403

    
404
    def get_memory(self, number):
405
        _mem, _flg = self._get_memobjs(number)
406
        mem = chirp_common.Memory()
407
        mem.number = number
408

    
409
        if _flg.get() == 0x0F:
410
            mem.empty = True
411
            return mem
412

    
413
        mem.freq = int(_mem.freq) * 100
414

    
415
        # compensate for 6.25 and 12.5 kHz tuning steps, add 500 Hz if needed
416
        lastdigit = int(_mem.freq) % 10
417
        if (lastdigit == 2 or lastdigit == 7):
418
            mem.freq += 50
419

    
420
        mem.offset = int(_mem.offset) * 100
421
        mem.name = str(_mem.name).rstrip()
422
        mem.duplex = DUPLEXES[_mem.duplex]
423
        mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width]
424

    
425
        rxtone = txtone = None
426
        rxmode = TMODES[_mem.rxtmode]
427
        txmode = TMODES[_mem.txtmode]
428
        if txmode == "Tone":
429
            txtone = TONES[_mem.txtone]
430
        elif txmode == "DTCS":
431
            txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
432
                                                                     'tx')]
433
        if rxmode == "Tone":
434
            rxtone = TONES[_mem.rxtone]
435
        elif rxmode == "DTCS":
436
            rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
437
                                                                     'rx')]
438

    
439
        rxpol = _mem.rxinv and "R" or "N"
440
        txpol = _mem.txinv and "R" or "N"
441

    
442
        chirp_common.split_tone_decode(mem,
443
                                       (txmode, txtone, txpol),
444
                                       (rxmode, rxtone, rxpol))
445

    
446
        mem.skip = _flg.get_skip() and "S" or _flg.get_pskip() and "P" or ""
447
        mem.power = POWER_LEVELS[_mem.power]
448

    
449
        return mem
450

    
451
    def set_memory(self, mem):
452
        _mem, _flg = self._get_memobjs(mem.number)
453
        if mem.empty:
454
            _flg.set()
455
            return
456
        _flg.clear()
457
        _mem.set_raw("\x00" * 32)
458

    
459
        _mem.freq = mem.freq / 100
460
        _mem.offset = mem.offset / 100
461
        _mem.name = mem.name.ljust(7)
462
        _mem.is_am = mem.mode == "AM"
463
        _mem.duplex = DUPLEXES.index(mem.duplex)
464

    
465
        try:
466
            _mem.channel_width = MODES.index(mem.mode)
467
        except ValueError:
468
            _mem.channel_width = 0
469

    
470
        ((txmode, txtone, txpol),
471
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
472

    
473
        _mem.txtmode = TMODES.index(txmode)
474
        _mem.rxtmode = TMODES.index(rxmode)
475
        if txmode == "Tone":
476
            _mem.txtone = TONES.index(txtone)
477
        elif txmode == "DTCS":
478
            self._set_dcs_index(_mem, 'tx',
479
                                chirp_common.ALL_DTCS_CODES.index(txtone))
480
        if rxmode == "Tone":
481
            _mem.rxtone = TONES.index(rxtone)
482
        elif rxmode == "DTCS":
483
            self._set_dcs_index(_mem, 'rx',
484
                                chirp_common.ALL_DTCS_CODES.index(rxtone))
485

    
486
        _mem.txinv = txpol == "R"
487
        _mem.rxinv = rxpol == "R"
488

    
489
        _flg.set_skip(mem.skip == "S")
490
        _flg.set_pskip(mem.skip == "P")
491

    
492
        if mem.power:
493
            _mem.power = POWER_LEVELS.index(mem.power)
494
        else:
495
            _mem.power = 0
496

    
497
    def get_settings(self):
498
        _settings = self._memobj.settings
499
        basic = RadioSettingGroup("basic", "Basic")
500
        settings = RadioSettings(basic)
501

    
502
        display = ["Frequency", "Channel", "Name"]
503
        rs = RadioSetting("display", "Display",
504
                          RadioSettingValueList(display,
505
                                                display[_settings.display]))
506
        basic.append(rs)
507

    
508
        apo = ["Off"] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)]
509
        rs = RadioSetting("apo", "Automatic Power Off",
510
                          RadioSettingValueList(apo,
511
                                                apo[_settings.apo]))
512
        basic.append(rs)
513

    
514
        def filter(s):
515
            s_ = ""
516
            for i in range(0, 8):
517
                c = str(s[i])
518
                s_ += (c if c in chirp_common.CHARSET_ASCII else "")
519
            return s_
520

    
521
        rs = RadioSetting("welcome", "Welcome Message",
522
                          RadioSettingValueString(0, 8,
523
                                                  filter(_settings.welcome)))
524
        basic.append(rs)
525

    
526
        rs = RadioSetting("beep", "Beep Enabled",
527
                          RadioSettingValueBoolean(_settings.beep))
528
        basic.append(rs)
529

    
530
        mute = ["Off", "TX", "RX", "TX/RX"]
531
        rs = RadioSetting("mute", "Sub Band Mute",
532
                          RadioSettingValueList(mute,
533
                                                mute[_settings.mute]))
534
        basic.append(rs)
535

    
536
        return settings
537

    
538
    def set_settings(self, settings):
539
        _settings = self._memobj.settings
540
        for element in settings:
541
            if not isinstance(element, RadioSetting):
542
                self.set_settings(element)
543
                continue
544
            name = element.get_name()
545
            setattr(_settings, name, element.value)
546

    
547
    @classmethod
548
    def match_model(cls, filedata, filename):
549
        return cls._file_ident in filedata[0x20:0x40]
550

    
551

    
552
@directory.register
553
class IntekHR2040Radio(AnyTone5888UVRadio):
554
    """Intek HR-2040"""
555
    VENDOR = "Intek"
556
    MODEL = "HR-2040"
557
    _file_ident = "HR-2040"
558

    
559

    
560
@directory.register
561
class PolmarDB50MRadio(AnyTone5888UVRadio):
562
    """Polmar DB-50M"""
563
    VENDOR = "Polmar"
564
    MODEL = "DB-50M"
565
    _file_ident = "DB-50M"
566

    
567

    
568
@directory.register
569
class PowerwerxDB750XRadio(AnyTone5888UVRadio):
570
    """Powerwerx DB-750X"""
571
    VENDOR = "Powerwerx"
572
    MODEL = "DB-750X"
573
    _file_ident = "DB-750X"
574

    
575
    def get_settings(self):
576
        return {}
(2-2/2)