Skip to content

Latest commit

 

History

History
290 lines (224 loc) · 14.6 KB

File metadata and controls

290 lines (224 loc) · 14.6 KB

DOKU — smart-reset-browser

Last updated: 2026-07-14

Technical architecture and implementation reference.


Overview

FastAPI server running on localhost:8765. The browser is the UI — no desktop GUI framework required. All camera communication is HTTP; real-time UI updates via WebSocket.

web_main.py → uvicorn → web/app.py → camera_plugins/ + ndi/
                     ↕ WebSocket
                   browser (HTMX + Vanilla JS)

Project Structure

smart-reset-browser/
├── web_main.py              # Entry point: uvicorn + system tray + browser open
├── requirements.txt         # Python dependencies
│
├── core/
│   ├── interfaces.py        # CameraProtocol ABC, CameraModule Protocol
│   ├── exceptions.py        # CameraError, PluginError, ...
│   ├── models.py            # DiscoveredCamera, ResetResult, ResetContext
│   ├── registry.py          # PluginRegistry — loads modules and transports
│   └── reset_engine.py      # ResetEngine — dispatches run_reset(context)
│
├── camera_plugins/
│   ├── panasonic/
│   │   ├── transport.py     # HTTP CGI, UDP discovery, model detection (AW- and AK- series)
│   │   ├── base.py          # Shared helpers (incl. delete_presets())
│   │   ├── aw_*.py          # Per-model reset modules (AW- series)
│   │   └── ak_*.py          # Per-model reset modules (AK- series)
│   └── birddog/
│       ├── transport.py     # REST/JSON, parallel subnet scan, port 8080
│       ├── base.py          # Shared helpers
│       └── p*.py            # Per-model reset modules
│
├── smart_reset/
│   ├── camera_state.py      # CameraSession — singleton session state
│   └── discovery.py         # Panasonic UDP discovery
│
├── ndi/
│   ├── ndi_input.py         # NDI 6 SDK ctypes wrapper
│   └── scopes.py            # Waveform + vectorscope computation (BT.709)
│
├── lib/ndi/                 # Bundled Processing.NDI.Lib.x64.dll
│
└── web/
    ├── app.py               # All FastAPI routes
    ├── ws_manager.py        # WebSocketManager + WebSocketLogHandler
    ├── templates/
    │   ├── base.html        # Layout, all JS, modals
    │   ├── index.html       # Main page content
    │   └── partials/
    │       ├── camera_panel.html   # HTMX fragment — full camera controls
    │       └── feature_btn.html    # HTMX fragment — single toggle button
    └── static/
        └── style.css

State

CameraSession (smart_reset/camera_state.py) is the only runtime state container.
Stored as a singleton in app.state.session. Never duplicated elsewhere.

Key fields:

  • ip, port, connected, camera_id, session_id
  • reset_in_progress, preset_delete_in_progress, balance_in_progress, connect_in_progress, scan_in_progress
  • balance_token: int — incremented to cancel stale ABB/AWW completion polls
  • feature_states: dict[str, bool] — toggle button states
  • dropdown_selections: dict[str, str] — current dropdown selections (plugin modules)
  • c_temp_command_map, gamma_command_map, lmatrix_command_map: dict[str, str] — label→command maps for legacy color-temp/gamma/linear-matrix dropdowns, paired with c_temp_selection, gamma_selection, lmatrix_selection: str
  • discovered_cameras: list[dict] — results of last network scan

reset_connection() clears all connection-specific fields on disconnect/failed connect.


Routes

Method Path Response Description
GET / HTML Main page
GET /api/camera/panel HTML fragment Current camera panel
GET /api/camera/state JSON Session state (debug)
POST /api/camera/scan HTML fragment Start network scan
POST /api/camera/connect HTML fragment Connect (body: ip, port)
POST /api/camera/disconnect HTML fragment Disconnect
POST /api/camera/reset HTML fragment Start reset sequence
POST /api/camera/delete-presets HTML fragment Delete presets in range (Panasonic UE-series only)
POST /api/camera/feature/{key} HTML fragment Toggle feature (body: enabled)
POST /api/camera/cycle/{key} HTML fragment Advance an N-state cycle button to its next state
POST /api/camera/trigger/{key} HTML fragment One-shot command
POST /api/camera/dropdown/{key} HTML fragment Set dropdown (body: label)
POST /api/camera/balance/{key} HTML fragment Start ABB / AWW
POST /api/logging JSON Enable/disable file logging
GET /api/ndi/sources JSON NDI sources on the network
WS /ws/logs WebSocket Live logs + camera events
WS /ws/ndi?source=NAME&mode=parade WebSocket binary NDI video + scopes

WebSocket Events (/ws/logs)

All messages are JSON.

Event type Fields Trigger
log text Every log line
reset_done status, ok, failed Reset complete
preset_delete_done status, ok, failed Preset delete complete (own route/task, independent of reset — see Reset flow)
balance_done ABB/AWW complete
camera_connected ip, camera_id Successful connect
camera_disconnected Disconnect

Client reacts:

  • reset_done / balance_done / preset_delete_donehtmx.trigger('#camera-panel', 'refreshPanel')
  • camera_connectedndiSyncConnect(ip) if sync enabled
  • camera_disconnectedndiSyncDisconnect() if sync enabled

NDI Monitor (ndi/)

ndi_input.py

ctypes wrapper around Processing.NDI.Lib.x64.dll (NDI 6 SDK).
Loaded from lib/ndi/ — no SDK installation required.

Public API:

is_available() -> bool
list_sources(timeout_ms=50) -> list[dict]            # {name, ip} — reads a warm, process-lifetime finder
grab_frame(ndi_name, timeout_ms=5000) -> np.ndarray   # (H, W, 3) RGB uint8
NDIFrameStream(ndi_name)                               # context manager, continuous
encode_jpeg(frame, width=640, quality=70) -> bytes

list_sources() reads a live snapshot off a single NDIlib_find_v2 finder created once at module import and kept alive for the process lifetime, instead of creating/destroying a finder per call — cuts typical response time from up to 5s (cold discovery) to ~0.2s (warm snapshot). See CLAUDE.md's "NDI source discovery" section for details.

NDI connection takes ~3–4 seconds for the first frame — normal SDK behaviour.

scopes.py

All computations in BT.709 / Rec. 709.

Waveformwaveform(frame, mode, out_w=640, out_h=360) -> np.ndarray (H, W, 3)

Mode Description
"parade" R, G, B channels side by side (213 px each)
"overlay" R, G, B on the same axes
"luma" Y = 0.2126·R + 0.7152·G + 0.0722·B, grey trace

Algorithm: NumPy bincount column histogram — single pass, no loops.
Graticule at 0, 25, 50, 75, 100 IRE.

Vectorscopevectorscope(frame, out_size=512) -> np.ndarray (H, W, 3)

BT.709 YCbCr:

Cb = (−102·R − 346·G + 450·B) / 1024 + 128
Cr = (+450·R − 408·G −  40·B) / 1024 + 128

Graticule: saturation rings every 10 %, crosshair at neutral (128, 128),
75 % colour-bar target boxes for Y, Cy, G, Mg, R, B.

/ws/ndi binary protocol

Each WebSocket message is prefixed with 1 byte:

Prefix Content
0x01 Video JPEG (640×360, quality 70)
0x02 Waveform JPEG (640×360, quality 85)
0x03 Vectorscope JPEG (512×512, quality 85)

Client sends plain text ("parade" / "overlay" / "luma") to switch waveform mode without reconnecting.


Camera Plugin System

Plugin fields (per module)

Field Type Description
CAMERA_ID str Primary model identifier
CAMERA_ID_ALIASES list[str] Additional IDs resolved to this module
DISPLAY_NAME str Human-readable model name
PROTOCOL str "panasonic" or "birddog" (default: "panasonic")
UI_LAYOUT list[tuple] [(btn1, btn2, btn3, dropdown_key), ...] per row
UI_BALANCE_DROPDOWN str | None Dropdown key to render in the trailing slot of the ABB/AWW balance-button row, next to AWW (optional, default None)
UI_BUTTONS dict Toggle: {key: {"on": cmd, "off": cmd}} / Trigger: {key: {"cmd": cmd}} / Cycle: {key: {"cycle": [{"label": str, "cmd": [cmd, ...]}, ...]}}
UI_BUTTON_LABELS dict Display names
UI_BUTTON_CONDITIONS dict Trigger enable condition: {key: {"dropdown": k, "value": v}}
UI_BUTTON_DROPDOWN_SYNC dict {btn: {True: {dd: label}, False: {dd: label}}}
UI_DROPDOWN_CONDITIONS dict {dropdown_key: [button_key, ...]} — dropdown disabled unless at least one listed button's feature state is True
UI_LAYOUT dropdown order Convention (not a field): all Panasonic modules order their UI_LAYOUT dropdown column top-to-bottom as Gamma/Gamma Mode → Matrix Preset/Matrix Type → Linear Matrix → Color Temp, including only the types that module actually has. See CLAUDE.md's "Advanced Controls dropdown order" section.
UI_DROPDOWNS dict {key: [(label, cmd), ...]}
UI_FEATURE_QUERIES dict Poll current feature state on connect
UI_FEATURE_RESPONSE_MAP dict Parse poll response
UI_DROPDOWN_QUERIES dict Poll current dropdown on connect
UI_DROPDOWN_RESPONSE_MAP dict Parse poll response
SUPPORTS_PRESET_DELETE bool Panasonic UE-series + HE-series/HR140 (not AK-UB300) — enables the Delete Presets range/all controls + button next to Reset Camera; gates start_delete_presets() in web/app.py
SUPPORTS_PRESET_DELETE_NAME_THUMBNAIL bool UE-series only — also delete Preset Name/Thumbnail (OSJ:36/37/3A/3B), not just Preset Memory (#C[nn]); passed explicitly to base.delete_presets()
SUPPORTS_OSD_TOGGLE bool Panasonic only — stateful ON/OFF OSD button (DUS:1/DUS:0 + QUS query) via the generic feature-toggle machinery
SUPPORTS_OSD_TRIGGER bool BirdDog only — stateless "Toggle OSD" button (no query endpoint exists) via the generic trigger route
run_reset(context) function Optional custom reset flow

Plugin loader (optional matching-plugin extension)

web/app.py's on_startup checks the SMART_RESET_PLUGIN env var; if set, it adds that path to sys.path, imports a module named matching_plugin, and mounts matching_plugin.router on the app (app.include_router(...)). The plugin reads app.state.session/app.state.registry directly — no other core changes. Its routes (/api/matching/status, /api/matching/capture, /api/matching/correct) are not part of this repo's route table; they only exist when the env var is set.

Connect flow

  1. POST /api/camera/connect
  2. All transports attempt query_camera_id(ip, port) in parallel
  3. First response wins — camera module loaded from PluginRegistry
    • Unknown BirdDog model → falls back to _BirdDog_Generic
    • Unknown Panasonic model → returns an error; session stays disconnected
  4. Port corrected for transport if needed (BirdDog: 8080)
  5. _sync_feature_states() polls all feature/dropdown states in executor
  6. camera_connected WS event broadcast

Reset flow

  1. POST /api/camera/reset
  2. session.reset_in_progress = True; background task via ThreadPoolExecutor
  3. ResetEngine.run()module.run_reset(context) (plugin path) — clears reset_in_progress itself in its own finally block
  4. _sync_feature_states() re-polls camera
  5. reset_done WS event broadcast

Post-reset target feature states (POST_RESET_FEATURE_STATES per module) send every Panasonic camera to Auto Iris = ON and Auto Focus = ON after reset. See CLAUDE.md's "Post-reset target state" section for the AW-UE150A DRS/Knee corrections and why AK-UB300 has no Auto Focus entry.

Preset delete flow (independent of reset)

  1. POST /api/camera/delete-presets_parse_preset_range(form) reads the range/all fields; route returns an error fragment if the module lacks SUPPORTS_PRESET_DELETE
  2. session.preset_delete_in_progress = True; both reset_in_progress and preset_delete_in_progress are checked on entry to start_reset()/start_delete_presets() so the two actions can't overlap; background task via ThreadPoolExecutor
  3. base.delete_presets(transport, ip, port, start, end, should_abort, include_name_thumbnail) runs in the executor — include_name_thumbnail derived from SUPPORTS_PRESET_DELETE_NAME_THUMBNAIL
  4. session.preset_delete_in_progress = False (unless the session went stale in the meantime)
  5. preset_delete_done WS event broadcast with ok/failed/status

Threading Model

  • Asyncio event loop — FastAPI routes, WebSocket, state mutations
  • ThreadPoolExecutor (_executor, 4 workers) — all blocking I/O (HTTP, UDP, NDI)
  • Thread → loop callbacksws_manager.broadcast_from_thread() via asyncio.run_coroutine_threadsafe()
  • Stale worker protectionsession_id, connected, and ip/port all checked before state mutations in background workers; expected_sid is captured by the caller before submitting to the executor so the check cannot drift

Frontend

  • HTMX — all button/form interactions; server responds with HTML fragments
  • Jinja2 — server-side rendering; no client-side state
  • WebSocket — real-time logs and events; reconnects automatically on drop
  • NDI SynclocalStorage key ndiSyncCamera; auto-start/stop on camera_connected / camera_disconnected
  • Themes — dark/light, localStorage key theme
  • No build step — no npm, no bundler

File Logging

POST /api/logging — adds or removes a FileHandler on the root logger at runtime.
Log path: Path.home() / "smart-reset.log" → typically C:\Users\username\smart-reset.log.
Enabling shows a popup with the exact path and a mailto link to support.


PyInstaller Notes

  • sys.frozen check in web/app.py's on_startup — explicit frozen-module-name lists per registry.load_package(...) call, one pass each for Panasonic AW- (aw_*), Panasonic AK- (ak_*), and BirdDog (p*); web_main.py itself has no plugin-module knowledge
  • Templates, static files, and lib/ndi/ embedded via --add-data
  • Single-instance enforced via Windows named mutex
  • System tray via pystray; stdout/stderr redirected to /dev/null when frozen