Consolidate firmware UI; server learns board from the device, not a picker
Build and push server image / build-and-push (push) Successful in 37s

All firmware-related controls (manual upload, Gitea repo URL,
auto-update checkbox, detected board, Update frame button) now live in
one "Firmware update" card instead of being split across the main
Settings form and a separate card.

The board variant used to pick a Gitea release asset was a dropdown
the user had to set by hand and could get wrong. The device now
reports it itself via a new X-Frame-Board header (CONFIG_FRAME_BOARD_NAME,
"devkit" by default, "xiao" in sdkconfig.xiao) on every /frame/config
poll, stored as device_board_variant -- the server learns it instead.
Update checks/applies are gated on the board being known, since there's
nothing to fetch until a device has checked in at least once.
This commit is contained in:
2026-07-21 21:58:59 -04:00
parent ad1fa99064
commit 7ea5c9a4fb
7 changed files with 128 additions and 60 deletions
+28 -17
View File
@@ -48,14 +48,17 @@ algorithm itself -- it just streams the response straight to the panel.
pushing this repo to a Gitea instance, `.gitea/workflows/firmware-release-build.yml`
builds both supported boards and publishes them as release assets
(`firmware-xiao.bin`/`firmware-devkit.bin`) whenever `firmware/version.txt`
changes on `main`. In the web UI's Settings form, set **Firmware Gitea
repo URL** to that repo (e.g. `https://git.example.com/owner/repo`) and
pick the frame's **board**; if the repo is private, also set
`GITEA_FIRMWARE_TOKEN` (a read-only PAT) in `docker-compose.yml`. The
server then periodically checks for a newer release and either shows
an "Update frame" button or, with **Automatically apply updates**
checked, stages it itself -- either way the frame only actually
updates on its own next wake (see `POST /api/firmware` above).
changes on `main`. In the web UI's "Firmware update" card, set the
**Gitea repo URL** to that repo (e.g. `https://git.example.com/owner/repo`);
if the repo is private, also set `GITEA_FIRMWARE_TOKEN` (a read-only
PAT) in `docker-compose.yml`. Which board's build to fetch is learned
from the frame itself (its `X-Frame-Board` header, `CONFIG_FRAME_BOARD_NAME`
on the firmware side) -- nothing to pick by hand, though the frame
does need to have checked in at least once first. The server then
periodically checks for a newer release and either shows an "Update
frame" button or, with **Automatically apply updates** checked,
stages it itself -- either way the frame only actually updates on its
own next wake (see `POST /api/firmware` above).
## Endpoints
@@ -117,7 +120,11 @@ algorithm itself -- it just streams the response straight to the panel.
device compares it against its own running version
(`esp_app_get_description()->version`, sent as an `X-Frame-Version`
request header, stored as `device_firmware_version`) to decide whether
to OTA
to OTA. The device also sends an `X-Frame-Board` header
(`CONFIG_FRAME_BOARD_NAME`, e.g. `"xiao"`), stored as
`device_board_variant` -- how the Gitea auto-update feature below
learns which board to fetch a release for, instead of a user picking
it
- `GET /frame/photo-info` -- `{"asset_id": ..., "location_line1": ... |
null, "location_line2": ... | null, "taken_at": ... | null}` for the
current photo (same idempotent current-photo semantics as
@@ -170,16 +177,20 @@ algorithm itself -- it just streams the response straight to the panel.
been uploaded yet
- `GET /api/firmware/check` -- throttled (`gitea_releases.UPDATE_CHECK_INTERVAL_S`,
15 min) check of the configured Gitea repo's latest release for the
frame's board variant. `{"enabled": false}` if no repo URL is
configured; otherwise `{"enabled": true, "latest_version": "1.2.3" | null,
"staged_version": "1.2.2" | null, "update_available": bool}`. If
"Automatically apply updates" is on and a newer release is found, this
call also stages it immediately (same effect as a manual upload) --
otherwise the web UI shows an "Update frame" button
frame's board variant (learned from the device, see `device_board_variant`
below -- not user-configured). `{"enabled": false}` if no repo URL is
configured; otherwise `{"enabled": true, "board": "xiao" | null,
"latest_version": "1.2.3" | null, "staged_version": "1.2.2" | null,
"update_available": bool}`. `update_available` stays false until the
board is known, regardless of what Gitea has. If "Automatically apply
updates" is on and a newer release is found, this call also stages it
immediately (same effect as a manual upload) -- otherwise the web UI
shows an "Update frame" button
- `POST /api/firmware/apply-latest` -- the "Update frame" button: pulls
and stages the latest Gitea release right now, bypassing the check
throttle. 404 if no repo is configured, has no releases, or the latest
release has no asset for the configured board variant
throttle. 400 if no repo is configured or no device has checked in
yet (board unknown); 404 if the repo has no releases, or the latest
release has no asset for the frame's board
- `GET /api/queue` -- `{"current": {...} | null, "upcoming": [...],
"device": {"last_seen": ts | null, "overdue": bool,
"firmware_version": "1.2.3" | null, "firmware_available": "1.2.4" | null,