src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 18:16:46 -0500
committerLuke Hoersten <[email protected]>2026-06-14 18:16:46 -0500
commit4a6bbdb075a5e4f9910cb9b85e615ff8c50aa4ec (patch)
tree39bf9c7fb9ef39d15bbad3526e16d1c3ddd88dca
parentd4fcec3a4d012332e7c476562f9363977ac65561 (diff)
Drop VIEWPORT_STATE_UNCONFIGURED — state is just awake/asleep
\`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.
-rw-r--r--README.md10
-rw-r--r--TESTING.md19
-rw-r--r--main/http_api.c18
-rw-r--r--main/local_screens.c5
-rw-r--r--main/nvs_config.c2
-rw-r--r--main/state_machine.c12
-rw-r--r--main/state_machine.h5
-rw-r--r--main/viewport_state.h3
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://<device-ip>/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://<device-ip>/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 <scrypted>/state via state_client.
-// Silently no-ops if the device is unconfigured.
+// {viewport,state} POST to <scrypted>/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