"""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: ... # ---------------------------------------------------------------------------