Merge pull request #64 from nathanjohnpayne/status-dot

This commit is contained in:
Hermann Björgvin
2026-06-05 18:54:35 +00:00
committed by GitHub
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); 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() { static void show_placeholder() {
// Solid dark background + centered status label. // Solid dark background + centered status label.
if (canvas_buf) { if (canvas_buf) {
+8
View File
@@ -26,3 +26,11 @@ bool splash_is_active(void);
// Root container (so ui.cpp can attach a click event). // Root container (so ui.cpp can attach a click event).
lv_obj_t* splash_get_root(void); lv_obj_t* splash_get_root(void);
// Mini animated creature for embedding elsewhere (e.g. the idle screen).
// Renders the named claudepix animation (e.g. "expression sleep") at ~px×px
// inside `parent`; returns the canvas object (position it with lv_obj_align) or
// NULL if the animation isn't found / allocation fails. Drive it with
// splash_mini_tick(). One mini creature at a time.
lv_obj_t* splash_mini_create(lv_obj_t *parent, const char *anim_name, int px);
void splash_mini_tick(void);
+66 -13
View File
@@ -119,6 +119,16 @@ static lv_obj_t* battery_img;
static lv_obj_t* logo_img; static lv_obj_t* logo_img;
static lv_image_dsc_t battery_dscs[5]; // empty, low, medium, full, charging static lv_image_dsc_t battery_dscs[5]; // empty, low, medium, full, charging
// ---- Live-data freshness → which usage sub-view to show ----
// usage panels when data is flowing, an idle "Zzz" screen when the host is
// connected but no usage update landed within DATA_FRESH_MS, the pairing hint
// when BLE is down. Re-evaluated every loop in ui_tick_anim().
static lv_obj_t* idle_group; // the "Zzz" idle screen
static uint32_t last_data_ms = 0; // lv_tick when the last valid usage update landed
static bool data_received = false; // any valid update since boot
static int view_state = -1; // -1 unknown / 0 pair / 1 idle / 2 usage
static const uint32_t DATA_FRESH_MS = 90000; // usage counts as "live" within this window (daemon sends ~60s)
// ---- Shared ---- // ---- Shared ----
static lv_image_dsc_t logo_dsc; static lv_image_dsc_t logo_dsc;
static screen_t current_screen = SCREEN_USAGE; static screen_t current_screen = SCREEN_USAGE;
@@ -322,6 +332,28 @@ static void build_pair_group(lv_obj_t* parent) {
lv_obj_add_flag(pair_group, LV_OBJ_FLAG_HIDDEN); // ui_update_ble_status decides lv_obj_add_flag(pair_group, LV_OBJ_FLAG_HIDDEN); // ui_update_ble_status decides
} }
// Idle "Zzz" screen — shown when the host is connected but no usage update has
// landed recently (token expired, daemon down, host asleep…). Full-screen, like
// the pairing hint, so we never render hours-old numbers as if they were live.
static void build_idle_group(lv_obj_t* parent) {
idle_group = lv_obj_create(parent);
lv_obj_set_size(idle_group, L.scr_w, L.scr_h - L.content_y);
lv_obj_set_pos(idle_group, 0, L.content_y);
lv_obj_set_style_bg_opa(idle_group, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(idle_group, 0, 0);
lv_obj_set_style_pad_all(idle_group, 0, 0);
lv_obj_clear_flag(idle_group, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(idle_group, LV_OBJ_FLAG_EVENT_BUBBLE);
// A shrunk-down sleeping creature (reused claudepix "expression sleep" art)
// sits between the header and the status line; the animated "Listening…"
// status line carries the words, so no extra text is needed here.
lv_obj_t* creature = splash_mini_create(idle_group, "expression sleep", 160);
if (creature) lv_obj_align(creature, LV_ALIGN_CENTER, 0, -20);
lv_obj_add_flag(idle_group, LV_OBJ_FLAG_HIDDEN); // update_view_state decides
}
static void init_usage_screen(lv_obj_t* scr) { static void init_usage_screen(lv_obj_t* scr) {
usage_container = lv_obj_create(scr); usage_container = lv_obj_create(scr);
lv_obj_set_size(usage_container, L.scr_w, L.scr_h); lv_obj_set_size(usage_container, L.scr_w, L.scr_h);
@@ -358,6 +390,7 @@ static void init_usage_screen(lv_obj_t* scr) {
&bar_weekly, &lbl_weekly_reset); &bar_weekly, &lbl_weekly_reset);
build_pair_group(usage_container); build_pair_group(usage_container);
build_idle_group(usage_container);
// Status line — always visible on the usage view. Driven by ui_tick_anim(). // Status line — always visible on the usage view. Driven by ui_tick_anim().
lbl_anim = lv_label_create(usage_container); lbl_anim = lv_label_create(usage_container);
@@ -393,10 +426,13 @@ void ui_init(void) {
battery_img = lv_image_create(scr); battery_img = lv_image_create(scr);
lv_image_set_src(battery_img, &battery_dscs[0]); lv_image_set_src(battery_img, &battery_dscs[0]);
lv_obj_set_pos(battery_img, L.scr_w - 48 - L.margin, L.title_y); lv_obj_set_pos(battery_img, L.scr_w - 48 - L.margin, L.title_y);
} }
void ui_update(const UsageData* data) { void ui_update(const UsageData* data) {
if (!data->valid) return; if (!data->valid) return;
last_data_ms = lv_tick_get(); // a valid usage update just landed → dot goes green
data_received = true;
int s_pct = (int)(data->session_pct + 0.5f); int s_pct = (int)(data->session_pct + 0.5f);
@@ -417,8 +453,33 @@ void ui_update(const UsageData* data) {
lv_label_set_text(lbl_weekly_reset, buf); lv_label_set_text(lbl_weekly_reset, buf);
} }
// Pick the usage-view sub-screen: pairing hint (BLE down), the idle "Zzz" screen
// (connected but data has gone stale), or the live usage panels. Only re-lays-out
// on an actual change. The animated status line stays visible everywhere — it
// reads "Listening…" on the idle screen, keeping it alive rather than frozen.
static void update_view_state(void) {
if (!usage_group || !pair_group || !idle_group) return;
int v;
if (!s_ble_connected) {
v = 0; // pairing hint
} else if (data_received && (lv_tick_get() - last_data_ms) < DATA_FRESH_MS) {
v = 2; // live usage
} else {
v = 1; // idle / Zzz
}
if (v == view_state) return;
view_state = v;
lv_obj_add_flag(pair_group, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(idle_group, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(usage_group, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(v == 0 ? pair_group : v == 1 ? idle_group : usage_group,
LV_OBJ_FLAG_HIDDEN);
}
void ui_tick_anim(void) { void ui_tick_anim(void) {
if (current_screen != SCREEN_USAGE) return; if (current_screen != SCREEN_USAGE) return;
update_view_state();
if (view_state == 1) splash_mini_tick(); // animate the sleeping creature on the idle screen
uint32_t now = lv_tick_get(); uint32_t now = lv_tick_get();
@@ -436,7 +497,9 @@ void ui_tick_anim(void) {
// Status text by priority. Whimsical messages only when connected & settled. // Status text by priority. Whimsical messages only when connected & settled.
const char* text; const char* text;
if (!s_ble_connected) { if (!s_ble_connected) {
text = ble_has_bonds() ? "Disconnected" : "Pairing"; text = "Waiting"; // advertising / waiting for a host connection
} else if (view_state == 1) { // idle — alternate so it reads as alive AND data-less
text = (anim_msg_idx & 1) ? "No data" : "Listening";
} else if (now - connected_at_ms < 5000) { } else if (now - connected_at_ms < 5000) {
text = "Connected"; text = "Connected";
} else { } else {
@@ -497,19 +560,9 @@ void ui_update_ble_status(ble_state_t state, const char* name, const char* mac)
bool was_connected = s_ble_connected; bool was_connected = s_ble_connected;
s_ble_connected = (state == BLE_STATE_CONNECTED); s_ble_connected = (state == BLE_STATE_CONNECTED);
// Connected → usage panels; otherwise → pairing hint. The bottom status
// line carries the live state word (Connected / Disconnected / Pairing).
if (usage_group && pair_group) {
if (s_ble_connected) {
lv_obj_clear_flag(usage_group, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(pair_group, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(usage_group, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(pair_group, LV_OBJ_FLAG_HIDDEN);
}
}
if (s_ble_connected && !was_connected) connected_at_ms = lv_tick_get(); if (s_ble_connected && !was_connected) connected_at_ms = lv_tick_get();
// pair / idle / usage — picked from connection + data freshness.
update_view_state();
} }
void ui_update_battery(int percent, bool charging) { void ui_update_battery(int percent, bool charging) {