# Импорт журнала CAN485 в ограниченные по объёму кривые. Каждый канал сохраняет собственные # временные метки: пропуск сообщения на одном канале не должен искусственно сдвигать другой. """Convert ESP CAN485 logs into bounded, independently timestamped curves.""" from __future__ import annotations from collections import deque from dataclasses import dataclass from protocan.legacycan import PROJECTS, LegacyCanFormat, decode from .can485_board import BoardFrame, SdLogImport, parse_sd_log @dataclass class CanLogChannel: name: str address: int points: deque decoded: bool = False def _values(frame: BoardFrame): if frame.rtr: return prefix = f"{'EXT' if frame.extended else 'STD'} {frame.identifier:08X}" if not frame.extended: from .deif_xdi import decode as decode_deif from .can_transport import build_frame packet = decode_deif(build_frame(frame.identifier, frame.data, ide=False)) fields = dict(packet.fields) if "RAW INT16 LE" in fields: # Invalid measurements must not become numeric spikes or raw-word curves. if "Статус" in fields or packet.warnings: return address = int(fields["Индекс объекта"], 16) subindex = int(fields["Подиндекс"], 16) label = f"{frame.source} · {prefix} · {packet.device_label} · 0x{address:04X}:{subindex:02X} · {fields['Параметр']}" raw = int(fields["RAW INT16 LE"]) yield (label + " · RAW", address, raw, True) if "Значение" in fields: number, unit = fields["Значение"].split(" ", 1) yield (label + " · " + unit, address, float(number), True) return if frame.extended: for project in PROJECTS: if project.format is not LegacyCanFormat.ROTATING_THREE_WORDS: continue node = project.node_for(frame.identifier) if node is None: continue packet = decode(project.format, node, frame.identifier, frame.data) if packet is not None: for address, value in packet.present_values: # Keep both CAN directions and nodes in separate curves. yield (f"{prefix} · {node.name} · R{address:04X}", address, value, False) return # Unknown protocols stay explicitly raw; never guess a register layout. for offset in range(0, len(frame.data), 2): part = frame.data[offset:offset + 2] label = f"BE16 [{offset}:{offset + 1}]" if len(part) == 2 else f"байт [{offset}]" yield (f"{prefix} · {label}", offset, int.from_bytes(part, "big"), False) def read_can_log(lines, *, reference_timestamp: float, capacity: int, max_total_points: int = 1_000_000, frame_limit: int = 100_000) -> tuple[list[CanLogChannel], SdLogImport]: imported = parse_sd_log(lines, reference_timestamp, limit=frame_limit) channels = {} for frame in imported.frames: for name, address, _value, decoded in _values(frame): channels.setdefault(name, (address, decoded)) if len(channels) > 4096: raise ValueError("CAN CSV: слишком много каналов (максимум 4096)") if not channels: raise ValueError("CAN CSV: нет данных для графиков") capacity = max(1, min(capacity, max_total_points // len(channels))) result = {name: CanLogChannel(name, address, deque(maxlen=capacity), decoded) for name, (address, decoded) in channels.items()} for frame in imported.frames: for name, _address, value, _decoded in _values(frame): result[name].points.append((frame.timestamp * 1000, value)) return list(result.values()), imported