<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/README.md, 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-19T14:46:37+00:00</updated>
<entry>
<title>readme: public-facing refresh — links up top, drop the v1-spec framing, 120-col reflow</title>
<updated>2026-07-19T14:46:37+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-19T14:41:43+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=642e5498dd886e7389169472909119f59635f8c4'/>
<id>urn:sha1:642e5498dd886e7389169472909119f59635f8c4</id>
<content type='text'>
Now that the repo is public: title becomes "Scrypted Viewport" with a
what-it-is intro (0.7s wake-to-video, 24fps, double-digit-ms g2g) and
up-front links to the write-up (nth.io/luke/projects/esp32-ethernet-
display), the canonical src.nth.io repo, and the GitHub mirror. The
"v1 Technical Design Specification" framing is gone (the v1/v2 script
references now read as "the script" + the plugin-conversion plan), the
Boot flow diagram is fenced, and all prose is reflowed to 120 columns
(markdown tables exempt — they can't wrap).
</content>
</entry>
<entry>
<title>docs: prune TESTING.md to its forward-looking content</title>
<updated>2026-07-18T01:39:31+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:39:31+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=ffd5b40586f6c1a8a0b245603c672afe521eb128'/>
<id>urn:sha1:ffd5b40586f6c1a8a0b245603c672afe521eb128</id>
<content type='text'>
The 730-line milestone-by-milestone bring-up log served its purpose —
every milestone (M1-M9) is verified on hardware, and the per-run
narratives are preserved in git history (last full version: 8eebc0e).
What survives, restructured (~230 lines):

- New-unit bring-up: hardware prereqs (all unknowns resolved), the
  flash -&gt; panel -&gt; touch -&gt; Scrypted sequence, and the panel
  failure-mode signals table (the content you actually need the day a
  second viewport gets provisioned).
- Regression recipes: condensed curl checks for config validation (incl.
  the 54-char name cap), frame push, state machine, OTA acceptance, and
  device-initiated POSTs.
- Outstanding tests: the honestly-never-run backlog (mid-frame cable
  pull, 24h soak, frame storm, brown-out, multi-viewport).
- Performance playbook: kept, with example log formats refreshed to the
  current stream instrumentation (the old examples showed the removed
  pre-stream snapshot path) and thresholds updated to the post-tuning
  baseline (24fps painted, pend_age as the backlog tripwire).

README: pointers at TESTING's old structure updated; the Hardware status
row no longer claims schematic confirmation is pending (those unknowns
were resolved during bring-up — only the per-unit flash-size check
remains).
</content>
</entry>
<entry>
<title>docs: mark M9 verified — live stream shipped as raw TCP :81, not POST /stream</title>
<updated>2026-07-18T01:27:50+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:27:50+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=8eebc0ea5b6001be6735cf679fb3b94757d0de02'/>
<id>urn:sha1:8eebc0ea5b6001be6735cf679fb3b94757d0de02</id>
<content type='text'>
M9 still carried the abandoned chunked-HTTP design and a pending status
long after the raw TCP data-socket stream was verified at painted =
sent = 24 fps (recv/decode task split d1c8d45 + TCP window/EMAC tuning).
Rewrite the milestone to describe what shipped, flip it ✅, and bump the
README's "M1 – M8" line to M9.
</content>
</entry>
<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>readme: document the Makefile build/OTA/verify workflow</title>
<updated>2026-07-18T01:13:06+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-18T01:13:06+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=903d1cf44e9e4b6a6733fbc5d405182b658e5d84'/>
<id>urn:sha1:903d1cf44e9e4b6a6733fbc5d405182b658e5d84</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: temp_c on the stats line; docs: triple-buffer model + window tuning</title>
<updated>2026-07-16T00:36:56+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:36:56+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=2af69f78a6179334c6f59d4273bd72ae170fb821'/>
<id>urn:sha1:2af69f78a6179334c6f59d4273bd72ae170fb821</id>
<content type='text'>
- Scrypted per-stream stats line gains temp=&lt;c&gt;C from /state temp_c —
  free thermal trending under streaming load.
- README Display strategy rewritten for the tear-free triple-buffer
  model: why the deferred fb-index reload tears under double
  buffering, why scan-tracked buffer roles beat vsync-waiting, and
  the tear_guard_engaged counter.
- New 'TCP window + EMAC tuning' section documenting the measured
  window raise, the EMAC-RX-pool-below-window RTO regression, and
  the pool &gt;= TCP_WND invariant; stale 'window bump is safe but
  won't help' backlog text updated.
- Memory strategy updated (3 fbs, decoder writes into them directly,
  stream body ring).
</content>
</entry>
<entry>
<title>docs: reframe protocol sections for the TCP-socket streaming model</title>
<updated>2026-07-01T02:22:22+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T02:22:22+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=ec49dcd0ac9bf73b8be56127eef30428af5824a4'/>
<id>urn:sha1:ec49dcd0ac9bf73b8be56127eef30428af5824a4</id>
<content type='text'>
The Wake/Sleep, race-handling, and idempotency sections still described the
old model where Scrypted streamed via POST /frame and learned of sleep from a
/frame 409. Streaming now runs over the TCP data socket (:81); the firmware
/frame endpoint remains only for one-shot snapshots/debug. Reframe the recovery
model accordingly:
- race-free property: frames arriving while asleep are discarded by the decode
  task (stream socket) / rejected 409 (/frame) — neither re-wakes; sleep is
  learned from the device's state=sleep callback + Scrypted's safety timer
- drop every "recovered by the next /frame returning 409" clause in favor of
  the safety timer + discard-while-asleep
- idempotency table: add the stream-socket row (paints if awake, discarded if
  asleep, skip-oldest)
- Loading screen: shown on wake AND each new stream connection until first paint
- Philosophy: list the MJPEG stream server (:81) and /firmware
Left the POST /frame endpoint's own API/semantics intact — it still exists.
</content>
</entry>
<entry>
<title>docs: update main README streaming section to the shipped TCP-socket path</title>
<updated>2026-07-01T02:18:43+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T02:18:43+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=a84f17ec5da36a704636c45aaa5a67c4b8a2bd5b'/>
<id>urn:sha1:a84f17ec5da36a704636c45aaa5a67c4b8a2bd5b</id>
<content type='text'>
The Scrypted Integration section still framed the snapshot POST /frame path as
v1 and MJPEG streaming as a future POST /stream milestone, contradicting the
already-current "What's next" perf section. Bring it in line:
- v1 is the live ffmpeg MJPEG stream over the raw TCP data socket (:81) with
  prebuffer fast-start (~0.7s wake-to-video); POST /stream was never the shape
- replace the /frame streaming code example with the TCP socket + framed-MJPEG
  sketch; brightness default 100 (was 80)
- sleep is learned via the device's sleep callback, not a /frame 409
- Status table: streaming + end-to-end are done/verified, not "not started"
- Idle: timer resets on any painted frame, not specifically a /frame POST

Left intact: the POST /frame API + protocol/idempotency sections — that
firmware endpoint still exists for one-shot snapshots/debug and its 409-when-
asleep semantics are unchanged.
</content>
</entry>
<entry>
<title>README: stream-server task split is the critical change that closed the painted&lt;sent gap</title>
<updated>2026-06-21T02:01:23+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-21T02:01:23+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=a438a6303e94cea99f4f4c51017d958d0b44b0ac'/>
<id>urn:sha1:a438a6303e94cea99f4f4c51017d958d0b44b0ac</id>
<content type='text'>
Document the architecture pivot. The per-frame budget table now
reflects the long-lived TCP stream socket on port 81 (replaced the
HTTP /frame pattern) and the new recv/decode/paint timings:
recv 14-18ms on the wire (pure body), dec ~5ms, paint &lt;35us, with
decode-task idle ~30ms per frame waiting on recv.

The critical change wasn't a TCP tuning knob — it was splitting
recv off its own FreeRTOS task with a 3-buffer PSRAM ring and a
1-deep latest-frame slot to the decode/paint task (d1c8d45). The
single-task loop blocked the socket for 6ms during decode+paint,
which against the IDF-default 5760-byte window forced a stop-go
cycle that capped painted at ~17fps. Raising the window to 65535
made it worse (g2g grew to 17s) because lwIP RX couldn't drain
the bursts and there's no way to skip-oldest on the kernel queue.
The task split eliminated the coupling: recv-task drains
continuously regardless of decode timing, the kernel buffer stays
near-empty, and painted now matches sent at the source rate.

Backlog entries for body shrink and OTA marked done.
</content>
</entry>
</feed>
