Saqr/core/events.py

96 lines
3.5 KiB
Python

"""Event-line emission and CSV writers.
The ``emit_event`` output line is a contract with ``robot.bridge`` — its
regex parses this exact format. Don't change the field order without updating
the bridge.
"""
from __future__ import annotations
import csv
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from core.compliance import split_wearing_missing
from core.detection import CLASS_ORDER
def now_iso() -> str:
return datetime.now().isoformat(timespec="seconds")
class EventLogger:
FIELDS = ["timestamp", "track_id", "event_type", "status",
"wearing", "missing", "unknown", "photo", "path", "snapshot"]
def __init__(self, path: Path):
self.path = path
self.path.parent.mkdir(parents=True, exist_ok=True)
if not self.path.exists():
with open(self.path, "w", newline="", encoding="utf-8") as f:
csv.DictWriter(f, fieldnames=self.FIELDS).writeheader()
def append(self, row: Dict[str, str]) -> None:
with open(self.path, "a", newline="", encoding="utf-8") as f:
csv.DictWriter(f, fieldnames=self.FIELDS).writerow(row)
def write_result_csv(tracks: List, output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
fields = ["photo", "track_id", "status", "last_seen",
"wearing", "missing", "unknown", *CLASS_ORDER, "path"]
rows = []
for track in sorted(tracks, key=lambda t: t.track_id):
wearing, missing, unknown = split_wearing_missing(track.items)
row = {
"photo": track.photo_path.name if track.photo_path else "",
"track_id": track.track_id,
"status": track.status,
"last_seen": track.last_seen_iso,
"wearing": ", ".join(wearing),
"missing": ", ".join(missing),
"unknown": ", ".join(unknown),
"path": str(track.photo_path) if track.photo_path else "",
}
for cls in CLASS_ORDER:
row[cls] = 1 if track.items.get(cls, 0.0) > 0 else 0
rows.append(row)
with open(output, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
w.writerows(rows)
def emit_event(track, event_logger: EventLogger, event_type: str = "STATUS_CHANGE",
force: bool = False, snapshot_path: Path = None) -> None:
if track.photo_path is None:
return
if not force and track.announced_status == track.status:
return
wearing, missing, unknown = split_wearing_missing(track.items)
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
msg = (
f"[{ts}] ID {track.track_id:04d} | {event_type} | {track.status} | "
f"wearing: {', '.join(wearing) or 'none'} | "
f"missing: {', '.join(missing) or 'none'} | "
f"unknown: {', '.join(unknown) or 'none'}"
)
print(msg, flush=True)
event_logger.append({
"timestamp": now_iso(),
"track_id": str(track.track_id),
"event_type": event_type,
"status": track.status,
"wearing": ", ".join(wearing),
"missing": ", ".join(missing),
"unknown": ", ".join(unknown),
"photo": track.photo_path.name if track.photo_path else "",
"path": str(track.photo_path) if track.photo_path else "",
"snapshot": str(snapshot_path) if snapshot_path else "",
})
track.announced_status = track.status
track.event_count += 1