<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport, branch v1.4.0</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.4.0</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.4.0'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-07-18T01:09:51+00:00</updated>
<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>tools: make targets + ota.sh for the build/OTA/verify loop</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=6c16b354e00e916b7e252b2304b71af87981f518'/>
<id>urn:sha1:6c16b354e00e916b7e252b2304b71af87981f518</id>
<content type='text'>
make build / cleanbuild / ota / verify / check wrap the recurring dev
loop, sourcing the ESP-IDF env per-recipe. `make ota` reconfigures first
(the embedded git hash stamps at configure time only), builds, pushes,
and verifies. tools/ota.sh encodes the OTA acceptance criterion — the
new image must show pending-verify on a fresh boot before flipping to
valid; "valid" with pending-verify never seen means the device silently
booted the old slot — and re-pushes once automatically on the known
first-push rollback quirk.
</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>main: advertise mac in mDNS TXT</title>
<updated>2026-07-18T00:49:46+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T00:49:46+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=2a4b8296d71f25e5a29a07ea6cc9f3ac31676c48'/>
<id>urn:sha1:2a4b8296d71f25e5a29a07ea6cc9f3ac31676c48</id>
<content type='text'>
Stable identity for the Scrypted plugin's discovery/auto-heal: the
viewport name is user-editable and the IP can renumber, but the MAC is
fixed. Discovery matches on it to relocate a device after a DHCP change.
</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>
