"""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