Last updated: 2026-07-14
Technical architecture and implementation reference.
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)
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
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_idreset_in_progress,preset_delete_in_progress,balance_in_progress,connect_in_progress,scan_in_progressbalance_token: int— incremented to cancel stale ABB/AWW completion pollsfeature_states: dict[str, bool]— toggle button statesdropdown_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 withc_temp_selection,gamma_selection,lmatrix_selection: strdiscovered_cameras: list[dict]— results of last network scan
reset_connection() clears all connection-specific fields on disconnect/failed connect.
| 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 |
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_done→htmx.trigger('#camera-panel', 'refreshPanel')camera_connected→ndiSyncConnect(ip)if sync enabledcamera_disconnected→ndiSyncDisconnect()if sync enabled
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) -> byteslist_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.
All computations in BT.709 / Rec. 709.
Waveform — waveform(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.
Vectorscope — vectorscope(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.
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.
| 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 |
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.
POST /api/camera/connect- All transports attempt
query_camera_id(ip, port)in parallel - 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
- Unknown BirdDog model → falls back to
- Port corrected for transport if needed (BirdDog: 8080)
_sync_feature_states()polls all feature/dropdown states in executorcamera_connectedWS event broadcast
POST /api/camera/resetsession.reset_in_progress = True; background task viaThreadPoolExecutorResetEngine.run()→module.run_reset(context)(plugin path) — clearsreset_in_progressitself in its ownfinallyblock_sync_feature_states()re-polls camerareset_doneWS 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.
POST /api/camera/delete-presets—_parse_preset_range(form)reads the range/all fields; route returns an error fragment if the module lacksSUPPORTS_PRESET_DELETEsession.preset_delete_in_progress = True; bothreset_in_progressandpreset_delete_in_progressare checked on entry tostart_reset()/start_delete_presets()so the two actions can't overlap; background task viaThreadPoolExecutorbase.delete_presets(transport, ip, port, start, end, should_abort, include_name_thumbnail)runs in the executor —include_name_thumbnailderived fromSUPPORTS_PRESET_DELETE_NAME_THUMBNAILsession.preset_delete_in_progress = False(unless the session went stale in the meantime)preset_delete_doneWS event broadcast withok/failed/status
- Asyncio event loop — FastAPI routes, WebSocket, state mutations
- ThreadPoolExecutor (
_executor, 4 workers) — all blocking I/O (HTTP, UDP, NDI) - Thread → loop callbacks —
ws_manager.broadcast_from_thread()viaasyncio.run_coroutine_threadsafe() - Stale worker protection —
session_id,connected, andip/portall checked before state mutations in background workers;expected_sidis captured by the caller before submitting to the executor so the check cannot drift
- 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 Sync —
localStoragekeyndiSyncCamera; auto-start/stop oncamera_connected/camera_disconnected - Themes — dark/light,
localStoragekeytheme - No build step — no npm, no bundler
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.
sys.frozencheck inweb/app.py'son_startup— explicit frozen-module-name lists perregistry.load_package(...)call, one pass each for Panasonic AW- (aw_*), Panasonic AK- (ak_*), and BirdDog (p*);web_main.pyitself 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/nullwhen frozen