mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:21:13 +00:00
e86084da6b
Implement the first runtime component boundaries around the shared contracts so downstream batches can consume typed frame, MAVLink, tile, and FDR behavior with focused tests and batch evidence. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
"""Public flight recorder interfaces."""
|
|
|
|
from typing import Any, Protocol
|
|
|
|
from shared.contracts import FdrEvent
|
|
from shared.errors import ErrorEnvelope
|
|
|
|
from .types import (
|
|
FdrAppendResult,
|
|
FdrExportRequest,
|
|
FdrExportResult,
|
|
FdrHealth,
|
|
FdrPayload,
|
|
FdrSegmentSummary,
|
|
)
|
|
|
|
|
|
class FlightRecorder(Protocol):
|
|
"""Append-only event recorder for runtime evidence."""
|
|
|
|
def append_event(self, event: Any) -> None:
|
|
"""Persist one FDR event."""
|
|
|
|
def export(self) -> Any:
|
|
"""Export recorded evidence for post-flight analysis."""
|
|
|
|
|
|
class InMemoryFlightRecorder:
|
|
"""Bounded append-only recorder for runtime evidence metadata."""
|
|
|
|
def __init__(self, segment_limit_bytes: int, storage_limit_bytes: int) -> None:
|
|
if segment_limit_bytes <= 0:
|
|
raise ValueError("segment_limit_bytes must be positive")
|
|
if storage_limit_bytes < segment_limit_bytes:
|
|
raise ValueError("storage_limit_bytes must be at least one segment")
|
|
self._segment_limit_bytes = segment_limit_bytes
|
|
self._storage_limit_bytes = storage_limit_bytes
|
|
self._segments: list[list[FdrEvent]] = [[]]
|
|
self._segment_bytes: list[int] = [0]
|
|
self._used_bytes = 0
|
|
|
|
@property
|
|
def health(self) -> FdrHealth:
|
|
if self._used_bytes >= self._storage_limit_bytes:
|
|
return FdrHealth(
|
|
status="critical",
|
|
used_bytes=self._used_bytes,
|
|
max_bytes=self._storage_limit_bytes,
|
|
message="fdr storage limit reached",
|
|
)
|
|
if self._used_bytes >= int(self._storage_limit_bytes * 0.9):
|
|
return FdrHealth(
|
|
status="degraded",
|
|
used_bytes=self._used_bytes,
|
|
max_bytes=self._storage_limit_bytes,
|
|
message="fdr storage nearing limit",
|
|
)
|
|
return FdrHealth(
|
|
status="ready",
|
|
used_bytes=self._used_bytes,
|
|
max_bytes=self._storage_limit_bytes,
|
|
message="fdr storage ready",
|
|
)
|
|
|
|
def append_event(self, event: FdrEvent, payload: FdrPayload) -> FdrAppendResult:
|
|
if self._used_bytes + payload.size_bytes > self._storage_limit_bytes:
|
|
return FdrAppendResult(
|
|
appended=False,
|
|
error=ErrorEnvelope(
|
|
component="fdr_observability",
|
|
category="resource",
|
|
message="fdr storage limit reached",
|
|
severity="critical",
|
|
retryable=False,
|
|
),
|
|
)
|
|
|
|
rollover = False
|
|
if self._segment_bytes[-1] + payload.size_bytes > self._segment_limit_bytes:
|
|
self._segments.append([])
|
|
self._segment_bytes.append(0)
|
|
rollover = True
|
|
|
|
segment_index = len(self._segments) - 1
|
|
stored_event = event.model_copy(update={"payload_ref": payload.ref})
|
|
self._segments[segment_index].append(stored_event)
|
|
self._segment_bytes[segment_index] += payload.size_bytes
|
|
self._used_bytes += payload.size_bytes
|
|
|
|
return FdrAppendResult(
|
|
appended=True,
|
|
event=stored_event,
|
|
segment_id=self._segment_id(segment_index),
|
|
rollover=rollover,
|
|
)
|
|
|
|
def export(self, request: FdrExportRequest) -> FdrExportResult:
|
|
segments = tuple(
|
|
FdrSegmentSummary(
|
|
segment_id=self._segment_id(index),
|
|
event_count=len(events),
|
|
bytes_used=self._segment_bytes[index],
|
|
)
|
|
for index, events in enumerate(self._segments)
|
|
if events
|
|
)
|
|
evidence_ref = f"fdr://exports/{request.mission_id}/{request.run_id}/evidence.json"
|
|
analytics_ref = (
|
|
f"fdr://exports/{request.mission_id}/{request.run_id}/analytics.parquet"
|
|
if request.include_analytics
|
|
else None
|
|
)
|
|
return FdrExportResult(
|
|
produced=True,
|
|
evidence_ref=evidence_ref,
|
|
segments=segments,
|
|
analytics_ref=analytics_ref,
|
|
)
|
|
|
|
def _segment_id(self, index: int) -> str:
|
|
return f"segment-{index + 1:04d}"
|