Files
templates/python/set_devices/can_monitor.py

109 lines
3.8 KiB
Python

"""Shared CAN protocol selection and disk-backed session buffer (no Qt)."""
import csv
import tempfile
from collections import Counter, deque
from datetime import datetime
from pathlib import Path
from protocan import protocan as pc
from .can_bridge import decode_frame
PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"),
("devboard", "DevBoard_V1 / SETCAN"))
def decode_protocol(frame, protocol="auto"):
if protocol == "devboard":
return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
return decode_frame(frame)
def protocol_values(identifier, extended):
if not extended:
return ("",) * 7
value = pc.ProtoCanId.parse(identifier)
return (str(value.priority), str(value.pm), str(value.device_type),
str(value.device), value.device_name,
"0x%X — %s" % (value.msg_type, value.msg_type_name),
"0x%04X" % value.body)
def parse_filter(text):
if not text.strip():
return None
try:
ids = {int(part.strip(), 16) for part in text.replace(";", ",").split(",")}
if any(value < 0 or value > 0x1FFFFFFF for value in ids):
raise ValueError()
return ids
except ValueError:
raise ValueError("Введите CAN ID в hex, например 701 или 0x701, 201") from None
class CanSession:
"""All frames spool to disk; only the latest 5000 stay in display memory.
Starting recording includes the current session buffer. Reset clears the
buffer/counters but never removes frames already written to a recording.
"""
HEADER = ("Время", "Канал", "Тип", "ID", "RTR", "DLC", "Данные")
def __init__(self):
self.recent = deque(maxlen=5000)
self.counts = Counter()
self.output = None
self.path = None
self.buffer = tempfile.TemporaryFile(mode="w+", encoding="utf-8", newline="")
def append(self, identifier, data, extended, rtr=False, source="CAN", timestamp=None):
row = ((datetime.fromtimestamp(timestamp) if timestamp is not None else datetime.now()).isoformat(timespec="milliseconds"),
source, "EXT" if extended else "STD", ("%08X" if extended else "%03X") % identifier,
int(rtr), len(data), bytes(data).hex(" ").upper())
csv.writer(self.buffer, delimiter=";").writerow(row)
self.recent.append((identifier, row))
self.counts[identifier] += 1
if self.output is not None:
csv.writer(self.output, delimiter=";").writerow(row)
def start(self, directory):
directory = Path(directory) / datetime.now().strftime("session_%Y%m%d_%H%M%S_%f")
directory.mkdir(parents=True, exist_ok=False)
path = directory / "frames.csv"
output = path.open("w", encoding="utf-8-sig", newline="")
try:
csv.writer(output, delimiter=";").writerow(self.HEADER)
self.buffer.seek(0)
while chunk := self.buffer.read(65536):
output.write(chunk)
output.flush()
except Exception:
output.close()
raise
finally:
self.buffer.seek(0, 2)
self.output, self.path = output, path
return path
def flush(self):
if self.output is not None:
self.output.flush()
def stop(self):
if self.output is not None:
self.output.close()
self.output = None
def reset(self):
self.buffer.seek(0)
self.buffer.truncate()
self.recent.clear()
self.counts.clear()
def close(self):
self.stop()
self.buffer.close()
def format_data(data: bytes, decimal: bool = False) -> str:
"""Format individual payload bytes without changing their order or width."""
return " ".join(str(byte) if decimal else "%02X" % byte for byte in data)