Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
133
python/logic_analyzer/decoders/set_uart.py
Normal file
133
python/logic_analyzer/decoders/set_uart.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Portable wire readers based on templates/c/set-protocol (no native DLL)."""
|
||||
import binascii
|
||||
|
||||
|
||||
V2 = {1: 'PING', 2: 'DEVICE_INFO', 3: 'CAPABILITIES', 8: 'DIAGNOSTICS',
|
||||
9: 'READ', 10: 'WRITE', 16: 'LOG_READ', 17: 'CATALOG',
|
||||
18: 'SUBSCRIBE', 19: 'PUBLISH', 20: 'UNSUBSCRIBE'}
|
||||
V2.update(dict((256 + i, 'FW_' + name) for i, name in enumerate(
|
||||
('BEGIN', 'DATA', 'END', 'ABORT', 'STATUS', 'ACTIVATE'))))
|
||||
V1 = dict(enumerate(('PING DEVICE_INFO GET_OBJECT SET_OBJECT GET_OBJECT_LIST '
|
||||
'GET_OBJECT_INFO COMMAND_STATUS DIAGNOSTICS READ_REGISTERS WRITE_REGISTERS '
|
||||
'FIRMWARE_BEGIN FIRMWARE_DATA FIRMWARE_END FIRMWARE_ABORT FIRMWARE_STATUS '
|
||||
'READ_LOGS GAS_CATALOG GAS_WATCH_SET GAS_WATCH_DATA').split(), 1))
|
||||
V1.update({128: 'NACK', 129: 'ACK', 130: 'ERROR'})
|
||||
V1.update(dict(enumerate(('SENSOR_SCAN SENSOR_LIST SENSOR_READ SENSOR_DATA '
|
||||
'SET_USER_BYTES SET_RESOLUTION SET_POLL_PERIOD SEND_ID_CAN UI_KEY UI_READ '
|
||||
'UI_STATE EEPROM_SCAN EEPROM_INFO EEPROM_READ EEPROM_LIST').split(), 32)))
|
||||
|
||||
|
||||
def number(data, order='little'):
|
||||
return int.from_bytes(bytes(data), order)
|
||||
|
||||
|
||||
def crc16(data, modbus=False):
|
||||
crc = 0xffff
|
||||
for byte in data:
|
||||
crc ^= byte if modbus else byte << 8
|
||||
for _ in range(8):
|
||||
if modbus:
|
||||
crc = (crc >> 1) ^ (0xa001 if crc & 1 else 0)
|
||||
else:
|
||||
crc = ((crc << 1) ^ (0x1021 if crc & 0x8000 else 0)) & 0xffff
|
||||
return crc
|
||||
|
||||
|
||||
def parse_frame(raw):
|
||||
raw = bytes(raw)
|
||||
bridge = raw[:2] == b'\xaa\x55'
|
||||
if bridge:
|
||||
if len(raw) < 11 or not 6 <= raw[2] <= 14 or len(raw) != raw[2] + 5:
|
||||
raise ValueError('Invalid ProtoCAN length')
|
||||
crc, received = crc16(raw[2:-2]), number(raw[-2:])
|
||||
ident, flags = number(raw[5:9]), raw[4]
|
||||
if ident > (0x1fffffff if flags & 1 else 0x7ff) or flags & 0xf0:
|
||||
raise ValueError('Invalid ProtoCAN ID/flags')
|
||||
result = dict(protocol='ProtoCAN bridge', sequence=raw[3], flags=flags,
|
||||
can_id=ident, payload=raw[9:-2])
|
||||
result['summary'] = 'ProtoCAN seq=%d ID=%08X flags=%02X' % (raw[3], ident, flags)
|
||||
else:
|
||||
if len(raw) < 12 or raw[:2] != b'\xa5\x5a' or raw[2] not in (1, 2):
|
||||
raise ValueError('Invalid SET signature/version')
|
||||
version = raw[2]
|
||||
header = 14 if version == 2 else 8
|
||||
if len(raw) < header + 4:
|
||||
raise ValueError('Incomplete SET header')
|
||||
size = number(raw[12:14]) if version == 2 else number(raw[6:8], 'big')
|
||||
if size > 512 or len(raw) != header + size + 4:
|
||||
raise ValueError('Invalid SET payload length')
|
||||
crc, received = binascii.crc32(raw[2:-4]) & 0xffffffff, number(raw[-4:])
|
||||
msg = number(raw[4:6]) if version == 2 else raw[3]
|
||||
seq = number(raw[10:12]) if version == 2 else number(raw[4:6], 'big')
|
||||
result = dict(protocol='SET v2' if version == 2 else 'SETGUI v1',
|
||||
message_type=msg, sequence=seq, payload=raw[header:-4])
|
||||
name = (V2 if version == 2 else V1).get(msg, 'TYPE_%04X' % msg)
|
||||
result['summary'] = '%s %s seq=%d len=%d' % (result['protocol'], name, seq, size)
|
||||
if version == 2:
|
||||
result.update(source=number(raw[6:8]), destination=number(raw[8:10]), flags=raw[3])
|
||||
result['summary'] += ' %04X->%04X flags=%02X' % (result['source'], result['destination'], raw[3])
|
||||
if raw[3] & 0xc0:
|
||||
raise ValueError('Reserved SET flags')
|
||||
if raw[3] & 1:
|
||||
if size < 2:
|
||||
raise ValueError('SET response missing status')
|
||||
result['status'] = number(result['payload'][:2])
|
||||
result['summary'] += ' status=%d' % result['status']
|
||||
if crc != received:
|
||||
raise ValueError('CRC mismatch: received %X expected %X' % (received, crc))
|
||||
result['raw'] = raw
|
||||
return result
|
||||
|
||||
|
||||
class StreamParser:
|
||||
"""Bounded byte stream; keep sample positions during resynchronization."""
|
||||
def __init__(self, mode='auto'):
|
||||
self.mode = mode
|
||||
self.buffer = []
|
||||
|
||||
def feed(self, value, start, end):
|
||||
self.buffer.append((value, start, end))
|
||||
events = []
|
||||
while len(self.buffer) >= 3:
|
||||
raw = bytes(item[0] for item in self.buffer)
|
||||
bridge = raw[:2] == b'\xaa\x55' and self.mode in ('auto', 'bridge')
|
||||
modern = raw[:2] == b'\xa5\x5a' and self.mode != 'bridge'
|
||||
if not bridge and not modern:
|
||||
del self.buffer[0]
|
||||
continue
|
||||
error = None
|
||||
if bridge:
|
||||
total = raw[2] + 5
|
||||
if not 6 <= raw[2] <= 14:
|
||||
error = 'Invalid ProtoCAN length'
|
||||
else:
|
||||
if raw[2] not in (1, 2) or self.mode in ('v1', 'v2') and raw[2] != int(self.mode[1]):
|
||||
error = 'Unexpected SET version'
|
||||
header = 14 if raw[2] == 2 else 8
|
||||
if not error and len(raw) < header:
|
||||
break
|
||||
size = number(raw[12:14]) if raw[2] == 2 else number(raw[6:8], 'big')
|
||||
total = header + size + 4
|
||||
if size > 512:
|
||||
error = 'Payload exceeds 512 bytes'
|
||||
if not error and len(raw) < total:
|
||||
break
|
||||
last = min(len(raw), total) - 1
|
||||
result = None
|
||||
if not error:
|
||||
try:
|
||||
result = parse_frame(raw[:total])
|
||||
except ValueError as exc:
|
||||
error = str(exc)
|
||||
events.append((self.buffer[0][1], self.buffer[last][2], result, error))
|
||||
del self.buffer[:1 if error else total]
|
||||
return events
|
||||
|
||||
def flush(self):
|
||||
if not self.buffer:
|
||||
return []
|
||||
pending = self.buffer
|
||||
self.buffer = []
|
||||
if pending[0][0] not in (0xa5, 0xaa):
|
||||
return []
|
||||
return [(pending[0][1], pending[-1][2], None, 'Incomplete frame')]
|
||||
Reference in New Issue
Block a user