<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/TESTING.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-20T16:40:26+00:00</updated>
<entry>
<title>phase 6: performance review playbook in TESTING.md + UDP-vs-TCP rationale in stream_server</title>
<updated>2026-06-20T16:40:26+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-20T16:40:26+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=6e0e0270e2cb9c54b1b7e63d9e67d8ac16792cef'/>
<id>urn:sha1:6e0e0270e2cb9c54b1b7e63d9e67d8ac16792cef</id>
<content type='text'>
Two pieces of documentation, neither changes behavior:

1. TESTING.md gains a "Performance review playbook" section: per-
   session capture commands for firmware serial + /state poll +
   Scrypted console, an annotated walkthrough of what each log
   shape means, an investigation-threshold table mapping
   user-facing symptoms to likely causes and the first thing to
   check, and a one-line tools list. Replaces the implicit "ask the
   maintainer how to debug" loop with a reproducible workflow.

2. stream_server.c gains a "Why TCP and not UDP" header comment
   documenting the analysis from the design phase. JPEGs are ~123
   IP datagrams at 1500 MTU; on hardwired Gigabit LAN switch-fabric
   loss is &lt; 1e-9/packet → per-frame corruption ≈ 1.2e-7. UDP's
   theoretical wins (no Nagle, latest-wins semantics) don't apply
   because TCP_NODELAY is on, socket.write p50 &lt; 1ms, and the
   FIONREAD trick already implements latest-wins on the receive
   side. UDP's costs (200-400 LOC of app-layer fragmentation, loss
   of nc/curl debug, FIONREAD trick stops working under
   fragmentation) are real. Documented as reference so a future
   contributor doesn't re-derive the analysis from scratch.
</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>TESTING: M5 ✅ verified 2026-06-14</title>
<updated>2026-06-14T23:12:46+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T23:12:46+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=d4fcec3a4d012332e7c476562f9363977ac65561'/>
<id>urn:sha1:d4fcec3a4d012332e7c476562f9363977ac65561</id>
<content type='text'>
</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>http_api: bump httpd stack to 8 KiB — POST /config was overflowing</title>
<updated>2026-06-14T21:51:17+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T21:51:17+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=ba396daafb4f37a0635a448eec5a001861914884'/>
<id>urn:sha1:ba396daafb4f37a0635a448eec5a001861914884</id>
<content type='text'>
POST /config has ~2.4 KiB of stack locals (2 KiB body buffer + the
scrypted URL + viewport name + cJSON parser frames) which overran the
default 4 KiB httpd task stack and tripped the stack-protect canary
mid-handler. The request body was applied to RAM + NVS, but the
handler crashed before sending the response, so curl saw a connection
reset and the device rebooted into "Stack protection fault".

M4 + M6 ✅ verified 2026-06-14 after the bump:
  - POST /config full / partial → 204; survives reboot via NVS
  - 5 validation failure modes → 400
  - POST /state wake/sleep → 204; idempotent repeats → 204
  - POST /frame while asleep → 409 with expected body
  - idle timer fires after idle_timeout_ms; 0 correctly disables
</content>
</entry>
<entry>
<title>TESTING: M3 / M7 / M8 ✅ verified 2026-06-14</title>
<updated>2026-06-14T21:42:29+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T21:42:29+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=2daab24dac1ed6f2c3054221d2464db7358816f6'/>
<id>urn:sha1:2daab24dac1ed6f2c3054221d2464db7358816f6</id>
<content type='text'>
Hardware bring-up of the Hosyond panel, FT5426 touch, and local
screens are all confirmed working on the Waveshare ESP32-P4-ETH.

Also retire the resolved hardware unknowns (DSI pin count, I²C pins,
BOOT button) — they're answered in the body now.

M4 / M5 / M6 still need ethernet + a curl-driven run to flip.
</content>
</entry>
<entry>
<title>M3 WIP: DSI bring-up + state machine + identity overlay</title>
<updated>2026-06-14T20:53:33+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T20:53:33+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=d5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2'/>
<id>urn:sha1:d5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2</id>
<content type='text'>
DSI / panel:
- LDO_VO3 acquired at 2500 mV (VDD_MIPI_DPHY) — without this the PHY
  PLL busy-waits forever inside esp_lcd_new_dsi_bus.
- PSRAM bumped to 200 MHz (CONFIG_IDF_EXPERIMENTAL_FEATURES) to keep
  the DPI fed without underruns.
- Pi-7"-style 800x480 panel init (REG_POWERON, PORTA/PORTB/PWM/PORTC)
  + 16-bit RGB565 framebuffer with portrait-mode rotation buffer in
  PSRAM. Currently shows garbled output — known issue, next commit
  switches to the TC358762-bridge-aware init sequence.

State + UX:
- viewport_state simplified to AWAKE/ASLEEP only; "unconfigured" is
  a flag, not a state. Tap always toggles; content choice (identity
  vs frame) is driven by the configured flag.
- BOOT button arms a 15 s identity overlay via local_screens_overlay;
  expired callback returns to prior state.
- Boot-done indicator: two backlight flashes (no usable LED GPIO on
  the Waveshare board — GPIO 35 BOOT is shared with EMAC TXD1).
- Best-effort subsystem init in app_main; display init deferred to
  its own task so a panel hang can't block networking.

Display test pattern: solid R/G/B for 2 s each on boot to characterize
the garble independent of text rendering.
</content>
</entry>
<entry>
<title>M1: link-loss &amp; recovery verified 2026-06-14</title>
<updated>2026-06-14T17:31:05+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T17:31:05+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=8d740665777af6d74b334e2dee98a53ebb9a29aa'/>
<id>urn:sha1:8d740665777af6d74b334e2dee98a53ebb9a29aa</id>
<content type='text'>
Real production failure mode for PoE-only deployments: a switch
reboot or briefly down VLAN yanks Ethernet without cutting power.
Confirmed the device handles it cleanly (no panic, no reset, no
state loss) and re-acquires DHCP on link-up.

Serial trace from the bench:

  W (748316) net_eth: link down                      # cable pulled
  I (916316) net_eth: link up, mac e8:f6:0a:e0:90:94 # plugged back in
  I (924816) net_eth: got ip 10.0.13.83 ...           # DHCP renewed ~8.5s later

Same boot throughout — uptime kept climbing past the disconnect, no
ESP reset. The HTTP server stayed bound and the mDNS responder task
kept running through the outage; both resumed serving the moment
DHCP came back. free_heap and free_psram identical to pre-disconnect
(no leak from the cycle).

TESTING.md M1 entry now captures this with the actual log line
sequence. The integration-suite "Cable pull mid-frame" item gets a
✅ for the idle/no-Scrypted variant; the actual mid-frame Scrypted
variant still pending.
</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>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>
</feed>
