Project

General

Profile

New Model #3509 » test.py

test program to decode radio transactions - James Lieb, 04/19/2018 01:40 PM

 
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

    
(2-2/11)