Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
1
python/logic_analyzer/decoders/__init__.py
Normal file
1
python/logic_analyzer/decoders/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable device libraries from setcorp/templates."""
|
||||
181
python/logic_analyzer/decoders/gate_timing.py
Normal file
181
python/logic_analyzer/decoders/gate_timing.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Pure timing checker used by the DSView decoder and unit tests.
|
||||
|
||||
All time values in profiles are nanoseconds. The ACK delay in the vendor
|
||||
data sheets is a typical value only, so the delay window is intentionally an
|
||||
engineering warning threshold. ACK pulse width has specified limits and is
|
||||
therefore checked as PASS/FAIL.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
PROFILES = {
|
||||
'1SP0635': {
|
||||
'ack_delay_typ_ns': 250.0,
|
||||
'ack_width_min_ns': 400.0,
|
||||
'ack_width_typ_ns': 700.0,
|
||||
'ack_width_max_ns': 1050.0,
|
||||
'fault_threshold_ns': 1500.0,
|
||||
},
|
||||
'1SD536F2': {
|
||||
'ack_delay_typ_ns': 380.0,
|
||||
'ack_width_min_ns': 600.0,
|
||||
'ack_width_typ_ns': 900.0,
|
||||
'ack_width_max_ns': 1800.0,
|
||||
# The application manual calls >1.5 us a fault, while individual
|
||||
# data sheets allow ACK pulses up to 1.8 us. A correlated pulse in
|
||||
# the specified ACK range wins; otherwise the longer limit is used.
|
||||
'fault_threshold_ns': 1800.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def format_ns(value_ns):
|
||||
if value_ns >= 1000000.0:
|
||||
return '%.3f ms' % (value_ns / 1000000.0)
|
||||
if value_ns >= 1000.0:
|
||||
return '%.3f us' % (value_ns / 1000.0)
|
||||
return '%.1f ns' % value_ns
|
||||
|
||||
|
||||
class TimingChecker(object):
|
||||
"""Match Vin edges to Vstat pulses and return annotation dictionaries."""
|
||||
|
||||
def __init__(self, samplerate, profile='1SP0635', delay_tolerance_ns=100.0,
|
||||
vin_active_high=True, vstat_active_high=True,
|
||||
custom=None):
|
||||
if not samplerate:
|
||||
raise ValueError('samplerate is required')
|
||||
if profile == 'custom':
|
||||
if not custom:
|
||||
raise ValueError('custom profile values are required')
|
||||
self.spec = dict(custom)
|
||||
else:
|
||||
self.spec = dict(PROFILES[profile])
|
||||
self.profile = profile
|
||||
self.samplerate = float(samplerate)
|
||||
self.delay_tolerance_ns = float(delay_tolerance_ns)
|
||||
self.vin_active_high = bool(vin_active_high)
|
||||
self.vstat_active_high = bool(vstat_active_high)
|
||||
self.pending = deque()
|
||||
self.status_start = None
|
||||
self.status_control = None
|
||||
|
||||
# Long enough to avoid calling a merely late ACK "missing", while
|
||||
# still producing a useful annotation during a capture.
|
||||
self.ack_timeout_ns = max(
|
||||
self.spec['ack_delay_typ_ns'] + 3.0 * self.delay_tolerance_ns,
|
||||
self.spec['ack_delay_typ_ns'] + self.spec['ack_width_max_ns'])
|
||||
|
||||
def samples_to_ns(self, samples):
|
||||
return float(samples) * 1000000000.0 / self.samplerate
|
||||
|
||||
def ns_to_samples(self, value_ns):
|
||||
return max(1, int(round(float(value_ns) * self.samplerate / 1000000000.0)))
|
||||
|
||||
def next_deadline(self):
|
||||
if not self.pending:
|
||||
return None
|
||||
return self.pending[0]['sample'] + self.ns_to_samples(self.ack_timeout_ns)
|
||||
|
||||
def on_control_edge(self, sample, level):
|
||||
state_on = bool(level) == self.vin_active_high
|
||||
item = {
|
||||
'sample': int(sample),
|
||||
'level': int(level),
|
||||
'edge': 'ON' if state_on else 'OFF',
|
||||
}
|
||||
self.pending.append(item)
|
||||
return [{
|
||||
'kind': 'control', 'start': int(sample), 'end': int(sample),
|
||||
'text': 'Vin %s' % item['edge'], 'short': item['edge'],
|
||||
}]
|
||||
|
||||
def expire(self, sample):
|
||||
events = []
|
||||
timeout_samples = self.ns_to_samples(self.ack_timeout_ns)
|
||||
while self.pending and int(sample) >= self.pending[0]['sample'] + timeout_samples:
|
||||
item = self.pending.popleft()
|
||||
end = item['sample'] + timeout_samples
|
||||
events.append({
|
||||
'kind': 'missing', 'start': item['sample'], 'end': end,
|
||||
'text': 'FAIL: no Vstat ACK after Vin %s (timeout %s)' %
|
||||
(item['edge'], format_ns(self.ack_timeout_ns)),
|
||||
'short': 'NO ACK',
|
||||
})
|
||||
return events
|
||||
|
||||
def on_status_edge(self, sample, level):
|
||||
sample = int(sample)
|
||||
is_active = bool(level) == self.vstat_active_high
|
||||
events = self.expire(sample)
|
||||
|
||||
if is_active:
|
||||
# Ignore a second active edge caused by an inconsistent trace.
|
||||
if self.status_start is not None:
|
||||
return events
|
||||
self.status_start = sample
|
||||
self.status_control = self.pending.popleft() if self.pending else None
|
||||
if self.status_control is not None:
|
||||
delay_ns = self.samples_to_ns(sample - self.status_control['sample'])
|
||||
typ_ns = self.spec['ack_delay_typ_ns']
|
||||
delta_ns = delay_ns - typ_ns
|
||||
in_window = abs(delta_ns) <= self.delay_tolerance_ns
|
||||
events.append({
|
||||
'kind': 'delay_ok' if in_window else 'delay_warn',
|
||||
'start': self.status_control['sample'], 'end': sample,
|
||||
'delay_ns': delay_ns,
|
||||
'text': '%s: ACK delay %s (typ %s, delta %+0.1f ns)' %
|
||||
('PASS' if in_window else 'WARN', format_ns(delay_ns),
|
||||
format_ns(typ_ns), delta_ns),
|
||||
'short': '%s %s' % ('OK' if in_window else 'WARN',
|
||||
format_ns(delay_ns)),
|
||||
})
|
||||
return events
|
||||
|
||||
if self.status_start is None:
|
||||
events.append({
|
||||
'kind': 'orphan', 'start': sample, 'end': sample,
|
||||
'text': 'Unexpected inactive Vstat edge', 'short': 'Vstat?',
|
||||
})
|
||||
return events
|
||||
|
||||
start = self.status_start
|
||||
control = self.status_control
|
||||
width_ns = self.samples_to_ns(sample - start)
|
||||
self.status_start = None
|
||||
self.status_control = None
|
||||
lo = self.spec['ack_width_min_ns']
|
||||
hi = self.spec['ack_width_max_ns']
|
||||
|
||||
if control is not None and lo <= width_ns <= hi:
|
||||
events.append({
|
||||
'kind': 'width_ok', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'PASS: ACK width %s (limit %s...%s)' %
|
||||
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
||||
'short': 'ACK %s' % format_ns(width_ns),
|
||||
})
|
||||
elif width_ns > self.spec['fault_threshold_ns']:
|
||||
events.append({
|
||||
'kind': 'fault', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'FAULT: Vstat active for %s' % format_ns(width_ns),
|
||||
'short': 'FAULT %s' % format_ns(width_ns),
|
||||
})
|
||||
elif control is None:
|
||||
events.append({
|
||||
'kind': 'orphan', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'Unexpected Vstat pulse %s (no Vin edge)' % format_ns(width_ns),
|
||||
'short': 'ORPHAN %s' % format_ns(width_ns),
|
||||
})
|
||||
else:
|
||||
events.append({
|
||||
'kind': 'width_fail', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'FAIL: ACK width %s outside %s...%s' %
|
||||
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
||||
'short': 'BAD ACK %s' % format_ns(width_ns),
|
||||
})
|
||||
return events
|
||||
95
python/logic_analyzer/decoders/pm35_uart.py
Normal file
95
python/logic_analyzer/decoders/pm35_uart.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""PM35 MODBUS 03/06, including the legacy 128-word/zero-byte-count reply."""
|
||||
from .set_uart import crc16, number
|
||||
|
||||
|
||||
def parse_pm35(raw, role):
|
||||
raw = bytes(raw)
|
||||
if len(raw) < 5:
|
||||
raise ValueError('Incomplete PM35 frame')
|
||||
if crc16(raw[:-2], True) != number(raw[-2:]):
|
||||
raise ValueError('PM35 CRC mismatch')
|
||||
address, function = raw[:2]
|
||||
result = dict(protocol='PM35', controller=address, function=function, raw=raw)
|
||||
if function in (0x83, 0x86):
|
||||
if len(raw) != 5 or role == 'request':
|
||||
raise ValueError('Invalid exception response')
|
||||
result['summary'] = 'PM35 node=%d exception fn=%02X code=%d' % (address, function, raw[2])
|
||||
elif function == 6 or function == 3 and role == 'request':
|
||||
if len(raw) != 8:
|
||||
raise ValueError('PM35 request/06 must contain 8 bytes')
|
||||
register, value = number(raw[2:4], 'big'), number(raw[4:6], 'big')
|
||||
if register >= 128 or function == 3 and not 1 <= value <= 128 - register:
|
||||
raise ValueError('PM35 register range outside 0..127')
|
||||
result.update(register=register, value=value)
|
||||
result['summary'] = 'PM35 node=%d %s R%d %s=%d' % (address,
|
||||
'READ' if function == 3 else 'WRITE/echo', register,
|
||||
'count' if function == 3 else 'value', value)
|
||||
if function == 6 and register == 127:
|
||||
result['summary'] += ' command_bits=%04X' % value
|
||||
elif function == 3:
|
||||
count = raw[2] or 256
|
||||
if count % 2 or len(raw) != count + 5:
|
||||
raise ValueError('Invalid PM35 response byte count')
|
||||
result['values'] = [number(raw[i:i+2], 'big') for i in range(3, len(raw)-2, 2)]
|
||||
result['summary'] = 'PM35 node=%d READ response %d words: %s' % (address,
|
||||
len(result['values']), ' '.join('%04X' % v for v in result['values']))
|
||||
else:
|
||||
raise ValueError('Unsupported PM35 function')
|
||||
return result
|
||||
|
||||
|
||||
class PM35Parser:
|
||||
def __init__(self, role):
|
||||
self.role = role
|
||||
self.buffer = []
|
||||
|
||||
def flush(self):
|
||||
if not self.buffer:
|
||||
return []
|
||||
ss, es = self.buffer[0][1], self.buffer[-1][2]
|
||||
self.buffer = []
|
||||
return [(ss, es, None, 'Incomplete PM35 frame')]
|
||||
|
||||
def feed(self, value, ss, es):
|
||||
self.buffer.append((value, ss, es))
|
||||
events = []
|
||||
while len(self.buffer) >= 3:
|
||||
raw = bytes(item[0] for item in self.buffer)
|
||||
fn = raw[1]
|
||||
if fn not in (3, 6, 0x83, 0x86):
|
||||
del self.buffer[0]
|
||||
continue
|
||||
role = self.role
|
||||
total = 5 if fn & 128 else 8
|
||||
if role == 'auto' and fn == 3:
|
||||
candidates = [(8, 'request'), ((raw[2] or 256) + 5, 'response')]
|
||||
found = None
|
||||
for length, candidate_role in candidates:
|
||||
if len(raw) >= length:
|
||||
try:
|
||||
found = (length, candidate_role, parse_pm35(raw[:length], candidate_role))
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if found:
|
||||
length, candidate_role, result = found
|
||||
events.append((self.buffer[0][1], self.buffer[length-1][2], result, None))
|
||||
del self.buffer[:length]
|
||||
continue
|
||||
if len(raw) < max(length for length, _ in candidates):
|
||||
break
|
||||
events.append((self.buffer[0][1], self.buffer[-1][2], None, 'Invalid PM35 frame/CRC'))
|
||||
del self.buffer[0]
|
||||
continue
|
||||
if fn == 3 and role == 'response':
|
||||
total = (raw[2] or 256) + 5
|
||||
if len(raw) < total:
|
||||
break
|
||||
result, error = None, None
|
||||
try:
|
||||
result = parse_pm35(raw[:total], role)
|
||||
except ValueError as exc:
|
||||
error = str(exc)
|
||||
events.append((self.buffer[0][1], self.buffer[total-1][2], result, error))
|
||||
del self.buffer[:1 if error else total]
|
||||
return events
|
||||
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
|
||||
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