OTA firmware store
This directory (mounted into the container at /app/firmware) is managed by
the hub at runtime:
firmware.bin– the latest image, available viaGET /ota/firmware.manifest.json– version / size / sha256 / url of the stored image.- Immutable images referenced by manifest URLs keep an already confirmed OTA download from changing when another image is uploaded.
ota_state.jsonandpcb_state.jsonare legacy single-device files. A global auto-update setting is NOT inherited by newly registered devices.
Upload a new image from the dashboard (Firmware / OTA panel) or via
POST /ota/upload. Nothing here needs to be committed; the folder is created
on first start if missing.
Multiple devices
Select a registered device in the dashboard before using its graphs, logs, calibration, PCB controls, commands or OTA menu. Uploading an image only changes the shared available image; it does not send an update command. The manual update button confirms the selected device's name, ID, version and image digest. Auto-update, if explicitly enabled, applies only to that device on reconnect. An OTA request being sent is not proof of successful installation: check the version reported after the device reconnects.
Device records, configuration and token hashes persist separately in
/app/state/devices.sqlite3 through the ./state volume. Processed graph buffers
are memory-only and start empty after a restart. Raw logs are stored under
data_logs/<device_id>/; old mixed logs at the root are not reassigned to a
device. Do not delete these volumes to rebuild the image.
Raw logs default to 30 days of retention and a 50 MiB daily cap per device;
LOG_RETENTION_DAYS and LOG_DAILY_MAX_BYTES can override these bounds. When
logging is degraded, live telemetry continues and the device status reports
log_degraded and log_error_count. Docker's operational logs rotate separately.
Provisioning And Connection Contract
The ESP32 firmware now accepts a logical PCB_ID build flag in
platformio_microtracking/platformio.ini. The hub representation is esp32-
plus exactly 12 lowercase hex digits, padded with zeroes:
-DPCB_ID=0x01 -> esp32-000000000001
-DPCB_ID=0x02 -> esp32-000000000002
PCB_ID must be unique, nonzero and at most 48 bits. It is independent of
PCB_VERSION, which selects the hardware pins. Default profiles use ID 0x01 for
the V1 environment and 0x02 for V2; change the ID as needed even when both boards
have the same hardware revision. A factory MAC can still be used as the 48-bit
value to preserve an existing registration. IDs are public, not passwords.
Register the exact IDs that will be compiled into the two boards:
docker compose exec tracker-hub python -m app.devices add --device-id esp32-000000000001 --alias "Tracker 1"
docker compose exec tracker-hub python -m app.devices add --device-id esp32-000000000002 --alias "Tracker 2"
docker compose exec tracker-hub python -m app.devices list
The CLI writes the generated token once to
/app/state/provision/<device_id>.v1.token, not stdout. Provision real secrets
inside the Linux container where file mode 0600 is enforced. Windows file
modes do not establish an equivalent private ACL. Configure the ESP32 through a
trusted local channel; remove the delivery file after provisioning. Configure
platformio_microtracking/include/cloud_credentials.h using the structure in
cloud_credentials.example.h: one CLOUD_DEVICE_TOKEN for each PCB_ID.
The private header is ignored by Git; never publish it or place actual tokens
in platformio.ini, command arguments, browser JavaScript, logs or URL queries.
Compilation fails if the ID is invalid or its token is missing/not 64 lowercase
hex characters. The firmware necessarily contains its selected token, so treat
the generated binary as sensitive as well.
Build using the correct hardware environment, without an upload target:
pio run -e waveshare_esp32_s3_nano
pio run -e waveshare_esp32_s3_nano_v2
The OTA image is .pio/build/<environment>/firmware.bin, not the bootloader or
the full flash image. Set the OTA version to 1.1.3-ws. If changing PCB_ID
within one environment, its next build replaces the previous firmware.bin.
Keep OTA auto-update OFF when using per-ID binaries: upload and manually push
the matching image to board 1, then build/upload/push board 2's image. Never
flash one ID's image into both cards, which would duplicate identity and token.
Version 1.1.3-ws fixes the duplicate CRLF in the Bearer extra header from
1.1.2-id. The WebSockets library appends its own terminator; including another
one ends HTTP headers before User-Agent and can immediately break the upgrade.
Existing registrations and tokens do not need to be changed for this fix.
The user-managed firmware must connect using validated TLS and this contract:
WSS /ws/esp32?device_id=esp32-000000000001
Authorization: Bearer <individual-device-token>
The new firmware sends the ID in the handshake and its Bearer token only in
the Authorization header, including on reconnect. WSS and its OTA downloader
validate the server certificate against ISRG Root X1 (the current hub chain)
and require valid NTP time. Changing the server to another CA requires updating
include/cloud_config.h; no insecure fallback is enabled.
Existing VER:, POS:, sample and TEL messages stay unchanged. Old anonymous
firmware will not connect to the authenticated hub. For the initial transition,
build the new image without replacing the running service, and provision with
docker compose run --rm --no-deps tracker-hub python -m app.devices add ...
if the old running image does not yet contain the CLI. Prepare the ESP32 client
and its credentials before switching the production service. Firmware changes
and OTA installations are performed by the operator, not by automated tests.
If the authenticated hub is already deployed, an old anonymous client cannot
receive its first cloud OTA command. Use the board's local web interface OTA
uploader (POST /update, on a trusted LAN) for that first image; do not enable an anonymous
fallback on the hub.
docker compose exec tracker-hub python -m app.devices rotate --device-id esp32-000000000001 --grace-hours 1
docker compose exec tracker-hub python -m app.devices retire-previous --device-id esp32-000000000001
docker compose exec tracker-hub python -m app.devices revoke --device-id esp32-000000000001
docker compose exec tracker-hub python -m app.devices rename --device-id esp32-000000000001 --alias "New name"
Rotation creates a new versioned delivery file. Install the new token and verify
its connection before retiring the previous token. Revocation invalidates both
tokens and closes the active session; it does not erase the device's settings.
Use --help for enable/disable and explicit migrate-primary --device-id ...
to import the old PCB setting into ONE registered device. Record any old
in-memory calibration before stopping the old hub and apply it to that device.
PRIMARY_DEVICE_ID is only needed for compatibility with old unscoped API URLs;
the new dashboard always addresses a device explicitly.
Deployment Boundary
The hub has one Uvicorn worker and no application-level operator login. Protect the dashboard, browser WebSocket, operator APIs and OTA administration with a VPN or authenticated reverse proxy before public deployment. CORS and per-device tokens do not provide operator authentication. Allow the ESP32 connection path to forward its own Bearer header; firmware download URLs must remain reachable by the ESP32, separately from protected upload/push controls.
Compose defaults to 127.0.0.1:4000. If Nginx Proxy Manager shares the hub's
Docker network, forward to esp32_tracker_hub:8000. Otherwise explicitly set
HUB_BIND_IP in the server's private .env to its LAN address and keep the
proxy pointed at that address on port 4000. Include the LAN dashboard origin
in ALLOWED_ORIGINS when direct LAN browser access is required.
Set ALLOWED_ORIGINS and OTA_PUBLIC_URL for the actual dashboard origin and
download host. Do not expose an alternative unprotected container/host port.
Back up state, firmware and raw logs before migration or rollback; keep the old
image and compatible state backup until the new hub is verified.
Stop the service before taking a filesystem backup of SQLite, or use SQLite's
online backup API. A live devices.sqlite3 file alone may omit committed data
still held in its WAL file.
Older firmware still uses its old OTA downloader for the first transition. New firmware validates TLS, but signed images/on-device SHA-256 pinning are not implemented by this change; a hub-side digest is not a device-side signature.