<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/main, 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: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>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>main: code-review fixes — brightness units, OTA stall cap, stats + locking</title>
<updated>2026-07-17T23:56:03+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-17T23:56:03+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=9d94f710f7b00d109bf6f6ee57996f43bce5c67f'/>
<id>urn:sha1:9d94f710f7b00d109bf6f6ee57996f43bce5c67f</id>
<content type='text'>
- display: s_last_pwm cached the raw 0-100 percentage at init but
  display_wake writes it straight to REG_PWM (0-255 duty) — first wake
  ran visibly dim until a /config brightness change. Shared pct_to_duty
  helper now converts in both paths.
- http_api: cap consecutive OTA recv timeouts (a stalled client spun
  forever holding s_ota_in_progress, wedging OTA until reboot); cap
  viewport name at 54 chars so viewport-&lt;name&gt; fits the 63-byte mDNS
  label; log mdns_service_refresh failures; /state builds JSON from a
  snapshot instead of holding the state lock across ~25 cJSON allocs;
  respond_400 delegates to respond_status.
- stream_server: bytes_in_window now counts painted frames only (frames
  discarded while asleep inflated the first post-wake window's MB/s,
  avg-jpeg and chunk/wire averages; recv_bytes folded in); so_rcvbuf
  carried into the stats snapshot under the mux; drop dead HEADER_BYTES;
  merge read_body_instrumented into read_n.
- state_machine: transition mutex serializes concurrent wake/sleep
  (display side-effects ran after the state lock dropped, so racing
  callers could leave the backlight contradicting st-&gt;state); wake-path
  placeholder paint now takes the decoder lock like the stream path
  (concurrent esp_lcd_panel_draw_bitmap from two tasks isn't safe).
- local_screens: overlay paint takes the decoder lock too; check the
  overlay timer create; panel dims from viewport_state.h.
- jpeg_decoder: try_lock before init returns busy instead of passing a
  NULL semaphore to xSemaphoreTake.
- net_eth: clear cached IP string on link down (stale /state + info).
- dead code: display_present_bgr888, TOUCH_FT5426_ADDR, touch s_task,
  JPEG_DECODER_MAX_OUTPUT_BYTES; doc drift in nvs_config.h /
  jpeg_decoder.h / app_main flag legend.
</content>
</entry>
<entry>
<title>release: v1.3.2</title>
<updated>2026-07-16T00:51:41+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:51:41+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=866015678918560a0d0519be8a2549e4fc23b2cd'/>
<id>urn:sha1:866015678918560a0d0519be8a2549e4fc23b2cd</id>
<content type='text'>
Tear-free display path and thermal visibility:
- display: triple buffering + scan tracking via on_refresh_done —
  decode target is never the scanning or pending fb, so the
  flip-vs-scan tear (measured on ~6% of painted frames at full
  stream rate) is impossible by construction, with zero added
  latency. /state tear_guard_engaged counts averted frames.
- temp: on-die TSENS reported as /state temp_c, on the info
  overlay, and on the Scrypted per-stream stats line.
- screens: INFO_MAX_LINES 16 -&gt; 20 (temp line was silently capped).
- docs: README Display strategy rewritten for the triple-buffer
  model; new TCP window + EMAC tuning section.

Set VIEWPORT_VERSION and scrypted/package.json to 1.3.2.
</content>
</entry>
<entry>
<title>screens: raise INFO_MAX_LINES 16 -&gt; 20 (temp line was silently dropped)</title>
<updated>2026-07-16T00:41:41+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:41:41+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=739e6ff1afe6b5b80cc28afa19aa4b1b42282765'/>
<id>urn:sha1:739e6ff1afe6b5b80cc28afa19aa4b1b42282765</id>
<content type='text'>
The info overlay already had exactly 16 ADD lines; the temp line
added in f59cea3 was the 17th, and the ADD macro's bounds guard
drops overflow lines without any diagnostic — so temp never
rendered. Give the cap headroom and document the silent-drop
behavior at the definition.
</content>
</entry>
<entry>
<title>temp: report on-die temperature in /state and the info screen</title>
<updated>2026-07-16T00:18:32+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:18:32+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=f59cea3be9eed8d63fa44d4fd360c9c68e9318f6'/>
<id>urn:sha1:f59cea3be9eed8d63fa44d4fd360c9c68e9318f6</id>
<content type='text'>
New chip_temp module wraps the ESP32-P4 TSENS driver (20-100C range
for best accuracy in the warm band a PoE + 200MHz-PSRAM device
lives in). /state gains temp_c (0.1C resolution, omitted when the
sensor is unavailable); the long-press info overlay gains a temp
line (lowercase c suffix — the local 8x8 font has no uppercase C).
Junction temperature, ~10-20C above ambient under load.
</content>
</entry>
<entry>
<title>display: tear-free frame path via triple buffering + scan tracking</title>
<updated>2026-07-16T00:10:57+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-16T00:10:57+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=659da7f36473a21494693f031a39fb6bb977b90a'/>
<id>urn:sha1:659da7f36473a21494693f031a39fb6bb977b90a</id>
<content type='text'>
draw_bitmap on a direct fb pointer only updates the driver's
cur_fb_index; the DPI DMA reloads that index at the END of the
in-progress frame scan (~21ms period at ~47Hz). Under double
buffering, flipping and immediately decoding the next frame into
the other fb writes a buffer the DMA may still be scanning out —
a torn frame. This regime is common now that the TCP window fix
delivers frames back-to-back (decode starts ~6ms after flip).

Fix with zero added latency: num_fbs 2 -&gt; 3 (+1.15MB PSRAM of 25MB
free), track the actually-scanning fb via on_refresh_done (fires in
the DMA-done ISR exactly when the DMA reloads cur_fb_index), and
pick the decode target as the fb that is neither pending display
nor scanning. Three buffers minus at most two excluded roles =
always a free one; no waiting on vsync anywhere.

Instrumented: /state tear_guard_engaged counts back-buffer picks
made while the previous fb was still mid-scan — each one is a
frame that would have torn under double buffering.
</content>
</entry>
<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>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>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>
</feed>
