diff --git a/.gitignore b/.gitignore index 37a6ac0..baaa3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,5 @@ daemon/ha_config.json # Session scratch — BLE daemon run logs + one-off on-device QA screenshots /m3_daemon*.log -/dimmer_qa.png +/*_qa.png /build-exe.log diff --git a/daemon/claude_usage_daemon_windows.py b/daemon/claude_usage_daemon_windows.py index a00ea13..0736169 100644 --- a/daemon/claude_usage_daemon_windows.py +++ b/daemon/claude_usage_daemon_windows.py @@ -915,6 +915,10 @@ async def connect_and_run(device, stop_event: asyncio.Event, tray_state=None) -> auth_problem = status.auth_problem cached = status.to_payload() cached["pv"] = active_pv # which brand theme the watch wears + try: # brand accent (0xRRGGBB) for the theme + cached["ac"] = int(provider.accent, 16) + except (ValueError, TypeError): + pass last_claude_poll = now # Now Playing (Phase 5) — best-effort Windows media session, read every diff --git a/daemon/tests/test_providers.py b/daemon/tests/test_providers.py index d393f45..b46e916 100644 --- a/daemon/tests/test_providers.py +++ b/daemon/tests/test_providers.py @@ -12,6 +12,13 @@ from daemon import config from daemon.providers import get_provider, AnthropicProvider, StubProvider, ProviderStatus +def _run(coro): + # Reuse the suite's shared event loop (never closed) like the other test + # modules — asyncio.run() would close it and set the current loop to None, + # breaking every later test that calls get_event_loop() (Python 3.13). + return asyncio.get_event_loop().run_until_complete(coro) + + def _write(tmp_path, monkeypatch, data: dict): """Point the config module at an isolated temp config (and LOCALAPPDATA, so the legacy-ha_config migration can't reach the real machine's file).""" @@ -67,7 +74,7 @@ def test_registry_types(): def test_stub_provider_poll_is_safe(): - st = asyncio.run(get_provider("zai").poll()) + st = _run(get_provider("zai").poll()) assert isinstance(st, ProviderStatus) assert st.ok is False diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 928e4ab..7dfb00d 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -166,6 +166,13 @@ static bool parse_json(const char* json, UsageData* out) { int maxk = dim["maxk"] | 6500; ui_dimmer_set_snapshot(on, bri, ct, mink, maxk); } + + // v3: provider theme — "pv" (id) selects the logo, "ac" (0xRRGGBB) the brand + // accent. Stamped on every usage payload; ui_set_theme is change-guarded so + // it only repaints on an actual provider switch. + const char* pv = doc["pv"] | (const char*)nullptr; + uint32_t ac = doc["ac"] | 0u; + if (pv != nullptr || ac != 0u) ui_set_theme(pv, ac); return true; } diff --git a/firmware/src/theme.h b/firmware/src/theme.h index aa68464..2d28f99 100644 --- a/firmware/src/theme.h +++ b/firmware/src/theme.h @@ -7,7 +7,8 @@ #define THEME_PANEL lv_color_hex(0x1f1f1e) // card/zone fill #define THEME_TEXT lv_color_hex(0xfaf9f5) // primary text #define THEME_DIM lv_color_hex(0xb0aea5) // secondary text -#define THEME_ACCENT lv_color_hex(0xd97757) // brand terra-cotta +#define THEME_ACCENT_HEX 0xd97757 // brand terra-cotta (raw, for runtime palette) +#define THEME_ACCENT lv_color_hex(THEME_ACCENT_HEX) // brand terra-cotta #define THEME_GREEN lv_color_hex(0x788c5d) #define THEME_AMBER lv_color_hex(0xd97757) #define THEME_RED lv_color_hex(0xc0392b) diff --git a/firmware/src/ui.cpp b/firmware/src/ui.cpp index 1473d0c..b6b2cee 100644 --- a/firmware/src/ui.cpp +++ b/firmware/src/ui.cpp @@ -100,7 +100,13 @@ static void compute_layout(const BoardCaps& c) { #define COL_PANEL THEME_PANEL #define COL_TEXT THEME_TEXT #define COL_DIM THEME_DIM -#define COL_ACCENT THEME_ACCENT +// COL_ACCENT is runtime (v3): the daemon pushes each provider's brand accent and +// ui_set_theme() swaps it live. Widgets that read COL_ACCENT directly pick up the +// new value on their next update; static widgets attach s_accent_style so a +// provider switch recolors them instantly (lv_obj_report_style_change). +static lv_color_t g_accent = lv_color_hex(THEME_ACCENT_HEX); +static lv_style_t s_accent_style; +#define COL_ACCENT g_accent #define COL_GREEN THEME_GREEN #define COL_AMBER THEME_AMBER #define COL_RED THEME_RED @@ -477,7 +483,7 @@ static void init_usage_screen(lv_obj_t* scr) { lbl_anim = lv_label_create(usage_container); lv_label_set_text(lbl_anim, ""); lv_obj_set_style_text_font(lbl_anim, &font_mono_32, 0); - lv_obj_set_style_text_color(lbl_anim, COL_ACCENT, 0); + lv_obj_add_style(lbl_anim, &s_accent_style, 0); // v3: recolors on provider switch lv_obj_align(lbl_anim, LV_ALIGN_BOTTOM_MID, 0, -15); } @@ -516,7 +522,7 @@ static lv_obj_t* make_tile(lv_obj_t* parent, const AppEntry* app, int w, int h) lv_obj_t* icon = lv_label_create(tile); lv_label_set_text(icon, app->symbol); lv_obj_set_style_text_font(icon, &lv_font_montserrat_28, 0); - lv_obj_set_style_text_color(icon, COL_ACCENT, 0); + lv_obj_add_style(icon, &s_accent_style, 0); // v3: recolors on provider switch lv_obj_t* lbl = lv_label_create(tile); lv_label_set_text(lbl, app->label); @@ -712,7 +718,7 @@ static void init_session_screen(lv_obj_t* scr) { sess_tokens_lbl = lv_label_create(session_container); lv_label_set_text(sess_tokens_lbl, "\xE2\x80\x94 tokens"); lv_obj_set_style_text_font(sess_tokens_lbl, &font_styrene_28, 0); - lv_obj_set_style_text_color(sess_tokens_lbl, COL_ACCENT, 0); + lv_obj_add_style(sess_tokens_lbl, &s_accent_style, 0); // v3: recolors on provider switch lv_obj_align(sess_tokens_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 152); sess_gen_lbl = lv_label_create(session_container); @@ -1282,6 +1288,11 @@ static void battery_screen_refresh(void) { void ui_init(void) { compute_layout(board_caps()); + // Shared accent style (v3 theming) — MUST be initialized before any screen is + // built, so widgets can attach it. ui_set_theme() recolors it live. + lv_style_init(&s_accent_style); + lv_style_set_text_color(&s_accent_style, g_accent); + lv_obj_t* scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, COL_BG, 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); @@ -1584,6 +1595,44 @@ screen_t ui_get_current_screen(void) { return current_screen; } +// ---- v3 provider theming -------------------------------------------------- +// Per-provider logo on the usage screen. Only the Claude mark ships today; adding +// a provider's logo is one row here plus an RGB565A8 asset. Unknown / not-yet- +// added providers fall back to the Claude mark, so a switch still recolors. +struct ProviderLogo { const char* pv; const lv_image_dsc_t* dsc; }; +static const ProviderLogo s_provider_logos[] = { + { "anthropic", &logo_dsc }, + // { "openai", &logo_openai_dsc }, // add when the asset lands + // { "zai", &logo_zai_dsc }, +}; +static const lv_image_dsc_t* logo_for(const char* pv) { + if (pv) + for (const auto& e : s_provider_logos) + if (strcmp(e.pv, pv) == 0) return e.dsc; + return &logo_dsc; // fallback: Claude mark until the provider ships a logo +} + +// Apply a provider's brand: recolor the shared accent style (every widget using +// it repaints via report_style_change), retint the few non-text accent widgets, +// and swap the logo. accent_rgb == 0 keeps the current accent (logo-only change). +void ui_set_theme(const char* pv, uint32_t accent_rgb) { + // Change-guarded: the daemon stamps pv/ac on every write (~3s), so only act + // on an actual switch — otherwise report_style_change would churn constantly. + static uint32_t last_accent = THEME_ACCENT_HEX; + static char last_pv[16] = ""; + if (accent_rgb && accent_rgb != last_accent) { + last_accent = accent_rgb; + g_accent = lv_color_hex(accent_rgb); + lv_style_set_text_color(&s_accent_style, g_accent); + lv_obj_report_style_change(&s_accent_style); + if (dim_arc) lv_obj_set_style_arc_color(dim_arc, g_accent, LV_PART_INDICATOR); + } + if (pv && strncmp(pv, last_pv, sizeof(last_pv)) != 0) { + strlcpy(last_pv, pv, sizeof(last_pv)); + if (logo_img) lv_image_set_src(logo_img, logo_for(pv)); + } +} + void ui_update_ble_status(ble_state_t state, const char* name, const char* mac) { (void)name; (void)mac; bool was_connected = s_ble_connected; diff --git a/firmware/src/ui.h b/firmware/src/ui.h index 9d16064..98e58de 100644 --- a/firmware/src/ui.h +++ b/firmware/src/ui.h @@ -17,6 +17,14 @@ enum screen_t { }; void ui_init(void); + +// v3 multi-provider theming. The daemon tags each usage payload with the active +// provider id (`pv`, e.g. "anthropic"/"openai"/"zai") and its brand accent +// (`ac`, 0xRRGGBB). ui_set_theme recolors the UI live via a shared accent style +// and swaps the per-provider logo. accent_rgb == 0 keeps the current accent. +// Change-guarded, so it's safe to call on every payload. +void ui_set_theme(const char* provider_id, uint32_t accent_rgb); + void ui_update(const UsageData* data); void ui_tick_anim(void); void ui_show_screen(screen_t screen);