Clawdmeter
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.
| Usage meter | Clawd animation screen |
|---|---|
![]() |
![]() |
The Clawd animations come from claudepix, @amaanbuilds's library of pixel-art Clawd sprites, check it out, it's lovely.
Screens
The device boots into the splash and stays there until you press the middle (PWR) button, which cycles between Usage and Bluetooth. Tap the screen anywhere (except the Reset zone on the Bluetooth screen) to flip back to the splash; tap again to dismiss it.
| Splash | Usage | Bluetooth |
|---|---|---|
![]() |
![]() |
![]() |
| Splash; touch-toggle anytime | Session and weekly utilization | Connection status and bond reset |
While the splash is up, the middle button cycles animations instead of screens. The firmware also auto-rotates every 20 s within the current usage-rate group, so a long stretch on the splash isn't just one Clawd on loop.
Hardware
Boards supported out of the box:
- Waveshare ESP32-S3-Touch-AMOLED-2.16
- Waveshare ESP32-C6-Touch-AMOLED-2.16
- Waveshare ESP32-S3-Touch-AMOLED-1.8
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.
Prerequisites
- Linux (tested on Ubuntu) or macOS
- PlatformIO CLI
- Linux:
curl,bluetoothctl,busctl(BlueZ Bluetooth stack) - macOS:
python3(the installer sets up a venv withbleakandhttpx) - Claude Code with an active subscription
macOS installation
The macOS host pieces — Python daemon, LaunchAgent, and flash helper — were ported by Chris Davidson (@lorddavidson). Thanks Chris!
Flash the firmware
./flash-mac.sh waveshare_amoled_216 # auto-detects /dev/cu.usbmodem*
./flash-mac.sh waveshare_amoled_18 /dev/cu.usbmodem1101 # 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).
Pair the device
After flashing, open System Settings → Bluetooth and click Connect next to "Clawdmeter". The daemon will discover it on its next scan (~30 s).
Install the daemon
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.sh
The 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
Linux installation
Flash the firmware
./flash.sh waveshare_amoled_216 # defaults to /dev/ttyACM0
./flash.sh waveshare_amoled_18 /dev/ttyACM1 # 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).
Pair the device
After flashing, the device advertises as "Claudemeter". Pair it once:
# Scan for the device
bluetoothctl scan le
# When "Claude Controller" 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:E5
The MAC address is shown on the Bluetooth screen — press the middle (PWR) button to cycle to it.
Install the daemon
The daemon polls your Claude usage every 60 seconds and sends it to the display over BLE.
./install.sh
systemctl --user start claude-usage-daemon
Check status: systemctl --user status claude-usage-daemon
View logs: journalctl --user -u claude-usage-daemon -f
How it works
- The daemon reads your Claude Code OAuth token — from the macOS Keychain (service
Claude Code-credentials) on macOS, or from~/.claude/.credentials.jsonon Linux. - It makes a minimal API call to
api.anthropic.com/v1/messages— one token of Haiku, basically free. - The usage numbers come straight out of the response headers (
anthropic-ratelimit-unified-5h-utilizationand friends). - The daemon connects to the ESP32 over BLE and writes a JSON payload to the GATT RX characteristic.
- The firmware parses it and updates the LVGL dashboard.
- The firmware also tracks the rate of change of session % over a 5-minute window and picks splash animations from the matching mood group.
- The two side buttons are independent of all of this — they send Space and Shift+Tab as BLE HID keyboard input to the paired host directly.
Physical buttons
The board has three side buttons. Left and right do the same thing on every screen; the middle button is screen-aware.
| Button | GPIO | Function |
|---|---|---|
| Left | GPIO 0 | Hold to send Space (Claude Code voice-mode push-to-talk) |
| Middle (PWR) | AXP2101 PKEY | Cycle screens (Usage ↔ Bluetooth); on splash, cycle animations |
| 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.
BLE protocol
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 (minutes), st = status, ok = success flag.
Recompiling fonts
The firmware/src/font_*.c files are pre-compiled LVGL bitmap fonts.
npm install -g lv_font_conv
Generate each one (one at a time — lv_font_conv doesn't like loop-driven invocations) with --no-compress (required for LVGL 9):
# Tiempos Text (titles, 56px)
lv_font_conv --font assets/TiemposText-400-Regular.otf -r 0x20-0x7E \
--size 56 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_tiempos_56.c --lv-include "lvgl.h"
# Styrene B (large numbers 48, panel labels 28, small text 24, minimal 20)
for size in 48 28 24 20; do
lv_font_conv --font assets/StyreneB-Regular.otf -r 0x20-0x7E \
--size $size --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_styrene_${size}.c --lv-include "lvgl.h"
done
# DejaVu Sans Mono (32px, with spinner Unicode chars)
lv_font_conv --font assets/DejaVuSansMono.ttf \
-r 0x20-0x7E,0xB7,0x2026,0x2722,0x2733,0x2736,0x273B,0x273D \
--size 32 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_mono_32.c --lv-include "lvgl.h"
Important: lv_font_conv v1.5.3 outputs LVGL 8 format. Each generated file must be patched for LVGL 9 compatibility:
- Remove
#if LVGL_VERSION_MAJOR >= 8guards aroundfont_dscand the font struct - Remove the
.cachefield fromfont_dsc - Add
.release_glyph = NULL,.kerning = 0,.static_bitmap = 0to the font struct - Add
.fallback = NULL,.user_data = NULLto the font struct
Without these patches, fonts compile but render as invisible.
CJK support
firmware/src/font_cjk_16.c covers the full CJK Unified Ideographs basic
block (U+4E00–U+9FFF, ~20k glyphs) plus ASCII, CJK punctuation, and
halfwidth/fullwidth forms. Generated from Noto Sans CJK SC
(SIL OFL 1.1) at 16px, 2bpp:
lv_font_conv --font NotoSansCJKsc-Regular.otf --size 16 --bpp 2 \
--no-compress --format lvgl --lv-include 'lvgl.h' \
-r '0x20-0x7E,0xB7,0x2014,0x2018-0x2019,0x201C-0x201D,0x2026,0x3000-0x303F,0x4E00-0x9FFF,0xFF00-0xFFEF' \
-o firmware/src/font_cjk_16.c
Then apply the four LVGL 9 patches above. Because the font has >65k of
glyph bitmap data, the build needs -DLV_FONT_FMT_TXT_LARGE=1 in
platformio.ini build flags so font descriptor offsets switch from
16-bit to 32-bit.
The CJK font is used for the Activity screen's user-prompt row and todo
content rows. The headline (28pt Styrene B) and titles stay ASCII-only
to preserve the brand font — Chinese text in those slots renders as
empty boxes. Add a font_cjk_28.c if full coverage is needed (~1MB
more flash).
Converting Lucide icons
The UI uses a small set of Lucide icons (bluetooth + battery states) converted to RGB565 / RGB565A8 C arrays for LVGL.
node tools/png_to_lvgl.js assets/icon_bluetooth_48.png icon_bluetooth_data ICON_BLUETOOTH_WIDTH ICON_BLUETOOTH_HEIGHT
Default tint is white (0xFFFFFF); Lucide PNGs ship as black-on-transparent and would render invisible against the dark UI without it. Pass --no-tint for pre-coloured artwork like the logo. Battery icons use RGB565A8 (alpha plane) so they blend cleanly over the splash; the rest are baked RGB565 over the panel colour. Paste the converter output into firmware/src/icons.h.
Splash animations
The animations come from claudepix.vercel.app,
a library of Clawd sprites. tools/scrape_claudepix.js evaluates the
site's JavaScript in a Node VM to pull out frame data and palettes, then
tools/convert_to_c.js turns everything into RGB565 C arrays and writes
firmware/src/splash_animations.h.
To re-pull (e.g. when the source library updates):
node tools/scrape_claudepix.js
node tools/convert_to_c.js
pio run -d firmware -t upload
See tools/README.md for details.
Credits
- Pixel-art Clawd animation by @amaanbuilds, sourced from claudepix.vercel.app. Frame data and palettes scraped + converted by the tooling in
tools/. - Lucide icon set (lucide.dev, MIT) for bluetooth and battery UI glyphs.
- Anthropic brand fonts (Tiempos Text, Styrene B) — see licensing warning below.
Licensing gray area warning
The software in this repository uses and adheres to the Anthropic brand guidelines and uses the same proprietary fonts that Anthropic has a license for but this software uses without permission as well as using assets from Anthropic such as the copyrighted Clawd mascot so even though the code in this repo is non-proprietary I will not license it myself under a copyleft license since this repo includes proprietary fonts and copyrighted assets. Please be aware of this if you fork or copy the code from this repo. You have been warned!





