Feature: display multiple Claude accounts with auto-rotation#52
Open
soapiece wants to merge 4 commits into
Open
Feature: display multiple Claude accounts with auto-rotation#52soapiece wants to merge 4 commits into
soapiece wants to merge 4 commits into
Conversation
On macOS the firmware is auto-connected by the OS as a BLE HID keyboard,
and CoreBluetooth excludes already-connected peripherals from scan
results. bleak's connect-by-address path also scans internally, so the
daemon's scan loop never found the device ("Device not found" forever)
even though it was plainly visible in System Settings.
Discover the target on macOS via CoreBluetooth's
retrieveConnectedPeripheralsWithServices_ and connect to the returned
peripheral directly (no scan). The custom service UUID is matched first
(unambiguous); the generic HID service 0x1812 is only trusted on an exact
DEVICE_NAME match so it can't grab an unrelated keyboard/mouse. A
peripheral that fails to connect is skipped for one cycle so the scan
fallback stays reachable. The device's two connection slots let the OS
HID link and the daemon run simultaneously.
Linux/BlueZ path is unchanged (still scans + caches address); the
redundant per-reconnect address save was removed so caching happens only
on a fresh scan.
Adds daemon/test_macos_connect.py, a foreground smoke test for the macOS
connect path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add multi-account support to the macOS/Linux daemon. Accounts are declared
in ~/.config/claude-usage-monitor/accounts.json as a list of
{label, config_dir}; absent/empty falls back to a single default account
(backward compatible).
Each account's OAuth token is resolved from its Claude Code profile: on
macOS via the per-profile Keychain item
("Claude Code-credentials[-sha256(config_dir_abspath)[:8]]"), with a file
credential fallback; on Linux from <config_dir>/.credentials.json. No
account names are hardcoded.
All accounts are polled concurrently each cycle and sent as a single JSON
array {"a":[{n,s,sr,w,wr,st,ok}, ...]} on the RX characteristic. A failed
account is still included as {n, ok:false} so the device can show it
offline rather than dropping it. Writes now use write-with-response so
CoreBluetooth performs a long write for multi-account arrays up to the
512-byte device buffer.
Firmware array parsing + per-account screen cycling lands in a follow-up
commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render multiple Claude accounts on the usage screen, driven by the
daemon's {"a":[...]} array payload. Each account shows as its own page
(account label as the title) and the device auto-rotates through them
every 5s, with a row of page dots indicating count and position.
- data.h: UsageData gains name[16]; add MAX_ACCOUNTS (8), bounded by the
512-byte BLE buffer.
- main.cpp: parse_json() reads the {"a":[...]} array into accounts[],
still accepting a legacy single object. Missing "ok" defaults to true
so legacy payloads render their numbers; only explicit "ok":false marks
an account offline. Splash rate animation tracks accounts[0].
- ui.cpp: store up to MAX_ACCOUNTS, render one at a time; auto-rotate in
ui_tick_anim() (manual cycle / screen entry / first multi-account
payload reset the timer so the shown account gets a full interval);
page-dot indicator; offline accounts show "--%"/"offline".
- ui.h: declare ui_update_accounts().
Single-account behavior is unchanged: no page dots, title "Usage", no
auto-rotation, USAGE<->BLUETOOTH toggle. Codex-reviewed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Multiple accounts" README section explaining accounts.json (label +
config_dir per account), how tokens resolve per Claude Code profile
(macOS Keychain item / Linux credentials file), the 8-account limit, and
the single-account default. Update the BLE protocol section for the new
{"a":[...]} array payload and offline-account shape. Add
daemon/accounts.example.json as a copy-paste starting point.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
|
Getting an Enterprise account at work soon so I'll look into this sorry I haven't gotten around to your PR yet. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Show usage for multiple Claude subscription accounts on the device, not just one. The Usage screen auto-rotates through each account (every 5s) with a row of page dots, and the middle (PWR) button steps through them manually before moving on to the Bluetooth screen.
How it works
Daemon — accounts are declared in
~/.config/claude-usage-monitor/accounts.jsonas a list of{label, config_dir}. No file = a single default account (fully backward compatible). Each account's token is resolved from its Claude Code profile:Claude Code-credentialsfor the default profile, orClaude Code-credentials-<hash>where<hash>is the first 8 hex ofsha256(<absolute config_dir>). (This is how Claude Code itself namespacesCLAUDE_CONFIG_DIRprofiles.)<config_dir>/.credentials.json.All accounts are polled concurrently each cycle and sent as a single JSON array on the RX characteristic:
{ "a": [ { "n": "main", "s": 45, "sr": 120, "w": 28, "wr": 7200, "st": "allowed", "ok": true } ] }A failed account is still included as
{ "n": "...", "ok": false }so the device shows it offline rather than dropping it. Writes use write-with-response so CoreBluetooth performs a long write for multi-account arrays up to the 512-byte device buffer.Firmware —
parse_json()reads the{"a":[...]}array into a fixedaccounts[](up toMAX_ACCOUNTS = 8, bounded by the BLE buffer), still accepting a legacy bare object. The usage screen renders one account at a time (label as the title), auto-rotates inui_tick_anim(), and draws a page-dot indicator. Offline accounts show--%/offline.Backward compatibility
With a single account, behavior is identical to before: no page dots, title stays "Usage", no auto-rotation, the same Usage↔Bluetooth toggle, and the legacy single-object payload still parses (a missing
okdefaults totrue).Testing
Verified end-to-end on a Waveshare AMOLED-2.16 with two real accounts: the daemon polls both concurrently and the device auto-rotates between them with correct per-account numbers and page dots; manual button cycling works; a transient poll failure correctly rendered the account offline. Both the daemon and firmware diffs were reviewed by a second model (Codex) and its findings applied (first-payload rotate-timer reset; legacy
ok-missing compatibility). Docs +daemon/accounts.example.jsonincluded.🤖 Generated with Claude Code