162 lines
6.5 KiB
Python
162 lines
6.5 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"),
|
||
("protocan", "ProtoCAN / GAS"),
|
||
("ump_balsam", "УМП / Balsam 167 (legacy CAN)"),
|
||
("ump1_gas167", "УМП 1 / ProtoCAN — карта GAS 167/v1"),
|
||
("ump2_gas167", "УМП 2 / ProtoCAN — карта GAS 167/v1"),
|
||
("ump_gas", "УМП / ProtoCAN GAS — новые ID 5000/6000"))
|
||
|
||
|
||
def decode_protocol(frame, protocol="auto"):
|
||
if protocol == "deif":
|
||
from .deif_xdi import decode
|
||
return decode(frame)
|
||
if protocol == "ump_balsam":
|
||
from .balsam_can import decode
|
||
result = decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
|
||
if result is not None:
|
||
return result
|
||
result = pc.Decoded(pc.ProtoCanId.parse(frame.can_id), len(frame.data),
|
||
bytes(frame.data), ide=frame.ide, rtr=frame.rtr,
|
||
protocol="BALZAM 167", message_label="Не Balsam 167")
|
||
result.summary = "УМП / Balsam 167: CAN ID или формат кадра не соответствует протоколу"
|
||
result.warnings.append(result.summary)
|
||
return result
|
||
if protocol in ("ump1_gas167", "ump2_gas167"):
|
||
from .ump_can import decode_gas167
|
||
return decode_gas167(frame, node=1 if protocol == "ump1_gas167" else 2)
|
||
if protocol in ("devboard", "protocan", "ump_gas"):
|
||
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)
|