Add alignment state indicator and pluggable data sources
- AlignmentThresholds dataclass + VisionEngine.__init__ for injection - alignment_state computed per snapshot (ready/aligning/fault) - MonitorPlot circle/vector/legend driven by state color - Status dot + label moved into POSITION VISUALIZATION panel header - CsvSessionSource + DemoSource pluggable DataSource layer - Synthetic DATA/ready/ and DATA/aligning/ sessions for visual testing - Fix CAM 3 label placement (below circle, not above)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Pluggable data sources for the wafer monitor.
|
||||
|
||||
A DataSource produces a MonitorSnapshot on demand. Today: CSV session + synthetic
|
||||
demo. Future: live MQTT from NanoBerry. App.py only talks to this interface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from .Calibration import BaselineCalibration
|
||||
from .CsvSource import find_session_files, load_camera_records_from_csv
|
||||
from .VisionEngine import MonitorSnapshot, VisionEngine
|
||||
|
||||
|
||||
class DataSource(Protocol):
|
||||
def latest_snapshot(self, baseline: BaselineCalibration) -> MonitorSnapshot: ...
|
||||
|
||||
def description(self) -> str: ...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P2.2 — CsvSessionSource for replaying a selected session.
|
||||
#
|
||||
# Resolves sibling files once from the seed path using P1.1, then re-reads
|
||||
# those files on every latest_snapshot() call. If discovery returns no files,
|
||||
# raises FileNotFoundError so a bad operator selection is visible immediately.
|
||||
# ---------------------------------------------------------------------------
|
||||
class CsvSessionSource:
|
||||
"""Reads a captured session from a directory of CSV files.
|
||||
|
||||
The operator selects the session folder (e.g. ``sessions/0540/``); this
|
||||
source discovers all cameras inside it automatically. Re-reads files on
|
||||
every ``latest_snapshot()`` call so "Reload Current Session" always reflects
|
||||
the latest on-disk state without cache invalidation.
|
||||
"""
|
||||
|
||||
def __init__(self, folder: Path, engine: VisionEngine) -> None:
|
||||
self.folder = Path(folder)
|
||||
self.engine = engine
|
||||
# Discover once at construction so missing-folder errors are immediate.
|
||||
paths = find_session_files(self.folder)
|
||||
if not paths:
|
||||
raise FileNotFoundError(f"No NanoBerry CSV files found in {self.folder}")
|
||||
self._cam_count = len(paths)
|
||||
|
||||
def latest_snapshot(self, baseline: BaselineCalibration) -> MonitorSnapshot:
|
||||
# Re-resolve on every call — supports "Reload Current Session".
|
||||
paths = find_session_files(self.folder)
|
||||
if not paths:
|
||||
raise FileNotFoundError(f"No NanoBerry CSV files found in {self.folder}")
|
||||
self._cam_count = len(paths)
|
||||
records = load_camera_records_from_csv(paths, baseline)
|
||||
return self.engine.snapshot_from_records(
|
||||
records,
|
||||
baseline,
|
||||
source_label=self.description(),
|
||||
)
|
||||
|
||||
def description(self) -> str:
|
||||
return f"Session: {self.folder.name} ({self._cam_count} cams)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P2.3 — DemoSource as the default safe source.
|
||||
#
|
||||
# First launch should not depend on MASTER_SD being present or on the working
|
||||
# directory. Wrapping VisionEngine.demo_snapshot() behind the same protocol
|
||||
# means demo mode, CSV replay, and future MQTT all flow through P3.1 with no
|
||||
# special-case read logic. The description is stable because P3.3 displays it
|
||||
# directly in the status bar.
|
||||
# ---------------------------------------------------------------------------
|
||||
class DemoSource:
|
||||
"""Synthetic perfect-world data — always works, used as fallback."""
|
||||
|
||||
def __init__(self, engine: VisionEngine) -> None:
|
||||
self.engine = engine
|
||||
|
||||
def latest_snapshot(self, baseline: BaselineCalibration) -> MonitorSnapshot:
|
||||
return self.engine.demo_snapshot(baseline)
|
||||
|
||||
def description(self) -> str:
|
||||
return "Demo: Simulated Data"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TODO P6.1: Post-scope hook for MqttSource implementing DataSource.
|
||||
#
|
||||
# THINKING:
|
||||
# Live MQTT should enter the app as another DataSource, not by changing
|
||||
# VisionEngine or App.read_wafer(). The source can own broker connection,
|
||||
# background subscription, and latest-frame buffers, then convert payloads to
|
||||
# CameraEdgeRecord before calling snapshot_from_records(). Keeping this as a
|
||||
# P6 task protects the beta session-loader milestone from broker/threading
|
||||
# risk while documenting the intended extension point.
|
||||
# Cross-reference: P6.2 adds the future App.py action that constructs it.
|
||||
#
|
||||
# Future signature:
|
||||
# class MqttSource:
|
||||
# def __init__(self, broker: str, engine: VisionEngine) -> None: ...
|
||||
# def latest_snapshot(self, baseline: BaselineCalibration) -> MonitorSnapshot: ...
|
||||
# ---------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user