mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 09:16:36 +00:00
feat: stage1 — domain schemas, SSE events, pydantic-settings config
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""Domain models — GPS, Camera, Geometry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GPSPoint(BaseModel):
|
||||
"""WGS-84 coordinate."""
|
||||
|
||||
lat: float = Field(..., ge=-90, le=90, description="Latitude")
|
||||
lon: float = Field(..., ge=-180, le=180, description="Longitude")
|
||||
|
||||
|
||||
class CameraParameters(BaseModel):
|
||||
"""Intrinsic camera parameters."""
|
||||
|
||||
focal_length: float = Field(..., gt=0, description="Focal length in mm")
|
||||
sensor_width: float = Field(..., gt=0, description="Sensor width in mm")
|
||||
sensor_height: float = Field(..., gt=0, description="Sensor height in mm")
|
||||
resolution_width: int = Field(..., gt=0, description="Image width in pixels")
|
||||
resolution_height: int = Field(..., gt=0, description="Image height in pixels")
|
||||
principal_point: tuple[float, float] | None = None
|
||||
distortion_coefficients: list[float] | None = None
|
||||
|
||||
|
||||
class Polygon(BaseModel):
|
||||
"""Bounding box defined by NW and SE corners."""
|
||||
|
||||
north_west: GPSPoint
|
||||
south_east: GPSPoint
|
||||
|
||||
|
||||
class Geofences(BaseModel):
|
||||
"""Collection of geofence polygons."""
|
||||
|
||||
polygons: list[Polygon] = Field(default_factory=list)
|
||||
@@ -0,0 +1,66 @@
|
||||
"""SSE event schemas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from gps_denied.schemas import GPSPoint
|
||||
|
||||
|
||||
class SSEEventType(str, Enum):
|
||||
"""Server-Sent Event types emitted during flight processing."""
|
||||
|
||||
FRAME_PROCESSED = "frame_processed"
|
||||
FRAME_REFINED = "frame_refined"
|
||||
SEARCH_EXPANDED = "search_expanded"
|
||||
USER_INPUT_NEEDED = "user_input_needed"
|
||||
PROCESSING_BLOCKED = "processing_blocked"
|
||||
FLIGHT_COMPLETED = "flight_completed"
|
||||
|
||||
|
||||
class FrameProcessedEvent(BaseModel):
|
||||
"""Payload for frame_processed / frame_refined events."""
|
||||
|
||||
frame_id: int
|
||||
gps: GPSPoint
|
||||
altitude: float | None = None
|
||||
confidence: float
|
||||
heading: float | None = None
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
class SearchExpandedEvent(BaseModel):
|
||||
"""Payload for search_expanded event."""
|
||||
|
||||
frame_id: int
|
||||
grid_size: int
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class UserInputNeededEvent(BaseModel):
|
||||
"""Payload for user_input_needed event."""
|
||||
|
||||
frame_id: int
|
||||
reason: str
|
||||
consecutive_failures: int = 0
|
||||
|
||||
|
||||
class FlightCompletedEvent(BaseModel):
|
||||
"""Payload for flight_completed event."""
|
||||
|
||||
frames_total: int
|
||||
frames_processed: int
|
||||
duration_seconds: float | None = None
|
||||
|
||||
|
||||
class SSEMessage(BaseModel):
|
||||
"""Generic SSE message wrapper."""
|
||||
|
||||
event: SSEEventType
|
||||
data: dict[str, Any]
|
||||
id: str | None = None
|
||||
retry: int | None = None
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Schemas for Flight API requests and responses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from gps_denied.schemas import CameraParameters, Geofences, GPSPoint
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Waypoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Waypoint(BaseModel):
|
||||
"""Single waypoint with optional refinement flag."""
|
||||
|
||||
id: str
|
||||
lat: float = Field(..., ge=-90, le=90)
|
||||
lon: float = Field(..., ge=-180, le=180)
|
||||
altitude: float | None = None
|
||||
confidence: float = Field(..., ge=0, le=1)
|
||||
timestamp: datetime
|
||||
refined: bool = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flight — Requests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FlightCreateRequest(BaseModel):
|
||||
"""POST /flights body."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: str = ""
|
||||
start_gps: GPSPoint
|
||||
rough_waypoints: list[GPSPoint] = Field(default_factory=list)
|
||||
geofences: Geofences = Field(default_factory=Geofences)
|
||||
camera_params: CameraParameters
|
||||
altitude: float = Field(..., gt=0)
|
||||
|
||||
|
||||
class UserFixRequest(BaseModel):
|
||||
"""POST /flights/{flightId}/user-fix body."""
|
||||
|
||||
frame_id: int = Field(..., ge=0)
|
||||
uav_pixel: tuple[float, float]
|
||||
satellite_gps: GPSPoint
|
||||
|
||||
|
||||
class ObjectToGPSRequest(BaseModel):
|
||||
"""POST /flights/{flightId}/frames/{frameId}/object-to-gps body."""
|
||||
|
||||
pixel_x: float = Field(..., ge=0)
|
||||
pixel_y: float = Field(..., ge=0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flight — Responses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FlightResponse(BaseModel):
|
||||
"""Response after flight creation."""
|
||||
|
||||
flight_id: str
|
||||
status: str
|
||||
message: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FlightDetailResponse(BaseModel):
|
||||
"""GET /flights/{flightId} response."""
|
||||
|
||||
flight_id: str
|
||||
name: str
|
||||
description: str
|
||||
start_gps: GPSPoint
|
||||
waypoints: list[Waypoint] = Field(default_factory=list)
|
||||
geofences: Geofences
|
||||
camera_params: CameraParameters
|
||||
altitude: float
|
||||
status: str
|
||||
frames_processed: int = 0
|
||||
frames_total: int = 0
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class FlightStatusResponse(BaseModel):
|
||||
"""GET /flights/{flightId}/status response."""
|
||||
|
||||
status: str
|
||||
frames_processed: int = 0
|
||||
frames_total: int = 0
|
||||
current_frame: int | None = None
|
||||
current_heading: float | None = None
|
||||
blocked: bool = False
|
||||
search_grid_size: int | None = None
|
||||
message: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class DeleteResponse(BaseModel):
|
||||
deleted: bool
|
||||
flight_id: str
|
||||
|
||||
|
||||
class UpdateResponse(BaseModel):
|
||||
updated: bool
|
||||
waypoint_id: str
|
||||
|
||||
|
||||
class BatchUpdateResponse(BaseModel):
|
||||
success: bool
|
||||
updated_count: int
|
||||
failed_ids: list[str] = Field(default_factory=list)
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Image Batch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class BatchMetadata(BaseModel):
|
||||
"""Metadata accompanying an image batch upload."""
|
||||
|
||||
start_sequence: int = Field(..., ge=0)
|
||||
end_sequence: int = Field(..., ge=0)
|
||||
batch_number: int = Field(..., ge=0)
|
||||
|
||||
|
||||
class BatchResponse(BaseModel):
|
||||
"""Response after batch upload."""
|
||||
|
||||
accepted: bool
|
||||
sequences: list[int] = Field(default_factory=list)
|
||||
next_expected: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User Fix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class UserFixResponse(BaseModel):
|
||||
accepted: bool
|
||||
processing_resumed: bool = False
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Object → GPS
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ObjectGPSResponse(BaseModel):
|
||||
gps: GPSPoint
|
||||
accuracy_meters: float
|
||||
frame_id: int
|
||||
pixel: tuple[float, float]
|
||||
Reference in New Issue
Block a user