Go back

A Claude usage dashboard on the M5Stack Basic: plan limits and API spend on your desk

The M5Stack Basic on a laptop keyboard showing the dashboard: a Claude Max account at 88% of its weekly limit, the 5-hour session at 30%, and 142M tokens today

I use Claude every day, and the question I ask most often is not about code. It is “how much of my week is left?” Claude Code’s /usage screen answers it, but only when I stop and type it. I wanted the answer on my desk, the way a clock shows the time.

The M5Stack Basic is a good fit. It is a finished device: a 2-inch 320×240 screen, three buttons, a battery and an ESP32 in a 54 mm case. No breadboard, no soldering.

This article describes the dashboard: what it shows, how the data gets from Claude to the screen, how the device connects without any setup, and the steps to build your own. The UI was designed on the M5Stack Core in Velxio before it touched the hardware, and that is the route I recommend if you want to build one. You can also run this firmware in your browser, on demo data, without the device.

The dashboard on the device: watch it on YouTube.

All the code, the firmware and the PC bridge, is on GitHub under the MIT license: velxio/m5claude. The section “The code, part by part” below walks through what each file does.

The hardware

The M5Stack Basic v2.7: an ESP32 with a 2-inch 320x240 IPS screen, three buttons, USB-C, a microSD slot and a 110 mAh battery

The board is the M5Stack Basic Core IoT Development Kit v2.7:

Nothing else is needed: the dashboard uses only what is in the case. The board has no IMU, microphone, LEDs or real-time clock, so the dashboard uses everything else: the screen and buttons, the speaker for alerts, the backlight to save battery, and the power chip to show the charge.

What the screen shows

The dashboard works with two kinds of Claude accounts, and with several of each:

For both, it shows tokens per day for the last week, split into input, output, cache reads and cache writes, and by model.

There are four views. B moves to the next one, A and C switch between accounts, holding B asks the PC for fresh data, and holding C opens the settings. This is every screen, with the animation each view plays when it opens:

An animated tour of every screen: the overview with three accounts, the limits, tokens and breakdown views, an API organization, the single-account overview, the battery charging, the settings in English and Spanish, the approval prompt and the waiting screen

Overview. One card per account. The layout adapts to how many accounts there are: one account fills the screen, two get taller cards with a row for today’s usage, and three or more become a scrolling list.

The overview with one Claude Max account: the weekly limit as a ring, the 5-hour session and per-model limits on the right, and today's tokens with a seven-day chart

The overview with two Max accounts and an API organization: weekly percentage, 5-hour and 7-day bars, reset times, and seven-day spend for the API account

Limits. The weekly limit as a ring, with the session and the per-model limits beside it. The model names come from Claude itself, so the rows follow whatever limits your plan has. Colors change at 60% (amber) and 85% (red).

The limits view: a 67% weekly ring, the 5-hour session at 42%, and Opus and Sonnet weekly limits

Tokens. Today, the last seven days, and the dollar figure, then tokens per day with a dashed line at the average. For a subscription, the dollar figure is what the same tokens would cost at API list prices. It is not a charge, but it is a useful way to see how heavily you use the plan.

The tokens view: 2.84M tokens today, 20.8M over seven days, an API value of $84.20, and a bar chart of tokens per day

The tokens view on the device, with 142M tokens today and 296M over seven days

Breakdown. The week’s tokens by type and by model. With Claude Code, cache reads are usually most of the total.

The breakdown view: a stacked bar of input, output, cache read and cache write tokens, and the share of each model

The breakdown view on the device: almost all of a week's 296M tokens are cache reads, split between Opus and Fable

The dot next to the clock shows the link: green while data is fresh, amber when it is stale, red when no PC is connected. The photos show my own account. The screenshots use demo accounts.

Battery, sound and settings

Next to the clock is the battery, drawn the way phones draw it: the level, and a lightning bolt with a fill that climbs while it charges. The IP5306 power chip reports only four steps (25, 50, 75 and 100%), so the number moves in steps of 25.

The header while charging: 75% with a lightning bolt and a green battery, next to the link dot and the clock

The speaker is used for alerts. When a session or weekly limit crosses 90% (or 80%, if you choose), the device plays a short double chime and shows which account and which limit. When a limit resets, it plays a rising chime. Approval requests from a new computer also make a sound. Button clicks are optional.

Holding C opens the settings. A and C move, B changes a value, and holding B closes the screen. The settings are saved on the device.

The settings screen: language, brightness, sounds, alert threshold, screen off on battery, dim at night, paired computers and the firmware version

The same screens in Spanish: the settings and the overview

The overview in Spanish

Making 110 mAh last

The first firmware drained the battery in well under an hour. The CPU ran at 240 MHz, the screen repainted every second, and the radio transmitted at full power with a 15–30 ms connection interval. None of that is needed to show a number that changes once a minute. On battery, the firmware now does this:

For all-day use away from the desk, the Battery Module 13.2 stacks under the Basic and adds 1500 mAh.

How it works

Data flows from Claude Code's login and transcripts and from the Admin API into the Python bridge, then over USB or Bluetooth LE to the M5Stack, which only draws what it is sent

The design rule is that the device knows nothing. It holds no Claude credentials and no Wi-Fi settings. A small program on the PC, the bridge, collects everything and sends the device a snapshot to draw. That keeps secrets off a device that sits on a desk, and it means changing computers or networks needs no reconfiguration.

Where the data comes from

The bridge is about 1,100 lines of Python with two dependencies (pyserial and bleak). It reads three sources:

  1. Plan limits, from Claude Code’s login. Each Claude Code config directory (~/.claude, ~/.claude-work, …) holds the login of one account. The bridge finds them all and uses each token to read the same limits Claude Code’s /usage screen shows. It only reads the token and never refreshes it, because refreshing would rotate the token and log Claude Code out.
  2. Tokens, from Claude Code’s transcripts. Every response Claude Code receives is written to a JSONL file under projects/, with its token usage and model. The bridge adds these up per day, removes duplicates (a resumed session copies earlier messages into a new file), and prices them with the API price list.
  3. Spend for API organizations, from the Usage and Cost Admin API. usage_report/messages gives tokens per day and model. cost_report gives the real spend. Both need an Admin API key (sk-ant-admin01-...).

Two lessons from running it against real accounts:

The snapshot

Every refresh produces one JSON line of about 600 bytes:

{"v":1,"ts":1792770195,"tz":3600,"acc":[
 {"n":"Personal","p":"Max 20x","k":"sub","ok":1,
  "s":[42,1792778235],"w":[67,1793037195],"x":[["Opus",54,1793037195],["Sonnet",31,1793037195]],
  "td":2840000,"tk":[2310000,1480000,15900000,1120000],"c":84.2,"ct":11.35,
  "d":[1900000,3400000,2600000,4100000,3300000,2670000,2840000],"m":[["Opus",61],["Sonnet",34],["Haiku",5]]}]}

s, w and x are the session, weekly and per-model limits as [percent, reset time]. d is tokens per day. ts and tz are the PC’s clock and UTC offset. The M5Stack Basic has no real-time clock and no time zone database, so the device takes both from the snapshot and counts seconds from there. That is enough for “resets in 2h 14m” and “Tue 04:00”.

Connecting without configuration

I wanted no setup at all. Typing a Wi-Fi password into a device with three buttons is unpleasant. Saving the PC’s IP address on the device breaks as soon as you change computers. So the device supports two links, and neither needs settings.

USB. Plug in the cable. The bridge looks for serial ports whose USB chip is an M5Stack’s (CH9102 or CP2104). It opens the port without toggling DTR and RTS, so the ESP32 does not reset, and waits for a greeting. The firmware sends {"hello":"m5claude",...} every two seconds until data arrives. If no greeting arrives within a few seconds, the bridge releases the port and leaves it alone for a while, so other ESP32 boards and flashing tools are not affected.

Bluetooth LE. The firmware advertises a GATT service with two characteristics: one the PC writes lines to, and one the device sends notifications on. The bridge scans for the service UUID and connects. There is no Windows pairing dialog, and nothing to choose from a list.

Since any computer in range could find the device, each new PC is approved once, on the device:

The approval prompt: "New computer, DESKTOP-STUDIO. Allow it to send usage data?" with Deny on button A and Allow on button C

The pairing exchange: hello from the device, the PC's name and stable id from the bridge, a pending answer, the on-screen prompt, then ok, a refresh request and the snapshots

The bridge keeps a random id in ~/.m5claude/pc_id. The device stores the ids of up to four approved PCs in NVS. A known PC is accepted at once, and holding A while powering on forgets them all. A device that is already connected over USB is skipped on Bluetooth, so it is not asked to approve the same PC twice.

Until the bridge is running, the device shows what to do:

The waiting screen: "Waiting for your PC. Run m5claude-bridge on your computer. It connects over USB or Bluetooth automatically."

The firmware

The firmware is an Arduino sketch built with arduino-cli on the arduino-esp32 2.0 core. It uses three libraries:

The UI code does not depend on the hardware. It draws into a LovyanGFX target and reads the three buttons. Everything specific to the board (the panel, USB, BLE, NVS) lives in the sketch.

Some details that make it look like a product rather than a demo:

Drawing without PSRAM, and the diagonal streaks

A full 320×240 frame in 16-bit color is 150 KB. The ESP32’s largest free memory block, with Bluetooth running, is smaller than that. So the UI is drawn four times per frame into a 320×60 band (38 KB). Each pass draws everything shifted up by the band’s offset, and parts that fall outside the band are clipped.

The first time this ran on the board, the screen showed diagonal streaks, and the approval prompt’s buttons appeared twice, at the top and at the bottom. The panel configuration was my first suspect. I rewrote it by hand from M5Stack’s documentation, and nothing changed. The cause was elsewhere:

With one buffer, the next band is drawn into the buffer while DMA is still sending it. With two buffers, one is drawn while the other is sent, and the CPU waits for the transfer before reusing a buffer

LovyanGFX sends a sprite to the panel with DMA, in the background. My loop started drawing the next band into the same buffer while the previous one was still being sent, so the panel received half of one band and half of the next. The fix uses two buffers in turn and waits for the previous transfer before pushing:

for (int y = 0, k = 0; y < 240; y += 60, k ^= 1) {
  LGFX_Sprite& band = bands[k];
  band.fillScreen(BG);
  draw_ui(band, /*offset_y=*/y);   // runs while the other buffer is still being sent
  lcd.waitDMA();                    // the other buffer's transfer has finished
  band.pushSprite(&lcd, 0, y);      // starts this buffer's transfer
}
lcd.waitDMA();

It costs one more 38 KB buffer, out of about 275 KB of free RAM.

The code, part by part

Download the code with git clone https://github.com/velxio/m5claude.git, or as a ZIP from the repository’s Code button. It has two halves that share nothing but the protocol: bridge/ runs on the PC, and firmware/ plus src/ run on the M5Stack.

On the M5Stack

firmware/m5claude/m5claude.ino: everything specific to the board.

src/app/app.cpp: the protocol, independent of the link.

src/model/: the data.

src/ui/: the screens.

On the PC

The bridge lives in bridge/m5claude_bridge/. Its only dependencies are pyserial and bleak. HTTPS calls use the standard library.

__main__.py: the command line and the main loop.

autostart.py registers the bridge to start at login without a window: the Run registry key with pythonw on Windows, a LaunchAgent on macOS, a systemd user unit on Linux.

config.py: the accounts.

sources/: one file per data source.

pricing.py holds API list prices per model family. It estimates the value of subscription usage.

snapshot.py: builds the snapshot.

hub.py keeps the newest snapshot and gives it to every connected device. A slow link gets only the latest one, never a backlog.

transports/: one file per link.

The repository’s README has the same tour as tables, plus troubleshooting.

Build your own

The order I recommend: design the screens in Velxio, then flash the board, then connect the bridge.

1. Run it and change it on the M5Stack Core in Velxio

Velxio simulates the M5Stack Core with its 320×240 screen, the A, B and C buttons and the battery chip. Laying out a 2-inch screen is a lot of small adjustments (a label two pixels too low, a number that doesn’t fit its card), and each one takes seconds in the browser instead of a flash cycle.

The dashboard is in Velxio’s example gallery, with the same source files as the repository:

  1. Open M5Stack Core: Claude usage dashboard (m5claude) and click the play button. No sign-up is needed. It starts on the demo accounts from this article.
  2. Click A and C to pick an account and B to step through the views. The simulator runs the firmware slower than the board, so to open the settings, hold C for a few seconds.
  3. To see your own numbers, run m5claude-bridge snapshot on your PC and paste the line it prints into the serial monitor. The first comment in the sketch has a sample line you can edit.
  4. Change the screens in ui.cpp and run again. When they look right, flash the board.

Two things differ from the board: Velxio has no Bluetooth, so that link is left out and the serial monitor is the only link, and the speaker code uses the newer ESP32 Arduino core’s calls. Each change is marked in the sketch.

To start a dashboard of your own from an empty screen, M5Stack Core: Hello LCD + Buttons is the smallest example. It uses M5Stack’s M5GFX, which is built on LovyanGFX and shares its drawing API, so code moves between the two.

2. Flash the firmware

git clone https://github.com/velxio/m5claude.git && cd m5claude
arduino-cli core install esp32:esp32
arduino-cli lib install LovyanGFX NimBLE-Arduino ArduinoJson

./firmware/build.sh /dev/ttyUSB0      # Linux / macOS
.\firmware\build.ps1 -Upload          # Windows, finds the COM port by itself

The scripts build for the M5Stack Core with a partition scheme that leaves room for a large app (PartitionScheme=no_ota). The firmware is about 820 KB. Two settings caught me out:

3. Run the bridge

pip install -e bridge                # from the repository root; installs pyserial and bleak
m5claude-bridge accounts             # which accounts it found
m5claude-bridge autostart on         # start now, and at every login

autostart on registers the bridge to start when you log in, with no window. On Windows it adds a value under the user’s Run registry key that launches pythonw. On macOS it installs a LaunchAgent, and on Linux a systemd user service. autostart status shows whether it is running and the last lines of its log, in ~/.m5claude/bridge.log. Only one bridge runs at a time: a second one would compete for the USB port and the Bluetooth connection, so it exits with a message. Running m5claude-bridge on its own keeps it in the foreground, with the log in the terminal.

Pro and Max accounts are found from Claude Code’s config directories. To add a second subscription, log it in under its own directory:

$env:CLAUDE_CONFIG_DIR = "$HOME\.claude-work"; claude    # then /login

API organizations need an Admin API key, from the Claude Console under Settings → Admin keys:

m5claude-bridge add-api "Acme API" --key-env ACME_ADMIN_KEY

4. Connect

Plug in the USB cable, or leave the device on the desk and let Bluetooth find it. The first time over Bluetooth, press C on the device to approve the PC. Data appears within a few seconds.

Caveats

What’s next

The code is at github.com/velxio/m5claude. Issues and pull requests are welcome.

Thanks to M5Stack for a board that is ready to use out of the box, and to the authors of LovyanGFX, NimBLE-Arduino, ArduinoJson and bleak.

Links to the M5Stack store on this page are affiliate links: if you buy through them, Velxio earns a small commission at no extra cost to you, and it helps keep the simulator free.


Share this post on:
David Montero

Written by

David Montero

Creator of Velxio, the open-source circuit and Arduino simulator.

GitHub velxio.dev

Related posts


Previous Post
Cute Desktop Robot: a face that listens on the ESP-VoCat, with its emulator in your browser
Next Post
Raspberry Pi 4 and 5 in the browser: basic mode and Linux mode, step by step