<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/main/touch.c, 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-15T01:11:21+00:00</updated>
<entry>
<title>Default name = MAC; brief wake on boot; drop dead touch defences</title>
<updated>2026-06-15T01:11:21+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T01:11:21+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=865c4859d710870245ae7954e729edcaf442a921'/>
<id>urn:sha1:865c4859d710870245ae7954e729edcaf442a921</id>
<content type='text'>
- Seed viewport_name with the full base MAC, colons stripped
  (e8:f6:0a:e0:90:94 → "e8f60ae09094"). Stable across reboots, globally
  unique, 12 alphanumeric chars — well inside the mDNS hostname limit
  even with the "viewport-" prefix.
- Add a mac_str field to viewport_state and expose it as the new "mac"
  key in GET /state and as a "mac" line on the info screen.
- "Configured" no longer requires a viewport_name (it always has the
  MAC default); the scrypted URL alone gates outbound POSTs and the
  loading-vs-info screen choice.
- Strip viewport_name[0] fallbacks in http_api / mdns / local_screens
  now that the field is never empty.
- Replace the wake-on-boot-stays-on behaviour with a ~600 ms flash
  followed by sleep — long enough for the FT5426 touch IC to come out
  of its initial unresponsive state, short enough that an idle device
  doesn't burn the backlight.
- Drop the touch defensive cruft added when we were chasing a PoE-power
  theory: the DEV_MODE=0x00 write at init, the touch_reset_pulse() call
  at init, and the runtime wedge-self-heal in the polling loop didn't
  actually fix anything — the wake-on-boot flash did. Also delete the
  display_touch_reset_pulse() helper since it now has zero callers.
</content>
</entry>
<entry>
<title>touch: wake on boot — FT5426 only reports while LCD is streaming + lit</title>
<updated>2026-06-15T00:58:45+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T00:58:45+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=8e2de89efb540fd6a3dce2086216d3afc49e6831'/>
<id>urn:sha1:8e2de89efb540fd6a3dce2086216d3afc49e6831</id>
<content type='text'>
Symptom: after a few cleanup cycles + power cycles, fresh boots left
touch dead — the FT5426 ack'd, polling read TD_STATUS=0x00 cleanly,
but taps never registered. Curl-waking the screen made touch work
again instantly; tapping the dark backlight-off panel did nothing.

Root cause: on this panel the FT5426 firmware only reports contact
points while the DSI link is actively streaming with the backlight on.
Booting straight into ASLEEP per the spec leaves taps silently unread,
and there's no built-in way to wake without touch (no hardware button
on this board — see prior commit). The fix is to wake on boot; the
idle timer cuts back to sleep after idle_timeout_ms if nothing happens.

Also defensively write DEV_MODE=0x00 (Working Mode) at touch_init,
and add a runtime self-heal: if the polling loop sees stuck TD_STATUS
above the 5-touch ceiling for ~3 s, pulse PC_RST_TP_N via the panel
MCU and retry. Combined with display_touch_reset_pulse() the chip
recovers from cold-boot wedges without manual intervention.
</content>
</entry>
<entry>
<title>touch: retry + hard-reset when FT5426 boots into wedged 0xff state</title>
<updated>2026-06-15T00:43:13+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-15T00:43:13+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=5bed2f0ccc9ca3bab80e1d2ed2a9dddc621428c6'/>
<id>urn:sha1:5bed2f0ccc9ca3bab80e1d2ed2a9dddc621428c6</id>
<content type='text'>
After a long bench session of taps + power cycles, the FT5426 on this
unit started returning dev_mode=0xff on every read. The polling loop
interpreted that as 15 simultaneous touches forever (td_status bits
0..3 all set), wedged `was_down=true` permanently, and no tap or
long-press ever fired — the screen looked dead.

Two layers of defence in touch_init:
  1. retry the dev_mode read up to 10 × 30 ms in case the chip is just
     slow to come up after PC_RST_TP_N is released.
  2. if still stuck, pulse PC_RST_TP_N via a new
     display_touch_reset_pulse() helper (drop reset for 20 ms then
     release + 50 ms settle), then retry the read another 10 × 30 ms.

Plus a polling-loop sanity check: an FT5x06 supports max 5 simultaneous
points, so a TD_STATUS lower-nibble value &gt; 5 is bogus and the poll
cycle is skipped. That keeps a wedged chip from producing phantom
"15 touches" reports if it slips into 0xff during runtime.

If both attempts still report 0xff, we log loudly and let the polling
task run — it harmlessly drops cycles via the &gt;5 sanity check. Wake
still works via POST /state from Scrypted; this only disables touch
gestures on a hardware-stuck boot.
</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>touch: drop factory-reset gesture</title>
<updated>2026-06-14T21:57:43+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T21:57:43+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=18ddde394832beb812181003016ff73498ac72a3'/>
<id>urn:sha1:18ddde394832beb812181003016ff73498ac72a3</id>
<content type='text'>
Holding the touch panel for 5 s to wipe NVS is dangerous (easy to
trigger by accident — phone resting on the panel, kids, etc.) and
useless as a recovery path (factory reset alone doesn't undo a bricked
config; a USB reflash does). Factory reset should be an explicit API
or USB-side action.

Touch gestures now: tap = toggle wake/sleep, ≥1.5 s hold = identity
overlay.
</content>
</entry>
<entry>
<title>M3 done: drop boot test, fold BOOT button into touch long-press</title>
<updated>2026-06-14T21:41:03+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T21:41:03+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=c6ce4b3f46a1f1adf700306dadc9034a6a2ac0d3'/>
<id>urn:sha1:c6ce4b3f46a1f1adf700306dadc9034a6a2ac0d3</id>
<content type='text'>
The Waveshare ESP32-P4-ETH board exposes BOOT on GPIO 35, but at runtime
that pin is owned by the EMAC TXD1 signal. No usable GPIO is wired to a
separate user button, so the BOOT-button task could never fire.

Move both BOOT-button behaviours onto the touch panel:
  - ≥1.5s hold → 15s identity overlay (was BOOT short-press)
  - ≥5s hold   → factory reset (was BOOT long-hold)
Short tap (&lt;500ms) still toggles wake/sleep. Long-press fires while the
finger is still down so the user gets immediate feedback at each
threshold.

Also strip the R/G/B 6 s boot test sequence — the panel now renders
correctly, so it's no longer useful diagnostically.

Boot subsystem flags drop the trailing B column: [EMHDJT].
</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>M7: touch + outbound /state POST to Scrypted</title>
<updated>2026-06-14T04:01:23+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T04:01:23+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=1d6cbb222ced226fcb482c0b19130774d181c8f8'/>
<id>urn:sha1:1d6cbb222ced226fcb482c0b19130774d181c8f8</id>
<content type='text'>
state_client.{h,c}:
- Worker task drains a depth-1 queue (xQueueOverwrite gives the
  replace-on-full semantics from the spec — in-flight POST is never
  cancelled; the next queued entry is overwritten by newer state).
- esp_http_client POST to &lt;scrypted&gt;/state with Content-Type application/
  json, User-Agent ScryptedViewport/&lt;version&gt;, Connection: close, 1s
  timeout. Body: {"viewport":"&lt;name&gt;","state":"wake"|"sleep"}.
- Any non-2xx or transport error increments state_post_failures and is
  otherwise ignored. Silently drops if no Scrypted URL is configured.

touch.{h,c}:
- FT5426 capacitive touch on the shared I2C bus at 0x38.
- 30ms polling task; tracks down/up transitions, detects taps as
  down-then-up within 500ms with a 150ms debounce.
- On tap, toggles wake/sleep via state_machine_set_local(), which drives
  the local transition AND fires state_client_post() at Scrypted.

state_machine adds state_machine_set_local(): runs the same transition
as state_machine_set() then enqueues an outbound POST. Idle-timer
expiry now uses this path so Scrypted sees idle-driven sleeps too
(the spec's "tighten the race with /frame 409" path stays in place as
the fallback).

display.h exposes display_i2c_bus(); touch.c uses it instead of
re-initializing the same I2C port.

app_main starts state_client right after the state machine and starts
touch after display init (touch is skipped if display isn't up since
they share the bus).

CMakeLists.txt: add esp_http_client to REQUIRES.

Build clean against ESP-IDF 5.4 (binary ~860 KB; jumped ~210 KB from
M6 because esp_http_client + cJSON path pulls in tcp_transport, mbedtls,
http_parser).

TESTING.md M7: flask-based test receiver, tap dispatch verification,
failure-path check (kill receiver, confirm counter increments),
queue-coalescing behavior, no-POST-on-Scrypted-initiated, no-POST-
when-unconfigured.
</content>
</entry>
</feed>
