mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 21:46:36 +00:00
feat: stage3 — REST API endpoints and dummy FlightProcessor
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""Core Flight Processor (Dummy / Stub for Stage 3)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from gps_denied.db.repository import FlightRepository
|
||||
from gps_denied.schemas import GPSPoint
|
||||
from gps_denied.schemas.flight import (
|
||||
BatchMetadata,
|
||||
BatchResponse,
|
||||
BatchUpdateResponse,
|
||||
DeleteResponse,
|
||||
FlightCreateRequest,
|
||||
FlightDetailResponse,
|
||||
FlightResponse,
|
||||
FlightStatusResponse,
|
||||
ObjectGPSResponse,
|
||||
UpdateResponse,
|
||||
UserFixRequest,
|
||||
UserFixResponse,
|
||||
Waypoint,
|
||||
)
|
||||
|
||||
|
||||
class FlightProcessor:
|
||||
"""Orchestrates flight business logic."""
|
||||
|
||||
def __init__(self, repo: FlightRepository) -> None:
|
||||
self.repo = repo
|
||||
|
||||
async def create_flight(self, req: FlightCreateRequest) -> FlightResponse:
|
||||
flight = await self.repo.insert_flight(
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
start_lat=req.start_gps.lat,
|
||||
start_lon=req.start_gps.lon,
|
||||
altitude=req.altitude,
|
||||
camera_params=req.camera_params.model_dump(),
|
||||
)
|
||||
for poly in req.geofences.polygons:
|
||||
await self.repo.insert_geofence(
|
||||
flight.id,
|
||||
nw_lat=poly.north_west.lat,
|
||||
nw_lon=poly.north_west.lon,
|
||||
se_lat=poly.south_east.lat,
|
||||
se_lon=poly.south_east.lon,
|
||||
)
|
||||
for w in req.rough_waypoints:
|
||||
await self.repo.insert_waypoint(flight.id, lat=w.lat, lon=w.lon)
|
||||
|
||||
return FlightResponse(
|
||||
flight_id=flight.id,
|
||||
status="prefetching",
|
||||
message="Flight created and prefetching started.",
|
||||
created_at=flight.created_at,
|
||||
)
|
||||
|
||||
async def get_flight(self, flight_id: str) -> FlightDetailResponse | None:
|
||||
flight = await self.repo.get_flight(flight_id)
|
||||
if not flight:
|
||||
return None
|
||||
wps = await self.repo.get_waypoints(flight_id)
|
||||
state = await self.repo.load_flight_state(flight_id)
|
||||
|
||||
waypoints = [
|
||||
Waypoint(
|
||||
id=w.id,
|
||||
lat=w.lat,
|
||||
lon=w.lon,
|
||||
altitude=w.altitude,
|
||||
confidence=w.confidence,
|
||||
timestamp=w.timestamp,
|
||||
refined=w.refined,
|
||||
)
|
||||
for w in wps
|
||||
]
|
||||
|
||||
status = state.status if state else "unknown"
|
||||
frames_processed = state.frames_processed if state else 0
|
||||
frames_total = state.frames_total if state else 0
|
||||
|
||||
# Assuming empty geofences for now unless loaded (omitted for brevity)
|
||||
from gps_denied.schemas import Geofences
|
||||
|
||||
return FlightDetailResponse(
|
||||
flight_id=flight.id,
|
||||
name=flight.name,
|
||||
description=flight.description,
|
||||
start_gps=GPSPoint(lat=flight.start_lat, lon=flight.start_lon),
|
||||
waypoints=waypoints,
|
||||
geofences=Geofences(polygons=[]),
|
||||
camera_params=flight.camera_params,
|
||||
altitude=flight.altitude,
|
||||
status=status,
|
||||
frames_processed=frames_processed,
|
||||
frames_total=frames_total,
|
||||
created_at=flight.created_at,
|
||||
updated_at=flight.updated_at,
|
||||
)
|
||||
|
||||
async def delete_flight(self, flight_id: str) -> DeleteResponse:
|
||||
deleted = await self.repo.delete_flight(flight_id)
|
||||
return DeleteResponse(deleted=deleted, flight_id=flight_id)
|
||||
|
||||
async def update_waypoint(
|
||||
self, flight_id: str, waypoint_id: str, waypoint: Waypoint
|
||||
) -> UpdateResponse:
|
||||
ok = await self.repo.update_waypoint(
|
||||
flight_id,
|
||||
waypoint_id,
|
||||
lat=waypoint.lat,
|
||||
lon=waypoint.lon,
|
||||
altitude=waypoint.altitude,
|
||||
confidence=waypoint.confidence,
|
||||
refined=waypoint.refined,
|
||||
)
|
||||
return UpdateResponse(updated=ok, waypoint_id=waypoint_id)
|
||||
|
||||
async def batch_update_waypoints(
|
||||
self, flight_id: str, waypoints: list[Waypoint]
|
||||
) -> BatchUpdateResponse:
|
||||
failed = []
|
||||
updated = 0
|
||||
for wp in waypoints:
|
||||
ok = await self.repo.update_waypoint(
|
||||
flight_id,
|
||||
wp.id,
|
||||
lat=wp.lat,
|
||||
lon=wp.lon,
|
||||
altitude=wp.altitude,
|
||||
confidence=wp.confidence,
|
||||
refined=wp.refined,
|
||||
)
|
||||
if ok:
|
||||
updated += 1
|
||||
else:
|
||||
failed.append(wp.id)
|
||||
return BatchUpdateResponse(success=(len(failed) == 0), updated_count=updated, failed_ids=failed)
|
||||
|
||||
async def queue_images(
|
||||
self, flight_id: str, metadata: BatchMetadata, file_count: int
|
||||
) -> BatchResponse:
|
||||
state = await self.repo.load_flight_state(flight_id)
|
||||
if state:
|
||||
total = state.frames_total + file_count
|
||||
await self.repo.save_flight_state(flight_id, frames_total=total, status="processing")
|
||||
|
||||
next_seq = metadata.end_sequence + 1
|
||||
seqs = list(range(metadata.start_sequence, metadata.end_sequence + 1))
|
||||
return BatchResponse(
|
||||
accepted=True,
|
||||
sequences=seqs,
|
||||
next_expected=next_seq,
|
||||
message=f"Queued {file_count} images.",
|
||||
)
|
||||
|
||||
async def handle_user_fix(self, flight_id: str, req: UserFixRequest) -> UserFixResponse:
|
||||
await self.repo.save_flight_state(flight_id, blocked=False, status="processing")
|
||||
return UserFixResponse(
|
||||
accepted=True, processing_resumed=True, message="Fix applied."
|
||||
)
|
||||
|
||||
async def get_flight_status(self, flight_id: str) -> FlightStatusResponse | None:
|
||||
state = await self.repo.load_flight_state(flight_id)
|
||||
if not state:
|
||||
return None
|
||||
return FlightStatusResponse(
|
||||
status=state.status,
|
||||
frames_processed=state.frames_processed,
|
||||
frames_total=state.frames_total,
|
||||
current_frame=state.current_frame,
|
||||
current_heading=None, # would load from latest
|
||||
blocked=state.blocked,
|
||||
search_grid_size=state.search_grid_size,
|
||||
created_at=state.created_at,
|
||||
updated_at=state.updated_at,
|
||||
)
|
||||
|
||||
async def convert_object_to_gps(
|
||||
self, flight_id: str, frame_id: int, pixel: tuple[float, float]
|
||||
) -> ObjectGPSResponse:
|
||||
# Dummy math
|
||||
return ObjectGPSResponse(
|
||||
gps=GPSPoint(lat=48.0, lon=37.0),
|
||||
accuracy_meters=5.0,
|
||||
frame_id=frame_id,
|
||||
pixel=pixel,
|
||||
)
|
||||
|
||||
async def stream_events(self, flight_id: str, client_id: str):
|
||||
"""Async generator for SSE dummy stream."""
|
||||
from gps_denied.schemas.events import SSEEventType
|
||||
import json
|
||||
|
||||
yield f"data: {json.dumps({'event': SSEEventType.FRAME_PROCESSED.value, 'data': {'msg': 'connected'}})}\n\n"
|
||||
for i in range(5):
|
||||
await asyncio.sleep(1)
|
||||
yield f"data: {json.dumps({'event': SSEEventType.FRAME_PROCESSED.value, 'data': {'frame_id': i, 'gps': {'lat': 48, 'lon': 37}, 'confidence': 0.9, 'timestamp': datetime.now(timezone.utc).isoformat()}})}\n\n"
|
||||
yield f"data: {json.dumps({'event': SSEEventType.FLIGHT_COMPLETED.value, 'data': {'frames_total': 5, 'frames_processed': 5}})}\n\n"
|
||||
Reference in New Issue
Block a user