1
|
#! /usr/bin/python
|
2
|
|
3
|
# Test script to play with the Wouxun KG-UV9D-Plus
|
4
|
# Based on the notes and work from the the Wouxun KG-UV8D Plus
|
5
|
# Pavel Milanes, CO7WT, pavelmc@gmail.com
|
6
|
|
7
|
|
8
|
# some notes from the chirp's driver
|
9
|
|
10
|
# Figured out how the data is encrypted and implement
|
11
|
# serial data encryption and decryption functions.
|
12
|
# The algorithm of decryption works like this:
|
13
|
# - the first byte of data stream is XOR by const 57h
|
14
|
# - each next byte is encoded by previous byte using the XOR
|
15
|
# including the checksum (e.g data[i - 1] xor data[i])
|
16
|
|
17
|
# Support for the Wouxun KG-UV8D Plus radio
|
18
|
# Serial coms are at 19200 baud
|
19
|
# The data is passed in variable length records
|
20
|
# Record structure:
|
21
|
# Offset Usage
|
22
|
# 0 start of record (\x7a)
|
23
|
# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write)
|
24
|
# 2 direction (\xff PC-> Radio, \x00 Radio -> PC)
|
25
|
# 3 length of payload (excluding header/checksum) (n)
|
26
|
# 4 payload (n bytes)
|
27
|
# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n
|
28
|
#
|
29
|
# Memory Read Records:
|
30
|
# the payload is 3 bytes, first 2 are offset (big endian),
|
31
|
# 3rd is number of bytes to read
|
32
|
# Memory Write Records:
|
33
|
# the maximum payload size (from the Wouxun software) seems to be 66 bytes
|
34
|
# (2 bytes location + 64 bytes data).
|
35
|
|
36
|
import sys
|
37
|
import struct
|
38
|
import string
|
39
|
|
40
|
cmd_name = {
|
41
|
0x80 : "ident",
|
42
|
0x81 : "hangup",
|
43
|
0x82 : "read config",
|
44
|
0x83 : "write config",
|
45
|
0x84 : "read channel memory",
|
46
|
0x85 : "write channel memory"}
|
47
|
|
48
|
# list of known config items. If it isn't here, we don't know it yet
|
49
|
config_item_type = {
|
50
|
0x740: "FM chan 1-8",
|
51
|
0x750: "FM chan 9-16",
|
52
|
0x760: "FM chan 17-20",
|
53
|
0x8b0: "Configuration",
|
54
|
0x8c0: "PTT-ANI",
|
55
|
0x8d0: "SCC",
|
56
|
0x8f0: "Display banner",
|
57
|
0x940: "Scan groups part1",
|
58
|
0x980: "Scan groups part2",
|
59
|
0x7400: "CALL-ID 1-8",
|
60
|
0x7440: "CALL-ID 9-16",
|
61
|
0x7480: "CALL-ID 17-20, names 0-4",
|
62
|
0x74c0: "CALL-ID names 5-12",
|
63
|
0x7500: "CALL-ID names 13-20"
|
64
|
}
|
65
|
|
66
|
# error counters
|
67
|
cmd_cnt = {}
|
68
|
rep_cnt = {}
|
69
|
cmd_cerrs = {}
|
70
|
rep_cerrs = {}
|
71
|
cmd_lerrs = {}
|
72
|
rep_lerrs = {}
|
73
|
|
74
|
def hexprint(data, addrfmt=None):
|
75
|
"""Return a hexdump-like encoding of @data"""
|
76
|
if addrfmt is None:
|
77
|
addrfmt = '%(addr)03i'
|
78
|
|
79
|
block_size = 16
|
80
|
|
81
|
lines = (len(data) / block_size)
|
82
|
if (len(data) % block_size > 0):
|
83
|
lines += 1
|
84
|
|
85
|
out = ""
|
86
|
left = len(data)
|
87
|
for block in range(0, lines):
|
88
|
addr = block * block_size
|
89
|
try:
|
90
|
out += addrfmt % locals()
|
91
|
except (OverflowError, ValueError, TypeError, KeyError):
|
92
|
out += "%03i" % addr
|
93
|
out += ': '
|
94
|
|
95
|
if left < block_size:
|
96
|
limit = left
|
97
|
else:
|
98
|
limit = block_size
|
99
|
|
100
|
for j in range(0, block_size):
|
101
|
if (j < limit):
|
102
|
out += "%02x " % data[(block * block_size) + j]
|
103
|
else:
|
104
|
out += " "
|
105
|
|
106
|
out += " "
|
107
|
|
108
|
for j in range(0, block_size):
|
109
|
|
110
|
if (j < limit):
|
111
|
_byte = data[(block * block_size) + j]
|
112
|
if _byte >= 0x20 and _byte < 0x7F:
|
113
|
out += "%s" % chr(_byte)
|
114
|
else:
|
115
|
out += "."
|
116
|
else:
|
117
|
out += " "
|
118
|
out += "\n"
|
119
|
if (left > block_size):
|
120
|
left -= block_size
|
121
|
|
122
|
return out
|
123
|
|
124
|
|
125
|
def bcd_encode(val, bigendian=True, width=None):
|
126
|
"""This is really old and shouldn't be used anymore"""
|
127
|
digits = []
|
128
|
while val != 0:
|
129
|
digits.append(val % 10)
|
130
|
val /= 10
|
131
|
|
132
|
result = ""
|
133
|
|
134
|
if len(digits) % 2 != 0:
|
135
|
digits.append(0)
|
136
|
|
137
|
while width and width > len(digits):
|
138
|
digits.append(0)
|
139
|
|
140
|
for i in range(0, len(digits), 2):
|
141
|
newval = struct.pack("B", (digits[i + 1] << 4) | digits[i])
|
142
|
if bigendian:
|
143
|
result = newval + result
|
144
|
else:
|
145
|
result = result + newval
|
146
|
|
147
|
return result
|
148
|
|
149
|
|
150
|
def pkt_encode(op, payload):
|
151
|
"""Assemble a packet for the radio and encode it for transmission.
|
152
|
Yes indeed, the checksum we store is only 4 bits. Why, I suspect it's a bug in
|
153
|
the radio firmware guys didn't want to fix, i.e. a typo 0xff -> 0xf..."""
|
154
|
|
155
|
data = bytearray()
|
156
|
data.append(0x7d) # tag that marks the beginning of the packet
|
157
|
data.append(op)
|
158
|
data.append(0xff) # 0xff is from app to radio
|
159
|
data.append(len(payload))
|
160
|
|
161
|
# calc checksum from op to end
|
162
|
cksum = op + 0xff + len(payload)
|
163
|
for byte in payload:
|
164
|
cksum += byte
|
165
|
data.append(byte)
|
166
|
data.append(cksum & 0xf) # Yea, this is a 4 bit cksum (also known as a bug)
|
167
|
|
168
|
# now obfuscate by an xor starting with first payload byte ^ 0x52 including the trailing cksum.
|
169
|
xorbits = 0x52
|
170
|
for i, byte in enumerate(data[4:]):
|
171
|
xord = xorbits ^ byte
|
172
|
data[i + 4] = xord
|
173
|
xorbits = xord
|
174
|
return(data)
|
175
|
|
176
|
def pkt_decode(data):
|
177
|
"""Take a packet hot off the wire and decode it into clear text and return the fields.
|
178
|
We say <<cleartext>> here because all it turns out to be is annoying obfuscation.
|
179
|
This is the inverse of pkt_decode"""
|
180
|
|
181
|
errs = ""
|
182
|
|
183
|
# we don't care about data[0]. It is always 0x7d sl leave it alone
|
184
|
op = data[1]
|
185
|
direction = data[2]
|
186
|
bytecount = data[3]
|
187
|
|
188
|
# First un-obfuscate the payload and cksum
|
189
|
payload = bytearray()
|
190
|
xorbits = 0x52
|
191
|
for i, byte in enumerate(data[4:]):
|
192
|
payload.append(xorbits ^ byte)
|
193
|
xorbits = byte
|
194
|
|
195
|
# Calculate the checksum starting with the 3 bytes of the header
|
196
|
cksum = op + direction + bytecount
|
197
|
for byte in payload[:-1]:
|
198
|
cksum += byte
|
199
|
cksum_match = (cksum & 0xf) == payload[-1] # yes, a 4 bit cksum to match the encode
|
200
|
if (not cksum_match):
|
201
|
errs += "Checksum missmatch: %x != %x; " % (cksum, payload[-1])
|
202
|
return (cksum_match, op, direction, bytecount, payload[:-1], errs)
|
203
|
|
204
|
# display helpers
|
205
|
|
206
|
def short2freq(num):
|
207
|
""" Convert a signed 16 bit integer into a frequency string expressed in MHz
|
208
|
This is stored in the radio as units of 10KHz which we compensate to Hz.
|
209
|
A value of -1 indicates <no freqency>, i.e. unused channel.
|
210
|
"""
|
211
|
|
212
|
if (num > 0):
|
213
|
return "%2d.%01d" % (num/10, num % 10)
|
214
|
else:
|
215
|
return "----"
|
216
|
|
217
|
def int2freq(num):
|
218
|
""" Convert a signed 32 bit integer into a frequency string expressed in MHz
|
219
|
This is stored in the radio as units of 10Hz which we compensate to Hz.
|
220
|
A value of -1 indicates <no freqency>, i.e. unused channel.
|
221
|
"""
|
222
|
|
223
|
if (num > 0):
|
224
|
return "%3d.%05d" % (num/100000, num % 100000)
|
225
|
else:
|
226
|
return "--none---"
|
227
|
|
228
|
|
229
|
def callid2str(blk):
|
230
|
"""Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999).
|
231
|
Note that the supplied software has a bug that allows '#' and an LF (0x0a)
|
232
|
to be inserted. We do not check this (yet)
|
233
|
"""
|
234
|
|
235
|
bin2ascii = string.maketrans("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09",
|
236
|
"0123456789")
|
237
|
cid = struct.unpack(">6s2x", blk)
|
238
|
return cid[0].translate(bin2ascii)
|
239
|
|
240
|
def str2callid(s):
|
241
|
""" See callid2str. This must edit check for numbers and range
|
242
|
"""
|
243
|
blk = bytearray()
|
244
|
for c in s:
|
245
|
blk.append(ord(c))
|
246
|
blk.append(0x00)
|
247
|
blk.append(0x00)
|
248
|
return blk
|
249
|
|
250
|
def conf_item_name(item):
|
251
|
""" Return a name for a config item/addres if we know it
|
252
|
"""
|
253
|
if (item in config_item_type):
|
254
|
name = config_item_type[item]
|
255
|
else:
|
256
|
name = "0x%x" % item
|
257
|
return name
|
258
|
|
259
|
# config content encoder/decoders
|
260
|
|
261
|
# 0x740, 0x750, 0x760
|
262
|
def display_fm_chans(addr, blk):
|
263
|
""" Display FM band channels. These are stored as 16 bit signed integers in units
|
264
|
of 10KHz. They seem to be read in 32 byte chunks and written in 8 byte chunks.
|
265
|
This is a simple structure.
|
266
|
|
267
|
struct fm_chan {
|
268
|
int16_t rx;
|
269
|
} chan[n]; /* where n is a multiple of 8 in range 8-48 although the read returns 40
|
270
|
"""
|
271
|
chan = ((addr - 0x740)>>1) + 1
|
272
|
while (len(blk) > 0):
|
273
|
f = struct.unpack('>h', blk[0:2])
|
274
|
print("chan %2d: %s" % (chan, short2freq(f[0])))
|
275
|
blk = blk[2:]
|
276
|
chan += 1
|
277
|
|
278
|
# 0x8b0
|
279
|
|
280
|
def display_radio_conf(addr, blk):
|
281
|
"""General radio configuration controls
|
282
|
"""
|
283
|
cnames = {
|
284
|
0: "c0",
|
285
|
1: "c1",
|
286
|
2: "c2",
|
287
|
3: "c3",
|
288
|
4: "c4",
|
289
|
5: "c5",
|
290
|
6: "c6",
|
291
|
7: "c7",
|
292
|
8: "c8",
|
293
|
9: "c9",
|
294
|
10: "c10",
|
295
|
11: "Power on display",
|
296
|
12: "c12",
|
297
|
13: "c13",
|
298
|
14: "c14",
|
299
|
15: "c15"
|
300
|
}
|
301
|
|
302
|
for i, name in enumerate(blk):
|
303
|
if (i == 11):
|
304
|
if (blk[i] == 0x00):
|
305
|
val = "BATTERY"
|
306
|
else:
|
307
|
val = "BITMAP"
|
308
|
else:
|
309
|
val = "0x%02x" % blk[i]
|
310
|
print("%s: %s" % (cnames[i], val))
|
311
|
|
312
|
# 0x8c0
|
313
|
|
314
|
def display_ptt_id(addr, blk):
|
315
|
"""Radio sends its caller id on transmission
|
316
|
"""
|
317
|
cid = callid2str(blk[0:8])
|
318
|
bits = blk[8:]
|
319
|
print("My call ID: %s, bits %s" % (cid, hexprint(bits)))
|
320
|
|
321
|
# 0x8d0
|
322
|
|
323
|
def display_scc_control(addr, blk):
|
324
|
"""This is the stun/kill/revive code and control?
|
325
|
This is not definitive yet. It doesn't brick the radio but still...
|
326
|
"""
|
327
|
scc = callid2str(blk[0:8])
|
328
|
bits = blk[8:]
|
329
|
print("My SCC code: %s, bits %s" % (scc, hexprint(bits)))
|
330
|
|
331
|
|
332
|
# 0x940, 0x980
|
333
|
group_chan = {}
|
334
|
group_names = {}
|
335
|
|
336
|
def display_scan_group(item, blk):
|
337
|
""" Display the scan group list. It spans two content blocks
|
338
|
block 0x940 is the first part followed by block 0x980 which
|
339
|
contains the remaining names. In real life, these config read/write
|
340
|
could come in any order. for here, we assume this order.
|
341
|
|
342
|
struct scan_940 {
|
343
|
struct {
|
344
|
int16_t start_chan;
|
345
|
int16_t end_chan;
|
346
|
} groups[10];
|
347
|
unsigned char[8] padding;
|
348
|
struct {
|
349
|
char name[8];
|
350
|
} names[2];
|
351
|
}
|
352
|
|
353
|
struct scan_980 {
|
354
|
struct {
|
355
|
char name[8];
|
356
|
} names[8]
|
357
|
}
|
358
|
"""
|
359
|
global group_chan
|
360
|
global group_names
|
361
|
|
362
|
if (item == 0x940):
|
363
|
# first pick up the start end pairs for each group
|
364
|
group_chan = {}
|
365
|
group_names = {} # first block so clear anything old/stale
|
366
|
for i in range(0,10):
|
367
|
rec = blk[0:4]
|
368
|
(start, end) = struct.unpack('>hh', rec)
|
369
|
group_chan[i] = (start, end)
|
370
|
blk = blk[4:]
|
371
|
blk = blk[8:] # align over padding
|
372
|
|
373
|
# Followed by the first two group names
|
374
|
for i in range(0,2):
|
375
|
rec = blk[0:8]
|
376
|
name = struct.unpack('8s', rec)
|
377
|
group_names[i] = name[0].replace('\0', ' ') # make trailing nulls into spaces for pretty
|
378
|
blk = blk[8:]
|
379
|
|
380
|
# Put it together for the first two groups.
|
381
|
print("Address: 0x%x" % item)
|
382
|
for i in range(0,2):
|
383
|
start, end = group_chan[i]
|
384
|
print("group %2d '%s': channel %i to %i" % (i+1, group_names[i], start, end))
|
385
|
else:
|
386
|
# We unpack and display the remaining groups assuming 0x940 came first...
|
387
|
for i in range(2,10):
|
388
|
rec = blk[0:8]
|
389
|
name = struct.unpack('8s', rec)
|
390
|
group_names[i] = name[0].replace('\0', ' ') # make trailing nulls into spaces for pretty
|
391
|
blk = blk[8:]
|
392
|
|
393
|
# Put it together for the first two groups.
|
394
|
print("Address: 0x%x" % item)
|
395
|
for i in range(2,10):
|
396
|
start, end = group_chan[i]
|
397
|
print("group %2d '%s': channel %i to %i" % (i+1, group_names[i], start, end))
|
398
|
|
399
|
# 0x7400, 0x7440, 0x7480, 0x74c0, 0x7500
|
400
|
callid_code = {}
|
401
|
callid_name = {}
|
402
|
|
403
|
def display_callid(addr, blk):
|
404
|
"""Decode CALL-ID table which spans four 64 byte transfers.
|
405
|
We assume here that the requests are in address order.
|
406
|
There are two codes/names per line as 0 and 1 but both names
|
407
|
and codes are 1-20
|
408
|
|
409
|
struct callid_7400_7440 {
|
410
|
struct {
|
411
|
unsigned char cid0[6];
|
412
|
unsigned char pad0[2];
|
413
|
unsigned char cid1[6];
|
414
|
unsigned char pad1[2];
|
415
|
} callids[4];
|
416
|
}
|
417
|
|
418
|
struct callid_7800 {
|
419
|
struct {
|
420
|
unsigned char cid0[6];
|
421
|
unsigned char pad0;
|
422
|
unsigned char cid1[6];
|
423
|
unsigned char pad1;
|
424
|
} callids[2];
|
425
|
struct {
|
426
|
char name0[6];
|
427
|
char pad0[2]
|
428
|
char name1[6];
|
429
|
char pad1[2]
|
430
|
} names[2]
|
431
|
}
|
432
|
|
433
|
struct callid_78c0_7500 {
|
434
|
struct {
|
435
|
char name0[6];
|
436
|
char pad0[2]
|
437
|
char name1[6];
|
438
|
char pad1[2]
|
439
|
} names[2]
|
440
|
}
|
441
|
"""
|
442
|
global callid_code
|
443
|
global callid_name
|
444
|
|
445
|
if (addr == 0x7400): # callid codes 1-8
|
446
|
callid_code = {}
|
447
|
callid_name = {}
|
448
|
for i in range(0,8):
|
449
|
cid = callid2str(blk[0:8])
|
450
|
callid_code[i] = cid
|
451
|
blk = blk[8:]
|
452
|
|
453
|
elif (addr == 0x7440): # callid codes 9-15
|
454
|
for i in range(8,16):
|
455
|
rec = blk[0:8]
|
456
|
cid = callid2str(blk[0:8])
|
457
|
callid_code[i] = cid
|
458
|
blk = blk[8:]
|
459
|
|
460
|
elif (addr == 0x7480): # callid codes 16-20, names 1-4
|
461
|
for i in range(16,20):
|
462
|
rec = blk[0:8]
|
463
|
cid = callid2str(blk[0:8])
|
464
|
callid_code[i] = cid
|
465
|
blk = blk[8:]
|
466
|
for i in range(0,4):
|
467
|
rec = blk[0:8]
|
468
|
name = struct.unpack(">6sxx", rec)
|
469
|
callid_name[i] = name[0]
|
470
|
blk = blk[8:]
|
471
|
|
472
|
elif (addr == 0x74c0): # names 5-13
|
473
|
for i in range(4,12):
|
474
|
rec = blk[0:8]
|
475
|
name = struct.unpack(">6sxx", rec)
|
476
|
callid_name[i] = name[0]
|
477
|
blk = blk[8:]
|
478
|
|
479
|
else: # 0x7500 names 14-20, print them.
|
480
|
for i in range(12,20):
|
481
|
rec = blk[0:8]
|
482
|
name = struct.unpack(">6sxx", rec)
|
483
|
callid_name[i] = name[0]
|
484
|
blk = blk[8:]
|
485
|
for i in range(0,20):
|
486
|
name = callid_name[i].replace('\0', ' ')
|
487
|
code = callid_code[i]
|
488
|
print("call-id %2d '%s': code '%s'" % (i+1, name, code))
|
489
|
|
490
|
# Command/reply type decodes and encodes
|
491
|
|
492
|
def display_config(addr, contents):
|
493
|
"""Decode config addr blobs into something useful/readable
|
494
|
"""
|
495
|
|
496
|
item_name = conf_item_name(addr)
|
497
|
print("config item %s (0x%x)" % (item_name, addr))
|
498
|
if (addr == 0x8b0):
|
499
|
display_radio_conf(addr, contents)
|
500
|
elif (addr == 0x8c0):
|
501
|
display_ptt_id(addr, contents)
|
502
|
elif (addr == 0x8d0):
|
503
|
display_scc_control(addr, contents)
|
504
|
elif (addr == 0x8f0): # no menu, Banner 16 bytes, null term text followed by space (0x20)
|
505
|
print("%s: '%s'" % (item_name, contents.replace('\0', ' '))) # make trailing nulls into spaces for pretty
|
506
|
elif (addr == 0x740 or addr == 0x750 or addr == 0x760):
|
507
|
display_fm_chans(addr, contents)
|
508
|
elif (addr == 0x940 or addr == 0x980):
|
509
|
display_scan_group(addr, contents)
|
510
|
elif (addr == 0x7400 or addr == 0x7440 or addr == 0x7480 or addr == 0x74c0 or addr == 0x7500):
|
511
|
display_callid(addr, contents)
|
512
|
else:
|
513
|
print("%s" % hexprint(contents))
|
514
|
|
515
|
def display_ident(blk):
|
516
|
"""The ident block identifies the radio and its capabilities. This block is always 78 bytes
|
517
|
The rev == '01' is the base radio and '02' seems to be the '-Plus' version
|
518
|
"""
|
519
|
|
520
|
fmt = '>7s2s'
|
521
|
rec = blk[0:9]
|
522
|
rest = blk[9:]
|
523
|
part1 = rest[0:15]
|
524
|
part2 = rest[15:28]
|
525
|
part21 = rest[28:33]
|
526
|
part3 = rest[33:48]
|
527
|
part4 = rest[48:61]
|
528
|
part41 = rest[61:]
|
529
|
(model, rev) = struct.unpack(fmt, rec)
|
530
|
print("model = '%s', revision = '%s'" % (model, rev))
|
531
|
print("part1 [%i]\n%s" % (len(part1), hexprint(part1)))
|
532
|
print("part2 [%i]\n%s" % (len(part2), hexprint(part2)))
|
533
|
print("part21 [%i]\n%s" % (len(part2), hexprint(part21)))
|
534
|
print("part3 [%i]\n%s" % (len(part3), hexprint(part3)))
|
535
|
print("part4 [%i]\n%s" % (len(part3), hexprint(part4)))
|
536
|
print("part41 [%i]\n%s" % (len(part3), hexprint(part41)))
|
537
|
|
538
|
# Channel memory read/write decode/encode
|
539
|
|
540
|
def display_chan_blk(address, blk):
|
541
|
"""The channel memory is composed of 96 byte blocks of the following
|
542
|
structure. Each block contains four memories. The blocks are sequencially
|
543
|
addressed to support the 1-999 memories.
|
544
|
|
545
|
struct chan_blk {
|
546
|
struct {
|
547
|
int32_t rx;
|
548
|
int32_t tx;
|
549
|
unsigned char[8];
|
550
|
} freq[4];
|
551
|
struct {
|
552
|
char name[8];
|
553
|
} name[8];
|
554
|
}
|
555
|
|
556
|
The radio deletes a channel by nulling the name and setting the receive
|
557
|
frequency to -1. tx freq is unchanged and bits partially change.
|
558
|
"""
|
559
|
if (len(blk) != 96):
|
560
|
print("display_chan_blk: block too small %d" % len(blk))
|
561
|
return
|
562
|
|
563
|
# First pick up the frequency and mode/modulation/??? records
|
564
|
freqs = {}
|
565
|
for i in range(0,4):
|
566
|
rec = blk[0:16]
|
567
|
(r, t, bitsh, bitsl) = struct.unpack('>llLL', rec)
|
568
|
freqs[i] = (r, t, bitsh, bitsl)
|
569
|
blk = blk[16:]
|
570
|
|
571
|
# Followed by the names
|
572
|
names = {}
|
573
|
for i in range(0,4):
|
574
|
rec = blk[0:8]
|
575
|
name = struct.unpack('8s', rec)
|
576
|
names[i] = name[0].replace('\0', ' ') # make trailing nulls into spaces for pretty
|
577
|
blk = blk[8:]
|
578
|
|
579
|
# Put it all together and print it
|
580
|
print("Address: 0x%x" % address)
|
581
|
for i in range(0,4):
|
582
|
r, t, bitsh, bitsl = freqs[i]
|
583
|
chan = ((address>>4) - 0xa0) + 1 +i # map memory address and record to radio channel number
|
584
|
print("rec %d, ch %3d [%s]: rx=%s, tx=%s, bits=%08x-%08x" % (i, chan, names[i], int2freq(r), int2freq(t),
|
585
|
bitsh, bitsl))
|
586
|
|
587
|
# Process a line to/from the radio
|
588
|
|
589
|
def _read_record(lineno, data):
|
590
|
"""Return valid, cmd, direction, size, payload """
|
591
|
|
592
|
global checksum_seed
|
593
|
|
594
|
errs = ""
|
595
|
|
596
|
### 7D 82 00 12 52 12 22 12 22 12 22 12 ED 12 22 12 22 12 22 12 ED 12 12
|
597
|
|
598
|
if (len(data) < 4):
|
599
|
print("Line %d: Error: Packet too short (%d)" % (lineno, len(data)))
|
600
|
return (False, 0, 0, 0, 0)
|
601
|
if (data[0] != 0x7D):
|
602
|
print("Line %d: Error: Packet start is not right. %02x != 0x7D" % (lineno, data[0]))
|
603
|
return (False, 0, 0, 0, 0)
|
604
|
|
605
|
# Decode the packet
|
606
|
(ckmatch, cmd, xfer_dir, _length, payload, decode_errs) = pkt_decode(data)
|
607
|
|
608
|
# Validations
|
609
|
|
610
|
# direction
|
611
|
if (xfer_dir == 0x00):
|
612
|
direction = "reply"
|
613
|
to_radio = False
|
614
|
elif (xfer_dir == 0xFF):
|
615
|
direction = "command"
|
616
|
to_radio = True
|
617
|
else:
|
618
|
print("Line %d: Unknown direction: %02x" % (lineno, xfer_dir))
|
619
|
return(False, 0, 0,0,0)
|
620
|
|
621
|
# Payload length
|
622
|
if (_length > 0 and ((len(payload)) != _length)): #
|
623
|
errs += "Packet length does not match len(payload) = %d, length = %d; " % (len(payload) , _length)
|
624
|
if (to_radio):
|
625
|
if (cmd in cmd_lerrs):
|
626
|
cmd_lerrs[cmd] += 1
|
627
|
else:
|
628
|
cmd_lerrs[cmd] = 1
|
629
|
else:
|
630
|
if (cmd in rep_lerrs):
|
631
|
rep_lerrs[cmd] += 1
|
632
|
else:
|
633
|
rep_lerrs[cmd] = 1
|
634
|
|
635
|
#Checksum match
|
636
|
if (not ckmatch):
|
637
|
errs += decode_errs
|
638
|
if (to_radio):
|
639
|
if (cmd in cmd_cerrs):
|
640
|
cmd_cerrs[cmd] += 1
|
641
|
else:
|
642
|
cmd_cerrs[cmd] = 1
|
643
|
else:
|
644
|
if (cmd in rep_cerrs):
|
645
|
rep_cerrs[cmd] += 1
|
646
|
else:
|
647
|
rep_cerrs[cmd] = 1
|
648
|
|
649
|
# Count commands and replys by type
|
650
|
if (to_radio):
|
651
|
if (cmd in cmd_cnt):
|
652
|
cmd_cnt[cmd] += 1
|
653
|
else:
|
654
|
cmd_cnt[cmd] = 1
|
655
|
else:
|
656
|
if (cmd in rep_cnt):
|
657
|
rep_cnt[cmd] += 1
|
658
|
else:
|
659
|
rep_cnt[cmd] = 1
|
660
|
|
661
|
# Decode packets
|
662
|
print("Line %d: %s %s (%d) %s" % (lineno, cmd_name[cmd], direction, _length, errs))
|
663
|
if (cmd == 0x80): # Hello, ident yourself
|
664
|
if (to_radio):
|
665
|
if(_length != 0):
|
666
|
print("malformed ident query")
|
667
|
else: # Hello, ident yourself
|
668
|
display_ident(payload)
|
669
|
|
670
|
elif (cmd == 0x81): # Hang up. We are done. No reply from radio
|
671
|
if (to_radio):
|
672
|
if(_length != 0):
|
673
|
print("malformed ident query")
|
674
|
else: # should get no hangup reply
|
675
|
print("Error: radio should reboot, not reply!")
|
676
|
else: # these commands move data
|
677
|
address = payload[0]<<8 | payload[1]
|
678
|
if (cmd == 0x82): # Read config
|
679
|
if to_radio:
|
680
|
count = payload[2]
|
681
|
print("config item %s (0x%x), fetch %d" % (conf_item_name(address), address, count))
|
682
|
else:
|
683
|
item_value = payload[2:]
|
684
|
display_config(address, item_value)
|
685
|
|
686
|
elif (cmd == 0x83): # write config
|
687
|
if to_radio:
|
688
|
item_value = payload[2:]
|
689
|
display_config(address, item_value)
|
690
|
else:
|
691
|
print("%s: OK" % conf_item_name(address))
|
692
|
|
693
|
elif (cmd == 0x84): # Read channel memory
|
694
|
if to_radio:
|
695
|
count = payload[2]
|
696
|
print("EEPROM addr 0x%x, fetch %d" % (address, count))
|
697
|
else:
|
698
|
eeprom_contents = payload[2:]
|
699
|
display_chan_blk(address, eeprom_contents)
|
700
|
|
701
|
elif (cmd == 0x85): # Write channel memory
|
702
|
if to_radio:
|
703
|
eeprom_contents = payload[2:]
|
704
|
display_chan_blk(address, eeprom_contents)
|
705
|
else:
|
706
|
print("%s: OK" % conf_item_name(address))
|
707
|
|
708
|
else:
|
709
|
print("Unknown %s command: %x\n[%i]\n%s" % (direction, cmd, len(payload), hexprint(payload)))
|
710
|
|
711
|
print("\n------------------ end of line ---------------------------")
|
712
|
# valid record
|
713
|
return (True, cmd, direction, _length, payload)
|
714
|
|
715
|
def l2b(data):
|
716
|
"""Line lo bytes string safely.
|
717
|
using chr() introduces unicode code point conversions.
|
718
|
We use (force) real byte (integer) types because duck typing of strings bite back."""
|
719
|
# convert the data in string format to unicode safe byte format
|
720
|
|
721
|
return bytearray.fromhex(data)
|
722
|
|
723
|
|
724
|
def dump(lineno, data):
|
725
|
(valid, cmd, direction, size, payload) = _read_record(lineno, l2b(data))
|
726
|
|
727
|
global cmd_cnt
|
728
|
global rep_cnt
|
729
|
global cmd_cerrs
|
730
|
global rep_cerrs
|
731
|
global cmd_lerrs
|
732
|
global rep_lerrs
|
733
|
|
734
|
# packet validation
|
735
|
if (valid == False):
|
736
|
# invalid packet, finish
|
737
|
print("Invalid packet at line %d" % lineno)
|
738
|
elif (cmd == 0x81): # roll up stats on hangup
|
739
|
print("At hangup at line %d. command stats\n # cmds cksm length" % lineno)
|
740
|
for cmd in list(cmd_cnt):
|
741
|
if (cmd in cmd_cerrs):
|
742
|
cerrs = cmd_cerrs[cmd]
|
743
|
else:
|
744
|
cerrs = 0
|
745
|
if (cmd in cmd_lerrs):
|
746
|
lerrs = cmd_lerrs[cmd]
|
747
|
else:
|
748
|
lerrs = 0
|
749
|
print("command %12s: %4d, %4d, %4d" % (cmd_name[cmd], cmd_cnt[cmd], cerrs, lerrs))
|
750
|
for cmd in list(rep_cnt):
|
751
|
if (cmd in rep_cerrs):
|
752
|
cerrs = rep_cerrs[cmd]
|
753
|
else:
|
754
|
cerrs = 0
|
755
|
if (cmd in rep_lerrs):
|
756
|
lerrs = rep_lerrs[cmd]
|
757
|
else:
|
758
|
lerrs = 0
|
759
|
print("reply %12s: %4d, %4d, %4d" % (cmd_name[cmd], rep_cnt[cmd], cerrs, lerrs))
|
760
|
cmd_cnt = {}
|
761
|
rep_cnt = {}
|
762
|
cmd_cerrs = {}
|
763
|
rep_cerrs = {}
|
764
|
cmd_lerrs = {}
|
765
|
rep_lerrs = {}
|
766
|
print("========================= end of session ====================================");
|
767
|
return cmd
|
768
|
|
769
|
|
770
|
# run it on the same folder as the data_down_clean.txt file
|
771
|
|
772
|
with open(sys.argv[1]) as f:
|
773
|
linenum = 1
|
774
|
for line in f:
|
775
|
line = line.replace('\n', '') # chomp
|
776
|
# drop empty lines and lines w/out the magic tag.
|
777
|
if (len(line) > 0 and line[0] in 'WR' and line[1] == ":"):
|
778
|
comm = dump(linenum, line[3:])
|
779
|
linenum = linenum + 1
|
780
|
|
781
|
## # dump just a few packets
|
782
|
## if (linenum > 3):
|
783
|
## break
|
784
|
|