<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/scrypted, branch main</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=main</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-07-18T01:25:24+00:00</updated>
<entry>
<title>docs: sync README + TESTING + scrypted README with v1.4.0 reality</title>
<updated>2026-07-18T01:25:24+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:25:24+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=482d8f45f18c6cc880bf2dfbbb70e3a4d704ac44'/>
<id>urn:sha1:482d8f45f18c6cc880bf2dfbbb70e3a4d704ac44</id>
<content type='text'>
Drift accumulated across the review-fix, mDNS-discovery, and tooling
work:

- Discovery: the design-era "use a Node mDNS library" flow is replaced
  with what shipped — the sandbox has no such libraries; the script does
  a dependency-free legacy-unicast dgram browse (no :5353 bind), host
  dropdowns, blank-name inheritance, and auto-heal on register failure
  (matched by the new mac TXT record) instead of periodic re-browsing.
- /state example: add mac, ota_state, panel dims, tear_guard_engaged,
  temp_c, and the stream stats object; correct `configured` semantics
  (scrypted URL only) and the never-null MAC-derived name in /config.
- /config: document the 54-char name cap (mDNS label limit).
- scrypted/README: add-device + settings tables gain the discovered-host
  dropdown, Wake triggers on create, and the canonical "Viewport name"
  rename field (the Scrypted pencil doesn't propagate — by design);
  trigger-scoped subscriptions; device-wake immediate-204 semantics;
  idle=0 no longer force-slept by the safety timer; "manual IP" dropped
  from limitations; discovery section reframed with the diagnostic.ts
  probe and bridge-networking caveat.
- Source map: dedupe the doubled touch.{h,c} row, add stream_server /
  ota / chip_temp rows, fix nvs_config + endpoint-count drift.
- Build/TESTING: point iteration at `make ota` (USB stays for first
  flash); TXT expectations include mac; DHCP-renumber failure mode now
  documents the verified auto-heal path.
</content>
</entry>
<entry>
<title>scrypted: plan the Scripts-script -&gt; real-plugin conversion</title>
<updated>2026-07-18T01:19:55+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:19:55+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=8c2862b17b9bd9ae194d18fc912785f899524d8f'/>
<id>urn:sha1:8c2862b17b9bd9ae194d18fc912785f899524d8f</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>release: v1.4.0</title>
<updated>2026-07-18T01:09:51+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:09:51+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=49c51b4e909053dc71350f4abe215acd29a714c6'/>
<id>urn:sha1:49c51b4e909053dc71350f4abe215acd29a714c6</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 118a04f</title>
<updated>2026-07-18T01:04:05+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:04:05+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=81306bf3039f24f7ace8ce9cafca31ae747ee630'/>
<id>urn:sha1:81306bf3039f24f7ace8ce9cafca31ae747ee630</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: subscribe only to selected wake-trigger interfaces</title>
<updated>2026-07-18T01:04:05+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:04:05+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=118a04f9a56ab338da746f566fca8799fbd3e13d'/>
<id>urn:sha1:118a04f9a56ab338da746f566fca8799fbd3e13d</id>
<content type='text'>
attachListener listened on BinarySensor+MotionSensor+ObjectDetector
unconditionally and relied on handleCameraEvent to filter by trigger —
correct, but a doorbell-only viewport still took a callback per motion
re-assert on a chatty camera, and the "subscribed to [...]" log implied
all three were wake sources. The listener set now follows the selected
triggers (zero triggers = tap-only: no listeners at all), and the
idempotency key becomes a "cameraId|triggers" signature
(attachedCameraId -&gt; attachedListenerSig) so a trigger change forces a
re-attach through the existing rebind path. Event-time filtering stays
as defense in depth (ObjectDetector still needs the person-class check).
</content>
</entry>
<entry>
<title>scrypted: name viewports from discovery; editable name on settings page</title>
<updated>2026-07-18T01:01:45+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:01:45+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=3350c4b1541d626189bc6554a774c6503e4c2f52'/>
<id>urn:sha1:3350c4b1541d626189bc6554a774c6503e4c2f52</id>
<content type='text'>
Creating a viewport with the name field blank fell back to "viewport"
with no way to change it afterward — display_name is deliberately the
canonical name (v.name drifts on reload), so the Scrypted device-name
pencil never propagated. Two fixes:

- createDevice: an empty name now inherits the discovered device's
  advertised TXT name, so picking "10.0.13.83 — kitchen (…)" from the
  host dropdown names the viewport "kitchen" without retyping.
- child settings gain a "Viewport name" field: updates display_name,
  mirrors the name onto the Scrypted device record, and re-registers so
  the firmware name + mDNS hostname follow.
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 3821512</title>
<updated>2026-07-18T00:49:57+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T00:49:57+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=d73622e3c1da423dbad2cab302a5b9d2f711f830'/>
<id>urn:sha1:d73622e3c1da423dbad2cab302a5b9d2f711f830</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: mdns discovery — browse, host choices, auto-heal</title>
<updated>2026-07-18T00:49:47+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T00:49:47+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=3821512b18be69cad002faf77f1e5844bda13c95'/>
<id>urn:sha1:3821512b18be69cad002faf77f1e5844bda13c95</id>
<content type='text'>
Browse for _scrypted-viewport._tcp with a plain dgram socket on an
EPHEMERAL port: per RFC 6762 §6.7 a query from a non-5353 source port is
a legacy unicast query and responders reply unicast to that port —
verified in the ESP-IDF responder (mdns_send.c answers to the querier's
addr/port whenever src_port != 5353). No :5353 bind means no conflict
with Scrypted's own HomeKit mDNS or a host avahi-daemon, and it works
under Docker host-networking or native alike. One response packet
carries PTR+SRV+TXT+A (RFC 6763 §12.1), so parsing is per-packet with
no follow-up queries; the wire codec (name compression included) is
~150 lines, no dependencies.

Surfaced three ways:
- add-device form: host field is a combobox listing discovered
  viewports as "ip — name (vX, WxH)"; manual entry still works and
  only the address token is stored.
- child settings page: same choices on the existing host field.
- auto-heal: when a /config register fails, re-browse and match by MAC
  (seeded from /state and discovery; survives renames) then by name;
  on a hit at a new address, rewrite host and retry once. Runs only on
  register failure, so it's rate-limited to the 5-min cycle and removes
  the need for a DHCP reservation.

Browses are best-effort ([] on any failure) and cached 30s so settings
re-renders don't spam the LAN. diagnostic.ts gains the same browse as a
paste-and-run probe that validates dgram + multicast reachability from
inside the real Scrypted sandbox before trusting the feature.
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 0ac000e</title>
<updated>2026-07-17T23:56:24+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-17T23:56:24+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=c9f0297dc86eaee7ef222a066587f4eb737d0ea8'/>
<id>urn:sha1:c9f0297dc86eaee7ef222a066587f4eb737d0ea8</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: code-review fixes — reload drain order, reconnect storm, wake guards</title>
<updated>2026-07-17T23:56:17+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-17T23:56:17+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=0ac000e0db3feb235fae37389d0af18609e91c50'/>
<id>urn:sha1:0ac000e0db3feb235fae37389d0af18609e91c50</id>
<content type='text'>
- bootstrap: drain the shutdown-cleaner array FIRST, not after the child
  re-discovery loop. The old order tore down the listeners the loop had
  just attached, and since the instance maps still referenced them the
  attachListener fast-path blocked the 5-min register-cycle self-heal
  from ever re-attaching after a warm re-paste. The attach cleaner now
  also invalidates listeners/attachedCameraId (one cleaner per attach),
  so a drain by another instance can't strand dead registrations either.
- stream socket: single-flight reconnect scheduled from 'close' only.
  A failed connect emits 'error' then 'close'; scheduling from both
  doubled outstanding attempts every 500ms against a rebooting device.
  openStreamSocket also destroys the previous socket first.
- onRequest wake: same streams/streamStarting guard as every other start
  path (a concurrent second startStream overwrote the streams entry and
  orphaned the first ffmpeg), respond 204 immediately (the device's POST
  times out at 1s — awaiting startStream turned every tap-wake into a
  firmware state_post_failure), and skip the redundant wake POST back.
- startStream: bail-out paths after the wake POST (camera missing, no
  usable stream, ffmpeg-input conversion failure) now send a
  compensating sleep instead of stranding the panel on Loading; the
  last-ditch getVideoStream fallback no longer rejects out of the call.
- key streams/streamStarting/stopStream by nativeId — v.name drifts to
  the nativeId on reload, stranding or duplicating streams keyed under
  the drifted value; findByName matches display_name first for the same
  reason.
- idle_timeout_ms=0: the Scrypted-side safety timeout still reclaims
  ffmpeg/socket but no longer POSTs sleep over the always-on setting.
- pending bindingDebounce timers cleared across re-paste (they fired
  against the dead instance and attached duplicate listeners);
  releaseDevice drops lastRegisterSig/streamStarting/debounce entries.
- start() single-flights bootstrap; createDevice drops its redundant
  second registerViewport; streamLogger rolls its window on quiet ticks
  (post-lull rates were diluted) and lastLogUs -&gt; lastLogMs; hoist the
  JPEG EOI needle + resume-scan offset in the demux loop; postJSON drops
  the dead 204 check; remove unused sandbox declares and the legacy
  streams.interval field.
- tsconfig: moduleResolution Node -&gt; Bundler (removed in TS 6).
</content>
</entry>
</feed>
