English | 简体中文
Put your remaining Codex allowance on your desk—clear at a glance and always visible.
CodexMeter is a host-and-device project that puts Codex / Agent usage on a dedicated desk display. The current release has one physically validated reference target: Waveshare ESP32-S3-Touch-AMOLED-2.16. The Windows host reads quota snapshots from the Codex runtime bundled with the ChatGPT desktop app, then delivers the same local cache to the device over Wi-Fi or BLE.
Quick start · Troubleshooting · Hardware support · Resource footprint · Display and controls · Transport and security · Development and testing · Roadmap
Important
This project displays Codex / Agent usage. It is not a unified usage API for every regular ChatGPT Plus or Pro chat model. This is a community project, not an official OpenAI client.
Physical Waveshare reference device over BLE · background distraction gently retouched · the screen uses the simulator's synthetic 73% / 6d 3h fixture, so it exposes no account usage and implies no subscription requirement
| Capability | What it provides | |
|---|---|---|
| 📊 | At-a-glance usage | A ring shows the main window's remaining allowance; the details page adds secondary model allowances and link status |
| 📡 | Dual transport | Wi-Fi with mDNS + HMAC-authenticated HTTP, or BLE GATT push from Windows through Bleak |
| 🔒 | Credentials stay local | OpenAI login data, OAuth tokens, and browser cookies are never stored in firmware or sent to the device |
| 🧭 | Reference-board integration | The current Waveshare target uses QMI8658 rotation and motion wake plus AXP2101 fuel-gauge data |
| 🛟 | Recoverable | NVS provisioning, cached-state display, reconnect logic, and a five-second long-press recovery path in BLE-only mode |
| 🧪 | Shared UI source | The SDL simulator and current physical target compile the same LVGL UI source—preview first, then flash |
SDL simulator render with synthetic data · the displayed plan label is illustrative, not a subscription requirement · BLE snapshots label both pages BLE
flowchart LR
A["ChatGPT sign-in state"] --> B["Codex app-server<br/>account/rateLimits/read"]
B --> C["Windows host service<br/>60-second upstream read + local cache"]
C -->|"Wi-Fi · mDNS + HMAC HTTP"| D["Current reference device<br/>Waveshare ESP32-S3 · 480 × 480 AMOLED"]
C -->|"BLE · Bleak + GATT"| D
Only the app-server poll on the Windows host reaches the Codex runtime. After a successful read, the default delay is 60 seconds plus 0–5 seconds of random jitter. The ESP32's five-second Wi-Fi pull and the BLE helper's two-second check read only the host cache; neither creates another OpenAI query. Consecutive failures back off for 2 / 4 / 8 / 15 minutes. OpenAI has not published a safe cadence for periodic polling; see Polling cadence and rate-limit risk for the complete boundary.
| Area | Current status |
|---|---|
| Host | Windows 11; ChatGPT desktop app signed in to an account with Codex / Agent usage; Node.js 20+ |
| Reference hardware | Waveshare ESP32-S3-Touch-AMOLED-2.16; 480 × 480 CO5300 AMOLED |
| Firmware | ESP-IDF 5.5.4 and 5.5.5; Waveshare BSP 2.0.1; LVGL 9 |
| Transport | Wi-Fi HTTP HMAC v2 and BLE GATT v1 both validated end to end on the same physical board |
| Peripherals | QMI8658 automatic rotation and motion wake, AXP2101 fuel gauge, battery and charging status |
| Target | Status | Validation scope |
|---|---|---|
| Waveshare ESP32-S3-Touch-AMOLED-2.16 | Supported reference target | The only target with physical validation for display, primary touch/page navigation, QMI8658 motion wake, AXP2101, Wi-Fi, and BLE; complete four-orientation touch validation remains pending |
| SDL simulator | UI-development target only | Uses the production LVGL UI; does not validate radio, display driver, touch controller, IMU, PMIC, or battery hardware |
| Other boards or hardware products | Not currently supported | A port must add and validate its BSP, display, touch, power, sensor, provisioning, and transport integration before this matrix marks it supported |
The protocol and UI boundaries are designed to make future hardware ports practical, but they are not a compatibility claim. A target becomes supported only after its build, protocol behavior, UI output, and physical-device validation are documented here.
Show full hardware-validation notes
account/rateLimits/readreturned real Codex quota data.- Physical flashing was verified. Display startup uses a 10-line (9,600-byte) single buffer in internal RAM, avoiding the BSP's 50-line double-buffer path in PSRAM.
- Repeated unplug/replug and rapid page-switch testing kept both overview and detail screens complete while network updates continued, with no SPI/LCD/DMA errors observed.
- The panel stays black during initialization, waits for the first complete frame, and invalidates the whole display again during the first second. This prevents a quick power cycle from showing only LVGL's partially updated regions.
- The QMI8658 orientation algorithm passes host tests and ESP-IDF builds; complete four-orientation touch validation on physical hardware remains pending.
- The tested BLE-only board wakes on QMI8658 motion, stays bright during continued
movement, consumes a touch-only wake gesture, changes page on the first touch after
motion wake, keeps both pages labeled
BLE, and transitionsLIVE→CACHED→LIVEwhen the Windows service stops and reconnects. - The AXP2101 is read directly for fuel-gauge percentage, battery voltage, charging, and external-power state. Percentage is not estimated with a linear voltage curve.
- NVS provisioning, dynamic mDNS, HTTP HMAC v2, and BLE GATT v1 have completed discovery, authentication, push, and ACK testing. BLE reconnects after a hard reset.
This public repository starts from a privacy-reviewed source snapshot. Future iterations are recorded with commits, releases, and tags rather than duplicated version folders.
The following numbers are measured reference points, not minimum requirements or hard limits. Windows process memory varies with the OS, Node/Python/Codex versions, Bluetooth driver, and uptime. Firmware image and linker figures are reproducible build-time data; they are not runtime free-heap measurements.
A BLE-only reference run collected 31 one-second samples over about 30.7 seconds on a Windows 11 host. The window included a Codex refresh, snapshot update, and BLE ACK. The complete background tree includes the hidden PowerShell launcher, Node daemon, Codex app-server child, Python virtual-environment launcher, and the Bleak bridge. Hardware model, processor topology, memory capacity, OS build, and measurement date are intentionally omitted because they are not required to interpret these results.
| Process | Avg. working set | Avg. private bytes | Avg. threads | Avg. handles |
|---|---|---|---|---|
| Hidden PowerShell launcher | 79.2 MiB | 65.5 MiB | 10.0 | 567 |
| Node daemon | 50.3 MiB | 62.2 MiB | 13.0 | 268 |
| Codex app-server | 43.3 MiB | 20.2 MiB | 42.1 | 350 |
| Python venv launcher | 4.1 MiB | 0.7 MiB | 1.0 | 60 |
| Python BLE bridge | 42.3 MiB | 23.0 MiB | 7.3 | 286 |
| Complete process tree | 219.1 MiB | 171.7 MiB | 73.4 | 1,532 |
Summed working set can count shared pages more than once; private bytes are private committed memory and are not the same as resident physical memory. Excluding the PowerShell wrapper, the remaining service tree measured about 139.9 MiB working set and 106.1 MiB private bytes.
All five process CPU counters stayed below Windows' 15.625 ms accounting step during the active sample window. The conservative measurement bound is therefore less than 0.255% of one core—not literal zero CPU. Across roughly the first six minutes including startup, the tree accumulated about three CPU-seconds, or about 0.85% of one core on average. Whole-machine percentages are intentionally omitted because they depend on the host's processor topology.
| Disk item | Measured size | Included in normal background operation? |
|---|---|---|
Project .runtime |
358.2 MiB | Yes: 341.6 MiB Codex runtime + 16.6 MiB BLE venv |
| Repository files | ≈2.88 MiB | Yes; includes source, docs, and reference images; excludes .git, .runtime, and build output |
firmware/build |
264.8 MiB | No; rebuildable developer output |
The external Node installation is shared and is not counted as project-only disk use. A normal successful upstream read occurs every 60–65 seconds. The BLE helper checks the loopback cache every two seconds but writes over BLE only when the snapshot generation or service state changes, normally once per upstream refresh.
The current BLE-only configuration was rebuilt from clean output with ESP-IDF 5.5.5; the clean and prior incremental builds produced the same sizes.
| Resource | Current build | Meaning |
|---|---|---|
Application .bin |
1,784,400 B / 1.702 MiB | firmware/build/codex_usage_meter.bin |
| Factory app partition | 8 MiB | 21.27% used; 6.298 MiB / 78.73% remains |
| Flash code | 1,095,604 B | Linked .text stored in Flash |
| Flash read-only data | 450,948 B | .rodata plus app descriptor |
| Linked DIRAM | 260,931 / 341,760 B | 76.35% of the shared IRAM/DRAM link pool; 80,829 B remains |
| IRAM executable sections | 213,883 B | IRAM .text plus vectors |
| Static DRAM | 47,048 B | 23,824 B .data + 23,224 B .bss |
| AMOLED draw buffer | 9,600 B / 9.375 KiB | Internal RAM, dynamically allocated at startup |
| QMI8658 task stack | 4,096 B / 4 KiB | Configured capacity, not measured peak use |
| Bootloader | 22,272 B / 21.75 KiB | 10,496 B remains in the bootloader region |
The configured 16 MiB Flash reserves 24 KiB for NVS, 4 KiB for PHY initialization, 8 MiB for the factory app, and 7 MiB for SPIFFS, leaving about 960 KiB unpartitioned. The 7 MiB SPIFFS entry is reserved capacity—not evidence that 7 MiB is currently used.
Runtime free internal heap, minimum-ever heap, free PSRAM, per-task stack high-water marks, and device CPU utilization are not currently instrumented. Reporting those accurately requires firmware telemetry; the 240 MHz CPU setting is a clock frequency, not a utilization figure. The fixed 9,600-byte draw buffer and 4,096-byte QMI8658 stack are additional runtime allocations and are not included in the 47,048-byte static DRAM line above.
To reproduce the firmware figures after activating ESP-IDF:
idf.py -C firmware fullclean
powershell -ExecutionPolicy Bypass -File .\scripts\build-firmware.ps1
idf.py -C firmware size- Windows 10 or 11
- The ChatGPT desktop app installed and signed in, with Codex opened at least once
- Node.js 20 or newer
- Native Windows Python 3.11 or newer for BLE, including
pipandvenv(do not use WSL Python) - ESP-IDF 5.5 or newer only when building firmware from source
- For the current reference firmware target: Waveshare ESP32-S3-Touch-AMOLED-2.16
- A USB data cable
- A 2.4 GHz Wi-Fi network reachable by the ESP32. The PC may use 5 GHz Wi-Fi or Ethernet as long as both devices can communicate on the same LAN.
Clone the repository and run the remaining commands from its root directory:
git clone https://github.com/waveshareteam/codex-meter.git
Set-Location .\codex-meterRecommended order: prepare and verify the host service → configure firmware → build → flash and provision. BLE-only operation does not require the ESP32 to join the home LAN, although the first pairing-token setup still opens a temporary password-protected device AP. If a step fails, begin with the ordered checks in Troubleshooting before changing firmware or erasing NVS.
Copy the same embedded Codex runtime from the installed ChatGPT application into the
project's ignored runtime directory. This does not download another Codex installation
or copy login credentials. The script accepts only known ChatGPT/Codex Appx runtimes
with a valid Authenticode signature whose signer is OpenAI; it will not implicitly run
an executable discovered through an arbitrary process, PATH, bin/, or
node_modules directory:
powershell -ExecutionPolicy Bypass -File .\scripts\prepare-windows.ps1Use -RuntimePath to select a different runtime explicitly. The OpenAI signature is
still verified before and after copying. To test a locally built, unsigned development
runtime, do not place it in .runtime; explicitly setting CODEX_EXECUTABLE means the
developer assumes responsibility for trusting that executable.
Start locally and verify the service first:
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1Open:
- Dashboard:
http://127.0.0.1:8787/ - Full data:
http://127.0.0.1:8787/api/usage - ESP32 payload:
http://127.0.0.1:8787/api/device
Once verified, stop it with Ctrl+C, then listen on the LAN:
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 -Bind 0.0.0.0If Windows Firewall prompts, allow access only on Private networks. Do not expose port 8787 to the public internet or an untrusted network.
The host script supports four modes:
-Transport |
Host behavior | Best suited for |
|---|---|---|
wifi |
Serves HTTP and publishes mDNS only when bound to a non-loopback address; this is the host script default | A stable, trusted LAN |
ble |
Runs BLE push only; HTTP is forced to loopback | No usable LAN, or no desire to open a firewall port |
both |
Keeps Wi-Fi and BLE active together | Explicit dual-link redundancy |
auto |
Currently behaves like both; the device selects the newest snapshot |
Recommended, with room for future power-saving and priority policy |
Before using BLE for the first time, create a project-specific virtual environment with native Windows Python and install the pinned Bleak dependency:
powershell -ExecutionPolicy Bypass -File .\scripts\prepare-ble-windows.ps1If several Python installations are present, choose one explicitly:
powershell -ExecutionPolicy Bypass -File .\scripts\prepare-ble-windows.ps1 `
-PythonPath "C:\Path\To\python.exe"Then start either mode:
# BLE only: port 8787 is not exposed to the LAN
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 `
-Transport ble
# Recommended: Wi-Fi + BLE; either link survives a brief failure of the other
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 `
-Bind 0.0.0.0 -Transport autoThe default app-server read interval is 60 seconds, with an additional 0–5 seconds of random jitter after every success. The host and launcher accept integer intervals from 60 seconds through 24 hours. Wi-Fi and BLE continue reading the local cache quickly, so a longer upstream interval does not slow link reconnection.
Pass -BlePython when using a custom virtual environment. While the host is running,
open http://127.0.0.1:8787/api/transports locally to inspect Wi-Fi listening, BLE
scan/connection state, and the latest ACK separately.
The BLE role split resembles Clawdmeter, but the protocol is independently implemented:
the ESP32 is the BLE peripheral/GATT server and the Windows Bleak helper is the
central/client. Advertising is used only for discovery. Quota data, pairing tokens, and
HMAC values never appear in advertising packets; quota data is written to the RX
characteristic only after a GATT connection is established. See
docs/transport.md for protocol details.
account/read exposes planType (for example, pro), but that value does not
distinguish Pro 5x from Pro 20x. account/rateLimits/read may also expose an optional
individualLimit, which OpenAI documents as an effective monthly credit limit; it is
not a 5x/20x entitlement or multiplier. The project can therefore display PRO or
PLUS automatically, but cannot reliably distinguish Pro 5x from Pro 20x. It
deliberately avoids guessing from percentages, reset times, monthly credit limits, or
usage history, because those signals can produce a label that looks precise while
being wrong.
If you have confirmed the plan through the official account page, override the device label explicitly:
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 `
-Transport auto -Bind 0.0.0.0 -PlanLabel "PRO 20X"You may also set an environment variable for the current PowerShell session. The
-PlanLabel command-line option takes precedence:
$env:METER_PLAN_LABEL = "PRO 20X"
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 `
-Transport auto -Bind 0.0.0.0Labels are limited to 20 ASCII characters, must begin and end with a letter or digit,
may contain letters, digits, spaces, +, _, and - in between, and are normalized
to uppercase before transmission. Automatic 5x/20x detection should be added only if
app-server eventually returns a dependable entitlement field.
On first launch, the host creates a stable host ID and a random pairing token in
CodexUsageMeter/host.json under the current user's local application-data directory.
These values are not written to the repository and contain no OpenAI login credentials.
The local dashboard displays both values and provides a copy-token button.
When LAN listening is enabled:
- Non-loopback access to
/api/devicerequires a random 128-bit nonce andAuthorization: Meter-HMAC <signature>. The host signs the exact response bytes as well. Normal HMAC requests and responses do not transmit the pairing token itself. - The dashboard, static assets,
/api/usage, and/api/hostaccept onlylocalhost,127.0.0.1, or[::1], and validate Host and Origin to prevent DNS rebinding. /healthzis available from the LAN, but returns only protocol version, service status, and coarse transport mode/Wi-Fi/BLE states.- mDNS advertises
_codex-meter._tcp.local. TXT records contain only protocol version, host ID, device endpoint, and authentication type; the pairing token is never advertised.
During first-time or recovery provisioning, the browser submits the token over HTTP inside the temporary WPA2-protected device AP; the firmware then stores it in NVS. Normal LAN traffic, mDNS, BLE advertising, and BLE snapshot frames do not carry the token. Treat the temporary AP password, setup page, and token as sensitive.
mDNS filters link-local, common VPN, and virtual adapters by default. If the computer has multiple physical adapters, name the intended interface before launch:
$env:METER_MDNS_INTERFACE = "Your LAN adapter name"
powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 -Bind 0.0.0.0If the host identity file is damaged, the service stops and reports an error instead of silently generating a new token and invalidating every paired device.
The current device link is intended for a trusted private LAN. Nonce + HMAC-SHA256 keeps the pairing token off the wire, rejects recently reused nonces, and lets the device verify that a response came from a host holding the key. HTTP still does not encrypt the quota JSON: a passive observer can read it, and an active intermediary can block or relay the connection. Do not expose the port on guest, hotel, or other untrusted networks; prefer the planned USB CDC fallback in those environments.
BLE reuses the same pairing token. Windows first validates the device's deviceProof
for a challenge, then the device verifies a snapshot HMAC bound to that challenge and
sequence number and reports the result through the ACK characteristic. The current
firmware does not enable BLE link-layer encryption or bonding. HMAC supplies identity
and integrity, not confidentiality, so a nearby passive observer may still see quota
JSON after connection. Do not treat BLE quota content as encrypted; see the transport
document for the complete boundary.
Normal use no longer requires compiling Wi-Fi credentials or a computer IP address
into the firmware. Public-build network settings, host ID, and pairing token are empty.
On first boot, the device shows a setup Wi-Fi network with a random password and stores
the submitted configuration in a dedicated NVS namespace. BLE only requires only the
pairing token; Wi-Fi, host ID, and manual address may remain empty. Other transport
modes still require 2.4 GHz Wi-Fi plus either a host ID or manual host address. If DHCP
changes the computer's address, the Wi-Fi link repeats mDNS discovery using the stable
host ID—no reflashing is required.
To change brightness, color thresholds, or migration defaults for a development board, open the ESP-IDF configuration UI:
idf.py -C firmware set-target esp32s3
idf.py -C firmware menuconfigRun set-target only for the initial setup or after selecting a different chip target.
Open CodexMeter:
Network and host > Usage data transport: selectAuto,Wi-Fi only,BLE only, orWi-Fi + BLE. Firmware defaults toAuto. At present,AutoandBothboth start both links and select the newest snapshot by the host'scapturedAtvalue.- The remaining fields under
Network and hostare factory/development migration defaults. Runtime NVS settings take precedence, and a manual host address is used only when mDNS discovery fails. Display and appearance: configure QMI8658 automatic rotation, motion wake, sampling/debounce thresholds, active/idle brightness, automatic dimming delay, quota color thresholds, and hysteresis. Motion wake has independent sensitivity and activity- hold settings and remains available when automatic rotation is disabled. TheBatterysubmenu controls AXP2101 sample cadence and low/critical battery thresholds.
During development, you can start the host first and use a script to enter the Wi-Fi
password privately while reading the host ID and pairing token automatically from the
local /api/host endpoint:
powershell -ExecutionPolicy Bypass -File .\scripts\configure-firmware.ps1BLE authentication also requires a host pairing token in device NVS. A brand-new blank
device temporarily opens a password-protected Wi-Fi setup portal even in BLE only
mode. In that mode, enter only the token; the Wi-Fi and host fields remain optional for
a future transport change. After saving and rebooting, the Wi-Fi data link stays off.
Development builds may use the script above to write the same migration default. The
token is never provisioned automatically through BLE advertising.
Add a manual address only when the network blocks mDNS:
powershell -ExecutionPolicy Bypass -File .\scripts\configure-firmware.ps1 `
-HostAddress 192.168.1.20These values are written to firmware/sdkconfig.local and the current sdkconfig, both
excluded by .gitignore. They are used only for the initial migration before NVS has
been established; later Kconfig changes do not overwrite configuration already saved
on the device.
Hardware-specific behavior in this section describes the current Waveshare reference target. Future ports may provide different sensors, power telemetry, and orientation adapters while preserving the shared UI behavior.
The home screen keeps only the most important information: remaining allowance,
window, reset time, and connection state. Tap to open the details page, which adds used
percentage, secondary allowances, and transport status; tap again to return. Both pages
render transport from the same snapshot source, so a BLE connection is labeled BLE
on both the overview footer and the details link card instead of reverting to a fixed
Wi-Fi label. Both pages show the battery percentage reported by the onboard AXP2101 in
the upper-right corner.
A lightning symbol means charging, USB means external power is available without a
battery, and an unavailable reading—or no valid sample for more than six sampling
periods—displays --% rather than misreporting a communication error as 0%.
After 60 seconds without touch or detected movement, brightness drops from 65% to 30% by default. The QMI8658 uses all three acceleration axes, an adaptive gravity baseline, and an 80 mg default threshold. Moving a dimmed device restores active brightness; the very next touch can change pages immediately. If touch itself wakes a device that was still, that one gesture is consumed and does not accidentally change pages. Continued movement publishes an activity heartbeat, so the display cannot dim until movement stops and a complete idle interval then elapses. Automatic dimming is considered disabled if idle brightness is not below active brightness.
Automatic rotation through the onboard QMI8658 is enabled by default. The AMOLED hardware orientation and touch coordinates change together only after acceleration has remained generally stable for about 600 ms, with up to 150 ms allowed for brief diagonal jitter. Rapid movement, a prolonged near-diagonal position, or a flat device preserves the last confirmed orientation; rotation is also deferred while the screen is held. An accelerometer physically cannot determine in-plane direction when lying flat. There is no preset confirmed direction at boot: the first stable QMI8658 result replaces the panel's initialization direction. Measurements on this board require a consistent 90° clockwise correction from QMI8658 coordinates to logical screen coordinates. The BSP's 90° and 270° commands also produced the opposite physical orientation on the tested panel, so the display adapter swaps those commands internally while gravity and touch logic continue to use the actual screen direction.
Color follows the remaining allowance:
| Remaining | Color | Label |
|---|---|---|
| 35%–100% | Green | PLENTY |
| 15%–34% | Orange | PAY ATTENTION |
| 10%–14% | Red | RUNNING LOW |
| 0%–9% | Red | ALMOST OUT |
The defaults use the 35%/15% warning levels common in existing usage-display projects,
then add a clearer urgent label at 10%. Three percent of hysteresis prevents color
flicker when rounded values move near a boundary. Every threshold is configurable in
menuconfig.
Battery status uses a separate, restrained palette: gray-white while discharging,
green while charging or full, orange at the default 11%–20%, and red at 0%–10%. Battery
thresholds are configurable under Display and appearance > Battery. Firmware enables
only battery detection and voltage ADC and, following Waveshare's official guidance,
disables the unconnected TS measurement on this board. It does not rewrite charging
current, 4.2 V termination voltage, or other charging policy.
The Node.js host has no third-party npm dependencies. Run BLE tests through the Windows virtual environment created by the preparation script:
npm test
npm run test:docs
.\.runtime\ble-venv\Scripts\python.exe -m unittest discover `
-s ble\tests -p "test_*.py"The simulator compiles the same firmware/main/meter_ui.c used on the device. It is the
fastest way to inspect the 480 × 480 layout, states, and interaction before flashing.
After preparing SDL2, CMake, and a compiler, run:
cmake -S ui-simulator -B ui-simulator/build -G Ninja `
-DCMAKE_BUILD_TYPE=Release
cmake --build ui-simulator/build
.\ui-simulator\build\codex_meter_ui_simulator.exeNumber keys select healthy, warning, critical, offline, and low-battery scenarios. Use
the arrow keys or a click to change pages. See
ui-simulator/README.md for the complete key map and
screenshot options.
Configure ESP-IDF in the current PowerShell session:
$env:IDF_PATH = "Your ESP-IDF directory"
$env:IDF_TOOLS_PATH = "The matching ESP-IDF tools directory"If the ESP-IDF installation uses a dedicated Python, also set:
$env:METER_IDF_PYTHON_PATH = "Directory containing Python"Build the firmware:
powershell -ExecutionPolicy Bypass -File .\scripts\build-firmware.ps1The first build downloads the Waveshare BSP, QMI8658, LVGL, and display drivers from the Espressif Component Registry. The resulting firmware is written to:
firmware/build/codex_usage_meter.bin
Orientation and battery-display policy can also be tested independently on the PC:
cmake -S firmware/host_tests -B firmware/host_tests/build
cmake --build firmware/host_tests/build
ctest --test-dir firmware/host_tests/build --output-on-failureConnect the USB data cable and list serial ports:
Get-CimInstance Win32_SerialPort | Select-Object DeviceID,NameFlash and open the serial monitor:
powershell -ExecutionPolicy Bypass -File .\scripts\flash-firmware.ps1 -Port COM8Replace COM8 with the actual port. Press Ctrl+] to exit the ESP-IDF serial monitor.
The new firmware startup log should contain
Using 10-line internal-RAM draw buffer; otherwise, the device is still running an
older build.
With a blank release configuration, the display shows:
- Setup network:
Codex-Meter-XXXXXX - A WPA2 password generated for this device
- Setup page:
192.168.4.1
Follow these steps:
- Copy the host ID and pairing token from the local dashboard.
- Connect a phone or PC to the displayed
Codex-Meter-XXXXXXnetwork. - Open
http://192.168.4.1/in a browser. - For
BLE only, enter only the pairing token. For other modes, enter 2.4 GHz Wi-Fi, the pairing token, and either the host ID or a manual host address. - Save. The device reboots automatically. BLE-only waits for a host holding the same token; other modes also discover the correct host through mDNS.
The setup page validates the connecting device's local AP address, Host/Origin, and an
in-page setup key. Configuration may be submitted only through the device's own
192.168.4.0/24 network. The setup AP uses a cryptographically random password stored
in NVS. Pairing tokens are never written to logs, mDNS records, or URLs.
An incorrect Wi-Fi configuration reopens the setup AP after a bounded number of failed connections. A signed 401/403 response from the host reopens it immediately. Repeated host-discovery or request failures also enter recovery provisioning. A brief outage keeps the most recent valid quota visible and marks it as cached.
Once a token exists, BLE only does not expose setup by itself. If Windows
host.json is lost, the host changes, or the token must be replaced, press and hold the
touchscreen for five seconds on either page. This physical, device-local recovery action
reopens the same WPA2- and setup-key-protected Codex-Meter-XXXXXX network. It does not
erase the old token and cannot be triggered remotely over BLE. If the old host still
maintains a BLE LIVE connection, the usage page remains visible first; stop the old
host service or disable Bluetooth to reveal the stored AP name and password.
If no serial port appears:
- Hold BOOT.
- Connect the USB data cable.
- Release BOOT.
- Check the serial ports again.
Complete manual host and hardware validation first, then install the current-user sign-in entry:
powershell -ExecutionPolicy Bypass -File .\scripts\prepare-windows.ps1
# Wi-Fi only
powershell -ExecutionPolicy Bypass -File .\scripts\install-autostart.ps1 `
-Transport wifi
# Or prepare BLE and install the recommended dual-transport entry
powershell -ExecutionPolicy Bypass -File .\scripts\prepare-ble-windows.ps1
powershell -ExecutionPolicy Bypass -File .\scripts\install-autostart.ps1 `
-Transport auto -IntervalMs 60000 -PlanLabel "PRO 20X"-PlanLabel is optional and should be supplied only after the plan multiplier has been
confirmed. -IntervalMs accepts 60000–86400000; the example uses the 60-second default.
Running the installer again updates the current user's startup entry. The BLE virtual
environment lives at .runtime/ble-venv; if the project is moved or deleted, prepare
the environment and install the startup entry again.
Uninstall with:
powershell -ExecutionPolicy Bypass -File .\scripts\install-autostart.ps1 -UninstallLogs are stored at CodexUsageMeter/host.log under the current user's local
application-data directory. At sign-in, a file larger than 2 MiB rotates to one
host.previous.log, preventing unbounded growth. Only one host instance may use the
same state directory. A duplicate exits before touching the Codex runtime, so parallel
launches cannot accidentally multiply the configured query cadence. If automatic
startup uses a custom BLE environment, pass the same full
-BlePython <path> to the installer; it verifies that the file exists first.
After discovering the host through mDNS, the ESP32 reads GET /api/device with
nonce/HMAC authentication. A complete synthetic example response after signature
verification looks like this (the data payload version remains 1):
{
"v": 1,
"status": "ok",
"capturedAt": 1785283200,
"nextPollAt": 1785283260,
"nextPollIn": 60,
"plan": "pro",
"planLabel": "PRO 20X",
"preferred": {
"id": "codex",
"name": null,
"primary": {
"used": 31,
"remaining": 69,
"windowMins": 10080,
"resetsAt": 1785813196,
"resetsIn": 529996
},
"secondary": null
},
"extras": []
}The host calculates resetsIn, so the ESP32 can show a reset countdown without NTP.
By default, the ESP32 reads the LAN cache every five seconds while the Windows host
requests a new quota from its local Codex runtime every 60 seconds. The two cadences do
not conflict. The UI does not repaint a host countdown every second, avoiding needless
AMOLED updates.
Within one connection, the BLE helper checks the local cache every two seconds and
pushes only when capturedAt, service state, or the next upstream-query time changes.
A new connection receives the current cache once. A per-second countdown does not
trigger a push, and none of these checks increase the app-server query rate. The ESP32
advertises a custom service; after connecting, Windows writes JSON equivalent to
/api/device to the GATT RX characteristic in chunks and waits for an ACK. Advertising
contains no quota data. BLE v1 UUID, chunking, and authentication conventions are
documented in docs/transport.md.
Three separate cadences are involved, and only the host app-server poll touches the local Codex runtime. The host reads immediately at startup. After success, the next read uses the configured interval—60 seconds by default, allowed range 60 seconds to 24 hours—plus 0–5 seconds of random jitter. Under the default configuration, consecutive failures back off for 2 / 4 / 8 / 15 minutes and remain capped at 15 minutes. The ESP32's five-second Wi-Fi pull and the BLE helper's two-second check only read this local cache; neither adds an OpenAI request.
account/rateLimits/read is a documented local Codex app-server RPC, not a standalone
public REST quota API for ChatGPT users. The current app-server implementation fetches
primary usage and detailed reset-credit information in parallel, so one local RPC
should not be assumed to equal exactly one backend HTTP request. OpenAI has not
published a safe periodic-polling cadence or a guarantee against rate limiting. The
OpenAI Pro usage guidance also notes that abusive automated or programmatic extraction
may trigger abuse safeguards; this project's personal quota display has no official
exception or safety guarantee.
Clawdmeter also uses a nominal 60-second cadence, but the audited Windows implementation
retries failures on a five-second tick and, on current main, makes a max_tokens: 1
model request to inspect response headers. This project uses the quota-read RPC and
backs off after every failed read. Clawdmeter's
PR #29 discussion
contains a public rate-limit report for another API-query approach, later traced to
five-second retries amplifying the problem.
No public account-ban evidence was found. That does not reveal the threshold of the
OpenAI RPC, and neither project provides an official guarantee that a cadence is safe.
Firmware separates the UI data model from the Wi-Fi/HTTP and BLE GATT implementations.
A future USB CDC transport only needs to supply the same snapshot/link callbacks; it
does not need to rewrite the UI. See docs/transport.md for the
design trade-offs.
The AXP2101 uses a separate local snapshot callback and is never overwritten by the host usage JSON. A background task samples it every five seconds by default, while the UI thread reads only the published snapshot and performs no I²C transaction inside an LVGL callback.
The preferred order is validated hardware expansion and reliability first, then richer visual data:
- Hardware port contract — isolate board-specific display, touch, power, sensor, and provisioning adapters. Keep a new port experimental until its build, protocol, UI, and physical-device evidence are recorded in the support matrix.
- USB CDC fallback — discover by VID/PID and unique chip identity, covering networks without Wi-Fi, Bluetooth blocked by policy, or congested radio conditions; never hard-code a changing COM port.
- On-device refresh request — short press asks the host for an immediate app-server refresh; long press changes pages or transport. Host-side debounce, cooldown, and in-flight coalescing prevent repeated taps from amplifying upstream traffic.
- Local tray and diagnostics — show the latest upstream refresh, Wi-Fi/BLE/USB state, recent ACK, and error reason, with rescan, copy-diagnostics, and startup controls.
- Trends and burn rate — retain sparse history only on the PC and show estimated exhaustion or a recent 24-hour trend on the detail page. Keep the home ring simple rather than filling it with charts.
- Signed OTA — accept a host-provided version manifest only when signature, board model, and rollback requirements all match; retain serial recovery.
- Multiple device identities — issue an independent key and display name per device, with a host allowlist, revocation, and last-seen time instead of one token shared forever.
Host HMAC v2 and its matching firmware must be deployed or rolled back as a pair; old-protocol firmware otherwise receives HTTP 401 responses.
codex app-serveris a documented local OpenAI interface, andaccount/rateLimits/readis part of its documented schema. This project is not an official OpenAI client and must still follow schema or behavior changes in the installed Codex version.- The current design reuses the login already stored by the ChatGPT desktop app; it does not call private ChatGPT backend routes directly.
- The app-server process launched by the merged ChatGPT app uses private stdio, so an
external program cannot attach to it.
prepare-windows.ps1copies the same installed executable, then starts a separate local process dedicated to this project. - If a ChatGPT update makes the copied runtime incompatible, run
prepare-windows.ps1 -Forceto copy the current version again.
Issues and pull requests are welcome. See CONTRIBUTING.md for full environment setup, branch conventions, and the test matrix. To make hardware reports reproducible, include the Windows version, exact board, ESP-IDF version, transport mode, and a minimized log excerpt. Before submitting, run at least the Node.js, Python BLE, or firmware host tests relevant to the change. UI changes should also be checked on both overview and details screens in the SDL simulator.
Never commit firmware/sdkconfig, pairing tokens, Wi-Fi passwords, host.json, OAuth
information, or local runtime files. Report security issues privately as described in
SECURITY.md.
Participation is governed by the Code of Conduct.
- OpenAI Codex app-server: Rate limits
- OpenAI Help: ChatGPT Pro tiers and usage boundaries
- Waveshare ESP32-S3-Touch-AMOLED-2.16 documentation
- Waveshare ESP-IDF AXP2101 example
- Official Waveshare ESP-IDF BSP
- Clawdmeter
- Codex Usage Display
- LimitBeacon default warning thresholds
- Apple Human Interface Guidelines: Color
This project uses an original interface and open-source system fonts. It does not copy Clawdmeter's proprietary fonts or character artwork.
This project is released under the MIT License. Third-party components and dependencies remain subject to their respective licenses.


