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:
@@ -527,6 +527,58 @@ async def refresh_token_if_needed(force: bool = False) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# --- Now Playing (Phase 5) ----------------------------------------------------
|
||||
# Best-effort read of the Windows "now playing" media session (System Media
|
||||
# Transport Controls) so the watch can show the current track. Pure local WinRT —
|
||||
# no network, no token — so it works even when the rate-limit data is unavailable.
|
||||
# Any failure degrades to {"np": 0} (nothing playing) and never disturbs the loop.
|
||||
NP_MAX_LEN = 60 # truncate title/artist so the BLE payload stays comfortably small
|
||||
|
||||
|
||||
async def read_now_playing() -> dict:
|
||||
"""Return compact now-playing fields for the BLE payload::
|
||||
|
||||
{"np": 0|1|2, "nt": <title>, "na": <artist>}
|
||||
|
||||
np: 0 = nothing playing, 1 = playing, 2 = paused. nt/na are omitted when
|
||||
empty. Never raises — the daemon must keep polling even if WinRT hiccups
|
||||
(and the winrt media package may simply be absent on some installs).
|
||||
"""
|
||||
try:
|
||||
from winrt.windows.media.control import (
|
||||
GlobalSystemMediaTransportControlsSessionManager as MediaManager,
|
||||
GlobalSystemMediaTransportControlsSessionPlaybackStatus as PB,
|
||||
)
|
||||
except ImportError:
|
||||
return {"np": 0}
|
||||
|
||||
try:
|
||||
mgr = await MediaManager.request_async()
|
||||
sess = mgr.get_current_session()
|
||||
if sess is None:
|
||||
return {"np": 0}
|
||||
status = sess.get_playback_info().playback_status
|
||||
if status == PB.PLAYING:
|
||||
np = 1
|
||||
elif status == PB.PAUSED:
|
||||
np = 2
|
||||
else:
|
||||
np = 0 # stopped / closed / changing -> treat as nothing playing
|
||||
out: dict = {"np": np}
|
||||
if np:
|
||||
info = await sess.try_get_media_properties_async()
|
||||
title = (info.title or "").strip()
|
||||
artist = (info.artist or "").strip()
|
||||
if title:
|
||||
out["nt"] = title[:NP_MAX_LEN]
|
||||
if artist:
|
||||
out["na"] = artist[:NP_MAX_LEN]
|
||||
return out
|
||||
except Exception as e: # WinRT can surface assorted OSError/RuntimeError types
|
||||
log(f"Now-playing read failed: {e!r}")
|
||||
return {"np": 0}
|
||||
|
||||
|
||||
async def _wait_first(*events: asyncio.Event, timeout: float) -> None:
|
||||
"""Return when any of `events` is set, or after `timeout` seconds.
|
||||
|
||||
@@ -655,6 +707,13 @@ async def connect_and_run(device, stop_event: asyncio.Event, tray_state=None) ->
|
||||
else:
|
||||
payload["ok"] = False # rate-limit unknown -> watch usage view goes idle
|
||||
|
||||
# Now Playing (Phase 5) — best-effort Windows media session. Local
|
||||
# only, so it's attached regardless of the rate-limit "ok" above.
|
||||
try:
|
||||
payload.update(await read_now_playing())
|
||||
except Exception as e: # never let media reading break the poll loop
|
||||
log(f"Now-playing skipped: {e!r}")
|
||||
|
||||
if await session.write_payload(payload):
|
||||
last_poll = time.time()
|
||||
used_successfully = True
|
||||
|
||||
Reference in New Issue
Block a user