<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/README.md, branch v1.1.0</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.1.0</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.1.0'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-06-15T12:59:44+00:00</updated>
<entry>
<title>sdkconfig: revert TCP window bump — net-negative on /frame path</title>
<updated>2026-06-15T12:59:44+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T12:59:44+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=ec8d498ba5040b7ba862211975d2043baa6e5536'/>
<id>urn:sha1:ec8d498ba5040b7ba862211975d2043baa6e5536</id>
<content type='text'>
Bumping WND/SND_BUF to 32 KiB + RECVMBOX to 16 + SACK on regressed
ttfb from ~350µs to ~1.2ms across every frame and introduced periodic
250+ms body stalls (TCP retransmit on the larger window). Mean body
unchanged. Reason: Scrypted's per-frame fetch() opens a fresh TCP
socket, so the larger receive window just slows slow-start on every
new connection rather than helping.

Reverted to lwIP defaults. The real fix is HTTP keep-alive on the
Scrypted side; will revisit window tuning once persistent connections
are in place. README backlog updated with what we learned.
</content>
</entry>
<entry>
<title>README: refresh per-frame budget with double-buffered measurements; network body is now the only big lever</title>
<updated>2026-06-15T12:52:34+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T12:52:34+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=d949b19a6b89b9e74e8706bafa1d1396ac8f17f8'/>
<id>urn:sha1:d949b19a6b89b9e74e8706bafa1d1396ac8f17f8</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scrypted: don't reset idle timer on each painted frame + finer timing</title>
<updated>2026-06-15T12:41:45+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T12:41:45+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=b3217b9dbb731ec69cd1ebde201aa2178143d961'/>
<id>urn:sha1:b3217b9dbb731ec69cd1ebde201aa2178143d961</id>
<content type='text'>
Two fixes plus a README refresh:

1. scrypted: pushStreamFrame previously reset the per-stream idle timer
   on every successful /frame response. That made the timer anchored to
   "frames are flowing" rather than to "the camera event that triggered
   the stream", so a continuously-streaming source would never let the
   stream time out. Removed the reset. The startStream → stopStream(false)
   cancel-and-replace path on repeated events still keeps the stream
   alive while the event keeps firing; idle (no new events) now actually
   ends the stream at idle_timeout_ms.

2. firmware: break the previous coarse recv/dec/paint timing into
      lock  : try_lock returned
      ttfb  : first httpd_req_recv chunk landed
      body  : remaining bytes received
      dec   : hardware JPEG decode
      paint : esp_lcd_panel_draw_bitmap returned
      post  : state-counter bookkeeping + unlock
   Logged every 10 frames at INFO. Splits the previously-fat recv bucket
   into TCP/HTTP handshake overhead (ttfb) vs wire-time (body), and
   surfaces any tail bookkeeping cost.

3. README: replace the stale "5 fps ceiling caused by CPU RGB conversion"
   guess with the actual measured per-phase budget and re-rank the
   backlog accordingly. Double-buffering the panel (paint 24 ms → ~2 ms)
   is now the highest-value next move; the previously-listed DMA-2D
   rewrite is moot because the CPU loop is already gone.
</content>
</entry>
<entry>
<title>README: refresh What's next — M1–M8 ✅, list current backlog (DMA-2D first, OTA next, ...)</title>
<updated>2026-06-15T12:06:27+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T12:06:27+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=78a1a327b23bf216c1021a06d22a67795176e288'/>
<id>urn:sha1:78a1a327b23bf216c1021a06d22a67795176e288</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Drop VIEWPORT_STATE_UNCONFIGURED — state is just awake/asleep</title>
<updated>2026-06-14T23:16:46+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T23:16:46+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=4a6bbdb075a5e4f9910cb9b85e615ff8c50aa4ec'/>
<id>urn:sha1:4a6bbdb075a5e4f9910cb9b85e615ff8c50aa4ec</id>
<content type='text'>
\`state\` now reports only the screen's runtime state (awake or asleep).
Whether a viewport is set up to talk to Scrypted is a separate
\`configured\` flag, derived from \`viewport_name &amp;&amp; scrypted_url\`.
There's no third state.

Behaviour changes:
- POST /state always succeeds; the previous 409 "device unconfigured"
  path is gone. The screen toggles regardless of /config status.
- POST /config now sets \`configured\` directly from the derived
  predicate instead of mutating the state enum.
- Outbound state-client POST to Scrypted is still gated on a scrypted
  URL being present — that's the only thing the configured flag now
  actually controls in the runtime path.

GET /state JSON unchanged in shape, but \`state\` is now never
"unconfigured" — that's reported through the existing \`configured\`
boolean instead.
</content>
</entry>
<entry>
<title>Info screen: full /config + /state dump, replaces identity screen</title>
<updated>2026-06-14T22:37:51+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T22:37:51+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=cbc4904aa35191f59aff93df5ed36f09f4cfa943'/>
<id>urn:sha1:cbc4904aa35191f59aff93df5ed36f09f4cfa943</id>
<content type='text'>
Previously the 4-line "identity" screen showed only viewport name, mDNS
host, IP, and state. Expanded to 15 lines covering the full GET /config
+ GET /state output (name, host, ip, state, configured, scrypted,
orientation, brightness, idle, fw, uptime, frames, errs, free heap,
free PSRAM), label/value pairs left-aligned with auto-scaled font.

Renames "identity" → "info" throughout — symbol, log messages,
README/TESTING references.

Also:
- Move the info-screen render's big locals (~1.6 KiB: lines[16][80] +
  scrypt[256] + vp_name[64]) to BSS. The touch task's 3 KiB stack
  was overflowing on every long-press, leaving whatever frame was
  previously on the panel — gave the appearance of a "blue screen"
  after the M5 test pattern.
- Drop the ≥5 s touch factory-reset gesture and remove all stale
  references to it in docs and comments. NVS wipe is now a USB-side
  `idf.py erase-flash` operation only.
- M5 follow-up TODO in jpeg_decoder.c: solid green renders ~black and
  solid blue renders green; not a byte-order issue (BGR setting turns
  red into blue). Tracked as a known M5 gap.
</content>
</entry>
<entry>
<title>M1 + M2 hardware-verified 2026-06-14 (Waveshare ESP32-P4-ETH)</title>
<updated>2026-06-14T17:20:18+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T17:20:18+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=14c94c7e9e90322d5486100687c5096213ab04db'/>
<id>urn:sha1:14c94c7e9e90322d5486100687c5096213ab04db</id>
<content type='text'>
First end-to-end bench verification. Board is the Waveshare
ESP32-P4-ETH (Amazon B0FN7JQ2V8), 32 MB PSRAM, 32 MB flash silkscreen,
ESP-IDF v5.4.1. No panel attached, just board on USB power + LAN.

What passed
-----------
M1 — Ethernet driver up; DHCP lease landed at 10.0.13.83 (gw
10.0.13.1, netmask 255.255.255.0). MAC e8:f6:0a:e0:90:94. 31.7 MB
PSRAM free. Boot completes in ~4s after the kernel handoff.

M2 — GET http://10.0.13.83/state returns the full spec JSON shape
(name=null, configured=false, state=unconfigured, resolution=480x800,
ip=10.0.13.83, free_heap=520995, free_psram=31730048, all counters at
0). mDNS browse from macOS (dns-sd -B _scrypted-viewport._tcp local.)
finds one instance named "viewport" within ~100ms.

Bare-board behavior (Ethernet unplugged) verified too: boot completes
cleanly with summary line `boot complete — subsystems [EMHdJ-B]
ip=(no link)`. Lowercase d = display down because no panel attached;
- for touch because it shares the panel I²C bus.

README status snapshot updated to reflect M1+M2 ✅ on hardware;
TESTING.md M1 and M2 sections gain the 2026-06-14 verification note
plus the three fixes that bring-up exposed (RXD0/RXD1 swap, mDNS
hostname order, elegant subsystem degradation — already shipped in
commit 220ee4c).
</content>
</entry>
<entry>
<title>M1+M2 hardware bring-up: 3 fixes + elegant subsystem degradation</title>
<updated>2026-06-14T17:11:43+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T17:11:43+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=220ee4cdc9b372e2a2b41ee48471ad0893b3a68a'/>
<id>urn:sha1:220ee4cdc9b372e2a2b41ee48471ad0893b3a68a</id>
<content type='text'>
First flash to real hardware (Waveshare ESP32-P4-ETH on USB power,
no LAN cable, no panel attached) exposed three bugs and the need to
degrade gracefully when peripherals are missing.

1. net_eth.c: RXD0 / RXD1 GPIO swap. ESP32-P4's EMAC iomux table
   (components/soc/esp32p4/emac_periph.c) fixes RXD0 to GPIO 29 and
   RXD1 to GPIO 30. The Waveshare-wiki/ESPHome research had them
   transposed. Symptom was:
     E (esp.emac.gpio): invalid RXD0 GPIO number
     E (esp.emac): esp_eth_mac_new_esp32 failed
     -&gt; ESP_ERROR_CHECK abort
   Fix: swap the two #defines. CRS_DV / TXD0 / TXD1 / TX_EN / REF_CLK
   pinout was already correct.

2. mdns_service.c: mdns_service_add() rejects with INVALID_ARG when
   the hostname isn't set yet (see mdns_responder.c:771 — first guard
   in mdns_service_add_for_host). We were setting hostname AFTER
   service_add inside apply_records(). Restructure mdns_service_start
   to: init -&gt; hostname -&gt; service_add -&gt; txt. apply_hostname() and
   apply_txt() helpers reuse the same snapshot under viewport_state's
   mutex; mdns_service_refresh() re-applies both.

3. app_main.c: every subsystem now best-effort instead of
   ESP_ERROR_CHECK abort. A single missing peripheral can't take down
   the rest of the firmware. End-of-boot summary line:
     boot complete — subsystems [EMHdJ-B]  ip=(no link)
   Uppercase letter = up, lowercase = down. E=Ethernet M=mDNS H=HTTP
   D=Display J=JPEG T=Touch B=BootButton. Touch shows '-' when display
   didn't come up (it shares the panel I2C bus). On a stripped board
   (just ESP32-P4-ETH on USB, no panel, no LAN) the line above prints
   and the device serves /state, /config, and mDNS over the loopback;
   plugging Ethernet in later picks up DHCP without a reboot.

Side-effects: the DHCP timeout in app_main shortened from 30s to 15s
so a no-cable boot finishes quickly. Reaching the "ready" state with
no link is fine — the driver keeps the link-up event handler armed and
gets the IP whenever a cable appears.

Verified on hardware (bare ESP32-P4-ETH, no LAN, no panel):
- ESP-IDF v5.4.1, esp32p4, 32 MB PSRAM detected, 16 MB flash config
  (actual 32 MB silkscreen — keeping 16 MB until access-beyond-16MB
  support lands in flash driver).
- Ethernet driver started — MAC e8:f6:0a:e0:90:94.
- mDNS up advertising viewport.local on _scrypted-viewport._tcp:80.
- HTTP up listening :80.
- JPEG decoder ready.
- Display reports "panel MCU @0x45 unreachable" — exactly correct
  for no-panel state.
- Touch skipped as designed.
- BOOT button registered on GPIO 0 (still a guess; harmless if wrong).

This is enough to flip M1's "code" status to ✅ on hardware once
Ethernet is plugged in. M2 (mDNS browse + GET /state) ready to verify.
</content>
</entry>
<entry>
<title>Identity screen: name + hostname + IP + state, auto-scaled</title>
<updated>2026-06-14T16:39:08+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T16:39:08+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=b22d05901dee4000ee012af470ddf898579257ae'/>
<id>urn:sha1:b22d05901dee4000ee012af470ddf898579257ae</id>
<content type='text'>
local_screens_show_ip() now renders four lines instead of two —
all the device's runtime identity at once:

  line 1: viewport name      ("mudroom"  / "viewport" if unconfigured)
  line 2: mDNS hostname      ("viewport-mudroom.local" / "viewport.local")
  line 3: IP                 ("192.168.1.42" / "no network")
  line 4: state              ("awake" / "asleep" / "unconfigured")

Same screen is shown on first boot, after factory reset, and as a
15s BOOT short-press overlay — operator always has the device's full
identity one button-press away (find it on the LAN, confirm name +
configuration without curling).

Font scale is auto-picked: the largest integer scale (1×–6×) where
the longest line fits within 90% of width AND all four lines plus
inter-line spacing fit within 90% of height. Works for both portrait
(480×800) and landscape (800×480) without separate code paths.

Font expanded to cover the new strings:
- All lowercase a–z (added b, f, h, j, k, m, q, s, u, x, y, z).
- Punctuation: dash, slash (digits, period, colon already covered).

Fallback policy unchanged: unsupported chars render as blank, which
keeps the table tight and the failure mode visible rather than
crashing.

README "Local rendering" + TESTING.md M8 updated to describe the
four-line identity layout and show both the unconfigured-boot and
configured-overlay example outputs.

Build verified clean against ESP-IDF 5.4 (binary ~873 KB, +1.6 KB
for the expanded font).
</content>
</entry>
<entry>
<title>Consolidate docs: merge DESIGN.md into README; sharpen status + roadmap</title>
<updated>2026-06-14T16:15:09+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T16:15:09+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=b7cf2f533749a577cc801ddf317691995d0bd23f'/>
<id>urn:sha1:b7cf2f533749a577cc801ddf317691995d0bd23f</id>
<content type='text'>
DESIGN.md was ~80% redundant with README.md (project summary, hardware,
API contract, callback contract, scrypted integration, milestone
acceptance criteria) and ~20% implementation guidance that's now better
served by the source code itself + TESTING.md. Removed it and folded
the unique parts into README.

README.md additions:
- "Status" table at the top: firmware, Scrypted side, hardware — each
  with where-we-are + what's-pending columns.
- "Firmware implementation notes" section: source map (one-line per
  main/*.{h,c} module), memory strategy, display strategy, error
  handling, coding standards. Replaces DESIGN.md sections 7-11, 15, 17.
- "What's next" section before Philosophy: nine items in priority
  order, each linking the specific TESTING.md or scrypted/ section.
  Makes "pick up cold" trivial.
- Philosophy section absorbed DESIGN.md's "Guiding Rule" as a
  blockquote at the bottom.
- "Related docs" rewritten — TESTING.md is now described as *the*
  self-contained verification reference.

TESTING.md additions:
- Preamble now describes the five-layer structure and the
  ⬜/🟡/✅ legend.
- New "Status snapshot" table — milestones + code + HW at a glance.
- New "Hardware prerequisites" — table of what to confirm/order,
  plus a numbered list of the four open unknowns (DSI FPC pin count,
  I²C jumper destinations, BOOT GPIO, flash silkscreen).
- New "Recommended bench order" — seven stages from "board on
  Ethernet over USB" through "integration suite," each with the
  commands or where-to-look.

DESIGN.md deleted. Single source of truth is now README.md (spec +
implementation notes + roadmap) and TESTING.md (verification
reference).
</content>
</entry>
</feed>
