Files
templates/python/set_devices/can_monitor.py

143 lines
5.3 KiB
Python

"""Shared CAN protocol selection and disk-backed session buffer (no Qt)."""
import csv
import os
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"),
("deif", "DEIF XDi-192 / CANopen"))
def decode_protocol(frame, protocol="auto"):
if protocol == "deif":
from .deif_xdi import decode
return decode(frame)
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; retain recent frames and unique-frame totals.
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.unique_frames = {}
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
# Timestamp is not part of a frame's identity. Keep the latest arrival
# and a session-wide count, including repeats outside the recent buffer.
key = row[1:]
previous = self.unique_frames.get(key)
self.unique_frames[key] = (row, previous[1] + 1 if previous else 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 save(self, path):
"""Save a full buffer snapshot without stopping an active recording."""
path = Path(path)
if self.output is not None and (path.resolve() == self.path.resolve()
or (path.exists() and path.samefile(self.path))):
raise ValueError("Выберите другой файл: в этот CSV сейчас идёт запись.")
temporary = None
try:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8-sig",
newline="", dir=path.parent, delete=False) as output:
temporary = Path(output.name)
csv.writer(output, delimiter=";").writerow(self.HEADER)
self.buffer.seek(0)
while chunk := self.buffer.read(65536):
output.write(chunk)
os.replace(temporary, path)
finally:
self.buffer.seek(0, 2)
if temporary is not None and temporary.exists():
temporary.unlink()
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()
self.unique_frames.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)