mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 02:16:36 +00:00
204 lines
7.2 KiB
Python
204 lines
7.2 KiB
Python
"""Core Flight Processor (Dummy / Stub for Stage 3)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from datetime import datetime, timezone
|
|
|
|
from gps_denied.core.pipeline import ImageInputPipeline
|
|
from gps_denied.core.results import ResultManager
|
|
from gps_denied.core.sse import SSEEventStreamer
|
|
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,
|
|
)
|
|
from gps_denied.schemas.image import ImageBatch
|
|
|
|
|
|
class FlightProcessor:
|
|
"""Manages business logic and background processing for flights."""
|
|
|
|
def __init__(self, repository: FlightRepository, streamer: SSEEventStreamer) -> None:
|
|
self.repository = repository
|
|
self.streamer = streamer
|
|
self.result_manager = ResultManager(repository, streamer)
|
|
self.pipeline = ImageInputPipeline(storage_dir=".image_storage", max_queue_size=50)
|
|
|
|
async def create_flight(self, req: FlightCreateRequest) -> FlightResponse:
|
|
flight = await self.repository.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.repository.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.repository.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.repository.get_flight(flight_id)
|
|
if not flight:
|
|
return None
|
|
wps = await self.repository.get_waypoints(flight_id)
|
|
state = await self.repository.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.repository.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.repository.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.repository.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.repository.load_flight_state(flight_id)
|
|
if state:
|
|
total = state.frames_total + file_count
|
|
await self.repository.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.repository.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.repository.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 stream."""
|
|
# Yield from the real SSE streamer generator
|
|
async for event in self.streamer.stream_generator(flight_id, client_id):
|
|
yield event
|