Files
espresso_frame/server/app/firmware.py
T
tfaour 94d67f7767
Build and push server image / build-and-push (push) Successful in 39s
Firmware CI releases + Gitea auto-update in the server
New Gitea Actions workflow builds both board variants and publishes
them as release assets whenever firmware/version.txt is bumped. The
server can now poll that repo's releases (next to the existing manual
upload) and either surface an "Update frame" button or, with
"Automatically apply updates" checked, stage the new build itself --
the frame still only updates on its own next wake either way.
2026-07-21 19:25:08 -04:00

40 lines
1.7 KiB
Python

"""Local firmware image storage + esp_app_desc_t parsing. Shared by the
manual upload path (POST /api/firmware) and the Gitea auto-update path
(see gitea_releases.py) -- both end up writing the same firmware.bin slot
that GET /frame/firmware streams to the device."""
from __future__ import annotations
from fastapi import HTTPException
from . import config
# ESP-IDF app images embed an esp_app_desc_t at byte offset 32 (24-byte
# image header + 8-byte first-segment header): magic word, then version
# (32 bytes, NUL-padded) at +16 and project name (32 bytes) at +48 --
# verified against this project's real build artifact.
APP_DESC_OFFSET = 32
APP_DESC_MAGIC = 0xABCD5432
EXPECTED_PROJECT_NAME = "espresso_frame"
def firmware_path():
return config.CONFIG_PATH.parent / "firmware.bin"
def parse_app_version(data: bytes) -> str:
"""Extracts the embedded version from an ESP-IDF app image, raising
HTTPException(400) for anything that isn't this project's firmware."""
if len(data) < APP_DESC_OFFSET + 80:
raise HTTPException(400, "File is too small to be a firmware image")
magic = int.from_bytes(data[APP_DESC_OFFSET : APP_DESC_OFFSET + 4], "little")
if magic != APP_DESC_MAGIC:
raise HTTPException(400, "Not an ESP-IDF application image")
version = data[APP_DESC_OFFSET + 16 : APP_DESC_OFFSET + 48].split(b"\x00")[0].decode(errors="replace")
project = data[APP_DESC_OFFSET + 48 : APP_DESC_OFFSET + 80].split(b"\x00")[0].decode(errors="replace")
if project != EXPECTED_PROJECT_NAME:
raise HTTPException(400, f"Image is for project '{project}', not '{EXPECTED_PROJECT_NAME}'")
if not version:
raise HTTPException(400, "Image has no embedded version")
return version