
O TokenMeter é um projeto de dashboard físico para acompanhar uso local de tokens do Codex e status do Claude Code em uma placa ESP32 com tela TFT touch. A ideia é simples: a placa não conversa diretamente com OpenAI ou Anthropic. Ela consulta uma bridge HTTP local rodando no seu computador, e essa bridge lê os arquivos locais que Codex e Claude Code já gravam na máquina.
O resultado é um painel de mesa com duas linhas principais: Codex e Claude Code. Ele mostra tokens, percentuais, janela de uso, status e uma barra visual. O firmware roda na ESP32; a coleta dos dados fica no computador.
Como o projeto funciona
A arquitetura tem três partes:
- ESP32 com display: firmware em Arduino framework, LVGL, TFT_eSPI e ArduinoJson. A placa conecta no Wi-Fi e faz requisições HTTP para a bridge local.
- Bridge Python: servidor em
server/run_server.py, com endpoints/api/codex,/api/claude,/api/summarye/api/health. - Fontes locais de uso: Codex é lido de
~/.codex/state_5.sqlite; Claude Code é lido do histórico local e, quando configurado, deserver/claude_status.jsongerado pelostatusLine.
Isso é importante: o projeto não usa API pública de billing, nem OpenAI Platform Usage API, nem Anthropic Admin API. Ele mostra aproximações locais baseadas no que existe no seu computador.
O que voce precisa
Hardware recomendado:
- placa ESP32-2432S028R, tambem conhecida como CYD;
- display TFT 2.8 polegadas 240x320 com driver ILI9341;
- touch resistivo XPT2046;
- cabo USB de dados;
- computador Windows, macOS ou Linux na mesma rede Wi-Fi da ESP32.
Software:
- Git;
- Python 3;
- PlatformIO CLI ou PlatformIO no VS Code;
- Codex instalado, autenticado e usado ao menos uma vez;
- Claude Code instalado, autenticado e usado ao menos uma vez.
No Linux, voce tambem deve configurar permissao para porta serial. No macOS e no Windows, algumas placas precisam de driver USB serial, como CH340.
Baixar o projeto
Linux e macOS:
git clone https://github.com/alestanalves/TokenMeter.git
cd TokenMeter
Windows PowerShell:
git clone https://github.com/alestanalves/TokenMeter.git
cd TokenMeter
Confira a estrutura principal:
src/main.cpp
include/config.h
platformio.ini
server/run_server.py
scripts/install_claude_statusline.py
scripts/claude_statusline.ps1
Instalar o PlatformIO
Se voce usa VS Code, o caminho mais simples e instalar a extensao PlatformIO IDE. Ela ja inclui o PlatformIO Core no terminal da extensao.
Se preferir CLI global, use Python.
Linux e macOS:
python3 --version
python3 -m pip install -U platformio
pio --version
Windows PowerShell:
py -3 --version
py -3 -m pip install -U platformio
platformio --version
Se pio ou platformio nao aparecerem no terminal depois da instalacao, feche e abra o terminal. No Windows, o PlatformIO instalado pela extensao do VS Code tambem costuma ficar em:
$env:USERPROFILE\.platformio\penv\Scripts\platformio.exe
No Linux, instale tambem as regras udev do PlatformIO para permitir upload sem sudo:
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules |
sudo tee /etc/udev/rules.d/99-platformio-udev.rules >/dev/null
sudo udevadm control --reload-rules
sudo udevadm trigger
Depois desconecte e conecte a placa novamente. Em algumas distros voce tambem precisa entrar no grupo da porta serial:
sudo usermod -aG dialout "$USER"
Saia e entre novamente na sessao para o grupo valer.
Instalar e preparar o Codex
O TokenMeter espera encontrar o banco local do Codex em:
~/.codex/state_5.sqlite
Instale o Codex CLI.
Linux e macOS:
curl -fsSL https://chatgpt.com/codex/install.sh | sh
Windows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
Alternativas suportadas incluem npm e Homebrew:
npm install -g @openai/codex
brew install --cask codex
Depois autentique e use o Codex ao menos uma vez:
codex login
codex
Faca uma interacao simples em qualquer projeto. Em seguida confira se o arquivo local existe.
Linux e macOS:
test -f "$HOME/.codex/state_5.sqlite" && echo "Codex OK"
Windows PowerShell:
Test-Path $env:USERPROFILE\.codex\state_5.sqlite
Se voce usa CODEX_HOME ou outro caminho customizado, depois configure paths.codex_state_db em server/config.json.
Instalar e preparar o Claude Code
Instale o Claude Code.
Linux, macOS e WSL:
curl -fsSL https://claude.ai/install.sh | bash
Windows PowerShell:
irm https://claude.ai/install.ps1 | iex
Windows CMD:
curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd
Abra o Claude Code e autentique:
claude
Dentro do Claude Code, voce pode usar /login se precisar trocar ou renovar a sessao.
Rodar a bridge local
Na raiz do projeto, inicie o servidor Python.
Linux e macOS:
python3 server/run_server.py
Windows PowerShell:
py -3 server\run_server.py
Por padrao ele escuta em:
http://0.0.0.0:8787
Teste localmente.
Linux e macOS:
curl http://localhost:8787/api/health
curl http://localhost:8787/api/codex
curl http://localhost:8787/api/claude
curl http://localhost:8787/api/summary
Windows PowerShell:
irm http://localhost:8787/api/health
irm http://localhost:8787/api/codex
irm http://localhost:8787/api/claude
irm http://localhost:8787/api/summary
Uma resposta saudavel de Codex deve trazer available: true e algum valor em tokens_used depois que o Codex ja tiver sido usado. Para Claude, a resposta melhora depois que configuramos o statusLine.
Descobrir o IP do computador
A ESP32 precisa acessar o IP do computador na rede local. Nao use localhost no firmware, porque na ESP32 isso apontaria para a propria ESP32.
Windows PowerShell:
ipconfig
Procure o IPv4 da placa Wi-Fi, por exemplo 192.168.1.50.
macOS:
ipconfig getifaddr en0
Se voce usa cabo, teste en1 ou veja todos:
ifconfig | grep "inet "
Linux:
hostname -I
Ou:
ip -4 addr
Teste a bridge pelo IP da rede:
curl http://192.168.1.50:8787/api/summary
No Windows, teste com PowerShell:
irm http://192.168.1.50:8787/api/summary
Se localhost funciona, mas o IP da rede nao funciona, quase sempre e firewall, rede convidado, VPN ou computador e ESP32 em redes diferentes.
Configurar o statusLine do Claude no Windows
No Windows, o projeto ja inclui um instalador para o statusLine:
py -3 scripts\install_claude_statusline.py
Ele cria ou atualiza:
C:\Users\SEU_USUARIO\.claude\settings.json
Depois abra o Claude Code normalmente:
claude
Envie qualquer mensagem. Quando o Claude Code executar o statusLine, o projeto deve criar:
server\claude_status.json
Confira:
Test-Path .\server\claude_status.json
Get-Content .\server\claude_status.json
irm http://localhost:8787/api/claude
Evite rodar Claude Code com --bare ou --safe-mode neste projeto, porque esses modos podem ignorar customizacoes como statusLine.
Configurar o statusLine no Linux e macOS
O script scripts/claude_statusline.ps1 do repositório foi escrito para PowerShell no Windows. No Linux e no macOS, o caminho mais robusto e criar um script Python local para gerar o mesmo arquivo server/claude_status.json.
Crie scripts/claude_statusline_unix.py:
cat > scripts/claude_statusline_unix.py <<'PY'
#!/usr/bin/env python3
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
def to_int(value, fallback=0):
if value is None:
return fallback
try:
return int(round(float(value)))
except (TypeError, ValueError):
return fallback
def format_reset(epoch):
if epoch is None:
return None
try:
return datetime.fromtimestamp(int(epoch), timezone.utc).astimezone().strftime("%H:%M")
except (OSError, OverflowError, TypeError, ValueError):
return None
raw = sys.stdin.read()
if not raw.strip():
raise SystemExit(0)
try:
payload = json.loads(raw)
except json.JSONDecodeError:
raise SystemExit(0)
rate_limits = payload.get("rate_limits") or {}
five = rate_limits.get("five_hour") or {}
week = rate_limits.get("seven_day") or {}
context = payload.get("context_window") or {}
percent = None
display = None
label = None
reset_at = None
status = "context"
if five.get("used_percentage") is not None:
percent = to_int(five.get("used_percentage"))
reset_local = format_reset(five.get("resets_at"))
display = f"{percent}%"
label = f"5h reset {reset_local}" if reset_local else "5h"
reset_at = five.get("resets_at")
status = "5h"
elif week.get("used_percentage") is not None:
percent = to_int(week.get("used_percentage"))
reset_local = format_reset(week.get("resets_at"))
display = f"{percent}%"
label = f"7d reset {reset_local}" if reset_local else "7d"
reset_at = week.get("resets_at")
status = "7d"
else:
percent = to_int(context.get("used_percentage"))
display = f"{percent}%"
label = "contexto"
tokens = to_int(context.get("total_input_tokens")) + to_int(context.get("total_output_tokens"))
model = (payload.get("model") or {}).get("display_name")
out = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"status": status,
"display_value": display,
"percent_used": percent,
"limit_label": label,
"reset_at": reset_at,
"tokens_used": tokens,
"model": model,
"source": "claude_statusline",
}
project_root = Path(__file__).resolve().parents[1]
out_path = project_root / "server" / "claude_status.json"
out_path.write_text(json.dumps(out, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
print(f"Claude {display} {label}".strip())
PY
chmod +x scripts/claude_statusline_unix.py
Agora registre esse script no ~/.claude/settings.json:
python3 - <<'PY'
import json
import shlex
from pathlib import Path
project = Path.cwd()
settings = Path.home() / ".claude" / "settings.json"
settings.parent.mkdir(parents=True, exist_ok=True)
data = {}
if settings.exists():
data = json.loads(settings.read_text(encoding="utf-8-sig"))
script = project / "scripts" / "claude_statusline_unix.py"
data["statusLine"] = {
"type": "command",
"command": "python3 " + shlex.quote(str(script)),
"padding": 0,
}
settings.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
print(settings)
print(data["statusLine"]["command"])
PY
Abra o Claude Code na raiz do projeto, envie uma mensagem e confira o arquivo:
claude
Em outro terminal:
test -f server/claude_status.json && cat server/claude_status.json
curl http://localhost:8787/api/claude
Os campos rate_limits.five_hour e rate_limits.seven_day podem aparecer apenas depois de uma resposta do Claude Code e dependem do tipo de conta. Se nao aparecerem, o script ainda grava o percentual de contexto.
Configurar o firmware com .env
Crie um arquivo .env na raiz do projeto. Esse arquivo nao deve ir para Git.
WIFI_SSID=sua-rede
WIFI_PASSWORD=sua-senha
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=300000
CODEX_TOKEN_LIMIT=0
CLAUDE_TOKEN_LIMIT=0
DEVICE_NAME=TokenMeter
Troque 192.168.1.50 pelo IP do computador que esta rodando a bridge.
Para testes, voce pode reduzir temporariamente o intervalo:
REFRESH_INTERVAL_MS=5000
O arquivo .env e lido por scripts/load_env.py antes do build do PlatformIO. Sempre recompile e faca upload de novo depois de alterar Wi-Fi, URLs ou limites.
Build do firmware
Com a placa desconectada, rode primeiro o build:
pio run -e esp32-2432S028R
No Windows, se pio nao estiver no PATH:
& $env:USERPROFILE\.platformio\penv\Scripts\platformio.exe run -e esp32-2432S028R
O PlatformIO vai baixar a plataforma Espressif32 e as bibliotecas:
- LVGL 8.4;
- TFT_eSPI;
- ArduinoJson.
Descobrir a porta serial
Conecte a ESP32 no USB.
Todos os sistemas:
pio device list
Windows costuma usar portas como:
COM4
COM5
macOS costuma usar:
/dev/cu.usbserial-*
/dev/cu.wchusbserial*
Linux costuma usar:
/dev/ttyUSB0
/dev/ttyACM0
Se nada aparecer, troque o cabo USB, instale o driver serial da placa ou revise permissao de porta no Linux.
Fazer upload para a ESP32
Windows:
pio run -e esp32-2432S028R -t upload --upload-port COM4
macOS:
pio run -e esp32-2432S028R -t upload --upload-port /dev/cu.usbserial-XXXX
Linux:
pio run -e esp32-2432S028R -t upload --upload-port /dev/ttyUSB0
Abra o monitor serial:
pio device monitor -b 115200 --port /dev/ttyUSB0
No Windows:
pio device monitor -b 115200 --port COM4
Se o upload travar em Connecting..., use o modo boot manual:
- Segure
BOOT. - Inicie o upload.
- Se continuar em
Connecting..., pressione e solteENouRSTmantendoBOOT. - Solte
BOOTquando a escrita comecar.
Liberar a bridge no firewall
A ESP32 precisa acessar a porta 8787 no computador.
Windows:
- quando o alerta do Firewall aparecer, permita Python na rede privada;
- se necessario, crie uma regra de entrada para TCP
8787; - confirme que o Wi-Fi esta como rede privada.
Linux com UFW:
sudo ufw allow 8787/tcp
macOS:
- em Ajustes do Sistema, permita conexoes de entrada para Python ou para o terminal usado;
- se estiver usando VPN, teste com a VPN desligada.
Teste pelo IP do computador antes de culpar a ESP32:
curl http://IP_DO_COMPUTADOR:8787/api/summary
Rodar a bridge em segundo plano
Durante desenvolvimento, deixe o terminal aberto. Para uso diario, voce pode iniciar em background.
Windows PowerShell:
Start-Process -WindowStyle Hidden -FilePath py -ArgumentList @("-3", "server\run_server.py") -WorkingDirectory (Get-Location)
Parar processos duplicados:
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -match "run_server.py" } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
Linux e macOS:
nohup python3 server/run_server.py > server/tokenmeter.log 2>&1 &
Parar:
pkill -f "server/run_server.py"
Se quiser algo permanente, crie um servico do systemd no Linux ou um LaunchAgent no macOS, apontando para a pasta do projeto.
Ajustar display e touch
Os pinos do display estao em platformio.ini. A configuracao atual usa ILI9341 em 240x320:
-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
Os pinos e calibracao do touch ficam em include/config.h. Para sobrescrever sem alterar o arquivo base, crie 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 3800
Se vermelho e azul aparecerem trocados, teste trocar:
-D TFT_RGB_ORDER=TFT_RGB
por:
-D TFT_RGB_ORDER=TFT_BGR
Depois rode build e upload novamente.
Configurar limites manuais
Se voce souber seu limite de tokens, pode usar as variaveis:
CODEX_TOKEN_LIMIT=5000000
CLAUDE_TOKEN_LIMIT=5000000
Tambem e possivel copiar server/config.example.json para server/config.json e ajustar janela, labels e caminhos.
Linux e macOS:
cp server/config.example.json server/config.json
Windows PowerShell:
Copy-Item server\config.example.json server\config.json
Exemplo de caminhos customizados:
{
"paths": {
"codex_state_db": "/home/seu-usuario/.codex/state_5.sqlite",
"claude_roots": [
"/home/seu-usuario/.claude/projects",
"/home/seu-usuario/.claude/sessions"
],
"claude_status_json": "/caminho/para/TokenMeter/server/claude_status.json"
}
}
No Windows, use barras duplas em JSON:
{
"paths": {
"codex_state_db": "C:\\Users\\SeuUsuario\\.codex\\state_5.sqlite"
}
}
Checklist de teste final
Antes de considerar pronto, valide nesta ordem:
codexfunciona e~/.codex/state_5.sqliteexiste.claudefunciona e ostatusLinegeraserver/claude_status.json.python3 server/run_server.pyoupy -3 server\run_server.pysobe a bridge.http://localhost:8787/api/summaryresponde.http://IP_DO_COMPUTADOR:8787/api/summaryresponde..envusa o IP do computador, naolocalhost.- O firmware foi recompilado depois do
.env. - O upload foi feito na porta serial correta.
- A ESP32 esta na mesma rede Wi-Fi do computador.
Problemas comuns
A ESP32 mostra sem dados
Verifique primeiro a URL:
CODEX_BRIDGE_URL=http://IP_DO_COMPUTADOR:8787/api/codex
CLAUDE_BRIDGE_URL=http://IP_DO_COMPUTADOR:8787/api/claude
Depois teste do computador:
curl http://IP_DO_COMPUTADOR:8787/api/summary
Se isso nao funciona, o problema esta antes da ESP32: bridge parada, firewall, IP errado, VPN ou rede diferente.
Codex aparece como sem banco local
Confira:
ls -l "$HOME/.codex/state_5.sqlite"
Windows:
Test-Path $env:USERPROFILE\.codex\state_5.sqlite
Se nao existir, use o Codex naquela maquina. Se existir em outro caminho, configure paths.codex_state_db em server/config.json.
Claude aparece como sem dados
O statusLine so grava depois que o Claude Code processa uma interacao. Abra claude, envie uma mensagem simples e confira:
cat server/claude_status.json
curl http://localhost:8787/api/claude
No Windows:
Get-Content .\server\claude_status.json
irm http://localhost:8787/api/claude
Se o arquivo nao existir, revise ~/.claude/settings.json e confirme se o comando aponta para o script certo.
Upload falha no Linux
Instale as regras udev, reinicie a sessao e confira a porta:
pio device list
ls -l /dev/ttyUSB0
Se a porta pertence ao grupo dialout, adicione seu usuario ao grupo e entre novamente na sessao.
A tela liga, mas nao renderiza direito
Confira se a placa realmente usa ILI9341 e a mesma pinagem do platformio.ini. Placas CYD parecidas podem usar variacoes de display, touch e backlight.
O touch esta invertido
Use include/config.local.h para testar TOUCH_SWAP_XY, TOUCH_INVERT_X e TOUCH_INVERT_Y. Recompile a cada mudanca.
Cuidados de seguranca
Nao publique estes arquivos:
.env
server/config.json
server/claude_status.json
include/config.local.h
.pio/
A bridge HTTP nao tem autenticacao. Use apenas na sua rede local confiavel e nao exponha a porta 8787 para a internet.
Tambem trate ~/.codex/auth.json, quando existir, como credencial sensivel. Ele pode conter tokens de acesso.