<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/esp32-poe-scrypted-viewport/Scrypted-Viewport-v2-Claude-Code-Implementation-Guide.md, branch v1.0.0</title>
<subtitle>ESP32-POE Scrypted viewport (private)
</subtitle>
<id>https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.0.0</id>
<link rel='self' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/atom?h=v1.0.0'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/'/>
<updated>2026-06-14T15:59:26+00:00</updated>
<entry>
<title>Rename implementation guide to DESIGN.md</title>
<updated>2026-06-14T15:59:26+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T15:59:26+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=47d8ed1904c9954363fe16f44cb8776412da6d57'/>
<id>urn:sha1:47d8ed1904c9954363fe16f44cb8776412da6d57</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Rename M7: Touch Callback -&gt; Touch + Outbound /state POST</title>
<updated>2026-06-14T02:38:15+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T02:38:15+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=01f8c1435a9c8a238e0ea04588e18421b6b01270'/>
<id>urn:sha1:01f8c1435a9c8a238e0ea04588e18421b6b01270</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Reframe protocol as REST peers; mDNS-SD discovery; explicit no-ack</title>
<updated>2026-06-14T02:31:28+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T02:31:28+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=f5b9f2f81fa71361418a475d801eb4adefcbc027'/>
<id>urn:sha1:f5b9f2f81fa71361418a475d801eb4adefcbc027</id>
<content type='text'>
Three structural shifts:

1. Discovery section spells out mDNS-SD. The ESP32 runs the ESP-IDF
   mdns responder and serves all .local records itself — A, SRV, PTR,
   TXT. Scrypted browses _scrypted-viewport._tcp.local with a Node
   mDNS-SD library that hits the multicast layer directly. Scrypted
   uses the IP from the SRV/A record, NOT OS-level .local hostname
   resolution. Re-browse every few minutes for DHCP renumbering.
   Manual host:port fallback for non-mDNS deployments.

2. Drop "callback" framing. The device and Scrypted are HTTP peers,
   both exposing POST /state with the same body shape {viewport,state}.
   Either side can push to the other to set state. Section is renamed
   "Device -&gt; Scrypted POST /state". No new vocabulary or semantics —
   just the truth about what the protocol is.

   - state_post_failures (was: callback_failures)
   - state_client.c (was: callback_client.c)
   - "callback" replaced with "outbound POST" / "inbound POST" / just
     "POST /state" throughout.

3. Explicit no-application-level-ack clause. HTTP 2xx is transport-
   only. Device does not retry, does not block subsequent state
   changes on the response, does not treat 5xx as anything beyond a
   counter increment. Idempotency + /frame 409 + each side's idle
   timer cover every failure mode. Scrypted-side logic must not wait
   for the device to confirm a state change.

Also: Scrypted Integration code in v2 guide rewritten to discover via
bonjour-service browse rather than hardcoded viewport-&lt;name&gt;.local
URLs. urlFor(name) function resolves the discovered IP per call.
mdns_service.c module spec now explicitly notes Scrypted uses the
browse, not the hostname.
</content>
</entry>
<entry>
<title>Consolidate REST API: 4 endpoints, partial /config, /state, GET /config</title>
<updated>2026-06-14T01:36:34+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T01:36:34+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=072504c2dfa19e4311e16e5c445d9cc19680dcf5'/>
<id>urn:sha1:072504c2dfa19e4311e16e5c445d9cc19680dcf5</id>
<content type='text'>
API surface is now:

  GET  /state    runtime status (replaces /health)
  GET  /config   persisted config
  POST /config   partial update; brightness is now a field here
  POST /state    {state: wake|sleep} (replaces /wake and /sleep)
  POST /frame    unchanged

POST /config now does a partial merge — only present fields are
written. To tweak only brightness, POST {"brightness": 50}.

POST /state replaces POST /wake and POST /sleep with a single
endpoint that takes {state: "wake"|"sleep"}. Idempotent: no-op if
already in the requested state.

Brightness is no longer its own endpoint; it lives in /config with
all the other persisted settings. PWM remains gamma-corrected.

Callback body renamed:
  was: {event: wake|sleep, type: tap|timeout}
  now: {state: wake|sleep, event: tap|timeout}

The new naming matches POST /state: "state" is the resulting wake/
sleep state and the imperative for Scrypted; "event" is the cause
(tap, timeout, future swipes).

Doc sync: README API/Wake-Sleep/Idempotency/Integration sections and
v2 impl guide API contract, modules, milestones, acceptance criteria
all updated to the new model.
</content>
</entry>
<entry>
<title>Reorder milestones so Config Persistence comes before Frame/Wake/Sleep</title>
<updated>2026-06-14T01:24:06+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T01:24:06+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=92cda972da3d1d8b6ed221687a1377765259f989'/>
<id>urn:sha1:92cda972da3d1d8b6ed221687a1377765259f989</id>
<content type='text'>
Previously M5 (Wake/Sleep + idle timer) and M4 (Frame) implicitly
depended on NVS values (orientation, idle_timeout_ms) that did not
exist until M6 (Config Persistence). Moving Config to M4 lets the
downstream endpoints read their parameters from NVS instead of
working around hardcoded defaults.

New order:
  M1 Board Bring-Up
  M2 HTTP + mDNS
  M3 Display Bring-Up
  M4 Config Persistence  (was M6)
  M5 JPEG Frame Push     (was M4)
  M6 Wake/Sleep/Brightness (was M5)
  M7 Touch Callback
  M8 Local Screens + BOOT button
  M9 Live Stream (/stream)

M4 now also validates orientation and idle_timeout_ms per spec,
applies orientation immediately (mDNS TXT + /health reflect it), and
establishes the brightness default (80) on first boot. M5's test
image is 480x800 by default (portrait); switch to 800x480 if /config
sets landscape. M6's idle timer reads idle_timeout_ms from NVS.
</content>
</entry>
<entry>
<title>Tighten protocol; add /wake, callback types, and orientation</title>
<updated>2026-06-14T01:22:11+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T01:22:11+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=0e315ea5069277a89a3330fedf702bd40f39ab09'/>
<id>urn:sha1:0e315ea5069277a89a3330fedf702bd40f39ab09</id>
<content type='text'>
State model is now strictly wake/sleep, owned by the device:
- Local triggers: tap (toggle), idle timeout (sleep only).
- Remote triggers: POST /wake, POST /sleep. Scrypted decides why
  (doorbell, motion, person, schedule) — the device does not know.
- /frame is pure paint: 204 when awake, 409 when asleep, no implicit
  wake. Eliminates the race where an in-flight frame could re-wake a
  device that just slept on a tap.

Orientation:
- Configurable via /config; default portrait.
- Effective resolution: 480x800 portrait, 800x480 landscape.
- Scrypted must render at the effective resolution. Device does not
  rotate or scale JPEG content.
- Orientation flows through mDNS TXT, /health, IP/Loading screens.

Callbacks:
- Add `type` field carrying the cause: tap or timeout (forward-compat
  for swipes etc).
- Drop wall-clock timestamp (no RTC, no SNTP).

Other:
- Boot: configured device boots to sleep (backlight off).
- /health adds state, last_frame_ms_ago, frame counters, error
  counters, brightness, orientation, idle_timeout_ms.
- idle_timeout_ms: 0 disables; non-zero must be &gt;= 5000; else 400.
- First-boot brightness default 80; apply gamma curve to PWM.
- Watchdog reboots hung tasks; recovers state from NVS.
- On-board status LED indicates network/config state.
- mDNS hostname-collision warning; ops notes.
- Idempotency table summarizing endpoint safety.
- Each viewport is bound 1:1 to a camera in Scrypted-side code.
- Reframe "no rendering engine" honestly as "no general-purpose UI
  framework"; IP and Loading screens are the only locally-drawn UI.

Doc sync: v2 implementation guide endpoints, modules, milestones,
acceptance criteria, error handling, and tests all updated to match.
</content>
</entry>
<entry>
<title>Spec wake/sleep, idle timeout, IP screen, BOOT button</title>
<updated>2026-06-14T00:58:56+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T00:58:56+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=869b47d1feb3f2f03c95d785d7aef270f31fb054'/>
<id>urn:sha1:869b47d1feb3f2f03c95d785d7aef270f31fb054</id>
<content type='text'>
- Couple backlight and Scrypted streaming as a single wake/sleep state owned by the device.
- Callback events are idempotent imperatives: wake = "start streaming", sleep = "stop streaming". Scrypted does not track viewport state.
- Each viewport is bound to one camera in Scrypted-side code; camera events (doorbell, person, motion) drive streaming.
- idle_timeout_ms set by Scrypted via /config (default 60000). Same value on both sides; timers run independently and either can end a session.
- Tap input only (long_press/swipes out of scope for v1). Tap is internal; callback carries the resulting state.
- IP screen on first boot, after factory reset, and as a 15s BOOT short-press overlay.
- Loading screen between wake and the next /frame.
- BOOT: short press = IP overlay, hold 5s = factory reset.
- Note that Scrypted side is code (Script for v1, custom plugin for v2 with /stream), not configuration.
</content>
</entry>
<entry>
<title>Flesh out protocol details and add implementation guide</title>
<updated>2026-06-14T00:13:48+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-06-14T00:13:48+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/esp32-poe-scrypted-viewport/commit/?id=3f385103739be139fb3a8423fc7d08a8b414c2ff'/>
<id>urn:sha1:3f385103739be139fb3a8423fc7d08a8b414c2ff</id>
<content type='text'>
Expand README with per-endpoint contracts (response codes, persistence,
size limits, idle/wake semantics) and a Scrypted integration section
covering registration, frame push, and touch handling. Add v2
implementation guide adapted from the DoorView spec.

Co-Authored-By: Claude Opus 4.7 (1M context) &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
