This file is the operator guide for building, flashing, running, and debugging CodexMeter in the real environment.
CodexMeter is a small desk display for Codex/OpenAI usage. It has two runtime parts:
- AtomS3 firmware in
firmware/, built with PlatformIO form5stack_atoms3. - A host daemon in
daemon/codex-usage-daemon.pythat reads usage/activity from the local machine and pushes compact JSON to the AtomS3.
The supported hardware target is the M5Stack AtomS3 ESP32-S3. USB serial is the normal transport. BLE is still implemented and useful as a fallback/debug path, but serial is the primary desk-prototype path.
Create the Python environment and install the daemon plus PlatformIO tooling:
python3 -m venv .venv
.venv/bin/python -m pip install platformio -r requirements.txtIf a global PlatformIO is already installed, the helper scripts may use it. On
this machine pio may also be available from ~/.local/bin/pio or
~/.platformio/penv/bin/pio.
.venv/bin/platformio run -d firmware -e m5stack_atoms3Equivalent when pio is on PATH:
pio run -d firmware -e m5stack_atoms3The firmware build includes:
firmware/src/atom_main.cppfirmware/src/ble.cpp- M5Unified, ArduinoJson, and NimBLE-Arduino
There is also a smoke-test firmware environment:
.venv/bin/platformio run -d firmware -e m5stack_atoms3_smokeFind the current serial port:
~/.platformio/penv/bin/pio device list
ls /dev/cu.usbmodem* /dev/ttyACM* /dev/ttyUSB* 2>/dev/nullCommon macOS AtomS3 ports:
/dev/cu.usbmodem101
/dev/cu.usbmodem1101
/dev/cu.usbmodemDC5475CBBC601
Flash with the helper:
./flash.sh /dev/cu.usbmodem1101 m5stack_atoms3The helper changes into firmware/ and runs:
pio run -e m5stack_atoms3 -t upload --upload-port <port>After a successful flash the board may reset and re-enumerate under a different
/dev/cu.usbmodem... path. That is normal.
If flashing fails with:
Failed to connect to ESP32-S3: No serial data received
put the AtomS3 into upload mode:
- Hold the AtomS3 side button.
- Keep holding until the internal green LED lights.
- Release the button.
- Re-run
pio device listorls /dev/cu.usbmodem*. - Flash again with the new port.
If the screen is dark after flashing, unplug and replug the AtomS3 without holding the side button.
Print the payload without touching the device:
.venv/bin/python ./daemon/codex-usage-daemon.py --printSend one serial update:
.venv/bin/python ./daemon/codex-usage-daemon.py --transport serial --onceRun continuously:
.venv/bin/python ./daemon/codex-usage-daemon.py --transport serialPin a port when auto-detection picks the wrong device:
.venv/bin/python ./daemon/codex-usage-daemon.py \
--transport serial \
--serial-port /dev/cu.usbmodemDC5475CBBC601Run over BLE instead of serial:
.venv/bin/python ./daemon/codex-usage-daemon.py --transport bleThe daemon loads optional environment values from:
~/.config/codexmeter/env
Example:
CODEXMETER_TRANSPORT=serial
CODEXMETER_POLL_INTERVAL=60
CODEXMETER_ACTIVITY_POLL_INTERVAL=2
CODEXMETER_SERIAL_PORT=/dev/cu.usbmodemDC5475CBBC601
CODEXMETER_DAILY_BUDGET_USD=10
CODEXMETER_WEEKLY_BUDGET_USD=50
CODEXMETER_LOCAL_DAILY_SESSIONS=12
CODEXMETER_LOCAL_WEEKLY_SESSIONS=60
# OPENAI_ADMIN_KEY=sk-admin-...For normal Codex usage display, the preferred source is the logged-in Codex auth file:
~/.codex/auth.json
An OpenAI admin/API key is optional and only used as the organization-cost fallback. Do not commit real keys.
./install.shOn macOS this writes:
~/Library/LaunchAgents/com.justin.codexmeter.plist
~/.config/codexmeter/env
Logs:
~/Library/Logs/codexmeter.log
~/Library/Logs/codexmeter.err.log
Restart on macOS:
launchctl unload ~/Library/LaunchAgents/com.justin.codexmeter.plist
launchctl load ~/Library/LaunchAgents/com.justin.codexmeter.plistOn Linux the installer writes a user systemd service:
systemctl --user status codex-usage-daemon
systemctl --user restart codex-usage-daemon
journalctl --user -u codex-usage-daemon -fShort-press the AtomS3 button to cycle screens:
- Usage: current window and weekly/secondary window remaining percentage.
- Connection: BLE advertising/connected state, device name, and BLE MAC.
- Status pet: animated pet plus rotating local status messages.
- Pet selector: selected pet preview.
- Now Working: current project, Codex task, current action, and last completed action.
Hold behavior:
- On the pet selector screen, hold to select the next pet.
- On the other screens, hold to clear BLE bonds.
Pet selection is persisted in ESP32 preferences under the codexmeter
namespace.
Build health:
.venv/bin/platformio run -d firmware -e m5stack_atoms3Daemon dependency health:
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python ./daemon/codex-usage-daemon.py --printSerial send health:
.venv/bin/python ./daemon/codex-usage-daemon.py --transport serial --onceExpected firmware boot serial output includes:
{"ready":true,"target":"AtomS3"}
M5.begin done
BLE init done
AtomS3 dashboard ready
Use a USB-C data cable, not a charge-only cable. Try a different physical port
and run pio device list again.
Opening USB CDC can reset ESP32-S3 sketches. The daemon waits five seconds after opening serial before it sends a payload for this reason. If the port name changes, rerun port discovery and use the new path.
Check the logs for the source failure. The daemon tries:
- Codex/ChatGPT OAuth usage from
~/.codex/auth.json. - OpenAI organization costs from
OPENAI_ADMIN_KEYorOPENAI_API_KEY. - Local Codex activity from
~/.codex/session_index.jsonl.
Fallback payloads have ok:false, no live percentages, and a status like
3d/12w sessions.
Use the device hold action on Usage, Connection, or Status to clear BLE bonds. The daemon also caches a BLE address at:
~/.config/codexmeter/ble-address
Remove that file if the host keeps trying a stale address.
The firmware expects one compact JSON object per line. Keep the payload under the 512-byte receive buffer used by serial and BLE. The daemon already compacts text fields before sending.