Files
bellsystems-cp/backend/firmware/router.py

138 lines
5.1 KiB
Python

from fastapi import APIRouter, Depends, Query, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional
import logging
from auth.models import TokenPayload
from auth.dependencies import require_permission
from firmware.models import FirmwareVersion, FirmwareListResponse, FirmwareMetadataResponse, UpdateType
from firmware import service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/firmware", tags=["firmware"])
ota_router = APIRouter(prefix="/api/ota", tags=["ota-telemetry"])
@router.post("/upload", response_model=FirmwareVersion, status_code=201)
async def upload_firmware(
hw_type: str = Form(...),
channel: str = Form(...),
version: str = Form(...),
update_type: UpdateType = Form(UpdateType.mandatory),
min_fw_version: Optional[str] = Form(None),
notes: Optional[str] = Form(None),
file: UploadFile = File(...),
_user: TokenPayload = Depends(require_permission("manufacturing", "add")),
):
file_bytes = await file.read()
return service.upload_firmware(
hw_type=hw_type,
channel=channel,
version=version,
file_bytes=file_bytes,
update_type=update_type,
min_fw_version=min_fw_version,
notes=notes,
)
@router.get("", response_model=FirmwareListResponse)
def list_firmware(
hw_type: Optional[str] = Query(None),
channel: Optional[str] = Query(None),
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
items = service.list_firmware(hw_type=hw_type, channel=channel)
return FirmwareListResponse(firmware=items, total=len(items))
@router.get("/{hw_type}/{channel}/latest", response_model=FirmwareMetadataResponse)
def get_latest_firmware(
hw_type: str,
channel: str,
hw_version: Optional[str] = Query(None, description="Hardware revision from NVS, e.g. '1.0'"),
current_version: Optional[str] = Query(None, description="Currently running firmware semver, e.g. '1.2.3'"),
):
"""Returns metadata for the latest firmware for a given hw_type + channel.
No auth required — devices call this endpoint to check for updates.
"""
return service.get_latest(hw_type, channel, hw_version=hw_version, current_version=current_version)
@router.get("/{hw_type}/{channel}/{version}/info", response_model=FirmwareMetadataResponse)
def get_firmware_info(hw_type: str, channel: str, version: str):
"""Returns metadata for a specific firmware version.
No auth required — devices call this to resolve upgrade chains.
"""
return service.get_version_info(hw_type, channel, version)
@router.get("/{hw_type}/{channel}/{version}/firmware.bin")
def download_firmware(hw_type: str, channel: str, version: str):
"""Download the firmware binary. No auth required — devices call this directly."""
path = service.get_firmware_path(hw_type, channel, version)
return FileResponse(
path=str(path),
media_type="application/octet-stream",
filename="firmware.bin",
)
@router.delete("/{firmware_id}", status_code=204)
def delete_firmware(
firmware_id: str,
_user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
service.delete_firmware(firmware_id)
# ─────────────────────────────────────────────────────────────────────────────
# OTA event telemetry — called by devices (no auth, best-effort)
# ─────────────────────────────────────────────────────────────────────────────
class OtaDownloadEvent(BaseModel):
device_uid: str
hw_type: str
hw_version: str
from_version: str
to_version: str
channel: str
class OtaFlashEvent(BaseModel):
device_uid: str
hw_type: str
hw_version: str
from_version: str
to_version: str
channel: str
sha256: str
@ota_router.post("/events/download", status_code=204)
def ota_event_download(event: OtaDownloadEvent):
"""Device reports that firmware was fully written to flash (pre-commit).
No auth required — best-effort telemetry from the device.
"""
logger.info(
"OTA download event: device=%s hw=%s/%s %s%s (channel=%s)",
event.device_uid, event.hw_type, event.hw_version,
event.from_version, event.to_version, event.channel,
)
service.record_ota_event("download", event.model_dump())
@ota_router.post("/events/flash", status_code=204)
def ota_event_flash(event: OtaFlashEvent):
"""Device reports that firmware partition was committed and device is rebooting.
No auth required — best-effort telemetry from the device.
"""
logger.info(
"OTA flash event: device=%s hw=%s/%s %s%s (channel=%s sha256=%.16s...)",
event.device_uid, event.hw_type, event.hw_version,
event.from_version, event.to_version, event.channel, event.sha256,
)
service.record_ota_event("flash", event.model_dump())