update: firmware and provisioning now supports bootloader and partition tables
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user