# C5 AC-5.2 fallback — 3 s no-estimate detector + signal emission

**Task**: AZ-388_c5_ac52_fallback
**Name**: C5 AC-5.2 fallback path — 3 s no-estimate detector + downstream signal
**Description**: Implement the AC-5.2 contract: when `current_estimate()` cannot return a fresh `EstimatorOutput` for ≥3 s (config `state.no_estimate_fallback_s`, default 3.0) — either because every call raised `EstimatorFatalError` OR the keyframe window has been empty — emit ONE downstream signal `kind="c5.state.no_estimate_fallback_engaged"` to FDR + GCS STATUSTEXT (severity CRITICAL) instructing C8 outbound to switch to FC IMU-only emission. The signal is emitted ONCE per fallback engagement (rate-limited by an `_in_fallback` boolean); on recovery (a fresh successful `current_estimate()`), emit ONE recovery signal `kind="c5.state.no_estimate_fallback_recovered"`. Add a private rolling counter `_last_successful_estimate_ns`; check at the top of every `current_estimate()` call AND on a separate watchdog tick (driven by C8 outbound's 5 Hz call cadence; the watchdog is implemented as a method `check_fallback_state(now_ns) -> bool` returning the current fallback state).
**Complexity**: 3 points
**Dependencies**: AZ-384 (`current_estimate` body), AZ-386 (same hook for ESKF), AZ-273 (FDR), AZ-272, AZ-390 (E-C8 — `GcsAdapter` Protocol surface), AZ-397 (E-C8 — `QgcTelemetryAdapter` concrete STATUSTEXT broadcast), AZ-263, AZ-269, AZ-266
**Component**: c5_state (epic AZ-260 / E-C5)
**Tracker**: AZ-388
**Epic**: AZ-260 (E-C5)

### Document Dependencies

- `_docs/02_document/contracts/c5_state/state_estimator_protocol.md` — Invariant 9.
- `_docs/02_document/components/07_c5_state/description.md` — § 5 (AC-5.2 fallback).

## Problem

Without this task, a sustained iSAM2 numerical failure or empty keyframe window would leave the system silently emitting stale or no estimates; C8 outbound would have no signal to switch to FC IMU-only; FC would receive degraded-quality estimates indefinitely.

## Outcome

- `_last_successful_estimate_ns` private counter (set on every successful `current_estimate()`).
- `_in_fallback` boolean (latched on engagement, cleared on recovery).
- Hook in `current_estimate()` (both estimators):
  - On entry: if `now_ns - _last_successful_estimate_ns > no_estimate_fallback_s * 1e9` AND not `_in_fallback` → engage fallback (emit signal, set flag, log).
  - On successful return: if `_in_fallback` → emit recovery signal, clear flag, log.
- New public method `check_fallback_state(now_ns) -> bool`: idempotent watchdog check returning the current fallback state. C8 outbound calls this at its 5 Hz cadence to drive the FC IMU-only switch even when no `current_estimate()` is being called.
- Engagement signal: FDR `kind="c5.state.no_estimate_fallback_engaged"` + GCS STATUSTEXT (CRITICAL severity, message "Onboard estimator lost; FC IMU-only").
- Recovery signal: FDR `kind="c5.state.no_estimate_fallback_recovered"` + GCS STATUSTEXT (NOTICE severity).
- Configurable threshold; AC-5.2 default 3.0 s.
- Unit tests: 3 s no estimate → engagement signal fires once; sustained no-estimate over 30 s → still ONE engagement signal (rate-limited); successful estimate after engagement → recovery signal fires once; `check_fallback_state` returns correct state from external watchdog.

## Scope

### Included
- `_last_successful_estimate_ns` counter + `_in_fallback` flag.
- Hooks in both estimators' `current_estimate`.
- Public `check_fallback_state(now_ns)` watchdog API.
- Engagement + recovery signal emission.
- Rate-limiting (one signal per state transition).
- Unit tests across both estimators.

### Excluded
- The actual FC IMU-only emission — owned by AZ-261 (C8 outbound).
- C5-IT-05 component-internal acceptance test — deferred to E-BBT.

## Acceptance Criteria

**AC-1: Engagement after 3 s of no estimate** — synthetic timeline with no successful `current_estimate` for 3.5 s → ONE engagement signal fires.

**AC-2: Engagement is one-shot** — sustained 30 s no-estimate → still ONE engagement signal (rate-limited).

**AC-3: Recovery signal** — after engagement, a successful `current_estimate` → ONE recovery signal.

**AC-4: `check_fallback_state` watchdog** — even without `current_estimate` being called, the watchdog method correctly reports `True` after 3 s.

**AC-5: GCS STATUSTEXT severity correct** — engagement = CRITICAL; recovery = NOTICE.

**AC-6: Configurable threshold** — `no_estimate_fallback_s = 5.0` → engagement at 5 s instead of 3 s.

**AC-7: Both estimators participate** — iSAM2 + ESKF both fire engagement / recovery signals correctly.

**AC-8: FDR record shapes** — engagement: `{reason: "no_successful_estimate_for_s"}`; recovery: `{recovered_after_s}`.

## Non-Functional Requirements

- `check_fallback_state` p99 ≤ 5 µs.
- Hook overhead in `current_estimate` < 10 µs.

## Constraints

- One signal per state transition (rate-limited).
- Both estimators MUST participate.
- Config threshold MUST be respected.

## Risks & Mitigation

- **Risk: `_last_successful_estimate_ns` race with watchdog** — single-writer thread; both update from the same thread (composition root binds C5 + C8 outbound 5 Hz tick handler).

## Runtime Completeness

- **Named capability**: AC-5.2 fallback detector.
- **Production code**: real counter, real signal emission, real watchdog method.
- **Unacceptable substitutes**: spamming engagement signals on every check (rate-limit violation); silently dropping recovery (would leave C8 in IMU-only forever).
