diff options
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | TESTING.md | 21 | ||||
| -rw-r--r-- | main/local_screens.c | 89 | ||||
| -rw-r--r-- | main/local_screens.h | 11 |
4 files changed, 103 insertions, 22 deletions
@@ -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 @@ -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 |
