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>
macOS Bluetooth quirks made the original BLE setup unusable from a Mac
host. Five focused changes to fix discovery, pairing, the keyboard
identification wizard, and concurrent connections, plus a couple of
cross-platform daemon/script niceties.
Firmware (ble.cpp):
* Advertise the standard HID Service UUID (0x1812) in the primary
packet. Without it, macOS Sequoia's Bluetooth Settings GUI
recognizes the device internally but silently hides it from the
"Nearby Devices" list. Service UUIDs >16-bit overflow the 31-byte
advertising packet, so the custom data-service UUID moved to the
scan response.
* Switch PnP ID from Apple's USB vendor (0x05AC + Magic Keyboard
PID 0x820A) to Espressif's BT SIG vendor (0x02E5). macOS validates
Apple-claimed HIDs against known device IDs and refuses to surface
a Connect button for spoofers.
* Add the LED output report (Num/Caps/Scroll Lock) to the HID
descriptor — macOS treats a keyboard descriptor without LEDs as
"incomplete" and triggers the Keyboard Setup Assistant repeatedly.
* Set HID country code to 33 (US ANSI) instead of 0 (Not Supported)
so macOS can identify the layout without asking the user.
* Bump CONFIG_BT_NIMBLE_MAX_CONNECTIONS to 2 and restart advertising
after each accept. macOS holds one connection for the HID keyboard
link; the daemon now gets its own slot for the data service in
parallel, instead of either side starving the other.
screenshot.sh: auto-pick /dev/cu.usbmodem101 on macOS vs /dev/ttyACM0
on Linux, fall back to PlatformIO's bundled Python if pyserial isn't
on the system Python (PEP 668 blocks `pip install` on Homebrew Python),
and pass the actual framebuffer dimensions to ffmpeg instead of
hardcoding 480x480.
daemon/claude_usage_daemon.py: log API HTTP status + response body on
4xx/5xx so silent token-expiry failures (the daemon was reporting
{"s":0,"w":0} payloads instead of surfacing a 401) are visible in the
daemon log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cherry-picks the macOS-specific pieces from PR #5: Python daemon using
bleak (CoreBluetooth backend), LaunchAgent template, flash-mac.sh, and
install-mac.sh. Token is read from the macOS Keychain ("Claude
Code-credentials" service); Linux behavior is unchanged. README split
into "macOS installation" and "Linux installation" sections with
parallel Flash / Pair / Install subsections.
Co-Authored-By: Chris Davidson <36679917+lorddavidson@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Cache invalidation: drop ~/.config/claude-usage-monitor/ble-address and remove
the dead MAC from bluez on connect failure, so swapping ESP boards no longer
pins the daemon to a stale factory MAC
- Scan robustness: pick first matching candidate (head -1), sanity-check cached
content for valid MAC format, handle multiple Claude Controllers in bluez
- 5s tick + 60s poll: inner loop wakes every 5s for fast disconnect detection
while keeping Anthropic API cadence at one minute
- Refresh-request channel: new GATT characteristic ...0004 NOTIFY, firmware
fires once in onSubscribe when has_received_data is false; daemon subscribes
via setsid'd dbus-monitor pipeline (process group cleanup avoids the
bash-wait-on-job hang we hit on disconnect)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move all communication from USB to BLE. The device now advertises as
"Claude Controller" and acts as both a BLE HID keyboard (for touch
gestures) and a GATT data server (for usage updates from the daemon).
- Add NimBLE-Arduino BLE module with custom GATT service + HID keyboard
- Add third screen (Bluetooth) with connection status, MAC, reset button
- Rewrite daemon in bash using bluetoothctl/busctl for BLE GATT writes
- Add screenshot capture via LVGL snapshot over serial
- Remove TinyUSB mode — normal pio upload works again
- Update README with BLE architecture, screenshots, gesture docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Physical desk monitor that displays Claude Code usage limits (5-hour session
and 7-day weekly utilization) on an ESP32-S3 touchscreen via USB serial.
Firmware: LVGL 9 dashboard with LovyanGFX on SC01 Plus, Anthropic brand
colors and custom fonts (Tiempos, Styrene B, DejaVu Sans Mono), Claude
spinner animation with rotating status words.
Daemon: Pure bash script that reads the Claude Code OAuth token, makes a
minimal Haiku API call, extracts usage from rate-limit response headers,
and sends JSON to the ESP32 over serial. Uses inotifywait for instant
USB plug/unplug detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>