Consolidate firmware UI; server learns board from the device, not a picker
Build and push server image / build-and-push (push) Successful in 37s
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:
+30
-13
@@ -207,10 +207,14 @@ def frame_config(request: Request):
|
||||
reachability check. Always returns 200 with current settings
|
||||
(defaults if nothing's been saved yet) -- no Immich-configured gate,
|
||||
since this doubles as the "is the server up" signal. Also captures
|
||||
the device's running firmware version (X-Frame-Version header) and
|
||||
advertises the uploaded OTA image's version, so the device's update
|
||||
check costs zero extra round trips."""
|
||||
the device's running firmware version and board variant (X-Frame-
|
||||
Version/X-Frame-Board headers -- the latter is how the Gitea
|
||||
auto-update feature learns which release asset to fetch, instead of
|
||||
a user picking it in the web UI) and advertises the uploaded OTA
|
||||
image's version, so the device's update check costs zero extra
|
||||
round trips."""
|
||||
reported_version = request.headers.get("X-Frame-Version", "")
|
||||
reported_board = request.headers.get("X-Frame-Board", "")
|
||||
with config.locked():
|
||||
cfg = config.load()
|
||||
cfg.last_seen = time.time()
|
||||
@@ -221,6 +225,8 @@ def frame_config(request: Request):
|
||||
if cfg.device_firmware_version and reported_version != cfg.device_firmware_version:
|
||||
cfg.stats.ota_updates_applied += 1
|
||||
cfg.device_firmware_version = reported_version
|
||||
if reported_board:
|
||||
cfg.device_board_variant = reported_board
|
||||
config.save(cfg)
|
||||
return {
|
||||
"refresh_interval_s": _effective_refresh_interval_s(cfg),
|
||||
@@ -276,7 +282,6 @@ def api_config_save(
|
||||
quiet_hours_end: str = Form("07:00"),
|
||||
timezone: str = Form("UTC"),
|
||||
firmware_update_repo_url: str = Form(""),
|
||||
firmware_board_variant: str = Form("xiao"),
|
||||
firmware_auto_update: bool = Form(False),
|
||||
):
|
||||
# Immich URL/API key/Gitea token are env-var only (IMMICH_URL/
|
||||
@@ -309,7 +314,6 @@ def api_config_save(
|
||||
if timezone in ALL_TIMEZONES:
|
||||
cfg.timezone = timezone
|
||||
cfg.firmware_update_repo_url = firmware_update_repo_url.strip()
|
||||
cfg.firmware_board_variant = firmware_board_variant if firmware_board_variant in ("xiao", "devkit") else "xiao"
|
||||
cfg.firmware_auto_update = firmware_auto_update
|
||||
cfg.stats.config_saves += 1
|
||||
config.save(cfg)
|
||||
@@ -493,13 +497,19 @@ def _fetch_latest_release(cfg: config.FrameConfig) -> dict | None:
|
||||
def _apply_gitea_update(cfg: config.FrameConfig) -> str:
|
||||
"""Downloads the configured Gitea repo's latest release asset for this
|
||||
frame's board variant and stages it exactly like a manual
|
||||
POST /api/firmware upload would. Network I/O happens before the lock
|
||||
is taken, matching the load/mutate/save concurrency pattern used
|
||||
elsewhere (see config.locked())."""
|
||||
POST /api/firmware upload would. The board comes from the device
|
||||
itself (device_board_variant, learned from its X-Frame-Board header
|
||||
on GET /frame/config -- see frame_config()), not a user picker, so
|
||||
there's nothing to fetch until a device has checked in at least
|
||||
once. Network I/O happens before the lock is taken, matching the
|
||||
load/mutate/save concurrency pattern used elsewhere (see
|
||||
config.locked())."""
|
||||
if not cfg.device_board_variant:
|
||||
raise HTTPException(400, "No frame has checked in yet -- can't tell which board's build to fetch")
|
||||
release = _fetch_latest_release(cfg)
|
||||
if not release:
|
||||
raise HTTPException(404, "No releases found in the configured Gitea repo")
|
||||
asset_name = gitea_releases.asset_name_for_board(cfg.firmware_board_variant)
|
||||
asset_name = gitea_releases.asset_name_for_board(cfg.device_board_variant)
|
||||
asset_url = release["assets"].get(asset_name)
|
||||
if not asset_url:
|
||||
raise HTTPException(404, f"Latest release has no '{asset_name}' asset")
|
||||
@@ -524,7 +534,11 @@ def api_firmware_check():
|
||||
(gitea_releases.UPDATE_CHECK_INTERVAL_S) -- cheap, since it only reads
|
||||
the release's tag name, not its binaries. If firmware_auto_update is
|
||||
on and a newer version is found, applies it immediately; otherwise
|
||||
just reports it so the web UI can offer the "Update frame" button."""
|
||||
just reports it so the web UI can offer the "Update frame" button.
|
||||
Applying (auto or manual) needs to know the frame's board, which is
|
||||
learned from the device's own X-Frame-Board header rather than
|
||||
picked by the user -- update_available stays false until a device
|
||||
has checked in at least once, regardless of what Gitea has."""
|
||||
cfg = config.load()
|
||||
if not cfg.firmware_update_repo_url:
|
||||
return {"enabled": False}
|
||||
@@ -543,9 +557,11 @@ def api_firmware_check():
|
||||
config.save(cfg)
|
||||
cfg = config.load()
|
||||
|
||||
update_available = bool(
|
||||
cfg.firmware_gitea_latest_version
|
||||
) and cfg.firmware_gitea_latest_version != cfg.firmware_available_version
|
||||
update_available = (
|
||||
bool(cfg.firmware_gitea_latest_version)
|
||||
and cfg.firmware_gitea_latest_version != cfg.firmware_available_version
|
||||
and bool(cfg.device_board_variant)
|
||||
)
|
||||
if update_available and cfg.firmware_auto_update:
|
||||
_apply_gitea_update(cfg)
|
||||
cfg = config.load()
|
||||
@@ -553,6 +569,7 @@ def api_firmware_check():
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"board": cfg.device_board_variant or None,
|
||||
"latest_version": cfg.firmware_gitea_latest_version or None,
|
||||
"staged_version": cfg.firmware_available_version or None,
|
||||
"update_available": update_available,
|
||||
|
||||
Reference in New Issue
Block a user