<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport, branch v1.0.0</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.0.0</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.0.0'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-06-20T15:41:08+00:00</updated>
<entry>
<title>release: v1.0.0 — streaming pivot complete</title>
<updated>2026-06-20T15:41:08+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:41:08+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=e6510f2a719cbab4fe9392206f38c90c0d3d3ee3'/>
<id>urn:sha1:e6510f2a719cbab4fe9392206f38c90c0d3d3ee3</id>
<content type='text'>
First milestone where the device runs the way it was intended end-
to-end:

- Raw-TCP streaming data plane at ~14-20 fps painted, sub-ms socket
  writes, no Nagle/ACK pathology.
- HTTP control plane (/state, /config) plus HTTP /frame for the
  one-shot snapshot fast path on wake. Snapshot via sharp → ~700ms
  first paint from a cold trigger (down from ~1000ms+ via ffmpeg).
- Firmware-side "always paint the latest" — FIONREAD skip drops
  superseded frames before decode, keeping glass-to-glass tight
  even when upstream produces faster than we can ingest.
- HW JPEG decode + zero-copy paint into the panel back framebuffer.
- 12 fps goal hit cleanly at q:v 1 (visually lossless). Source
  pinned to medium-resolution substream so the camera supplies
  enough frames to actually fill the pipe.
- Scrypted side: per-host node:http keep-alive Agent with NODELAY,
  the cascading sharp → mediaManager → ffmpeg snapshot transform,
  wake-trigger picker on the new-device dialog, and the leaked-
  setInterval-across-script-reloads bug squashed.

Bumping VIEWPORT_VERSION 0.1.0 → 1.0.0 so the boot log and info
screen reflect it.
</content>
</entry>
<entry>
<title>Revert "firmware: revert lwIP window bump — broke the stream accept path"</title>
<updated>2026-06-20T15:39:28+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:39:28+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=6ceebe21c5271c4603493329f4496becbc388ef5'/>
<id>urn:sha1:6ceebe21c5271c4603493329f4496becbc388ef5</id>
<content type='text'>
This reverts commit 6e36cbc14a06b8c0e15bfe78afb8adb43f8909ba.
</content>
</entry>
<entry>
<title>firmware: revert lwIP window bump — broke the stream accept path</title>
<updated>2026-06-20T15:38:46+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:38:46+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=6e36cbc14a06b8c0e15bfe78afb8adb43f8909ba'/>
<id>urn:sha1:6e36cbc14a06b8c0e15bfe78afb8adb43f8909ba</id>
<content type='text'>
Hypothesis confirmed by the symptoms: with WND_DEFAULT=65535,
SND_BUF=65535, RECVMBOX=16, the firmware boots fine and the stream
server logs "listening on tcp/:81", but Scrypted's first SYN never
produces a "client connected from ..." entry. The script-side
socket stays stuck in pending-connect — neither a connect nor an
error event fires — and every ffmpeg-emitted frame gets dropped.

Root cause is almost certainly lwIP's PBUF / MEMP pools not being
scaled to back the 64KB windows on multiple sockets (httpd has 4,
stream has 1, that's 5 × 64KB = 320KB of buffer commitment from a
heap that's also feeding PSRAM-backed framebuffers, JPEG scratch,
and Ethernet DMA). lwIP doesn't surface the allocation failure as
an accept error — it just refuses to complete the handshake.

Backing off the changes for now. The streaming path already hits
~14-20 fps at our measured 5.3 MB/s ingest ceiling under the lwIP
defaults — that's plenty above the 12-fps goal. Revisit window
tuning later by also bumping CONFIG_LWIP_PBUF_POOL_SIZE and
MEMP_NUM_TCP_PCB so the buffer commitment is actually allocatable.

Keeping the FIONREAD skip in stream_server (it's orthogonal to lwIP
buffer sizes and only helps).
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 496da49</title>
<updated>2026-06-20T15:31:32+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:31:32+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=79d3e379c6c5f9a465c9333410830e9037c6a1c7'/>
<id>urn:sha1:79d3e379c6c5f9a465c9333410830e9037c6a1c7</id>
<content type='text'>
</content>
</entry>
<entry>
<title>"paint the latest, drop the rest" — FIONREAD skip + Scrypted backpressure-blind + lwIP TCP window bump</title>
<updated>2026-06-20T15:30:24+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:30:24+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=496da49dd5ce4a3bfc4b5df84ce5c7f7bdbcf504'/>
<id>urn:sha1:496da49dd5ce4a3bfc4b5df84ce5c7f7bdbcf504</id>
<content type='text'>
Three changes that work together to make the stream "always paint
what's freshest, never sit on a stale frame":

#1 — Firmware FIONREAD skip in stream_server
Right after the body of frame N comes off the wire (and before we
unlock the decoder + spend ~6ms on decode + paint), check the
kernel receive buffer with ioctl(FIONREAD). If at least one more
header (8 bytes) is queued, frame N is no longer the freshest
possible — skip its decode + paint and loop back to read frame
N+1. The TCP recv cost is unavoidable (bytes still have to cross
the wire) but the decoder + paint cost is saved on every superseded
frame. Glass-to-glass latency on the latest frame drops by however
many frames had backed up.

#2 — Scrypted: keep writing past kernel-buffer backpressure
Previously: when sock.write() returned false we dropped the next
ffmpeg frame at source. New: we keep writing through. Node buffers
internally; under our load (~16 MB/s ffmpeg → ~5-7 MB/s firmware)
the buffer rarely exceeds a frame or two. With the firmware now
silently skipping decode on backed-up frames (#1), excess frames
get shed for free on the device side. Scrypted's job is just to
hand the firmware the freshest bytes as fast as possible.
socketBackpressured is still tracked for the diagnostic log.

#3 — lwIP TCP window bump (revisiting earlier regression)
The previous attempt at LWIP_TCP_WND_DEFAULT=32k regressed under
HTTP because every /frame opened a fresh socket and we paid the
slow-start cost repeatedly. The streaming pivot eliminated that:
the socket is long-lived, slow-start runs exactly once, then we
ride the full window for the rest of the session. Bumping to
65535 (max for stock lwIP), SND_BUF to match, RECVMBOX to 16, SACK
on. Expected: recv throughput ceiling moves up from ~5.3 MB/s,
which directly raises the fps ceiling (recv is currently 37ms of
the 43ms per-frame total).
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 521de7e</title>
<updated>2026-06-20T15:12:10+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:12:10+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=cebd60d4be74ddac9c18a7b08eef9ec00cbc31f5'/>
<id>urn:sha1:cebd60d4be74ddac9c18a7b08eef9ec00cbc31f5</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: show Wake triggers on new-device dialog + stop leaking re-register interval</title>
<updated>2026-06-20T15:11:57+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:11:57+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=521de7ef844a4b5c1eb660ff54382b958fb62bd7'/>
<id>urn:sha1:521de7ef844a4b5c1eb660ff54382b958fb62bd7</id>
<content type='text'>
#1 — Wake triggers in the new-device dialog
The "+ Add Device" form was missing the Wake triggers multi-select,
so a new viewport defaulted to the per-getter fallback of all three
(doorbell + motion + person) silently and the user only saw the
field after first edit. Now the dialog includes it up front with
all three pre-selected, and createDevice persists whatever the user
ticked into storage in the same JSON shape Viewport.putSetting uses
for subsequent edits.

#2 — Stop leaking the periodic re-register interval
Why the user was seeing the "registered ..." log line repeat 14
times in steady-state with nothing happening: Scrypted's Scripts
sandbox does NOT garbage-collect setInterval handles when a script
is re-pasted/reloaded. Every re-paste left an orphan interval
running against the previous Provider instance, accumulating one
extra timer per reload. Every 5 minutes (REREGISTER_INTERVAL_MS),
all N timers fired at once, producing N "registered ..." log lines
in rapid succession.

Fix: keep the timer handle on globalThis under a well-known key. At
script start, clearInterval the previous one (if any) before
arming the new one. Idempotent across reloads.
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = f982b88</title>
<updated>2026-06-20T15:07:15+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:07:15+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=f93179d9d1cd9e2f2b3977cd7020e13f133588b2'/>
<id>urn:sha1:f93179d9d1cd9e2f2b3977cd7020e13f133588b2</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: drop substream picker, hardcode best-resolution + skip low-res</title>
<updated>2026-06-20T15:06:58+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:06:58+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=f982b88d447f265f9847a48946a9903d1faebf49'/>
<id>urn:sha1:f982b88d447f265f9847a48946a9903d1faebf49</id>
<content type='text'>
Reverts the per-viewport "Camera substream" UI added in 6d74d02 —
having a knob the user has to find and tune is the wrong shape. The
goal is "best quality, highest fps that's actually achievable" and
that's a deterministic walk, not a user choice.

New walk: medium-resolution → local → remote → camera-default.

Explicitly NOT in the list:
- low-resolution: the camera's preview substream, capped at the
  ~5-8 fps that's been bottlenecking us.
- remote-recorder: has the camera's ~10s prebuffer baked in (we'd
  display the past rather than the present).

Wire cost is unchanged: every input resolution is re-encoded to
panel-native 800x480 mjpeg q:v 1 before going to the device. The
only tradeoff for picking the main stream is Scrypted-side ffmpeg
CPU, which is plentiful. Throughput to the firmware is the same;
upstream camera fps is what changes (5fps → 15-30fps typical).
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 6d74d02</title>
<updated>2026-06-20T15:02:48+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T15:02:48+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=aa1ce70d546355997c58488fc409767d1dc8088f'/>
<id>urn:sha1:aa1ce70d546355997c58488fc409767d1dc8088f</id>
<content type='text'>
</content>
</entry>
</feed>
