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