Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
124
python/logic_analyzer/decoders/set_can.py
Normal file
124
python/logic_analyzer/decoders/set_can.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""ProtoCAN, Balsam and SET v2 classic CAN binding."""
|
||||
from .set_uart import number, parse_frame
|
||||
|
||||
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS',
|
||||
4: 'MODBUS_COIL', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING',
|
||||
7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A',
|
||||
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
|
||||
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY '
|
||||
'COMMIT CONFIRM REBOOT ABORT QUERY_PROGRESS').split(), 1))
|
||||
|
||||
|
||||
def legacy(ident, data, profile='protocan'):
|
||||
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
|
||||
raise ValueError('Invalid classic CAN frame')
|
||||
relative = ident - 0xba0000
|
||||
if profile == 'balsam':
|
||||
if not (0 <= relative < 13 or 16 <= relative < 29):
|
||||
return None
|
||||
if len(data) != 8:
|
||||
raise ValueError('Balsam requires DLC=8')
|
||||
header = number(data[:2], 'big')
|
||||
address, mask = header & 0x1fff, header >> 13
|
||||
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
|
||||
for i in range(3) if mask & (4 >> i)]
|
||||
return dict(protocol='Balsam', registers=registers, summary=
|
||||
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
|
||||
'data' if relative >= 16 else 'command', mask,
|
||||
' '.join('R%04X=%04X' % pair for pair in registers)))
|
||||
msg, body = (ident >> 16) & 15, ident & 0xffff
|
||||
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
|
||||
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
|
||||
device_id=(ident >> 20) & 15, message_type=msg, body=body)
|
||||
text = 'ProtoCAN %s dev=%d:%d route=%d priority=%d body=%04X' % (
|
||||
TYPES.get(msg, 'TYPE_%X' % msg), result['device_type'], result['device_id'],
|
||||
result['route'], result['priority'], body)
|
||||
if msg == 3:
|
||||
if len(data) % 2 or body + len(data)//2 > 65536:
|
||||
raise ValueError('Invalid GAS register payload/range')
|
||||
result['registers'] = [(body + i//2, number(data[i:i+2])) for i in range(0, len(data), 2)]
|
||||
text += ' ' + ('READ' if not data else ' '.join('R%04X=%04X' % pair for pair in result['registers']))
|
||||
elif 4 <= msg <= 7:
|
||||
text += ' address=%03X count=%d' % (body >> 4, body & 15)
|
||||
elif msg == 8:
|
||||
text += ' info=%02X code=%02X' % (body >> 8, body & 255)
|
||||
elif msg in (9, 12):
|
||||
command = body & 255
|
||||
text += ' session=%d command=%s' % (body >> 8, BOOT.get(command, '%02X' % command))
|
||||
if msg == 12:
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT_STATUS requires DLC=8')
|
||||
text += ' status=%d slot=%d next_block=%d CRC32=%08X' % (data[0], data[1], number(data[2:4]), number(data[4:8]))
|
||||
elif command in (3, 4):
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT metadata requires DLC=8')
|
||||
if command == 3:
|
||||
text += ' image_size=%d CRC32=%08X' % (number(data[:4]), number(data[4:]))
|
||||
else:
|
||||
text += ' product=%d hw=%d..%d version=%08X' % (number(data[:2]), data[2], data[3], number(data[4:]))
|
||||
elif command in BOOT and data:
|
||||
raise ValueError('BOOT control requires DLC=0')
|
||||
elif msg in (10, 11):
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT_DATA requires DLC=8')
|
||||
text += ' block=%d offset=0x%X' % (body, body * 8)
|
||||
elif msg in (0, 1, 2):
|
||||
shift = 4 if msg == 0 else 12
|
||||
text += ' type=%X value=%X' % (body >> shift, body & ((1 << shift) - 1))
|
||||
result['summary'] = text
|
||||
return result
|
||||
|
||||
|
||||
class Reassembler:
|
||||
def __init__(self):
|
||||
self.pending = {}
|
||||
|
||||
def feed(self, ident, data, ss, es, now_ms):
|
||||
events = []
|
||||
for key, state in list(self.pending.items()):
|
||||
if now_ms - state['time'] >= 500:
|
||||
events.append((state['start'], es, None, 'SET CAN timeout ID=%08X' % key))
|
||||
del self.pending[key]
|
||||
if ident >> 24 != 0x12:
|
||||
return events
|
||||
try:
|
||||
if not 1 <= len(data) <= 8:
|
||||
raise ValueError('Invalid SET CAN DLC')
|
||||
pci = data[0] & 0xf0
|
||||
if pci == 0x30:
|
||||
if len(data) != 3 or data[0] & 15 > 2 or data[2] > 127:
|
||||
raise ValueError('Invalid FLOW_CONTROL')
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='FLOW_CONTROL status=%d block=%d st_min=%d ms' % (data[0] & 15, data[1], data[2])), None))
|
||||
elif pci == 0x10:
|
||||
if data[0] != 0x10 or len(data) != 8 or not 18 <= number(data[1:3]) <= 530:
|
||||
raise ValueError('Invalid FIRST segment')
|
||||
old = self.pending.pop(ident, None)
|
||||
if old:
|
||||
events.append((old['start'], es, None, 'FIRST replaced incomplete packet'))
|
||||
if len(self.pending) >= 64:
|
||||
raise ValueError('Too many concurrent SET CAN packets (64)')
|
||||
self.pending[ident] = dict(start=ss, time=now_ms, total=number(data[1:3]),
|
||||
data=bytearray(data[3:]), sequence=1)
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='FIRST length=%d' % number(data[1:3])), None))
|
||||
elif pci == 0x20:
|
||||
state = self.pending.get(ident)
|
||||
if not state or len(data) < 2 or data[0] & 15 != state['sequence']:
|
||||
raise ValueError('Unexpected CONSECUTIVE sequence')
|
||||
state['data'].extend(data[1:])
|
||||
state['time'], state['sequence'] = now_ms, (state['sequence'] + 1) & 15
|
||||
if len(state['data']) > state['total']:
|
||||
raise ValueError('SET CAN packet exceeds advertised length')
|
||||
if len(state['data']) == state['total']:
|
||||
packet = parse_frame(state['data'])
|
||||
if packet['protocol'] != 'SET v2' or packet['source'] != (ident >> 8) & 255 or packet['destination'] != (ident >> 16) & 255 or bool(packet['flags'] & 32) != bool(ident & 128):
|
||||
raise ValueError('CAN ID and SET header disagree')
|
||||
events.append((state['start'], es, packet, None))
|
||||
del self.pending[ident]
|
||||
else:
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='CONSECUTIVE SN=%d' % (data[0] & 15)), None))
|
||||
else:
|
||||
raise ValueError('Unknown SET CAN PCI')
|
||||
except ValueError as exc:
|
||||
state = self.pending.pop(ident, None)
|
||||
events.append((state['start'] if state else ss, es, None, str(exc)))
|
||||
return events
|
||||
Reference in New Issue
Block a user