Skip to content

Repository files navigation

SmallTV PRO Clawdmeter

Rust firmware for the GeekMagic SmallTV PRO (ESP32-WROOM-32) that displays your Claude Code rate-limit usage on the built-in 240×240 ST7789 display.

Inspired by Clawdmeter. Instead of a BLE + Python daemon, the ESP32 talks to Anthropic directly over WiFi: you authorize it once from a browser, and it keeps itself signed in from then on. Reading usage costs no tokens.

Uses Claude Code's OAuth client and an undocumented usage endpoint — see Disclaimer.

Hardware

Part Details
MCU ESP32-D0WDQ6 rev1.0 (dual-core, 240 MHz, 520 KB SRAM, no PSRAM)
Display ST7789, 240×240, SPI Mode 3
Crystal 40 MHz
Device GeekMagic SmallTV PRO v02-22

Pin wiring

GPIO Function Notes
18 SPI CLK VSPI default
23 SPI MOSI VSPI default
3 SPI CS also UART0 RX — see note below
2 Display DC
4 Display RST
21 Backlight high = on

Alternative pinout: some board revisions use CLK=14, MOSI=13 (HSPI). If the display stays blank, change those two lines in src/main.rs.

GPIO3 note: GPIO3 doubles as UART0 RX (serial input from host) and SPI CS. Serial input is only possible before the display is initialized. After that, everything (including authorization) happens over WiFi.

What it shows

Media

  • 5h — the rolling session window, 0–100%, with a bar and a countdown to its reset.
  • 7d — the weekly all-model window.
  • ALLOWED / RATE LIMITED — whether any window is exhausted.
  • The title bar shows the account tier (Claude Max / Claude Pro) when the profile endpoint answers.
  • If the device is not authorized yet, the screen shows its IP address and asks you to open it in a browser.

Setup

1. Install the ESP32 Rust toolchain (once)

cargo install espup
espup install
. $HOME/export-esp.sh      # add to your shell profile

2. Install espflash

cargo install espflash

3. Build and flash

No credentials needed at build time.

cargo build --release
espflash flash target/xtensa-esp32-espidf/release/clawdmeter-rs --monitor
# or:
./flash.sh

4. First-time WiFi setup

On first boot the device has no WiFi config. Open espflash monitor (or any serial terminal at 115200 baud) before or immediately after powering the device. You will be prompted:

I (...) clawdmeter_rs: === No WiFi config. Enter credentials in serial monitor ===
I (...) clawdmeter_rs: SSID:
MyNetwork
I (...) clawdmeter_rs: Password:
mysecret

WiFi credentials are saved to NVS and survive reboots. To reconfigure, erase NVS:

espflash erase-flash   # wipes everything; reflash firmware afterwards

5. Authorize the device

Once the device is on Wi-Fi it shows its IP address on the display (and logs it to serial). Authorization is a one-time browser round-trip — after that the device renews its own tokens and never needs attention again.

  1. Open http://<device-ip>/ from a phone or laptop on the same network.
  2. Tap Authorize with Claude. This opens claude.ai, where you sign in and approve access.
  3. You get back a string that looks like <code>#<state>. Copy it.
  4. Paste it into the form on the device page and submit.

The device exchanges the code for an access + refresh token pair. Only the refresh token is written to NVS; access tokens live in RAM and are renewed automatically ~60 seconds before they expire, and again if the API answers 401. Authorization survives reboots.

The device page generates fresh PKCE secrets on every load, so use the Authorize link and the paste form from the same page load — reloading the page invalidates the previous link.

This is the only way to give the device credentials — there is no manual token upload. To re-authorize (different account, revoked access), just open the page again and repeat the four steps.

Disclaimer

This project talks to endpoints that were built for Anthropic's own Claude Code client:

  • It authenticates with Claude Code's OAuth client_id. Anthropic offers no way to register a third-party OAuth application, so there is no first-party alternative.
  • It reads usage from GET https://api.anthropic.com/api/oauth/usage, which is undocumented and carries no compatibility guarantee.

Anthropic can change, restrict, or remove either of these at any time and without notice, which would break this firmware. Check that this use fits Anthropic's terms for your account before relying on it.

Crate stack

Crate Role
esp-idf-hal SPI, GPIO, UART, delay
esp-idf-svc WiFi, HTTPS (mbedTLS), HTTP server, NVS, hardware RNG
embedded-graphics 2D drawing primitives, fonts
serde / serde_json usage + token response parsing
sha2 SHA-256 for the PKCE code challenge

No external display driver library — ST7789 is driven directly to avoid version-compatibility issues between embedded-hal 1.0 and the mipidsi/display-interface ecosystem.

Architecture

main loop (every 60s)
  └── access_token()            RAM tokens → refresh with the token in NVS
  └── api::fetch_usage()        GET /api/oauth/usage
        └── limits[]: session + weekly_all  (percent, resets_at)
  └── ui::draw_usage()          render bars + countdown to display

HTTP server (background, port 80)
  ├── GET  /                    authorization page (PKCE link + paste form)
  └── POST /oauth               exchange code#state → tokens, store refresh in NVS

Reading usage costs no quota: /api/oauth/usage is a plain read-only lookup. Earlier versions instead fired a max_tokens: 1 request at claude-haiku-4-5 just to harvest its anthropic-ratelimit-unified-* response headers — that spent tokens in order to measure them, and is gone.

The response is parsed from limits[] (kind = session / weekly_all), which is where the live counters are; the flat five_hour / seven_day objects are only used as a fallback because they come back as null on most accounts.

Troubleshooting

Symptom Fix
Display blank Try CLK=14, MOSI=13 in src/main.rs
Display colors wrong Toggle INVON in display.rs::init()
Display rotated Change MADCTL byte in display.rs::init()
TLS handshake fails Ensure CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y in sdkconfig.defaults
Serial prompt not appearing Open monitor before powering the device
Can't type in serial prompt Must open monitor before display init (GPIO3 conflict)
Authorization page unreachable Check device IP in serial logs; ensure same network
state mismatch on submit The page was reloaded after opening the authorize link — reload once, then use that page's link and form together
claude.ai says "Invalid request format" The authorize URL is malformed. It must match the reference clients exactly: 43-char state, and scope spaces encoded as + (see oauth::push_param)
Failed: token endpoint HTTP 400 The code was already used or expired; start again from the Authorize link
Display shows "Authorize" again after working The refresh token was rejected (revoked, or password changed) — authorize once more

About

Claude Code rate limit monitoring for GeekMagic SmallTV PRO

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages