v2: real-time Now Playing cadence + robust long-title BLE writes

Decouple the two data sources that share the BLE link:
- Anthropic usage / rate-limit: still polled every 60s.
- Windows media session: read every 3s and pushed the moment the track or
  play/pause state changes, so a song change reaches the watch in seconds
  instead of at the next 60s poll. The last usage payload is cached and merged
  into each now-playing write, so the firmware always gets one complete JSON
  object and the usage screens never blank between polls.

Fix: long media titles (a 44-char Cyrillic title -> ~256 B once json escapes
each char to \uXXXX) overflowed the ATT MTU, so every write-without-response
failed with E_INVALIDARG and tripped the zombie-link break in a reconnect loop.
Switch the RX write to response=True (WinRT does a reliable long write; the RX
char already advertises WRITE and NimBLE reassembles into its 512 B buffer) and
serialize with ensure_ascii=False so Cyrillic goes as 2-byte UTF-8 instead of
6-byte escapes. Verified on hardware: stable link, writes succeed across the
60s heartbeat.

Firmware: only re-set the Now Playing labels when the track / state actually
changed, so the faster cadence does not restart the circular title-scroll
animation on every (often identical) payload.

requirements-windows.txt: add winrt-Windows.Media[.Control]. Phase 5 imports
them but they were never declared, so now-playing silently degraded to
"nothing playing" on a fresh machine.

tests: fix two stale poll_api assertions that expected an "ok" key poll_api has
not emitted since that flag moved to the caller (connect_and_run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-21 09:43:31 +03:00
co-authored by Claude Opus 4.8
parent b8d4daa03f
commit 650af4221b
5 changed files with 124 additions and 55 deletions
+4 -3
View File
@@ -73,7 +73,7 @@ def test_poll_api_nominal(monkeypatch):
assert payload["s"] == 42
assert payload["w"] == 10
assert payload["st"] == "allowed"
assert payload["ok"] is True
# "ok" is added by the caller (connect_and_run), not by poll_api itself.
# reset_minutes allows ±1 minute tolerance
assert abs(payload["sr"] - 60) <= 1, f"Expected ~60, got {payload['sr']}"
assert abs(payload["wr"] - 1440) <= 1, f"Expected ~1440, got {payload['wr']}"
@@ -416,9 +416,10 @@ def test_wire_bytes_compact_json_shape(monkeypatch):
assert ": " not in wire_str, f"Non-compact JSON detected: {wire_str!r}"
assert ", " not in wire_str, f"Non-compact JSON detected: {wire_str!r}"
# Must start with '{' and contain all required keys
# Must start with '{' and contain every key poll_api emits. ("ok" is added
# later by connect_and_run, so it is intentionally not part of poll_api output.)
assert wire_str.startswith("{")
for key in ("s", "sr", "w", "wr", "st", "ok"):
for key in ("s", "sr", "w", "wr", "st"):
assert f'"{key}"' in wire_str, f"Missing key {key!r} in wire bytes: {wire_str!r}"