125 lines
6.6 KiB
Python
125 lines
6.6 KiB
Python
"""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
|