1
|
# Copyright 2019 Dan Clemmensen <DanClemmensen@Gmail.com>
|
2
|
#
|
3
|
# test the serial io code from ft4.py
|
4
|
# This program is free software: you can redistribute it and/or modify
|
5
|
# it under the terms of the GNU General Public License as published by
|
6
|
# the Free Software Foundation, either version 3 of the License, or
|
7
|
# (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
"""
|
17
|
Unit test for ft4.py serial io.
|
18
|
unit-test module has stubs for averything in CHIRP that is needed by the
|
19
|
serial FT4 serial i/o functions. I copied those functions into this module
|
20
|
I don't know how to do real Python debugging. I'm trying to make this compatible
|
21
|
with both Python 2 and Python 3.
|
22
|
"""
|
23
|
import serial
|
24
|
import os
|
25
|
import time
|
26
|
import struct
|
27
|
|
28
|
class Util:
|
29
|
def hexprint(self, msg): #given bytes, return UTF-8 hex string
|
30
|
return u"".join(u"%02x"% x for x in bytearray(msg))
|
31
|
|
32
|
class Log:
|
33
|
def debug(self, msg):
|
34
|
print ("Debug: " + msg + "\n")
|
35
|
|
36
|
class Errors(Exception):
|
37
|
def RadioError(self,msg):
|
38
|
return(msg)
|
39
|
|
40
|
util = Util()
|
41
|
LOG = Log()
|
42
|
errors = Errors()
|
43
|
|
44
|
|
45
|
# Begin Serial transfer utilities for the SCU-35 cable.
|
46
|
|
47
|
# The serial transfer protocol was implemented here after snooping the wire.
|
48
|
# After it was implemented, we noticed that it is identical to the protocol
|
49
|
# implemented in th9000.py. A non-echo version is implemented in anytone_ht.py.
|
50
|
#
|
51
|
# The pipe.read and pipe.write functions use bytes, not strings. The serial
|
52
|
# transfer utilities operate only to move data between the memory object and
|
53
|
# the serial port. The code runs on either Python 2 or Python3, so some
|
54
|
# constructs could be better optimized for one or the other, but not both.
|
55
|
|
56
|
|
57
|
def checkSum8(data):
|
58
|
"""
|
59
|
Calculate the 8 bit checksum of buffer
|
60
|
Input: buffer - bytes
|
61
|
returns: integer
|
62
|
"""
|
63
|
return sum(x for x in bytearray(data)) & 0xFF
|
64
|
|
65
|
|
66
|
def sendcmd(pipe, cmd, response_len):
|
67
|
"""
|
68
|
send a command bytelist to radio,receive and return the resulting bytelist.
|
69
|
Input: pipe - serial port object to use
|
70
|
cmd - bytes to send
|
71
|
response_len - number of bytes of expected response,
|
72
|
not including the ACK.
|
73
|
This cable is "two-wire": The TxD and RxD are "or'ed" so we receive
|
74
|
whatever we send and then whatever response the radio sends. We check the
|
75
|
echo and strip it, returning only the radio's response.
|
76
|
We also check and strip the ACK character at the end of the response.
|
77
|
"""
|
78
|
pipe.write(cmd)
|
79
|
echo = pipe.read(len(cmd))
|
80
|
if echo != cmd:
|
81
|
msg = "Bad echo. Sent:" + util.hexprint(cmd) + ", "
|
82
|
msg += "Received:" + util.hexprint(echo)
|
83
|
LOG.debug(msg)
|
84
|
raise errors.RadioError("Incorrect echo on serial port.")
|
85
|
if response_len > 0:
|
86
|
response = pipe.read(response_len)
|
87
|
else:
|
88
|
response = b""
|
89
|
ack = pipe.read(1)
|
90
|
if ack != b'\x06':
|
91
|
LOG.debug("missing ack: expected 0x06, got" + util.hexprint(ack))
|
92
|
raise errors.RadioError("Incorrect ACK on serial port.")
|
93
|
return response
|
94
|
|
95
|
|
96
|
def getblock(pipe, addr):
|
97
|
"""
|
98
|
read a single 16-byte block from the radio
|
99
|
send the command and check the response
|
100
|
returns the 16-byte bytearray
|
101
|
"""
|
102
|
cmd = struct.pack(">cHb", b"R", addr, 16)
|
103
|
response = sendcmd(pipe, cmd, 21)
|
104
|
if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]):
|
105
|
msg = "Bad response. Sent:" + util.hexprint(cmd) + ", "
|
106
|
msg += b"Received:" + util.hexprint(response)
|
107
|
LOG.debug(msg)
|
108
|
raise errors.RadioError("Incorrect response to read.")
|
109
|
if checkSum8(response[1:20]) != bytearray(response)[20]:
|
110
|
LOG.debug(b"Bad checksum: " + util.hexprint(response))
|
111
|
raise errors.RadioError("bad block checksum.")
|
112
|
return response[4:20]
|
113
|
|
114
|
|
115
|
def do_download(radio):
|
116
|
"""
|
117
|
Read memory from the radio.
|
118
|
send "PROGRAM" to command the radio into clone mode,
|
119
|
read the initial string (version?)
|
120
|
read the memory blocks
|
121
|
send "END"
|
122
|
"""
|
123
|
pipe = radio.pipe # Get the serial port connection
|
124
|
|
125
|
if b"QX" != sendcmd(pipe, b"PROGRAM", 2):
|
126
|
raise errors.RadioError("expected QX from radio.")
|
127
|
version = sendcmd(pipe, b'\x02', 15)
|
128
|
data = b""
|
129
|
for _i in range(radio.numblocks):
|
130
|
data += getblock(pipe, 16 * _i)
|
131
|
sendcmd(pipe, b"END", 0)
|
132
|
return (version, data)
|
133
|
|
134
|
|
135
|
def putblock(pipe, addr, data):
|
136
|
"""
|
137
|
write a single 16-byte block to the radio
|
138
|
send the command and check the response
|
139
|
"""
|
140
|
chkstr = struct.pack(">Hb", addr, 16) + data
|
141
|
msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06'
|
142
|
sendcmd(pipe, msg, 0)
|
143
|
|
144
|
|
145
|
def do_upload(radio):
|
146
|
"""
|
147
|
Write memory image to radio
|
148
|
send "PROGRAM" to command the radio into clone mode,
|
149
|
write the memory blocks. Skip the first block
|
150
|
send "END"
|
151
|
"""
|
152
|
pipe = radio.pipe # Get the serial port connection
|
153
|
|
154
|
if b"QX" != sendcmd(pipe, b"PROGRAM", 2):
|
155
|
raise errors.RadioError("expected QX from radio.")
|
156
|
data = radio.get_mmap()
|
157
|
sendcmd(pipe, b'\x02', 15)
|
158
|
for _i in range(1, radio.numblocks):
|
159
|
putblock(pipe, 16*_i, data[16*_i:16*(_i+1)])
|
160
|
sendcmd(pipe, b"END", 0)
|
161
|
return
|
162
|
# End serial transfer utilities
|
163
|
|
164
|
# stub radio class for test
|
165
|
class Radio(object):
|
166
|
BAUDRATE = 9600
|
167
|
|
168
|
def __init__(self, port=None, imgFN=None,numblocks=0x215):
|
169
|
"""
|
170
|
imgFN: image file name
|
171
|
port: Serial Port name ie com? or /dev/ttyUSB0?
|
172
|
"""
|
173
|
|
174
|
self.port = port
|
175
|
self.imgFN = imgFN
|
176
|
|
177
|
self.pipe = None #Current I/O device file or port
|
178
|
self.memMap = None
|
179
|
self.numblocks = numblocks
|
180
|
self.mem_map_size = 16 * numblocks +15
|
181
|
|
182
|
def openPort(self, port=None, to=4):
|
183
|
"""
|
184
|
Open serial port set parameters
|
185
|
raise exception not valid
|
186
|
"""
|
187
|
self.close()
|
188
|
|
189
|
if port is None:
|
190
|
port = self.port # maybe None
|
191
|
|
192
|
try:
|
193
|
self.pipe = serial.Serial(port, 9600, timeout=to)
|
194
|
self.pipe.dtr = True
|
195
|
self.pipe.flushInput()
|
196
|
self.pipe.flushOutput()
|
197
|
except (serial.SerialException, ValueError) as e:
|
198
|
raise e
|
199
|
|
200
|
def get_mmap(self):
|
201
|
return self.memMap
|
202
|
|
203
|
def close(self):
|
204
|
"""
|
205
|
close any open port/file
|
206
|
"""
|
207
|
try:
|
208
|
self.pipe.close()
|
209
|
except:
|
210
|
pass
|
211
|
finally:
|
212
|
self.pipe = None
|
213
|
|
214
|
UPLOAD = False
|
215
|
def test():
|
216
|
print("Power On Radio")
|
217
|
time.sleep(3)
|
218
|
io=Radio(r'/dev/ttyUSB0',None,0x215) #FT-4 on Linux
|
219
|
io.openPort()
|
220
|
version, data=do_download(io)
|
221
|
io.memMap = data
|
222
|
io.close()
|
223
|
print(b"Version=" + version)
|
224
|
with open(r'./dump.ft4', 'wb') as f:
|
225
|
f.write(data)
|
226
|
f.close()
|
227
|
print("Wrote raw image to ./dump.ft4")
|
228
|
if UPLOAD:
|
229
|
print("Power Radio off and back on")
|
230
|
time.sleep(3)
|
231
|
io.openPort()
|
232
|
print("sending to radio")
|
233
|
do_upload(io)
|
234
|
print("transfer complete")
|
235
|
|
236
|
# main program
|
237
|
if __name__ =="__main__":
|
238
|
test()
|
239
|
|
240
|
|