# Component — `scan_controller`

**Layer**: Decision + Memory
**Status**: forward-looking design (Rust)

## 1. Purpose

The system's brain. A deterministic typed state machine — `ZoomedOut`, `ZoomedIn { roi, hold_started_at }`, and `TargetFollow { target_id, started_at }`. Owns the POI queue, timeouts, the ≤5 POIs/min operator-review cap, the confidence-scaled operator-decision window, gimbal command issuance, and the new/moved/existing/removed dispatch into `mapobjects_store`.

The full behaviour-tree spec — including tick scenarios and the 15 fixed-wing rules — lives in `system-flows.md §F4`.

## 2. Inputs

| Input | Source | Cadence | Notes |
|---|---|---|---|
| `DetectionBatch` | `detection_client` | per frame | Tier 1 primitives. |
| `MovementCandidate` | `movement_detector` | per frame at both zoom-out and zoom-in (suppressed only during `TargetFollow`) | Each candidate carries `source_zoom_band`. |
| `Tier2Evidence` | `semantic_analyzer` | per zoom-in hold | Path / endpoint / concealment scoring. |
| `VlmAssessment` | `vlm_client` (optional) | per zoom-in endpoint hold | `status: disabled` if VLM is off. |
| Operator commands | `operator_bridge` | event | confirm / decline / target-follow start / target-follow release. Authenticated, signed, replay-protected upstream of this component. |
| UAV telemetry | `mavlink_layer` (via `mission_executor`) | 10 Hz target | Position used for proximity-weighted POI priority and middle-waypoint computation. |
| Mission state | `mission_executor` | event | Current waypoint, mission progress; used for sweep-vs-route alignment. |
| MapObjects sync state | `mapobjects_store` | event at startup + post-flight | `synced` / `cached_fallback` / `degraded` — surfaces a health flag and (for `degraded`) suppresses MapObject diff classifications until corrected. |

## 3. Outputs

| Output | Consumer | Shape |
|---|---|---|
| `GimbalCommand` (yaw / pitch / zoom) | `gimbal_controller` | per state-machine tick or per zoom-in plan step |
| `POI` to operator | `operator_bridge` (then `telemetry_stream`) | enqueue / dequeue events |
| Middle-waypoint hint | `mission_executor` | event on operator-confirmed target |
| MapObjects update | `mapobjects_store` | new / moved / existing / removed dispatch |
| Health metric | health aggregator | `state`, `pois_in_queue`, `pois_per_min`, `tick_latency_p99`, `last_state_change_ts`, `mapobjects_sync_state`. |

## 4. Key Responsibilities

- Run the `ZoomedOut` / `ZoomedIn` / `TargetFollow` state machine. Transitions are explicit, typed, and fully enumerated; no ad-hoc booleans.
- Maintain the POI queue ordered by `confidence × proximity_to_current_camera × age_factor`. Hard-cap output to ≤5 POIs/min surfaced to the operator.
- Apply the confidence-scaled operator decision window (40 % → 30 s, 100 % → 120 s, linear; below 40 % the POI is not surfaced). Timeout = forget; decline = `IgnoredItem` entry via `mapobjects_store`.
- Suppress new POIs whose `(MGRS, class_group)` matches an existing `IgnoredItem`.
- For each new detection or movement candidate: compute the H3 cell, ask `mapobjects_store` to classify as new / moved / existing, and only surface non-existing entries.
- **Zoom-in candidate handling.** When a `MovementCandidate` arrives with `source_zoom_band = zoomed_in`, evaluate against the current ROI: if inside, bump current-ROI confidence; if outside the ROI but inside the broader zoomed FOV, enqueue as a candidate-POI; only interrupt the current zoom-in hold if the candidate's priority exceeds the current hold's priority.
- On operator confirmation: hand a middle-waypoint hint to `mission_executor`, transition to `TargetFollow`, and command `gimbal_controller` to keep the target in the centre 25 % of frame.
- On operator decline / timeout / target loss: append (decline only) an `IgnoredItem` and return to `ZoomedOut`.
- On `mapobjects_store` reporting `sync_state = degraded`, surface health → red and **do not** classify new detections (avoid corrupting the central observation log on next push); continue to surface POIs to the operator on Tier-1 + movement evidence alone.

## 5. Internal State

The state machine lives entirely in this component. State variables:

- Current state: `ZoomedOut | ZoomedIn { roi, hold_started_at } | TargetFollow { target_id, started_at }`.
- POI queue: ordered, with per-entry priority and queue position.
- Per-class operator-decision-window thresholds.
- Last-N tick timestamps for tick-latency observability.
- Frame-rate floor monitor: when sustained FPS < 10, suppress `ZoomedOut → ZoomedIn` transitions and surface health → yellow.

State is in-process only; restart starts in `ZoomedOut` with an empty queue.

## 6. Failure Modes

| Failure | Detection | Behaviour |
|---|---|---|
| `detection_client` health red | health input | Continue zoom-out sweep; emit no new POIs from Tier 1; movement candidates still flow. |
| `movement_detector` health red | health input | Continue; lose movement-candidate enqueueing. |
| `semantic_analyzer` health red | health input | Skip Tier 2; surface POIs with Tier-1-only evidence; flag in operator overlay. |
| `vlm_client` returns `status: disabled \| timeout \| ipc_error \| schema_invalid` | per-call status | Surface POI without VLM evidence (fail-closed). |
| `gimbal_controller` not ready | health input | Stay in current state; alert; do not silently drop scan steps. |
| `operator_bridge` disconnected | health input | Continue zoom-out (operator UI is unreachable, but the system must not crash); pause POI surfacing; resume on reconnect. F10 lost-link ladder owns the larger response. |
| `mapobjects_store` sync degraded | sync_state input | Suppress diff classifications; surface POIs on Tier-1 + movement only; health → red. |
| Sustained FPS < 10 | self-instrumented | Suppress zoom-in transitions; health → yellow. |
| Tick-latency above budget | self-instrumented | Health → yellow; investigate (likely upstream consumer slowness). |

## 7. Dependencies

**In-process** (input): `detection_client`, `movement_detector`, `semantic_analyzer`, `vlm_client`, `operator_bridge`, `mission_executor`, `mapobjects_store`.
**In-process** (output): `gimbal_controller`, `operator_bridge`, `mission_executor`, `mapobjects_store`.

**External**: none directly. All external integrations are mediated by other components.

## 8. Non-Functional Targets

| Concern | Target |
|---|---|
| Tick latency | ≤10 ms p99 |
| POI enqueue → operator surface | ≤1 s in normal load |
| POI rate to operator | ≤5 POIs/min (hard cap) |
| Zoom-out → zoom-in transition | ≤2 s including physical zoom |
| Zoom-in hold duration | configurable; default 5 s/POI |
| Target-follow centre-window | target inside centre 25 % of frame while visible |
| Frame-rate floor | ≥10 fps sustained; below this, suppress zoom-in transitions |

## 9. References

- `architecture.md §3`, `§5 Architectural Principles`, `§6 NFR`, `§7.6 Scan controller and POI queue`, `§7.12 New vs Existing / Moved / Removed Object Detection`, `§7.13 MapObjects Sync`.
- `system-flows.md §F4 Scan controller behaviour tree` (full BT spec, tick scenarios, 15 fixed-wing rules).
- `data_model.md §POI`, `§IgnoredItem`, `§MapObject`.
