| Age | Commit message (Collapse) | Author | Files | Lines |
|
state_machine.{h,c} — central wake/sleep transitions:
- state_machine_init() creates the esp_timer one-shot for the idle timer.
- state_machine_set(target) is idempotent and atomic. On AWAKE: backlight
on, idle timer (re)armed. On ASLEEP: idle timer cancelled, backlight off.
Rejects with INVALID_STATE when the device is unconfigured.
- state_machine_frame_painted() restarts the idle timer if awake; called
by /frame after each successful paint.
- Idle-timer callback transitions to ASLEEP. TODO M7 hook: outbound POST
{viewport, state:sleep} to <scrypted>/state.
http_api.c:
- POST /state: parse {state}, accept "wake"/"sleep", reject others 400.
Unconfigured device → 409 "device unconfigured". Already-in-state → 204
(idempotent no-op). Successful transition → 204.
- POST /frame: 409 Conflict when state != AWAKE. After successful paint,
call state_machine_frame_painted() so the idle clock keeps resetting
while frames stream.
app_main:
- Initialize state_machine before http_api so the route handler can drive
it from request 0.
- After display_init(), reconcile the panel with the boot state:
UNCONFIGURED → test pattern (placeholder until M8 IP screen)
ASLEEP → display_sleep() so a configured device boots dark
AWAKE → leave on (not reached on fresh boot)
Disable path: idle_timeout_ms=0 in /config means the timer is never armed
and a wake state persists until /state {sleep} or a power cycle.
Build clean against ESP-IDF 5.4 (binary ~645 KB).
TESTING.md M6 expands with idempotency checks, 409-when-asleep, 409-when-
unconfigured, idle-timer firing within idle_timeout_ms+slack, /frame
restarting the idle timer, idle-timer disable via idle_timeout_ms=0, and
the M7 dependency note about the missing outbound sleep POST.
|
|
jpeg_decoder.{h,c} wraps esp_driver_jpeg (ESP32-P4 hardware decoder).
- One-time engine + DMA-aligned PSRAM scratch buffer setup (1 MB input,
768 KB output @ panel native 800x480 RGB565).
- try_lock + unlock so concurrent /frame POSTs get 503 instead of
queueing, per spec.
- jpeg_decoder_get_info() reports the dimensions; the http handler
validates them against the effective resolution before painting.
display.h adds display_present_rgb565(src, w, h):
- Landscape: src is 800x480, memcpy 1:1 into the panel framebuffer.
- Portrait: src is 480x800, software rotate 90° CW into the 800x480
panel framebuffer. (PPA / 2D-DMA hardware rotation is a later
optimization if portrait latency matters.)
http_api.c adds POST /frame:
- Content-Type must be image/jpeg → else 400.
- Empty body → 400. > 1 MB → 413. Display not initialized → 500.
- jpeg_decoder_try_lock(0) for concurrency: second post returns 503.
- Body streamed into the decoder's input buffer in chunks.
- Decode failure or dimension mismatch → 400 + decode_errors++.
- Paint failure → 500.
- Success → frames_received++, last_frame_us = esp_timer_get_time(),
204 No Content.
app_main initializes the JPEG decoder after display_init(). Both are
best-effort: failures log a warning and leave the rest of the firmware
running.
CMakeLists.txt: add esp_driver_jpeg to REQUIRES.
Known gap (M6 closes it): /frame currently paints regardless of
wake/sleep state. The 409-when-asleep rule lands with POST /state
in M6.
Build clean against ESP-IDF 5.4 (binary ~640 KB).
TESTING.md M5 expanded with portrait/landscape test commands,
ImageMagick test-image recipes, the full negative matrix (wrong
Content-Type, oversize, wrong dims, concurrent, garbage), and the
M6 dependency note.
|
|
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 <64 chars; scrypted starts with
http:// and <256 chars; idle_timeout_ms 0 or >=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.
|
|
|
|
display.{h,c} brings up the Hosyond 5\" 800x480 panel via the Raspberry
Pi 7\" touchscreen architecture:
- I2C init to the panel-side MCU at 0x45 (Pi 7\" register map ported
from drivers/gpu/drm/panel/panel-raspberrypi-touchscreen.c).
- POWERON write + 120ms settle.
- ESP32-P4 MIPI-DSI bring-up in DPI video mode: 2-lane, 480 Mbps,
800x480 @ 60Hz, RGB565, canonical Pi 7\" timings (HSW=18/HBP=20/HFP=62,
VSW=4/VBP=27/VFP=18, 30 MHz pixel clock).
- Gamma-corrected (^2.2) PWM brightness via REG_PWM (0x86).
- display_sleep / display_wake hooks for M6.
- display_fill() and display_test_pattern() (8 vertical color bars)
for M3 acceptance and reuse by M8 local screens.
app_main calls display_init() as best-effort — if the panel isn't
attached or wiring is wrong, the rest of the firmware (Ethernet, mDNS,
/state) keeps running. On success it paints the test pattern.
Pin assignments (PIN_I2C_SDA=7, PIN_I2C_SCL=8) match Waveshare's
bundled-panel BSP convention but are flagged TODO until the user's
specific FPC adapter wiring is confirmed.
main/CMakeLists.txt: add driver, esp_driver_i2c, esp_lcd to REQUIRES.
Build verified clean against ESP-IDF 5.4 for target esp32p4. M3
acceptance (color bars on screen) cannot be verified without a
flashed board + connected panel.
|
|
- viewport_state.{h,c}: shared state struct (config, run-state, counters,
orientation, timestamps) behind a FreeRTOS mutex. Modules update it
under viewport_state_lock(); GET /state serializes a snapshot.
- http_api.{h,c}: starts esp_http_server on :80, registers GET /state.
Returns the full JSON shape from the spec (name, version, configured,
state, uptime_ms, last_frame_ms_ago, frames_received, decode_errors,
state_post_failures, resolution, ip, free_heap, free_psram).
- mdns_service.{h,c}: mdns_init + advertise _scrypted-viewport._tcp.local
on :80 with version/resolution/orientation/name TXT records.
mdns_service_refresh() reapplies hostname + TXT after /config writes
(called from M4 onward).
- main/CMakeLists.txt: add esp_http_server, esp_timer, json, mdns to
REQUIRES. espressif/mdns added as managed component via
main/idf_component.yml.
app_main calls viewport_state_init early so any module can read defaults
before /config arrives.
Also adds TESTING.md tracking per-milestone verification status and an
integration test plan to run after M9 (races, failure modes, longevity,
power cycles, negative protocol, multi-viewport).
M1 promoted to "🟡 builds clean" in TESTING.md; combined HW verification
of M1+M2 will be one flash session. Build verified against ESP-IDF 5.4
for target esp32p4 (binary ~550 KB).
|
|
net_eth module brings up the Waveshare ESP32-P4-ETH-POE Ethernet
interface (internal EMAC + IP101GRI PHY) and waits for a DHCP lease.
Pin map confirmed via Waveshare wiki + ESPHome's working config:
MDC=31, MDIO=52, REF_CLK=50 (CLK_EXT_IN from PHY)
TX_EN=49, TXD0=34, TXD1=35
CRS_DV=28, RXD0=30, RXD1=29
PHY reset/enable=51, PHY addr=1
app_main initializes NVS, netif, the default event loop, then starts
the Ethernet driver and waits up to 30s for an IP. On success it logs
the IP; on timeout it logs a warning and proceeds (the driver keeps
retrying in the background).
sdkconfig: declare 16 MB flash so partitions.csv (6.1 MB) fits.
main/CMakeLists.txt: explicit REQUIRES esp_eth esp_event esp_netif
nvs_flash.
Acceptance (per M1 in the impl guide): device gets a DHCP lease over
Ethernet and prints its IP. Build is clean against ESP-IDF 5.4 for
target esp32p4.
|
|
|
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
- 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.
|
|
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]>
|
|
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.
|