<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/main/nvs_config.c, 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-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>Tidy: delete dead code, inline single-use helpers, fix double-lock</title>
<updated>2026-06-14T23:30:44+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T23:30:44+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=186fd85422f99dfb59d89ae9338dc50627f7c3e5'/>
<id>urn:sha1:186fd85422f99dfb59d89ae9338dc50627f7c3e5</id>
<content type='text'>
Code-review pass after the M5 colour fix. -126 net lines (-215 / +89).

Dead code removed:
- display_fill, display_test_pattern — M3 bring-up self-tests, no callers
- net_eth_is_up — no callers
- nvs_config_reset — no callers (factory-reset gesture was removed earlier;
  NVS wipe now goes through `idf.py erase-flash`)

Helpers inlined or collapsed (each had one call site):
- state_name / orientation_name — ternaries at the cJSON site
- fmt_bytes / fmt_uptime — inline scope in local_screens_show_info
- expected_dims (http_api) + effective_dims (local_screens) — merged into
  viewport_state_effective_dims() in viewport_state, one canonical source

state_machine cleanup:
- arm_idle_timer_unlocked / disarm_idle_timer collapsed to one arm_idle_timer(ms)
  that takes the snapshotted timeout (ms==0 disables). Removes the misleading
  name and the second viewport_state lock acquisition per painted frame.
- state_machine_set + state_machine_frame_painted now snapshot state + idle_ms
  under one lock.

mdns_service cleanup:
- snapshot_state / apply_hostname / apply_txt collapsed into a single
  apply_state(include_hostname) helper; mdns_service_start grabs hostname
  once for hostname_set + log instead of going under the lock three times.

http_api cleanup:
- POST /config brightness-changed path uses the local `bright` it already
  validated instead of re-locking + re-reading st-&gt;brightness.
- Trimmed verbose bring-up comments (stack-size justification, M5 saga,
  DSI shadow struct) to one line each — kept the load-bearing facts.

Smoke-tested on hardware: boot clean, display + JPEG + touch all up
([DJT]), info screen renders, no crashes.
</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>M4: NVS-backed /config with partial updates + validation</title>
<updated>2026-06-14T03:42:14+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T03:42:14+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=e7feac61e5ea275f303574fedd942ebed8fc8e73'/>
<id>urn:sha1:e7feac61e5ea275f303574fedd942ebed8fc8e73</id>
<content type='text'>
nvs_config.{h,c} — persist the runtime config (viewport, scrypted,
idle_timeout_ms, orientation, brightness) under a single NVS namespace.
nvs_config_load() applies persisted values over the in-RAM defaults on
boot and flips state from UNCONFIGURED to ASLEEP once both name and
Scrypted URL are present. nvs_config_save() commits the whole record
atomically.

http_api.c — add GET /config and POST /config:
  - GET serializes viewport_state to the spec's JSON shape, with null
    for unset string fields and defaults filled in for the rest.
  - POST is partial: each field is optional; only present fields are
    validated and applied. Validation runs on a staged copy and errors
    short-circuit with 400 + reason before any state mutation, so a
    rejected request leaves the device untouched.
  - Validation rules: viewport non-empty &lt;64 chars; scrypted starts with
    http:// and &lt;256 chars; idle_timeout_ms 0 or &gt;=5000; orientation in
    {portrait,landscape}; brightness 0..100.
  - Side-effects fire after the lock + save: brightness change pushes
    PWM to the panel MCU; viewport/orientation change reapplies mDNS
    hostname + TXT.
  - 204 on success; 400 with a single-line reason on validation error.

app_main calls nvs_config_load() right after viewport_state_init(), so
mdns_service_start() and display_init() see the persisted hostname,
orientation, and brightness from the first packet/PWM.

Build clean against ESP-IDF 5.4 (binary ~620 KB).

TESTING.md M3 now documents the Hosyond jumper wiring (5V/GND/SDA=GPIO7/
SCL=GPIO8 from board to panel header; DSI FPC carries only the high-
speed lanes). M4 entry expands the validation matrix and side-effects
to verify.
</content>
</entry>
</feed>
