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
|
import time
|
17
|
import logging
|
18
|
|
19
|
from chirp import util, memmap, chirp_common, bitwise, directory, errors
|
20
|
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
|
21
|
|
22
|
LOG = logging.getLogger(__name__)
|
23
|
|
24
|
CHUNK_SIZE = 16
|
25
|
|
26
|
|
27
|
def _send(s, data):
|
28
|
for i in range(0, len(data), CHUNK_SIZE):
|
29
|
chunk = data[i:i+CHUNK_SIZE]
|
30
|
s.write(chunk)
|
31
|
echo = s.read(len(chunk))
|
32
|
if chunk != echo:
|
33
|
raise Exception("Failed to read echo chunk")
|
34
|
|
35
|
|
36
|
# The IDBLOCK is the first thing sent during an upload or download
|
37
|
# and indicates the radio subtype:
|
38
|
# USA Unmodified: b"\x0c\x01\x41\x33\x35\x02\x00\xb8"
|
39
|
# USA With extended TX mod: b"\x0c\x01\x41\x33\x35\x03\x00\xb9"
|
40
|
SUPPORTED_IDBLOCKS = [b"\x0c\x01\x41\x33\x35\x02\x00\xb8",
|
41
|
b"\x0c\x01\x41\x33\x35\x03\x00\xb9"]
|
42
|
TRAILER = b"\x0c\x02\x41\x33\x35\x00\x00\xb7"
|
43
|
ACK = b"\x0C\x06\x00"
|
44
|
|
45
|
|
46
|
def _download(radio):
|
47
|
data = b""
|
48
|
attempts = 30
|
49
|
for _i in range(0, attempts):
|
50
|
data = radio.pipe.read(8)
|
51
|
if data in SUPPORTED_IDBLOCKS:
|
52
|
radio.subtype = data
|
53
|
break
|
54
|
LOG.debug('Download attempt %i received %i: %s',
|
55
|
_i, len(data), util.hexprint(data))
|
56
|
if radio.status_fn:
|
57
|
status = chirp_common.Status()
|
58
|
status.max = 1
|
59
|
status.cur = 0
|
60
|
status.msg = "Waiting for radio (%i)" % (
|
61
|
attempts - (_i + 1))
|
62
|
radio.status_fn(status)
|
63
|
|
64
|
LOG.debug("Header:\n%s" % util.hexprint(data))
|
65
|
|
66
|
if len(data) != 8:
|
67
|
raise Exception("Failed to read header")
|
68
|
|
69
|
_send(radio.pipe, ACK)
|
70
|
|
71
|
data = b""
|
72
|
|
73
|
while len(data) < radio._block_sizes[1]:
|
74
|
time.sleep(0.1)
|
75
|
chunk = radio.pipe.read(38)
|
76
|
LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
|
77
|
if len(chunk) == 8:
|
78
|
LOG.debug("END?")
|
79
|
elif len(chunk) != 38:
|
80
|
LOG.debug("Should fail?")
|
81
|
break
|
82
|
# raise Exception("Failed to get full data block")
|
83
|
else:
|
84
|
cs = 0
|
85
|
for byte in chunk[:-1]:
|
86
|
cs += byte
|
87
|
if chunk[-1] != (cs & 0xFF):
|
88
|
raise Exception("Block failed checksum!")
|
89
|
|
90
|
data += chunk[5:-1]
|
91
|
|
92
|
_send(radio.pipe, ACK)
|
93
|
if radio.status_fn:
|
94
|
status = chirp_common.Status()
|
95
|
status.max = radio._block_sizes[1]
|
96
|
status.cur = len(data)
|
97
|
status.msg = "Cloning from radio"
|
98
|
radio.status_fn(status)
|
99
|
|
100
|
LOG.debug("Total: %i" % len(data))
|
101
|
|
102
|
return memmap.MemoryMapBytes(data)
|
103
|
|
104
|
|
105
|
def _upload(radio):
|
106
|
for _i in range(0, 10):
|
107
|
data = radio.pipe.read(256)
|
108
|
if not data:
|
109
|
break
|
110
|
LOG.debug("What is this garbage?\n%s" % util.hexprint(data))
|
111
|
|
112
|
_send(radio.pipe, radio.subtype)
|
113
|
time.sleep(1)
|
114
|
ack = radio.pipe.read(300)
|
115
|
LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack)))
|
116
|
if ack != ACK:
|
117
|
raise Exception("Radio did not ack ID")
|
118
|
|
119
|
block = 0
|
120
|
while block < (radio.get_memsize() // 32):
|
121
|
data = b"\x0C\x03\x00\x00" + bytes([block])
|
122
|
data += radio.get_mmap()[block*32:(block+1)*32]
|
123
|
cs = 0
|
124
|
for byte in data:
|
125
|
cs += byte
|
126
|
data += bytes([cs & 0xFF])
|
127
|
|
128
|
LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data)))
|
129
|
|
130
|
_send(radio.pipe, data)
|
131
|
time.sleep(0.1)
|
132
|
ack = radio.pipe.read(3)
|
133
|
if ack != ACK:
|
134
|
raise Exception("Radio did not ack block %i" % block)
|
135
|
|
136
|
if radio.status_fn:
|
137
|
status = chirp_common.Status()
|
138
|
status.max = radio._block_sizes[1]
|
139
|
status.cur = block * 32
|
140
|
status.msg = "Cloning to radio"
|
141
|
radio.status_fn(status)
|
142
|
block += 1
|
143
|
|
144
|
_send(radio.pipe, TRAILER)
|
145
|
|
146
|
|
147
|
MEM_FORMAT = """
|
148
|
struct {
|
149
|
bbcd freq[4];
|
150
|
u8 unknown1[4];
|
151
|
bbcd offset[2];
|
152
|
u8 unknown2[2];
|
153
|
u8 pskip:1,
|
154
|
skip:1,
|
155
|
unknown3:1,
|
156
|
isnarrow:1,
|
157
|
power:2,
|
158
|
duplex:2;
|
159
|
u8 unknown4:6,
|
160
|
tmode:2;
|
161
|
u8 tone;
|
162
|
u8 dtcs;
|
163
|
} memory[200];
|
164
|
|
165
|
#seekto 0x0E00;
|
166
|
struct {
|
167
|
char name[6];
|
168
|
} names[200];
|
169
|
"""
|
170
|
|
171
|
MODES = ["FM", "NFM"]
|
172
|
TMODES = ["", "Tone", "TSQL", "DTCS"]
|
173
|
DUPLEX = ["", "-", "+", ""]
|
174
|
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=65),
|
175
|
chirp_common.PowerLevel("Mid", watts=25),
|
176
|
chirp_common.PowerLevel("Low2", watts=10),
|
177
|
chirp_common.PowerLevel("Low1", watts=5),
|
178
|
]
|
179
|
CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + "()+-=*/???|_"
|
180
|
|
181
|
|
182
|
@directory.register
|
183
|
class FT2800Radio(YaesuCloneModeRadio):
|
184
|
"""Yaesu FT-2800"""
|
185
|
VENDOR = "Yaesu"
|
186
|
MODEL = "FT-2800M"
|
187
|
|
188
|
_block_sizes = [8, 7680]
|
189
|
_memsize = 7680
|
190
|
|
191
|
@property
|
192
|
def subtype(self):
|
193
|
# If our image is from before the subtype was stashed, assume
|
194
|
# the default unmodified US ID block
|
195
|
return bytes(self.metadata.get('subtype_idblock',
|
196
|
SUPPORTED_IDBLOCKS[0]))
|
197
|
|
198
|
@subtype.setter
|
199
|
def subtype(self, value):
|
200
|
self.metadata = {'subtype_idblock': [x for x in value]}
|
201
|
|
202
|
@classmethod
|
203
|
def get_prompts(cls):
|
204
|
rp = chirp_common.RadioPrompts()
|
205
|
rp.pre_download = _(
|
206
|
"1. Turn radio off.\n"
|
207
|
"2. Connect cable\n"
|
208
|
"3. Press and hold in the MHz, Low, and D/MR keys on the radio "
|
209
|
"while turning it on\n"
|
210
|
"4. Radio is in clone mode when TX/RX is flashing\n"
|
211
|
"5. <b>After clicking OK</b>, "
|
212
|
"press the MHz key on the radio to send"
|
213
|
" image.\n"
|
214
|
" (\"TX\" will appear on the LCD). \n")
|
215
|
rp.pre_upload = _(
|
216
|
"1. Turn radio off.\n"
|
217
|
"2. Connect cable\n"
|
218
|
"3. Press and hold in the MHz, Low, and D/MR keys on the radio "
|
219
|
"while turning it on\n"
|
220
|
"4. Radio is in clone mode when TX/RX is flashing\n"
|
221
|
"5. Press the Low key on the radio "
|
222
|
"(\"RX\" will appear on the LCD).\n"
|
223
|
"6. Click OK.")
|
224
|
return rp
|
225
|
|
226
|
def get_features(self):
|
227
|
rf = chirp_common.RadioFeatures()
|
228
|
|
229
|
rf.memory_bounds = (0, 199)
|
230
|
|
231
|
rf.has_ctone = False
|
232
|
rf.has_tuning_step = False
|
233
|
rf.has_dtcs_polarity = False
|
234
|
rf.has_bank = False
|
235
|
|
236
|
rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0,
|
237
|
20.0, 25.0, 50.0, 100.0]
|
238
|
rf.valid_modes = MODES
|
239
|
rf.valid_tmodes = TMODES
|
240
|
rf.valid_bands = [(137000000, 174000000)]
|
241
|
rf.valid_power_levels = POWER_LEVELS
|
242
|
rf.valid_duplexes = DUPLEX
|
243
|
rf.valid_skips = ["", "S", "P"]
|
244
|
rf.valid_name_length = 6
|
245
|
rf.valid_characters = CHARSET
|
246
|
|
247
|
return rf
|
248
|
|
249
|
def sync_in(self):
|
250
|
self.pipe.parity = "E"
|
251
|
self.pipe.timeout = 1
|
252
|
start = time.time()
|
253
|
try:
|
254
|
self._mmap = _download(self)
|
255
|
except errors.RadioError:
|
256
|
raise
|
257
|
except Exception as e:
|
258
|
LOG.exception('Failed download')
|
259
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
260
|
LOG.info("Downloaded in %.2f sec" % (time.time() - start))
|
261
|
self.process_mmap()
|
262
|
|
263
|
def sync_out(self):
|
264
|
self.pipe.timeout = 1
|
265
|
self.pipe.parity = "E"
|
266
|
start = time.time()
|
267
|
try:
|
268
|
_upload(self)
|
269
|
except errors.RadioError:
|
270
|
raise
|
271
|
except Exception as e:
|
272
|
LOG.exception('Failed upload')
|
273
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
274
|
LOG.info("Uploaded in %.2f sec" % (time.time() - start))
|
275
|
|
276
|
def process_mmap(self):
|
277
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
278
|
|
279
|
def get_raw_memory(self, number):
|
280
|
return repr(self._memobj.memory[number])
|
281
|
|
282
|
def get_memory(self, number):
|
283
|
_mem = self._memobj.memory[number]
|
284
|
_nam = self._memobj.names[number]
|
285
|
mem = chirp_common.Memory()
|
286
|
|
287
|
mem.number = number
|
288
|
|
289
|
if _mem.get_raw()[0] == "\xFF":
|
290
|
mem.empty = True
|
291
|
return mem
|
292
|
|
293
|
mem.freq = int(_mem.freq) * 10
|
294
|
mem.offset = int(_mem.offset) * 100000
|
295
|
mem.duplex = DUPLEX[_mem.duplex]
|
296
|
mem.tmode = TMODES[_mem.tmode]
|
297
|
mem.rtone = chirp_common.TONES[_mem.tone]
|
298
|
mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
|
299
|
mem.name = str(_nam.name).rstrip()
|
300
|
mem.mode = _mem.isnarrow and "NFM" or "FM"
|
301
|
mem.skip = _mem.pskip and "P" or _mem.skip and "S" or ""
|
302
|
mem.power = POWER_LEVELS[_mem.power]
|
303
|
|
304
|
return mem
|
305
|
|
306
|
def set_memory(self, mem):
|
307
|
_mem = self._memobj.memory[mem.number]
|
308
|
_nam = self._memobj.names[mem.number]
|
309
|
|
310
|
if mem.empty:
|
311
|
_mem.set_raw("\xFF" * (_mem.size() // 8))
|
312
|
return
|
313
|
|
314
|
if _mem.get_raw()[0] == "\xFF":
|
315
|
# Empty -> Non-empty, so initialize
|
316
|
_mem.set_raw("\x00" * (_mem.size() // 8))
|
317
|
|
318
|
# initializing unknowns
|
319
|
_mem.unknown1 = (0xFF, 0xFF, 0xFF, 0xFF)
|
320
|
_mem.unknown2 = (0x00, 0x00)
|
321
|
_mem.unknown3 = 0x01
|
322
|
_mem.unknown4 = 0x3C
|
323
|
|
324
|
_mem.freq = mem.freq / 10
|
325
|
_mem.offset = mem.offset / 100000
|
326
|
_mem.duplex = DUPLEX.index(mem.duplex)
|
327
|
_mem.tmode = TMODES.index(mem.tmode)
|
328
|
_mem.tone = chirp_common.TONES.index(mem.rtone)
|
329
|
_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
|
330
|
_mem.isnarrow = MODES.index(mem.mode)
|
331
|
_mem.pskip = mem.skip == "P"
|
332
|
_mem.skip = mem.skip == "S"
|
333
|
if mem.power:
|
334
|
_mem.power = POWER_LEVELS.index(mem.power)
|
335
|
else:
|
336
|
_mem.power = 0
|
337
|
|
338
|
_nam.name = mem.name.ljust(6)[:6]
|
339
|
|
340
|
@classmethod
|
341
|
def match_model(cls, filedata, filename):
|
342
|
return len(filedata) == cls._memsize
|