Pulling the USB Bluetooth dongle made BleakScanner raise from inside the main
loop; the exception propagated out of daemon_main, the bg thread died, and the
tray froze on its last state with no recovery (reported: "daemon crashed and
doesn't come back").
Two layers of hardening:
1. main() loop now wraps scan+connect in try/except — adapter-gone errors are
logged, the tray shows Scanning, and it backs off and retries, so replugging
the adapter recovers automatically. CancelledError is re-raised.
2. The tray supervises the asyncio loop: an unexpected crash is logged and the
loop is RESTARTED after a backoff, gated by a quit_evt so Quit still stops
cleanly (and call_soon_threadsafe is guarded against a closed loop).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs made the Session numbers wildly inflated (~$300/122M when the real
figures were ~$46/57M):
1. No de-duplication. Claude Code copies prior history into a new transcript
file on every compaction/resume, so the same API turn appears in several
.jsonl files. Summing all lines counted each turn multiple times — ~2.7x
inflation today (a heavily-compacted session). De-dup by (messageId,
requestId), matching ccusage.
2. Wrong Opus pricing. Used the older $15/$75 in/out rates; claude-opus-4-x is
actually $5/$25 (cache-write $10, cache-read $0.50) — 3x too high. Rates
verified against ccusage/LiteLLM, which they now reproduce to 100%.
Verified end to end against a fresh ccusage run: total tokens, output tokens and
cost all match exactly (57,256,809 tok / $45.63). Daemon-only change; no firmware
reflash needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The total is ~97% cache reads, which reads as implausibly large on its own.
Show the cache-inclusive total as the headline token number and the output
tokens ("what Claude actually generated", ~1.1M) as a small line beneath it —
impressive total, believable sub-number. Daemon sends a new `to` field; firmware
parses output_today and renders it. Re-spaced the Session layout for the 5th row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The count is assistant API turns (each tool-use round is one), not user chat
messages — "messages" misled into reading it as conversation count / all-time.
"requests" reads correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Daemon: compute_today_usage() sums today's Claude Code token usage across the
local project transcripts (~/.claude/projects/**/*.jsonl), reading only files
modified today so the scan stays cheap, and prices it with Anthropic list rates
to show the equivalent API cost (the ccusage-style flex for subscription users).
The compact fields tk (total tokens), tc (cost in cents) and tn (message count)
piggyback onto the existing 60s BLE payload.
Firmware: UsageData gains tokens_today (64-bit) / cost_cents_today /
messages_today; parse_json reads tk/tc/tn; a new Session screen shows the cost
as the hero with total tokens and message count below, refreshed from ui_update
so it's current whenever opened. Wire SCREEN_SESSION to the real screen (was a
stub). Verified end to end: daemon sends e.g. tk=105374109 tc=26838 tn=663.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Soundpad app screen: a 2x3 grid of colored pads that each emit one of
F13..F18 over the existing BLE HID keyboard, for mapping to a host soundboard.
Key release is deferred via a one-shot LVGL timer so the UI thread never blocks.
Raise the HID report-map keycode ceiling from 101 (0x65) to 115 (0x73) in
ble.cpp so F13..F24 (0x68..0x73) are within the declared key-array range — they
were silently undeliverable before. Wire SCREEN_SOUNDPAD to the real screen
(was a stub).
NOTE: the HID report descriptor changed, so the host must re-pair to refresh its
cached map before F13..F18 are recognized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tile grid used track_cross_place=CENTER, which vertically centers the
overflowing block of rows — hiding the top row under the title and springing
back to it after an elastic pull. Switch to START so rows stack from the top:
the first row sits under the title and the rest is reached by scrolling down.
Nudge the back chevron down/right so it clears the rounded top-left corner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the splash<->usage tap-toggle with a home + launcher model. Tapping the
Claude logo on the home (usage) screen opens a scrollable 2-column launcher
driven by an app registry — adding a screen is one registry line plus a builder,
no edits to shared navigation. A shared back chevron returns launcher->home and
app->launcher; tapping the splash returns to home.
Screens wired: Usage (home) and Animations (splash) are real; Bluetooth shows
live connection state + device name + MAC; Soundpad/Session/Now Playing/Home
point at a shared "coming soon" stub until their phases land. Enable Montserrat
20/28 for the LVGL symbol glyphs used by tile icons and the back chevron (the
brand Styrene/Tiempos fonts are Latin-only). Flash 20.3% -> 20.9%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-hardware calibration (5-point corner+center tap) confirms the FT3168 on the
2.06 panel reports directly in panel pixel space (~0..409 x ~0..501), correctly
oriented with no axis swap or mirror — a center tap reads ~(209,228) against the
410x502 panel center (205,251), a 1:1 mapping within finger error. The existing
raw passthrough is correct; record the verified result in the driver comment.
The temporary on-screen TOUCH_DEBUG overlay used to gather this has been removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freeze the classic line on main (tagged v1.0-classic) and open the v2
development branch. Add a firmware version constant (2.0.0-dev) reported in
the boot banner so the daemon can tell classic from v2, and document the
phased v2 plan in docs/v2-roadmap.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>