Project

General

Profile

Bug #341 » alinco.py

Tom Hayward, 11/02/2012 10:29 AM

 
1
# Copyright 2011 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
from chirp import chirp_common, bitwise, memmap, errors, directory, util
17

    
18
import time
19

    
20
DRX35_MEM_FORMAT = """
21
#seekto 0x0120;
22
u8 used_flags[25];
23

    
24
#seekto 0x0200;
25
struct {
26
  u8 unknown1:2,
27
     isnarrow:1,
28
     unknown9:1,
29
     ishigh:1,
30
     unknown2:3;
31
  u8 unknown3:6,
32
     duplex:2;
33
  u8 unknown4:4,
34
     tmode:4;
35
  u8 unknown5:4,
36
     step:4;
37
  bbcd freq[4];
38
  u8 unknown6[1];
39
  bbcd offset[3];
40
  u8 rtone;
41
  u8 ctone;
42
  u8 dtcs_tx;
43
  u8 dtcs_rx;
44
  u8 name[7];
45
  u8 unknown8[9];
46
} memory[100];
47

    
48
#seekto 0x0130;
49
u8 skips[25];
50
"""
51

    
52
# Response length is:
53
# 1. \r\n
54
# 2. Four-digit address, followed by a colon
55
# 3. 16 bytes in hex (32 characters)
56
# 4. \r\n
57
RLENGTH = 2 + 5 + 32 + 2
58

    
59
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0]
60

    
61
def isascii(data):
62
    for byte in data:
63
        if (ord(byte) < ord(" ") or ord(byte) > ord("~")) and \
64
                byte not in "\r\n":
65
            return False
66
    return True
67

    
68
def tohex(data):
69
    if isascii(data):
70
        return repr(data)
71
    string = ""
72
    for byte in data:
73
        string += "%02X" % ord(byte)
74
    return string
75

    
76
class AlincoStyleRadio(chirp_common.CloneModeRadio):
77
    """Base class for all known Alinco radios"""
78
    _memsize = 0
79
    _model = "NONE"
80

    
81
    def _send(self, data):
82
        print "PC->R: (%2i) %s" % (len(data), tohex(data))
83
        self.pipe.write(data)
84
        self.pipe.read(len(data))
85

    
86
    def _read(self, length):
87
        data = self.pipe.read(length)
88
        print "R->PC: (%2i) %s" % (len(data), tohex(data))
89
        return data
90

    
91
    def _download_chunk(self, addr):
92
        if addr % 16:
93
            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
94

    
95
        cmd = "AL~F%04XR\r\n" % addr
96
        self._send(cmd)
97

    
98
        resp = self._read(RLENGTH).strip()
99
        if len(resp) == 0:
100
            raise errors.RadioError("No response from radio")
101
        if ":" not in resp:
102
            raise errors.RadioError("Unexpected response from radio")
103
        addr, _data = resp.split(":", 1)
104
        data = ""
105
        for i in range(0, len(_data), 2):
106
            data += chr(int(_data[i:i+2], 16))
107

    
108
        if len(data) != 16:
109
            print "Response was:"
110
            print "|%s|"
111
            print "Which I converted to:"
112
            print util.hexprint(data)
113
            raise Exception("Radio returned less than 16 bytes")
114

    
115
        return data
116

    
117
    def _download(self, limit):
118
        self._identify()
119

    
120
        data = ""
121
        for addr in range(0, limit, 16):
122
            data += self._download_chunk(addr)
123
            time.sleep(0.1)
124

    
125
            if self.status_fn:
126
                status = chirp_common.Status()
127
                status.cur = addr + 16
128
                status.max = self._memsize
129
                status.msg = "Downloading from radio"
130
                self.status_fn(status)
131

    
132
        self._send("AL~E\r\n")
133
        self._read(20)
134

    
135
        return memmap.MemoryMap(data)
136

    
137
    def _identify(self):
138
        for _i in range(0, 3):
139
            self._send("%s\r\n" % self._model)
140
            resp = self._read(6)
141
            if resp.strip() == "OK":
142
                return True
143
            time.sleep(1)
144

    
145
        return False
146

    
147
    def _upload_chunk(self, addr):
148
        if addr % 16:
149
            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
150

    
151
        _data = self._mmap[addr:addr+16]
152
        data = "".join(["%02X" % ord(x) for x in _data])
153

    
154
        cmd = "AL~F%04XW%s\r\n" % (addr, data)
155
        self._send(cmd)
156

    
157
    def _upload(self, limit):
158
        if not self._identify():
159
            raise Exception("I can't talk to this model")
160

    
161
        for addr in range(0x100, limit, 16):
162
            self._upload_chunk(addr)
163
            time.sleep(0.1)
164

    
165
            if self.status_fn:
166
                status = chirp_common.Status()
167
                status.cur = addr + 16
168
                status.max = self._memsize
169
                status.msg = "Uploading to radio"
170
                self.status_fn(status)
171

    
172
        self._send("AL~E\r\n")
173
        self.pipe._read(20)
174

    
175
    def process_mmap(self):
176
        self._memobj = bitwise.parse(DRX35_MEM_FORMAT, self._mmap)
177

    
178
    def sync_in(self):
179
        try:
180
            self._mmap = self._download(self._memsize)
181
        except errors.RadioError:
182
            raise
183
        except Exception, e:
184
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
185
        self.process_mmap()
186

    
187
    def sync_out(self):
188
        try:
189
            self._upload(self._memsize)
190
        except errors.RadioError:
191
            raise
192
        except Exception, e:
193
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
194

    
195
    def get_raw_memory(self, number):
196
        return repr(self._memobj.memory[number])
197

    
198
DUPLEX = ["", "-", "+"]
199
TMODES = ["", "Tone", "", "TSQL"] + [""] * 12
200
TMODES[12] = "DTCS"
201
DCS_CODES = {
202
    "Alinco" : chirp_common.DTCS_CODES,
203
    "Jetstream" : [17] + chirp_common.DTCS_CODES,
204
}
205

    
206
CHARSET = (["\x00"] * 0x30) + \
207
    [chr(x + ord("0")) for x in range(0, 10)] + \
208
    [chr(x + ord("A")) for x in range(0, 26)] + [" "] + \
209
    list("\x00" * 128)
210

    
211
def _get_name(_mem):
212
    name = ""
213
    for i in _mem.name:
214
        if not i:
215
            break
216
        name += CHARSET[i]
217
    return name
218

    
219
def _set_name(mem, _mem):
220
    name = [0x00] * 7
221
    j = 0
222
    for i in range(0, 7):
223
        try:
224
            name[j] = CHARSET.index(mem.name[i])
225
            j += 1
226
        except IndexError:
227
            pass
228
        except ValueError:
229
            pass
230
    return name
231

    
232
ALINCO_TONES = list(chirp_common.TONES)
233
ALINCO_TONES.remove(159.8)
234
ALINCO_TONES.remove(165.5)
235
ALINCO_TONES.remove(171.3)
236
ALINCO_TONES.remove(177.3)
237
ALINCO_TONES.remove(183.5)
238
ALINCO_TONES.remove(189.9)
239
ALINCO_TONES.remove(196.6)
240
ALINCO_TONES.remove(199.5)
241
ALINCO_TONES.remove(206.5)
242
ALINCO_TONES.remove(229.1)
243
ALINCO_TONES.remove(254.1)
244

    
245
class DRx35Radio(AlincoStyleRadio):
246
    """Base class for the DR-x35 radios"""
247
    _range = [(118000000, 155000000)]
248
    _power_levels = []
249
    _valid_tones = ALINCO_TONES
250

    
251
    def get_features(self):
252
        rf = chirp_common.RadioFeatures()
253
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
254
        rf.valid_modes = ["FM", "NFM"]
255
        rf.valid_skips = ["", "S"]
256
        rf.valid_bands = self._range
257
        rf.memory_bounds = (0, 99)
258
        rf.has_ctone = True
259
        rf.has_bank = False
260
        rf.has_dtcs_polarity = False
261
        rf.valid_tuning_steps = STEPS
262
        rf.valid_name_length = 7
263
        rf.valid_power_levels = self._power_levels
264
        return rf
265

    
266
    def get_memory(self, number):
267
        _mem = self._memobj.memory[number]
268
        _skp = self._memobj.skips[number / 8]
269
        _usd = self._memobj.used_flags[number / 8]
270
        bit = (0x80 >> (number % 8))
271

    
272
        mem = chirp_common.Memory()
273
        mem.number = number
274
        if not _usd & bit and self.MODEL != "JT220M":
275
            mem.empty = True
276
            return mem
277

    
278
        mem.freq = int(_mem.freq) * 100
279
        mem.rtone = self._valid_tones[_mem.rtone]
280
        mem.ctone = self._valid_tones[_mem.ctone]
281
        mem.duplex = DUPLEX[_mem.duplex]
282
        mem.offset = int(_mem.offset) * 100
283
        mem.tmode = TMODES[_mem.tmode]
284
        mem.dtcs = DCS_CODES[self.VENDOR][_mem.dtcs_tx]
285
        mem.tuning_step = STEPS[_mem.step]
286

    
287
        if _mem.isnarrow:
288
            mem.mode = "NFM"
289

    
290
        if self._power_levels:
291
            mem.power = self._power_levels[_mem.ishigh]
292

    
293
        if _skp & bit:
294
            mem.skip = "S"
295

    
296
        mem.name = _get_name(_mem).rstrip()
297

    
298
        return mem
299

    
300
    def set_memory(self, mem):
301
        _mem = self._memobj.memory[mem.number]
302
        _skp = self._memobj.skips[mem.number / 8]
303
        _usd = self._memobj.used_flags[mem.number / 8]
304
        bit = (0x80 >> (mem.number % 8))
305

    
306
        if mem.empty:
307
            _usd &= ~bit
308
            return
309
        else:
310
            _usd |= bit
311

    
312
        _mem.freq = mem.freq / 100
313

    
314
        try:
315
            _tone = mem.rtone
316
            _mem.rtone = self._valid_tones.index(mem.rtone)
317
            _tone = mem.ctone
318
            _mem.ctone = self._valid_tones.index(mem.ctone)
319
        except ValueError:
320
            raise errors.UnsupportedToneError("This radio does not support " +
321
                                              "tone %.1fHz" % _tone)
322

    
323
        _mem.duplex = DUPLEX.index(mem.duplex)
324
        _mem.offset = mem.offset / 100
325
        _mem.tmode = TMODES.index(mem.tmode)
326
        _mem.dtcs_tx = DCS_CODES[self.VENDOR].index(mem.dtcs)
327
        _mem.dtcs_rx = DCS_CODES[self.VENDOR].index(mem.dtcs)
328
        _mem.step = STEPS.index(mem.tuning_step)
329

    
330
        _mem.isnarrow = mem.mode == "NFM"
331
        if self._power_levels:
332
            _mem.ishigh = mem.power is None or \
333
                mem.power == self._power_levels[1]
334

    
335
        if mem.skip:
336
            _skp |= bit
337
        else:
338
            _skp &= ~bit
339

    
340
        _mem.name = _set_name(mem, _mem)
341

    
342
@directory.register
343
class DR03Radio(DRx35Radio):
344
    """Alinco DR03"""
345
    VENDOR = "Alinco"
346
    MODEL = "DR03T"
347

    
348
    _model = "DR135"
349
    _memsize = 4096
350
    _range = [(28000000, 29695000)]
351

    
352
    @classmethod
353
    def match_model(cls, filedata, filename):
354
        return len(filedata) == cls._memsize and \
355
            filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x28)
356

    
357
@directory.register
358
class DR06Radio(DRx35Radio):
359
    """Alinco DR06"""
360
    VENDOR = "Alinco"
361
    MODEL = "DR06T"
362

    
363
    _model = "DR435"
364
    _memsize = 4096
365
    _range = [(50000000, 53995000)]
366

    
367
    @classmethod
368
    def match_model(cls, filedata, filename):
369
        return len(filedata) == cls._memsize and \
370
            filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x50)
371
            
372
@directory.register
373
class DR135Radio(DRx35Radio):
374
    """Alinco DR135"""
375
    VENDOR = "Alinco"
376
    MODEL = "DR135T"
377

    
378
    _model = "DR135"
379
    _memsize = 4096
380
    _range = [(118000000, 173000000)]
381

    
382
    @classmethod
383
    def match_model(cls, filedata, filename):
384
        return len(filedata) == cls._memsize and \
385
            filedata[0x64] == chr(0x01) and filedata[0x65] == chr(0x44)
386

    
387
@directory.register
388
class DR235Radio(DRx35Radio):
389
    """Alinco DR235"""
390
    VENDOR = "Alinco"
391
    MODEL = "DR235T"
392

    
393
    _model = "DR235"
394
    _memsize = 4096
395
    _range = [(216000000, 280000000)]
396

    
397
    @classmethod
398
    def match_model(cls, filedata, filename):
399
        return len(filedata) == cls._memsize and \
400
            filedata[0x64] == chr(0x02) and filedata[0x65] == chr(0x22)
401

    
402
@directory.register
403
class DR435Radio(DRx35Radio):
404
    """Alinco DR435"""
405
    VENDOR = "Alinco"
406
    MODEL = "DR435T"
407

    
408
    _model = "DR435"
409
    _memsize = 4096
410
    _range = [(350000000, 511000000)]
411

    
412
    @classmethod
413
    def match_model(cls, filedata, filename):
414
        return len(filedata) == cls._memsize and \
415
            filedata[0x64] == chr(0x04) and filedata[0x65] == chr(0x00)
416

    
417
@directory.register
418
class DJ596Radio(DRx35Radio):
419
    """Alinco DJ596"""
420
    VENDOR = "Alinco"
421
    MODEL = "DJ596"
422

    
423
    _model = "DJ596"
424
    _memsize = 4096
425
    _range = [(136000000, 174000000), (400000000, 511000000)]
426
    _power_levels = [chirp_common.PowerLevel("Low", watts=1.00),
427
                     chirp_common.PowerLevel("High", watts=5.00)]
428

    
429
    @classmethod
430
    def match_model(cls, filedata, filename):
431
        return len(filedata) == cls._memsize and \
432
            filedata[0x64] == chr(0x45) and filedata[0x65] == chr(0x01)
433

    
434
@directory.register
435
class JT220MRadio(DRx35Radio):
436
    """Jetstream JT220"""
437
    VENDOR = "Jetstream"
438
    MODEL = "JT220M"
439

    
440
    _model = "DR136"
441
    _memsize = 8192
442
    _range = [(216000000, 280000000)]
443

    
444
    @classmethod
445
    def match_model(cls, filedata, filename):
446
        return len(filedata) == cls._memsize and \
447
            filedata[0x60:0x64] == "2009"
(2-2/3)