Project

General

Profile

New Model #1031 » anytone.py

Dan Smith, 01/09/2014 04:16 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

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

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

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

    
72
#seekto 0x0030;
73
struct {
74
	char serial[16];
75
} serial_no;
76

    
77
#seekto 0x0050;
78
struct {
79
	char date[16];
80
} version;
81

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

    
100
#seekto 0x0540;
101
struct memory memblk1[12];
102

    
103
#seekto 0x2000;
104
struct memory memory[758];
105

    
106
#seekto 0x7ec0;
107
struct memory memblk2[10];
108
"""
109

    
110
class FlagObj(object):
111
    def __init__(self, flagobj, which):
112
        self._flagobj = flagobj
113
        self._which = which
114

    
115
    def _get(self, flag):
116
        return getattr(self._flagobj, "%s_%s" % (self._which, flag))
117

    
118
    def _set(self, flag, value):
119
        return setattr(self._flagobj, "%s_%s" % (self._which, flag), value)
120

    
121
    def get_skip(self):
122
        return self._get("skip")
123

    
124
    def set_skip(self, value):
125
        self._set("skip", value)
126

    
127
    skip = property(get_skip, set_skip)
128

    
129
    def get_pskip(self):
130
        return self._get("pskip")
131

    
132
    def set_pskip(self, value):
133
        self._set("pskip", value)
134

    
135
    pskip = property(get_pskip, set_pskip)
136

    
137
    def set(self):
138
        self._set("unknown", 3)
139
        self._set("skip", 1)
140
        self._set("pskip", 1)
141

    
142
    def clear(self):
143
        self._set("unknown", 0)
144
        self._set("skip", 0)
145
        self._set("pskip", 0)
146

    
147
    def get(self):
148
        return (self._get("unknown") << 2 |
149
                self._get("skip") << 1 |
150
                self._get("pskip"))
151

    
152
    def __repr__(self):
153
        return repr(self._flagobj)
154

    
155
def _is_loc_used(memobj, loc):
156
    return memobj.flags[loc / 2].get_raw() != "\xFF"
157

    
158
def _addr_to_loc(addr):
159
    return (addr - 0x2000) / 32
160

    
161
def _should_send_addr(memobj, addr):
162
    if addr < 0x2000 or addr >= 0x7EC0:
163
        return True
164
    else:
165
        return _is_loc_used(memobj, _addr_to_loc(addr))
166

    
167
def _debug(string):
168
    if "CHIRP_DEBUG" in os.environ or True:
169
        print string
170

    
171
def _echo_write(radio, data):
172
    try:
173
        radio.pipe.write(data)
174
        radio.pipe.read(len(data))
175
    except Exception, e:
176
        print "Error writing to radio: %s" % e
177
        raise errors.RadioError("Unable to write to radio")
178

    
179
def _read(radio, length):
180
    try:
181
        data = radio.pipe.read(length)
182
    except Exception, e:
183
        print "Error reading from radio: %s" % e
184
        raise errors.RadioError("Unable to read from radio")
185

    
186
    if len(data) != length:
187
        print "Short read from radio (%i, expected %i)" % (len(data),
188
                                                           length)
189
        print util.hexprint(data)
190
        raise errors.RadioError("Short read from radio")
191
    return data
192

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

    
195
def _ident(radio):
196
    radio.pipe.setTimeout(1)
197
    _echo_write(radio, "PROGRAM")
198
    response = radio.pipe.read(3)
199
    if response != "QX\x06":
200
        print "Response was:\n%s" % util.hexprint(response)
201
        raise errors.RadioError("Unsupported model")
202
    _echo_write(radio, "\x02")
203
    response = radio.pipe.read(16)
204
    _debug(util.hexprint(response))
205
    if response[1:8] not in valid_model:
206
        print "Response was:\n%s" % util.hexprint(response)
207
        raise errors.RadioError("Unsupported model")
208

    
209
def _finish(radio):
210
    endframe = "\x45\x4E\x44"
211
    _echo_write(radio, endframe)
212
    result = radio.pipe.read(1)
213
    if result != "\x06":
214
        print "Got:\n%s" % util.hexprint(result)
215
        raise errors.RadioError("Radio did not finish cleanly")
216

    
217
def _checksum(data):
218
    cs = 0
219
    for byte in data:
220
        cs += ord(byte)
221
    return cs % 256
222

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

    
258
def _download(radio):
259
    _ident(radio)
260

    
261
    memobj = None
262

    
263
    data = ""
264
    for start, end in radio._ranges:
265
        for addr in range(start, end, 0x10):
266
            if memobj is not None and not _should_send_addr(memobj, addr):
267
                block = "\xFF" * 0x10
268
            else:
269
                block = _send(radio, 'R', addr, 0x10)
270
            data += block
271

    
272
            status = chirp_common.Status()
273
            status.cur = len(data)
274
            status.max = end
275
            status.msg = "Cloning from radio"
276
            radio.status_fn(status)
277

    
278
            if addr == 0x19F0:
279
                memobj = bitwise.parse(_mem_format, data)
280

    
281
    _finish(radio)
282

    
283
    return memmap.MemoryMap(data)
284

    
285
def _upload(radio):
286
    _ident(radio)
287

    
288
    for start, end in radio._ranges:
289
        for addr in range(start, end, 0x10):
290
            if addr < 0x0100:
291
                continue
292
            if not _should_send_addr(radio._memobj, addr):
293
                continue
294
            block = radio._mmap[addr:addr + 0x10]
295
            _send(radio, 'W', addr, len(block), block)
296

    
297
            status = chirp_common.Status()
298
            status.cur = addr
299
            status.max = end
300
            status.msg = "Cloning to radio"
301
            radio.status_fn(status)
302

    
303
    _finish(radio)
304

    
305
TONES = [62.5] + list(chirp_common.TONES)
306
TMODES = ['', 'Tone', 'DTCS']
307
DUPLEXES = ['', '-', '+']
308
MODES = ["FM", "FM", "NFM"]
309
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50),
310
                chirp_common.PowerLevel("Mid1", watts=25),
311
                chirp_common.PowerLevel("Mid2", watts=10),
312
                chirp_common.PowerLevel("Low", watts=5)]
313

    
314

    
315
@directory.register
316
class AnyTone5888UVRadio(chirp_common.CloneModeRadio,
317
                         chirp_common.ExperimentalRadio):
318
    """AnyTone 5888UV"""
319
    VENDOR = "AnyTone"
320
    MODEL = "5888UV"
321
    BAUD_RATE = 9600
322
    _file_ident = "ANYTONE"
323

    
324
    # May try to mirror the OEM behavior later
325
    _ranges = [
326
        (0x0000, 0x8000),
327
        ]
328

    
329
    @classmethod
330
    def get_prompts(cls):
331
        rp = chirp_common.RadioPrompts()
332
        rp.experimental = ("The Anytone driver is currently experimental. "
333
                           "There are no known issues with it, but you should "
334
                           "proceed with caution.")
335
        return rp
336

    
337
    def get_features(self):
338
        rf = chirp_common.RadioFeatures()
339
        rf.has_settings = True
340
        rf.has_bank = False
341
        rf.has_cross = True
342
        rf.has_tuning_step = False
343
        rf.has_rx_dtcs = True
344
        rf.valid_skips = ["", "S", "P"]
345
        rf.valid_modes = ["FM", "NFM", "AM"]
346
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
347
        rf.valid_cross_modes = ['Tone->DTCS', 'DTCS->Tone',
348
                                '->Tone', '->DTCS', 'Tone->Tone']
349
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
350
        rf.valid_bands = [(108000000, 500000000)]
351
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
352
        rf.valid_name_length = 7
353
        rf.valid_power_levels = POWER_LEVELS
354
        rf.memory_bounds = (1, 758)
355
        return rf
356

    
357
    def sync_in(self):
358
        self._mmap = _download(self)
359
        self.process_mmap()
360

    
361
    def sync_out(self):
362
        _upload(self)
363

    
364
    def process_mmap(self):
365
        self._memobj = bitwise.parse(mem_format, self._mmap)
366

    
367
    def _get_memobjs(self, number):
368
        number -= 1
369
        _mem = self._memobj.memory[number]
370
        _flg = FlagObj(self._memobj.flags[number / 2],
371
                       number % 2 and "even" or "odd")
372
        return _mem, _flg
373

    
374
    def _get_dcs_index(self, _mem, which):
375
        base = getattr(_mem, '%scode' % which)
376
        extra = getattr(_mem, '%sdcsextra' % which)
377
        return (int(extra) << 8) | int(base)
378

    
379
    def _set_dcs_index(self, _mem, which, index):
380
        base = getattr(_mem, '%scode' % which)
381
        extra = getattr(_mem, '%sdcsextra' % which)
382
        base.set_value(index & 0xFF)
383
        extra.set_value(index >> 8)
384

    
385
    def get_raw_memory(self, number):
386
        _mem, _flg = self._get_memobjs(number)
387
        return repr(_mem) + repr(_flg)
388

    
389
    def get_memory(self, number):
390
        _mem, _flg = self._get_memobjs(number)
391
        mem = chirp_common.Memory()
392
        mem.number = number
393

    
394
        if _flg.get() == 0x0F:
395
            mem.empty = True
396
            return mem
397

    
398
        mem.freq = int(_mem.freq) * 100
399
        mem.offset = int(_mem.offset) * 100
400
        mem.name = str(_mem.name).rstrip()
401
        mem.duplex = DUPLEXES[_mem.duplex]
402
        mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width]
403

    
404
        rxtone = txtone = None
405
        rxmode = TMODES[_mem.rxtmode]
406
        txmode = TMODES[_mem.txtmode]
407
        if txmode == "Tone":
408
            txtone = TONES[_mem.txtone]
409
        elif txmode == "DTCS":
410
            txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
411
                                                                     'tx')]
412
        if rxmode == "Tone":
413
            rxtone = TONES[_mem.rxtone]
414
        elif rxmode == "DTCS":
415
            rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
416
                                                                     'rx')]
417

    
418
        rxpol = _mem.rxinv and "R" or "N"
419
        txpol = _mem.txinv and "R" or "N"
420

    
421
        chirp_common.split_tone_decode(mem,
422
                                       (txmode, txtone, txpol),
423
                                       (rxmode, rxtone, rxpol))
424

    
425
        mem.skip = _flg.get_skip() and "S" or _flg.get_pskip() and "P" or ""
426
        mem.power = POWER_LEVELS[_mem.power]
427

    
428
        return mem
429

    
430
    def set_memory(self, mem):
431
        _mem, _flg = self._get_memobjs(mem.number)
432
        if mem.empty:
433
            _flg.set()
434
            return
435
        _flg.clear()
436
        _mem.set_raw("\x00" * 32)
437

    
438
        _mem.freq = mem.freq / 100
439
        _mem.offset = mem.offset / 100
440
        _mem.name = mem.name.ljust(7)
441
        _mem.is_am = mem.mode == "AM"
442
        _mem.duplex = DUPLEXES.index(mem.duplex)
443

    
444
        try:
445
            _mem.channel_width = MODES.index(mem.mode)
446
        except ValueError:
447
            _mem.channel_width = 0
448

    
449
        ((txmode, txtone, txpol),
450
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
451

    
452
        _mem.txtmode = TMODES.index(txmode)
453
        _mem.rxtmode = TMODES.index(rxmode)
454
        if txmode == "Tone":
455
            _mem.txtone = TONES.index(txtone)
456
        elif txmode == "DTCS":
457
            self._set_dcs_index(_mem, 'tx',
458
                                chirp_common.ALL_DTCS_CODES.index(txtone))
459
        if rxmode == "Tone":
460
            _mem.rxtone = TONES.index(rxtone)
461
        elif rxmode == "DTCS":
462
            self._set_dcs_index(_mem, 'rx',
463
                                chirp_common.ALL_DTCS_CODES.index(rxtone))
464

    
465
        _mem.txinv = txpol == "R"
466
        _mem.rxinv = rxpol == "R"
467

    
468
        _flg.set_skip(mem.skip == "S")
469
        _flg.set_pskip(mem.skip == "P")
470

    
471
        if mem.power:
472
            _mem.power = POWER_LEVELS.index(mem.power)
473
        else:
474
            _mem.power = 0
475

    
476
    def get_settings(self):
477
        _settings = self._memobj.settings
478
        settings = RadioSettingGroup('all', 'All Settings')
479

    
480
        display = ["Frequency", "Channel", "Name"]
481
        rs = RadioSetting("display", "Display",
482
                          RadioSettingValueList(display,
483
                                                display[_settings.display]))
484
        settings.append(rs)
485

    
486
        apo = ["Off"] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)]
487
        rs = RadioSetting("apo", "Automatic Power Off",
488
                          RadioSettingValueList(apo,
489
                                                apo[_settings.apo]))
490
        settings.append(rs)
491

    
492
        def filter(s):
493
            s_ = ""
494
            for i in range(0, 8):
495
                c = str(s[i])
496
                s_ += (c if c in chirp_common.CHARSET_ASCII else "")
497
            return s_
498

    
499
        rs = RadioSetting("welcome", "Welcome Message",
500
                          RadioSettingValueString(0, 8,
501
                                                  filter(_settings.welcome)))
502
        settings.append(rs)
503

    
504
        rs = RadioSetting("beep", "Beep Enabled",
505
                          RadioSettingValueBoolean(_settings.beep))
506
        settings.append(rs)
507

    
508
        mute = ["Off", "TX", "RX", "TX/RX"]
509
        rs = RadioSetting("mute", "Sub Band Mute",
510
                          RadioSettingValueList(mute,
511
                                                mute[_settings.mute]))
512
        settings.append(rs)
513

    
514
        return settings
515

    
516
    def set_settings(self, settings):
517
        _settings = self._memobj.settings
518
        for element in settings:
519
            name = element.get_name()
520
            setattr(_settings, name, element.value)
521

    
522
    @classmethod
523
    def match_model(cls, filedata, filename):
524
        return cls._file_ident in filedata[0x30:0x40]
525

    
526
@directory.register
527
class IntekHR2040Radio(AnyTone5888UVRadio):
528
    """Intek HR-2040"""
529
    VENDOR = "Intek"
530
    MODEL = "HR-2040"
531
    _file_ident = "HR-2040"
532

    
533
@directory.register
534
class PolmarDB50MRadio(AnyTone5888UVRadio):
535
    """Polmar DB-50M"""
536
    VENDOR = "Polmar"
537
    MODEL = "DB-50M"
538
    _file_ident = "DB-50M"
539

    
540
@directory.register
541
class PolmarDB50MRadio(AnyTone5888UVRadio):
542
    """Powerwerx DB-750X"""
543
    VENDOR = "Powerwerx"
544
    MODEL = "DB-750X"
545
    _file_ident = "DB-750X"
(4-4/8)