ESP32 desk dashboard that shows Claude Code usage
ESP32 desk dashboard that shows Claude Code usage
A small ESP32 dashboard I made for my desk to keep an eye on Claude Code usage.
It runs on a Waveshare ESP32-S3-Touch-AMOLED-2.16 as well as a few other alternative boards and pairs over Bluetooth, the splash screen plays pixel-art Clawd animations that get busier when your usage rate climbs. The two side buttons send Space and Shift+Tab over BLE HID for Claude Code's voice mode and mode-toggle shortcuts.
The device boots into the splash. Tap the screen anywhere to switch to the Usage view; tap again to flip back to the splash.
| Splash | Usage |
|---|---|
| Splash; touch-toggle anytime | Session and weekly utilization |
While the splash is up, the middle (PWR) button cycles animations. Hold the power button for 3 seconds, then release, to put the device into pairing mode — this clears the saved Bluetooth bond and re-advertises. The firmware also auto-rotates animations every 20 s within the current usage-rate group, so a long stretch on the splash isn't just one Clawd on loop.
Boards supported out of the box:
Please check if a pull request exists for your alternative hardware port before opening a new one, providing QA feedback and testing on the same hardware is more valuable than duplicate pull requests.
Porting to another board: the firmware is a thin HAL with per-board folders under firmware/src/boards/. Drop in a new folder and a new PlatformIO env — main.cpp, ui.cpp, and splash.cpp never need to change. See docs/porting/adding-a-board.md for the walk-through and docs/porting/hal-contract.md for the interfaces a port must implement.
curl, bluetoothctl, busctl (BlueZ Bluetooth stack)python3 (the installer sets up a venv with bleak and httpx)python3 3.11+ (the installer sets up a venv with bleak, httpx, and pystray)The macOS host pieces — Python daemon, LaunchAgent, and flash helper — were ported by Chris Davidson (@lorddavidson). Thanks Chris!
./flash-mac.sh waveshare_amoled_216 # ESP32-S3 2.16" (auto-detects /dev/cu.usbmodem*)
./flash-mac.sh waveshare_amoled_216_c6 # ESP32-C6 2.16" variant
./flash-mac.sh waveshare_amoled_18 /dev/cu.usbmodem1101 # ESP32-S3 1.8" (or pass an explicit USB serial port)The board env name is required. Run ./flash-mac.sh with no args to see the available envs (scraped from firmware/platformio.ini).
After flashing, open System Settings → Bluetooth and click Connect next to "Clawdmeter". The daemon only ever connects to the peripheral this Mac is paired/connected to — it never scans for a nearby device — so once it's connected here the daemon picks it up on its next poll (~60 s).
The daemon reads your Claude OAuth token from the macOS Keychain (service Claude Code-credentials), polls usage every 60 s, and pushes it to the display over BLE.
./install-mac.shThe installer creates a Python venv in daemon/.venv/, installs bleak and httpx, renders a LaunchAgent into ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist, and loads it. The first run is launched interactively so macOS prompts for Bluetooth permission.
Useful commands:
launchctl list | grep claude-usage # check it's running
tail -F ~/Library/Logs/claude-usage-daemon.out.log # live logs
launchctl unload ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist # stop
launchctl load -w ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist # start./flash.sh waveshare_amoled_216 # ESP32-S3 2.16" (defaults to /dev/ttyACM0)
./flash.sh waveshare_amoled_216_c6 # ESP32-C6 2.16" variant
./flash.sh waveshare_amoled_18 /dev/ttyACM1 # ESP32-S3 1.8" (or pass an explicit USB serial port)The board env name is required. Run ./flash.sh with no args to see the available envs (scraped from firmware/platformio.ini).
After flashing, the device advertises as "Clawdmeter". Pair it once:
# Scan for the device
bluetoothctl scan le
# When "Clawdmeter" appears, pair and trust it
bluetoothctl pair F4:12:FA:C0:8F:E5 # use your device's MAC
bluetoothctl trust F4:12:FA:C0:8F:E5To re-pair later, hold the power button for 3 seconds then release — the device clears its saved bond and re-advertises.
The daemon polls your Claude usage every 60 seconds and sends it to the display over BLE.
./install.sh
systemctl --user start claude-usage-daemonCheck status: systemctl --user status claude-usage-daemon
View logs: journalctl --user -u claude-usage-daemon -f
Runs natively on Windows — no WSL required. A system-tray app polls your usage and pushes it over BLE, and starts automatically at login.
claude login completed. The token is read from %USERPROFILE%\.claude\.credentials.json (falling back to %LOCALAPPDATA%\Claude\ then %APPDATA%\Claude\).%USERPROFILE%\Clawdmeter), not a \\wsl$ share — the installer refuses a WSL path.pio run -d firmware -e waveshare_amoled_216 -t upload --upload-port COM5 # use your device's COM portRun pio run -d firmware with no env to see the available board envs.
The device is a bonded BLE HID keyboard, so pair it once: Settings → Bluetooth & devices → Add device → Bluetooth, then select "Clawdmeter". Pairing is required — it enables the physical buttons and keeps a persistent connection (the device keeps showing your last-synced usage even after the daemon quits). To undo, use Remove device (this disables the buttons).
From the repo root in PowerShell:
powershell -ExecutionPolicy Bypass -File install-windows.ps1This creates a venv, installs bleak/httpx/pystray/Pillow from the in-repo requirements (no internet downloads), registers a per-user login-autostart entry (HKCU\…\Run, no admin needed), and launches the tray app headlessly (no console window).
python -m venv .venv
.venv\Scripts\Activate.ps1 # if blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then retry
pip install -r daemon\requirements-windows.txt
python daemon\claude_usage_daemon_windows.py # runs in the foreground; Ctrl+C to stopThe icon's corner bubble shows state — green Connected, amber Scanning, red Error — and hovering shows the status (Connected · last update HH:MM). A notification fires once when it enters Error (e.g. an expired token). Right-click for the menu:
Get-Content $env:LOCALAPPDATA\Clawdmeter\daemon.log -Tail 30 # view logs
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v Clawdmeter /f # remove autostart| Symptom | Fix |
|---|---|
Device not found |
Power on the device; make sure it's in range and paired. |
token expired toast / API HTTP 401 |
Re-run claude login, then restart the daemon. |
Connection failed |
Toggle Windows Bluetooth off/on in Settings. |
Warning: running under Linux/WSL |
Run from a native PowerShell window, not a WSL shell. |
Claude Code-credentials) on macOS, or from ~/.claude/.credentials.json on Linux (%USERPROFILE%\.claude\.credentials.json on Windows).api.anthropic.com/v1/messages — one token of Haiku, basically free.anthropic-ratelimit-unified-5h-utilization and friends).The board has three side buttons. Left and right send HID keys; the middle (PWR) button cycles splash animations and, held for 3 seconds, triggers pairing mode.
| Button | GPIO | Function |
|---|---|---|
| Left | GPIO 0 | Hold to send Space (Claude Code voice-mode push-to-talk) |
| Middle (PWR) | AXP2101 PKEY | On splash: cycle animations. Hold 3s + release: pairing mode |
| Right | GPIO 18 | Press to send Shift+Tab (Claude Code mode toggle) |
Space and Shift+Tab go out as standard BLE HID keyboard reports, so they trigger in whatever window has focus on the paired host — not just Claude Code.
The device advertises a custom GATT service alongside the standard HID keyboard service:
| UUID | |
|---|---|
| Data Service | 4c41555a-4465-7669-6365-000000000001 |
| RX Characteristic (write) | 4c41555a-4465-7669-6365-000000000002 |
| TX Characteristic (notify) | 4c41555a-4465-7669-6365-000000000003 |
| HID Service | 00001812-0000-1000-8000-00805f9b34fb |
JSON payload format (written to RX):
{ "s": 45, "sr": 120, "w": 28, "wr": 7200, "st": "allowed", "ok": true }Fields: s = session %, sr = session reset (minutes), w = weekly %, wr = weekly reset (
No open issues yet, or sync has not completed.