src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-17 20:19:55 -0500
committerLuke Hoersten <[email protected]>2026-07-17 20:19:55 -0500
commit8c2862b17b9bd9ae194d18fc912785f899524d8f (patch)
tree4553cbdfc47dbc65e0a502f9bc671a2a94561067
parent903d1cf44e9e4b6a6733fbc5d405182b658e5d84 (diff)
scrypted: plan the Scripts-script -> real-plugin conversion
PLUGIN-CONVERSION.md captures the full plan so a future session can pick it up cold: why (the Scripts sandbox's reload-leak machinery — cleaner registry, drain ordering, name-drift workarounds — exists only because of the paste-deploy model), the port map (what moves 1:1, what changes, what dies), open questions to verify (orphaned ffmpeg on worker restart, storage attach at startup), the cutover order (never run script + plugin together), and the verification suite. Linked from the README's Related docs and Status table.
-rw-r--r--README.md3
-rw-r--r--scrypted/PLUGIN-CONVERSION.md129
2 files changed, 131 insertions, 1 deletions
diff --git a/README.md b/README.md
index da29976..b10d08e 100644
--- a/README.md
+++ b/README.md
@@ -25,13 +25,14 @@ Scrypted Viewport owns Ethernet, JPEG decode, display, touch input and outbound
- [`TESTING.md`](TESTING.md) — **the self-contained verification reference.** Status snapshot for every milestone, hardware prerequisites + open unknowns, bench-order playbook, per-milestone `curl` recipes, integration test suite. Start here before flashing anything.
- [`scrypted/README.md`](scrypted/README.md) — Scrypted-side script install + per-viewport binding UI (Path B with camera picker and mDNS auto-resolve).
+- [`scrypted/PLUGIN-CONVERSION.md`](scrypted/PLUGIN-CONVERSION.md) — TODO: plan for repackaging the script as a real installable Scrypted plugin (kills the Scripts-sandbox reload-leak machinery, makes deploys scriptable).
## Status
| Layer | Where we are | What's pending |
| --- | --- | --- |
| Firmware (`main/`) | Full path ✅ on hardware: Ethernet, panel, streaming, OTA. Painted = sent = 24 fps at the Unifi medium substream, sub-50 ms glass-to-glass over a raw TCP data socket (:81). Binary ~900 KB, ESP-IDF 5.4 / `esp32p4`. See [What's next](#whats-next) for the measured budget. | Backlog only (task-watchdog counters, multi-camera per viewport, production sealing). |
-| Scrypted side (`scrypted/`) | v1 Script — DeviceProvider with per-viewport child devices, camera picker, and live ffmpeg MJPEG streaming over the TCP data socket with prebuffer fast-start (~0.7 s wake-to-video). Verified end-to-end. | v2: repackage the single-file script as an installable plugin. |
+| Scrypted side (`scrypted/`) | v1 Script — DeviceProvider with per-viewport child devices, camera picker, and live ffmpeg MJPEG streaming over the TCP data socket with prebuffer fast-start (~0.7 s wake-to-video). Verified end-to-end. | v2: repackage the single-file script as an installable plugin — planned in [`scrypted/PLUGIN-CONVERSION.md`](scrypted/PLUGIN-CONVERSION.md). |
| Hardware | Ethernet pin map confirmed (Waveshare wiki + ESPHome). Hosyond panel architecture confirmed (Pi 7"-style, TC358762 bridge + ATTINY MCU at I²C `0x45`). Jumper wiring documented. | Schematic confirmation needed for DSI FPC pin count, I²C GPIO mapping, BOOT button GPIO, flash size. All gated by [`TESTING.md`'s Hardware prerequisites](TESTING.md#hardware-prerequisites). |
See [`TESTING.md`](TESTING.md) for the full milestone-by-milestone status and the bench-order playbook.
diff --git a/scrypted/PLUGIN-CONVERSION.md b/scrypted/PLUGIN-CONVERSION.md
new file mode 100644
index 0000000..084b83b
--- /dev/null
+++ b/scrypted/PLUGIN-CONVERSION.md
@@ -0,0 +1,129 @@
+# Plan: convert the Scripts-plugin script into a real Scrypted plugin
+
+Status: **planned, not started.** The script (`scrypted-viewport.ts`, deployed
+by pasting into Scrypted's Scripts editor) is fully working as of v1.4.0 —
+this conversion is a structural investment, not a bug fix. Written so a
+future session can pick it up cold.
+
+## Why
+
+The Scripts sandbox does not tear down a previous load's resources before
+constructing a new instance. Most of the script's hardest-won machinery
+exists *only* to survive that:
+
+- the `globalThis.__viewportShutdownCleaners` registry + drain-at-bootstrap
+ ordering (a re-paste must kill the prior load's ffmpeg children, sockets,
+ intervals, and camera listeners)
+- listener-map invalidation inside the cleaners (so a drain by another
+ instance can't strand `attachedListenerSig`/`listeners` claiming live
+ registrations)
+- the `display_name`-is-canonical scheme (v.name drifts to the nativeId on
+ script reload)
+- the storage-attach race on reload (root cause of the historical
+ doorbell-wake bug) and the register-cycle self-healing re-attach
+- `SCRIPT_VERSION` stamping + manual paste as the whole deploy story
+
+A real plugin runs in its own worker process that Scrypted kills and
+restarts on deploy: the cross-reload leak class disappears, and deploys
+become scriptable (`npx scrypted deploy`) — no more paste step, and agent
+sessions can ship plugin changes end-to-end like they already do firmware.
+
+Secondary wins: real `import sdk from '@scrypted/sdk'` with actual types
+(today everything is `declare const` + `type Foo = any`), npm deps become
+possible (not currently needed), per-device consoles.
+
+## Shape
+
+- New top-level `plugin/` package (scaffold: `npx scrypted create` or copy
+ the @scrypted/plugin template; webpack build → `dist/plugin.zip`).
+ Keep `scrypted/scrypted-viewport.ts` frozen and working until cutover is
+ verified, then delete it and fold `scrypted/README.md` install docs into
+ the plugin's.
+- Not published to npm — deployed straight at the LAN server:
+ `npx scrypted login <server>` once, then `npx scrypted deploy <server>`.
+- `Makefile`: add `make plugin` (build + deploy). Retire the
+ `SCRIPT_VERSION` stamp convention; `package.json` version + git replace it.
+
+## Port map (what moves, what changes, what dies)
+
+Moves ~1:1 (the classes already implement the right interfaces):
+- `ScryptedViewportProvider` (DeviceProvider, DeviceCreator,
+ HttpRequestHandler, Settings, StartStop) and `Viewport` (Settings)
+- the whole stream pipeline (ffmpeg spawn via `child_process`, JPEG demux,
+ raw TCP :81 framing, skip-oldest backpressure, streamLogger)
+- mDNS discovery (`mdnsBrowse` + wire codec — keep the hand-rolled
+ dependency-free version; it's proven against real firmware responses),
+ host comboboxes, auto-heal, mac seeding
+- wake/sleep + trigger-scoped `attachListener`/`handleCameraEvent`
+
+Changes:
+- `declare const` globals → `import sdk, { ScryptedDeviceBase,
+ ScryptedInterface, ScryptedDeviceType, ... } from '@scrypted/sdk'`;
+ `systemManager` etc. become `sdk.systemManager` etc.
+- Replace the `type X = any` aliases with real SDK types incrementally —
+ start loose, tighten after the port compiles.
+- Drop the bootstrap `onDeviceDiscovered` hack that overrides the Scripts
+ plugin's hardcoded `Unknown` device type — the plugin's `package.json`
+ `scrypted` section declares type/interfaces properly.
+- `HttpRequestHandler` endpoint URL changes (new plugin id in the path).
+ No firmware change needed: the 5-minute `registerViewport` cycle POSTs
+ the new callback base via `/config` automatically.
+
+Dies (the point of the exercise):
+- `pushShutdownCleaner`/`drainShutdownCleaners` and everything that exists
+ to serve them. KEEP the register-cycle `attachListener` self-heal (cheap
+ idempotent belt-and-braces) and `display_name`-as-canonical (harmless,
+ and protects against any residual name races).
+- `SCRIPT_VERSION`.
+
+## Open questions to verify during the port
+
+1. **Orphaned ffmpeg on plugin restart.** The worker process is killed on
+ deploy; spawned ffmpeg children survive parent death but should die on
+ the next stdout write (EPIPE) since their pipe consumer is gone. Verify
+ with a deploy mid-stream (`ps` for ffmpeg after). If they linger, add a
+ `process.on('exit')`-adjacent kill or track children and SIGTERM them
+ in a StartStop.stop path.
+2. **Storage race**: confirm `deviceManager.getDeviceStorage` is reliably
+ attached at plugin startup for pre-existing children (expected: yes —
+ this was a Scripts-sandbox artifact). If yes, the "no camera assigned /
+ no host" boot warnings disappear.
+3. Settings UI parity: `combobox`/`choices`, `deviceFilter`, multi-select
+ triggers, one-shot action toggles — all standard Setting fields, expect
+ no changes; eyeball each page.
+4. `endpointManager.getInsecurePublicLocalEndpoint(nativeId)` behavior in
+ a real plugin (should be identical).
+
+## Cutover checklist (order matters)
+
+1. Deploy the plugin; do NOT bind any viewports yet.
+2. STOP the old script via its Scripts-UI Stop (drains every listener /
+ stream / interval — verified working), then delete the script device.
+ Never run both live: two instances = duplicate camera listeners =
+ duplicate streams to one panel (known failure mode).
+3. Recreate the kitchen viewport in the plugin: "+ Add Device", pick the
+ discovered host (name auto-fills from mDNS TXT), pick camera, set
+ triggers. (Only one device exists today; storage migration is not worth
+ building.)
+4. Confirm firmware `/config` shows the NEW callback URL after the first
+ register (or force it with a settings save).
+5. Run the verification suite below.
+
+## Verification suite (same bar as the script)
+
+- boot log clean, no storage-race warnings
+- doorbell press → wake + stream (`event BinarySensor ... (wake)`)
+- panel tap → `recv "kitchen" -> wake (device-initiated)` + stream, full
+ brightness, 204 on the device side (`state_post_failures` stays 0)
+- manual Wake/Sleep toggles from settings
+- rename via "Viewport name" → firmware `/config` + mDNS hostname follow
+- auto-heal: set host to a wrong IP → `mdns auto-heal` log line corrects it
+- deploy mid-stream → old ffmpeg exits (no orphan), next event streams
+- steady-state stream health: painted ≈ 24 fps, g2g ~60-100 ms, `/state`
+ stream window sane (see TESTING.md for the sampling loop)
+
+## Estimate
+
+Roughly one focused session: scaffold + mechanical port + build tooling in
+the first half; cutover + the verification suite in the second. Firmware
+is untouched throughout.