Files
autopilot/_docs/03_implementation/batch_09_cycle1_report.md
T
Oleksandr Bezdieniezhnykh 358b2fbb53 [AZ-652] mission_executor safety + resume + middle-waypoint (batch 9)
Geofence (INCLUSION+EXCLUSION, ≤500 ms detect→RTL), battery
thresholds (RTL@25%/land@15% + signed override), middle-waypoint
re-upload (CLEAR_ALL→upload→SET_CURRENT(0)), and post-flight
mapobjects push trigger. Adds production MAVLink command issuers
for both geofence and battery failsafe families.

Implements 6 ACs with 12 integration tests + module unit tests;
full workspace test suite green. See batch_09_cycle1_report.md
for AC coverage and known limitations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 19:48:46 +03:00

13 KiB
Raw Blame History

Batch 9 (cycle 1) implementation report

Tasks: AZ-652 Component scope: mission_executor Verdict: PASS_WITH_WARNINGS — proceed; flagged items below.

Tasks

AZ-652 mission_executor_safety_and_resume — Geofence + battery + middle-waypoint + post-flight

Outcome: Implemented. All six acceptance criteria green; production MAVLink command issuers wired for both geofence and battery families.

Production code added:

  • crates/mission_executor/src/internal/geofence.rs

    • GeofenceVerdict { Ok, InclusionExit, ExclusionEntry } — symmetric semantics (both variants treated as faults; the C++ behaviour of silently ignoring EXCLUSION is rejected).
    • GeofenceMonitor — pure point-in-polygon evaluator (ray-casting, no external crate dependency; geo would have pulled num-traits etc. for one function we can implement in 25 LOC).
    • GeofenceEvent { Violation, RtlIssued, RtlSendFailed } — broadcast surface.
    • GeofenceCommandIssuer trait — separate from the lost-link issuer per the AZ-651 "each failsafe family owns its command surface" pattern.
    • MavlinkGeofenceCommandIssuer — production impl that calls mavlink_layer::MavlinkHandle::send_command(MAV_CMD_NAV_RETURN_TO_LAUNCH).
    • GeofenceDriver — wiring layer; 100 ms tick, edge-triggered RTL (only on Ok→violation), shutdown-aware.
  • crates/mission_executor/src/internal/battery_thresholds.rs

    • BatteryConfig { rtl_threshold_pct, hard_floor_pct } — defaults 25 % / 15 % per task spec.
    • BatteryOverride — signed (signature pre-validated by operator_bridge per AZ-689); fields carry operator id + rationale for audit logging.
    • BatteryAction { None, IssueRtl, IssueLandNow } — discriminator returned by the pure monitor.
    • BatteryMonitor — pure logic: latches once it has fired so the same RTL is not re-issued on the next tick; honours active override (suppresses RTL only — hard-floor land is not override-able).
    • BatteryCommandIssuer trait + MavlinkBatteryCommandIssuer production impl (MAV_CMD_NAV_RETURN_TO_LAUNCH for RTL, MAV_CMD_NAV_LAND for hard-floor land-now).
    • BatteryDriver — wiring layer; subscribes to SYS_STATUS-projected battery percentages, emits audit-log entries for overrides via tracing.
  • crates/mission_executor/src/internal/middle_waypoint.rs

    • MiddleWaypointHint { at, insert_after_seq, label } — externally supplied by scan_controller (the spec excludes the placement algorithm from this task).
    • MissionRePlanner::on_middle_waypoint(hint, current_mission) — runs MISSION_CLEAR_ALL → upload patched waypoints → MISSION_SET_CURRENT(0) via the MissionDriver trait. Returns the patched mission so the executor can mirror it into the FSM's mission field.
    • MissionRePlanner::on_target_follow_release(reason, original_mission, current_position) — re-uploads the original mission anchored at the current position.
  • crates/mission_executor/src/internal/post_flight.rs

    • MapObjectsPusher trait (production impl is mission_client::MissionClientHandle::push_mapobjects_diff per AZ-647); MapObjectsDiffSource trait (production impl is mapobjects_store::MapObjectsStoreHandle::dump_pending per AZ-654).
    • PostFlightPusher::push_once(mission_id) — called from the POST_FLIGHT_SYNC entry guard. Errors are logged but never block the executor's progression to DONE (spec is explicit: degraded push surfaces a manual-replay warning; FSM still reaches DONE).
  • crates/mission_executor/src/lib.rs

    • MissionExecutorHandle gained driver: Arc<dyn MissionDriver> and hard_floor_active: Arc<AtomicBool> fields.
    • insert_middle_waypoint(Coordinate) now delegates to MissionRePlanner and updates the FSM's mission on success.
    • failsafe_trigger(FailsafeKind) extended to handle BatteryRtl, BatteryHardFloor, GeofenceInclusion, GeofenceExclusion — all transition FlyMission → Land via the existing transition_flymission_to_land helper; BatteryHardFloor additionally latches hard_floor_active.
    • health() flips to red while hard_floor_active is set regardless of FSM state.
    • clear_hard_floor() — operator-driven recovery (ground-test workflow, swapped battery).
    • #[doc(hidden)] force_state_for_tests(state) — integration-test back-door so failsafe behaviour can be asserted in the FlyMission state without wiring the full transition harness. Hidden from rustdoc and not part of the public API.

Tests:

  • crates/mission_executor/tests/safety_and_resume.rs (12 integration tests; all green):
    • ac1_inclusion_geofence_exit_triggers_rtl (AC-1).
    • ac2_exclusion_geofence_entry_triggers_rtl (AC-2).
    • ac3a_battery_rtl_at_threshold (AC-3, RTL branch).
    • ac3b_battery_land_now_at_hard_floor_and_flips_health_red (AC-3, hard-floor branch + health).
    • ac4_signed_override_suppresses_battery_rtl (AC-4).
    • ac5_middle_waypoint_reupload_sequence (AC-5; asserts MISSION_CLEAR_ALL → upload → MISSION_SET_CURRENT(0) order via spy driver).
    • ac6_post_flight_push_triggered_once_executor_reaches_done (AC-6).
    • ac6_degraded_push_does_not_block_caller (AC-6 negative path).
    • battery_rtl_failsafe_transitions_flymission_to_landfailsafe_trigger plumbing.
    • battery_hard_floor_failsafe_latches_health_red — latch persistence + recovery.
    • target_follow_release_recomputes_and_reuploadsMissionRePlanner::on_target_follow_release.
    • battery_override_can_be_applied_via_handle_apply_override_channel — override propagation surface.
  • Module unit tests (internal::geofence::tests 6 tests; internal::battery_thresholds::tests 8 tests; internal::middle_waypoint::tests 4 tests; internal::post_flight::tests 2 tests) cover the pure-logic surface.

AC coverage

AC Behaviour Test Status
AC-1 INCLUSION exit → RTL ≤500 ms; FSM → Land; alert observable ac1_inclusion_geofence_exit_triggers_rtl PASS
AC-2 EXCLUSION entry → RTL ≤500 ms (parity with INCLUSION); alert observable ac2_exclusion_geofence_entry_triggers_rtl PASS
AC-3a SYS_STATUS ≤25 % → RTL; FSM → Land ac3a_battery_rtl_at_threshold PASS
AC-3b SYS_STATUS <15 % → MAV_CMD_NAV_LAND; health → red ac3b_battery_land_now_at_hard_floor_and_flips_health_red PASS
AC-4 Signed BatteryOverride { until_ts } suppresses RTL; audit-log entry ac4_signed_override_suppresses_battery_rtl PASS
AC-5 MISSION_CLEAR_ALL → upload → MISSION_SET_CURRENT(0) in order, ≤2 s e2e ac5_middle_waypoint_reupload_sequence PASS
AC-6 On POST_FLIGHT_SYNC entry → push_mapobjects_diff exactly once; FSM still reaches DONE on push failure ac6_post_flight_push_triggered_once_executor_reaches_done, ac6_degraded_push_does_not_block_caller PASS

Code review

Spec compliance: PASS. All six ACs implemented with test seams that demonstrate the spec'd state transitions. The two AC-3 branches and the two AC-6 branches (happy + degraded) are split into separate tests for blast-radius isolation.

Architecture compliance: PASS.

  • Layer 3 coordinator (mission_executor) imports only shared, mavlink_layer, mission_client (via traits in this batch), and mapobjects_store (via traits in this batch). No new Layer 3 ↔ Layer 3 imports.
  • MavlinkGeofenceCommandIssuer and MavlinkBatteryCommandIssuer are the production wiring for the two new failsafe families; both call mavlink_layer::MavlinkHandle::send_command(CommandLong) via the existing mavlink_layer Public API (same surface AZ-651's MavlinkCommandIssuer uses for lost-link).
  • The MAV_CMD_NAV_LAND constant is co-located with the battery driver since that is the only family that issues it; MAV_CMD_NAV_RETURN_TO_LAUNCH continues to live in internal::lost_link and is re-exported (both families share the constant rather than defining a duplicate).

SRP: PASS.

  • geofence.rs — pure monitor + driver + production command issuer; one file because the three concepts are tightly coupled and the file is ~470 LOC.
  • battery_thresholds.rs — same structure for battery.
  • middle_waypoint.rs — pure replanner + types; no driver task (it is invoked synchronously by MissionExecutorHandle::insert_middle_waypoint).
  • post_flight.rs — pure orchestrator + two traits; no MAVLink dependency (the push goes through mission_client).

Runtime completeness: PASS. The Runtime Completeness section of the spec required real point-in-polygon, real SYS_STATUS decode, and real MAV_CMD_* issuance. All three are present:

  • Point-in-polygon: ray-casting in geofence::point_in_polygon (deterministic, branch-coverage tested).
  • SYS_STATUS decode: the battery driver consumes shared::models::telemetry::UavSysStatus which is already produced by mavlink_layer's MavlinkProjection (AZ-649).
  • MAV_CMD_* issuance: MavlinkGeofenceCommandIssuer and MavlinkBatteryCommandIssuer both call the production MavlinkHandle::send_command surface.

Test discipline: PASS. Each AC maps to one named test (two branches each for AC-3 and AC-6). AAA pattern with language-appropriate comment syntax (// Arrange / // Act / // Assert). Spy implementations (SpyGeofenceIssuer, SpyBatteryIssuer, SpyMissionDriver, SpyPusher) record calls in Arc<Mutex<Vec<_>>> and are asserted on directly — no "no error thrown" tests.

Security quick-scan: PASS. No string-interpolated commands; no untrusted input parsing in this batch. BatteryOverride signature validation is excluded from this task's scope (handled by operator_bridge per AZ-689). The driver assumes the override surface has already verified signatures upstream — this is documented in the type's docstring.

Performance scan: PASS. Geofence monitor ticks at 10 Hz × O(total vertices); with the operational ≤8 fences × ≤32 vertices typical for a single mission this is a few hundred FLOPs per tick — well under the AZ-652 ≤500 ms response budget. The 100 ms tick gives a worst-case 100 ms detection latency, plus the MAVLink command round-trip; well inside ≤500 ms.

Cross-task consistency: N/A — this batch contains a single task.

Module-layout drift (minor)

_docs/02_document/module-layout.md lists crates/mission_executor/src/internal/geofence/* (a folder). This batch implements it as a single file (crates/mission_executor/src/internal/geofence.rs). The file is ~470 LOC and cohesive (pure monitor + driver + production command issuer); splitting into a folder for this batch would be premature. If a future batch adds new geofence variants (cylinder, altitude floor) or polygon preprocessing (R-tree), the file becomes a folder at that point. Flagged here so the next module-layout sync picks it up.

Known limitations (warnings)

  1. MavlinkBatteryCommandIssuer::issue_land_now passes all param_* zeroed. Per architecture.md §7.7 this asks the airframe to pick the safest reachable landing point. If a future BIT item or operator setting wants to bias toward a specific recovery point, the issuer gains a Coordinate parameter at that point. Currently no caller supplies one.

  2. force_state_for_tests is hidden from rustdoc but is a public symbol. It is marked #[doc(hidden)] and only used by tests/safety_and_resume.rs. An alternative would be a cfg(test)-only module, but that does not work for integration tests (which compile against the public API). This is the same back-door pattern used by several existing FSM crates in the workspace.

  3. Audit-log persistence is a tracing::info! call, not a database write. The spec excludes shared::audit persistence from this task; the driver emits a structured tracing::info!(target = "audit", ...) entry which the runtime's tracing subscriber routes to the audit sink wired by shared::audit (when it lands). This matches the AZ-651 lost-link audit-log pattern.

Auto-fix attempts during the batch

  • cargo fmt -p mission_executor straightened use mavlink_layer::{CommandLong, MavlinkHandle, SendCommandError}; after adding the production issuers.
  • Removed an unused mpsc import from tests/safety_and_resume.rs (initial draft used a channel; final version uses a watch for telemetry replay).
  • clippy -p mission_executor --tests -- -D warnings is green.

Test reproduction

cargo build -p mission_executor --tests
cargo test  -p mission_executor                  # all green
cargo test --test safety_and_resume -p mission_executor   # 12 tests; 0 failed
cargo clippy -p mission_executor --tests -- -D warnings
cargo test --workspace                            # all green

Candidates for batch 10

  • AZ-653 gimbal_a40_transport — opens up the gimbal_link BIT evaluator slot (AZ-650 batch 8 noted it as the natural next slot).
  • AZ-689 operator_bridge_signed_commands — closes the upstream signature-validation gap referenced by AC-4's audit-log note here.

Batch 10 sizing: one of the above; not both. AZ-653 unblocks more downstream BIT slots; AZ-689 closes a documented gap in this batch's audit-log surface.