mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 09:06:37 +00:00
194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""REST API Endpoints for Flight Management."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, File, Form, HTTPException, Path, UploadFile
|
|
from sse_starlette.sse import EventSourceResponse
|
|
|
|
from gps_denied.api.deps import ProcessorDep, SessionDep
|
|
from gps_denied.schemas.flight import (
|
|
BatchMetadata,
|
|
BatchResponse,
|
|
BatchUpdateResponse,
|
|
DeleteResponse,
|
|
FlightCreateRequest,
|
|
FlightDetailResponse,
|
|
FlightResponse,
|
|
FlightStatusResponse,
|
|
ObjectGPSResponse,
|
|
ObjectToGPSRequest,
|
|
UpdateResponse,
|
|
UserFixRequest,
|
|
UserFixResponse,
|
|
Waypoint,
|
|
)
|
|
|
|
router = APIRouter(prefix="/flights", tags=["flights"])
|
|
|
|
|
|
@router.post("", response_model=FlightResponse, status_code=201)
|
|
async def create_flight(
|
|
req: FlightCreateRequest,
|
|
processor: ProcessorDep,
|
|
session: SessionDep,
|
|
) -> FlightResponse:
|
|
"""Create a new flight and trigger prefetching."""
|
|
res = await processor.create_flight(req)
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.get("/{flight_id}", response_model=FlightDetailResponse)
|
|
async def get_flight(
|
|
flight_id: Annotated[str, Path(..., title="The ID of the flight")],
|
|
processor: ProcessorDep,
|
|
) -> FlightDetailResponse:
|
|
"""Get complete flight information."""
|
|
res = await processor.get_flight(flight_id)
|
|
if not res:
|
|
raise HTTPException(status_code=404, detail="Flight not found")
|
|
return res
|
|
|
|
|
|
@router.delete("/{flight_id}", response_model=DeleteResponse)
|
|
async def delete_flight(
|
|
flight_id: Annotated[str, Path(...)],
|
|
processor: ProcessorDep,
|
|
session: SessionDep,
|
|
) -> DeleteResponse:
|
|
"""Delete a flight and all associated data."""
|
|
res = await processor.delete_flight(flight_id)
|
|
if not res.deleted:
|
|
raise HTTPException(status_code=404, detail="Flight not found")
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.put("/{flight_id}/waypoints/{waypoint_id}", response_model=UpdateResponse)
|
|
async def update_waypoint(
|
|
flight_id: Annotated[str, Path(...)],
|
|
waypoint_id: Annotated[str, Path(...)],
|
|
waypoint: Waypoint,
|
|
processor: ProcessorDep,
|
|
session: SessionDep,
|
|
) -> UpdateResponse:
|
|
"""Update a specific waypoint."""
|
|
res = await processor.update_waypoint(flight_id, waypoint_id, waypoint)
|
|
if not res.updated:
|
|
raise HTTPException(status_code=404, detail="Waypoint or Flight not found")
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.put("/{flight_id}/waypoints/batch", response_model=BatchUpdateResponse)
|
|
async def batch_update_waypoints(
|
|
flight_id: Annotated[str, Path(...)],
|
|
waypoints: list[Waypoint],
|
|
processor: ProcessorDep,
|
|
session: SessionDep,
|
|
) -> BatchUpdateResponse:
|
|
"""Batch update multiple waypoints."""
|
|
res = await processor.batch_update_waypoints(flight_id, waypoints)
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.post("/{flight_id}/images/batch", response_model=BatchResponse, status_code=202)
|
|
async def upload_image_batch(
|
|
flight_id: Annotated[str, Path(...)],
|
|
metadata: Annotated[str, Form(...)],
|
|
images: list[UploadFile] = File(...),
|
|
processor: ProcessorDep = None, # type: ignore
|
|
session: SessionDep = None, # type: ignore
|
|
) -> BatchResponse:
|
|
"""Upload a batch of UAV images."""
|
|
try:
|
|
meta_dict = json.loads(metadata)
|
|
meta_obj = BatchMetadata(**meta_dict)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid metadata JSON: {e}")
|
|
|
|
f_info = await processor.get_flight(flight_id)
|
|
if not f_info:
|
|
raise HTTPException(status_code=404, detail="Flight not found")
|
|
|
|
# P1#6: Batch size validation (allow 1-50 for dev, spec says 10-50)
|
|
if len(images) < 1 or len(images) > 50:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"Batch must contain 1-50 images, got {len(images)}",
|
|
)
|
|
|
|
res = await processor.queue_images(flight_id, meta_obj, len(images))
|
|
|
|
# P1#5: Spawn background task to process each frame
|
|
import cv2
|
|
import numpy as np
|
|
|
|
async def _process_batch():
|
|
for idx, upload in enumerate(images):
|
|
raw = await upload.read()
|
|
arr = np.frombuffer(raw, dtype=np.uint8)
|
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
|
if img is not None:
|
|
frame_id = meta_obj.start_frame_id + idx
|
|
await processor.process_frame(flight_id, frame_id, img)
|
|
|
|
asyncio.create_task(_process_batch())
|
|
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.post("/{flight_id}/user-fix", response_model=UserFixResponse)
|
|
async def submit_user_fix(
|
|
flight_id: Annotated[str, Path(...)],
|
|
fix_data: UserFixRequest,
|
|
processor: ProcessorDep,
|
|
session: SessionDep,
|
|
) -> UserFixResponse:
|
|
"""Submit a verified GPS anchor to unblock processing."""
|
|
res = await processor.handle_user_fix(flight_id, fix_data)
|
|
await session.commit()
|
|
return res
|
|
|
|
|
|
@router.post("/{flight_id}/frames/{frame_id}/object-to-gps", response_model=ObjectGPSResponse)
|
|
async def convert_object_to_gps(
|
|
flight_id: Annotated[str, Path(...)],
|
|
frame_id: Annotated[int, Path(...)],
|
|
req: ObjectToGPSRequest,
|
|
processor: ProcessorDep,
|
|
) -> ObjectGPSResponse:
|
|
"""Convert a pixel coordinate to GPS coordinate for an object."""
|
|
return await processor.convert_object_to_gps(flight_id, frame_id, (req.pixel_x, req.pixel_y))
|
|
|
|
|
|
@router.get("/{flight_id}/status", response_model=FlightStatusResponse)
|
|
async def get_flight_status(
|
|
flight_id: Annotated[str, Path(...)],
|
|
processor: ProcessorDep,
|
|
) -> FlightStatusResponse:
|
|
"""Get processing status of a flight."""
|
|
res = await processor.get_flight_status(flight_id)
|
|
if not res:
|
|
raise HTTPException(status_code=404, detail="Flight not found")
|
|
return res
|
|
|
|
|
|
@router.get("/{flight_id}/stream")
|
|
async def create_sse_stream(
|
|
flight_id: Annotated[str, Path(...)],
|
|
processor: ProcessorDep,
|
|
) -> EventSourceResponse:
|
|
"""SSE endpoint for real-time processing events."""
|
|
f_info = await processor.get_flight(flight_id)
|
|
if not f_info:
|
|
raise HTTPException(status_code=404, detail="Flight not found")
|
|
|
|
return EventSourceResponse(processor.stream_events(flight_id, client_id="default"))
|