Files
templates/python/logic_analyzer/analysis.py

359 lines
16 KiB
Python

"""Offline analysis of timestamped edges; no resampling or hardware access."""
from __future__ import annotations
from bisect import bisect_left, bisect_right
from dataclasses import dataclass
import heapq
import math
from .files import ImportCancelled
from .decoders.gate_timing import TimingChecker
from .decoders.set_uart import StreamParser
from .decoders.pm35_uart import PM35Parser
from .decoders.set_can import legacy, Reassembler
@dataclass
class AnalysisEvent:
start: float
end: float
kind: str
text: str
details: str = ''
@dataclass
class AnalysisResult:
events: list
truncated: bool
start: float
end: float
def pulse_measurements(channel, time, start, end):
"""Measure a complete adjacent high/low cycle, without scanning the record."""
edges = channel.edges
index = bisect_right(edges, time)
result = {'level': channel.initial ^ (index & 1)}
if 0 < index < len(edges):
left, right = edges[index - 1], edges[index]
if start <= left < right <= end:
result['width'] = right - left
result['high' if result['level'] else 'low'] = right - left
# Prefer the next complete interval, otherwise the preceding one.
other = None
if index + 1 < len(edges) and edges[index + 1] <= end:
other = edges[index + 1] - right
elif index >= 2 and edges[index - 2] >= start:
other = left - edges[index - 2]
if other is not None:
result['low' if result['level'] else 'high'] = other
result['period'] = result['high'] + result['low']
result['frequency'] = 1 / result['period']
result['duty'] = 100 * result['high'] / result['period']
return result
def interval_edges(channel, a, b):
"""Count rising/falling edges in (min(a,b), max(a,b)]."""
left, right = sorted((a, b))
lo, hi = bisect_right(channel.edges, left), bisect_right(channel.edges, right)
count = hi - lo
first_rising = channel.initial ^ (lo & 1) == 0
rising = count // 2 + int(bool(count & 1) and first_rising)
return rising, count - rising
def _cancel(cancel):
if cancel():
raise ImportCancelled()
def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=False, cancel=lambda: False):
"""Yield (start, end, byte, error), sampling 8-bit UART at bit centres."""
bit = 1.0 / baud
bits = 9 + (parity != 'none') + stops
edges = channel.edges
index = bisect_left(edges, start)
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
if level:
index += 1
continue
finish = time + bits * bit
if finish > end + bit * 1e-6:
yield time, end, None, 'Неполный UART-байт в конце диапазона'
break
read = lambda pos: channel.level_at(time + pos * bit) ^ inverted
if read(0.5):
index += 1 # A glitch shorter than half a start bit.
continue
values = [read(1.5 + i) for i in range(8)]
value = sum(v << i for i, v in enumerate(values))
error = None
if parity != 'none' and read(9.5) != ((sum(values) + (parity == 'odd')) & 1):
error = 'Ошибка чётности UART'
stop_start = 9 + (parity != 'none')
if any(read(stop_start + i + 0.5) != 1 for i in range(stops)):
error = 'Ошибка стопового бита UART / BREAK'
yield time, finish, value, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def _number(bits):
value = 0
for bit in bits:
value = (value << 1) | bit
return value
def can_crc(bits):
crc = 0
for bit in bits:
feedback = ((crc >> 14) & 1) ^ bit
crc = (crc << 1) & 0x7fff
if feedback:
crc ^= 0x4599
return crc
class _CanBits:
def __init__(self, channel, time, end, bitrate, inverted, sample_point):
self.channel, self.time, self.end = channel, time, end
self.bit, self.inverted = 1 / bitrate, inverted
self.sample_point = sample_point
self.position, self.previous, self.run = 0, None, 0
self.bits = []
self.last_sample = time
def raw(self):
# Resynchronise on recessive->dominant edges near the bit boundary.
boundary = self.time + self.position * self.bit
if self.position:
lo = bisect_right(self.channel.edges, max(self.last_sample, boundary - self.bit * .2))
hi = bisect_right(self.channel.edges, boundary + self.bit * .2, lo)
for index in range(lo, hi):
level = self.channel.initial ^ ((index + 1) & 1) ^ self.inverted
if level == 0:
self.time += self.channel.edges[index] - boundary
boundary = self.channel.edges[index]
break
sample = boundary + self.sample_point * self.bit
if sample >= self.end:
raise EOFError('Неполный CAN-кадр в конце диапазона')
self.last_sample = sample
self.position += 1
return self.channel.level_at(sample) ^ self.inverted
def stuffed(self):
if self.run == 5:
value = self.raw()
if value == self.previous:
raise ValueError('CAN: ошибка bit stuffing / error frame')
self.previous, self.run = value, 1
value = self.raw()
self.run = self.run + 1 if value == self.previous else 1
self.previous = value
self.bits.append(value)
return value
def take(self, count):
return _number([self.stuffed() for _ in range(count)])
def can_frames(channel, start, end, bitrate, inverted=False, sample_point=.7, cancel=lambda: False):
"""Classic CAN, strict stuffing/CRC/form validation before application decode."""
edges = channel.edges
index = bisect_left(edges, start)
bit = 1 / bitrate
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
previous = edges[index - 1] if index else start
# SOF follows at least three recessive intermission bits.
if level or time - previous < bit * 2.9:
index += 1
continue
reader = _CanBits(channel, time, end, bitrate, inverted, sample_point)
frame, error = None, None
try:
if reader.take(1):
raise ValueError('CAN: неверный SOF')
ident = reader.take(11)
rtr, extended = reader.take(1), reader.take(1)
if extended:
if rtr != 1:
raise ValueError('CAN: неверный SRR')
ident = (ident << 18) | reader.take(18)
rtr = reader.take(1)
if reader.take(2):
raise ValueError('CAN FD / reserved bits не поддерживаются')
elif reader.take(1):
raise ValueError('CAN FD / reserved bit не поддерживается')
dlc = reader.take(4)
if dlc > 8:
raise ValueError('CAN: поддерживается classic DLC 0…8')
data = bytes(reader.take(8) for _ in range(0 if rtr else dlc))
expected = can_crc(reader.bits)
received = reader.take(15)
if reader.run == 5:
if reader.raw() == reader.previous:
raise ValueError('CAN: неверный последний stuff bit')
if reader.raw() != 1:
raise ValueError('CAN: неверный CRC delimiter')
ack = reader.raw() == 0
if reader.raw() != 1 or any(reader.raw() != 1 for _ in range(7)):
raise ValueError('CAN: неверный ACK delimiter / EOF')
if received != expected:
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
frame = dict(ident=ident, extended=bool(extended), remote=bool(rtr),
data=data, dlc=dlc, ack=ack)
except (ValueError, EOFError) as exc:
error = str(exc)
finish = min(end, reader.time + reader.position * bit)
yield time, finish, frame, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def analyze_capture(capture, options, start=None, end=None, progress=lambda n: None, cancel=lambda: False):
start = capture.start if start is None else max(capture.start, start)
end = capture.end if end is None else min(capture.end, end)
if end <= start:
raise ValueError('Выберите непустой интервал анализа.')
mode = options['mode']
channel = capture.channels[options.get('channel', 0)]
events = []
last_progress = -1
limit = options.get('max_events', 50000)
if limit <= 0:
raise ValueError('Лимит результатов должен быть положительным.')
def emit(a, b, kind, text, details=''):
if len(events) >= limit:
raise OverflowError()
events.append(AnalysisEvent(a, b, kind, text, details))
def report(time):
nonlocal last_progress
_cancel(cancel)
value = min(99, int((time - start) / (end - start) * 100))
if value != last_progress:
progress(value)
last_progress = value
def parsed(items):
for a, b, frame, error in items:
emit(a, b, 'error' if error else 'frame', error or frame['summary'],
'' if not frame else bytes(frame.get('raw', frame.get('payload', b''))).hex(' '))
if frame and frame.get('protocol') == 'ProtoCAN bridge' and frame['flags'] & 1 and not frame['flags'] & 10:
try:
application = legacy(frame['can_id'], frame['payload'])
emit(a, b, 'frame', application['summary'])
except ValueError as exc:
emit(a, b, 'error', str(exc))
truncated = False
try:
if mode in ('1SP0635', '1SD536F2'):
other = options.get('status_channel', 1)
if other == options.get('channel', 0):
raise ValueError('Vin и Vstat должны быть разными каналами.')
status = capture.channels[other]
checker = TimingChecker(1e9, mode, options.get('tolerance_ns', 100),
not options.get('vin_low', False), not options.get('status_low', False))
# The pure checker works in integer ticks. Offset before rounding
# to preserve ns precision for CSV timestamps far from zero.
tick = lambda t: round((t - start) * 1e9)
def timing(items):
for item in items:
text = item['text']
if item['kind'] == 'fault':
text += ' · аварийная обратная связь; превышение тока не подтверждено'
emit(start + item['start'] / 1e9, start + item['end'] / 1e9, item['kind'], text)
def transitions(ch, which):
first = bisect_right(ch.edges, start)
for i in range(first, bisect_right(ch.edges, end)):
yield ch.edges[i], which, ch.initial ^ ((i + 1) & 1)
if bool(status.level_at(start)) == checker.vstat_active_high:
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
for time, which, level in heapq.merge(transitions(channel, 0), transitions(status, 1)):
report(time)
timing(checker.expire(tick(time)))
timing(checker.on_control_edge(tick(time), level) if which == 0 else checker.on_status_edge(tick(time), level))
timing(checker.expire(tick(end)))
if checker.status_start is not None:
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
for pending in checker.pending:
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin: диапазон закончился до тайм-аута ACK')
elif mode in ('UART', 'SET UART', 'PM35 UART'):
baud = options.get('baudrate', 115200)
parity, stops = options.get('parity', 'none'), options.get('stops', 1)
if not math.isfinite(baud) or baud <= 0 or parity not in ('none', 'even', 'odd') or stops not in (1, 2):
raise ValueError('Некорректные настройки UART.')
parser = (StreamParser(options.get('protocol', 'auto')) if mode == 'SET UART' else
PM35Parser(options.get('role', 'response')) if mode == 'PM35 UART' else None)
gap = (3.5 * (9 + (parity != 'none') + stops) / baud if mode == 'PM35 UART'
else options.get('gap_ms', 100) / 1000)
last = None
for a, b, value, error in uart_frames(channel, start, end, baud, parity, stops, options.get('inverted', False), cancel):
report(a)
if parser and last is not None and gap > 0 and a - last >= gap:
parsed(parser.flush())
last = b
if error:
if parser:
parsed(parser.flush())
emit(a, b, 'error', error)
elif parser:
parsed(parser.feed(value, a, b))
else:
emit(a, b, 'byte', 'UART 0x%02X' % value, chr(value) if 32 <= value < 127 else '')
if parser:
parsed(parser.flush())
elif mode in ('CAN', 'SET CAN'):
bitrate = options.get('baudrate', 1000000)
sample_point = options.get('sample_point', 70) / 100
if not math.isfinite(bitrate) or bitrate <= 0 or not .1 <= sample_point <= .95:
raise ValueError('Некорректный битрейт / точка выборки CAN.')
reassembler = Reassembler()
for a, b, frame, error in can_frames(channel, start, end, bitrate, options.get('inverted', False), sample_point, cancel):
report(a)
if error:
emit(a, b, 'error', error)
# Never bridge an invalid/missing physical frame.
for pending in reassembler.pending.values():
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
reassembler.pending.clear()
continue
ident, data = frame['ident'], frame['data']
emit(a, b, 'can', 'CAN %s ID=%08X DLC=%d %s %s' % (
'EXT' if frame['extended'] else 'STD', ident, frame['dlc'],
'RTR' if frame['remote'] else 'DATA', 'ACK' if frame['ack'] else 'NACK'), data.hex(' '))
if mode == 'SET CAN' and frame['extended'] and not frame['remote']:
protocol = options.get('protocol', 'protocan')
if protocol == 'set-v2':
parsed(reassembler.feed(ident, data, a, b, b * 1000))
else:
try:
result = legacy(ident, data, protocol)
if result:
emit(a, b, 'frame', result['summary'], data.hex(' '))
except ValueError as exc:
emit(a, b, 'error', str(exc))
for pending in reassembler.pending.values():
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
else:
raise ValueError('Неизвестный анализатор: ' + mode)
except OverflowError:
truncated = True
_cancel(cancel)
progress(100)
events.sort(key=lambda event: (event.start, event.end))
return AnalysisResult(events, truncated, start, end)