src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--TESTING.md21
-rw-r--r--main/local_screens.c89
-rw-r--r--main/local_screens.h11
4 files changed, 103 insertions, 22 deletions
diff --git a/README.md b/README.md
index 409a20c..3723596 100644
--- a/README.md
+++ b/README.md
@@ -341,10 +341,10 @@ Failure modes do not corrupt state:
The device renders exactly two things itself; everything else is a JPEG from Scrypted:
-- **IP screen**: IP address and `viewport.local` as plain centered text. Shown on first boot until `/config`, after factory reset, and as a 15s overlay when BOOT is short-pressed. Rendered in the current orientation (portrait by default).
+- **Identity screen**: four centered lines — viewport name, mDNS hostname, IP address, and current state. Shown on first boot until `/config`, after factory reset, and as a 15s overlay when BOOT is short-pressed. The font scale auto-fits the longest line within 90% of the screen width. Unconfigured devices show `viewport` / `viewport.local` / `<ip>` / `unconfigured`; configured devices show e.g. `mudroom` / `viewport-mudroom.local` / `192.168.1.42` / `asleep`.
- **Loading screen**: shown between a wake and the next `/frame` arriving. Plain "Loading…" text. Rendered in the current orientation.
-Both use a small embedded bitmap font. No LVGL, no general text engine.
+Both use a small embedded bitmap font — full lowercase a–z, digits, period, colon, dash, slash, plus uppercase `L` for "Loading...". No LVGL, no general text engine.
## Scrypted Integration
diff --git a/TESTING.md b/TESTING.md
index 20f377d..b3500ef 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -540,32 +540,41 @@ Tap rapidly (faster than the receiver can ack) and confirm the receiver only see
## M8 — Local Screens + BOOT button
-**Acceptance**: IP screen on first boot; loading screen on every wake; BOOT button works.
+**Acceptance**: identity screen on first boot; loading screen on every wake; BOOT button works.
**Visual checks (panel-attached)**
-- Fresh flash (or after factory reset) → screen shows two centered white-on-black lines:
+- Fresh flash (or after factory reset) → screen shows four centered white-on-black lines (auto-scaled to fit):
```
+ viewport
viewport.local
192.168.x.y
+ unconfigured
```
- `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.
+- **BOOT short-press while configured** → identity overlay for 15s showing four lines populated from current state, e.g.:
+ ```
+ mudroom
+ viewport-mudroom.local
+ 192.168.1.42
+ asleep
+ ```
- `POST /state {state:sleep}` (or idle timeout, or tap-while-awake) → backlight off.
**BOOT button** (GPIO 0 placeholder — see Hardware note below)
-- Short press at any state: backlight wakes if it was off, IP screen overlays for 15s, then `local_screens_restore_for_state()` paints black (Scrypted's next `/frame` repaints if awake; the state machine handles backlight if asleep). Does not change wake/sleep state.
+- Short press at any state: backlight wakes if it was off, identity screen overlays for 15s, then `local_screens_restore_for_state()` paints black (Scrypted's next `/frame` repaints if awake; the state machine handles backlight if asleep). Does not change wake/sleep state.
- Hold ≥ 5s: serial logs `BOOT held 5000ms → factory reset`, NVS clears via `nvs_config_reset()`, `esp_restart()`. After reboot the device comes back UNCONFIGURED with the IP screen.
**Hardware note**: `PIN_BOOT_BUTTON = 0` in `button.c` is a guess. ESP32-P4's official strap pin is GPIO35 but that's owned by RMII TXD1 at runtime. Most Waveshare ESP32-P4 boards expose a separate user button on a free GPIO — confirm against the schematic and update `PIN_BOOT_BUTTON` if needed. If the wrong pin is wired, the button just never triggers (input reads stuck-high) and the rest of M8 still works.
**Negative / edge**
-- BOOT overlay during AWAKE: the overlay paints over the live frame. Scrypted's next `/frame` (within ~1s in a normal stream) overwrites it. Acceptable: the operator sees the IP for ≤ 1 frame interval then the live view resumes.
-- BOOT overlay during ASLEEP: backlight comes on for the overlay, then `restore_for_state` paints black. The state machine's `display_sleep()` is NOT re-issued — that's an open follow-up if the test pattern stays lit after expiry.
-- Font fallback: any character outside the supported set (digits, dot, colon, `L`, lowercase a/c/d/e/g/i/l/n/o/p/r/t/v/w, space) renders as blank. Today the only strings drawn are `viewport.local`, the IPv4 string, and `Loading...` — all covered.
+- BOOT overlay during AWAKE: the overlay paints over the live frame. Scrypted's next `/frame` (within ~1s in a normal stream) overwrites it. Acceptable: the operator sees the identity for ≤ 1 frame interval then the live view resumes.
+- BOOT overlay during ASLEEP: backlight comes on for the overlay, then `restore_for_state` paints black. The state machine's `display_sleep()` is NOT re-issued — that's an open follow-up if the screen stays lit after expiry.
+- Font fallback: any character outside the supported set (digits, period, colon, dash, slash, `L`, lowercase a–z, space) renders as blank. The identity screen, loading screen, and IPv4 strings are all covered.
**Status**: 🟡 builds clean against ESP-IDF 5.4 (binary ~870 KB). Hardware verification awaits panel + BOOT-pin confirmation.
diff --git a/main/local_screens.c b/main/local_screens.c
index d30750a..5167d9d 100644
--- a/main/local_screens.c
+++ b/main/local_screens.c
@@ -15,13 +15,16 @@ static const char *TAG = "screens";
#define PANEL_H 480
#define MAX_BUF_PX (PANEL_W * PANEL_H)
-// 8x8 bitmap font, lit pixels = foreground. Only the characters used by the
-// IP and Loading screens are defined; the rest stay zero ('?' shows up as a
-// blank box for unsupported characters). Designators keep the data sparse-
-// looking but the table is just a contiguous block (95 chars × 8 bytes).
+// 8x8 bitmap font, lit pixels = foreground. Covers the characters used by
+// any text the device draws locally: lowercase a–z, digits, period, colon,
+// dash, slash, plus uppercase L for "Loading...". Unsupported chars render
+// blank. The table is a contiguous 95-char × 8-byte block; designators
+// keep it readable.
static const uint8_t FONT[95][8] = {
[' ' - 0x20] = {0,0,0,0,0,0,0,0},
+ ['-' - 0x20] = {0,0,0,0x7E,0,0,0,0},
['.' - 0x20] = {0,0,0,0,0,0x18,0x18,0},
+ ['/' - 0x20] = {0x06,0x0C,0x0C,0x18,0x18,0x30,0x30,0x60},
[':' - 0x20] = {0,0x18,0x18,0,0,0x18,0x18,0},
['0' - 0x20] = {0x3C,0x66,0x66,0x66,0x66,0x66,0x66,0x3C},
@@ -38,19 +41,31 @@ static const uint8_t FONT[95][8] = {
['L' - 0x20] = {0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x7E},
['a' - 0x20] = {0,0,0x3C,0x06,0x3E,0x66,0x66,0x3E},
+ ['b' - 0x20] = {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x7C},
['c' - 0x20] = {0,0,0x3C,0x66,0x60,0x60,0x66,0x3C},
['d' - 0x20] = {0x06,0x06,0x3E,0x66,0x66,0x66,0x66,0x3E},
['e' - 0x20] = {0,0,0x3C,0x66,0x7E,0x60,0x66,0x3C},
+ ['f' - 0x20] = {0x1C,0x36,0x30,0x7C,0x30,0x30,0x30,0x30},
['g' - 0x20] = {0,0,0x3E,0x66,0x66,0x3E,0x06,0x3C},
+ ['h' - 0x20] = {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x66},
['i' - 0x20] = {0x18,0,0x18,0x18,0x18,0x18,0x18,0x18},
+ ['j' - 0x20] = {0x06,0,0x06,0x06,0x06,0x06,0x66,0x3C},
+ ['k' - 0x20] = {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x66},
['l' - 0x20] = {0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x3C},
+ ['m' - 0x20] = {0,0,0x66,0x7F,0x7F,0x6B,0x63,0x63},
['n' - 0x20] = {0,0,0x7C,0x66,0x66,0x66,0x66,0x66},
['o' - 0x20] = {0,0,0x3C,0x66,0x66,0x66,0x66,0x3C},
['p' - 0x20] = {0,0,0x7C,0x66,0x66,0x7C,0x60,0x60},
+ ['q' - 0x20] = {0,0,0x3E,0x66,0x66,0x3E,0x06,0x06},
['r' - 0x20] = {0,0,0x7C,0x66,0x60,0x60,0x60,0x60},
+ ['s' - 0x20] = {0,0,0x3E,0x60,0x3C,0x06,0x06,0x7C},
['t' - 0x20] = {0x18,0x18,0x3C,0x18,0x18,0x18,0x18,0x1C},
+ ['u' - 0x20] = {0,0,0x66,0x66,0x66,0x66,0x66,0x3E},
['v' - 0x20] = {0,0,0x66,0x66,0x66,0x66,0x3C,0x18},
['w' - 0x20] = {0,0,0x66,0x66,0x66,0x6E,0x7E,0x36},
+ ['x' - 0x20] = {0,0,0x66,0x3C,0x18,0x18,0x3C,0x66},
+ ['y' - 0x20] = {0,0,0x66,0x66,0x66,0x3E,0x0C,0x78},
+ ['z' - 0x20] = {0,0,0x7E,0x0C,0x18,0x30,0x60,0x7E},
};
#define FG 0xFFFF // white
@@ -125,18 +140,72 @@ esp_err_t local_screens_show_ip(void)
{
if (!s_fb) return ESP_ERR_INVALID_STATE;
+ // Build the four identity lines from current state. This is the screen
+ // shown on first boot, after factory reset, and as a 15s BOOT-button
+ // overlay — it's the "who am I" view the operator needs to find the
+ // device on the LAN and confirm it's configured the way they expect.
+ //
+ // line 1: viewport name ("mudroom" / "viewport" if unconfigured)
+ // line 2: mDNS hostname ("viewport-mudroom.local" or "viewport.local")
+ // line 3: IP address ("192.168.x.y" or "no network")
+ // line 4: state ("awake" / "asleep" / "unconfigured")
+
+ char line_name[64], line_host[80], line_ip[24], line_state[24];
+
+ viewport_state_lock();
+ viewport_state_t *st = viewport_state_get();
+
+ if (st->viewport_name[0]) {
+ snprintf(line_name, sizeof(line_name), "%s", st->viewport_name);
+ snprintf(line_host, sizeof(line_host), "viewport-%s.local", st->viewport_name);
+ } else {
+ snprintf(line_name, sizeof(line_name), "viewport");
+ snprintf(line_host, sizeof(line_host), "viewport.local");
+ }
+ switch (st->state) {
+ case VIEWPORT_STATE_AWAKE: snprintf(line_state, sizeof(line_state), "awake"); break;
+ case VIEWPORT_STATE_ASLEEP: snprintf(line_state, sizeof(line_state), "asleep"); break;
+ default: snprintf(line_state, sizeof(line_state), "unconfigured"); break;
+ }
+ viewport_state_unlock();
+
+ const char *ip = net_eth_get_ip_str();
+ snprintf(line_ip, sizeof(line_ip), "%s", (ip && ip[0]) ? ip : "no network");
+
+ const char *lines[] = { line_name, line_host, line_ip, line_state };
+ const int n_lines = (int)(sizeof(lines) / sizeof(lines[0]));
+
uint16_t w, h;
effective_dims(&w, &h);
- int scale = (w < 800) ? 3 : 4; // 3x for portrait, 4x for landscape
+
+ // Pick the largest integer scale where the longest line fits within
+ // 90% of the screen width and all four lines plus inter-line spacing
+ // (half a line each) fit within 90% of the screen height. Falls back
+ // to scale 1 if the longest line is unusually long.
+ size_t longest = 0;
+ for (int i = 0; i < n_lines; ++i) {
+ size_t l = strlen(lines[i]);
+ if (l > longest) longest = l;
+ }
+ int scale = 1;
+ for (int s = 6; s >= 1; --s) {
+ int line_w = (int)longest * 8 * s;
+ int total_h = n_lines * 8 * s + (n_lines - 1) * 4 * s;
+ if (line_w <= (w * 9) / 10 && total_h <= (h * 9) / 10) {
+ scale = s;
+ break;
+ }
+ }
+
int line_h = 8 * scale;
- int spacing = line_h / 2;
- int total_h = 2 * line_h + spacing;
+ int spacing = 4 * scale;
+ int total_h = n_lines * line_h + (n_lines - 1) * spacing;
int y0 = (h - total_h) / 2;
clear(BG);
- draw_centered(w, h, y0, "viewport.local", scale);
- draw_centered(w, h, y0 + line_h + spacing, net_eth_get_ip_str(), scale);
-
+ for (int i = 0; i < n_lines; ++i) {
+ draw_centered(w, h, y0 + i * (line_h + spacing), lines[i], scale);
+ }
return display_present_rgb565(s_fb, w, h);
}
diff --git a/main/local_screens.h b/main/local_screens.h
index 20b2b36..ffe9a3d 100644
--- a/main/local_screens.h
+++ b/main/local_screens.h
@@ -7,10 +7,13 @@
// bitmap font.
esp_err_t local_screens_init(void);
-// Render the unconfigured / identify screen — two centered lines:
-// "viewport.local"
-// <current IP>
-// Shown on first boot, after factory reset, and as a BOOT-button overlay.
+// Render the identity / "who am I" screen — four centered lines:
+// <viewport name> ("viewport" if unconfigured)
+// viewport-<name>.local (mDNS hostname)
+// <current IP> ("no network" if no DHCP lease)
+// <state> ("awake" / "asleep" / "unconfigured")
+// Font scale is auto-picked to fit the longest line within 90% of width.
+// Shown on first boot, after factory reset, and as a 15s BOOT-button overlay.
esp_err_t local_screens_show_ip(void);
// Render the loading screen — centered "Loading…" — shown on every wake