src.nth.io/

summaryrefslogtreecommitdiff
path: root/README.md
AgeCommit message (Collapse)AuthorFilesLines
33 hoursreadme: public-facing refresh — links up top, drop the v1-spec framing, ↵mainLuke Hoersten1-136/+307
120-col reflow Now that the repo is public: title becomes "Scrypted Viewport" with a what-it-is intro (0.7s wake-to-video, 24fps, double-digit-ms g2g) and up-front links to the write-up (nth.io/luke/projects/esp32-ethernet- display), the canonical src.nth.io repo, and the GitHub mirror. The "v1 Technical Design Specification" framing is gone (the v1/v2 script references now read as "the script" + the plugin-conversion plan), the Boot flow diagram is fenced, and all prose is reflowed to 120 columns (markdown tables exempt — they can't wrap).
3 daysdocs: prune TESTING.md to its forward-looking contentLuke Hoersten1-3/+3
The 730-line milestone-by-milestone bring-up log served its purpose — every milestone (M1-M9) is verified on hardware, and the per-run narratives are preserved in git history (last full version: 8eebc0e). What survives, restructured (~230 lines): - New-unit bring-up: hardware prereqs (all unknowns resolved), the flash -> panel -> touch -> Scrypted sequence, and the panel failure-mode signals table (the content you actually need the day a second viewport gets provisioned). - Regression recipes: condensed curl checks for config validation (incl. the 54-char name cap), frame push, state machine, OTA acceptance, and device-initiated POSTs. - Outstanding tests: the honestly-never-run backlog (mid-frame cable pull, 24h soak, frame storm, brown-out, multi-viewport). - Performance playbook: kept, with example log formats refreshed to the current stream instrumentation (the old examples showed the removed pre-stream snapshot path) and thresholds updated to the post-tuning baseline (24fps painted, pend_age as the backlog tripwire). README: pointers at TESTING's old structure updated; the Hardware status row no longer claims schematic confirmation is pending (those unknowns were resolved during bring-up — only the per-unit flash-size check remains).
3 daysdocs: mark M9 verified — live stream shipped as raw TCP :81, not POST /streamLuke Hoersten1-1/+1
M9 still carried the abandoned chunked-HTTP design and a pending status long after the raw TCP data-socket stream was verified at painted = sent = 24 fps (recv/decode task split d1c8d45 + TCP window/EMAC tuning). Rewrite the milestone to describe what shipped, flip it ✅, and bump the README's "M1 – M8" line to M9.
3 daysdocs: sync README + TESTING + scrypted README with v1.4.0 realityLuke Hoersten1-31/+42
Drift accumulated across the review-fix, mDNS-discovery, and tooling work: - Discovery: the design-era "use a Node mDNS library" flow is replaced with what shipped — the sandbox has no such libraries; the script does a dependency-free legacy-unicast dgram browse (no :5353 bind), host dropdowns, blank-name inheritance, and auto-heal on register failure (matched by the new mac TXT record) instead of periodic re-browsing. - /state example: add mac, ota_state, panel dims, tear_guard_engaged, temp_c, and the stream stats object; correct `configured` semantics (scrypted URL only) and the never-null MAC-derived name in /config. - /config: document the 54-char name cap (mDNS label limit). - scrypted/README: add-device + settings tables gain the discovered-host dropdown, Wake triggers on create, and the canonical "Viewport name" rename field (the Scrypted pencil doesn't propagate — by design); trigger-scoped subscriptions; device-wake immediate-204 semantics; idle=0 no longer force-slept by the safety timer; "manual IP" dropped from limitations; discovery section reframed with the diagnostic.ts probe and bridge-networking caveat. - Source map: dedupe the doubled touch.{h,c} row, add stream_server / ota / chip_temp rows, fix nvs_config + endpoint-count drift. - Build/TESTING: point iteration at `make ota` (USB stays for first flash); TXT expectations include mac; DHCP-renumber failure mode now documents the verified auto-heal path.
3 daysscrypted: plan the Scripts-script -> real-plugin conversionLuke Hoersten1-1/+2
PLUGIN-CONVERSION.md captures the full plan so a future session can pick it up cold: why (the Scripts sandbox's reload-leak machinery — cleaner registry, drain ordering, name-drift workarounds — exists only because of the paste-deploy model), the port map (what moves 1:1, what changes, what dies), open questions to verify (orphaned ffmpeg on worker restart, storage attach at startup), the cutover order (never run script + plugin together), and the verification suite. Linked from the README's Related docs and Status table.
3 daysreadme: document the Makefile build/OTA/verify workflowLuke Hoersten1-0/+23
5 daysscrypted: temp_c on the stats line; docs: triple-buffer model + window tuningLuke Hoersten1-5/+19
- Scrypted per-stream stats line gains temp=<c>C from /state temp_c — free thermal trending under streaming load. - README Display strategy rewritten for the tear-free triple-buffer model: why the deferred fb-index reload tears under double buffering, why scan-tracked buffer roles beat vsync-waiting, and the tear_guard_engaged counter. - New 'TCP window + EMAC tuning' section documenting the measured window raise, the EMAC-RX-pool-below-window RTO regression, and the pool >= TCP_WND invariant; stale 'window bump is safe but won't help' backlog text updated. - Memory strategy updated (3 fbs, decoder writes into them directly, stream body ring).
2026-06-30docs: reframe protocol sections for the TCP-socket streaming modelLuke Hoersten1-10/+12
The Wake/Sleep, race-handling, and idempotency sections still described the old model where Scrypted streamed via POST /frame and learned of sleep from a /frame 409. Streaming now runs over the TCP data socket (:81); the firmware /frame endpoint remains only for one-shot snapshots/debug. Reframe the recovery model accordingly: - race-free property: frames arriving while asleep are discarded by the decode task (stream socket) / rejected 409 (/frame) — neither re-wakes; sleep is learned from the device's state=sleep callback + Scrypted's safety timer - drop every "recovered by the next /frame returning 409" clause in favor of the safety timer + discard-while-asleep - idempotency table: add the stream-socket row (paints if awake, discarded if asleep, skip-oldest) - Loading screen: shown on wake AND each new stream connection until first paint - Philosophy: list the MJPEG stream server (:81) and /firmware Left the POST /frame endpoint's own API/semantics intact — it still exists.
2026-06-30docs: update main README streaming section to the shipped TCP-socket pathLuke Hoersten1-16/+17
The Scrypted Integration section still framed the snapshot POST /frame path as v1 and MJPEG streaming as a future POST /stream milestone, contradicting the already-current "What's next" perf section. Bring it in line: - v1 is the live ffmpeg MJPEG stream over the raw TCP data socket (:81) with prebuffer fast-start (~0.7s wake-to-video); POST /stream was never the shape - replace the /frame streaming code example with the TCP socket + framed-MJPEG sketch; brightness default 100 (was 80) - sleep is learned via the device's sleep callback, not a /frame 409 - Status table: streaming + end-to-end are done/verified, not "not started" - Idle: timer resets on any painted frame, not specifically a /frame POST Left intact: the POST /frame API + protocol/idempotency sections — that firmware endpoint still exists for one-shot snapshots/debug and its 409-when- asleep semantics are unchanged.
2026-06-20README: stream-server task split is the critical change that closed the ↵Luke Hoersten1-18/+19
painted<sent gap Document the architecture pivot. The per-frame budget table now reflects the long-lived TCP stream socket on port 81 (replaced the HTTP /frame pattern) and the new recv/decode/paint timings: recv 14-18ms on the wire (pure body), dec ~5ms, paint <35us, with decode-task idle ~30ms per frame waiting on recv. The critical change wasn't a TCP tuning knob — it was splitting recv off its own FreeRTOS task with a 3-buffer PSRAM ring and a 1-deep latest-frame slot to the decode/paint task (d1c8d45). The single-task loop blocked the socket for 6ms during decode+paint, which against the IDF-default 5760-byte window forced a stop-go cycle that capped painted at ~17fps. Raising the window to 65535 made it worse (g2g grew to 17s) because lwIP RX couldn't drain the bursts and there's no way to skip-oldest on the kernel queue. The task split eliminated the coupling: recv-task drains continuously regardless of decode timing, the kernel buffer stays near-empty, and painted now matches sent at the source rate. Backlog entries for body shrink and OTA marked done.
2026-06-20firmware: OTA firmware updates via POST /firmware + rollbackLuke Hoersten1-1/+59
Streams the raw .bin to the inactive ota_0/ota_1 slot via esp_ota_*, flips otadata, replies 200, reboots after 500 ms. Single-shot guarded by atomic_flag (409 on concurrent). CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE armed: new images boot pending-verify and ota_arm_healthy_timer marks them valid after 30 s of healthy uptime; otherwise the bootloader reverts on next reset. /state gains ota_state.
2026-06-15sdkconfig: revert TCP window bump — net-negative on /frame pathLuke Hoersten1-3/+3
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.
2026-06-15README: refresh per-frame budget with double-buffered measurements; network ↵Luke Hoersten1-22/+23
body is now the only big lever
2026-06-15scrypted: don't reset idle timer on each painted frame + finer timingLuke Hoersten1-11/+29
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.
2026-06-15README: refresh What's next — M1–M8 ✅, list current backlog (DMA-2D ↵Luke Hoersten1-11/+11
first, OTA next, ...)
2026-06-14Drop VIEWPORT_STATE_UNCONFIGURED — state is just awake/asleepLuke Hoersten1-5/+5
\`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 && 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.
2026-06-14Info screen: full /config + /state dump, replaces identity screenLuke Hoersten1-10/+14
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.
2026-06-14M1 + M2 hardware-verified 2026-06-14 (Waveshare ESP32-P4-ETH)Luke Hoersten1-1/+1
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).
2026-06-14M1+M2 hardware bring-up: 3 fixes + elegant subsystem degradationLuke Hoersten1-1/+3
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 -> 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 -> hostname -> service_add -> 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.
2026-06-14Identity screen: name + hostname + IP + state, auto-scaledLuke Hoersten1-2/+2
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).
2026-06-14Consolidate docs: merge DESIGN.md into README; sharpen status + roadmapLuke Hoersten1-0/+79
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).
2026-06-14M9-precursor: v1 Scrypted Script (snapshot-rate end-to-end)Luke Hoersten1-1/+1
scrypted/scrypted-viewport.ts — single-file TypeScript script for Scrypted's Scripts plugin. Binds N viewports to N Scrypted cameras and implements the full Scrypted side of the protocol: Outbound (Scrypted -> device): - POST /config on script load + every 5 min (re-syncs after device reboot or DHCP renumber). - POST /state {wake} when a bound camera fires an event or when the device's own POST /state {wake} arrives (operator tap). - POST /frame at FRAME_INTERVAL_MS (1 fps default) via camera.takePicture({picture:{width,height}}). - POST /state {sleep} when the Scrypted-side per-stream timer expires. Inbound (device -> Scrypted POST <base>/state): - {state:wake} -> startStream() (cancels pending sleep timer + any prior interval, then begins pushing frames). - {state:sleep} -> stopStream(sendSleep=false) (no echo back). - Unknown viewport -> 404. Bad body -> 400. Race rules from the spec are honored: - Every callback cancels prior pending state for that viewport before applying the new state (cancelPendingSleep semantics). - startStream / stopStream are idempotent — re-entering is safe. - On /frame returning 409, stream stops immediately without echoing /state {sleep} back (the device already slept). Configuration is a single BINDINGS array at the top of the file — edit name/host/cameraId/orientation per viewport and save in the Scrypted UI. No package install. Tuning constants (IDLE_TIMEOUT_MS, FRAME_INTERVAL_MS, REREGISTER_INTERVAL_MS, etc.) are right above the class. scrypted/README.md walks through Scripts-plugin install, the BINDINGS schema, what each constant controls, an event-by-event description of the script's behavior, the v1 limitations (snapshot-rate, manual IP, camera must respect picture dims), and an end-to-end smoke test. Top-level README's Scrypted Integration section now links the script file and the install README directly.
2026-06-14README: link Amazon SKUs + note Pi-style panel architectureLuke Hoersten1-2/+2
2026-06-13Reframe protocol as REST peers; mDNS-SD discovery; explicit no-ackLuke Hoersten1-56/+161
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 -> 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-<name>.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.
2026-06-13Consolidate REST API: 4 endpoints, partial /config, /state, GET /configLuke Hoersten1-69/+84
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.
2026-06-13Tighten protocol; add /wake, callback types, and orientationLuke Hoersten1-52/+116
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 >= 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.
2026-06-13Spec wake/sleep, idle timeout, IP screen, BOOT buttonLuke Hoersten1-16/+75
- 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.
2026-06-13Flesh out protocol details and add implementation guideLuke Hoersten1-18/+111
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) <[email protected]>
2026-06-13Initial Scrypted Viewport scaffoldLuke Hoersten1-0/+100
Plain ESP-IDF project targeting Waveshare ESP32-P4-ETH-POE with a 5" 800x480 MIPI-DSI capacitive touch panel. Stub app_main with TODOs for Ethernet, mDNS, HTTP API (/health, /config, /frame, /sleep, /brightness), JPEG decode, and touch callback delivery per the v1 spec in README.md.