diff --git a/docs/command_reference.md b/docs/command_reference.md index 88f7729..729e6c6 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -20,7 +20,7 @@ Use these commands from the UART0 `serial-tool>` administration console. Run `he | `display save` / `display load` | Save the working aging settings to NVS or load them. | | `display defaults` / `display reset` | Apply 300/600-second defaults in RAM, or apply and persist them. | -When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. A missing OLED remains nonfatal; after reconnecting it safely, one new button press requests a bounded reprobe and is consumed without navigating. +When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. At normal boot, an initialized OLED shows a bounded five-second identity animation before the status UI begins; it scrolls the device name in yellow and draws the compact upright-terminal logo in blue. A missing OLED remains nonfatal; after reconnecting it safely, one new button press requests a bounded reprobe and is consumed without navigating. ## Serial service diff --git a/docs/electrical_tests.md b/docs/electrical_tests.md index fd91891..16df947 100644 --- a/docs/electrical_tests.md +++ b/docs/electrical_tests.md @@ -157,9 +157,11 @@ For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then u Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data. -### 10. Phase 7E persistence and fault recovery +### 10. Phase 7E boot animation, persistence, and fault recovery -First shorten the delays for a bounded aging-policy test: +On each normal boot with an initialized OLED, first verify the bounded five-second identity animation: the full `ESP32 SERIAL SWISS ARMY KNIFE` name scrolls across the yellow panel; the blue panel shows the compact right-oriented monochrome logo with an upright `>_` prompt and serial/USB cable ends entering from the left. Wi-Fi activity and the cursor blink at the 4 Hz animation cadence. A missing OLED must skip the animation without preventing normal UART0 recovery. + +Then shorten the delays for a bounded aging-policy test: ```text display status diff --git a/docs/roadmap.md b/docs/roadmap.md index 168bd99..3141d5f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -227,6 +227,7 @@ The persistent yellow strip uses fixed-position serial, Wi-Fi-strength, USB, Web - Wi-Fi lifecycle results report a queued request rather than falsely claiming an asynchronous transition has completed. HTTPS/SSH start requests require a connected station or active AP. The current writer is only ever force-released to no writer; the UI is not a broker client and cannot acquire or assign a writer lease. - Target-hardware validation passed for menu navigation and timeout behavior, controls and asynchronous results, two-second confirmations and cancellation, Wi-Fi next-profile progression/wrap, chord rejection, wake-press suppression, and preservation of UART0 plus transport recovery. 5. **Phase 7E — Reliability, persistence, and documentation — Implemented; validation pending** + - At startup, a bounded five-second OLED-only identity animation renders a compact monochrome, right-oriented derivative of the project logo in the blue panel: its upright `>_` terminal remains readable, while both USB and serial cable ends enter from the left. The full device name scrolls through the yellow panel. A missing or failing OLED skips the animation without delaying recovery services further. - A versioned `local_ui` NVS configuration makes the dim and display-off inactivity delays independently configurable from 0 through 86400 seconds. Zero disables a transition; defaults remain 300/600 seconds. UART0 `display` commands provide status, RAM edits, save/load, defaults, and atomic reset behavior. - A button held for ten seconds is quarantined until its debounced release, after which it rearms. Quarantined inputs no longer keep a chord latched or block the other controls. A confirmation hold can execute at most once. - Dim/off wake and missing-display recovery now occur only on a new debounced press edge. A held or stuck input cannot continuously probe an absent OLED, flood logs, or indefinitely refresh the inactivity timer; the wake gesture remains consumed. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bd37992..94c77ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "secure_random.c" "status_led.c" "local_display.c" + "local_boot_animation.c" "local_status_ui.c" "local_ui_config.c" "local_ui_console.c" diff --git a/src/local_boot_animation.c b/src/local_boot_animation.c new file mode 100644 index 0000000..7a57030 --- /dev/null +++ b/src/local_boot_animation.c @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Compact monochrome derivative of images/logo.png for the 128x48 content panel. */ + +#include "local_boot_animation.h" + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "local_display.h" + +#define BOOT_ANIMATION_FRAMES 20U +#define BOOT_ANIMATION_FRAME_MS 250U +#define BOOT_ANIMATION_NAME_SPEED 16U + +static const char s_device_name[] = "ESP32 SERIAL SWISS ARMY KNIFE"; + +static void pixel(int x, int y) +{ + if (x >= 0 && x < (int)LOCAL_DISPLAY_WIDTH && + y >= 0 && y < (int)LOCAL_DISPLAY_CONTENT_HEIGHT) { + local_display_frame_set_pixel(LOCAL_DISPLAY_PANEL_CONTENT, (uint8_t)x, + (uint8_t)y, true); + } +} + +static void line(int x0, int y0, int x1, int y1) +{ + int dx = x1 >= x0 ? x1 - x0 : x0 - x1; + int sx = x0 < x1 ? 1 : -1; + int dy = y1 >= y0 ? y0 - y1 : y1 - y0; + int sy = y0 < y1 ? 1 : -1; + int error = dx + dy; + + for (;;) { + pixel(x0, y0); + if (x0 == x1 && y0 == y1) { + return; + } + int twice_error = 2 * error; + if (twice_error >= dy) { + error += dy; + x0 += sx; + } + if (twice_error <= dx) { + error += dx; + y0 += sy; + } + } +} + +static void rectangle(int x, int y, int width, int height) +{ + line(x, y, x + width - 1, y); + line(x, y, x, y + height - 1); + line(x + width - 1, y, x + width - 1, y + height - 1); + line(x, y + height - 1, x + width - 1, y + height - 1); +} + +static void draw_serial_connector(void) +{ + rectangle(2, 8, 16, 10); + rectangle(4, 10, 12, 6); + for (int column = 0; column < 5; ++column) { + pixel(6 + column * 2, 12); + pixel(6 + column * 2, 14); + } + line(18, 13, 30, 22); + line(18, 16, 28, 25); +} + +static void draw_usb_connector(void) +{ + rectangle(2, 31, 14, 11); + rectangle(4, 33, 10, 7); + line(16, 35, 29, 30); + line(16, 39, 30, 34); + pixel(7, 35); + pixel(10, 38); +} + +static void draw_terminal(bool cursor_on) +{ + rectangle(28, 21, 42, 22); + rectangle(30, 23, 38, 18); + /* Keep the prompt upright even though the source logo is reoriented. */ + line(38, 28, 45, 33); + line(45, 33, 38, 38); + if (cursor_on) { + line(51, 37, 59, 37); + } + line(31, 27, 31, 39); + line(67, 27, 67, 39); + line(34, 22, 34, 20); + line(63, 22, 63, 20); +} + +static void draw_board(void) +{ + rectangle(72, 7, 28, 36); + rectangle(78, 14, 16, 17); + for (int row = 0; row < 5; ++row) { + pixel(74, 11 + row * 6); + pixel(76, 11 + row * 6); + pixel(96, 11 + row * 6); + pixel(98, 11 + row * 6); + } + for (int column = 0; column < 6; ++column) { + pixel(79 + column * 3, 35); + pixel(79 + column * 3, 38); + } + line(70, 28, 72, 28); + line(70, 34, 72, 34); +} + +static void draw_wifi(uint8_t frame) +{ + bool outer = (frame % 2U) == 0U; + if (outer) { + line(103, 8, 109, 3); + line(109, 3, 115, 8); + } + line(105, 12, 109, 8); + line(109, 8, 113, 12); + line(107, 16, 109, 14); + line(109, 14, 111, 16); + pixel(109, 19); +} + +static void draw_logo(uint8_t frame) +{ + draw_serial_connector(); + draw_usb_connector(); + draw_terminal((frame % 2U) == 0U); + draw_board(); + draw_wifi(frame); +} + +static void draw_scrolling_name(uint8_t frame) +{ + int text_width = ((int)sizeof(s_device_name) - 1) * 6; + int x = (int)LOCAL_DISPLAY_WIDTH - (int)frame * BOOT_ANIMATION_NAME_SPEED; + if (x < -text_width) { + x += text_width + (int)LOCAL_DISPLAY_WIDTH; + } + const char *text = s_device_name; + while (x < 0 && *text != '\0') { + x += 6; + ++text; + } + if (x < (int)LOCAL_DISPLAY_WIDTH && *text != '\0') { + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, (uint8_t)x, 4U, text); + } +} + +esp_err_t local_boot_animation_play(void) +{ + for (uint8_t frame = 0U; frame < BOOT_ANIMATION_FRAMES; ++frame) { + esp_err_t error = local_display_frame_begin(); + if (error != ESP_OK) { + return error; + } + local_display_frame_clear_all(); + draw_scrolling_name(frame); + draw_logo(frame); + error = local_display_frame_end(); + if (error != ESP_OK) { + return error; + } + vTaskDelay(pdMS_TO_TICKS(BOOT_ANIMATION_FRAME_MS)); + } + return ESP_OK; +} diff --git a/src/local_boot_animation.h b/src/local_boot_animation.h new file mode 100644 index 0000000..3756692 --- /dev/null +++ b/src/local_boot_animation.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Bounded boot animation for the optional local OLED. */ + +#pragma once + +#include "esp_err.h" + +/* Plays the OLED-only startup identity animation; a missing display is nonfatal. */ +esp_err_t local_boot_animation_play(void); diff --git a/src/main.c b/src/main.c index d38e20b..724cafa 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include "esp_psram.h" #include "network_console.h" #include "local_display.h" +#include "local_boot_animation.h" #include "local_status_ui.h" #include "local_ui_config.h" #include "local_ui_console.h" @@ -70,6 +71,12 @@ void app_main(void) if (local_display_error != ESP_OK) { ESP_LOGW(TAG, "Local display unavailable: %s", esp_err_to_name(local_display_error)); + } else { + local_display_error = local_boot_animation_play(); + if (local_display_error != ESP_OK) { + ESP_LOGW(TAG, "Local boot animation unavailable: %s", + esp_err_to_name(local_display_error)); + } } }