159 lines
8.6 KiB
Markdown
159 lines
8.6 KiB
Markdown
# 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:
|
||
|
||
```text
|
||
-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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```text
|
||
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.
|
||
|
||
```sh
|
||
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.
|