ESP32CodexClaude CodePlatformIOProjetos30 de junho de 2026

Como criar um TokenMeter com ESP32 para Codex e Claude Code

Um passo a passo completo para montar um dashboard local de tokens em ESP32, com bridge Python, PlatformIO, Codex e Claude Code no Linux, macOS e Windows.

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/summary e /api/health.
  • Fontes locais de uso: Codex é lido de ~/.codex/state_5.sqlite; Claude Code é lido do histórico local e, quando configurado, de server/claude_status.json gerado pelo statusLine.

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:

  1. Segure BOOT.
  2. Inicie o upload.
  3. Se continuar em Connecting..., pressione e solte EN ou RST mantendo BOOT.
  4. Solte BOOT quando 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:

  1. codex funciona e ~/.codex/state_5.sqlite existe.
  2. claude funciona e o statusLine gera server/claude_status.json.
  3. python3 server/run_server.py ou py -3 server\run_server.py sobe a bridge.
  4. http://localhost:8787/api/summary responde.
  5. http://IP_DO_COMPUTADOR:8787/api/summary responde.
  6. .env usa o IP do computador, nao localhost.
  7. O firmware foi recompilado depois do .env.
  8. O upload foi feito na porta serial correta.
  9. 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.

Referencias