This firmware runs on a YD-ESP32-S3 and shows the Sub2API account's
server-defined today_actual_cost, today_tokens, and tpm on a 128x32
SSD1306 OLED.
Network, authentication, SNTP, and polling work runs in a FreeRTOS worker so the
display loop remains responsive.
Use a 3.3 V SSD1306 I2C OLED at address 0x3C.
| OLED | YD-ESP32-S3 |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SDA | GPIO5 |
| SCL | GPIO6 |
The I2C bus runs at 100 kHz. At startup, and every two seconds while the OLED is
absent, Serial reports the I2C addresses found by the scan or
I2C display missing.
Create the ignored local configuration from config.example.yaml:
wifi:
ssid: "replace-with-wifi-ssid"
password: "replace-with-wifi-password"
service:
login_endpoint: "https://sub2api.example.com/api/v1/auth/login"
refresh_endpoint: "https://sub2api.example.com/api/v1/auth/refresh"
usage_endpoint: "https://sub2api.example.com/api/v1/usage/dashboard/stats"
username: "replace-with-login-email"
password: "replace-with-login-password"
polling:
interval_seconds: 60
request_timeout_ms: 10000Replace the sub2api.example.com placeholder with your Sub2API host and save the
file as config.local.yaml. All endpoints must be absolute HTTPS URLs, the usage
endpoint must have the exact path shown above with no query string, the polling
interval must be at least 2 seconds, and the timeout must be from 1,000 through
60,000 milliseconds. service.username is sent in the login request's email
field.
YAML is validated only during the build. The firmware contains no runtime YAML parser, and a missing or invalid field stops the build without printing secret values.
The Wi-Fi and service credentials are compiled into the firmware. Rotating
access and refresh tokens are stored together in ESP32 NVS. The generated build
artifacts and a board in another person's possession can therefore expose
credentials or tokens unless ESP32 flash encryption is separately configured
and enabled. Keep config.local.yaml, .pio/, firmware images, and physical
boards protected.
HTTPS uses hostname and certificate-chain verification with the committed DigiCert Global Root G2 certificate. The transport does not use insecure TLS. If the service changes to a chain outside that root, update the trust anchor and firmware.
Install the pinned tools and run the automated checks with the nonsecret example configuration:
rtk uv sync
APP_CONFIG_FILE=config.example.yaml rtk uv run pytest -q
APP_CONFIG_FILE=config.example.yaml rtk uv run pio test -e nativeBuild and upload with the ignored real configuration, then open Serial at 115200 baud:
make build
make upload
make monitorThe Makefile defaults to config.local.yaml, the yd_esp32s3 environment,
/dev/cu.usbmodem21201, and 115200 baud. Override any value on the command
line when needed:
make upload PORT=/dev/cu.usbmodemXXXX CONFIG=config.other.yamlThe YD-ESP32-S3 native USB JTAG/Serial interface is built with USB CDC enabled, so the same port is used for upload and monitor. Replace the example port if the host assigns a different device name. DTR and RTS are held inactive while monitoring so opening the terminal does not repeatedly reset the native USB interface.
Serial output is deliberately limited to boot/I2C status and a nonsecret worker-start failure category. It does not print configuration, endpoint URLs, request bodies, authorization headers, credentials, or tokens.
After a successful fetch, the OLED shows the cost in a large font on the left.
The right side shows daily tokens on the first row and TPM on the second, both
right-aligned in the small font and without labels. Daily tokens use truncated
integer K, M, or B notation. TPM uses truncated integer K or M
notation. A one-pixel vertical line is centered in the unused pixels between
the cost and right column; no space characters are used for layout.
If the large cost and right column do not fit together, only the cost is shown. The stale marker is drawn independently and does not change usage content coordinates.
Before the first reading, the OLED shows the active WIFI, TIME, AUTH, or
FETCH phase. A failure is reduced to a nonsecret category:
| Display | Meaning |
|---|---|
WIFI ERR |
Wi-Fi connection was lost |
TIME ERR |
wall-clock synchronization failed |
AUTH ERR |
the service rejected authentication |
TLS ERR |
certificate or TLS setup failed |
NET ERR |
DNS, timeout, or other network transport failure |
PARSE ERR |
the response could not be parsed |
API ERR |
the API returned an unsuccessful response envelope |
SIZE ERR |
the response exceeded the bounded buffer |
HTTP nnn |
HTTP or rate-limit response with status nnn |
HTTP ERR |
HTTP failure without a usable status |
After the first successful fetch, the display always retains the last cost and token reading; a 2x2-pixel white dot in the upper-right marks it stale until another fetch succeeds. Wi-Fi reconnect, authentication, HTTP, TLS, and parsing work never blocks the Arduino display loop.
Transient failures use exponential retry delays from 5 seconds up to 5 minutes.
A rate limit honors a valid Retry-After value. A usage 401 gets one bounded
refresh/login recovery path and one usage retry; repeated failures enter
backoff. Successful fetches clear the stale marker, reset backoff, and resume
the configured polling interval.
The API server's configured timezone defines "today". The firmware sends no
timezone query parameter and does not recalculate daily totals locally.

