Skip to content

Feature: display multiple Claude accounts with auto-rotation#52

Open
soapiece wants to merge 4 commits into
HermannBjorgvin:mainfrom
soapiece:feature/multi-account
Open

Feature: display multiple Claude accounts with auto-rotation#52
soapiece wants to merge 4 commits into
HermannBjorgvin:mainfrom
soapiece:feature/multi-account

Conversation

@soapiece

Copy link
Copy Markdown
Contributor

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.

Note: this is stacked on #51 (the macOS BLE fix). Until that merges, the diff here also includes its commit; it'll collapse to just the multi-account commits once #51 is merged.

How it works

Daemon — accounts are declared in ~/.config/claude-usage-monitor/accounts.json as 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:

  • macOS: the per-profile Keychain item — Claude Code-credentials for the default profile, or Claude Code-credentials-<hash> where <hash> is the first 8 hex of sha256(<absolute config_dir>). (This is how Claude Code itself namespaces CLAUDE_CONFIG_DIR profiles.)
  • Linux: <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.

Firmwareparse_json() reads the {"a":[...]} array into a fixed accounts[] (up to MAX_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 in ui_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 ok defaults to true).

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.json included.

🤖 Generated with Claude Code

soapiece and others added 4 commits May 30, 2026 21:32
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>
@HermannBjorgvin

Copy link
Copy Markdown
Owner

Getting an Enterprise account at work soon so I'll look into this sorry I haven't gotten around to your PR yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants