Skip to content

Repository files navigation

📟 CodexMeter

English | 简体中文

Put your remaining Codex allowance on your desk—clear at a glance and always visible.

Host Reference hardware CI ESP-IDF Transport License: MIT

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.

📸 Reference hardware

Physical CodexMeter reference device showing the BLE-connected overview screen with a softly blurred keyboard background

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

✨ Highlights

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

🖥️ UI preview

Overview screen with a 73% allowance ring, battery level, and live connection status    Details screen with seven-day and Codex Spark allowance cards plus Wi-Fi status

SDL simulator render with synthetic data · the displayed plan label is illustrative, not a subscription requirement · BLE snapshots label both pages BLE

🧩 How it works

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
Loading

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.

✅ Compatibility and validation

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

🧩 Hardware support

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/read returned 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 transitions LIVECACHEDLIVE when 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.

📏 Resource footprint

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.

🖥️ Windows host service

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.

📟 ESP32-S3 firmware

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

🚀 Quick start

📋 Prerequisites

  • 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 pip and venv (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.

📥 Get the source

Clone the repository and run the remaining commands from its root directory:

git clone https://github.com/waveshareteam/codex-meter.git
Set-Location .\codex-meter

Recommended order: prepare and verify the host serviceconfigure firmwarebuildflash 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.

🖥️ Host service

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.ps1

Use -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.ps1

Open:

  • 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.0

If Windows Firewall prompts, allow access only on Private networks. Do not expose port 8787 to the public internet or an untrusted network.

📡 Choose Wi-Fi, BLE, or both

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.ps1

If 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 auto

The 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.

🏷️ Subscription labels and Pro 5x/20x

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.0

Labels 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.

🔐 Host identity and transport security

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/device requires a random 128-bit nonce and Authorization: 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/host accept only localhost, 127.0.0.1, or [::1], and validate Host and Origin to prevent DNS rebinding.
  • /healthz is 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.0

If 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.

⚙️ Firmware configuration

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 menuconfig

Run set-target only for the initial setup or after selecting a different chip target.

Open CodexMeter:

  • Network and host > Usage data transport: select Auto, Wi-Fi only, BLE only, or Wi-Fi + BLE. Firmware defaults to Auto. At present, Auto and Both both start both links and select the newest snapshot by the host's capturedAt value.
  • The remaining fields under Network and host are 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. The Battery submenu 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.ps1

BLE 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.20

These 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.

🎛️ Display and controls

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.

🧪 Development and testing

🖥️ Host and BLE

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"

🖼️ SDL UI simulator

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.exe

Number 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.

🔧 ESP-IDF firmware

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.ps1

The 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-failure

🔥 Flashing and first-time provisioning

Connect the USB data cable and list serial ports:

Get-CimInstance Win32_SerialPort | Select-Object DeviceID,Name

Flash and open the serial monitor:

powershell -ExecutionPolicy Bypass -File .\scripts\flash-firmware.ps1 -Port COM8

Replace 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.

📶 First-boot provisioning

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:

  1. Copy the host ID and pairing token from the local dashboard.
  2. Connect a phone or PC to the displayed Codex-Meter-XXXXXX network.
  3. Open http://192.168.4.1/ in a browser.
  4. 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.
  5. 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:

  1. Hold BOOT.
  2. Connect the USB data cable.
  3. Release BOOT.
  4. Check the serial ports again.

🔄 Start automatically after sign-in

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 -Uninstall

Logs 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.

🧾 Data protocol

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.

⏱️ Polling cadence and rate-limit risk

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.

🗺️ Roadmap

The preferred order is validated hardware expansion and reliability first, then richer visual data:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Signed OTA — accept a host-provided version manifest only when signature, board model, and rollback requirements all match; retain serial recovery.
  7. 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.

⚠️ Interface stability and boundaries

  • codex app-server is a documented local OpenAI interface, and account/rateLimits/read is 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.ps1 copies 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 -Force to copy the current version again.

🤝 Contributing

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.

📚 References

This project uses an original interface and open-source system fonts. It does not copy Clawdmeter's proprietary fonts or character artwork.

📄 License

This project is released under the MIT License. Third-party components and dependencies remain subject to their respective licenses.

About

A dedicated desk display for Codex usage — Windows host, BLE/Wi-Fi transport, and an ESP32-S3 reference implementation.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages