mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-22 04:51:09 +00:00
[AZ-651] [AZ-668] lost-link failsafe ladder + mapobjects persistence (batch 7)
AZ-651 (mission_executor lost-link ladder):
- LostLinkLadder pure-logic state machine (LinkOk -> Degraded -> Lost
-> LinkLostInFollow + MavlinkLost branch). Configurable thresholds
via LostLinkConfig.
- LostLinkCommandIssuer trait + MavlinkCommandIssuer production impl
emitting MAV_CMD_NAV_RETURN_TO_LAUNCH via MavlinkHandle::send_command.
- LostLinkDriver task wires the ladder to operator-link watch, MAVLink
LinkEvent broadcast, and optional target-follow signal. On RTL,
driver calls the issuer THEN MissionExecutorHandle::failsafe_trigger.
- failsafe_trigger(LinkLost | LinkLostInFollow) short-circuits FlyMission
-> Land via direct FSM state mutation + TransitionEvent emission;
Paused state is intentionally NOT overridden.
- Tests: 4/4 ACs locally green (degraded-no-rtl; lost-fires-once;
follow-grace; mavlink-loss-no-rtl) plus driver + FSM integration.
AZ-668 (mapobjects_store persistence):
- Snapshot serializable shape + Store::{to_snapshot,from_snapshot}
round trip.
- MapObjectsPersistence async trait + JsonSnapshotEngine default impl
(write to .tmp, sync_all, atomic rename, best-effort parent fsync).
- PersistenceError::{Corrupt, SchemaMismatch} surfaces explicit errors
on bad blob; PersistenceMetrics tracks last_snapshot_ts,
snapshot_size_bytes, snapshot_errors_total.
- MapObjectsStore::from_snapshot factory for crash recovery from the
composition root.
- Tests: 4/4 ACs locally green (round-trip; atomic rename ignores
partial .tmp; crash recovery preserves pending; corruption returns
explicit error) plus schema-mismatch + metrics smoke checks.
Quality gates:
- cargo fmt: clean.
- cargo clippy -p mission_executor -p mapobjects_store --tests: 0 warns.
- cargo test --workspace: all green.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,6 +33,11 @@ use shared::models::mission::{Coordinate, MissionItem, MissionWaypoint};
|
||||
mod internal;
|
||||
|
||||
pub use internal::driver::{DriverError, MissionDriver};
|
||||
pub use internal::lost_link::{
|
||||
LadderEvent, LadderInput, LadderOutput, LadderState, LostLinkCommandIssuer, LostLinkConfig,
|
||||
LostLinkDriver, LostLinkLadder, LostLinkLadderHandle, MavlinkCommandIssuer,
|
||||
MAV_CMD_NAV_RETURN_TO_LAUNCH,
|
||||
};
|
||||
pub use internal::telemetry::{
|
||||
Consumer, DropCountingReceiver, MavlinkProjection, TelemetryForwarder,
|
||||
};
|
||||
@@ -244,10 +249,56 @@ impl MissionExecutorHandle {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn failsafe_trigger(&self, _kind: FailsafeKind) -> Result<()> {
|
||||
Err(AutopilotError::NotImplemented(
|
||||
"mission_executor::failsafe_trigger (AZ-651)",
|
||||
))
|
||||
/// Apply a failsafe response immediately.
|
||||
///
|
||||
/// AZ-651 implements the link-loss family: `LinkLost` and
|
||||
/// `LinkLostInFollow` both cause the FSM to short-circuit from
|
||||
/// `FlyMission` to `Land` (and the lost-link driver issues
|
||||
/// `MAV_CMD_NAV_RETURN_TO_LAUNCH` separately so the airframe also
|
||||
/// returns home — the FSM transition reflects the autopilot's
|
||||
/// internal accounting). Other states are NOT overridden: if the
|
||||
/// FSM is still in `Disconnected` / `Armed` / `TakeOff` /
|
||||
/// `MissionUploaded`, the airframe failsafe is the right authority
|
||||
/// and we let it handle the abort.
|
||||
///
|
||||
/// Battery and geofence failsafes (`BatteryRtl`, `BatteryHardFloor`,
|
||||
/// `GeofenceInclusion`, `GeofenceExclusion`) land in AZ-652 with
|
||||
/// their own state-aware overrides; calling this method with one
|
||||
/// of those kinds returns `NotImplemented` for now.
|
||||
///
|
||||
/// Calling this while the FSM is already `Paused` is a no-op (we
|
||||
/// do not clobber the existing pause).
|
||||
pub async fn failsafe_trigger(&self, kind: FailsafeKind) -> Result<()> {
|
||||
match kind {
|
||||
FailsafeKind::LinkLost | FailsafeKind::LinkLostInFollow => {
|
||||
let mut core = self.core.lock().await;
|
||||
if core.state == MissionState::FlyMission {
|
||||
let from = core.state;
|
||||
core.state = MissionState::Land;
|
||||
let _ = self.events_tx.send(TransitionEvent {
|
||||
variant: core.variant,
|
||||
from,
|
||||
to: MissionState::Land,
|
||||
at: chrono::Utc::now(),
|
||||
retry_count: 0,
|
||||
});
|
||||
}
|
||||
// Other states (incl. Paused) — leave alone. The
|
||||
// airframe's own failsafe (or whatever paused us) is
|
||||
// authoritative.
|
||||
Ok(())
|
||||
}
|
||||
FailsafeKind::LinkDegraded => {
|
||||
// Degraded is yellow-health-only; no transition needed.
|
||||
Ok(())
|
||||
}
|
||||
FailsafeKind::BatteryRtl
|
||||
| FailsafeKind::BatteryHardFloor
|
||||
| FailsafeKind::GeofenceInclusion
|
||||
| FailsafeKind::GeofenceExclusion => Err(AutopilotError::NotImplemented(
|
||||
"mission_executor::failsafe_trigger: battery/geofence land in AZ-652",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-AZ-648 helper kept for callers that only need to validate a
|
||||
|
||||
Reference in New Issue
Block a user