96 lines
3.5 KiB
Python
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
|