Neon Meter accepts the same compact provider payload over USB serial and BLE. USB serial is preferred by the host app when a CoreS3 is connected by cable; BLE remains the wireless fallback.
USB uses the CoreS3 CDC serial interface at 115200 baud. Hosts must set DTR
high so ESP32-S3 USB CDC delivers serial traffic, and should keep RTS low to
avoid toggling the boot/reset line. Frames are UTF-8 JSON objects delimited by
\n. The firmware may also print diagnostic text on the same serial stream, so
hosts must ignore non-JSON lines and unknown JSON frames.
The host probes a serial port by writing:
{"type":"hello","protocol":"neon-meter-usb","version":1}The firmware responds:
{"type":"hello","protocol":"neon-meter-usb","version":1,"device":"Neon Meter","firmwareVersion":"1.0.8","chipFamily":"ESP32-S3","capabilities":["ble-repair"]}After a successful handshake, the host sends a heartbeat every 5 seconds:
{"type":"ping","protocol":"neon-meter-usb","version":1}The firmware does not answer ping. Any valid inbound USB protocol frame keeps
the USB app connection active; if no such frame arrives for more than 15
seconds, the device clears the USB connected state and returns to its normal BLE
or waiting status.
The host writes the provider bundle inside a typed payload frame:
{"type":"payload","payload":{"rotationSeconds":30,"providers":[]}}For compatibility, the firmware also accepts a raw provider bundle or raw single-provider object as one newline-delimited JSON line.
The firmware sends these control frames:
| Frame | Meaning |
|---|---|
{"type":"ack","ack":true} |
Payload accepted. |
{"type":"err","err":true} |
Payload rejected or failed to parse. |
{"type":"refresh-requested"} |
Host should send a fresh provider bundle. |
Firmware that lists ble-repair in the hello capabilities array accepts this
request over USB:
{"type":"ble-repair"}Before making any Bluetooth changes, the firmware acknowledges the request:
{"type":"ble-repair-accepted","ok":true}It then clears stored peer bonds, generates and persists a new random-static BLE identity, and restarts. The new identity makes the meter discoverable as a fresh peripheral when the computer still retains an obsolete pairing record. Hosts must treat this command as optional for compatibility with older firmware.
The firmware advertises as Neon Meter and exposes a custom BLE GATT service:
| Item | UUID |
|---|---|
| Data Service | 41494d45-7465-7220-0000-000000000001 |
| RX write characteristic | 41494d45-7465-7220-0000-000000000002 |
| TX notify/read characteristic | 41494d45-7465-7220-0000-000000000003 |
| Refresh notify characteristic | 41494d45-7465-7220-0000-000000000004 |
| Firmware metadata read characteristic | 41494d45-7465-7220-0000-000000000005 |
The UUIDs are stable for host compatibility.
The host writes a compact UTF-8 JSON payload to RX. The device notifies TX with
{"ack":true} or {"err":true}. When the host subscribes to the refresh
characteristic and the device has no data yet, the device sends a one-byte
notification to request a fresh payload.
Newer firmware also exposes a read-only metadata characteristic:
{"firmwareVersion":"1.0.8","chipFamily":"ESP32-S3"}Hosts should treat this metadata as optional so older firmware remains connectable.
Current Neon Meter hosts send one bundle with every detected provider. If the
bundle contains one provider, the device keeps showing that provider. If it
contains two, the device rotates between cached provider screens using
rotationSeconds, which defaults to 30.
{
"rotationSeconds": 30,
"providers": [
{
"p": "claude",
"title": "Claude Code",
"se": true,
"s": 29,
"sl": "Session",
"sr": 142,
"w": 4,
"wl": "Weekly",
"wr": 9730,
"st": "allowed",
"detail": "5h 29% / 7d 4%",
"ok": true
},
{
"p": "chatgpt",
"title": "ChatGPT",
"se": true,
"s": 55,
"sl": "Session",
"sr": 180,
"w": 40,
"wl": "Weekly",
"wr": 10080,
"st": "ok",
"detail": "5h 55% / 7d 40%",
"ok": true
}
]
}Single compact provider objects remain supported for compatibility.
| Field | Type | Meaning |
|---|---|---|
p |
string | Provider key, such as claude, chatgpt, or host. Defaults to claude for Clawdmeter compatibility. |
title |
string | Optional UI title. Defaults from provider. |
se |
boolean | Whether a real Session window is available. Missing means true; false hides Session everywhere. |
s |
number | Primary/current usage percent, clamped to 0-100. |
sl |
string | Primary/current panel label. Defaults to Current. |
sr |
number | Minutes until primary/current window reset. 0 means the reset is due, and -1 means unknown. |
w |
number | Secondary/weekly/monthly usage percent, clamped to 0-100. |
wl |
string | Secondary panel label. Defaults to Weekly. |
wr |
number | Minutes until secondary window reset. 0 means the reset is due, and -1 means unknown. |
st |
string | Provider status, such as ok, allowed, limited, or error. |
detail |
string | Optional short status/spend note. |
ok |
boolean | Whether the host-side fetch succeeded. |
The s and w fields remain consumed usage percentages for host compatibility.
The firmware displays them as remaining gauge capacity: 100 - s and 100 - w.
When Codex has switched to Luna Reserve, the host sends the active reserve pool
as a single-window payload. It uses title: "Luna-Reserve", se: false, and
the reserve pool's consumed percentage and reset time in w, wl, and wr.
The firmware hides the ordinary Session panel and displays the reserve pool as
the remaining-capacity gauge. This does not treat the reserve pool as an
ordinary weekly limit.
Claude-compatible:
{"s":29,"sr":142,"w":4,"wr":9730,"st":"allowed","ok":true}ChatGPT/Codex:
{"p":"chatgpt","title":"ChatGPT","se":true,"s":55,"sl":"Session","sr":180,"w":40,"wl":"Weekly","wr":10080,"st":"ok","detail":"5h 55% / 7d 40%","ok":true}ChatGPT/Codex with no Session window:
{"p":"chatgpt","title":"ChatGPT","se":false,"s":0,"sl":"Session","sr":-1,"w":52,"wl":"Weekly","wr":7942,"st":"ok","detail":"7d 52%","ok":true}The host classifies exact 18000-second Session and 604800-second Weekly
windows by duration rather than primary_window or secondary_window
position. Updated firmware hides the complete Session panel while se is
false, moves Weekly into the upper panel slot, and restores both panels when a
Session window returns.