Skip to content

Latest commit

 

History

History
131 lines (100 loc) · 4.82 KB

File metadata and controls

131 lines (100 loc) · 4.82 KB

CodexMeter Notes

ESP32-S3 desk display + iOS app/widget for Codex/OpenAI usage tracking. Adapted from Clawdmeter.

Project Layers

Layer Tech Entrypoint
Firmware C++ (Arduino), PlatformIO firmware/atom_main.cpp
Daemon Python 3.10+, bleak/pyserial/zeroconf daemon/codex-usage-daemon.py
iOS App Swift, XcodeGen ios/CodexMeterApp/project.yml

Development Commands

Firmware:

pio run -d firmware -e m5stack_atoms3           # build full firmware
pio run -d firmware -e m5stack_atoms3_smoke     # build minimal smoke test (RGB cycle + device name)
./flash.sh [port] [env]                         # build + upload (default: /dev/cu.usbmodem101, m5stack_atoms3)

Daemon:

pip install -r requirements.txt
./daemon/codex-usage-daemon.py --print                                           # smoke test
./daemon/codex-usage-daemon.py --transport serial --once                          # serial to AtomS3
./daemon/codex-usage-daemon.py --transport ble                                   # BLE to AtomS3
./daemon/codex-usage-daemon.py --http-port 9595                                  # HTTP server for iOS app
CODEXMETER_PROVIDER=claude ./daemon/codex-usage-daemon.py --print                # Claude mode

Testing:

ruff check .              # Python lint
pytest -q                 # Python tests (tests/test_ble_device_selection.py, tests/test_claude_local_usage.py)
# iOS: open in Xcode and run CodexMeterApp scheme, or `xcodegen generate && xcodebuild test`

iOS:

cd ios/CodexMeterApp
xcodegen generate         # regenerate .xcodeproj from project.yml
# then open in Xcode or use xcodebuild

Install as service:

./install.sh              # macOS LaunchAgent or Linux systemd user service

Health check:

./doctor.sh               # Python version, venv, deps, PlatformIO, USB port, Codex auth, API key, service status

Config

~/.config/codexmeter/env — full env var reference in admin.md. Key vars:

  • OPENAI_ADMIN_KEY or OPENAI_API_KEY (for OpenAI costs API)
  • CODEXMETER_PROVIDER=claude (switch to Claude Code activity tracking)
  • CODEXMETER_TRANSPORT=serial|ble|none
  • CODEXMETER_HTTP_PORT (default 9595 for iOS app)

Daemon Data Sources

Priority: Codex OAuth (~/.codex/auth.jsonchatgpt.com/backend-api/wham/usage) > OpenAI costs API > local activity (~/.codex/sessions/*.jsonl or ~/.claude/projects/**/*.jsonl).

Firmware

  • Two PlatformIO environments: m5stack_atoms3 (full, src: atom_main.cpp + ble.cpp) and m5stack_atoms3_smoke (minimal, src: smoke.cpp)
  • Key libs: M5Unified, ArduinoJson, NimBLE-Arduino
  • 5 screens: Usage, Connection, Status Pet, Pet Selector, Now Working
  • Button: short-press cycles screens, hold (2s) selects pet or clears BLE bonds
  • 512-byte device buffer, prefers serial over BLE
  • Serial: 115200 baud, \n terminated
  • Upload mode: hold button while plugging in until green LED

iOS App

  • XcodeGen project (project.yml), generate with xcodegen generate
  • Targets: CodexMeterApp (main), CodexMeterAppWidget (WidgetKit), CodexMeterAppTests
  • Bonjour/mDNS discovery of daemon via MDNSBrowser.swift
  • BLE connection to ESP32 via BLEManager.swift (CoreBluetooth)
  • App Groups: group.com.codexmeter (shared UserDefaults with widget)
  • Personal Team free accounts: app group container not provisioned → UserDefaults.sharedGroup falls back to .standard, widget won't sync

BLE

  • Device name: Codex Controller
  • Service: 434f4445-584d-4554-4552-000000000001
    • RX (write): ...0002
    • TX (read/notify): ...0003
    • Refresh (notify): ...0004
  • Host writes compact JSON to RX:
{"s":45,"sr":120,"w":28,"wr":7200,"st":"$2.31 today","ok":true}
  • Daemon caches BLE addresses; can stale if device re-pairs. Fallback: delete ~/.config/codexmeter/ble_address.txt

CI

  • python-ci.yml: ruff + pytest on ubuntu
  • ios-ci.yml: xcodegen generate → simulator tests → widget build (macos-15)
  • Both trigger on push/PR to main

Known Fixes

Xcode 16.4 / Swift 6: @MainActor on protocol conformances

extension BLEManager: @MainActor CBCentralManagerDelegate { }  // ERROR

Fix: Remove @MainActor from extension line. Class is already @MainActor, conformances inherit implicitly.

Personal Team: App Groups container unavailable

UserDefaults(suiteName: "group.com.codexmeter") returns nil. Fix:

extension UserDefaults {
    static let sharedGroup = UserDefaults(suiteName: "group.com.codexmeter") ?? .standard
}

Resources

  • admin.md — operator runbook (setup, flash, daemon, config, troubleshooting)
  • systm.md — internal architecture (device state, screens, payload, BLE/serial protocol)
  • docs/E2E_TESTING.md — daemon HTTP endpoint testing, mDNS verification
  • TODO.md — pending improvements