# 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).