diff options
| author | Luke Hoersten <[email protected]> | 2026-07-17 20:19:55 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-07-17 20:19:55 -0500 |
| commit | 8c2862b17b9bd9ae194d18fc912785f899524d8f (patch) | |
| tree | 4553cbdfc47dbc65e0a502f9bc671a2a94561067 | |
| parent | 903d1cf44e9e4b6a6733fbc5d405182b658e5d84 (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.md | 3 | ||||
| -rw-r--r-- | scrypted/PLUGIN-CONVERSION.md | 129 |
2 files changed, 131 insertions, 1 deletions
@@ -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. |
