diff --git a/.gitignore b/.gitignore index 68f5381..224585b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ **/.vscode/* !**/.vscode/settings.json !**/.vscode/extensions.json +# Personal Claude Code settings and notes +.claude/ # ---------- Env ---------- # The root .env (ports for the whole stack) plus frontend/.env and frontend/.env.docker @@ -104,3 +106,12 @@ frontend/.auth/ # ---------- Docs ---------- docs/_build/ /site + +# Backups written by newswitch.config_io when saving the device config +backend/configs/*.bak + +# Local hardware settings and generated browser-test reports +backend/configs/*.local.json +backend/configs/*.persistent.yaml +frontend/playwright-report/ +frontend/test-results/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b1465e8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python", + "python.analysis.typeCheckingMode": "standard", + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.cwd": "${workspaceFolder}/backend", + "python.testing.pytestArgs": ["tests"], + "files.watcherExclude": { + "**/.venv/**": true + } +} diff --git a/README.md b/README.md index 20c403a..023cd27 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,8 @@ VITE_SCHEMA_STATES_URL=http://my-lab-box:${BACKEND_PORT:-8099}/schemas/states VITE_SCHEMA_LOCKS_URL=http://my-lab-box:${BACKEND_PORT:-8099}/schemas/locks ``` -Apart from `BACKEND_HOST` / `BACKEND_PORT`, the backend reads no `.env` — the rest is configured in code -via `ImswitchConfig` (`backend/newswitch/app.py`). +Apart from `BACKEND_HOST` / `BACKEND_PORT`, the backend reads no `.env` — the rest is configured in +`backend/base_config.yaml`, read by `ImswitchConfig` (`backend/newswitch/app.py`). ## Releases diff --git a/SRS/CHANGELOG_KC.md b/SRS/CHANGELOG_KC.md deleted file mode 100644 index 21b4552..0000000 --- a/SRS/CHANGELOG_KC.md +++ /dev/null @@ -1,91 +0,0 @@ -# Changelog from C.Karras - -## Version: 1.2 29.07.26 — DEC-001: live-video transport decided - -Documentation only. **No source code was changed.** - -### Decision -- **`DEC-001` — the live-video transport is LiveKit / WebRTC / VP8 with an SFU**, per - `basicRequirements.md` §Image handling. The architecture-freeze rule `CON-ARC-002` is - **exceptionally waived for this subsystem only**; all other subsystems remain frozen. -- Closes `OP-001` (was the highest-severity CONFLICT in the register). - -### Changed — `DOC/SRS_V1.md` -- `IF-CM-005` rewritten from ***UNCLEAR*** to a binding requirement; new `IF-CM-007` … `IF-CM-012` - specify the I420 conversion, the `capture_frame()` publish path, the SFU deployment, the - `livekit-client` subscriber, token/room configurability, and independence of the video plane from - the control plane. -- New `CON-ARC-002-E1` records the freeze exemption explicitly. -- New `NFR-PER-009` (VP8 encode CPU budget); `NFR-PER-003` notes that the latency target now sets the - encoder operating point. -- System-topology diagram updated: the video plane goes through an SFU, not a direct socket. -- New `ASM-07` (WebRTC needs UDP/ICE reachability), `ASM-08` (software VP8 fits the Pi 5 budget — - unproven), `DEP-08` confirmed, `DEP-09` (libvpx) added. -- **New Appendix D — Decision log.** All future decisions are recorded there. - -### Changed — `DOC/OpenPoints.md` -- `OP-001` marked ✅ CLOSED with a full Decision block; the original analysis is retained per the - register's own rule. -- `OP-004` (user identity) **raised in priority** — LiveKit rooms are token-authenticated, so video - now depends on identity. -- `OP-015` (latency target) **raised in priority** — the transport is fixed, the operating point is not. -- `OP-023` annotated: `DEC-001` establishes the precedent that freeze exemptions are granted - individually, named and recorded. - -### Changed — `DOC/MVP_TASKS.md` -- LiveKit removed from the not-in-MVP list; it is now MVP scope. -- **New tasks:** `T-039` (VP8 encode feasibility spike on the Pi 5), `T-047` (deploy the LiveKit - server), `T-048` (access tokens and room model), `T-049` (frontend `livekit-client` subscriber), - `T-077` (retire the superseded H.264/Zstd path — *after* the replacement is proven on hardware). -- `T-041` rewritten as a publish pipeline into the SFU; `T-019` reduced to deleting the dead - live-view URL conventions rather than unifying them. -- Critical path redrawn: three parallel tracks; `T-060` (user identity) pulled forward out of Phase 5. -- Totals: 54 → 59 tasks, ~34 → ~37 person-weeks, ~20–24 → ~21–26 calendar weeks. - -### Changed — `DOC/InvariantMatrix.md` / `.csv` -- New row `C-12` — video-plane (SFU) connection indicator, visible in every mode. The video plane can - now fail independently of the control plane (`IF-CM-012`), and the GUI must say so. 60 → 61 rows. - -### Open and now urgent -`OP-015` (latency target) and `OP-004` (user identity) both rose as a direct consequence of this -decision and should be answered before Phase 3 starts. `ASM-08` — software VP8 on a Pi 5 alongside -acquisition — is unproven and is what `T-039` exists to establish, early. - -## Version: 1.1 29.07.26 — Requirements engineering pass - -Documentation only. **No source code was changed.** - -### Added -- `DOC/SRS_STANDARD.md` — research findings on how to set up a proper SRS: standards landscape - (ISO/IEC/IEEE 29148:2018 as primary, ISO/IEC 25010:2023 for quality attributes), the prescribed SRS - outline, well-formed-requirement syntax, the nine individual and five set-level requirement quality - characteristics, requirement attributes, verification methods, and the identifier scheme adopted here. -- `DOC/SRS_V1.md` — draft SRS structured per 29148 clause 9.6. ~180 requirements across functional, - interface, usability, performance, data, constraint and quality sections, each with priority, source - and verification method. Includes a verification chapter with ten acceptance scenarios - (`ACC-01` … `ACC-10`) and a traceability appendix mapping `basicRequirements.md` sections and - `InitReview.md` findings onto requirement IDs. -- `DOC/OpenPoints.md` — register of 32 open points from the SRS draft, by severity (S1–S3) and status - (OPEN / ASSUMED / CONFLICT). Three are conflicts between source documents: live-video transport - (`OP-001`), cache-vs-durable-data (`OP-018`), and the boundary of the architecture freeze (`OP-023`). -- `DOC/InvariantMatrix.md` — GUI operability invariant matrix: eight-gate precedence chain, ten - invariants, eight operating modes, per-function availability for 60 GUI functions. Normative for the - rule that only usable controls may be shown. -- `DOC/InvariantMatrix.csv` — spreadsheet-editable companion with the same 60 rows; intended as the - source for generated frontend gating and as a directly executable test table. -- `DOC/MVP_TASKS.md` — 54 tasks in seven phases from the current tree to an MVP, with dependencies, - effort estimates, definition of done per task, critical path, an explicit not-in-MVP list, and the - ten decisions that must precede implementation. - -### Notes -- Every ***UNCLEAR*** marker in `SRS_V1.md` has a matching `OP-nnn` entry in `OpenPoints.md`. No open - question was resolved by invention. -- `DOC/basicRequirements.md` and `DOC/InitReview.md` were read as inputs and left unmodified, per - `DOC/TODOS.md`. -- Highest-priority decisions before implementation starts: `OP-023` (scope of the architecture freeze) - and `OP-011` (500 MB/s — per camera or aggregate). Both change what gets built, not just how. - -## Version: 1.0 28.07.26 -- Initial draft -- serves as original for all future versions -- basic design decisicions, that can be concluded from the current structure shall not be changed. What this means is written in ***General setup rule*** fo the SRS.md diff --git a/SRS/InitReview.md b/SRS/InitReview.md deleted file mode 100644 index 7900b9f..0000000 --- a/SRS/InitReview.md +++ /dev/null @@ -1,506 +0,0 @@ -# newswitch — Initial Architecture Review - -**Date:** 2026-07-29 -**Branch reviewed:** `DEV_KC` (working tree, including uncommitted changes) -**Scope:** Architecture of `backend/` and `frontend/`. No code was changed. - ---- - -## 1. Executive summary - -newswitch is a **two-halves monorepo** implementing a web-stack microscope controller: -a Python/FastAPI backend built on `rekuest-next`, and a React/TypeScript frontend whose -typed API layer is **generated from the running backend**. The overall design is coherent -and unusually well-documented for an alpha, with a clear `Protocol → Manager → State` -layering on the backend and a clean generated-client boundary to the frontend. - -**Verdict on the architecture: sound and worth keeping.** The layering choices (structural -protocols, state-with-locks, generated client, separated data planes) are the right ones for -a lab instrument that must run both virtually and against real hardware. - -**Verdict on the current tree: broken.** The in-flight UC2 hardware work has left the -backend unable to import at all, so the backend, its test suite, and the frontend codegen -are all down. This is a small, well-understood breakage — not an architectural problem — -but nothing else can be validated until it is fixed. - -| | Backend | Frontend | -|---|---|---| -| Source LOC | ~8,600 (`newswitch/`) | ~18,700 hand-written + ~6,400 shadcn `ui/` + ~5,100 generated `apps/` | -| Test LOC | ~1,580 (7 files) | ~270 (2 files) | -| Import/build state | **Fails to import** | Not buildable locally (`node_modules` absent) | -| Lint | 41 errors, **all** in in-flight UC2 files | not run (deps absent) | -| Format | 8 files unformatted, all in-flight | not run (deps absent) | - ---- - -## 2. System topology - -``` -┌──────────────────────────── frontend (Vite/React 19) ────────────────────────────┐ -│ src/apps/default/** ← GENERATED from the backend schemas (committed on purpose) │ -│ src/lib/rekuest/** ← hand-written client: transport, state, task, lock stores │ -│ src/components/** ← microscope panels + react-three-fiber "stage" 3D viewer │ -└───────┬────────────────────┬───────────────────────┬─────────────────────┬────────┘ - │ control plane │ video plane │ bulk data plane │ codegen - │ WS /ws │ WS /stream/{codec}/ │ HTTP /cache/{id}/** │ HTTP /schemas/* - │ (rekuest agent: │ {slot} │ (Zarr v3 chunks) │ (build time only) - │ actions, states, │ (H.264 / Zstd) │ HTTP /files/** │ - │ locks, tasks) │ │ │ -┌───────┴────────────────────┴───────────────────────┴─────────────────────┴────────┐ -│ backend (FastAPI + rekuest-next agent) │ -│ app.py wiring: provide_managers() @startup, @register functions │ -│ protocols/ Protocol interfaces + @state dataclasses (the contract) │ -│ managers/ implementations: virtual/ | uc2/ | io/ | cache/ │ -│ routines/ composite operations (MDA, region scan, calibration) │ -│ broadcasters/ FrameBroadcaster + shared H264/Zstd encoders │ -│ uc2_devices/ SDK-only camera driver layer (+ native C++ ring buffer, WIP) │ -└───────────────────────────────────────────────────────────────────────────────────┘ -``` - -### Four distinct data planes — a genuine strength - -The design deliberately does **not** push everything through one channel: - -1. **Control plane** — `rekuest-next` over `/ws`: action invocation, task lifecycle, - state snapshots + JSON-patch deltas, lock acquisition. -2. **Video plane** — `/stream/h264/{slot}` and `/stream/zstd/{slot}`: per-detector live - view. Encoders are *shared* between clients with identical configs - (`FrameBroadcaster._encoders` keyed by `(slot, config)`), so N viewers cost one encode. -3. **Bulk data plane** — acquired frames are written to a local **Zarr v3** store and served - as raw chunks over `/cache/{store_id}/{key}`, consumed directly by `zarrita` in the - browser. Large image data never travels through the control socket. -4. **File plane** — `/files/**` for exported PNG/TIFF artefacts. - -Separating these is the single best structural decision in the codebase. - ---- - -## 3. Backend architecture - -### 3.1 Layering - -``` -protocols/ ── Protocol (structural interface) + @state dataclass, paired per subsystem - ▲ e.g. stage.py → StageManager (Protocol) + StageState (@state) - │ -managers/ ── concrete implementations, selected at startup - │ virtual/ VirtualStage, VirtualLED, VirtualDetector, VirtualObjective, VirtualFilterBank - │ uc2/ UC2Serial, UC2Stage, Uc2Detector, (galvo, filter bank — unwired) - │ io/ LocalFileIOManager cache/ LocalCacheManager - │ + cross-cutting: LightPath, Metadata, Acquisition, Expanse, Calibration, PythonHook - ▲ -app.py ── provide_managers() constructs everything and returns a 26-tuple for DI -``` - -`provide_managers()` is the composition root. `@startup` returns a tuple whose **type -annotation** is the DI registration — rekuest matches each returned object to the -declared protocol/state type and injects it into `@register`ed functions by parameter type. - -**Assessment.** The protocol/manager split is the right abstraction and it demonstrably -works: the same registered functions drive the virtual microscope and (by design) real UC2 -hardware, selected by one flag (`ImswitchConfig.use_virtual_microscope`). Using structural -`Protocol` rather than inheritance keeps drivers free of framework coupling. - -**Concerns.** - -- **The composition root is doing too much.** `provide_managers()` is ~150 lines returning - a 26-element tuple, with the virtual/hardware branch interleaved into the middle of state - construction. Adding a subsystem means editing the return type, the tuple, and the body in - three places that must stay positionally aligned. This should be split into small - builder functions (`build_states()`, `build_device_managers(config, states)`, - `build_domain_managers(...)`), or the tuple replaced by a container object. -- **Hardware selection is inconsistent.** `use_virtual_microscope=False` swaps only serial, - stage and detector. Illumination, objective and filter bank stay virtual unconditionally, - even though `managers/uc2/filter_bank_manager.py` (212 LOC) and - `managers/uc2/galvo_scanner.py` (122 LOC) exist. There is no per-subsystem configuration. -- **Config is a stub.** `ImswitchConfig` has 5 fields, and real values are hardcoded at the - call sites: `port="/dev/ttyUSB0"`, `baudrate=115200`, `base_path="/tmp/newswitch/images"`, - `settings_path="uc2DevSettings.json"`. `ImswitchConfig` is the intended home for these - (`test.py` already mutates `use_virtual_microscope` on it), and the README states the - backend deliberately reads no `.env` — so the config object needs to grow rather than the - literals staying scattered. - -### 3.2 State and locking - -States are plain dataclasses decorated `@state(required_locks=[...])`; managers are -decorated `@context(locks=[...])`. Mutation of shared state requires holding the named lock, -and the lock vocabulary (`stage_position`, `camera_parameters`, `objective`, `illumination`, -`filter_bank`, `io`, `expanse_state`, `cache`, `hook_registry`) is exported to the frontend -via `/schemas/locks` and mirrored into `src/apps/default/hooks/locks/**`. - -This is the strongest idea in the system: **the concurrency model is part of the published -API**, so the UI can grey out a control because the backend lock that guards it is held — -by another browser tab, by a running MDA, or by a script. Optimistic updates are declared -the same way (`OptimisticInput` on `move_stage` → generated `OptimisticStageState` in -`moveStage.ts`), so the UI's optimistic behaviour is backend-authored rather than -reimplemented client-side. - -Caveat: 12 `@state` classes exist but only 11 are wired. `protocols/z_range.py` + -`managers/virtual/virtual_z_range.py` are referenced from nowhere (verified by grep) — dead -on arrival. - -### 3.3 The `Kube` / `KubeState` duplication - -`protocols/core.py` (569 LOC) defines **nine** `*Kube` classes (Objective, ObjectiveTurret, -Detector, Illumination, Dichroic, Stage, Filter, FilterBank, Generic) and **nine** parallel -`*KubeState` classes with near-identical fields — the difference being that `*KubeState` -carries live values (gain, exposure, intensity, wavelength) while `*Kube` carries only -identity + affine matrix. `MetadataManager` converts between them. - -This duplication is load-bearing and it **propagates into the frontend**: there are two -near-identical renderer trees, `components/stage/planes/kubes/**` (renders `KubeState`, for -the light path recorded in an acquired frame) and `components/stage/planes/lightpath/kubes/**` -(renders `Kube`, for the live/possible light path). Both are live — the file pairs differ by -6–32 lines each. One backend modelling decision is costing two parallel 3D renderer -hierarchies plus two `LightPathEdges` implementations plus two panel sets. - -**Recommendation:** unify on one kube type with an optional live-state payload -(`Kube` + `state: KubeLiveState | None`), and collapse the frontend renderers to one tree. -This is the single highest-leverage refactor available, and it should be done *before* more -kube types are added, not after. - -Related: `LightPath.transformation_hash()` (`core.py:254`) is dead (no callers) and buggy — -it filters `self.kubes` (which holds `Kube` values) with `isinstance(kube, FilterBankKubeState)`, -so the filter branch can never match, and it interpolates the whole `objective` object into -the hash string rather than its slot id. - -### 3.4 Acquisition path - -`capture_image` → `AcquistionManager.acquire()` → for each active detector: -`DetectorManager.capture_image()` → `LightPathManager.get_light_path_for_detector()` → -`MetadataManager.get_metadata_for_light_path()` → `CacheManager.save_frame()` (Zarr v3) → -`Frame` appended to `ExpanseState.current_frames` and returned. - -This is a clean pipeline with good separation. Two notes: - -- `LocalCacheManager` hardcodes `default_chunks=(1, 256, 256)` and **asserts** the array is - exactly 3-D; `AcquistionManager` satisfies this by reshaping every 2-D camera frame to - `(1, H, W)`. Multi-channel or true-volumetric detectors will not fit this contract without - a change to both. -- `delete_on_restart=True` wipes `/tmp/newswitch/cache` on every boot. Correct for a cache; - worth being explicit that the expanse is not durable across restarts. - -### 3.5 Streaming - -`FrameBroadcaster` is well-designed: shared encoders keyed by `(detector_slot, config)`, -refcounted subscriptions, `stop_all_encoders()` on shutdown to reap ffmpeg subprocesses. -The `@shutdown` hook (`release_managers`) explicitly documents *why* it exists. - -Issues: - -- **`release_managers` takes `detector: DetectorManager` and never uses it.** `Uc2DetectorManager` - has a `shutdown()` that stops streaming and releases every SDK camera handle — it is never - called. On the hardware path, camera handles leak on every restart. -- **`get_latest_frame()` / `_latest_frame` is vestigial** — `broadcast_sync` never assigns - `_latest_frame`, so the accessor always returns `None`. -- **The live loop is a `while True` with no backpressure.** `Uc2DetectorManager.acquire_live()` - sleeps only when `is_acquiring` is false; when it is true but no detector is active, the - inner `for` body never runs and the outer loop spins at 100% CPU with no sleep. - `VirtualDetectorManager` handles this case (`koil.sleep(0.1)`); the UC2 version does not. - Neither loop paces itself against exposure time — the virtual one even has a dangling - comment ("Use exposure time from first active detector for frame interval") with no code. - -### 3.6 Route-level dependency injection - -`routes/http/files.py`, `routes/http/cache.py` and `routes/ws/liveview.py` each reach managers -via `request.app.state.agent.get_context_for_type(X)`, with a hand-written -`get_X_from_request` helper duplicated per module. It works and is documented, but it is a -service-locator escape hatch: FastAPI routes bypass the typed DI that `@register`ed functions -enjoy. One shared `Depends` factory (`context_dependency(CacheManager)`) would remove the -triplication. - -**Security note on `/files/{file_path:path}`:** unlike the cache route — which does a proper -`is_relative_to(store_dir)` traversal check — the files route passes the client-supplied path -straight to `io_manager.file_exists()` and then to `FileResponse` with no confinement to -`LocalFileConfig.base_path`. Combined with `allow_origins=["*"]` CORS, this is an arbitrary-file-read -surface. Acceptable on an isolated lab network; not acceptable if the instrument is ever -reachable beyond it. Worth fixing regardless, since the correct pattern already exists 40 -lines away in `cache.py`. - -### 3.7 `uc2_devices/` — the hardware driver layer - -The intent is well thought through and thoroughly documented (an 18 KB README with mermaid -diagrams, a documented state machine for the native ring buffer, an on-disk RAW format spec): - -- SDK-only layer *below* the managers, so `Uc2DetectorManager` composes a driver rather than - inheriting from one. -- **ABC rather than Protocol here — deliberately and correctly**, because drivers share real - logic (`apply_settings`, `compute_variance`, `save_stack`, context-manager lifecycle). - The README explains this choice explicitly. Good. -- The stated boundary — *control in Python, frame data never through Python* — is the right - call for a Pi-class device at ~1 GB/s, with `grab_frame()` as an explicitly-labelled - snapshot-only path and the native ring buffer owning the sustained stream. - -**However, this layer is currently mid-demolition** and is the source of every blocking -problem in §5.1. The entire C++ core, the pybind11 bindings, the ring buffer, the HIK driver -and the tests are deleted in the working tree (~4,000 lines), the root `CMakeLists.txt` has -its pybind targets commented out, and the remaining Python still imports the deleted pieces. -The documentation now describes a system that no longer exists in the tree. - ---- - -## 4. Frontend architecture - -### 4.1 The generated-client boundary - -`plugins/generate-app.ts` (1,648 LOC) runs in Vite's `buildStart`, fetches -`/schemas/implementations`, `/schemas/states`, `/schemas/locks` from a *running* backend, and -emits `src/apps/default/**`: one module per action with a Zod schema, TS types, an -`ActionDefinition` (including `lockKeys` and optimistic accessors) and a `use()` hook; -plus one module per state and lock. `blok.json` and the generated tree are committed on -purpose. - -This is a strong contract: 25 backend registrations map 1:1 to 25 generated action modules, -and validation, lock metadata and optimistic behaviour all derive from the same source of -truth. Docstrings become JSDoc. - -The known hazard is documented honestly in the README and CI: **the generator fails silently** -(fetch errors are warnings; it falls back to the committed output). The mitigations are real -— `just dev` sequences backend-then-frontend, `just drift-check` and the `codegen-drift` CI -job boot the backend, regenerate, and fail on any diff, and compose's `depends_on` + -healthcheck removes the race entirely. The compensating controls are the right ones; the -residual risk is that a developer running `yarn dev` alone gets no hard failure. - -Also worth noting: `codegen-drift` is the only CI job that catches this, and it currently -cannot pass, because the backend cannot boot (§5.1). - -### 4.2 The `lib/rekuest` client - -~6,000 LOC of hand-written client, layered: - -- `transport/` — `TransportProvider` + `subscription-manager`: one WebSocket per app, INIT - handshake declaring the action/state/lock keys this client cares about, reconnect with - exponential backoff, ping keepalive. -- `state/` — global state store with snapshot + **JSON-patch delta** application - (`fast-json-patch`), Zod validation of snapshots, and a `selectPath` selector family. - `materialization.ts` reconstructs typed values from the wire format. -- `task/` — task store + `useAction`, optimistic application/rollback. -- `locks/` — lock store and `useLock`. -- `createScopedProvider` / `createScopedStore` — per-route scoping, so `/` and `/replay` - hold independent store instances. - -The union wire format is handled by a well-explained `createIndexedUnion` Zod codec -(positional `{__use, __value}` envelopes, idempotent decode so patched documents re-validate). -This is the only part of the frontend with real unit tests (`indexed-union.test.ts`, -`store.test.ts`). - -**Concern: `BundleProvider.tsx` is 1,377 lines** and is the orchestration hub for every -store, subscription and patch stream. It is the highest-risk file in the frontend — the most -complex, the most central, and effectively untested. If any part of the client deserves -extraction and unit tests, it is this one. - -`src/store/createScopedStore.ts` and `src/hooks/useLock.tsx` are thin re-export shims for -`lib/rekuest` equivalents — harmless, but two import paths for one thing. - -### 4.3 Components - -Two tiers: `components/ui/**` (~6,400 LOC of vendored shadcn/Radix primitives — treat as -dependency, not code to maintain) and the application tier: - -- `components/microscope/**` — control panels (stage, camera, illumination, filter bank, - objective, MDA). **`MultidimensionalAcquisitionControl.tsx` (1,110) and - `MultidimensionalAcquisitionDialog.tsx` (995) are outliers** — 2,100 lines for one feature, - with visible overlap between the two. Prime extraction candidates. -- `components/stage/**` — the react-three-fiber viewer: planes, volumes, kube renderers, - Zarr-backed chunk loading with an LRU cache and colormaps, live textures fed from the - H.264/Zstd sockets, interaction controllers. Architecturally the most interesting part, and - where the `Kube`/`KubeState` duplication (§3.3) is paid for. -- `components/lightpathstate/**` — a `@xyflow/react` + `dagre` node-graph view of the light path. - -Local UI state lives in `src/store/**` (zustand + immer, scoped per route) and is properly -kept separate from backend-synced state in `lib/rekuest/state`. - -### 4.4 Live-view wiring is inconsistent - -Three URL conventions coexist and only one of them is correct: - -| Consumer | URL it builds | Backend route | Status | -|---|---|---|---| -| `useH264LiveTexture` | `BACKEND_API + "/stream/h264/" + slot` | `/stream/h264/{slot}` | path correct, but built from the **http** base and handed to `new WebSocket()` | -| `useZstdLiveTexture` | `BACKEND_API + "/stream/zstd/" + slot` | `/stream/zstd/{slot}` | same | -| `StreamingView` (default prop) | `VITE_WEBSOCKET_URL + "/video"` → `ws://…/ws/video` | **no such route** | dead | - -`LiveView.tsx` renders `` with no `url` prop, so the microscope panel's live -view points at an endpoint that does not exist — a leftover from an earlier single-stream -design (`/video`) that survived the move to per-slot, per-codec routes. The `constants.tsx` -pair uses `BACKEND_API` (http) where `BACKEND_WS` (ws) is the declared convention; modern -browsers do accept `http(s)` in the `WebSocket` constructor, so this may work today, but it -contradicts the project's own env-var contract and will break behind a proxy that maps the -two differently. - ---- - -## 5. Findings - -### 5.1 Blocking — the backend does not run - -1. **`newswitch/uc2_devices/__init__.py` is deleted, but `uc2_detector_manager.py` still - imports from it, and `app.py` imports that manager unconditionally at module scope.** - Every entry point dies at import: - - ``` - app.py:29 → managers/uc2/uc2_detector_manager.py:38 - → from newswitch.uc2_devices import CameraInfo, Uc2Camera, Uc2DevSettings - FileNotFoundError: .../newswitch/uc2_devices/__init__.py - ``` - - Consequence: `uvicorn main:app` fails, `uv run pytest` fails at conftest collection - (**the whole 1,583-line backend suite is unrunnable**), `just dev-backend` fails, and the - frontend codegen therefore silently falls back to committed hooks. - (Aggravating detail: the editable install is a scikit-build-core loader whose file map - still points at the deleted `__init__.py`, so a plain re-sync is also needed after - restoring it.) - -2. **No concrete camera driver exists.** `hik_camera.py` is deleted, so `Uc2Camera` — an ABC - with abstract methods — is the only "driver". `Uc2DetectorManager`'s default - `drivers=(Uc2Camera,)` calls `Uc2Camera.enumerate()`, an abstract classmethod with an - empty body returning `None`, and then iterates it. The hardware path cannot work even - after fixing (1). - -3. **The native layer is gone but still referenced.** ~4,000 lines of C++ (ring buffer, - MVS/HIK capture engine, pybind11 bindings, C++ and Python tests) are deleted; - `backend/CMakeLists.txt` has all pybind targets commented out; `uc2_camera.py` still tries - to load `_ringbuffer` (it degrades gracefully); `uc2_devices/README.md` still documents - the whole thing as present. The build backend is still `scikit-build-core`, so every wheel - build runs CMake for what is now an empty project. - -### 5.2 Structural - -4. **`Kube` / `KubeState` duplication** (§3.3) — 18 near-identical backend classes driving two - parallel frontend renderer hierarchies. Highest-leverage refactor. -5. **`provide_managers()` as a 26-tuple composition root** (§3.1) — three places must stay - positionally aligned per subsystem added. -6. **Partial hardware abstraction** (§3.1) — only 3 of 6 device subsystems switch on - `use_virtual_microscope`; two written UC2 managers are unreachable. -7. **`BundleProvider.tsx` at 1,377 LOC** (§4.2) — the frontend's most central and least - tested module. -8. **`Uc2DetectorManager.shutdown()` is never called** (§3.5) — `release_managers` receives - the detector and ignores it; SDK handles leak on the hardware path. -9. **Unbounded live-acquisition loops** (§3.5) — `Uc2DetectorManager.acquire_live()` busy-spins - when acquiring with no active detector; neither loop paces against exposure time. -10. **Path traversal on `/files/{file_path:path}`** (§3.6) — no confinement to `base_path`, - while the neighbouring cache route does it correctly. -11. **Live-view URLs inconsistent; `StreamingView` points at a dead route** (§4.4). - -### 5.3 Hygiene / smaller - -12. **Silent settings loss.** `app.py` passes `settings_path="uc2DevSettings.json"` (relative - to CWD) while the file lives at `newswitch/uc2_devices/uc2DevSettings.json`; - `Uc2DevSettings.load()` returns empty defaults for a missing file, so a misconfigured - path is indistinguishable from "no cameras configured". Separately, - `uc2DevSettings.json` and the README both document `recording_format`, but - `Uc2CameraSettings` has no such field and `from_dict` drops it silently. -13. **`enumerate()` contract mismatch (uncommitted).** The working tree changes the ABC's - return type to `list[Uc2Camera]`, but `Uc2DetectorManager` still treats the items as - `CameraInfo` (`driver(info)`, `info.serial`, `info.model`). -14. **Dead code:** `protocols/z_range.py` + `managers/virtual/virtual_z_range.py` (unwired); - `LightPath.transformation_hash()` (no callers, and buggy); `FrameBroadcaster.get_latest_frame()` - (always `None`); `plugins/gonio_meter.py` (docstring only). -15. **Lint/format regression, fully localised.** 41 ruff errors and 8 unformatted files — all - in the in-flight UC2 files (27 errors in `uc2_camera.py` alone). The rest of the backend - is clean, so this is new debt, not accumulated debt. Also, `[tool.ruff] extend-select` - is in the deprecated top-level position and ruff warns on every run; it belongs under - `[tool.ruff.lint]`. -16. **`pyproject.toml` metadata commented out** (uncommitted): `readme`, `homepage`. - Presumably a workaround for a build error; it makes published wheels metadata-poor. -17. **Test asymmetry.** Backend ~1,580 test LOC across 7 files vs frontend ~270 LOC across 2. - Nothing covers `BundleProvider`, the transport layer, the stage/3D components, or the - codegen plugin — the codegen plugin's only guard is the CI drift job. -18. **CORS `allow_origins=["*"]` with `allow_credentials=True`** — invalid per spec (browsers - reject the combination) and permissive by intent. Fine for a lab box; make it a config - knob before any wider deployment. -19. **Empty scaffolding:** `CLAUDE.md`, `DOC/SRS.md`, `DOC/TODOS.md`, `DOC/CHANGELOG_KC.md` - are 0 bytes. `newswitch/routes/__init__.py` and `newswitch/managers/__init__.py` are - docstring-only. - ---- - -## 6. Tooling, build and CI - -**Good:** -- `just` as the single task entry point, with recipes that encode the ordering constraint - (`dev` waits on the backend before starting vite) and warn loudly when it is violated. -- CI runs a 3-OS × 2-Python matrix for the backend, plus coverage, ruff check *and* ruff - format on one platform. -- The frontend job comments record real history: `types` used to point at a solution tsconfig - with `files: []` and checked **zero** files, hiding 165 type errors and 90 lint problems. - Both gates are now blocking. That is exactly the right response. -- `codegen-drift` boots the real backend and diffs the regenerated output — the correct - compensating control for a silently-failing generator, and only possible because both - halves live at one commit. -- `ci-ok` aggregate job so path-filtered skips don't produce a vacuously-green required check. -- Dockerfile runs as uid 1000 with a documented reason (bind-mounted `agent_data.db` would - otherwise be root-owned); compose healthcheck probes the exact endpoint codegen needs. -- lefthook: commitlint on `commit-msg`, ruff/prettier on `pre-commit`, with `src/apps/**` - excluded because the generator owns its formatting. -- Unified semantic-release bumping `backend/pyproject.toml` and `frontend/package.json` to - one version. - -**Gaps:** -- **No CI job builds the native/C++ side**, and `scikit-build-core` is still the build backend - for a CMake project that now compiles nothing. `just build` runs `uv build`, which will - invoke CMake; this is untested in CI. -- **`agent_data.db` exists at both repo root and `backend/`** as untracked runtime artefacts - (`*.db` is gitignored, correctly). The root copy suggests the backend has been started from - the wrong CWD at some point — the same class of CWD-relative-path fragility as finding (12). -- CI never exercises the Docker path. - ---- - -## 7. Recommendations, in order - -**Immediate — restore a working tree (hours):** -1. Restore `newswitch/uc2_devices/__init__.py` (and re-sync the editable install), or make - `app.py`'s `Uc2DetectorManager` import lazy so the virtual path never depends on hardware - modules. Prefer **both**: a hardware driver should not be able to break virtual mode at - import time. -2. Get `uv run pytest` green again and confirm `just drift-check` passes — nothing else can - be verified until these two do. -3. Run `ruff format` + `ruff check --fix` over the UC2 files and clear the remaining 38 by - hand; move `extend-select` under `[tool.ruff.lint]`. -4. Decide the fate of the native layer and make the tree say so: either restore it, or drop - `scikit-build-core` back to a pure-Python backend and move `uc2_devices/README.md`'s - native sections to a design doc marked "planned". Documentation describing deleted code is - worse than no documentation. - -**Short term — close the correctness gaps (days):** -5. Call `detector.shutdown()` from `release_managers` (§5.2-8). -6. Add the missing sleep/pacing to both `acquire_live()` loops (§5.2-9). -7. Confine `/files/**` to `base_path` using the same check `cache.py` already implements (§5.2-10). -8. Point `StreamingView` at a real route and unify the three live-view URL conventions on - `BACKEND_WS` (§5.2-11). -9. Move `port`, `baudrate`, `base_path`, `settings_path` into `ImswitchConfig`; resolve - `uc2DevSettings.json` relative to the package, and make a missing settings file at an - explicitly-configured path an error rather than empty defaults (§5.3-12). -10. Restore or write one concrete camera driver so the hardware path is exercisable at all — - a `MockCamera` (the README already contains a complete one) would let the whole UC2 - branch be unit-tested with no SDK present. - -**Medium term — pay down the structural debt (weeks):** -11. Unify `Kube`/`KubeState` and collapse the two frontend renderer trees (§3.3). Do this - before adding kube types. -12. Break up `provide_managers()` and replace the 26-tuple with a container (§3.1). -13. Extract `BundleProvider.tsx` into testable units and add transport/patch-application - tests (§4.2). -14. Split the ~2,100-line MDA control/dialog pair (§4.3). -15. Make hardware selection per-subsystem and wire up (or delete) the unreachable UC2 - filter-bank and galvo managers (§5.2-6). -16. Delete the dead code in §5.3-14 rather than leaving it to be rediscovered. - ---- - -## 8. Closing assessment - -The bones are good. The protocol/manager/state layering, the four separated data planes, the -lock vocabulary as published API, and the generate-the-client-from-the-backend contract are -all decisions that will still look right in a year — and the tooling around them (the -sequencing in `just dev`, the drift-check job, the honest comments about gates that used to -be dead) shows a team that has already been bitten by the failure modes and built controls -for them. - -The two things holding it back are both fixable and both localised: an in-flight hardware -refactor that has left the tree in a non-importing state, and one modelling decision -(`Kube`/`KubeState`) whose duplication has now replicated itself across the frontend. Fix the -first this week; plan the second before the kube taxonomy grows further. - -*No code was modified in the course of this review.* diff --git a/SRS/InvariantMatrix.csv b/SRS/InvariantMatrix.csv deleted file mode 100644 index ebde1e3..0000000 --- a/SRS/InvariantMatrix.csv +++ /dev/null @@ -1,72 +0,0 @@ -ID,Group,Function,Lock,CapabilityKey,M0_DISCONNECTED,M1_OBSERVER,M2_IDLE,M3_LIVE,M4_MOVING,M5_PROCEDURE,M6_SAFE_ERROR,M7_CALIB_MANUAL,Requirement,Notes,Status -S-01,Session,Backend connection indicator,,,V,V,V,V,V,V,V,V,IF-UI-004,,ACTIVE -S-02,Session,Device connection indicators,,,V,V,V,V,V,V,V,V,IF-UI-003,,ACTIVE -S-03,Session,Connected-user list,,,V,V,V,V,V,V,V,V,REQ-SES-011,,ACTIVE -S-04,Session,Control-authority indicator,,,V,V,V,V,V,V,V,V,REQ-SES-011,,ACTIVE -S-05,Session,Request control,,,H,A,H,H,H,H,D,H,REQ-SES-003,,ACTIVE -S-06,Session,Release control,,,H,H,A,A,D,C,D,D,REQ-SES-012,,ACTIVE -S-07,Session,Safe state / stop all,,,H,D,A,A,A,C,D,A,REQ-SAF-001,See OP-028 (observer access),ACTIVE -S-08,Session,Acknowledge error / recover,,,H,D,H,H,H,H,A,H,REQ-DEV-005,,ACTIVE -S-09,Session,Stale-state banner,,,V,H,H,H,H,H,H,H,INV-005,,ACTIVE -D-01,Devices,Device list / status,,,V,V,V,V,V,V,V,V,REQ-DEV-002,,ACTIVE -D-02,Devices,Initialise / start device,,,H,D,A,D,D,D,A,D,REQ-DEV-001,,ACTIVE -D-03,Devices,Stop device,,,H,D,C,C,D,D,D,D,REQ-DEV-001,,ACTIVE -D-04,Devices,Reload device configuration,,,H,D,C,D,D,D,C,D,REQ-DAT-002,,ACTIVE -C-01,Camera,Select live-view camera,camera_parameters,,H,D,A,A,A,D,D,A,REQ-IMG-002,,ACTIVE -C-02,Camera,Start live view,camera_parameters,,H,D,A,H,A,D,D,H,REQ-IMG-002,,ACTIVE -C-03,Camera,Stop live view,camera_parameters,,H,D,H,A,A,D,D,A,REQ-IMG-002,,ACTIVE -C-04,Camera,Snapshot,io,,H,D,A,A,D,D,D,A,REQ-IMG-004,See OP-030 (snapshot while moving),ACTIVE -C-05,Camera,Camera name,camera_parameters,,H,D,A,A,A,D,D,D,REQ-CAM-001,,ACTIVE -C-06,Camera,Exposure time (slider + entry),camera_parameters,exposure,H,D,A,A,A,D,D,A,REQ-CAM-002,Per-camera G1 gate,ACTIVE -C-07,Camera,Gain (slider + entry),camera_parameters,gain,H,D,A,A,A,D,D,A,REQ-CAM-003,Per-camera G1 gate,ACTIVE -C-08,Camera,Frame rate (slider + entry),camera_parameters,framerate,H,D,A,A,A,D,D,A,REQ-CAM-004,Per-camera G1 gate,ACTIVE -C-09,Camera,Channel weighting,,channel_weighting,H,H,H,A,H,H,H,A,REQ-CAM-005,Live viewer only; see OP-003,ACTIVE -C-10,Camera,Live image canvas,,,V,V,H,V,V,V,H,V,REQ-IMG-002,,ACTIVE -C-11,Camera,Frame-drop / rate indicator,,,V,V,H,V,V,V,H,V,REQ-IMG-012,,ACTIVE -C-12,Camera,Video-plane (SFU) connection indicator,,,V,V,V,V,V,V,V,V,IF-CM-012,Video plane is separate from the control plane since DEC-001 (LiveKit),ACTIVE -L-01,Illumination,Light source on/off,illumination,switchable,H,D,A,A,A,D,D,A,REQ-LGT-001,,ACTIVE -L-02,Illumination,Light source power (slider + entry),illumination,power,H,D,A,A,A,D,D,A,REQ-LGT-002,G9 value gate: hidden while source is off,ACTIVE -L-03,Illumination,Light source identity / wavelength,,wavelength,V,V,V,V,V,V,V,V,REQ-LGT-005,,ACTIVE -L-04,Illumination,Emission indicator,,,V,V,V,V,V,V,V,V,NFR-SAF-003,,ACTIVE -T-01,Stage,Jog / step buttons (+/- per axis),stage_position,jog,H,D,A,A,A,D,D,A,REQ-STG-001,Per-axis G1 gate,ACTIVE -T-02,Stage,Position display (X/Y/Z),,,V,V,V,V,V,V,V,V,REQ-STG-006,,ACTIVE -T-03,Stage,Absolute position entry + confirm,stage_position,absolute,H,D,A,A,D,D,D,A,REQ-STG-009,,ACTIVE -T-04,Stage,Home (separate menu),stage_position,home,H,D,C,C,D,D,D,D,REQ-STG-004,Must not sit in the centre of the direction cluster (REQ-STG-005),ACTIVE -T-05,Stage,Stop motion,stage_position,,H,D,H,H,A,H,H,A,REQ-STG-011,,ACTIVE -T-06,Stage,Jog / step mode switch,stage_position,,H,D,A,A,D,D,D,A,REQ-STG-008,,ACTIVE -T-07,Stage,Step size (separate menu),stage_position,,H,D,A,A,D,D,D,A,REQ-STG-007,,ACTIVE -T-08,Stage,Velocity (separate menu),stage_position,velocity,H,D,A,A,D,D,D,D,REQ-STG-007,Per-controller G1 gate,ACTIVE -T-09,Stage,Acceleration (separate menu),stage_position,acceleration,H,D,A,A,D,D,D,D,REQ-STG-007,Per-controller G1 gate,ACTIVE -T-10,Stage,Jerk (separate menu),stage_position,jerk,H,D,A,A,D,D,D,D,REQ-STG-007,Per-controller G1 gate,ACTIVE -T-11,Stage,Travel-limit display,,limit_min/limit_max,V,V,V,V,V,V,V,V,REQ-STG-010,,ACTIVE -O-01,Optics,Objective selection,objective,switchable,H,D,A,C,D,D,D,D,REQ-OPT-001,Confirm in LIVE: changes FOV and focus,ACTIVE -O-02,Optics,Filter selection,filter_bank,switchable,H,D,A,A,A,D,D,D,REQ-OPT-002,,ACTIVE -O-03,Optics,Rotator control,UNCLEAR,axes,H,D,A,A,A,D,D,D,REQ-OPT-003,Lock undefined; see OP-032,DRAFT -O-04,Optics,Light-path view,,,V,V,V,V,V,V,V,V,REQ-OPT-004,,ACTIVE -P-01,Procedures,Z-scan configure,stage_position+camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-001,,ACTIVE -P-02,Procedures,Z-scan start,stage_position+camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-001,,ACTIVE -P-03,Procedures,Autofocus select measure,camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-003,Measure set unspecified; see OP-005,DRAFT -P-04,Procedures,Autofocus start,stage_position+camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-003,,ACTIVE -P-05,Procedures,Focus lock on/off,stage_position,,H,D,A,A,D,D,D,D,REQ-PRC-005,Principle unspecified; see OP-006,DRAFT -P-06,Procedures,Overview image start,stage_position+camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-007,,ACTIVE -P-07,Procedures,Calibration manual mode,stage_position+objective,,H,D,A,D,D,D,D,A,REQ-PRC-011,,ACTIVE -P-08,Procedures,Calibration automatic mode,stage_position+objective,,H,D,A,D,D,D,D,H,REQ-PRC-011,,ACTIVE -P-09,Procedures,Calibration set landmark,stage_position,,H,D,H,H,H,H,H,A,REQ-PRC-009,,ACTIVE -P-10,Procedures,Calibration accept / reject result,objective,,H,D,H,H,H,H,H,A,REQ-PRC-012,,ACTIVE -P-11,Procedures,XY stitching configure & start,stage_position+camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-014,Overlap/blending unspecified; see OP-007,DRAFT -P-12,Procedures,Time series configure & start,camera_parameters,,H,D,A,A,D,D,D,D,REQ-PRC-017,Bounds unspecified; see OP-026,DRAFT -P-13,Procedures,Sample-area definition,stage_position,,H,D,A,A,D,D,D,D,REQ-PRC-018,,ACTIVE -P-14,Procedures,Sample-area fine adjustment at power-up,stage_position,,H,D,A,D,D,D,D,D,REQ-PRC-019,,ACTIVE -P-15,Procedures,Abort running procedure,,,H,D,H,H,H,A,D,A,REQ-PRC-020,,ACTIVE -P-16,Procedures,Procedure progress display,,,V,V,H,H,H,V,V,V,REQ-PRC-021,,ACTIVE -X-01,Data,Acquired-data viewer,,,V,V,V,V,V,V,V,V,REQ-DAT-001,,ACTIVE -X-02,Data,Export / download dataset,io,,H,A,A,A,A,A,A,A,REQ-DAT-006,Observer permitted; confirm against OP-004,ACTIVE -X-03,Data,Clear acquired data,expanse_state,,H,D,C,D,D,D,D,D,REQ-DAT-005,,ACTIVE -X-04,Data,Browse historical states,,,V,V,V,V,V,V,V,V,REQ-HIS-001,,ACTIVE -X-05,Data,Re-apply a historical state,all device locks,,H,D,C,D,D,D,D,D,REQ-HIS-003,May switch lasers on; see OP-008 and OP-013,DRAFT -X-06,Data,Storage-capacity indicator,,,V,V,V,V,V,V,V,V,REQ-DAT-008,,ACTIVE -G-01,Diagnostics,Log viewer,,,V,A,A,A,A,A,A,A,REQ-LOG-001,,ACTIVE -G-02,Diagnostics,Log level selection,,,H,D,A,A,A,A,A,A,REQ-LOG-004,,ACTIVE -G-03,Diagnostics,Settings view (read),,,V,V,V,V,V,V,V,V,REQ-DAT-002,,ACTIVE -G-04,Diagnostics,Settings edit,,,H,D,C,D,D,D,D,D,REQ-DAT-002,,ACTIVE -G-05,Diagnostics,Error / notification area,,,V,V,V,V,V,V,V,V,REQ-LOG-005,,ACTIVE diff --git a/SRS/InvariantMatrix.md b/SRS/InvariantMatrix.md deleted file mode 100644 index 2094333..0000000 --- a/SRS/InvariantMatrix.md +++ /dev/null @@ -1,329 +0,0 @@ -# GUI Operability Invariant Matrix — newswitch - -**Document ID:** INVMAT -**Version:** 1.0 (draft) -**Date:** 2026-07-29 -**Normative for:** `SRS_V1.md` → `IF-UI-005`, `IF-UI-010`, `NFR-USA-002`, `NFR-USA-003` -**Editable companion:** `DOC/InvariantMatrix.csv` (same content, spreadsheet-editable, one row per -GUI function) - ---- - -## 1. Purpose - -`basicRequirements.md` states: *"Only show GUI elements, which are really implemented and possible to -use in the current version of the microscope."* - -That is a strong requirement and it cannot be satisfied by ad-hoc `disabled={…}` expressions scattered -through the frontend. This document defines, for **every GUI function**, the conditions under which it -is shown, enabled, disabled or hidden — so that the rule is stated once, reviewable by a -non-programmer, and testable. - -"Invariant" here means: **these relationships hold in every version of the software.** Adding a -feature adds a row; it does not change the gating logic. - ---- - -## 2. How to edit this document - -1. Edit `DOC/InvariantMatrix.csv` in a spreadsheet (one row per GUI function, one column per mode), or - edit the tables in §7 of this file directly. Keep the two in step. -2. Adding a GUI function → add a row. Fill **every** column: an empty cell is a defect, not a default. -3. Adding an operating mode → add a column **and** review every existing row. -4. Never delete a row for a removed feature — mark its status `RETIRED` so the history of the decision - survives. -5. Any change here is a change to a normative document: log it in `DOC/CHANGELOG_KC.md`. - ---- - -## 3. Cell values - -| Code | Meaning | Rendered as | -|---|---|---| -| **A** | **Available** — control is shown and actionable | normal, interactive | -| **C** | **Confirm** — available, but activation requires an explicit confirmation step | normal + confirm dialog | -| **D** | **Disabled** — shown, not actionable, **with a stated reason** (`NFR-USA-003`) | greyed, tooltip gives cause | -| **V** | **View only** — a display element, never actionable | read-only indicator | -| **H** | **Hidden** — not rendered at all | absent from the DOM | - -**D vs. H is the central distinction.** -*Hidden* means "this instrument can never do this" — the user must not learn the control exists. -*Disabled* means "not right now" — the user must be able to see it and understand why. - ---- - -## 4. State variables - -The mode of the GUI is a function of these variables. They are all backend-owned; the frontend derives -its rendering from them and holds no authority of its own. - -| Var | Name | Values | Source | -|---|---|---|---| -| `conn` | Backend connection | `up` / `down` | frontend transport watchdog | -| `auth` | Control authority | `holder` / `observer` | backend session manager | -| `cap[f]` | Capability for function *f* | `supported` / `unsupported` | device capability descriptor (`REQ-DEV-003`) | -| `dev[s]` | Device status of subsystem *s* | `ready` / `absent` / `error` | device manager | -| `lock[l]` | Lock *l* | `free` / `self` / `other` | backend lock vocabulary | -| `act` | Current activity | `idle` / `live` / `moving` / `procedure` | backend | -| `safe` | Safe state | `normal` / `engaged` | backend | -| `val[x]` | Value-dependent visibility | e.g. light source on/off | device state | - ---- - -## 5. Gate precedence — the invariant that governs all others - -Every control is evaluated through this chain **in order**. The **first gate that fires wins**, and a -gate that hides always beats a gate that disables. - -``` - G1 CAPABILITY cap[f] = unsupported ................................ → H - G2 PRESENCE dev[s] = absent ..................................... → H - G3 CONNECTION conn = down ......................................... → D (whole GUI, one banner) - G4 SAFE STATE safe = engaged ...................................... → D (except recovery controls) - G5 DEVICE ERROR dev[s] = error ...................................... → D (except recovery controls) - G6 AUTHORITY auth = observer ..................................... → D - G7 LOCK lock[l] = other ..................................... → D - G8 MODE look up the matrix in §7 ............................ → A / C / D / H - G9 VALUE val[x] condition not met ............................ → H - otherwise .......................................................... → A -``` - -**INV-001** — A control for an unsupported capability is **never** rendered, in any mode, for any user, -including the control-holding user. Capability gating precedes everything. - -**INV-002** — A control that is rendered and enabled **shall** succeed when activated, or fail with a -device-reported error. It shall never fail because the GUI offered something the system could not do. - -**INV-003** — A disabled control **shall** state its cause, and the cause **shall** be the gate that -fired: "no control authority", "held by ", "device not ready", "procedure running", "safe state -engaged", "connection lost". - -**INV-004** — Hiding is a property of the *instrument*; disabling is a property of the *moment*. If a -control's visibility would change without a hardware or configuration change, it must be D, not H. - -**INV-005** — Every display element (`V`) remains visible in every mode, including `conn = down`, where -it shows the last known value marked explicitly as stale. - -**INV-006** — No control that commands motion or emission is `A` while `auth = observer`. There is no -exception, including for read-back or "preview" controls that internally command the device. - -**INV-007** — The safe-state / stop-all control is reachable in every mode in which any device can be -active. It is the only control that may bypass gates G6–G8 ***UNCLEAR*** — see `OP-010` and `OP-028` -below. - -**INV-008** — A procedure holds the locks of every subsystem it drives for its whole duration -(`REQ-PRC-022`); therefore during `act = procedure` the affected controls are disabled by G7, not by a -separate rule. G8 restates this only for readability. - ---- - -## 6. Operating modes (matrix columns) - -| Mode | Condition | Description | -|---|---|---| -| **M0 DISCONNECTED** | `conn = down` | No backend. GUI shows last known state, marked stale | -| **M1 OBSERVER** | `conn = up`, `auth = observer` | Connected, watching, no device authority | -| **M2 IDLE** | `auth = holder`, `act = idle` | Full control, nothing running | -| **M3 LIVE** | `auth = holder`, `act = live` | Live view running | -| **M4 MOVING** | `auth = holder`, `act = moving` | Commanded stage motion in progress | -| **M5 PROCEDURE** | `auth = holder`, `act = procedure` | Z-scan / stitch / time series / MDA / autofocus running | -| **M6 SAFE / ERROR** | `safe = engaged` or `dev[*] = error` | Safe state entered or unrecovered device error | -| **M7 CALIB-MANUAL** | `act = procedure`, manual calibration | Special: the procedure *requires* the user to drive the stage | - -M7 exists because manual calibration (`REQ-PRC-011`) inverts the usual rule — a procedure is running, -yet stage control must remain with the user. - ---- - -## 7. The matrix - -### 7.1 Session and global - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| S-01 | Backend connection indicator | — | V | V | V | V | V | V | V | V | IF-UI-004 | -| S-02 | Device connection indicators | — | V | V | V | V | V | V | V | V | IF-UI-003 | -| S-03 | Connected-user list | — | V | V | V | V | V | V | V | V | REQ-SES-011 | -| S-04 | Control-authority indicator | — | V | V | V | V | V | V | V | V | REQ-SES-011 | -| S-05 | Request control | — | H | A | H | H | H | H | D | H | REQ-SES-003 | -| S-06 | Release control | — | H | H | A | A | D | C | D | D | REQ-SES-012 | -| S-07 | Safe state / stop all | — | H | D | A | A | A | C | D | A | REQ-SAF-001 | -| S-08 | Acknowledge error / recover | — | H | D | H | H | H | H | A | H | REQ-DEV-005 | -| S-09 | Stale-state banner | — | V | H | H | H | H | H | H | H | INV-005 | - -### 7.2 Devices - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| D-01 | Device list / status | — | V | V | V | V | V | V | V | V | REQ-DEV-002 | -| D-02 | Initialise / start device | — | H | D | A | D | D | D | A | D | REQ-DEV-001 | -| D-03 | Stop device | — | H | D | C | C | D | D | D | D | REQ-DEV-001 | -| D-04 | Reload device configuration | — | H | D | C | D | D | D | C | D | REQ-DAT-002 | - -### 7.3 Camera and imaging - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| C-01 | Select live-view camera | `camera_parameters` | H | D | A | A | A | D | D | A | REQ-IMG-002 | -| C-02 | Start live view | `camera_parameters` | H | D | A | H | A | D | D | H | REQ-IMG-002 | -| C-03 | Stop live view | `camera_parameters` | H | D | H | A | A | D | D | A | REQ-IMG-002 | -| C-04 | Snapshot | `io` | H | D | A | A | D | D | D | A | REQ-IMG-004 | -| C-05 | Camera name | `camera_parameters` | H | D | A | A | A | D | D | D | REQ-CAM-001 | -| C-06 | Exposure time (slider + entry) | `camera_parameters` | H | D | A | A | A | D | D | A | REQ-CAM-002 | -| C-07 | Gain (slider + entry) | `camera_parameters` | H | D | A | A | A | D | D | A | REQ-CAM-003 | -| C-08 | Frame rate (slider + entry) | `camera_parameters` | H | D | A | A | A | D | D | A | REQ-CAM-004 | -| C-09 | Channel weighting | — | H | H | H | A | H | H | H | A | REQ-CAM-005 | -| C-10 | Live image canvas | — | V | V | H | V | V | V | H | V | REQ-IMG-002 | -| C-11 | Frame-drop / rate indicator | — | V | V | H | V | V | V | H | V | REQ-IMG-012 | -| C-12 | Video-plane (SFU) connection indicator | — | V | V | V | V | V | V | V | V | IF-CM-012 | - -**Note on C-06…C-08:** these rows are subject to G1 per camera. A camera that cannot set frame rate -renders no C-08 control **for that camera**, while another camera in the same GUI may render it -(`REQ-CAM-007`). - -### 7.4 Illumination - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| L-01 | Light source on/off | `illumination` | H | D | A | A | A | D | D | A | REQ-LGT-001 | -| L-02 | Light source power (slider + entry) | `illumination` | H | D | A | A | A | D | D | A | REQ-LGT-002 | -| L-03 | Light source identity / wavelength | — | V | V | V | V | V | V | V | V | REQ-LGT-005 | -| L-04 | Emission indicator | — | V | V | V | V | V | V | V | V | NFR-SAF-003 | - -**L-02 is additionally gated by G9:** hidden whenever the corresponding source is off -(`REQ-LGT-003`). This is the reference example of a value gate. - -### 7.5 Stage - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| T-01 | Jog / step buttons (± per axis) | `stage_position` | H | D | A | A | A | D | D | A | REQ-STG-001 | -| T-02 | Position display (X/Y/Z) | — | V | V | V | V | V | V | V | V | REQ-STG-006 | -| T-03 | Absolute position entry + confirm | `stage_position` | H | D | A | A | D | D | D | A | REQ-STG-009 | -| T-04 | Home (separate menu) | `stage_position` | H | D | C | C | D | D | D | D | REQ-STG-004/005 | -| T-05 | Stop motion | `stage_position` | H | D | H | H | A | H | H | A | REQ-STG-011 | -| T-06 | Jog ↔ step mode switch | `stage_position` | H | D | A | A | D | D | D | A | REQ-STG-008 | -| T-07 | Step size (separate menu) | `stage_position` | H | D | A | A | D | D | D | A | REQ-STG-007 | -| T-08 | Velocity (separate menu) | `stage_position` | H | D | A | A | D | D | D | D | REQ-STG-007 | -| T-09 | Acceleration (separate menu) | `stage_position` | H | D | A | A | D | D | D | D | REQ-STG-007 | -| T-10 | Jerk (separate menu) | `stage_position` | H | D | A | A | D | D | D | D | REQ-STG-007 | -| T-11 | Travel-limit display | — | V | V | V | V | V | V | V | V | REQ-STG-010 | - -**Per-axis G1:** an axis the controller does not expose renders no T-01 control for that axis -(`REQ-STG-003`). Likewise T-08…T-10 appear only for parameters the controller actually supports -(`REQ-STG-007`) — a controller without jerk control shows no jerk field, ever. - -### 7.6 Optics - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| O-01 | Objective selection | `objective` | H | D | A | C | D | D | D | D | REQ-OPT-001 | -| O-02 | Filter selection | `filter_bank` | H | D | A | A | A | D | D | D | REQ-OPT-002 | -| O-03 | Rotator control | ***UNCLEAR*** `OP-012` | H | D | A | A | A | D | D | D | REQ-OPT-003 | -| O-04 | Light-path view | — | V | V | V | V | V | V | V | V | REQ-OPT-004 | - -**O-01 is `C` in M3:** changing the objective during live view abruptly changes magnification, focus -and field of view. It is permitted, but confirmed. -**O-01/O-02 are `D` in M7:** calibration is objective- and filter-specific; changing either mid-run -invalidates the transform. - -### 7.7 Procedures - -| # | GUI function | Locks | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| P-01 | Z-scan — configure | `stage_position`,`camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-001 | -| P-02 | Z-scan — start | as P-01 | H | D | A | A | D | D | D | D | REQ-PRC-001 | -| P-03 | Autofocus — select measure | `camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-003 | -| P-04 | Autofocus — start | `stage_position`,`camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-003 | -| P-05 | Focus lock on/off | `stage_position` | H | D | A | A | D | D | D | D | REQ-PRC-005 | -| P-06 | Overview image — start | `stage_position`,`camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-007 | -| P-07 | Calibration — manual mode | `stage_position`,`objective` | H | D | A | D | D | D | D | A | REQ-PRC-011 | -| P-08 | Calibration — automatic mode | `stage_position`,`objective` | H | D | A | D | D | D | D | H | REQ-PRC-011 | -| P-09 | Calibration — set landmark | `stage_position` | H | D | H | H | H | H | H | A | REQ-PRC-009 | -| P-10 | Calibration — accept / reject result | `objective` | H | D | H | H | H | H | H | A | REQ-PRC-012 | -| P-11 | XY stitching — configure & start | `stage_position`,`camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-014 | -| P-12 | Time series — configure & start | `camera_parameters` | H | D | A | A | D | D | D | D | REQ-PRC-017 | -| P-13 | Sample-area definition | `stage_position` | H | D | A | A | D | D | D | D | REQ-PRC-018 | -| P-14 | Sample-area fine adjustment (power-up) | `stage_position` | H | D | A | D | D | D | D | D | REQ-PRC-019 | -| P-15 | Abort running procedure | — | H | D | H | H | H | A | D | A | REQ-PRC-020 | -| P-16 | Procedure progress display | — | V | V | H | H | H | V | V | V | REQ-PRC-021 | - -**P-01…P-14 are `D`, not `H`, in M5.** A user who cannot start a second Z-scan while the first is -running must still be able to see that the function exists — per `INV-004`, the restriction is a -property of the moment. - -### 7.8 Data and history - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| X-01 | Acquired-data viewer | — | V | V | V | V | V | V | V | V | REQ-DAT-001 | -| X-02 | Export / download dataset | `io` | H | A | A | A | A | A | A | A | REQ-DAT-006 | -| X-03 | Clear acquired data | `expanse_state` | H | D | C | D | D | D | D | D | REQ-DAT-005 | -| X-04 | Browse historical states | — | V | V | V | V | V | V | V | V | REQ-HIS-001 | -| X-05 | Re-apply a historical state | all device locks | H | D | C | D | D | D | D | D | REQ-HIS-003 | -| X-06 | Storage-capacity indicator | — | V | V | V | V | V | V | V | V | REQ-DAT-008 | - -**X-02 is `A` in M1:** downloading data does not command the instrument, so an observer may do it. -This is the only device-adjacent function an observer can act on — confirm against `OP-004`. -**X-05 is `C` and ***UNCLEAR***:** re-applying a state may switch lasers on. Blocked on `OP-008` and -`OP-013`. - -### 7.9 Diagnostics - -| # | GUI function | Lock | M0 | M1 | M2 | M3 | M4 | M5 | M6 | M7 | Requirement | -|---|---|---|---|---|---|---|---|---|---|---|---| -| G-01 | Log viewer | — | V | A | A | A | A | A | A | A | REQ-LOG-001 | -| G-02 | Log level selection | — | H | D | A | A | A | A | A | A | REQ-LOG-004 | -| G-03 | Settings view (read) | — | V | V | V | V | V | V | V | V | REQ-DAT-002 | -| G-04 | Settings edit | — | H | D | C | D | D | D | D | D | REQ-DAT-002 | -| G-05 | Error / notification area | — | V | V | V | V | V | V | V | V | REQ-LOG-005 | - ---- - -## 8. Capability keys - -G1 requires the backend to state, per device instance, which functions it supports. This is the -capability descriptor of `REQ-DEV-003`. Minimum key set implied by the matrix: - -| Subsystem | Capability keys | -|---|---| -| Camera | `exposure`, `gain`, `framerate`, `channel_weighting`, `roi`, `binning`, `trigger` — each with `readable`, `writable`, `min`, `max`, `step`, `unit` | -| Stage | per axis: `present`, `jog`, `absolute`, `home`, `velocity`, `acceleration`, `jerk`, `limit_min`, `limit_max`, `unit` | -| Illumination | per source: `switchable`, `power`, `min`, `max`, `unit`, `wavelength` | -| Objective | `slots`, `switchable`, per slot: `magnification`, `na`, `pixel_size` | -| Filter bank | per bank: `slots`, `switchable`, per slot: `name`, `band` | -| Rotator | `axes`, `absolute`, `continuous`, `min`, `max`, `unit` — ***UNCLEAR***, `OP-014` | - -**INV-009** — The capability descriptor is the **only** source for G1. The frontend shall not infer -support from a device name, model string or hardcoded table. - -**INV-010** — A capability descriptor shall report the *device's* limits, not the *GUI's* preferences -(`REQ-CAM-008`). - ---- - -## 9. Open points arising from this matrix - -| ID | Question | Affected cells | -|---|---|---| -| OP-028 | May the safe-state / stop-all control (S-07) be operated by an **observer** (`auth = observer`)? Physical-safety argument says yes; control-arbitration argument says no. `INV-007` currently says `D` in M1 | S-07 / M1 | -| OP-029 | During a procedure that outlives its user (`REQ-SES-008`), which mode applies — M5 with no holder, or a new mode M8 UNATTENDED? | all M5 cells | -| OP-030 | Is snapshot (C-04) permitted during stage motion (M4)? Currently `D` on image-quality grounds; a user tracking a moving sample may disagree | C-04 / M4 | -| OP-031 | Should M4 MOVING be a distinct mode at all, or is it a per-control lock state? A CAN-bus stage may report motion per axis, allowing Y control while X moves | all M4 cells | -| OP-032 | Are rotators (O-03) covered by the `filter_bank` lock or do they need their own lock in the backend vocabulary? | O-03 | - -These are registered in `DOC/OpenPoints.md` §5 with full context and recommendations. - ---- - -## 10. Verification - -| Method | What it proves | -|---|---| -| **I** | Every GUI function in the implementation appears as a row here; no row is unimplemented and no control is unlisted | -| **T** | Automated test per row: force the mode, assert the rendered state matches the cell. This matrix is directly executable as a test table | -| **D** | `ACC-04` in `SRS_V1.md` §4.3 — a camera without frame-rate control shows no frame-rate control | - -**Recommendation:** generate the frontend gating from `InvariantMatrix.csv` rather than hand-writing -it. The file is already the shape of a lookup table, and the backend already publishes the lock -vocabulary and (once `REQ-DEV-003` exists) the capability descriptors that G1 and G7 need. diff --git a/SRS/KeyFeatureOverview.ods b/SRS/KeyFeatureOverview.ods deleted file mode 100644 index ddb0060..0000000 Binary files a/SRS/KeyFeatureOverview.ods and /dev/null differ diff --git a/SRS/MVP_TASKS.md b/SRS/MVP_TASKS.md deleted file mode 100644 index c092c0b..0000000 --- a/SRS/MVP_TASKS.md +++ /dev/null @@ -1,313 +0,0 @@ -# Path to a Minimum Viable Product — newswitch - -**Document ID:** MVP_TASKS -**Version:** 1.0 (draft) -**Date:** 2026-07-29 -**Derived from:** `DOC/SRS_V1.md` (target) and `DOC/InitReview.md` (Version 1.0, current state) -**Constraint:** the architecture of `basicRequirements.md` §Base architectural setup is preserved; -no task below changes the backend/frontend split, the routines → managers → protocols layering, the -DI model, the control-plane transport structure or the generated-client contract. - -**One exemption is in force:** `DEC-001` (2026-07-29) replaces the **video plane** with -LiveKit/WebRTC/VP8, explicitly waiving the freeze for that subsystem only (`CON-ARC-002-E1`). - ---- - -## 1. What the MVP is - -**One operator, at a browser, controls one real UC2 microscope and gets data off it.** - -Concretely — the MVP is reached when this scenario runs end to end on real hardware: - -> Start the backend on the Pi. Open the GUI in Chrome. See that the camera, stage and illumination are -> connected. Start the live view. Adjust exposure and gain — and see no frame-rate slider, because -> this camera has no frame-rate control. Switch on a light source and set its power. Jog the stage in -> X, Y and Z; type an absolute Z position and watch the stage move there. Take a snapshot; the file -> lands on the operator's machine. Run a Z-scan over 20 planes; the images and their metadata are -> stored and viewable. Open a second browser: it can watch but not touch. Close the first browser: -> the second gains control. Close both: the lasers go off and the stage stops. - -The live view in that scenario is delivered over **LiveKit/WebRTC/VP8** (`DEC-001`), which means an -SFU is part of the MVP deployment — not a later addition. - -**The MVP deliberately excludes** (see §6): stitching, focus lock, time series, DAHENG cameras, the -overview camera, and historical-state re-application. - ---- - -## 2. Task format - -| Field | Meaning | -|---|---| -| **ID** | `T-nnn`, stable | -| **Effort** | `S` ≤ 1 day · `M` 2–4 days · `L` 1–2 weeks · `XL` > 2 weeks | -| **Deps** | tasks that must complete first | -| **Req** | requirements from `SRS_V1.md` this task satisfies | -| **DoD** | definition of done — the observable result | - ---- - -## 3. Phase 0 — Restore a working tree - -**Nothing else can be verified until this phase is green.** All four tasks come from -`InitReview.md` §5.1 and §7. - -| ID | Task | Effort | Deps | Req | DoD | -|---|---|---|---|---|---| -| **T-001** | Restore `newswitch/uc2_devices/__init__.py` (or equivalent) and re-sync the editable install so the package imports | S | — | NFR-MNT-006 | `python -c "import newswitch.app"` succeeds | -| **T-002** | Make the `Uc2DetectorManager` import in `app.py` lazy, so a hardware-driver failure cannot break virtual mode at import time | S | T-001 | REQ-DEV-007 | Backend starts in virtual mode with the UC2 driver module deliberately broken | -| **T-003** | Get the backend test suite running and green again | M | T-001 | NFR-MNT-006 | `uv run pytest` collects and passes; CI backend job green | -| **T-004** | Restore lint/format cleanliness: `ruff format` + `ruff check --fix` over the UC2 files, clear the remaining errors by hand, move `extend-select` under `[tool.ruff.lint]` | S | T-001 | NFR-MNT-004 | `ruff check` and `ruff format --check` clean; no deprecation warning | -| **T-005** | Confirm the frontend codegen drift check passes against the restored backend | S | T-003 | IF-SW-003, NFR-CMP-001 | `just drift-check` green; `codegen-drift` CI job green | -| **T-006** | **Decide the fate of the native layer** and make the tree consistent with the decision: either restore the C++ ring buffer, bindings and tests, or drop `scikit-build-core` and move the native sections of `uc2_devices/README.md` into a design doc marked "planned" | M | T-001 | NFR-MNT-005, REQ-IMG-007 | No documentation describes absent code; `uv build` succeeds and does what the docs say | - -> **Blocked on a decision:** T-006 depends on `OP-011` (is 500 MB/s per camera or aggregate?). If the -> real sustained rate is well below 500 MB/s, a Python-level buffer may carry the MVP and the native -> layer can be deferred. **Answer `OP-011` before starting T-006.** - -**Phase exit criterion:** backend imports, tests pass, lint clean, codegen drift clean, and the tree's -documentation matches the tree's contents. - ---- - -## 4. Phase 1 — Foundations - -These are the pieces every later task depends on. Building them after the device work means retrofitting -them into the device work. - -| ID | Task | Effort | Deps | Req | DoD | -|---|---|---|---|---|---| -| **T-010** | **Logging facility** as a top-level backend tool: fatal/error/warning/info/debug, timestamp + level + module + user + device, level configurable at start-up | M | T-001 | REQ-LOG-001…004, CON-DEV-006 | Every layer logs through it; level set from config without code change | -| **T-011** | **Grow `ImswitchConfig`**: move `port`, `baudrate`, `base_path`, `settings_path` and travel limits out of call sites into config; resolve `uc2DevSettings.json` relative to the package, not the CWD | M | T-001 | IF-HW-007, REQ-DAT-004, NFR-FLX-002 | No hardcoded device paths remain; starting from any CWD works identically | -| **T-012** | Make a missing settings file at an explicitly configured path a hard error rather than silent empty defaults | S | T-011 | REQ-DEV-008 | Wrong path → clear startup error naming the path, not "no cameras found" | -| **T-013** | **Capability descriptor** — define the schema and publish it per device instance (parameter, readable/writable, min, max, step, unit) | L | T-001 | REQ-DEV-003, NFR-FUN-001 | Every manager returns a descriptor; schema published to the frontend; keys per `InvariantMatrix.md` §8 | -| **T-014** | **Safe state** — implement `enter_safe_state()`: all illumination off, all stage motion stopped, logged with cause | M | T-010 | REQ-SAF-001…003, NFR-SAF-001 | Callable from shutdown, last-user-disconnect and unrecoverable device error; verified in virtual mode | -| **T-015** | Call `detector.shutdown()` from `release_managers`; release every SDK camera handle on shutdown | S | T-001 | REQ-DEV-006, NFR-REL-004 | Restart cycle on hardware leaks no handles | -| **T-016** | Add pacing/sleep to both `acquire_live()` loops; pace against exposure time; no busy-spin with no active detector | S | T-001 | REQ-IMG-011, NFR-PER-007 | Idle backend CPU < 5 % of one core on the Pi | -| **T-017** | Confine `/files/{file_path:path}` to `base_path` using the traversal check `cache.py` already implements | S | T-001 | NFR-SEC-001 | Traversal attempt returns 404/403; regression test added | -| **T-018** | Make CORS origins configurable; closed list as the deployed default | S | T-011 | NFR-SEC-002, IF-CM-006 | Wildcard only under an explicit development flag | -| **T-019** | Retire the three inconsistent live-view URL conventions of `InitReview.md` §4.4 — the dead `/ws/video` default in `StreamingView` and the hand-written H.264/Zstd texture hooks are superseded by `DEC-001`. `IF-CM-004` stays in force for the control and bulk-data endpoints that remain | S | T-001 | IF-CM-004 | No dead route references remain; one URL convention for the surviving endpoints | - -**Phase exit criterion:** configuration, logging, capability reporting and safe state exist and are used. - ---- - -## 5. Phase 2 — Device control and GUI gating - -| ID | Task | Effort | Deps | Req | DoD | -|---|---|---|---|---|---| -| **T-020** | **One concrete camera driver.** Restore or rewrite the HIK driver on top of [UC2_HikPy]; plus a `MockCamera` so the whole UC2 branch is unit-testable with no SDK present | L | T-006, T-013 | IF-HW-001, NFR-FUN-002 | Hardware path enumerates and opens a real HIK camera; CI exercises the UC2 branch via `MockCamera` | -| **T-021** | Fix the `enumerate()` contract mismatch between the ABC and `Uc2DetectorManager` (`list[Uc2Camera]` vs. `CameraInfo`) | S | T-020 | IF-HW-001 | One consistent contract; typed and tested | -| **T-022** | **Per-subsystem hardware/virtual selection** replacing the single `use_virtual_microscope` flag | M | T-011 | IF-SW-006 | Any subsystem can be real while others are virtual; the mixed configuration is a tested case | -| **T-023** | Wire the existing UC2 **illumination** path to hardware (currently virtual unconditionally) | M | T-022 | REQ-LGT-001…004 | Real light source switches on/off and sets power from the GUI | -| **T-024** | Verify and complete the UC2 **stage** path over CAN: jog, step, absolute, home, position read-back, velocity/acceleration/jerk where supported | L | T-022, T-013 | REQ-STG-001…013, IF-HW-003 | All stage requirements demonstrable on hardware | -| **T-025** | **Soft travel limits** enforced in the backend, independent of frontend validation; limits from configuration | M | T-011, T-024 | REQ-STG-010, NFR-SAF-002 | Out-of-range command rejected with a message; no motion occurs | -| **T-026** | **Frontend capability gating (G1/G2)** — build device panels from the capability descriptor; unsupported parameters are not rendered | L | T-013 | IF-UI-005, REQ-DEV-004, REQ-CAM-007, REQ-STG-003/007 | `ACC-04` passes: a camera without frame-rate control shows no frame-rate control | -| **T-027** | **Frontend mode gating (G3–G9)** — implement the gate chain of `InvariantMatrix.md` §5 as one shared mechanism, driven by the CSV table | L | T-026, T-013 | IF-UI-010, NFR-USA-002/003 | Every row of `InvariantMatrix.csv` has a passing test; disabled controls state their cause | -| **T-028** | Bidirectional sync review: every control reflects device-originated changes; rejected values revert to the device-reported value with a message | M | T-026 | IF-UI-006, REQ-CAM-009 | Change made from a second client appears in the first within 200 ms | -| **T-029** | Move the stage **home** control out of the direction-button cluster into a separate menu; move step size / velocity / acceleration / jerk into that menu | S | T-026 | REQ-STG-005, REQ-STG-007 | Layout matches `basicRequirements.md` §Stages | -| **T-030** | Units and labels pass: ms, dB, 1/s, µm on every numeric control; slider and numerical entry always consistent | S | T-026 | IF-UI-007/008, NFR-USA-005 | Inspection checklist complete | - -**Phase exit criterion:** a real camera, a real stage and a real light source are controllable from the -GUI, and the GUI shows only what they support. - ---- - -## 6. Phase 3 — Imaging path - -| ID | Task | Effort | Deps | Req | DoD | -|---|---|---|---|---|---| -| **T-039** | **Spike: VP8 encode feasibility on the Pi 5.** Measure software libvpx VP8 encode cost at candidate resolutions and frame rates, alongside a running acquisition. Output: the encoder operating point (resolution, frame rate, decimation) that meets the latency budget within the CPU budget | M | — | NFR-PER-009, ASM-08 | A measured table of resolution × frame rate × CPU × latency; a chosen operating point, or an escalation that the Pi cannot carry it | -| **T-047** | **Deploy the LiveKit server** as a component of the instrument stack: compose service, configuration, healthcheck, startup ordering, lifecycle alongside the backend | M | T-011 | IF-CM-009, IF-CM-011, DEP-08 | Backend publishes to a running SFU; server restarts cleanly with the stack | -| **T-048** | **LiveKit access tokens and room model**: backend mints a token per user per session; room naming per instrument/detector; token lifetime and renewal | M | T-047, T-060 | IF-CM-011, REQ-SES-010 | A user with no valid token cannot subscribe; a reconnecting user is re-issued a token without losing their place in the authority queue | -| **T-049** | **Frontend LiveKit subscriber**: `livekit-client`, track subscription, render into `