1
|
|
2
|
import struct
|
3
|
import logging
|
4
|
from chirp.drivers import icf
|
5
|
from chirp import chirp_common, util, errors, bitwise, directory
|
6
|
from chirp.memmap import MemoryMap
|
7
|
from chirp.drivers.icf import IcomBankModel
|
8
|
|
9
|
LOG = logging.getLogger(__name__)
|
10
|
|
11
|
MEM_FORMAT = """
|
12
|
bbcd number[2];
|
13
|
u8 unknown1;
|
14
|
lbcd freq[5];
|
15
|
u8 unknown2:5,
|
16
|
mode:3;
|
17
|
"""
|
18
|
MEM_VFO_FORMAT = """
|
19
|
u8 vfo;
|
20
|
bbcd number[2];
|
21
|
u8 unknown1;
|
22
|
lbcd freq[5];
|
23
|
u8 unknown2:5,
|
24
|
mode:3;
|
25
|
u8 unknown1;
|
26
|
u8 unknown2:2,
|
27
|
duplex:2,
|
28
|
unknown3:1,
|
29
|
tmode:3;
|
30
|
u8 unknown4;
|
31
|
bbcd rtone[2];
|
32
|
u8 unknown5;
|
33
|
bbcd ctone[2];
|
34
|
u8 unknown6[2];
|
35
|
bbcd dtcs;
|
36
|
u8 unknown[17];
|
37
|
char name[9];
|
38
|
"""
|
39
|
mem_duptone_format = """
|
40
|
bbcd number[2];
|
41
|
u8 unknown1;
|
42
|
lbcd freq[5];
|
43
|
u8 unknown2:5,
|
44
|
mode:3;
|
45
|
u8 unknown1;
|
46
|
u8 unknown2:2,
|
47
|
duplex:2,
|
48
|
unknown3:1,
|
49
|
tmode:3;
|
50
|
u8 unknown4;
|
51
|
bbcd rtone[2];
|
52
|
u8 unknown5;
|
53
|
bbcd ctone[2];
|
54
|
u8 unknown6[2];
|
55
|
bbcd dtcs;
|
56
|
u8 unknown[11];
|
57
|
char name[9];
|
58
|
"""
|
59
|
|
60
|
MEM_IC7100_FORMAT = """
|
61
|
u8 bank; // 1 bank number
|
62
|
bbcd number[2]; // 2,3
|
63
|
u8 splitSelect; // 4 split and select memory settings
|
64
|
lbcd freq[5]; // 5-9 operating freq
|
65
|
u8 mode; // 10 operating mode
|
66
|
u8 filter; // 11 filter
|
67
|
u8 dataMode; // 12 data mode setting (on or off)
|
68
|
u8 duplex:4, // 13 duplex on/-/+
|
69
|
tmode:4; // 13 tone
|
70
|
u8 dsql:4, // 14 digital squelch
|
71
|
unknown1:4; // 14 zero
|
72
|
bbcd rtone[3]; // 15-17 repeater tone freq
|
73
|
bbcd ctone[3]; // 18-20 tone squelch setting
|
74
|
u8 dtcsPolarity; // 21 DTCS polarity
|
75
|
u8 unknown2:4, // 22 zero
|
76
|
firstDtcs:4; // 22 first digit of DTCS code
|
77
|
u8 secondDtcs:4, // 23 second digit DTCS
|
78
|
thirdDtcs:4; // 23 third digit DTCS
|
79
|
u8 digitalSquelch; // 24 Digital code squelch setting
|
80
|
u8 duplexOffset[3]; // 25-27 duplex offset freq
|
81
|
char destCall[8]; // 28-35 destination call sign
|
82
|
char accessRepeaterCall[8];// 36-43 access repeater call sign
|
83
|
char linkRepeaterCall[8]; // 44-51 gateway/link repeater call sign
|
84
|
bbcd duplexSettings[47]; // repeat of 5-51 for duplex
|
85
|
char name[16]; // 52-60 Name of station
|
86
|
"""
|
87
|
|
88
|
|
89
|
class Frame:
|
90
|
"""Base class for an ICF frame"""
|
91
|
_cmd = 0x00
|
92
|
_sub = 0x00
|
93
|
|
94
|
def __init__(self):
|
95
|
self._data = ""
|
96
|
|
97
|
def set_command(self, cmd, sub):
|
98
|
"""Set the command number (and optional subcommand)"""
|
99
|
self._cmd = cmd
|
100
|
self._sub = sub
|
101
|
|
102
|
def get_data(self):
|
103
|
"""Return the data payload"""
|
104
|
return self._data
|
105
|
|
106
|
def set_data(self, data):
|
107
|
"""Set the data payload"""
|
108
|
self._data = data
|
109
|
|
110
|
def send(self, src, dst, serial, willecho=True):
|
111
|
"""Send the frame over @serial, using @src and @dst addresses"""
|
112
|
raw = struct.pack("BBBBBB", 0xFE, 0xFE, src, dst, self._cmd, self._sub)
|
113
|
raw += str(self._data) + chr(0xFD)
|
114
|
|
115
|
LOG.debug("%02x -> %02x (%i):\n%s" %
|
116
|
(src, dst, len(raw), util.hexprint(raw)))
|
117
|
|
118
|
serial.write(raw)
|
119
|
if willecho:
|
120
|
echo = serial.read(len(raw))
|
121
|
if echo != raw and echo:
|
122
|
LOG.debug("Echo differed (%i/%i)" % (len(raw), len(echo)))
|
123
|
LOG.debug(util.hexprint(raw))
|
124
|
LOG.debug(util.hexprint(echo))
|
125
|
|
126
|
def read(self, serial):
|
127
|
"""Read the frame from @serial"""
|
128
|
data = ""
|
129
|
while not data.endswith(chr(0xFD)):
|
130
|
char = serial.read(1)
|
131
|
if not char:
|
132
|
LOG.debug("Read %i bytes total" % len(data))
|
133
|
raise errors.RadioError("Timeout")
|
134
|
data += char
|
135
|
|
136
|
if data == chr(0xFD):
|
137
|
raise errors.RadioError("Radio reported error")
|
138
|
|
139
|
src, dst = struct.unpack("BB", data[2:4])
|
140
|
LOG.debug("%02x <- %02x:\n%s" % (src, dst, util.hexprint(data)))
|
141
|
|
142
|
self._cmd = ord(data[4])
|
143
|
self._sub = ord(data[5])
|
144
|
self._data = data[6:-1]
|
145
|
|
146
|
return src, dst
|
147
|
|
148
|
def get_obj(self):
|
149
|
raise errors.RadioError("Generic frame has no structure")
|
150
|
|
151
|
|
152
|
class MemFrame(Frame):
|
153
|
"""A memory frame"""
|
154
|
_cmd = 0x1A
|
155
|
_sub = 0x00
|
156
|
_loc = 0
|
157
|
|
158
|
def set_location(self, loc):
|
159
|
"""Set the memory location number"""
|
160
|
self._loc = loc
|
161
|
self._data = struct.pack(">H", int("%04i" % loc, 16))
|
162
|
|
163
|
def make_empty(self):
|
164
|
"""Mark as empty so the radio will erase the memory"""
|
165
|
self._data = struct.pack(">HB", int("%04i" % self._loc, 16), 0xFF)
|
166
|
|
167
|
def is_empty(self):
|
168
|
"""Return True if memory is marked as empty"""
|
169
|
return len(self._data) < 5
|
170
|
|
171
|
def get_obj(self):
|
172
|
"""Return a bitwise parsed object"""
|
173
|
self._data = MemoryMap(str(self._data)) # Make sure we're assignable
|
174
|
return bitwise.parse(MEM_FORMAT, self._data)
|
175
|
|
176
|
def initialize(self):
|
177
|
"""Initialize to sane values"""
|
178
|
self._data = MemoryMap("".join(["\x00"] * (self.get_obj().size() / 8)))
|
179
|
|
180
|
|
181
|
class MultiVFOMemFrame(MemFrame):
|
182
|
"""A memory frame for radios with multiple VFOs"""
|
183
|
def set_location(self, loc, vfo=1):
|
184
|
self._loc = loc
|
185
|
self._data = struct.pack(">BH", vfo, int("%04i" % loc, 16))
|
186
|
|
187
|
def get_obj(self):
|
188
|
self._data = MemoryMap(str(self._data)) # Make sure we're assignable
|
189
|
return bitwise.parse(MEM_VFO_FORMAT, self._data)
|
190
|
|
191
|
class IC7100MemFrame(MultiVFOMemFrame):
|
192
|
"""A memory frame for IC-7100"""
|
193
|
|
194
|
def get_obj(self):
|
195
|
self._data = MemoryMap(str(self._data)) # Make sure we're assignable
|
196
|
return bitwise.parse(MEM_IC7100_FORMAT, self._data)
|
197
|
|
198
|
|
199
|
class DupToneMemFrame(MemFrame):
|
200
|
def get_obj(self):
|
201
|
self._data = MemoryMap(str(self._data))
|
202
|
return bitwise.parse(mem_duptone_format, self._data)
|
203
|
|
204
|
|
205
|
class IcomCIVRadio(icf.IcomLiveRadio):
|
206
|
"""Base class for ICOM CIV-based radios"""
|
207
|
BAUD_RATE = 19200
|
208
|
MODEL = "CIV Radio"
|
209
|
_model = "\x00"
|
210
|
_template = 0
|
211
|
|
212
|
def _send_frame(self, frame):
|
213
|
return frame.send(ord(self._model), 0xE0, self.pipe,
|
214
|
willecho=self._willecho)
|
215
|
|
216
|
def _recv_frame(self, frame=None):
|
217
|
if not frame:
|
218
|
frame = Frame()
|
219
|
frame.read(self.pipe)
|
220
|
return frame
|
221
|
|
222
|
def _initialize(self):
|
223
|
pass
|
224
|
|
225
|
def _detect_echo(self):
|
226
|
echo_test = "\xfe\xfe\xe0\xe0\xfa\xfd"
|
227
|
self.pipe.write(echo_test)
|
228
|
resp = self.pipe.read(6)
|
229
|
LOG.debug("Echo:\n%s" % util.hexprint(resp))
|
230
|
return resp == echo_test
|
231
|
|
232
|
def __init__(self, *args, **kwargs):
|
233
|
icf.IcomLiveRadio.__init__(self, *args, **kwargs)
|
234
|
|
235
|
self._classes = {
|
236
|
"mem": MemFrame,
|
237
|
}
|
238
|
|
239
|
if self.pipe:
|
240
|
self._willecho = self._detect_echo()
|
241
|
LOG.debug("Interface echo: %s" % self._willecho)
|
242
|
self.pipe.setTimeout(1)
|
243
|
|
244
|
# f = Frame()
|
245
|
# f.set_command(0x19, 0x00)
|
246
|
# self._send_frame(f)
|
247
|
#
|
248
|
# res = f.read(self.pipe)
|
249
|
# if res:
|
250
|
# LOG.debug("Result: %x->%x (%i)" %
|
251
|
# (res[0], res[1], len(f.get_data())))
|
252
|
# LOG.debug(util.hexprint(f.get_data()))
|
253
|
#
|
254
|
# self._id = f.get_data()[0]
|
255
|
self._rf = chirp_common.RadioFeatures()
|
256
|
|
257
|
self._initialize()
|
258
|
|
259
|
def get_features(self):
|
260
|
return self._rf
|
261
|
|
262
|
def _get_template_memory(self):
|
263
|
f = self._classes["mem"]()
|
264
|
f.set_location(self._template)
|
265
|
self._send_frame(f)
|
266
|
f.read(self.pipe)
|
267
|
return f
|
268
|
|
269
|
def get_raw_memory(self, number):
|
270
|
f = self._classes["mem"]()
|
271
|
f.set_location(number)
|
272
|
self._send_frame(f)
|
273
|
f.read(self.pipe)
|
274
|
return repr(f.get_obj())
|
275
|
|
276
|
def get_memory(self, number):
|
277
|
LOG.debug("Getting %i" % number)
|
278
|
f = self._classes["mem"]()
|
279
|
f.set_location(number)
|
280
|
self._send_frame(f)
|
281
|
|
282
|
mem = chirp_common.Memory()
|
283
|
mem.number = number
|
284
|
|
285
|
f = self._recv_frame(f)
|
286
|
if len(f.get_data()) == 0:
|
287
|
raise errors.RadioError("Radio reported error")
|
288
|
if f.get_data() and f.get_data()[-1] == "\xFF":
|
289
|
mem.empty = True
|
290
|
return mem
|
291
|
|
292
|
memobj = f.get_obj()
|
293
|
LOG.debug(repr(memobj))
|
294
|
|
295
|
mem.freq = int(memobj.freq)
|
296
|
mem.mode = self._rf.valid_modes[memobj.mode]
|
297
|
|
298
|
if self._rf.has_name:
|
299
|
mem.name = str(memobj.name).rstrip()
|
300
|
|
301
|
if self._rf.valid_tmodes:
|
302
|
mem.tmode = self._rf.valid_tmodes[memobj.tmode]
|
303
|
|
304
|
if self._rf.has_dtcs:
|
305
|
# FIXME
|
306
|
mem.dtcs = bitwise.bcd_to_int([memobj.dtcs])
|
307
|
|
308
|
if "Tone" in self._rf.valid_tmodes:
|
309
|
mem.rtone = int(memobj.rtone) / 10.0
|
310
|
|
311
|
if "TSQL" in self._rf.valid_tmodes and self._rf.has_ctone:
|
312
|
mem.ctone = int(memobj.ctone) / 10.0
|
313
|
|
314
|
if self._rf.valid_duplexes:
|
315
|
mem.duplex = self._rf.valid_duplexes[memobj.duplex]
|
316
|
|
317
|
return mem
|
318
|
|
319
|
def set_memory(self, mem):
|
320
|
f = self._get_template_memory()
|
321
|
if mem.empty:
|
322
|
f.set_location(mem.number)
|
323
|
f.make_empty()
|
324
|
self._send_frame(f)
|
325
|
return
|
326
|
|
327
|
# f.set_data(MemoryMap(self.get_raw_memory(mem.number)))
|
328
|
# f.initialize()
|
329
|
|
330
|
memobj = f.get_obj()
|
331
|
memobj.number = mem.number
|
332
|
memobj.freq = int(mem.freq)
|
333
|
memobj.mode = self._rf.valid_modes.index(mem.mode)
|
334
|
if self._rf.has_name:
|
335
|
memobj.name = mem.name.ljust(self._rf.valid_name_length)[:self._rf.valid_name_length]
|
336
|
|
337
|
if self._rf.valid_tmodes:
|
338
|
memobj.tmode = self._rf.valid_tmodes.index(mem.tmode)
|
339
|
|
340
|
if self._rf.valid_duplexes:
|
341
|
memobj.duplex = self._rf.valid_duplexes.index(mem.duplex)
|
342
|
|
343
|
if self._rf.has_ctone:
|
344
|
memobj.ctone = int(mem.ctone * 10)
|
345
|
memobj.rtone = int(mem.rtone * 10)
|
346
|
|
347
|
LOG.debug(repr(memobj))
|
348
|
self._send_frame(f)
|
349
|
|
350
|
f = self._recv_frame()
|
351
|
LOG.debug("Result:\n%s" % util.hexprint(f.get_data()))
|
352
|
|
353
|
|
354
|
@directory.register
|
355
|
class Icom7200Radio(IcomCIVRadio):
|
356
|
"""Icom IC-7200"""
|
357
|
MODEL = "7200"
|
358
|
_model = "\x76"
|
359
|
_template = 201
|
360
|
|
361
|
def _initialize(self):
|
362
|
self._rf.has_bank = False
|
363
|
self._rf.has_dtcs_polarity = False
|
364
|
self._rf.has_dtcs = False
|
365
|
self._rf.has_ctone = False
|
366
|
self._rf.has_offset = False
|
367
|
self._rf.has_name = False
|
368
|
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY"]
|
369
|
self._rf.valid_tmodes = []
|
370
|
self._rf.valid_duplexes = []
|
371
|
self._rf.valid_bands = [(1800000, 59000000)]
|
372
|
self._rf.valid_tuning_steps = []
|
373
|
self._rf.valid_skips = []
|
374
|
self._rf.memory_bounds = (1, 200)
|
375
|
|
376
|
|
377
|
@directory.register
|
378
|
class Icom7000Radio(IcomCIVRadio):
|
379
|
"""Icom IC-7000"""
|
380
|
MODEL = "7000"
|
381
|
_model = "\x70"
|
382
|
_template = 102
|
383
|
|
384
|
def _initialize(self):
|
385
|
self._classes["mem"] = MultiVFOMemFrame
|
386
|
self._rf.has_bank = False
|
387
|
self._rf.has_dtcs_polarity = True
|
388
|
self._rf.has_dtcs = True
|
389
|
self._rf.has_ctone = True
|
390
|
self._rf.has_offset = False
|
391
|
self._rf.has_name = True
|
392
|
self._rf.has_tuning_step = False
|
393
|
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"]
|
394
|
self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
|
395
|
self._rf.valid_duplexes = ["", "-", "+"]
|
396
|
self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)]
|
397
|
self._rf.valid_tuning_steps = []
|
398
|
self._rf.valid_skips = []
|
399
|
self._rf.valid_name_length = 9
|
400
|
self._rf.valid_characters = chirp_common.CHARSET_ASCII
|
401
|
self._rf.memory_bounds = (1, 99)
|
402
|
|
403
|
@directory.register
|
404
|
class Icom7100Radio(IcomCIVRadio):
|
405
|
"""Icom IC-7100"""
|
406
|
MODEL = "7100"
|
407
|
_model = "\x88"
|
408
|
_template = 102
|
409
|
_num_banks = 5
|
410
|
|
411
|
def _initialize(self):
|
412
|
self._classes["mem"] = IC7100MemFrame
|
413
|
self._rf.has_bank = True
|
414
|
self._rf.has_bank_index = False
|
415
|
self._rf.has_bank_names = False
|
416
|
self._rf.has_dtcs_polarity = False
|
417
|
self._rf.has_dtcs = False
|
418
|
self._rf.has_ctone = True
|
419
|
self._rf.has_offset = False
|
420
|
self._rf.has_name = True
|
421
|
self._rf.has_tuning_step = False
|
422
|
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM", "CW-R", "RTTY-R"]
|
423
|
self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
|
424
|
self._rf.valid_duplexes = ["", "-", "+"]
|
425
|
self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)]
|
426
|
self._rf.valid_tuning_steps = []
|
427
|
self._rf.valid_skips = []
|
428
|
self._rf.valid_name_length = 16
|
429
|
self._rf.valid_characters = chirp_common.CHARSET_ASCII
|
430
|
self._rf.memory_bounds = (1, 99)
|
431
|
|
432
|
def get_bank_model(self):
|
433
|
rf = self.get_features()
|
434
|
if rf.has_bank:
|
435
|
if not rf.has_bank_index:
|
436
|
return IcomBankModel(self)
|
437
|
|
438
|
|
439
|
@directory.register
|
440
|
class Icom746Radio(IcomCIVRadio):
|
441
|
"""Icom IC-746"""
|
442
|
MODEL = "746"
|
443
|
BAUD_RATE = 9600
|
444
|
_model = "\x56"
|
445
|
_template = 102
|
446
|
|
447
|
def _initialize(self):
|
448
|
self._classes["mem"] = DupToneMemFrame
|
449
|
self._rf.has_bank = False
|
450
|
self._rf.has_dtcs_polarity = False
|
451
|
self._rf.has_dtcs = False
|
452
|
self._rf.has_ctone = True
|
453
|
self._rf.has_offset = False
|
454
|
self._rf.has_name = True
|
455
|
self._rf.has_tuning_step = False
|
456
|
self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"]
|
457
|
self._rf.valid_tmodes = ["", "Tone", "TSQL"]
|
458
|
self._rf.valid_duplexes = ["", "-", "+"]
|
459
|
self._rf.valid_bands = [(30000, 199999999)]
|
460
|
self._rf.valid_tuning_steps = []
|
461
|
self._rf.valid_skips = []
|
462
|
self._rf.valid_name_length = 9
|
463
|
self._rf.valid_characters = chirp_common.CHARSET_ASCII
|
464
|
self._rf.memory_bounds = (1, 99)
|
465
|
|
466
|
CIV_MODELS = {
|
467
|
(0x76, 0xE0): Icom7200Radio,
|
468
|
(0x70, 0xE0): Icom7000Radio,
|
469
|
(0x46, 0xE0): Icom746Radio,
|
470
|
(0x88, 0xE0): Icom7100Radio
|
471
|
}
|
472
|
|
473
|
|
474
|
def probe_model(ser):
|
475
|
"""Probe the radio attatched to @ser for its model"""
|
476
|
f = Frame()
|
477
|
f.set_command(0x19, 0x00)
|
478
|
|
479
|
for model, controller in CIV_MODELS.keys():
|
480
|
f.send(model, controller, ser)
|
481
|
try:
|
482
|
f.read(ser)
|
483
|
except errors.RadioError:
|
484
|
continue
|
485
|
|
486
|
if len(f.get_data()) == 1:
|
487
|
model = ord(f.get_data()[0])
|
488
|
return CIV_MODELS[(model, controller)]
|
489
|
|
490
|
if f.get_data():
|
491
|
LOG.debug("Got data, but not 1 byte:")
|
492
|
LOG.debug(util.hexprint(f.get_data()))
|
493
|
raise errors.RadioError("Unknown response")
|
494
|
|
495
|
raise errors.RadioError("Unsupported model")
|