Greenfield Steps 1-6 baseline for the autopilot rewrite from legacy Qt/C++ to a Rust workspace. - Remove legacy Qt/C++ tree (ai_controller, drone_controller, misc/camera, python_scaffold, root Dockerfile, autopilot.pro, legacy main.py / requirements.txt). - Add _docs/00_problem (problem, restrictions, acceptance criteria, security approach, input data + fixtures). - Add _docs/01_solution/solution_draft01. - Add _docs/02_document (architecture, system-flows, data_model, glossary, decision-rationale, deployment, 13 component descriptions, tests/ specs, FINAL_report, module-layout). - Add _docs/02_tasks/todo with 47 task specs (AZ-640..AZ-686, one bootstrap + 46 component tasks) and _dependencies_table.md. - Add .cursor/rules/artifact-srp.mdc (single-responsibility rule for canonical _docs artifacts). - Track autodev state in _docs/_autodev_state.md (Step 6 completed, ready for Step 7 Implement). Jira: bootstrap AZ-626; component epics AZ-627..AZ-639; tasks AZ-640..AZ-686. Total complexity 173 points across 12 epics. Co-authored-by: Cursor <cursoragent@cursor.com>
66 KiB
autopilot — System Flows
This document traces the main runtime flows through the component graph. Each flow has the same structure: entry point, narrative, sequence diagram, error scenarios, and (for non-trivial flows) a data-flow table. F4 is the full behaviour-tree spec for scan_controller.
Flow Index
| # | Flow | Notes |
|---|---|---|
| F1 | Frame pipeline (RTSP → bboxes → fan-out) | 5–7 participants; the system's main data plane in. |
| F2 | Movement detection (zoom-out + zoom-in) | Telemetry-synchronised ego-motion compensation; per-zoom-band thresholds; mandatory. |
| F3 | VLM confirmation (optional) | Explicit VlmAssessment loopback + fail-closed branches + "VLM disabled" alt. |
| F4 | Scan controller behaviour tree | Full BT spec (text + ASCII tree + YAML + 15 fixed-wing rules + tick scenarios). |
| F5 | Operator round trip | Always-on stream + POI confirm / decline / follow / timeout. |
| F6 | Mission lifecycle | mission_client → mission_executor → mavlink_layer → ArduPilot; multirotor + fixed-wing variants; lost-link failsafe. |
| F7 | MapObjects + ignored-items (in-flight) | H3 lookup, k-ring, class-group, distance/move thresholds, REMOVED diff, ignored-items append. |
| F8 | MapObjects sync (central DB) | Pre-flight pull / post-flight push against /missions/{id}/mapobjects; batched only for MVP. |
| F9 | Pre-flight self-test (BIT) | Gates takeoff on every dependency in §5 plus mission load + MapObjects pre-flight pull. |
| F10 | Lost-link failsafe ladder | LinkOk → LinkDegraded → LinkLost → LinkLostInFollow with grace windows; default RTL after 30 s. |
F1. Frame pipeline (RTSP → bboxes → fan-out)
Entry: frame_ingest opens an RTSP session against the ViewPro A40 and pushes frames at the platform-supported rate.
Narrative:
frame_ingestdecodes RTSP frames. Each decoded frame carries a monotonic timestamp, the gimbal's pan/tilt/zoom snapshot, and the UAV motion sample, all synchronised within the configured skew tolerance (out-of-tolerance frames are dropped or downgraded — see F2).- Each frame is forwarded to
detection_client, which streams it on a bi-directional gRPC channel to the external../detectionsservice.../detectionsreturns a list of normalized bboxes (class, confidence, geometry). - The bbox set fans out, in parallel, to:
telemetry_stream— for operator-side overlay rendering (always-on; not gated on detection content).scan_controller— for the zoom-out sweep / zoom-in decision, the POI queue, and the ≤5 POIs/min cap.semantic_analyzer— only whenscan_controlleris inZoomedIn; it consumes the cropped ROI and returns Tier 2 evidence (path freshness, endpoint scoring, concealment score).vlm_client— only whenscan_controllerrequests zoom-in confirmation and the VLM optionality flag is enabled; see F3.
scan_controlleremits a single coherent decision per tick: continue zoom-out, transition to zoom-in on a queued POI, hold endpoint for VLM, or enter target-follow on operator confirmation. Decision-to-movement latency budget is ≤500 ms.
Sequence diagram:
sequenceDiagram
participant CAM as ViewPro A40 (RTSP)
participant FI as frame_ingest
participant DC as detection_client
participant DETS as ../detections
participant SC as scan_controller
participant SA as semantic_analyzer
participant TS as telemetry_stream
CAM-->>FI: RTSP frames
FI->>DC: frame + telemetry snapshot
DC->>DETS: bidir gRPC frame
DETS-->>DC: bboxes (class, conf, geometry)
par fan-out
DC->>TS: bboxes for operator overlay
and
DC->>SC: bboxes for POI queue
and
DC->>SA: ROI crops (zoom-in only)
end
SA-->>SC: Tier 2 evidence
SC->>SC: tick decision (ZoomedOut / ZoomedIn / TargetFollow)
Error scenarios:
- gRPC stream disconnect to
../detections→detection_clientreconnects with backoff; in-flight frames are dropped, not retried (per-frame freshness matters more than completeness). Health endpoint reflects the dependency state. - Telemetry skew exceeds tolerance →
frame_ingestflags the frame;movement_detector(F2) refuses to consume it;scan_controllerstill receives bboxes but with the skew flag set. - Bbox payload schema-invalid →
detection_clientrejects the message and logs structured error; no silent swallow. - Tier 2 ROI inference fails →
semantic_analyzerreturns a typed error;scan_controllertreats the ROI as inconclusive and proceeds per the configured Tier-2-failure policy (default: hold for VLM if enabled, else release POI withinconclusive).
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| Camera → frame_ingest | H.264/265 RTSP | RTSP/RTP | per-frame |
| frame_ingest → detection_client | frame + telemetry snapshot | in-process | per-frame |
| detection_client ↔ ../detections | bidi gRPC frame ↔ bboxes | gRPC | per-frame |
| detection_client → telemetry_stream | bboxes for overlay | in-process | per-frame |
| detection_client → scan_controller | bboxes for POI queue | in-process | per-frame |
| detection_client → semantic_analyzer | ROI crops (zoom-in only) | in-process | per-ROI |
| semantic_analyzer → scan_controller | Tier 2 evidence | in-process | per-ROI |
F2. Movement detection (zoom-out + zoom-in)
Entry: movement_detector subscribes to frames + synchronised telemetry from frame_ingest whenever scan_controller is in ZoomedOut or ZoomedIn. It is suppressed only during TargetFollow (the gimbal is dominated by tracking commands).
Narrative:
movement_detectorconsumes only frames whose telemetry skew is within tolerance (frame timestamp ↔ gimbal pan/tilt/zoom ↔ UAV motion). The skew tolerance is per zoom band — tighter at zoom-in (gimbal slewing dominates the residual signal at narrow FOV).- It computes ego-motion compensation using OpenCV optical flow / global-motion estimation, fused with the gimbal angle / zoom and UAV velocity. Stable objects (trees, houses, terrain) must not be reported as moving solely because the platform moves.
- Residual motion clusters that survive ego-motion subtraction are emitted as movement candidates, each with a normalised box, a confidence proxy, and a
source_zoom_bandenum (zoomed_out|zoomed_in). - The cluster-persistence threshold and residual-velocity floor are configured per zoom band. The pixel-to-metre ratio differs by ~10×, so the same residual pixel motion implies very different physical motion; the configuration normalises this.
- Movement candidates are enqueued by
scan_controller. Enqueue-latency budget is ≤1 s for zoom-out candidates, ≤1.5 s for zoom-in candidates (allowing a brief gimbal-stability window). They share the POI queue (and the ≤5 POIs/min cap) with semantic POIs. - At zoom-in, the typical lifecycle is: a candidate appears mid-hold → if it remains within the current ROI, the ROI's confidence is bumped (no new POI); → if it appears outside the current ROI but within the broader zoomed FOV, it becomes a candidate-POI for the queue; → if the gimbal needs to retarget,
scan_controllerdecides whether to interrupt the current hold (only for higher-priority candidates). - After zoom-in confirmation, the system attempts semantic / YOLO confirmation as vehicle, people, or other relevant target. Target-follow only starts after operator confirmation.
Adequacy at zoom-in (research item, see architecture.md §8 Q14). Classical OpenCV optical flow / global-motion estimation is well-validated at zoom-out but degrades when the gimbal is actively path-following at narrow FOV. The benchmark gate measures the false-positive rate at zoom-in independently from zoom-out. If the zoom-in cap is exceeded with classical CV, the implementation falls back to a learned optical-flow / CNN motion-segmentation module behind a feature flag, while keeping the same Frame + telemetry → Vec<MovementCandidate> interface contract.
Sequence diagram:
sequenceDiagram
participant FI as frame_ingest
participant MD as movement_detector
participant SC as scan_controller
participant GC as gimbal_controller
FI->>MD: frame + telemetry (skew OK; zoom band tagged)
MD->>MD: ego-motion compensation (per-zoom-band)
alt residual motion cluster
MD->>SC: movement candidate (bbox, conf, source_zoom_band)
alt source_zoom_band == zoomed_out
SC->>SC: enqueue as zoom-in POI (≤1 s)
SC->>GC: zoom + center on candidate
else source_zoom_band == zoomed_in
alt within current ROI
SC->>SC: bump current ROI confidence
else outside current ROI
SC->>SC: enqueue as candidate-POI (≤1.5 s)
Note over SC: interrupt only if higher priority<br/>than current hold
end
end
else stable scene
MD-->>MD: drop (do not emit)
end
Error scenarios:
- Telemetry skew out of tolerance →
movement_detectorskips the frame; logs the skew; does not emit candidates from unsynchronised data. Skew tolerance is per-zoom-band; zoom-in is stricter. - Optical-flow failure on degenerate frames (low texture, motion blur) → returns no candidates; not an error. At zoom-in this is more frequent (narrow FOV, gimbal slewing); aggregate "no-candidate" rate above a threshold is surfaced as health → yellow.
- POI queue saturated at ≤5 POIs/min cap → newest candidate is held with aging; oldest unprocessed candidates may age out per the queue policy. Zoom-in candidates inherit the same cap (no separate budget).
- Zoom-in confirmation rejects the candidate (no semantic / YOLO match) → POI released;
scan_controllerreturns to zoom-out (or to the next queued POI). - Sustained zoom-in false-positive flood (per-zoom-band threshold exceeded across the running window) → automatically suppress zoom-in movement detection and surface health → yellow; classical-CV failure mode for Q14 follow-up.
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| frame_ingest → movement_detector | frame + telemetry snapshot | in-process | per-frame (both zoom bands; suppressed only in TargetFollow) |
| movement_detector → scan_controller | movement candidate (bbox + conf + source_zoom_band) | in-process | per-candidate |
| scan_controller → gimbal_controller | zoom + centre command | in-process | per-POI transition |
F3. VLM confirmation (optional)
Entry: scan_controller is in ZoomedIn, holding on a POI endpoint, and the runtime configuration flag vlm_enabled is true (which is itself gated by the benchmark-gate result; see architecture.md §7.6 Local VLM confirmation).
Narrative:
scan_controllerrequests confirmation for one bounded ROI crop. It hands the ROI plus context (POI class group, confidence, prior Tier 2 evidence) tovlm_client.vlm_clientvalidates the request payload (size limit, format allow-list, peer credential check), then pushes one bounded ROI crop + a short prompt over a Unix-domain socket to a local NanoLLM/VILA1.5-3B process.- The local VLM responds;
vlm_clientvalidates the answer against the structuredVlmAssessmentschema (label enum, confidence, evidence spans, reason, status). Free-form text is not a downstream API. - Success path:
vlm_clientemits the validatedVlmAssessmentback toscan_controller.scan_controllerintegrates it with Tier 2 evidence and decides whether to surface the POI to the operator (viaoperator_bridge), release the POI, or hold target-follow. - Fail-closed paths — if any of the following occur,
vlm_clientreturns aVlmAssessmentwithstatusset accordingly andlabel = inconclusive:- Schema-invalid output →
status: schema_invalid. - Timeout (response > 5 s/ROI budget) →
status: timeout. - IPC error (socket closed, peer-cred check failed, oversized payload, decode failure) →
status: ipc_error. In every fail-closed case,scan_controllerMUST NOT promote the POI to a confirmed target on VLM evidence; it falls back to Tier 2 evidence + operator review. No silent swallow; the failure is logged with the originating POI ID.
- Schema-invalid output →
- VLM-disabled alt path: when
vlm_enabled == false(benchmark gate failed, or build-time feature module is absent),scan_controllerskips the VLM step entirely. The Level-2 hold proceeds on Tier 2 evidence alone; the operator timeout still applies; the POI is surfaced to the operator withvlm_status: disabledso the UI can render the source-of-evidence indicator correctly.
Sequence diagram:
sequenceDiagram
participant SC as scan_controller
participant VC as vlm_client
participant VLM as NanoLLM/VILA1.5-3B (local IPC)
participant OB as operator_bridge
alt vlm_enabled == true
SC->>VC: ROI crop + context (ZoomedIn hold)
VC->>VC: validate payload (size/format/peer-cred)
VC->>VLM: bounded crop + short prompt (UDS)
alt VLM responds within ≤5 s
VLM-->>VC: response text
VC->>VC: schema validation
alt schema OK
VC-->>SC: VlmAssessment {label, conf, status: ok}
SC->>OB: surface POI with VLM evidence
else schema_invalid
VC-->>SC: VlmAssessment {label: inconclusive, status: schema_invalid}
SC->>OB: surface POI WITHOUT VLM evidence (fail-closed)
end
else timeout / IPC error
VC-->>SC: VlmAssessment {label: inconclusive, status: timeout|ipc_error}
SC->>OB: surface POI WITHOUT VLM evidence (fail-closed)
end
else vlm_enabled == false
SC->>OB: surface POI with vlm_status: disabled
end
Error scenarios:
- Sequential GPU contention — VLM and YOLO share GPU memory.
scan_controllerenforces no concurrent execution; any concurrency violation is a logic bug and must be alarmed, not silently retried. - Repeated
ipc_errorin short window →vlm_clientraises a structured health alert (does not silently disable VLM). The operator-facing health surface reflects degraded VLM availability;scan_controllercontinues operating with VLM treated as unavailable until recovery. - Oversized ROI payload → rejected at validation step; never sent to VLM. Logged.
- Free-form text outside the schema →
status: schema_invalid; never converted to aconfirmedlabel.
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| scan_controller → vlm_client | ROI crop + context | in-process | per-Level-2 hold |
| vlm_client ↔ NanoLLM | bounded crop + short prompt ↔ structured response | Unix-domain socket | per-request |
| vlm_client → scan_controller | VlmAssessment (always returned, status reflects outcome) |
in-process | per-request |
| scan_controller → operator_bridge | POI + evidence (with vlm_status field) |
in-process | per-POI surface |
F4. Scan controller behaviour tree
Entry: scan_controller ticks at a fixed rate (10 Hz) from process start; this section is the full spec.
This flow is the spec for scan_controller. The rewrite uses a deterministic typed state machine inspired by the behaviour-tree (BT) model below — every tick re-evaluates the root, so a high-priority safety condition immediately preempts lower-priority mission work. The BT below is the canonical decomposition; the implementation may flatten it into a state machine as long as the priorities, preemption, blackboard semantics, and tick scenarios are preserved.
What is a Behaviour Tree
A behaviour tree (BT) is a hierarchical model that controls decision-making. The tree is ticked (evaluated) from the root every cycle. Each node returns one of three statuses: Success, Failure, or Running.
Core node types
| Node | Symbol | Behaviour |
|---|---|---|
| Selector (fallback) | ? |
Tries children left-to-right. Succeeds on first child success. Fails only if all children fail. |
| Sequence | → |
Runs children left-to-right. Fails on first child failure. Succeeds only if all children succeed. |
| Condition | ◇ |
Checks a boolean predicate. Returns Success or Failure instantly (no Running). |
| Action | ▢ |
Executes a command. Can return Running while in progress. |
| Decorator | δ |
Wraps a single child and modifies its result (invert, repeat, timeout, etc.). |
Why it works for UAVs
- Priority is structural: nodes higher and to the left are checked first.
- Reactivity is built in: every tick re-evaluates from the root, so a high-priority safety condition immediately preempts lower-priority mission work.
- Modularity: subtrees can be developed, tested, and swapped independently.
Tick cycle visualised
Every tick (e.g. 10 Hz):
Root Selector
├─ [1] Safety checks ← evaluated FIRST every tick
├─ [2] Target engagement ← only if safety passes
└─ [3] Search pattern ← only if no target active
If during search the UAV drifts outside boundary, the next tick will hit the safety branch first and trigger recovery before search continues.
UAV mission context
- Platform: fixed-wing surveillance UAV.
- Target priority: 1) Artillery, 2) Tanks, 3) Trucks and cars.
- Constraints: operational boundary geofence, dynamic battery RTB, lost-link protocol.
Sequence diagram (one tick)
sequenceDiagram
participant TICK as Tick (10 Hz)
participant SC as scan_controller (root selector)
participant SAFE as Safety subtree
participant TENG as Target Engagement subtree
participant SRCH as Search subtree
participant BB as Blackboard
TICK->>SC: tick()
SC->>SAFE: evaluate
alt safety triggered
SAFE->>BB: read state (battery, geofence, comms, weather, health)
SAFE-->>SC: Running (action in progress) | Success
Note over SC: preempt lower priorities
else safety passes
SC->>TENG: evaluate
alt target detected
TENG->>BB: read target_*_detected
TENG-->>SC: Running (orbit/track) | Success
else no target
SC->>SRCH: evaluate
SRCH->>BB: read tree_row_detected / trench_detected
SRCH-->>SC: Running (fly leg / investigate)
end
end
Full behaviour tree structure
Root [Selector]
├── Safety [Selector]
│ ├── Boundary Recovery [Sequence]
│ │ ├── [Condition] outside_boundary?
│ │ └── [Action] recover_to_closest_boundary_point
│ │
│ ├── Low Battery RTB [Sequence]
│ │ ├── [Condition] battery ≤ energy_to_exit + reserve?
│ │ └── [Action] return_to_exit_point
│ │
│ ├── Lost Link [Sequence]
│ │ ├── [Condition] comms_lost > timeout?
│ │ └── [Action] execute_lost_link_route
│ │
│ ├── No-Fly Zone [Sequence]
│ │ ├── [Condition] approaching_nfz?
│ │ └── [Action] divert_around_nfz
│ │
│ ├── Weather Abort [Sequence]
│ │ ├── [Condition] wind > max_safe_wind?
│ │ └── [Action] return_to_exit_point
│ │
│ └── Emergency Health [Sequence]
│ ├── [Condition] critical_failure_detected?
│ └── [Action] emergency_rtb
│
├── Target Engagement [Selector]
│ ├── Artillery Track [Sequence]
│ │ ├── [Condition] artillery_detected?
│ │ ├── [Action] classify_confirm_artillery
│ │ └── [Action] orbit_and_track_artillery
│ │
│ ├── Tank Track [Sequence]
│ │ ├── [Condition] tank_detected?
│ │ ├── [Action] classify_confirm_tank
│ │ └── [Action] orbit_and_track_tank
│ │
│ └── Vehicle Track [Sequence]
│ ├── [Condition] truck_or_car_detected?
│ ├── [Action] classify_confirm_vehicle
│ └── [Action] orbit_and_track_vehicle
│
└── Search Pattern [Sequence]
├── [Action] fly_search_legs
└── Area Investigation [Selector]
├── Tree Row Investigation [Sequence]
│ ├── [Condition] tree_row_detected?
│ └── [SubTree] investigate_tree_row
│
├── Trench Investigation [Sequence]
│ ├── [Condition] trench_detected?
│ └── [SubTree] investigate_trench
│
└── [Action] continue_to_next_search_leg
Key subtrees explained
1. Safety — Boundary Recovery
Boundary Recovery [Sequence]
├── [Condition] NOT inside_boundary
└── [Action] recover_to_closest_boundary_point
Runs every tick. The action computes the closest point on the planned route that lies inside the geofence and commands a turn toward it, respecting minimum turn radius and airspeed constraints of the fixed-wing platform.
2. Safety — Battery RTB
The threshold is dynamic, not a fixed percentage.
battery_remaining_wh ≤ energy_to_exit_wh + reserve_wh
Where:
energy_to_exit_wh= f(distance_to_exit, ground_speed, headwind, altitude_delta, turn_penalties).reserve_wh= contingency_reserve + landing_reserve.
This is recalculated every tick based on current position, wind, and flight profile. As the UAV moves further from exit, the threshold rises. If the UAV is close to exit, more mission time is available.
3. Target Engagement — Priority
The selector tries artillery first. If no artillery, it tries tanks. If no tanks, it tries trucks/cars. This is structural priority — no scoring logic needed; the tree position defines it.
Each target sequence:
- Condition: detection model flags target type with confidence above threshold.
- Classify and confirm: zoom camera, run secondary classifier, verify.
- Orbit and track: enter loiter pattern around confirmed target, transmit coordinates and video.
A target lock timeout decorator wraps tracking actions. If the target is lost for T seconds, the action fails and the tree falls through to search.
4. Search Pattern — Area Sweep
The search subtree handles the default behaviour when no target is actively tracked.
Search Pattern [Sequence]
├── [Action] fly_search_legs ← follow planned lawnmower/sector pattern
└── Area Investigation [Selector] ← react to features found during sweep
├── Tree Row Investigation ...
├── Trench Investigation ...
└── [Action] continue_to_next_leg ← nothing interesting, keep sweeping
Search Pattern: Tree Row and Trench Investigation
This is a multi-phase investigation pattern that runs across both system zoom levels. The UAV flies a standard search pattern (lawnmower, expanding square, or sector scan) while in ZoomedOut. While flying, the detection model continuously analyses the camera feed. When it spots a feature of interest, the BT enters an investigation subtree which transitions to ZoomedIn and then deepens within it.
Note on nomenclature. The "Phase 1/2/3" below are investigation phases inside the BT, NOT the system-level zoom states. Phase 1 happens in
ZoomedOut; Phases 2 and 3 happen inZoomedIn(at medium and max zoom respectively).
Phase 1 — Feature Detection (zoom-out wide-area scan)
During normal search legs the detection model looks for:
- Tree rows (linear vegetation features that can conceal equipment).
- Trenches (linear earthwork features).
If detected, the corresponding investigation subtree activates.
Phase 2 — Tree Row Investigation (zoom-in, medium zoom)
investigate_tree_row [Sequence]
├── [Action] adjust_altitude_or_zoom_for_tree_row
├── [Action] fly_along_tree_row
└── Feature Scan [Selector]
├── Car Entrance Found [Sequence]
│ ├── [Condition] car_entrance_detected?
│ └── [SubTree] detailed_inspection
│
├── Tracks Found [Sequence]
│ ├── [Condition] car_or_truck_tracks_detected?
│ └── [SubTree] detailed_inspection
│
├── Caponier Found [Sequence]
│ ├── [Condition] caponier_detected?
│ └── [SubTree] detailed_inspection
│
├── Trash Found [Sequence]
│ ├── [Condition] trash_detected?
│ └── [SubTree] detailed_inspection
│
├── Military Vehicle Found [Sequence]
│ ├── [Condition] military_vehicle_detected?
│ └── [SubTree] detailed_inspection
│
├── Truck Found [Sequence]
│ ├── [Condition] truck_detected?
│ └── [SubTree] detailed_inspection
│
├── Car Found [Sequence]
│ ├── [Condition] car_detected?
│ └── [SubTree] detailed_inspection
│
└── [Action] mark_tree_row_clear_and_resume
The UAV adjusts camera zoom (or reduces altitude within safe bounds) and flies along the tree row length. The detection model scans for indicators of concealed activity.
Phase 3 — Detailed Inspection (zoom-in, max zoom)
detailed_inspection [Sequence]
├── [Action] zoom_to_max_or_descend
├── [Action] loiter_over_point_of_interest
├── [Action] run_high_res_classifier
├── Vehicle Classification [Selector]
│ ├── [Sequence]
│ │ ├── [Condition] is_tank_or_artillery?
│ │ └── [Action] flag_high_priority_target → feeds back to Target Engagement
│ │
│ ├── [Sequence]
│ │ ├── [Condition] is_truck?
│ │ └── [Action] flag_medium_priority_target
│ │
│ ├── [Sequence]
│ │ ├── [Condition] is_car?
│ │ └── [Action] flag_low_priority_target
│ │
│ └── [Action] log_evidence_and_resume
├── [Action] capture_snapshot_and_coordinates
└── [Action] transmit_report
When a high-priority target is confirmed at Phase 3, it is written to the blackboard. On the next tick, the root-level Target Engagement selector picks it up and takes over with orbit-and-track behaviour.
Investigation flow summary
Phase 1: Wide-area scan (ZoomedOut, search legs)
│
▼ tree row or trench detected
Phase 2: fly along feature (ZoomedIn, medium zoom)
│
▼ car entrance / tracks / caponier / trash / vehicle detected
Phase 3: loiter over point (ZoomedIn, max zoom), classify
│
▼ confirmed military target
Target Engagement takes over (orbit + track + report)
Additional Rules for Fixed-Wing Surveillance UAV
Already in the tree above
- Boundary geofence enforcement — continuous, highest priority.
- Dynamic battery RTB — position-aware energy calculation.
- Lost-link protocol — predefined safe route after comms timeout.
- No-fly zone avoidance — hard constraint, same tier as boundary.
- Weather/wind abort — return if wind exceeds platform limits.
- Emergency health monitor — critical failure triggers immediate RTB.
Additional considerations
- Airspace deconfliction — if ADS-B or transponder data shows traffic, execute avoidance manoeuvre before resuming mission.
- Sensor confidence gating — only promote a detection to target tracking when classification confidence exceeds a configurable threshold, reducing false positives.
- Target lock timeout — if a tracked target is lost from the sensor for T seconds, downgrade and return to search instead of orbiting indefinitely.
- Energy-aware search pattern — as battery depletes, shrink search sectors toward the exit point direction so RTB distance remains short.
- Minimum altitude floor — fixed-wing must maintain safe AGL altitude; the tree should prevent descent commands that violate this.
- Stall speed protection — if airspeed drops near stall threshold (e.g., strong headwind + slow manoeuvre), override current action and increase throttle / reduce bank angle.
- Camera gimbal limits — if the target moves beyond gimbal range, reposition the aircraft rather than losing tracking.
- Duplicate target suppression — if a target at location X was already reported and confirmed, do not re-enter full investigation; mark as known and continue search.
- Mission time limit — even if battery allows, enforce maximum mission duration for operational reasons (crew rotation, replanning windows).
YAML representation
tree_id: uav_surveillance_v2
version: 2
root: root_selector
tick_rate_hz: 10
blackboard_schema:
battery_remaining_wh: float
energy_to_exit_wh: float
reserve_wh: float
inside_boundary: bool
comms_active: bool
comms_lost_duration_sec: float
wind_speed_ms: float
critical_failure: bool
approaching_nfz: bool
target_artillery_detected: bool
target_tank_detected: bool
target_vehicle_detected: bool
tree_row_detected: bool
trench_detected: bool
car_entrance_detected: bool
car_or_truck_tracks_detected: bool
caponier_detected: bool
trash_detected: bool
military_vehicle_detected: bool
truck_detected: bool
car_detected: bool
current_altitude_agl_m: float
airspeed_ms: float
parameters:
comms_lost_timeout_sec: 10
target_lock_timeout_sec: 15
max_wind_speed_ms: 18
min_altitude_agl_m: 80
stall_speed_ms: 22
classification_confidence_threshold: 0.75
nodes:
root_selector:
type: Selector
children: [safety_selector, target_engagement_selector, search_sequence]
# --- Safety ---
safety_selector:
type: Selector
children:
- boundary_recovery_seq
- low_battery_seq
- lost_link_seq
- nfz_seq
- weather_seq
- emergency_seq
boundary_recovery_seq:
type: Sequence
children: [cond_outside_boundary, act_recover_to_boundary]
low_battery_seq:
type: Sequence
children: [cond_low_battery, act_return_to_exit]
lost_link_seq:
type: Sequence
children: [cond_comms_lost, act_lost_link_route]
nfz_seq:
type: Sequence
children: [cond_approaching_nfz, act_divert_nfz]
weather_seq:
type: Sequence
children: [cond_high_wind, act_return_to_exit]
emergency_seq:
type: Sequence
children: [cond_critical_failure, act_emergency_rtb]
# --- Target Engagement ---
target_engagement_selector:
type: Selector
children: [artillery_seq, tank_seq, vehicle_seq]
artillery_seq:
type: Sequence
children: [cond_artillery, act_classify_artillery, act_track_artillery]
tank_seq:
type: Sequence
children: [cond_tank, act_classify_tank, act_track_tank]
vehicle_seq:
type: Sequence
children: [cond_vehicle, act_classify_vehicle, act_track_vehicle]
# --- Search Pattern ---
search_sequence:
type: Sequence
children: [act_fly_search_legs, area_investigation_selector]
area_investigation_selector:
type: Selector
children: [tree_row_seq, trench_seq, act_continue_next_leg]
tree_row_seq:
type: Sequence
children: [cond_tree_row, subtree_investigate_tree_row]
trench_seq:
type: Sequence
children: [cond_trench, subtree_investigate_trench]
# subtree_investigate_trench mirrors subtree_investigate_tree_row structure
# (adjust_zoom → fly_along_trench → feature_scan_selector → detailed_inspection)
# --- Tree Row Investigation (Phase 2 — ZoomedIn, medium) ---
subtree_investigate_tree_row:
type: Sequence
children:
- act_zoom_medium
- act_fly_along_tree_row
- tree_row_feature_selector
tree_row_feature_selector:
type: Selector
children:
- car_entrance_seq
- tracks_seq
- caponier_seq
- trash_seq
- mil_vehicle_seq
- truck_seq
- car_seq
- act_mark_clear
car_entrance_seq:
type: Sequence
children: [cond_car_entrance, subtree_detailed_inspection]
tracks_seq:
type: Sequence
children: [cond_tracks, subtree_detailed_inspection]
caponier_seq:
type: Sequence
children: [cond_caponier, subtree_detailed_inspection]
trash_seq:
type: Sequence
children: [cond_trash, subtree_detailed_inspection]
mil_vehicle_seq:
type: Sequence
children: [cond_mil_vehicle, subtree_detailed_inspection]
truck_seq:
type: Sequence
children: [cond_truck_in_row, subtree_detailed_inspection]
car_seq:
type: Sequence
children: [cond_car_in_row, subtree_detailed_inspection]
# --- Detailed Inspection (Level 3) ---
subtree_detailed_inspection:
type: Sequence
children:
- act_zoom_max
- act_loiter_over_poi
- act_run_hires_classifier
- vehicle_class_selector
- act_capture_snapshot
- act_transmit_report
vehicle_class_selector:
type: Selector
children:
- high_priority_seq
- medium_priority_seq
- low_priority_seq
- act_log_evidence
high_priority_seq:
type: Sequence
children: [cond_is_tank_or_artillery, act_flag_high_priority]
medium_priority_seq:
type: Sequence
children: [cond_is_truck, act_flag_medium_priority]
low_priority_seq:
type: Sequence
children: [cond_is_car, act_flag_low_priority]
# --- Conditions ---
cond_outside_boundary:
type: Condition
eval: "not inside_boundary"
cond_low_battery:
type: Condition
eval: "battery_remaining_wh <= (energy_to_exit_wh + reserve_wh)"
cond_comms_lost:
type: Condition
eval: "comms_lost_duration_sec > comms_lost_timeout_sec"
cond_approaching_nfz:
type: Condition
eval: "approaching_nfz"
cond_high_wind:
type: Condition
eval: "wind_speed_ms > max_wind_speed_ms"
cond_critical_failure:
type: Condition
eval: "critical_failure"
cond_artillery:
type: Condition
eval: "target_artillery_detected"
cond_tank:
type: Condition
eval: "target_tank_detected"
cond_vehicle:
type: Condition
eval: "target_vehicle_detected"
cond_tree_row:
type: Condition
eval: "tree_row_detected"
cond_trench:
type: Condition
eval: "trench_detected"
cond_car_entrance:
type: Condition
eval: "car_entrance_detected"
cond_tracks:
type: Condition
eval: "car_or_truck_tracks_detected"
cond_caponier:
type: Condition
eval: "caponier_detected"
cond_trash:
type: Condition
eval: "trash_detected"
cond_mil_vehicle:
type: Condition
eval: "military_vehicle_detected"
cond_truck_in_row:
type: Condition
eval: "truck_detected"
cond_car_in_row:
type: Condition
eval: "car_detected"
cond_is_tank_or_artillery:
type: Condition
eval: "classified_type in ['tank', 'artillery']"
cond_is_truck:
type: Condition
eval: "classified_type == 'truck'"
cond_is_car:
type: Condition
eval: "classified_type == 'car'"
# --- Actions ---
act_recover_to_boundary:
type: Action
call: recover_to_closest_boundary_point
act_return_to_exit:
type: Action
call: return_to_exit_point
act_lost_link_route:
type: Action
call: execute_lost_link_route
act_divert_nfz:
type: Action
call: divert_around_nfz
act_emergency_rtb:
type: Action
call: emergency_rtb
act_classify_artillery:
type: Action
call: classify_confirm_artillery
act_track_artillery:
type: Action
call: orbit_and_track
params: { target_type: artillery }
act_classify_tank:
type: Action
call: classify_confirm_tank
act_track_tank:
type: Action
call: orbit_and_track
params: { target_type: tank }
act_classify_vehicle:
type: Action
call: classify_confirm_vehicle
act_track_vehicle:
type: Action
call: orbit_and_track
params: { target_type: vehicle }
act_fly_search_legs:
type: Action
call: fly_search_legs
act_continue_next_leg:
type: Action
call: continue_to_next_search_leg
act_zoom_medium:
type: Action
call: adjust_zoom_or_altitude
params: { level: medium }
act_fly_along_tree_row:
type: Action
call: fly_along_feature
act_mark_clear:
type: Action
call: mark_area_clear_resume_search
act_zoom_max:
type: Action
call: adjust_zoom_or_altitude
params: { level: max }
act_loiter_over_poi:
type: Action
call: loiter_over_point_of_interest
act_run_hires_classifier:
type: Action
call: run_high_resolution_classifier
act_flag_high_priority:
type: Action
call: flag_target
params: { priority: high }
act_flag_medium_priority:
type: Action
call: flag_target
params: { priority: medium }
act_flag_low_priority:
type: Action
call: flag_target
params: { priority: low }
act_log_evidence:
type: Action
call: log_evidence_and_resume
act_capture_snapshot:
type: Action
call: capture_snapshot_and_coordinates
act_transmit_report:
type: Action
call: transmit_report
How the tick cycle plays out — example scenarios
Scenario A: Normal search, nothing found
Tick 1: Safety → all pass → Target → none → Search → fly leg 3 → no features → continue
Tick 2: Safety → all pass → Target → none → Search → fly leg 3 → no features → continue
...
Scenario B: Tree row detected, tracks found, confirmed tank
Tick 40: Safety ✓ → Target: none → Search: tree_row_detected=true
→ zoom medium → fly along tree row
Tick 41: Safety ✓ → Target: none → Search: car_or_truck_tracks_detected=true
→ zoom max → loiter → run classifier → classified_type=tank
→ flag_high_priority → writes target_tank_detected to blackboard
Tick 42: Safety ✓ → Target: target_tank_detected=true
→ classify_confirm_tank → orbit_and_track (tank)
The tree row investigation naturally escalated into target tracking through the blackboard.
Scenario C: Battery drops during tracking
Tick 100: Safety: battery_remaining ≤ energy_to_exit + reserve → TRUE
→ return_to_exit_point (overrides active tank tracking)
Safety always wins. The UAV breaks off tracking and returns.
Scenario D: Boundary breach during search
Tick 55: Safety: outside_boundary=true → recover_to_closest_boundary_point
Tick 56: Safety: outside_boundary=true → still recovering (Running)
Tick 57: Safety: inside_boundary=true → pass → resume mission
Error scenarios
- Stale blackboard data — if the perception layer fails to update
*_detectedflags, conditions read stale values.scan_controllermust associate every blackboard write with a freshness timestamp; a stale flag must be treated asfalseafter a configurable TTL. - Action returning
Runningindefinitely — wrapped by a timeout decorator (target_lock_timeout_sec,comms_lost_timeout_sec, etc.). Timeout →Failure→ tree falls through to a lower-priority subtree. - Conflicting safety triggers — Safety subtree is a Selector: only the highest-priority active branch runs. Lower-priority conditions are still evaluated next tick; preemption is implicit.
- Concurrency violation between Tier 2 / VLM —
scan_controllerenforces sequential GPU use; any concurrent invocation is a logic bug and must alarm.
Data-flow table
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| perception layer (F1/F2/F3) → scan_controller | bboxes, motion candidates, Tier 2 evidence, VlmAssessment |
in-process | per-tick |
| scan_controller ↔ blackboard | typed state (battery, geofence, comms, target/feature flags) | in-process | per-tick |
| scan_controller → gimbal_controller | pan / tilt / zoom commands | in-process | per-action |
| scan_controller → mission_executor | route hints / middle-waypoint requests | in-process | per-decision |
| scan_controller → operator_bridge | POI surface / target-follow start-stop | in-process | per-event |
F5. Operator round trip
Entry: two parallel sub-flows: (a) the always-on data plane (camera + telemetry stream), and (b) the POI surface + operator response loop. Both flow through operator_bridge and telemetry_stream.
Narrative:
- Always-on stream (a).
telemetry_streamcontinuously pushes the camera feed and telemetry (UAV position, gimbal state, bbox overlay metadata) to the Ground Station API over modem. This stream is not detection-gated: the operator always sees the live feed even when no POI is queued. - POI surface (b). When
scan_controllerdecides to surface a POI,operator_bridgepackages the POI (class group, confidence, MGRS coordinate, snapshot URL, evidence including optionalVlmAssessment) and pushes it to the Ground Station as a typed event. - The Ground Station renders the POI in the operator UI alongside the live overlay. The operator chooses one of:
confirm→ target-follow / middle-waypoint,decline→ ignored-items,start-follow/release-follow, or no action (timeout). - Operator timeout scales with confidence — 40 % → 30 s, 100 % → 120 s, linearly. Timeout → POI is forgotten (not added to ignored-items). Decline → POI is appended to ignored-items via
mapobjects_store(see F7) so the same scene does not re-surface. - Confirm path branches on intent:
- Confirm as target →
scan_controllerenters target-follow mode (gimbal keeps target in centre 25 %); see F6 for middle-waypoint propagation. - Start-follow / release-follow →
scan_controllertoggles target-follow; UAV continues mission per F6.
- Confirm as target →
- The operator response is delivered to autopilot via the modem in the reverse direction;
operator_bridgevalidates the response payload (POI ID match, type-safe action enum) before forwarding toscan_controller.
Sequence diagram:
sequenceDiagram
participant SC as scan_controller
participant OB as operator_bridge
participant TS as telemetry_stream
participant GS as Ground Station API
participant OP as Operator browser
participant MO as mapobjects_store
par always-on stream
TS->>GS: camera + telemetry + bbox overlay (modem)
GS-->>OP: live feed render
and POI round trip
SC->>OB: surface POI (class, MGRS, conf, evidence)
OB->>GS: typed POI event
GS-->>OP: POI prompt
alt operator confirms
OP->>GS: confirm | start-follow | release-follow
GS->>OB: response
OB->>SC: action (validated)
else operator declines
OP->>GS: decline
GS->>OB: response
OB->>MO: append to ignored-items (F7)
OB->>SC: declined
else timeout (confidence-scaled)
SC->>SC: forget POI (no ignored-items append)
end
end
Error scenarios:
- Modem outage —
telemetry_streambuffers a bounded amount of recent stream; on reconnect the live feed resumes from current. Backlog beyond buffer is dropped (live operator value > completeness). Health surface reflects the outage. - POI response with mismatched POI ID —
operator_bridgerejects; logged. No state change. - Repeat-decline of the same scene — ignored-items deduplicates by
MGRS + class; subsequent identical declines are no-ops. - Operator no-response — confidence-scaled timeout fires; POI is forgotten (not declined). Distinguishes "operator chose to ignore" from "operator never saw it".
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| telemetry_stream → Ground Station | camera + telemetry + overlay | modem stream | continuous |
| operator_bridge → Ground Station | typed POI events | modem (event channel) | per-POI |
| Ground Station → operator_bridge | operator response | modem (response channel) | per-response |
| operator_bridge → scan_controller | validated action | in-process | per-response |
| operator_bridge → mapobjects_store | ignored-items append | in-process | per-decline |
F6. Mission lifecycle
Entry: mission_client pulls a mission from the external missions API at process start (and on configurable refresh).
Narrative:
mission_clientissuesGET /missions/{id}(or the equivalent canonical endpoint per../_docs/02_missions.md). It receives aMissionpayload conforming to the sharedmission-schemaartefact (Mission / Waypoint / Vehicle).mission_executorreceives the mission and selects the variant: multirotor or fixed-wing. The variant exclusively owns its state table (perarchitecture.md §7.7); the base coordinator does not contain variant-specific concepts.mission_executortranslates theMissionItems intoMissionWaypoints usable bymavlink_layer(the wire-level MAVLink contract). The translation contract is documented indata_model.md §Rewrite Entities > MissionItem vs MissionWaypoint.mavlink_layeruploads the mission to the autopilot (ArduPilot/PX4) via the hand-rolled MAVLink subset (~10–15 commands). Arming and takeoff use variant-specific paths:- Multirotor —
arm→takeoff→start_mission. - Plane —
upload_mission→wait_for_AUTO_mode→start_mission(arming and takeoff happen via RC AUTO mode in ArduPilot).
- Multirotor —
- Middle-waypoint POST on confirm. When the operator confirms a POI as a target (F5),
mission_executorrequestsmission_clientto POST a middle-waypoint insert against the missions API; on acceptance,mission_executorupdates the local mission and re-sends the affectedMissionWaypoints to ArduPilot viamavlink_layer. - On
mission_finishedcallback from ArduPilot,mission_executorissues the variant-appropriate landing sequence.
Sequence diagram:
sequenceDiagram
participant MC as mission_client
participant MIS as missions API
participant ME as mission_executor (multirotor | fixed-wing)
participant ML as mavlink_layer
participant AP as ArduPilot / PX4
MC->>MIS: GET /missions/{id}
MIS-->>MC: Mission payload
MC->>ME: deliver Mission
ME->>ME: translate to MissionWaypoint[]
ME->>ML: upload_mission
ML->>AP: MAVLink upload
alt multirotor
ME->>ML: arm / takeoff
ML->>AP: MAVLink arm / takeoff
else plane
ME->>ML: wait for AUTO mode
AP-->>ML: AUTO mode active
end
ME->>ML: start_mission
ML->>AP: MAVLink start
AP-->>ML: telemetry, mission progress
Note over ME: F5 confirm → middle-waypoint POST
ME->>MC: insert waypoint
MC->>MIS: POST middle-waypoint
MIS-->>MC: ack
MC->>ME: confirmed
ME->>ML: re-send affected waypoints
AP-->>ML: mission_finished
ML-->>ME: finished
ME->>ML: land
Error scenarios:
missionsAPI unreachable at startup →mission_clientretries with bounded backoff; surfaces health degradation; does not startmission_executoruntil a mission is loaded. No silent default-mission fallback.- Mission schema mismatch →
mission_clientrejects the payload with structured error and refuses to start. - MAVLink upload partial failure →
mavlink_layerreturns typed error;mission_executorretries per its variant policy or escalates. - Middle-waypoint POST rejected by missions API (validation, conflict) →
mission_executordoes NOT modify the local mission; surfaces the error to the operator viaoperator_bridge. The target-follow toggle is independent and continues regardless. mission_finishednever arrives → bounded watchdog. Variant policy decides whether to RTB or land in place.
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| missions API ↔ mission_client | Mission payload (mission-schema) |
HTTP REST | per-mission load + per-confirm POST |
| mission_client → mission_executor | Mission |
in-process | per-load |
| mission_executor → mavlink_layer | MissionWaypoint[], control commands |
in-process | per-upload + per-event |
| mavlink_layer ↔ ArduPilot | MAVLink v2 | UDP / serial | streaming |
| ArduPilot → mission_executor | telemetry + mission_finished |
callback chain | per-event |
F7. MapObjects + ignored-items
Entry: triggered by either (a) a new detection from F1, or (b) an operator decline from F5, or (c) a region-end sweep complete signal from scan_controller.
Narrative:
- On each new detection (gps, class, confidence, size):
- Compute H3 cell index at the chosen resolution (default
res 10≈ 15 m edge). - Build composite key =
H3_cell + class. - Query
grid_disk(H3_cell, k=2)to fetch all neighbouring cells (handles the H3 cell-boundary discontinuity). - For each neighbouring cell, look up objects in the same class group (configurable: e.g.
{military_vehicle, tank, artillery}collapse together). - Decision:
- Match within
distance_threshold(default 50 m) and position delta <move_threshold(default 10 m) →EXISTING(no update). - Match within
distance_thresholdand position delta ≥move_threshold→MOVED(update position + last_seen). - No match →
NEW(insert with H3 hash + MGRS key).
- Match within
- Compute H3 cell index at the chosen resolution (default
- On full region-sweep complete,
mapobjects_storediffs the previously-known set against the re-observed set in scanned cells. Unrevisited entries becomeREMOVEDcandidates and are surfaced (typically to the operator for confirmation rather than auto-purged, to avoid losing real but missed objects). - On operator decline of a POI (from F5): append
(MGRS, class, decline_time, operator_id)to the ignored-items list.scan_controllerconsults ignored-items before promoting any future detection that hits the sameMGRS + classkey, so declined scenes do not re-surface. - The 30 km broad-radius pre-filter is performed at a coarser H3 resolution (e.g.
res 4≈ 22 km edge) before the fine-grained k-ring query.
Sequence diagram:
sequenceDiagram
participant SC as scan_controller
participant DC as detection_client
participant MO as mapobjects_store
DC->>SC: bbox (class, conf, geometry, GPS)
SC->>MO: classify(detection)
MO->>MO: compute H3 cell
MO->>MO: grid_disk(k=2) lookup, class-group filter
alt match within distance_threshold AND delta < move_threshold
MO-->>SC: EXISTING (no update)
else match within distance_threshold AND delta ≥ move_threshold
MO->>MO: update position, last_seen
MO-->>SC: MOVED
else no match
MO->>MO: insert (H3 + MGRS + class)
MO-->>SC: NEW
end
Note over SC,MO: at region-end:
SC->>MO: region_sweep_complete(scanned_cells)
MO->>MO: diff observed vs known
MO-->>SC: REMOVED candidates
Note over SC,MO: on operator decline (F5):
SC->>MO: ignored_items_append(MGRS, class)
Error scenarios:
- Stale GPS / inaccurate MGRS →
mapobjects_storemay classify a true existing object asNEW(or vice versa). Mitigated by the k-ring widen and the configurable thresholds; persistent mismatches surface as duplicate-detection bursts and indicate a GPS-quality issue rather than a map bug. - Class confusion between similar primitives (e.g.
tree_blockvstree_row) → class-group configuration determines whether they collapse. Misconfiguration shows as ping-pong betweenMOVEDandNEWfor the same scene. - H3 cell-boundary discontinuity → already mitigated by k-ring query.
- Region-sweep
REMOVEDover-trigger when the operator re-routes mid-region → diff considers only cells that were fully scanned. Partial-scan cells are excluded fromREMOVEDcandidates. - Ignored-items unbounded growth → bounded by configurable retention policy (e.g. expire entries older than mission, or per-mission scope). Out of MVP scope to auto-purge by hard count.
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| scan_controller → mapobjects_store | detection (gps, class, conf, size) | in-process | per-detection |
| mapobjects_store → scan_controller | classification (EXISTING / MOVED / NEW / REMOVED) |
in-process | per-detection / per-region-end |
| scan_controller → mapobjects_store | region_sweep_complete(scanned_cells) |
in-process | per-region-end |
| operator_bridge → mapobjects_store | ignored-items append (MGRS, class) | in-process | per-decline |
F8. MapObjects sync (central DB, mission-bracketing)
Entry: this flow has two trigger points — pre-flight (after mission_client fetches the mission and before BIT can complete) and post-flight (after landing, RTL, or mission abort).
Narrative:
- Pre-flight pull (a). After
GET /missions/{id}succeeds,mission_clientissuesGET /missions/{id}/mapobjectsagainst the samemissionsAPI. The response is the central map-state for the mission's bounding box (perarchitecture.md §7.13). mission_clienthands the response tomapobjects_store, which hydratescurrent_state(keyed by(h3_cell, class_group)) andpending_ignored(any union-merged ignored items from prior missions in the same area).mapobjects_storereportssync_state = synced(orcached_fallbackif the central API was unreachable and the operator acknowledged continuing on cache, ordegradedif the cache was stale beyond the configurable freshness window).- In-flight (b).
mapobjects_storeis authoritative; every NEW / MOVED / EXISTING / REMOVED-CANDIDATE classification from F7 plus every IgnoredItem append from F5 is written topending_observations/pending_ignoredwithpending_upload = true. No central writes during flight (Frozen choice 6 — batched only for MVP). - Post-flight push (c). After
mission_executorreaches a terminal state (landed, RTL completed, or aborted), it triggersmission_clienttoPOST /missions/{id}/mapobjectswith the full pass diff andPOST /missions/{id}/mapobjects/ignoredwith any new declines. - The central
missionsAPI merges per its conflict-resolution rules (§7.13 — append-only observation log + computed current view). Acknowledgement clearspending_upload. - Push-failure persistence. If the central API is unreachable post-flight, the pending diff is kept on disk; bounded retry runs on a timer. After max retries, the operator is surfaced with a warning; the data is preserved for manual replay.
Sequence diagram:
sequenceDiagram
participant ME as mission_executor
participant MC as mission_client
participant MIS as missions API (central)
participant MO as mapobjects_store
participant SC as scan_controller
participant OP as operator (via operator_bridge)
Note over ME,MO: pre-flight (after mission GET)
MC->>MIS: GET /missions/{id}/mapobjects
alt 200 OK
MIS-->>MC: central map state
MC->>MO: hydrate
MO-->>SC: sync_state = synced
else unreachable / timeout
MC->>OP: surface BIT degradation
alt operator acknowledges cached fallback
MO-->>SC: sync_state = cached_fallback
else operator aborts
ME->>ME: BIT fail; do not arm
end
else 4xx
MC->>OP: surface error (mission ID / auth)
ME->>ME: BIT fail; do not arm
end
Note over MO: in-flight: pending_observations + pending_ignored grow
Note over MO: NO central writes during flight
Note over ME,MO: post-flight (terminal state reached)
ME->>MC: trigger upload
MC->>MIS: POST /missions/{id}/mapobjects (pass diff)
MC->>MIS: POST /missions/{id}/mapobjects/ignored (declines)
alt 200 OK
MIS-->>MC: ack
MC->>MO: clear pending_upload
MO-->>SC: sync_state = synced
else unreachable / 5xx
MC->>MC: persist pending diff on disk
MC->>MC: bounded retry (timer)
MC->>OP: surface warning after max retries
else 4xx
MC->>OP: surface rejection (full payload logged)
end
Error scenarios:
- Pre-flight pull unreachable → BIT degrades; operator must acknowledge cached fallback or abort. Never silent.
- Pre-flight pull returns stale cache only → surface freshness with the operator-acknowledgement prompt.
- In-flight crash before post-flight push → on next boot,
mapobjects_storefinds non-emptypending_observationsfor a mission that has terminated;mission_clientruns the post-flight push at startup before BIT completes for any new mission. - Post-flight push partial success (mapobjects 200 but ignored 5xx, or vice versa) → independent retry per endpoint; do not roll back the successful one.
- Mission deleted centrally between pre-flight pull and post-flight push (
DELETE /missions/{id}cascade hit while UAV was airborne) → post-flight POST returns 404; the on-device pending diff is logged as orphaned and retained for forensic review (operator decision on whether to discard). - Conflict at the central store (two UAVs reporting incompatible state) → not surfaced as an error to autopilot; the central API resolves per §7.13 conflict rules and returns 200 regardless.
Data-flow table:
| From → To | Payload | Channel | Lifecycle |
|---|---|---|---|
| mission_client → missions API | GET /missions/{id}/mapobjects |
HTTP REST | once per mission, pre-flight |
| missions API → mission_client | central map state (mapobjects + ignored) | HTTP REST | once per mission, pre-flight |
| mapobjects_store → scan_controller | sync_state (synced / cached_fallback / degraded) |
in-process | event |
| scan_controller → mapobjects_store | NEW / MOVED / EXISTING / REMOVED-CANDIDATE / IgnoredItem | in-process | per-detection / per-decline (in-flight) |
| mission_client → missions API | POST /missions/{id}/mapobjects (pass diff) |
HTTP REST | once per mission, post-flight (with retry) |
| mission_client → missions API | POST /missions/{id}/mapobjects/ignored |
HTTP REST | once per mission, post-flight |
F9. Pre-flight self-test (BIT)
Entry: mission_executor enters the BIT phase after mission_client completes the pre-flight pull (F8) — before ARMED (multirotor) or WAIT_AUTO (fixed-wing).
Narrative:
mission_executorevaluates a fixed checklist per dependency. Each item has three results:OK,DEGRADED(operator may acknowledge to continue),FAIL(must be resolved; BIT cannot pass).- Checklist items (in evaluation order):
- GPS lock (with configured min-satellite count and accuracy).
- Camera RTSP healthy (frames flowing within timeout; resolution + framerate as configured).
- Gimbal homed (yaw / pitch / zoom feedback within tolerance of last commanded).
../detectionsreachable + warmed (at least one round-trip frame succeeded).- VLM warm if
vlm_enabled(at least one structuredVlmAssessmentreturned during warmup). mission_clientmission loaded + schema-validated.mapobjects_storepre-flight pull complete (syncedorcached_fallbackafter operator acknowledgement;degradedis FAIL).- Persistent-store free space ≥ configured floor.
- Wall-clock bound to GPS or NTP within tolerance.
- MAVLink heartbeat + airframe health (battery, sensor health) within thresholds.
- The aggregate status flows to
operator_bridge→ Ground Station → operator UI as a structured BIT report. The operator may acknowledgeDEGRADEDitems individually (each acknowledgement is recorded with operator ID + timestamp). - On all items
OK(or DEGRADED-acknowledged),mission_executortransitions toARMED/WAIT_AUTO. On anyFAIL, the transition is blocked; the operator must resolve.
Sequence diagram:
sequenceDiagram
participant ME as mission_executor
participant H as health aggregator
participant DEPS as dependencies (frame_ingest, gimbal, mavlink, ../detections, vlm, mapobjects, mission_client, ...)
participant OB as operator_bridge
participant OP as operator UI
ME->>H: run_BIT()
H->>DEPS: snapshot health + functional probes
DEPS-->>H: per-dep status
H-->>ME: BIT report (item × {OK | DEGRADED | FAIL})
ME->>OB: surface BIT report
OB->>OP: BIT prompt
alt all OK
ME->>ME: transition to ARMED / WAIT_AUTO
else some DEGRADED
OP->>OB: acknowledge degraded items (signed)
OB->>ME: acknowledgement
ME->>ME: transition to ARMED / WAIT_AUTO
else any FAIL
ME->>ME: hold; no transition
end
Error scenarios:
- BIT report contains a FAIL → no transition; operator must investigate.
- Operator acknowledges a DEGRADED item that should be FAIL → not allowed by
operator_bridgevalidation; any such request is rejected. - Health flips during BIT → BIT is re-run from the start; partial acknowledgements are invalidated.
F10. Lost-link failsafe ladder
Entry: mission_executor continuously evaluates the operator/Ground-Station modem link per the ladder defined in architecture.md §7.7.
Narrative:
- Every tick,
mission_executorreadslast_operator_heartbeat_tsand computes the current rung:LinkOk,LinkDegraded,LinkLost, orLinkLostInFollow. LinkOk(last heartbeat ≤ 5 s) — no behavioural change. Mission continues as planned.LinkDegraded(5 s < last heartbeat ≤ 30 s) — surface health → yellow; queue all POI surface-events for replay-on-recovery (do not drop them; the operator may still see them on reconnect within the grace window).LinkLost(last heartbeat > 30 s, and target-follow inactive) — trigger RTL viaMAV_CMD_NAV_RETURN_TO_LAUNCH; log mission abort with reason; continue logging the mission diff tomapobjects_storeso post-flight push (F8) can succeed when the link recovers (or eventually, after landing, on cellular/Wi-Fi at the home base).LinkLostInFollow(last heartbeat > 30 s, in target-follow) — extend grace by 30 s (operator may have momentarily lost link during a confirmed engagement); on grace expiry, fall through toLinkLost.- MAVLink-link loss to ArduPilot/PX4 is a separate, more severe event:
mission_executorcannot command the airframe at all. Health flips to red; the airframe's own MAVLink failsafe (configured in ArduPilot/PX4) takes over. We do NOT override the airframe failsafe.
Sequence diagram:
sequenceDiagram
participant TICK as tick (10 Hz)
participant ME as mission_executor
participant ML as mavlink_layer
participant AP as ArduPilot / PX4
participant OB as operator_bridge
participant SC as scan_controller
loop every tick
TICK->>ME: tick
ME->>OB: read last_operator_heartbeat_ts
alt LinkOk (≤ 5 s)
ME->>ME: continue mission
else LinkDegraded (5–30 s)
ME->>ME: surface health → yellow
ME->>OB: queue POI surface-events
else LinkLost (> 30 s, no follow)
ME->>ML: MAV_CMD_NAV_RETURN_TO_LAUNCH
ML->>AP: send
AP-->>ML: ack
ME->>SC: notify mission abort (reason: lost_link)
else LinkLostInFollow
ME->>ME: extend grace 30 s
alt grace expires
ME->>ML: MAV_CMD_NAV_RETURN_TO_LAUNCH
ME->>SC: exit target-follow; mission abort
end
end
end
Note over ME,AP: MAVLink link loss is separate
alt MAVLink heartbeat lost > timeout
ME->>ME: health → red
Note over AP: ArduPilot's own failsafe takes over
end
Error scenarios:
- Heartbeat clock skew → governed by the wall-clock policy in
architecture.md §7.3 Reliability and safety. Drift > 200 ms surfaces health yellow; the lost-link ladder uses monotonic timing only. - Operator acks a POI during
LinkDegraded→ ack is processed normally on receipt; the queue replays any unsent events. Sequence numbers prevent reordering. - RTL refused by ArduPilot (mode lock, geofence anomaly) → bounded retry; if RTL is blocked persistently, escalate to land-now. Health → red.
- Operator deliberately suppresses lost-link RTL (signed override) → permitted only via signed command (Q9); recorded in audit log with operator ID and rationale.
Cross-flow notes
- Single Rust process, single BT. All ten flows run inside the same autopilot binary. Cross-flow state transitions go through
scan_controller's behaviour tree (F4); there is no per-flow private state machine that can drift out of sync with the others. - Evidence → decision → action. F1 (frames + Tier 1), F2 (movement candidates at zoom-out and zoom-in), and F3 (VLM) produce evidence. F4 consumes evidence and decides. F5 (operator round trip), F6 (mission lifecycle), F7 (MapObjects + ignored items in-flight), F8 (MapObjects sync at mission boundaries), F9 (pre-flight BIT), and F10 (lost-link ladder) are the action-side consequences of F4 decisions and the surrounding mission lifecycle.
- POI lifecycle is end-to-end. A POI is born in F1/F2, scored by F3, queued and surfaced through F4 → F5, and ends in either F6 (middle-waypoint insert on confirm) or F7 (ignored-item append on decline) or simply expires (timeout). F8 ensures the F7 outcomes survive across missions (central observation log + ignored-items merge). The state machine in F4 is the single owner of the POI's transitions.
- Telemetry plane is parallel, not serial.
telemetry_stream(referenced from every other flow) runs continuously and is independent of detection state. The operator always has the live feed; F5 only adds POI-specific overlays on top. - Hard cap is enforced once. The ≤5 POIs/min operator-review cap lives only in F4. Other flows produce as many candidates as they can; F4 decides which ever surface to F5.
- Mission lifecycle bookends. F9 (pre-flight BIT) gates entry into the operational state machine. F8 (MapObjects pre-flight pull) is a BIT input; F8 (post-flight push) is part of the post-mission cleanup (whether mission completed normally, was RTL'd by F10, or was aborted by operator). A mission's data integrity centrally depends on F8 succeeding eventually — even for crashed UAVs, the on-device pending diff is durable so a recovered airframe can replay.
- Movement detection is dual-zoom in MVP. F2 covers both zoom-out (well-validated, classical OpenCV) and zoom-in (benchmark-gated; see Q14). The zoom-in scope expands the set of POIs the system can produce; the ≤5 POIs/min cap in F4 absorbs the additional load by deprioritising rather than dropping.