firmware: idle screen when usage data goes stale

When the host is connected over BLE but no usage update has landed recently
(~90s — token expired, daemon down, host asleep), show a calm idle screen
instead of leaving hours-old numbers on screen as if they were live: a shrunk,
animated "expression sleep" creature (reused claudepix art) centered between
the header and the status line.

The usage view resolves to three sub-screens via a data-freshness timer: live
panels, this idle screen, or the existing pairing hint. The bottom status line
animates present-participle gerunds: the idle screen alternates "Listening…" /
"No data…" (alive + explicit), the disconnected screen uses "Waiting…"
(replacing the static "Disconnected…").

splash_mini_create()/splash_mini_tick() render any claudepix animation shrunk
into an arbitrary parent, keeping the art data in the splash module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Payne
2026-06-05 11:26:57 -07:00
co-authored by Claude Opus 4.8
parent d50091ff31
commit 72524ef1fb
3 changed files with 134 additions and 13 deletions
+60
View File
@@ -89,6 +89,66 @@ static void render_frame(const uint8_t *cells, const uint16_t *palette) {
if (canvas) lv_obj_invalidate(canvas);
}
// ---- Mini creature: a small animated creature for embedding in other screens
// (e.g. the idle "sleeping" indicator). Self-contained — its own canvas and
// buffer, independent of the full-screen splash above. ----
static lv_obj_t *mini_canvas = NULL;
static uint16_t *mini_buf = NULL;
static int mini_cell = 0;
static int mini_w = 0;
static const splash_anim_def_t *mini_anim = NULL;
static uint16_t mini_frame = 0;
static uint32_t mini_started = 0;
static void mini_render(void) {
if (!mini_buf || !mini_anim) return;
const uint8_t *cells = mini_anim->frames[mini_frame];
const uint16_t *pal = mini_anim->palette;
for (int gy = 0; gy < GRID; gy++) {
for (int gx = 0; gx < GRID; gx++) {
uint8_t code = cells[gy * GRID + gx];
uint16_t color = (pal && code < SPLASH_PALETTE_SIZE) ? pal[code] : COL_EMPTY;
for (int dy = 0; dy < mini_cell; dy++) {
uint16_t *dst = &mini_buf[(gy * mini_cell + dy) * mini_w + gx * mini_cell];
for (int dx = 0; dx < mini_cell; dx++) dst[dx] = color;
}
}
}
if (mini_canvas) lv_obj_invalidate(mini_canvas);
}
lv_obj_t* splash_mini_create(lv_obj_t *parent, const char *anim_name, int px) {
mini_anim = NULL;
for (int i = 0; i < SPLASH_ANIM_COUNT; i++) {
if (strcmp(splash_anims[i].name, anim_name) == 0) { mini_anim = &splash_anims[i]; break; }
}
if (!mini_anim) return NULL;
mini_cell = px / GRID;
if (mini_cell < 1) mini_cell = 1;
mini_w = GRID * mini_cell;
#ifdef BOARD_HAS_PSRAM
const uint32_t caps = MALLOC_CAP_SPIRAM;
#else
const uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
#endif
mini_buf = (uint16_t*)heap_caps_malloc(mini_w * mini_w * 2, caps);
if (!mini_buf) return NULL;
mini_canvas = lv_canvas_create(parent);
lv_canvas_set_buffer(mini_canvas, mini_buf, mini_w, mini_w, LV_COLOR_FORMAT_RGB565);
mini_frame = 0;
mini_started = millis();
mini_render();
return mini_canvas;
}
void splash_mini_tick(void) {
if (!mini_buf || !mini_anim || mini_anim->frame_count == 0) return;
if (millis() - mini_started < mini_anim->holds[mini_frame]) return;
mini_started = millis();
mini_frame = (mini_frame + 1) % mini_anim->frame_count;
mini_render();
}
static void show_placeholder() {
// Solid dark background + centered status label.
if (canvas_buf) {