Files

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 via GET /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.json and pcb_state.json are 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.