[AZ-649] [AZ-674] [AZ-667] telemetry + vlm schema + mapobjects hydrate batch 6

AZ-649 mission_executor telemetry forwarding:
- shared::models::telemetry::UavTelemetry canonical model
- TelemetryForwarder with atomic ArcSwap snapshot + 3 lossy
  tokio::sync::broadcast channels (MissionExecutor, ScanController,
  MavlinkUplink) + per-consumer drop counters
- MavlinkProjection::from_mavlink for HEARTBEAT/GLOBAL_POSITION_INT/
  ATTITUDE/SYS_STATUS
- spawn_mavlink_pump bridges mavlink_layer into the forwarder at the
  binary edge

AZ-674 vlm_client schema validation + model_version tracking:
- AssessmentParser owns schema validation + model-version state
- wire::read_response_raw splits raw bytes from parsing so invalid
  payloads can be logged size-capped
- VlmStatus gains an Inconclusive variant; exhaustive-match test
  guards downstream consumers
- VlmPipelineStatus mirrors the new variant in shared::models::poi

AZ-667 mapobjects_store hydrate + pending logs + cascade:
- SyncState enum aligned with description.md (FreshBoot, Synced,
  CachedFallback, Degraded, Failed)
- Store::hydrate(MapObjectsBundle) replaces in-memory map atomically;
  freshness=Stale -> CachedFallback
- classify() + end_of_pass append MapObjectObservation events to
  pending_observations (New/Moved/Existing/RemovedCandidate)
- apply_decline + LocalAppended ignored items append to pending_ignored
- drain_pending() returns and clears both logs
- cascade_mission(id) purges by_cell + IgnoredSet + pending logs
- Health surface reports sync_state, pending_obs, pending_ign

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 17:40:43 +03:00
parent b5cc0c321c
commit e56d428753
26 changed files with 2122 additions and 62 deletions
+1
View File
@@ -11,5 +11,6 @@ pub mod mission;
pub mod movement;
pub mod operator;
pub mod poi;
pub mod telemetry;
pub mod tier2;
pub mod vlm;
+2
View File
@@ -13,6 +13,7 @@ pub enum VlmPipelineStatus {
NotRequested,
Pending,
Ok,
Inconclusive,
Timeout,
SchemaInvalid,
IpcError,
@@ -23,6 +24,7 @@ impl From<VlmStatus> for VlmPipelineStatus {
fn from(s: VlmStatus) -> Self {
match s {
VlmStatus::Ok => Self::Ok,
VlmStatus::Inconclusive => Self::Inconclusive,
VlmStatus::Timeout => Self::Timeout,
VlmStatus::SchemaInvalid => Self::SchemaInvalid,
VlmStatus::IpcError => Self::IpcError,
+96
View File
@@ -0,0 +1,96 @@
//! `UavTelemetry` — projection of decoded MAVLink telemetry into a
//! typed snapshot that downstream consumers (`scan_controller`,
//! `movement_detector`, `telemetry_stream`, BIT) consume.
//!
//! Authoritative projection rules:
//!
//! - `position` from `GLOBAL_POSITION_INT` (id 33). Latitude/longitude
//! are kept in their MAVLink-native E7 form so consumers that
//! compare against waypoints (also E7) don't re-introduce float
//! round-trip drift. Altitude is in metres (MSL + AGL relative).
//! Velocities are in m/s, heading in degrees [0, 360).
//! - `attitude` from `ATTITUDE` (id 30). Angles in radians per the
//! MAVLink convention.
//! - `mode` from `HEARTBEAT` (id 0). The `(base_mode, custom_mode)`
//! pair is the canonical (vehicle-type-specific) discriminator;
//! `system_status` is the MAV_STATE enum (`MAV_STATE_ACTIVE` etc.).
//! - `sys_status` from `SYS_STATUS` (id 1). Battery + comms + sensor
//! health bitfield — the bits consumers actually read are
//! documented in `architecture.md §5.6`.
//! - `monotonic_ts_ns` is the host monotonic timestamp captured the
//! moment the originating MAVLink message was decoded. Strictly
//! non-decreasing across snapshots. Boot-time-relative fields
//! (`ts_boot_ms`) are kept on each sub-struct so consumers that
//! already correlate against MAVLink time-bases (e.g. EKF logs)
//! don't lose them.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct UavPosition {
pub lat_e7: i32,
pub lon_e7: i32,
pub alt_m: f32,
pub relative_alt_m: f32,
pub vx_mps: f32,
pub vy_mps: f32,
pub vz_mps: f32,
pub heading_deg: f32,
pub ts_boot_ms: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct UavAttitude {
pub roll: f32,
pub pitch: f32,
pub yaw: f32,
pub rollspeed: f32,
pub pitchspeed: f32,
pub yawspeed: f32,
pub ts_boot_ms: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct UavMode {
pub base_mode: u8,
pub custom_mode: u32,
pub system_status: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct UavSysStatus {
pub voltage_battery_mv: u16,
pub current_battery_ca: i16,
pub battery_remaining: i8,
pub onboard_sensors_health: u32,
pub errors_comm: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct UavTelemetry {
pub position: Option<UavPosition>,
pub attitude: Option<UavAttitude>,
pub mode: Option<UavMode>,
pub sys_status: Option<UavSysStatus>,
pub monotonic_ts_ns: u64,
}
impl UavTelemetry {
/// Empty snapshot used as the initial value before any telemetry
/// has arrived.
pub fn empty() -> Self {
Self {
position: None,
attitude: None,
mode: None,
sys_status: None,
monotonic_ts_ns: 0,
}
}
}
impl Default for UavTelemetry {
fn default() -> Self {
Self::empty()
}
}
+14
View File
@@ -16,10 +16,24 @@ pub enum VlmLabel {
Error,
}
/// Exhaustive status enum per AZ-674 §AC-4. Consumers MUST match every
/// variant — no `_ => …` catch-alls in the policy code path.
///
/// Distinction from [`VlmLabel`]: `status` says "did the VLM call
/// itself produce a usable answer"; `label` says "what does that
/// answer mean". `(status = Ok, label = Inconclusive)` is a valid
/// combination — the call succeeded, the model said it couldn't
/// classify. `status = Inconclusive` is reserved for the case where
/// the call returned a structured assessment but the verdict envelope
/// is itself "inconclusive" at the protocol level (model abstained,
/// not the same as label-inconclusive). Keeping both lets the
/// scan_controller distinguish "VLM declined to commit" from "VLM
/// committed to 'inconclusive'".
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VlmStatus {
Ok,
Inconclusive,
Timeout,
SchemaInvalid,
IpcError,