Portuguese / Português do Brasil
Local token and limit dashboard for an ESP32 2.8-inch TFT touch display. The firmware shows local Codex usage and Claude Code status by polling a small HTTP bridge running on your computer.
The ESP32 does not call OpenAI, Anthropic, Codex, or Claude APIs directly. Personal Codex and Claude Code accounts do not expose a complete public per-user usage API, so this project reads local data sources:
- Codex: token counts from
~/.codex/state_5.sqlite. - Claude Code: local session history plus the Claude Code
statusLineoutput written toserver/claude_status.json. - ESP32: Wi-Fi client that polls the bridge endpoints and renders the values with LVGL.
- ESP32 firmware for the ESP32-2432S028R / CYD-style 240x320 TFT board.
- Local Python bridge with
/api/codex,/api/claude,/api/summary, and/api/health. - Codex local SQLite usage reader.
- Claude Code local JSON/JSONL history reader.
- Claude Code
statusLineinstaller for live 5-hour / 7-day status when available. - Configurable refresh interval, local bridge URLs, and manual token limits.
- Touch refresh button on the display.
Recommended hardware:
- ESP32-2432S028R / CYD board, or equivalent.
- 2.8-inch 240x320 TFT display with ILI9341 driver.
- XPT2046 resistive touch controller.
- ESP32 module with Wi-Fi.
- USB cable for power, flashing, and serial monitor.
- Windows PC running the local bridge.
- Optional USB serial driver such as CH340, depending on your board.
Board purchase link used for this build:
Confirm the seller listing, pinout, display driver, and touch controller before buying, because similar ESP32 display boards can use different wiring.
Optional printed support:
Check the MakerWorld model page for print files, fit, license, attribution, and remix rules before printing or sharing modified parts.
- PlatformIO
- Arduino framework for ESP32
- LVGL 8.4
- TFT_eSPI
- ArduinoJson
- Python 3 standard library HTTP server
- PowerShell script for Claude Code
statusLine
.
+- src/
| +- main.cpp # ESP32 firmware, LVGL UI, Wi-Fi, HTTP polling
| +- logo_assets.c # Generated LVGL image assets
+- imgs/
| +- project-logo.png # AI-generated project logo
| +- codex.png
| +- claudecode.png
+- include/
| +- config.h # Defaults and local override hook
| +- config.example.h
| +- lv_conf.h
+- scripts/
| +- load_env.py # Injects .env values into PlatformIO build flags
| +- install_claude_statusline.py
| +- claude_statusline.ps1
| +- generate_logo_assets.py
+- server/
| +- run_server.py # Local HTTP bridge
| +- config.example.json
+- platformio.ini
+- README.md
+- README.pt-BR.md
Local files intentionally ignored by Git:
.env
.pio/
.vscode/
server/config.json
server/claude_status.json
include/config.local.h
__pycache__/
*.pyc
- Windows with PowerShell.
- Python 3.
- PlatformIO CLI or the PlatformIO VS Code extension.
- Claude Code installed and authenticated.
- Codex used on the same machine, so
~/.codex/state_5.sqliteexists. - ESP32-2432S028R / CYD-style board or compatible wiring.
Quick checks:
py -3 --version
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe --versionIf PlatformIO is not installed, install it through the VS Code extension or with Python:
py -3 -m pip install platformioInstall Claude Code:
irm https://claude.ai/install.ps1 | iexIf the installer says C:\Users\YOUR_USER\.local\bin is not in your PATH, add it:
$claudeBin = Join-Path $env:USERPROFILE ".local\bin"
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (($userPath -split ";") -notcontains $claudeBin) {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$claudeBin", "User")
}
$env:Path += ";$claudeBin"Test it:
claude --version
claudeDo not run Claude Code with --bare or --safe-mode for this project, because those modes can ignore customizations such as statusLine.
From the project root:
cd C:\Users\YOUR_USER\Documents\codes\codex-claude-project
py -3 scripts\install_claude_statusline.pyThe installer updates:
C:\Users\YOUR_USER\.claude\settings.json
It installs a command similar to:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "...\scripts\claude_statusline.ps1"Open Claude Code normally and send any message. Then verify that the local status file exists:
Test-Path .\server\claude_status.json
Get-Content .\server\claude_status.jsonIf it does not exist, restart the terminal, run claude, send a simple message, and run the installer again if needed.
Create a local .env file in the project root. Do not commit it.
Example:
WIFI_SSID=your-network
WIFI_PASSWORD=your-password
CODEX_BRIDGE_URL=http://192.168.1.50:8787/api/codex
CLAUDE_BRIDGE_URL=http://192.168.1.50:8787/api/claude
USE_CODEX=1
USE_CLAUDE=1
USAGE_WINDOW_DAYS=7
REFRESH_INTERVAL_MS=5000
CODEX_TOKEN_LIMIT=0
CLAUDE_TOKEN_LIMIT=0
DEVICE_NAME=TokenMeterUse the IPv4 address of the computer running the bridge. Do not use localhost in ESP32 firmware, because localhost would point to the ESP32 itself.
Find your PC IP:
ipconfigThe .env file is loaded by scripts/load_env.py during the PlatformIO build. Rebuild and upload the firmware after changing .env.
Start the bridge:
py -3 server\run_server.pyIt listens on:
http://0.0.0.0:8787
Endpoints:
http://localhost:8787/api/codex
http://localhost:8787/api/claude
http://localhost:8787/api/summary
http://localhost:8787/api/health
Test locally:
irm http://localhost:8787/api/codex
irm http://localhost:8787/api/claude
irm http://localhost:8787/api/summaryTest the IP used by the ESP32:
irm http://192.168.1.50:8787/api/summaryIf localhost works but the LAN IP does not, check Windows Firewall and confirm that the ESP32 and PC are on the same network.
Run the bridge in the background:
Start-Process -WindowStyle Hidden -FilePath py -ArgumentList @("-3", "server\run_server.py") -WorkingDirectory (Get-Location)Stop duplicate bridge processes:
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -match "run_server.py" } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }By default the server reads:
C:\Users\YOUR_USER\.codex\state_5.sqlite
Expected healthy response:
irm http://localhost:8787/api/codex{
"id": "codex",
"available": true,
"status": "ok",
"tokens_used": 123456
}If Codex has never been used on that machine, the endpoint can return no local data.
Claude Code writes live status data to:
server/claude_status.json
The bridge also scans Claude local history under:
C:\Users\YOUR_USER\.claude\projects
C:\Users\YOUR_USER\.claude\sessions
Expected healthy response:
irm http://localhost:8787/api/claude{
"id": "claude",
"available": true,
"status": "5h",
"display_value": "2%",
"percent_used": 2,
"limit_label": "5h reset 06:10"
}If the response says available: false or sem dados, generate a new Claude Code response and check server/claude_status.json.
Copy the example config only if you need manual paths or manual values:
Copy-Item server\config.example.json server\config.jsonExample manual Claude status:
{
"bind": "0.0.0.0",
"port": 8787,
"providers": {
"claude": {
"enabled": true,
"percent_used": 62,
"display_value": "62%",
"limit_label": "5h reset 18:00",
"status": "manual"
}
},
"paths": {
"codex_state_db": null,
"claude_status_json": null
}
}server/config.json is ignored by Git.
Build:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe run -e esp32-2432S028RList serial ports:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe device listUpload:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe run -e esp32-2432S028R -t upload --upload-port COM4Serial monitor:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe device monitor -b 115200 --port COM4If upload fails with a boot mode error:
- Hold
BOOT. - Start the upload command.
- If it stays at
Connecting..., press and releaseEN/RSTonce while holdingBOOT. - Release
BOOTwhen writing starts.
Use a full erase when the display shows leftovers from old firmware or the board behaves strangely:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe run -e esp32-2432S028R -t erase --upload-port COM4
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe run -e esp32-2432S028R -t upload --upload-port COM4Current TFT configuration in platformio.ini:
-D ILI9341_DRIVER=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=320
-D TFT_MISO=12
-D TFT_MOSI=13
-D TFT_SCLK=14
-D TFT_CS=15
-D TFT_DC=2
-D TFT_RST=-1
-D TFT_RGB_ORDER=TFT_RGB
-D TFT_BL=21
-D TFT_BACKLIGHT_ON=HIGH
-D SPI_FREQUENCY=20000000
-D SPI_READ_FREQUENCY=16000000
-D SPI_TOUCH_FREQUENCY=2500000Touch defaults in include/config.h:
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_MOSI 32
#define TOUCH_MISO 39
#define TOUCH_CLK 25For a different touch calibration, create include/config.local.h:
#pragma once
#define TOUCH_SWAP_XY 1
#define TOUCH_INVERT_X 1
#define TOUCH_INVERT_Y 0
#define TOUCH_MIN_X 250
#define TOUCH_MAX_X 3800
#define TOUCH_MIN_Y 250
#define TOUCH_MAX_Y 3800irm http://YOUR_PC_IP:8787/api/summaryCheck:
- The IP in
.envis the PC LAN IP. - The ESP32 and PC are on the same Wi-Fi/network.
- Windows Firewall allows port
8787. - The bridge is running.
- The firmware was rebuilt after changing
.env.
Test-Path .\server\claude_status.json
Get-Content .\server\claude_status.json
irm http://localhost:8787/api/claudeThen open Claude Code normally and send a message. Avoid --bare and --safe-mode.
Test-Path $env:USERPROFILE\.codex\state_5.sqlite
irm http://localhost:8787/api/codexUse Codex on this machine to generate local history.
The project currently uses:
-D TFT_RGB_ORDER=TFT_RGBIf red and blue are inverted on your board, try:
-D TFT_RGB_ORDER=TFT_BGRThen rebuild and upload again.
Before publishing:
git init
git add .
git statusMake sure these files are not staged:
.env
.pio/
.vscode/
server/config.json
server/claude_status.json
include/config.local.h
Then commit and push:
git commit -m "Initial ESP32 token meter"
git branch -M main
git remote add origin https://github.com/YOUR_USER/YOUR_REPO.git
git push -u origin main- This project does not use OpenAI Platform Usage/Costs API, Codex Enterprise Analytics, or Anthropic Admin API.
- Token and percentage values are local approximations based on files available on your machine.
- Claude Code
statusLinevalues are updated only when Claude Code executes the status line command.
