"""The Qt-free contract the web layer depends on. The web layer (``app``/``routes``/``streaming``) is deliberately free of any Qt or hardware knowledge: it talks only to a :class:`WebController`. The embedded bridge in ``gui/controllers/app_window_web_mixin.py`` implements this protocol by forwarding control actions to the AppWindow's *existing* buttons and exposing read-only snapshots — so the very same desktop logic backs the browser, with no duplicated control flow. """ from __future__ import annotations from typing import Protocol, runtime_checkable class WebActionError(Exception): """A web-triggered desktop action failed, carrying the operator-facing reason. Control methods run the matching desktop action *synchronously* and raise this when that action reports an error (the same message the desktop would show), so the HTTP layer can return it instead of a misleading "ok". Distinct from a plain ``ValueError`` (rejected by web-side validation before the action even runs). """ @runtime_checkable class WebController(Protocol): """Control + read surface the web layer needs; implemented by the Qt bridge. Control methods are *synchronous*: each runs the corresponding desktop action on the GUI thread and only returns once it has completed, raising :class:`WebActionError` if the action surfaced an error. This is what lets the browser show real failures (e.g. a save into an existing directory) instead of a blind success. """ def start(self) -> None: """Start a continuous run (the desktop "Start" button).""" def single_capture(self) -> None: """Run a single-capture acquisition (the desktop "Single Capture" button).""" def stop(self) -> None: """Stop the running pipeline (the desktop "Stop" button).""" def start_recording(self, path: str, name: str, count: int) -> None: """Start a run (if stopped) and record the next ``count`` measurements to disk. ``path``/``name`` mirror the shared save destination fields (blank ``path`` keeps the configured one); ``count`` sets how many measurements are written. Raises :class:`WebActionError` if the destination already exists. """ def capture_tmp_reference(self) -> None: """Capture and select a temporary reference (the desktop button).""" def remove_last_measurement(self) -> None: """Remove the most recent measurement (the desktop button).""" def apply_live_settings(self, fields: dict) -> list: """Apply live processor settings; returns the current settings schema.""" def load_config(self, name: str) -> None: """Load a run-config profile by file name (the desktop "Load Config" button).""" def save_dataset(self, path: str, name: str) -> None: """Save the runtime dataset to ``path``/``name`` (the desktop "Save Dataset" button).""" def current_live_settings(self) -> list: """Return the live-settings schema (built from the Qt widgets).""" def list_configs(self) -> list[str]: """Return the run-config file names available to load.""" def status(self) -> dict: """Return a snapshot of pipeline/run state.""" def peek_frame(self) -> dict | None: """Return the latest rendered-plot frame (PNG of the Qt plot), or ``None``."""