<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport, branch v1.3.1</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.3.1</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.3.1'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-07-16T00:04:56+00:00</updated>
<entry>
<title>release: v1.3.1</title>
<updated>2026-07-16T00:04:56+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:04:56+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=7620b935728ea964ddca09b91ac0be36069a8c64'/>
<id>urn:sha1:7620b935728ea964ddca09b91ac0be36069a8c64</id>
<content type='text'>
Stream throughput tuning, measured on hardware (kitchen panel):
wire 53 -&gt; 74 Mbps, per-frame recv 30.5 -&gt; 21.4 ms, painted fps
19.8 -&gt; 23.6, g2g 73 -&gt; 62 ms, sender backpressure 61% -&gt; 47%.

- lwip: TCP_WND 5760 -&gt; 23040 (16 x MSS), RECVMBOX 6 -&gt; 32
- eth: EMAC RX DMA pool sized above the TCP window (1600B x 24);
  below-window pool caused silent burst tail-drop + ~200-400ms
  sender RTO stalls
- stream: TCP-window decomposition instrumentation (wire kbps,
  hdr_gap, pend_age) in the window log and /state; connect log
  stamps TCP_WND/MSS/RECVMBOX

Set VIEWPORT_VERSION and scrypted/package.json to 1.3.1.
</content>
</entry>
<entry>
<title>eth: size EMAC RX DMA pool above the TCP window (1600B x 24)</title>
<updated>2026-07-15T23:38:26+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-15T23:38:26+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=8c05eddbd12c6a78a7247763d98f2e2a8ac77e1e'/>
<id>urn:sha1:8c05eddbd12c6a78a7247763d98f2e2a8ac77e1e</id>
<content type='text'>
WND=23040 alone regressed: sender fps 20 -&gt; 14, drop-oldest 3x,
recv bimodal (min 20ms but 200-450ms stalls; firmware wire_min
6.2Mbps / wire_max 75Mbps, recv_max 253ms). The stalls are RTO
recovery: the default EMAC RX pool (20 x 512B = 10KB) is smaller
than the 23KB the window now invites in flight, so a full-window
burst overruns the RX descriptors, the burst tail is dropped with
no dup-ACKs behind it, and the sender waits out a ~200ms min-RTO.

1600B buffers fit one MSS frame per buffer (1 descriptor/packet
instead of 3); 24 of them = 38.4KB &gt;= window + slack. ~44KB more
internal RAM (491KB free).
</content>
</entry>
<entry>
<title>lwip: raise TCP_WND 5760-&gt;23040, RECVMBOX 6-&gt;32 (window step 1)</title>
<updated>2026-07-15T23:15:36+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-15T23:15:36+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=143c03f1b0fe35484d410f97539af1d7fe28b930'/>
<id>urn:sha1:143c03f1b0fe35484d410f97539af1d7fe28b930</id>
<content type='text'>
Instrumented baseline at WND=5760 (kitchen, ~202KB frames, 20fps
sender) confirmed the receive window as the dominant throttle:

- wire 53.0/58.8 Mbps avg/max = the WND/RTT ceiling, vs ~94 Mbps
  line rate on the 10/100 PHY
- recv_chunk_max pinned at exactly 5760 (drains window-quantized)
- recv_calls avg 60/frame; queued_at_body always 0
- sender backpressured 44-61% of frames, drop-oldest 17-54/window
- meanwhile pend_age avg 58us: decode idles, receive starves it

Expected: recv_avg ~30ms -&gt; ~20ms, wire toward line rate, sender
bp%/drop-oldest down, g2g 73ms -&gt; ~60ms. Tripwires (revert if hit):
pend_age or recv_dropped_oldest growing window-over-window, g2g
regressing. RECVMBOX scales with the window: 23040 = 16 segments
in flight, a 6-deep mbox would silently become the new cap.
</content>
</entry>
<entry>
<title>stream: instrument TCP-window decomposition (wire kbps, hdr_gap, pend_age)</title>
<updated>2026-07-15T23:04:31+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-15T23:04:31+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=6ee12595d339eeafc7fdc7dbb9ba4c4c757f3565'/>
<id>urn:sha1:6ee12595d339eeafc7fdc7dbb9ba4c4c757f3565</id>
<content type='text'>
Before touching CONFIG_LWIP_TCP_WND_DEFAULT, make the window question
decidable from the logs. New per-window metrics in the stream log,
/state, and stats struct:

- wire min/avg/max kbps: instantaneous throughput while each body
  drained (jpeg_len/recv_us). Ceiling ~= TCP_WND/RTT, so it scales
  with the window iff the window is the limiter.
- hdr_gap min/avg/max us: time blocked waiting for the next header
  after finishing a body. Large = sender-paced; ~0 = receive path
  is the bottleneck.
- pend_age min/avg/max us: publish-&gt;claim latency of painted frames.
  Growing across windows = queue backlog building, the failure mode
  that killed the previous WND=65535 attempt.

Together with recv/dec/paint the frame interval is now fully
decomposable: interval ~= hdr_gap + recv + pend_age + dec + paint.

Also stamp TCP_WND/TCP_MSS/RECVMBOX into the client-connect log line
so every capture is self-labeled with the config it ran under.
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 01a588e</title>
<updated>2026-07-01T19:33:44+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T19:33:44+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=55303088ea499017c75203eef71588d18609f124'/>
<id>urn:sha1:55303088ea499017c75203eef71588d18609f124</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: fix stale Streaming header comment (TCP socket, q:v default 1, skip-oldest)</title>
<updated>2026-07-01T19:33:44+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T19:33:44+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=01a588e8658160504a9e861e1f8b133e0251f630'/>
<id>urn:sha1:01a588e8658160504a9e861e1f8b133e0251f630</id>
<content type='text'>
</content>
</entry>
<entry>
<title>release: v1.3.0</title>
<updated>2026-07-01T19:23:49+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T19:23:49+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=bdf497172a62c4ac318fa087bc59ef95813af61a'/>
<id>urn:sha1:bdf497172a62c4ac318fa087bc59ef95813af61a</id>
<content type='text'>
Unify firmware and plugin versions for the production release. Highlights
since v1.1.0/1.2.0:
- event wake fixed (self-healing camera-listener re-attach after the reload/
  add storage race); doorbell/motion/person confirmed on hardware
- stop() fully tears down (stop+start == fresh load); stale childId pruning;
  live-stream events ignored (no queue/relaunch)
- triggers default to person+doorbell; doorbell hidden for non-doorbell cams
- cold-start (wake -&gt; live video) ~6s -&gt; ~0.7s: prebuffered substream by id +
  request &gt;= GOP + burst-friendly ffmpeg flags; snapshot removed
- firmware: Loading screen on new stream connection (no stale-frame flash)
- logging trimmed for chatty cameras
- README/protocol docs rewritten for the TCP-socket streaming model

Set VIEWPORT_VERSION and scrypted/package.json to 1.3.0.
</content>
</entry>
<entry>
<title>scrypted: stamp SCRIPT_VERSION = 0be8f18</title>
<updated>2026-07-01T19:19:01+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T19:19:01+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=ad29fa22e8fc9c15c2cda5232a2b269594475d16'/>
<id>urn:sha1:ad29fa22e8fc9c15c2cda5232a2b269594475d16</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: trim stream logging volume</title>
<updated>2026-07-01T18:22:05+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-01T18:22:05+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=0be8f188b46bd073e4c6836dedf79a5282250828'/>
<id>urn:sha1:0be8f188b46bd073e4c6836dedf79a5282250828</id>
<content type='text'>
A chatty camera keeps a viewport streaming for long stretches, and the old
logging emitted ~5 cold-start stamps + a 10s window line per ~10s per stream.
Cut it down:
- keep a single cold-start stamp (first ffmpeg frame); drop spawned / first
  stdout byte / first socket.write.
- gate the 10s window line on noteworthy windows only (socket-not-ready drops,
  a buffer-cap flush this window, firmware shedding &gt;=2fps, or painted &lt;20fps);
  healthy 24fps windows print nothing — the per-wake lines already show liveness.
- log "registered" only when the pushed config actually changes, not on every
  5-minute reregister cycle. lastRegisterSig cleared on stop() for fresh-load
  parity.
No behavior change beyond logging.
</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>
</feed>
