v3 M1c (firmware): Provider screen — active source + tap-to-switch
New SCREEN_PROVIDER (Menu → "Provider"): shows the active provider's brand name
in the accent colour (recolors live on a theme switch via the shared accent
style) and its "i / c" position among the enabled providers. A Switch button
notifies {"cmd":"provnext"}; the daemon owns the enabled set/order and cycles to
the next one, so the watch stays dumb. When only one provider is enabled the
button is replaced by a hint.
main.cpp parses the optional pnm/pi/pc payload fields into ui_set_provider_badge.
Builds green on all four boards (206/216/18/216_c6); verified on 2.06 hardware
(OpenAI green theme + "2 / 3" + Switch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,13 @@ static bool parse_json(const char* json, UsageData* out) {
|
|||||||
const char* pv = doc["pv"] | (const char*)nullptr;
|
const char* pv = doc["pv"] | (const char*)nullptr;
|
||||||
uint32_t ac = doc["ac"] | 0u;
|
uint32_t ac = doc["ac"] | 0u;
|
||||||
if (pv != nullptr || ac != 0u) ui_set_theme(pv, ac);
|
if (pv != nullptr || ac != 0u) ui_set_theme(pv, ac);
|
||||||
|
|
||||||
|
// v3 Provider screen badge — optional "pnm" (name) + "pi"/"pc" (1-based
|
||||||
|
// position / count among enabled providers). Present on the ~60s heartbeat.
|
||||||
|
const char* pnm = doc["pnm"] | (const char*)nullptr;
|
||||||
|
int pi = doc["pi"] | 0;
|
||||||
|
int pc = doc["pc"] | 0;
|
||||||
|
if (pnm != nullptr || pc != 0) ui_set_provider_badge(pnm, pi, pc);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ static lv_obj_t* sess_cost_lbl;
|
|||||||
static lv_obj_t* sess_tokens_lbl;
|
static lv_obj_t* sess_tokens_lbl;
|
||||||
static lv_obj_t* sess_gen_lbl;
|
static lv_obj_t* sess_gen_lbl;
|
||||||
static lv_obj_t* sess_msgs_lbl;
|
static lv_obj_t* sess_msgs_lbl;
|
||||||
|
static lv_obj_t* provider_container; // which usage source the watch shows (v3)
|
||||||
|
static lv_obj_t* prov_name_lbl; // active provider's brand name (accent-coloured)
|
||||||
|
static lv_obj_t* prov_pos_lbl; // "1 / 3" position among enabled providers
|
||||||
|
static lv_obj_t* prov_switch_btn; // tap → {"cmd":"provnext"} (daemon cycles)
|
||||||
|
static lv_obj_t* prov_hint_lbl; // shown instead of the button when only one is enabled
|
||||||
static lv_obj_t* nowplaying_container; // media now-playing (Phase 5)
|
static lv_obj_t* nowplaying_container; // media now-playing (Phase 5)
|
||||||
static lv_obj_t* np_icon_lbl; // play / pause / music glyph
|
static lv_obj_t* np_icon_lbl; // play / pause / music glyph
|
||||||
static lv_obj_t* np_title_lbl; // track title (scrolls if long)
|
static lv_obj_t* np_title_lbl; // track title (scrolls if long)
|
||||||
@@ -192,6 +197,7 @@ static const AppEntry APPS[] = {
|
|||||||
{ SCREEN_NOWPLAYING, "Now Playing", LV_SYMBOL_PLAY },
|
{ SCREEN_NOWPLAYING, "Now Playing", LV_SYMBOL_PLAY },
|
||||||
{ SCREEN_HOMEASSIST, "Home", LV_SYMBOL_HOME },
|
{ SCREEN_HOMEASSIST, "Home", LV_SYMBOL_HOME },
|
||||||
{ SCREEN_DIMMER, "Dimmer", LV_SYMBOL_SETTINGS },
|
{ SCREEN_DIMMER, "Dimmer", LV_SYMBOL_SETTINGS },
|
||||||
|
{ SCREEN_PROVIDER, "Provider", LV_SYMBOL_SHUFFLE },
|
||||||
{ SCREEN_BLUETOOTH, "Bluetooth", LV_SYMBOL_BLUETOOTH },
|
{ SCREEN_BLUETOOTH, "Bluetooth", LV_SYMBOL_BLUETOOTH },
|
||||||
};
|
};
|
||||||
#define APP_COUNT (sizeof(APPS) / sizeof(APPS[0]))
|
#define APP_COUNT (sizeof(APPS) / sizeof(APPS[0]))
|
||||||
@@ -736,6 +742,78 @@ static void init_session_screen(lv_obj_t* scr) {
|
|||||||
lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider (v3): which usage source the watch is displaying. Shows the active
|
||||||
|
// provider's brand name in the accent colour and its position among the enabled
|
||||||
|
// set; the Switch button asks the daemon to advance to the next one (the daemon
|
||||||
|
// owns the enabled list/order, so the watch just says "next"). Name/position are
|
||||||
|
// refreshed from ui_set_provider_badge() as payloads arrive.
|
||||||
|
static void prov_switch_cb(lv_event_t* e) {
|
||||||
|
(void)e;
|
||||||
|
ble_send_command("{\"cmd\":\"provnext\"}");
|
||||||
|
if (prov_pos_lbl) lv_label_set_text(prov_pos_lbl, "Switching\xE2\x80\xA6"); // …
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init_provider_screen(lv_obj_t* scr) {
|
||||||
|
provider_container = lv_obj_create(scr);
|
||||||
|
lv_obj_set_size(provider_container, L.scr_w, L.scr_h);
|
||||||
|
lv_obj_set_pos(provider_container, 0, 0);
|
||||||
|
lv_obj_set_style_bg_opa(provider_container, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(provider_container, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(provider_container, 0, 0);
|
||||||
|
lv_obj_clear_flag(provider_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
make_screen_title(provider_container, "Provider");
|
||||||
|
|
||||||
|
lv_obj_t* cap = lv_label_create(provider_container);
|
||||||
|
lv_label_set_text(cap, "Showing on watch");
|
||||||
|
lv_obj_set_style_text_font(cap, &font_styrene_16, 0);
|
||||||
|
lv_obj_set_style_text_color(cap, COL_DIM, 0);
|
||||||
|
lv_obj_align(cap, LV_ALIGN_TOP_MID, 0, L.content_y + 34);
|
||||||
|
|
||||||
|
prov_name_lbl = lv_label_create(provider_container);
|
||||||
|
lv_label_set_text(prov_name_lbl, "Claude Code");
|
||||||
|
lv_obj_set_style_text_font(prov_name_lbl, &font_tiempos_34, 0);
|
||||||
|
lv_obj_add_style(prov_name_lbl, &s_accent_style, 0); // brand colour, recolors on switch
|
||||||
|
lv_obj_align(prov_name_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 70);
|
||||||
|
|
||||||
|
prov_pos_lbl = lv_label_create(provider_container);
|
||||||
|
lv_label_set_text(prov_pos_lbl, "");
|
||||||
|
lv_obj_set_style_text_font(prov_pos_lbl, &font_styrene_20, 0);
|
||||||
|
lv_obj_set_style_text_color(prov_pos_lbl, COL_DIM, 0);
|
||||||
|
lv_obj_align(prov_pos_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 122);
|
||||||
|
|
||||||
|
// Switch button — advance to the next enabled provider.
|
||||||
|
prov_switch_btn = lv_obj_create(provider_container);
|
||||||
|
lv_obj_set_size(prov_switch_btn, 190, 74);
|
||||||
|
lv_obj_align(prov_switch_btn, LV_ALIGN_TOP_MID, 0, L.content_y + 178);
|
||||||
|
lv_obj_set_style_bg_color(prov_switch_btn, COL_PANEL, 0);
|
||||||
|
lv_obj_set_style_bg_opa(prov_switch_btn, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(prov_switch_btn, 16, 0);
|
||||||
|
lv_obj_set_style_border_width(prov_switch_btn, 0, 0);
|
||||||
|
lv_obj_set_style_border_width(prov_switch_btn, 3, LV_STATE_PRESSED); // accent ring on press
|
||||||
|
lv_obj_set_style_border_color(prov_switch_btn, COL_ACCENT, LV_STATE_PRESSED);
|
||||||
|
lv_obj_clear_flag(prov_switch_btn, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_flag(prov_switch_btn, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(prov_switch_btn, prov_switch_cb, LV_EVENT_CLICKED, NULL);
|
||||||
|
|
||||||
|
// Brand text is Styrene (ASCII-only); LVGL symbol glyphs live in Montserrat,
|
||||||
|
// so a symbol here would render as tofu. Keep the label plain "Switch".
|
||||||
|
lv_obj_t* sw_lbl = lv_label_create(prov_switch_btn);
|
||||||
|
lv_label_set_text(sw_lbl, "Switch");
|
||||||
|
lv_obj_set_style_text_font(sw_lbl, &font_styrene_20, 0);
|
||||||
|
lv_obj_set_style_text_color(sw_lbl, COL_TEXT, 0);
|
||||||
|
lv_obj_center(sw_lbl);
|
||||||
|
|
||||||
|
prov_hint_lbl = lv_label_create(provider_container);
|
||||||
|
lv_label_set_text(prov_hint_lbl, "Only provider enabled");
|
||||||
|
lv_obj_set_style_text_font(prov_hint_lbl, &font_styrene_16, 0);
|
||||||
|
lv_obj_set_style_text_color(prov_hint_lbl, COL_DIM, 0);
|
||||||
|
lv_obj_align(prov_hint_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 200);
|
||||||
|
lv_obj_add_flag(prov_hint_lbl, LV_OBJ_FLAG_HIDDEN); // shown only when count == 1
|
||||||
|
|
||||||
|
lv_obj_add_flag(provider_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
// Now Playing: the Windows media session relayed by the daemon. A play/pause glyph
|
// Now Playing: the Windows media session relayed by the daemon. A play/pause glyph
|
||||||
// and status line reflect playback; the title scrolls if it overflows. Independent
|
// and status line reflect playback; the title scrolls if it overflows. Independent
|
||||||
// of rate-limit data, so it stays live even when the OAuth token is unavailable.
|
// of rate-limit data, so it stays live even when the OAuth token is unavailable.
|
||||||
@@ -1313,6 +1391,7 @@ void ui_init(void) {
|
|||||||
init_soundpad_screen(scr);
|
init_soundpad_screen(scr);
|
||||||
init_session_screen(scr);
|
init_session_screen(scr);
|
||||||
init_nowplaying_screen(scr);
|
init_nowplaying_screen(scr);
|
||||||
|
init_provider_screen(scr);
|
||||||
init_homeassist_screen(scr);
|
init_homeassist_screen(scr);
|
||||||
init_dimmer_screen(scr);
|
init_dimmer_screen(scr);
|
||||||
init_battery_screen(scr);
|
init_battery_screen(scr);
|
||||||
@@ -1546,6 +1625,7 @@ void ui_show_screen(screen_t screen) {
|
|||||||
if (soundpad_container) lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
|
if (soundpad_container) lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (session_container) lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
|
if (session_container) lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (nowplaying_container) lv_obj_add_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN);
|
if (nowplaying_container) lv_obj_add_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (provider_container) lv_obj_add_flag(provider_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (homeassist_container) lv_obj_add_flag(homeassist_container, LV_OBJ_FLAG_HIDDEN);
|
if (homeassist_container) lv_obj_add_flag(homeassist_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (dimmer_container) lv_obj_add_flag(dimmer_container, LV_OBJ_FLAG_HIDDEN);
|
if (dimmer_container) lv_obj_add_flag(dimmer_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (battery_container) lv_obj_add_flag(battery_container, LV_OBJ_FLAG_HIDDEN);
|
if (battery_container) lv_obj_add_flag(battery_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -1562,6 +1642,7 @@ void ui_show_screen(screen_t screen) {
|
|||||||
case SCREEN_SOUNDPAD: lv_obj_clear_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN); break;
|
case SCREEN_SOUNDPAD: lv_obj_clear_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
case SCREEN_SESSION: lv_obj_clear_flag(session_container, LV_OBJ_FLAG_HIDDEN); break;
|
case SCREEN_SESSION: lv_obj_clear_flag(session_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
case SCREEN_NOWPLAYING: lv_obj_clear_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN); break;
|
case SCREEN_NOWPLAYING: lv_obj_clear_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
|
case SCREEN_PROVIDER: lv_obj_clear_flag(provider_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break;
|
case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
case SCREEN_HOMEASSIST:
|
case SCREEN_HOMEASSIST:
|
||||||
lv_obj_clear_flag(homeassist_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_clear_flag(homeassist_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -1633,6 +1714,27 @@ void ui_set_theme(const char* pv, uint32_t accent_rgb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh the Provider screen's name + "i / c" position, and show the Switch
|
||||||
|
// button unless exactly one provider is enabled (then a hint replaces it). Safe
|
||||||
|
// to call on every payload; name==nullptr leaves the current name in place.
|
||||||
|
void ui_set_provider_badge(const char* name, int index, int count) {
|
||||||
|
if (!provider_container) return;
|
||||||
|
if (name && *name && prov_name_lbl) lv_label_set_text(prov_name_lbl, name);
|
||||||
|
if (prov_pos_lbl) {
|
||||||
|
if (count > 0 && index > 0) lv_label_set_text_fmt(prov_pos_lbl, "%d / %d", index, count);
|
||||||
|
else lv_label_set_text(prov_pos_lbl, "");
|
||||||
|
}
|
||||||
|
bool single = (count == 1); // count 0 (unknown) keeps the button visible
|
||||||
|
if (prov_switch_btn) {
|
||||||
|
if (single) lv_obj_add_flag(prov_switch_btn, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
else lv_obj_clear_flag(prov_switch_btn, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
if (prov_hint_lbl) {
|
||||||
|
if (single) lv_obj_clear_flag(prov_hint_lbl, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
else lv_obj_add_flag(prov_hint_lbl, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ui_update_ble_status(ble_state_t state, const char* name, const char* mac) {
|
void ui_update_ble_status(ble_state_t state, const char* name, const char* mac) {
|
||||||
(void)name; (void)mac;
|
(void)name; (void)mac;
|
||||||
bool was_connected = s_ble_connected;
|
bool was_connected = s_ble_connected;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ enum screen_t {
|
|||||||
SCREEN_HOMEASSIST, // Home Assistant controls (Phase 6)
|
SCREEN_HOMEASSIST, // Home Assistant controls (Phase 6)
|
||||||
SCREEN_DIMMER, // tilt-to-dim light control (Phase 6 step 3 / M3)
|
SCREEN_DIMMER, // tilt-to-dim light control (Phase 6 step 3 / M3)
|
||||||
SCREEN_BLUETOOTH, // BLE connection info
|
SCREEN_BLUETOOTH, // BLE connection info
|
||||||
|
SCREEN_PROVIDER, // which usage source the watch displays (v3) + Switch
|
||||||
SCREEN_BATTERY, // battery detail (voltage + time left) — opened by tapping the battery icon
|
SCREEN_BATTERY, // battery detail (voltage + time left) — opened by tapping the battery icon
|
||||||
SCREEN_COUNT,
|
SCREEN_COUNT,
|
||||||
};
|
};
|
||||||
@@ -25,6 +26,12 @@ void ui_init(void);
|
|||||||
// Change-guarded, so it's safe to call on every payload.
|
// 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_set_theme(const char* provider_id, uint32_t accent_rgb);
|
||||||
|
|
||||||
|
// v3 Provider screen badge: the active provider's human name (`pnm`) and its
|
||||||
|
// 1-based position/count among the enabled providers (`pi`/`pc`), so the screen
|
||||||
|
// shows "Claude Code · 1 / 3" and hides the Switch button when only one exists.
|
||||||
|
// Safe to call on every payload. name==nullptr keeps the current name.
|
||||||
|
void ui_set_provider_badge(const char* name, int index, int count);
|
||||||
|
|
||||||
void ui_update(const UsageData* data);
|
void ui_update(const UsageData* data);
|
||||||
void ui_tick_anim(void);
|
void ui_tick_anim(void);
|
||||||
void ui_show_screen(screen_t screen);
|
void ui_show_screen(screen_t screen);
|
||||||
|
|||||||
Reference in New Issue
Block a user