v3 M1b: runtime per-provider theming (firmware) + accent push (daemon)
The watch recolors to the active provider's brand, live, and it's cheap to add more providers later. Firmware: - theme.h: expose THEME_ACCENT_HEX; ui.cpp: COL_ACCENT becomes a runtime lv_color_t backed by a shared style (s_accent_style). Static accent widgets (launcher tile icons, usage status line, session tokens) attach the shared style, so ui_set_theme() recolors them instantly via lv_obj_report_style_change — no widget tracking, no recreation. - ui_set_theme(pv, accent_rgb): swaps the accent + a small logo table keyed by provider id (fallback to the Claude mark until a provider ships a logo). Change- guarded, so the ~3s payload cadence doesn't churn. - main.cpp: parse "pv" (id) + "ac" (0xRRGGBB) from the payload and apply. - Adding a provider needs zero firmware color edits (accent is data-driven) and at most one logo-table row. Daemon: push "ac" = active provider's brand accent alongside "pv". Test fix: test_providers used asyncio.run(), which closed the suite's shared event loop and broke every later get_event_loop() test (Py3.13) — mirror the repo's _run() helper instead. Verified on the 2.06: forcing an OpenAI-green theme recolored the live launcher icons (report_style_change path). 216 builds clean; daemon suite 94 passed.
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+53
-4
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user