v2 Phase 5: Now Playing — Windows media session on the watch
The daemon reads the Windows "now playing" media session (WinRT SMTC) and
adds np/nt/na to the BLE payload; the watch shows a play/pause glyph, a
scrolling title, artist and status. Best-effort and token-independent, so
it works even when the rate-limit data is unavailable.
Cyrillic titles: Styrene B has no Cyrillic glyphs, so the title/artist use
new composite fonts (font_styrene_cyr_{28,20}) — Latin from Styrene B,
Cyrillic + typographic punctuation from Montserrat, merged by lv_font_conv.
Latin titles stay on-brand; only Cyrillic falls back to Montserrat.
- daemon: read_now_playing() via winrt-Windows.Media.Control (lazy import,
never raises; PLAYING->1, PAUSED->2, else 0; title/artist truncated,
empty fields omitted)
- firmware: UsageData np_state/np_title/np_artist + parser; the real Now
Playing screen replaces the shared stub (SCREEN_NOWPLAYING)
- fonts: assets/Montserrat-Medium.ttf, font_styrene_cyr_{28,20}.c
- tools: patch_lvgl9_font.py (automates the 4 LVGL 9 font patches),
screenshot_win.py (Windows serial screenshot via pyserial + Pillow)
- README: document Cyrillic composite font generation
Verified on hardware (waveshare_amoled_206): idle state, a live Latin
title (scrolling), and Cyrillic (Prohozhdenie / Dunduk) all render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+80
-1
@@ -14,6 +14,10 @@ LV_FONT_DECLARE(font_styrene_24);
|
||||
LV_FONT_DECLARE(font_styrene_20);
|
||||
LV_FONT_DECLARE(font_styrene_16);
|
||||
LV_FONT_DECLARE(font_styrene_14);
|
||||
// Composite Styrene (Latin) + Montserrat (Cyrillic) — used where user content
|
||||
// (media titles) can be non-Latin; brand Styrene has no Cyrillic glyphs.
|
||||
LV_FONT_DECLARE(font_styrene_cyr_28);
|
||||
LV_FONT_DECLARE(font_styrene_cyr_20);
|
||||
LV_FONT_DECLARE(font_mono_32);
|
||||
|
||||
// Layout values computed from the active board's geometry. Populated once
|
||||
@@ -134,6 +138,11 @@ static lv_obj_t* sess_cost_lbl;
|
||||
static lv_obj_t* sess_tokens_lbl;
|
||||
static lv_obj_t* sess_gen_lbl;
|
||||
static lv_obj_t* sess_msgs_lbl;
|
||||
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_title_lbl; // track title (scrolls if long)
|
||||
static lv_obj_t* np_artist_lbl; // track artist
|
||||
static lv_obj_t* np_status_lbl; // "Playing" / "Paused"
|
||||
|
||||
// App registry — the launcher renders one tile per entry, so adding a screen is
|
||||
// one line here plus its builder. Order = tile order. "Animations" reuses the
|
||||
@@ -734,6 +743,53 @@ static void init_session_screen(lv_obj_t* scr) {
|
||||
lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// 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
|
||||
// of rate-limit data, so it stays live even when the OAuth token is unavailable.
|
||||
static void init_nowplaying_screen(lv_obj_t* scr) {
|
||||
nowplaying_container = lv_obj_create(scr);
|
||||
lv_obj_set_size(nowplaying_container, L.scr_w, L.scr_h);
|
||||
lv_obj_set_pos(nowplaying_container, 0, 0);
|
||||
lv_obj_set_style_bg_opa(nowplaying_container, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(nowplaying_container, 0, 0);
|
||||
lv_obj_set_style_pad_all(nowplaying_container, 0, 0);
|
||||
lv_obj_clear_flag(nowplaying_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
make_screen_title(nowplaying_container, "Now Playing");
|
||||
|
||||
np_icon_lbl = lv_label_create(nowplaying_container);
|
||||
lv_label_set_text(np_icon_lbl, LV_SYMBOL_AUDIO);
|
||||
lv_obj_set_style_text_font(np_icon_lbl, &lv_font_montserrat_28, 0);
|
||||
lv_obj_set_style_text_color(np_icon_lbl, COL_DIM, 0);
|
||||
lv_obj_align(np_icon_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 35);
|
||||
|
||||
np_title_lbl = lv_label_create(nowplaying_container);
|
||||
lv_obj_set_width(np_title_lbl, L.content_w);
|
||||
lv_label_set_long_mode(np_title_lbl, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(np_title_lbl, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_font(np_title_lbl, &font_styrene_cyr_28, 0);
|
||||
lv_obj_set_style_text_color(np_title_lbl, COL_TEXT, 0);
|
||||
lv_label_set_text(np_title_lbl, "Nothing playing");
|
||||
lv_obj_align(np_title_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 120);
|
||||
|
||||
np_artist_lbl = lv_label_create(nowplaying_container);
|
||||
lv_obj_set_width(np_artist_lbl, L.content_w);
|
||||
lv_label_set_long_mode(np_artist_lbl, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_style_text_align(np_artist_lbl, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_font(np_artist_lbl, &font_styrene_cyr_20, 0);
|
||||
lv_obj_set_style_text_color(np_artist_lbl, COL_DIM, 0);
|
||||
lv_label_set_text(np_artist_lbl, "");
|
||||
lv_obj_align(np_artist_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 170);
|
||||
|
||||
np_status_lbl = lv_label_create(nowplaying_container);
|
||||
lv_obj_set_style_text_font(np_status_lbl, &font_styrene_16, 0);
|
||||
lv_obj_set_style_text_color(np_status_lbl, COL_DIM, 0);
|
||||
lv_label_set_text(np_status_lbl, "");
|
||||
lv_obj_align(np_status_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 215);
|
||||
|
||||
lv_obj_add_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// ======== Public API ========
|
||||
|
||||
void ui_init(void) {
|
||||
@@ -759,6 +815,7 @@ void ui_init(void) {
|
||||
init_bt_screen(scr);
|
||||
init_soundpad_screen(scr);
|
||||
init_session_screen(scr);
|
||||
init_nowplaying_screen(scr);
|
||||
|
||||
logo_img = lv_image_create(scr);
|
||||
lv_image_set_src(logo_img, &logo_dsc);
|
||||
@@ -801,6 +858,27 @@ void ui_update(const UsageData* data) {
|
||||
lv_label_set_text_fmt(sess_msgs_lbl, "%d requests", data->messages_today);
|
||||
}
|
||||
|
||||
// Now Playing — local media session relayed by the daemon. Like Session, it's
|
||||
// independent of the rate-limit data, so refresh it before the ok==false bail.
|
||||
if (nowplaying_container) {
|
||||
if (data->np_state == 0) {
|
||||
lv_label_set_text(np_icon_lbl, LV_SYMBOL_AUDIO);
|
||||
lv_obj_set_style_text_color(np_icon_lbl, COL_DIM, 0);
|
||||
lv_label_set_text(np_title_lbl, "Nothing playing");
|
||||
lv_obj_set_style_text_color(np_title_lbl, COL_DIM, 0);
|
||||
lv_label_set_text(np_artist_lbl, "");
|
||||
lv_label_set_text(np_status_lbl, "");
|
||||
} else {
|
||||
bool playing = (data->np_state == 1);
|
||||
lv_label_set_text(np_icon_lbl, playing ? LV_SYMBOL_PLAY : LV_SYMBOL_PAUSE);
|
||||
lv_obj_set_style_text_color(np_icon_lbl, COL_ACCENT, 0);
|
||||
lv_label_set_text(np_title_lbl, data->np_title[0] ? data->np_title : "(no title)");
|
||||
lv_obj_set_style_text_color(np_title_lbl, COL_TEXT, 0);
|
||||
lv_label_set_text(np_artist_lbl, data->np_artist);
|
||||
lv_label_set_text(np_status_lbl, playing ? "Playing" : "Paused");
|
||||
}
|
||||
}
|
||||
|
||||
// Rate-limit utilization (5h / 7d). The daemon sets ok=false when this data
|
||||
// is unavailable (expired OAuth token, API down). Skip the bars and DON'T
|
||||
// bump the freshness clock then, so the usage view falls back to its idle
|
||||
@@ -934,6 +1012,7 @@ void ui_show_screen(screen_t screen) {
|
||||
if (bt_container) lv_obj_add_flag(bt_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 (nowplaying_container) lv_obj_add_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN);
|
||||
splash_hide();
|
||||
|
||||
switch (screen) {
|
||||
@@ -942,8 +1021,8 @@ void ui_show_screen(screen_t screen) {
|
||||
case SCREEN_MENU: lv_obj_clear_flag(menu_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_NOWPLAYING: lv_obj_clear_flag(nowplaying_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||
case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||
case SCREEN_NOWPLAYING:
|
||||
case SCREEN_HOMEASSIST:
|
||||
stub_show_for(screen);
|
||||
lv_obj_clear_flag(stub_container, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
Reference in New Issue
Block a user