mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 15:51:10 +00:00
bc40ea7300
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>
1392 lines
66 KiB
Markdown
1392 lines
66 KiB
Markdown
# 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**:
|
||
|
||
1. `frame_ingest` decodes 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).
|
||
2. Each frame is forwarded to `detection_client`, which streams it on a bi-directional gRPC channel to the external `../detections` service. `../detections` returns a list of normalized bboxes (class, confidence, geometry).
|
||
3. 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 when `scan_controller` is in `ZoomedIn`; it consumes the cropped ROI and returns Tier 2 evidence (path freshness, endpoint scoring, concealment score).
|
||
- `vlm_client` — only when `scan_controller` requests zoom-in confirmation **and** the VLM optionality flag is enabled; see F3.
|
||
4. `scan_controller` emits 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**:
|
||
|
||
```mermaid
|
||
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_client` reconnects 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_ingest` flags the frame; `movement_detector` (F2) refuses to consume it; `scan_controller` still receives bboxes but with the skew flag set.
|
||
- **Bbox payload schema-invalid** → `detection_client` rejects the message and logs structured error; no silent swallow.
|
||
- **Tier 2 ROI inference fails** → `semantic_analyzer` returns a typed error; `scan_controller` treats the ROI as inconclusive and proceeds per the configured Tier-2-failure policy (default: hold for VLM if enabled, else release POI with `inconclusive`).
|
||
|
||
**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**:
|
||
|
||
1. `movement_detector` consumes 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).
|
||
2. 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.
|
||
3. 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_band` enum (`zoomed_out` | `zoomed_in`).
|
||
4. 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.
|
||
5. 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.
|
||
6. **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_controller` decides whether to interrupt the current hold (only for higher-priority candidates).
|
||
7. 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**:
|
||
|
||
```mermaid
|
||
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_detector` skips 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_controller` returns 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**:
|
||
|
||
1. `scan_controller` requests confirmation for one bounded ROI crop. It hands the ROI plus context (POI class group, confidence, prior Tier 2 evidence) to `vlm_client`.
|
||
2. `vlm_client` validates 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.
|
||
3. The local VLM responds; `vlm_client` validates the answer against the structured `VlmAssessment` schema (label enum, confidence, evidence spans, reason, status). Free-form text is not a downstream API.
|
||
4. **Success path**: `vlm_client` emits the validated `VlmAssessment` back to `scan_controller`. `scan_controller` integrates it with Tier 2 evidence and decides whether to surface the POI to the operator (via `operator_bridge`), release the POI, or hold target-follow.
|
||
5. **Fail-closed paths** — if any of the following occur, `vlm_client` returns a `VlmAssessment` with `status` set accordingly and `label = 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_controller` MUST 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.
|
||
6. **VLM-disabled alt path**: when `vlm_enabled == false` (benchmark gate failed, or build-time feature module is absent), `scan_controller` skips 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 with `vlm_status: disabled` so the UI can render the source-of-evidence indicator correctly.
|
||
|
||
**Sequence diagram**:
|
||
|
||
```mermaid
|
||
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_controller` enforces no concurrent execution; any concurrency violation is a logic bug and must be alarmed, not silently retried.
|
||
- **Repeated `ipc_error` in short window** → `vlm_client` raises a structured health alert (does not silently disable VLM). The operator-facing health surface reflects degraded VLM availability; `scan_controller` continues 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 a `confirmed` label.
|
||
|
||
**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
|
||
|
||
```text
|
||
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)
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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.
|
||
|
||
```text
|
||
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:
|
||
|
||
1. Condition: detection model flags target type with confidence above threshold.
|
||
2. Classify and confirm: zoom camera, run secondary classifier, verify.
|
||
3. 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.
|
||
|
||
```text
|
||
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 in `ZoomedIn` (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)
|
||
|
||
```text
|
||
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)
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
1. **Boundary geofence enforcement** — continuous, highest priority.
|
||
2. **Dynamic battery RTB** — position-aware energy calculation.
|
||
3. **Lost-link protocol** — predefined safe route after comms timeout.
|
||
4. **No-fly zone avoidance** — hard constraint, same tier as boundary.
|
||
5. **Weather/wind abort** — return if wind exceeds platform limits.
|
||
6. **Emergency health monitor** — critical failure triggers immediate RTB.
|
||
|
||
#### Additional considerations
|
||
|
||
7. **Airspace deconfliction** — if ADS-B or transponder data shows traffic, execute avoidance manoeuvre before resuming mission.
|
||
8. **Sensor confidence gating** — only promote a detection to target tracking when classification confidence exceeds a configurable threshold, reducing false positives.
|
||
9. **Target lock timeout** — if a tracked target is lost from the sensor for T seconds, downgrade and return to search instead of orbiting indefinitely.
|
||
10. **Energy-aware search pattern** — as battery depletes, shrink search sectors toward the exit point direction so RTB distance remains short.
|
||
11. **Minimum altitude floor** — fixed-wing must maintain safe AGL altitude; the tree should prevent descent commands that violate this.
|
||
12. **Stall speed protection** — if airspeed drops near stall threshold (e.g., strong headwind + slow manoeuvre), override current action and increase throttle / reduce bank angle.
|
||
13. **Camera gimbal limits** — if the target moves beyond gimbal range, reposition the aircraft rather than losing tracking.
|
||
14. **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.
|
||
15. **Mission time limit** — even if battery allows, enforce maximum mission duration for operational reasons (crew rotation, replanning windows).
|
||
|
||
### YAML representation
|
||
|
||
```yaml
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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 `*_detected` flags, conditions read stale values. `scan_controller` must associate every blackboard write with a freshness timestamp; a stale flag must be treated as `false` after a configurable TTL.
|
||
- **Action returning `Running` indefinitely** — 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_controller` enforces 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**:
|
||
|
||
1. **Always-on stream** (a). `telemetry_stream` continuously 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.
|
||
2. **POI surface** (b). When `scan_controller` decides to surface a POI, `operator_bridge` packages the POI (class group, confidence, MGRS coordinate, snapshot URL, evidence including optional `VlmAssessment`) and pushes it to the Ground Station as a typed event.
|
||
3. 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).
|
||
4. **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.
|
||
5. **Confirm path** branches on intent:
|
||
- **Confirm as target** → `scan_controller` enters target-follow mode (gimbal keeps target in centre 25 %); see F6 for middle-waypoint propagation.
|
||
- **Start-follow / release-follow** → `scan_controller` toggles target-follow; UAV continues mission per F6.
|
||
6. The operator response is delivered to autopilot via the modem in the reverse direction; `operator_bridge` validates the response payload (POI ID match, type-safe action enum) before forwarding to `scan_controller`.
|
||
|
||
**Sequence diagram**:
|
||
|
||
```mermaid
|
||
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_stream` buffers 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_bridge` rejects; 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**:
|
||
|
||
1. `mission_client` issues `GET /missions/{id}` (or the equivalent canonical endpoint per `../_docs/02_missions.md`). It receives a `Mission` payload conforming to the shared `mission-schema` artefact (Mission / Waypoint / Vehicle).
|
||
2. `mission_executor` receives the mission and selects the variant: **multirotor** or **fixed-wing**. The variant exclusively owns its state table (per `architecture.md §7.7`); the base coordinator does not contain variant-specific concepts.
|
||
3. `mission_executor` translates the `MissionItem`s into `MissionWaypoint`s usable by `mavlink_layer` (the wire-level MAVLink contract). The translation contract is documented in `data_model.md §Rewrite Entities > MissionItem vs MissionWaypoint`.
|
||
4. `mavlink_layer` uploads 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).
|
||
5. **Middle-waypoint POST on confirm**. When the operator confirms a POI as a target (F5), `mission_executor` requests `mission_client` to POST a middle-waypoint insert against the missions API; on acceptance, `mission_executor` updates the local mission and re-sends the affected `MissionWaypoint`s to ArduPilot via `mavlink_layer`.
|
||
6. On `mission_finished` callback from ArduPilot, `mission_executor` issues the variant-appropriate landing sequence.
|
||
|
||
**Sequence diagram**:
|
||
|
||
```mermaid
|
||
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**:
|
||
|
||
- **`missions` API unreachable** at startup → `mission_client` retries with bounded backoff; surfaces health degradation; does not start `mission_executor` until a mission is loaded. No silent default-mission fallback.
|
||
- **Mission schema mismatch** → `mission_client` rejects the payload with structured error and refuses to start.
|
||
- **MAVLink upload partial failure** → `mavlink_layer` returns typed error; `mission_executor` retries per its variant policy or escalates.
|
||
- **Middle-waypoint POST rejected** by missions API (validation, conflict) → `mission_executor` does NOT modify the local mission; surfaces the error to the operator via `operator_bridge`. The target-follow toggle is independent and continues regardless.
|
||
- **`mission_finished` never 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**:
|
||
|
||
1. **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_threshold` **and** position delta ≥ `move_threshold` → `MOVED` (update position + last_seen).
|
||
- No match → `NEW` (insert with H3 hash + MGRS key).
|
||
2. **On full region-sweep complete**, `mapobjects_store` diffs the previously-known set against the re-observed set in scanned cells. Unrevisited entries become `REMOVED` candidates and are surfaced (typically to the operator for confirmation rather than auto-purged, to avoid losing real but missed objects).
|
||
3. **On operator decline of a POI** (from F5): append `(MGRS, class, decline_time, operator_id)` to the **ignored-items** list. `scan_controller` consults ignored-items before promoting any future detection that hits the same `MGRS + class` key, so declined scenes do not re-surface.
|
||
4. 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**:
|
||
|
||
```mermaid
|
||
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_store` may classify a true existing object as `NEW` (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_block` vs `tree_row`) → class-group configuration determines whether they collapse. Misconfiguration shows as ping-pong between `MOVED` and `NEW` for the same scene.
|
||
- **H3 cell-boundary discontinuity** → already mitigated by k-ring query.
|
||
- **Region-sweep `REMOVED` over-trigger** when the operator re-routes mid-region → diff considers only cells that were fully scanned. Partial-scan cells are excluded from `REMOVED` candidates.
|
||
- **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**:
|
||
|
||
1. **Pre-flight pull** (a). After `GET /missions/{id}` succeeds, `mission_client` issues `GET /missions/{id}/mapobjects` against the same `missions` API. The response is the central map-state for the mission's bounding box (per `architecture.md §7.13`).
|
||
2. `mission_client` hands the response to `mapobjects_store`, which hydrates `current_state` (keyed by `(h3_cell, class_group)`) and `pending_ignored` (any union-merged ignored items from prior missions in the same area).
|
||
3. `mapobjects_store` reports `sync_state = synced` (or `cached_fallback` if the central API was unreachable and the operator acknowledged continuing on cache, or `degraded` if the cache was stale beyond the configurable freshness window).
|
||
4. **In-flight** (b). `mapobjects_store` is authoritative; every NEW / MOVED / EXISTING / REMOVED-CANDIDATE classification from F7 plus every IgnoredItem append from F5 is written to `pending_observations` / `pending_ignored` with `pending_upload = true`. **No central writes during flight** (Frozen choice 6 — batched only for MVP).
|
||
5. **Post-flight push** (c). After `mission_executor` reaches a terminal state (landed, RTL completed, or aborted), it triggers `mission_client` to `POST /missions/{id}/mapobjects` with the full pass diff and `POST /missions/{id}/mapobjects/ignored` with any new declines.
|
||
6. The central `missions` API merges per its conflict-resolution rules (§7.13 — append-only observation log + computed current view). Acknowledgement clears `pending_upload`.
|
||
7. **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**:
|
||
|
||
```mermaid
|
||
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_store` finds non-empty `pending_observations` for a mission that has terminated; `mission_client` runs 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**:
|
||
|
||
1. `mission_executor` evaluates a fixed checklist per dependency. Each item has three results: `OK`, `DEGRADED` (operator may acknowledge to continue), `FAIL` (must be resolved; BIT cannot pass).
|
||
2. **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).
|
||
- `../detections` reachable + warmed (at least one round-trip frame succeeded).
|
||
- VLM warm if `vlm_enabled` (at least one structured `VlmAssessment` returned during warmup).
|
||
- `mission_client` mission loaded + schema-validated.
|
||
- `mapobjects_store` pre-flight pull complete (`synced` or `cached_fallback` after operator acknowledgement; `degraded` is 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.
|
||
3. The aggregate status flows to `operator_bridge` → Ground Station → operator UI as a structured BIT report. The operator may acknowledge `DEGRADED` items individually (each acknowledgement is recorded with operator ID + timestamp).
|
||
4. On all items `OK` (or DEGRADED-acknowledged), `mission_executor` transitions to `ARMED` / `WAIT_AUTO`. On any `FAIL`, the transition is blocked; the operator must resolve.
|
||
|
||
**Sequence diagram**:
|
||
|
||
```mermaid
|
||
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_bridge` validation; 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**:
|
||
|
||
1. Every tick, `mission_executor` reads `last_operator_heartbeat_ts` and computes the current rung: `LinkOk`, `LinkDegraded`, `LinkLost`, or `LinkLostInFollow`.
|
||
2. **`LinkOk`** (last heartbeat ≤ 5 s) — no behavioural change. Mission continues as planned.
|
||
3. **`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).
|
||
4. **`LinkLost`** (last heartbeat > 30 s, and target-follow inactive) — trigger RTL via `MAV_CMD_NAV_RETURN_TO_LAUNCH`; log mission abort with reason; continue logging the mission diff to `mapobjects_store` so post-flight push (F8) can succeed when the link recovers (or eventually, after landing, on cellular/Wi-Fi at the home base).
|
||
5. **`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 to `LinkLost`.
|
||
6. **MAVLink-link loss to ArduPilot/PX4** is a separate, more severe event: `mission_executor` cannot 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**:
|
||
|
||
```mermaid
|
||
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.
|