update: firmware and provisioning now supports bootloader and partition tables

This commit is contained in:
2026-03-16 08:52:58 +02:00
parent 360725c93f
commit 4381a6681d
15 changed files with 776 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
import hashlib
import logging
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from fastapi import HTTPException
@@ -10,6 +12,8 @@ from shared.firebase import get_db
from shared.exceptions import NotFoundError
from firmware.models import FirmwareVersion, FirmwareMetadataResponse, UpdateType
logger = logging.getLogger(__name__)
COLLECTION = "firmware_versions"
VALID_HW_TYPES = {"vesper", "vesper_plus", "vesper_pro", "chronos", "chronos_pro", "agnus", "agnus_mini"}
@@ -46,13 +50,18 @@ def _doc_to_firmware_version(doc) -> FirmwareVersion:
def _fw_to_metadata_response(fw: FirmwareVersion) -> FirmwareMetadataResponse:
download_url = f"/api/firmware/{fw.hw_type}/{fw.channel}/{fw.version}/firmware.bin"
is_emergency = fw.update_type == UpdateType.emergency
is_mandatory = fw.update_type in (UpdateType.mandatory, UpdateType.emergency)
return FirmwareMetadataResponse(
hw_type=fw.hw_type,
channel=fw.channel,
channel=fw.channel, # firmware validates this matches requested channel
version=fw.version,
size_bytes=fw.size_bytes,
size=fw.size_bytes, # firmware reads "size"
size_bytes=fw.size_bytes, # kept for admin-panel consumers
sha256=fw.sha256,
update_type=fw.update_type,
update_type=fw.update_type, # urgency enum — for admin panel display
mandatory=is_mandatory, # firmware reads this to decide auto-apply
emergency=is_emergency, # firmware reads this to decide immediate apply
min_fw_version=fw.min_fw_version,
download_url=download_url,
uploaded_at=fw.uploaded_at,
@@ -130,7 +139,7 @@ def list_firmware(
return items
def get_latest(hw_type: str, channel: str) -> FirmwareMetadataResponse:
def get_latest(hw_type: str, channel: str, hw_version: str | None = None, current_version: str | None = None) -> FirmwareMetadataResponse:
if hw_type not in VALID_HW_TYPES:
raise HTTPException(status_code=400, detail=f"Invalid hw_type '{hw_type}'")
if channel not in VALID_CHANNELS:
@@ -180,6 +189,22 @@ def get_firmware_path(hw_type: str, channel: str, version: str) -> Path:
return path
def record_ota_event(event_type: str, payload: dict[str, Any]) -> None:
"""Persist an OTA telemetry event (download or flash) to Firestore.
Best-effort — caller should not raise on failure.
"""
try:
db = get_db()
db.collection("ota_events").add({
"event_type": event_type,
"received_at": datetime.now(timezone.utc),
**payload,
})
except Exception as exc:
logger.warning("Failed to persist OTA event (%s): %s", event_type, exc)
def delete_firmware(doc_id: str) -> None:
db = get_db()
doc_ref = db.collection(COLLECTION).document(doc_id)