From 4a6bbdb075a5e4f9910cb9b85e615ff8c50aa4ec Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Sun, 14 Jun 2026 18:16:46 -0500 Subject: Drop VIEWPORT_STATE_UNCONFIGURED — state is just awake/asleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`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. --- README.md | 10 +++++----- TESTING.md | 19 ++++++------------- main/http_api.c | 18 +++--------------- main/local_screens.c | 5 +---- main/nvs_config.c | 2 +- main/state_machine.c | 12 ++++++------ main/state_machine.h | 5 ++--- main/viewport_state.h | 3 +-- 8 files changed, 25 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index a142ee5..b242ff7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ See [`TESTING.md`](TESTING.md) for the full milestone-by-milestone status and th Power -> DHCP -> mDNS (_scrypted-viewport._tcp.local) --> If unconfigured: backlight on, show IP + hostname (persistent until `/config`) +-> If no `scrypted` URL set: backlight on, show info screen (persistent until `/config`) -> If configured: enter sleep state (backlight off, wait for `POST /state` or a tap) ## Resolution @@ -121,7 +121,7 @@ Returns `200 OK` with JSON: } ``` -`state` is `awake`, `asleep`, or `unconfigured`. `last_frame_ms_ago` is `null` if no frame has been received since boot. +`state` is `awake` or `asleep` (it reports the screen's current state only). `configured` is the separate flag that reports whether `viewport` and `scrypted` are both set. `last_frame_ms_ago` is `null` if no frame has been received since boot. ### POST /state @@ -426,10 +426,10 @@ Scrypted should use the same `idle_timeout_ms` value it sent in `/config` as its - Provisioning: flash the same firmware to every device. On first boot the screen shows its IP; register it from Scrypted via `POST /config`. - Viewport names must be unique across the LAN — mDNS hostnames are derived from `viewport` and two devices configured with the same name will collide. - NVS wipe: plug USB and run `idf.py erase-flash` followed by `idf.py flash`. The device boots clean and shows the info screen until `/config` is POSTed. -- No DHCP lease: keep retrying; do not reboot. Screen shows "no network" if unconfigured. +- No DHCP lease: keep retrying; do not reboot. The info screen shows "ip no network" until a lease arrives. - Ethernet disconnect: reconnect automatically. If Scrypted is unreachable, displays go stale — nothing the device can do about it. - Watchdog: the ESP-IDF task watchdog reboots the device if a task hangs. Soft state is rebuilt from NVS on every boot. -- Status LED (on-board): solid = configured & online, slow blink = unconfigured (waiting for `/config`), fast blink = no network. The screen tells the same story but the LED is visible when the screen is asleep. +- No usable on-board status LED on the Waveshare ESP32-P4-ETH. The info screen tells the boot story instead — short-tap to wake, long-press to overlay it. ## Build @@ -476,7 +476,7 @@ Every endpoint is idempotent; every failure leaves the device in a sane state. - JPEG decode fails → `decode_errors++`, return 400 or 500, keep previous frame on screen, wake/sleep state unchanged. - Outbound `/state` POST fails → `state_post_failures++`, continue. Local state change still happened. No retry queue; Scrypted catches up via its own timeout, the next event, or the next `/frame` returning 409. - Ethernet disconnects → driver reconnects automatically. No reboot loop. `GET /state` keeps serving over loopback for diagnostics. -- Display init fails → log loudly, keep serving the rest of the API with `state="unconfigured"`. The protocol is still usable for re-registration. +- Display init fails → log loudly, keep serving the rest of the API. The protocol is still usable for re-registration. - Task hangs → ESP-IDF watchdog reboots. NVS rebuilds soft state on the next boot. ### Coding standards diff --git a/TESTING.md b/TESTING.md index 23531ba..bca9479 100644 --- a/TESTING.md +++ b/TESTING.md @@ -159,14 +159,14 @@ dns-sd -B _scrypted-viewport._tcp local. # macOS avahi-browse -r _scrypted-viewport._tcp # Linux ``` -Expected `/state` body on a fresh device (unconfigured): +Expected `/state` body on a fresh device (no `/config` posted yet): ```json { "name": null, "version": "0.1.0", "configured": false, - "state": "unconfigured", + "state": "asleep", "uptime_ms": 12345, "last_frame_ms_ago": null, "frames_received": 0, @@ -183,7 +183,7 @@ Expected mDNS browse output should show a `_scrypted-viewport._tcp` instance wit **Status**: ✅ verified 2026-06-14 alongside M1. -- `GET http://10.0.13.83/state` returned the full spec JSON: `name=null, configured=false, state=unconfigured, resolution=480x800, ip=10.0.13.83, free_psram=31730048, ...`. +- `GET http://10.0.13.83/state` returned the full spec JSON: `name=null, configured=false, state=asleep, resolution=480x800, ip=10.0.13.83, free_psram=31730048, ...`. - `dns-sd -B _scrypted-viewport._tcp local.` on macOS surfaced one instance named `viewport`. Bare-board behavior (no panel, no ethernet) was also verified — the device boots cleanly and prints a summary like `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). @@ -466,14 +466,7 @@ done curl http:///state | jq .state # expect "awake" ``` -Unconfigured device rejects `POST /state` with 409: - -```bash -# (After fresh boot / NVS erase, no /config yet) -curl -i -X POST -H "Content-Type: application/json" \ - -d '{"state":"wake"}' http:///state -# expect: 409 Conflict "device unconfigured" -``` +`POST /state` always works regardless of whether `/config` has been posted — `state` is just the screen's awake/asleep, decoupled from the `configured` flag. Outbound POST-to-Scrypted is the part gated on a scrypted URL being present. Bad input: @@ -552,7 +545,7 @@ Tap rapidly (faster than the receiver can ack) and confirm the receiver only see - `curl -X POST -d '{"state":"sleep"}' /state` — receiver should print **nothing** (Scrypted already knows it initiated). - Same for `/state {wake}` and `/frame` (asleep → 409, no callback either). -**No POST when unconfigured** +**No POST when no scrypted URL is set** - Before `/config` provides a Scrypted URL, tapping the screen does nothing outbound (no Scrypted URL to call). No queue entries dropped to disk. @@ -566,7 +559,7 @@ Tap rapidly (faster than the receiver can ack) and confirm the receiver only see **Visual checks (panel-attached)** -- Fresh flash → screen shows the info screen (~15 lines of `label value` pairs, white on black, auto-scaled). When unconfigured it reports `name unset`, `config no`, `state unconfigured`. +- Fresh flash → screen shows the info screen (~15 lines of `label value` pairs, white on black, auto-scaled). With no `/config` posted it reports `name unset`, `config no`, `state asleep`, `scrypt none`. - `POST /config` with viewport + scrypted → device transitions to ASLEEP, backlight off. - `POST /state {state:wake}` (or tap) → backlight on, `Loading...` centered until the first `/frame` lands. - `POST /frame` while AWAKE → loading screen replaced by the JPEG. diff --git a/main/http_api.c b/main/http_api.c index 5b5769a..dce57bb 100644 --- a/main/http_api.c +++ b/main/http_api.c @@ -26,11 +26,7 @@ static const char *TAG = "http_api"; static const char *state_name(viewport_run_state_t s) { - switch (s) { - case VIEWPORT_STATE_AWAKE: return "awake"; - case VIEWPORT_STATE_ASLEEP: return "asleep"; - default: return "unconfigured"; - } + return (s == VIEWPORT_STATE_AWAKE) ? "awake" : "asleep"; } static const char *orientation_name(viewport_orientation_t o) @@ -246,10 +242,7 @@ static esp_err_t config_post_handler(httpd_req_t *req) } // A configured device has both a viewport name and a scrypted URL. - if (st->viewport_name[0] && st->scrypted_url[0] && !st->configured) { - st->configured = true; - if (st->state == VIEWPORT_STATE_UNCONFIGURED) st->state = VIEWPORT_STATE_ASLEEP; - } + st->configured = (st->viewport_name[0] && st->scrypted_url[0]); viewport_state_unlock(); @@ -304,11 +297,6 @@ static esp_err_t state_post_handler(httpd_req_t *req) cJSON_Delete(root); esp_err_t err = state_machine_set(target); - if (err == ESP_ERR_INVALID_STATE) { - httpd_resp_set_status(req, "409 Conflict"); - httpd_resp_set_type(req, "text/plain"); - return httpd_resp_send(req, "device unconfigured", HTTPD_RESP_USE_STRLEN); - } if (err != ESP_OK) { httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "text/plain"); @@ -358,7 +346,7 @@ static esp_err_t frame_post_handler(httpd_req_t *req) return respond_status(req, "500 Internal Server Error", "display not initialized"); } - // /frame requires AWAKE. Asleep / unconfigured → 409. + // /frame requires AWAKE. Asleep → 409. if (state_machine_current() != VIEWPORT_STATE_AWAKE) { return respond_status(req, "409 Conflict", "device asleep — POST /state {\"state\":\"wake\"} first"); diff --git a/main/local_screens.c b/main/local_screens.c index 4b639ef..7459d9a 100644 --- a/main/local_screens.c +++ b/main/local_screens.c @@ -297,10 +297,7 @@ esp_err_t local_screens_show_info(void) char heap_str[12]; fmt_bytes(heap_str, sizeof(heap_str), free_heap); char psram_str[12]; fmt_bytes(psram_str, sizeof(psram_str), free_psram); - const char *state_str = - (state == VIEWPORT_STATE_AWAKE) ? "awake" - : (state == VIEWPORT_STATE_ASLEEP) ? "asleep" - : "unconfigured"; + const char *state_str = (state == VIEWPORT_STATE_AWAKE) ? "awake" : "asleep"; // Label width is fixed at 8 chars (trailing spaces pad it). Values are // left-aligned at column 8. Auto-scaler then picks a font scale to fit. diff --git a/main/nvs_config.c b/main/nvs_config.c index 164db53..f6a9805 100644 --- a/main/nvs_config.c +++ b/main/nvs_config.c @@ -70,7 +70,7 @@ esp_err_t nvs_config_load(void) ? "landscape" : "portrait", st->brightness); } else { - ESP_LOGI(TAG, "partial config in NVS — staying unconfigured"); + ESP_LOGI(TAG, "partial config in NVS — viewport or scrypted URL still missing"); } done: diff --git a/main/state_machine.c b/main/state_machine.c index 3d95308..a4d4dab 100644 --- a/main/state_machine.c +++ b/main/state_machine.c @@ -61,14 +61,14 @@ esp_err_t state_machine_set(viewport_run_state_t target) if (target == VIEWPORT_STATE_AWAKE) { if (display_is_up()) { display_wake(); - // Content choice: configured devices show "Loading…" until - // Scrypted pushes a /frame; unconfigured devices show the - // info screen since there's no Scrypted to push anything. + // Content choice: a configured device shows "Loading…" until + // Scrypted pushes a /frame; a device with no scrypted URL + // shows the info screen since there's no Scrypted to push. if (configured) local_screens_show_loading(); else local_screens_show_info(); } arm_idle_timer_unlocked(); - ESP_LOGI(TAG, "AWAKE (%s)", configured ? "configured" : "unconfigured"); + ESP_LOGI(TAG, "AWAKE (%s)", configured ? "configured" : "no scrypted URL"); } else { disarm_idle_timer(); if (display_is_up()) display_sleep(); @@ -89,8 +89,8 @@ void state_machine_set_local(viewport_run_state_t target) { esp_err_t err = state_machine_set(target); if (err != ESP_OK) return; - // Only POST when there's a Scrypted to talk to. Unconfigured tap toggles - // change the local display state without notifying anyone. + // Only POST when there's a Scrypted to talk to. Without a scrypted URL, + // tap toggles change the local display state without notifying anyone. viewport_state_lock(); bool configured = viewport_state_get()->configured; viewport_state_unlock(); diff --git a/main/state_machine.h b/main/state_machine.h index 8caf670..22aebad 100644 --- a/main/state_machine.h +++ b/main/state_machine.h @@ -6,7 +6,6 @@ esp_err_t state_machine_init(void); // Transition to AWAKE or ASLEEP. Idempotent: no-op if already there. -// Returns ESP_ERR_INVALID_STATE if the device is unconfigured. // Side-effects (under one critical section): // AWAKE -> backlight on, idle timer (re)armed // ASLEEP -> backlight off, idle timer cancelled, framebuffer discarded @@ -14,8 +13,8 @@ esp_err_t state_machine_set(viewport_run_state_t target); // Device-initiated variant (tap, idle timer expiry). Drives the same // transition as state_machine_set() and additionally enqueues a -// {viewport,state} POST to /state via state_client. -// Silently no-ops if the device is unconfigured. +// {viewport,state} POST to /state via state_client when a +// `scrypted` URL is configured. void state_machine_set_local(viewport_run_state_t target); // Called by /frame after a successful paint. Restarts the idle timer diff --git a/main/viewport_state.h b/main/viewport_state.h index b580ff7..3f6147f 100644 --- a/main/viewport_state.h +++ b/main/viewport_state.h @@ -13,9 +13,8 @@ typedef enum { } viewport_orientation_t; typedef enum { - VIEWPORT_STATE_UNCONFIGURED = 0, + VIEWPORT_STATE_ASLEEP = 0, // boot default; backlight off VIEWPORT_STATE_AWAKE, - VIEWPORT_STATE_ASLEEP, } viewport_run_state_t; // Read-mostly snapshot of device state used by /state. Counters are -- cgit v1.2.3