Add bounded OLED boot identity animation

This commit is contained in:
2026-08-29 21:05:22 +02:00
parent 276559536b
commit 96e5792273
7 changed files with 197 additions and 3 deletions
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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
+1
View File
@@ -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.
+1
View File
@@ -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"
+174
View File
@@ -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 <stdbool.h>
#include <stdint.h>
#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;
}
+9
View File
@@ -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);
+7
View File
@@ -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));
}
}
}