mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 14:31:09 +00:00
[AZ-666] [AZ-673] [AZ-648] ignored set + UDS VLM + mission FSM batch 5
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
AZ-666 mapobjects_store: - internal/ignored.rs (HashSet<(mgrs, class_group)> for O(1) suppression) - internal/passes.rs (per-region PassTracker with observed-id set and end-of-pass removed-candidate sweep) - Classification::Ignored wired into classify; apply_decline + is_ignored + pass_start + end_of_pass on MapObjectsStoreHandle - new tests/ignored_and_sweep.rs (3 AC + 2 supplementary) AZ-673 vlm_client: - internal/peer_cred.rs (Linux SO_PEERCRED via libc getsockopt; PeerCredOutcome::SkippedNonLinux on macOS dev hosts per description.md §8) - internal/prompt.rs (pre-send ROI size + format + prompt non-emptiness validation) - internal/wire.rs (length-prefixed JSON envelope with base64 ROI) - internal/uds_client.rs (tokio UnixStream client; bounded reconnect; hard-stop on peer-cred mismatch; per-request deadline) - VlmClient with both eager (open/connect) and lazy (new) ctor - workspace Cargo.toml: base64 + libc as workspace deps AZ-648 mission_executor: - internal/types.rs (Variant, MissionState, TransitionKey, Telemetry, TransitionEvent, StepOutcome) - internal/driver.rs (MissionDriver trait + DriverError + DriverAction) - internal/fsm.rs (variant-agnostic Transition + FsmCore + step_one with per-transition retry budget keyed by TransitionKey) - internal/multirotor.rs + internal/fixed_wing.rs (typed transition tables; multirotor has Armed/TakeOff, fixed-wing parks in WaitAuto for operator AUTO) - public API: MissionExecutor::run spawns the FSM task and returns a clone-safe MissionExecutorHandle (state, health, subscribe, paused_reason, retry_count) - new tests/state_machine.rs (AC-1..AC-4 via ScriptedDriver fake; SITL conformance lands with AZ-649 telemetry forwarding) Workspace: cargo fmt + clippy -D warnings clean; full cargo test --workspace --all-features green (1 ignored = AZ-665 perf gate). Tasks moved todo/ → done/, autodev state set to batch 6 selection. Refs: _docs/03_implementation/batch_05_cycle1_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,9 +15,12 @@ use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use h3o::CellIndex;
|
||||
use shared::error::Result;
|
||||
use shared::models::mapobject::IgnoredItem;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::h3_index::{cell_of, grid_disk, haversine_m, DEFAULT_K_RING, DEFAULT_RESOLUTION};
|
||||
use super::ignored::IgnoredSet;
|
||||
use super::passes::{bbox_contains, PassTracker, RegionBbox};
|
||||
|
||||
/// Per-detection input to `classify`. This bundles the georeferenced
|
||||
/// payload the architecture-level "detection" carries (gps, class, conf,
|
||||
@@ -86,14 +89,26 @@ pub enum Classification {
|
||||
Existing {
|
||||
id: Uuid,
|
||||
},
|
||||
/// Reserved for AZ-666 end-of-pass sweep.
|
||||
RemovedCandidate {
|
||||
id: Uuid,
|
||||
},
|
||||
/// Reserved for AZ-666 ignored-suppression.
|
||||
/// Suppressed because the `(mgrs, class_group)` pair is in the
|
||||
/// `IgnoredSet` — the operator previously declined this POI.
|
||||
/// `scan_controller` must drop the detection without queueing it.
|
||||
Ignored,
|
||||
}
|
||||
|
||||
/// Object that the store knew about at pass start but did not see
|
||||
/// re-observed before `end_of_pass`. See `system-flows.md §F7`
|
||||
/// "end-of-pass sweep" — operator (not device) decides removal.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemovedCandidate {
|
||||
pub id: Uuid,
|
||||
pub mgrs: String,
|
||||
pub class: String,
|
||||
pub class_group: String,
|
||||
pub gps_lat: f64,
|
||||
pub gps_lon: f64,
|
||||
pub last_seen: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Stored shape. Fields beyond what `classify` reads are kept for the
|
||||
/// next batch in the same component (AZ-666 ignored-suppression / sweep,
|
||||
/// AZ-667 hydrate / dump_pending) which will surface them via the engine
|
||||
@@ -122,6 +137,8 @@ pub struct Store {
|
||||
by_cell: HashMap<CellIndex, Vec<StoredMapObject>>,
|
||||
/// Total object count, maintained alongside `by_cell` for O(1) metrics.
|
||||
len: usize,
|
||||
ignored: IgnoredSet,
|
||||
passes: PassTracker,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
@@ -130,6 +147,8 @@ impl Store {
|
||||
config,
|
||||
by_cell: HashMap::new(),
|
||||
len: 0,
|
||||
ignored: IgnoredSet::new(),
|
||||
passes: PassTracker::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +156,79 @@ impl Store {
|
||||
self.len
|
||||
}
|
||||
|
||||
/// Exposed for AZ-666/AZ-667 engine plug-points (`internal::engine::*`).
|
||||
/// Forward-use hook for AZ-667 / AZ-668 engine plug-points.
|
||||
#[allow(dead_code)]
|
||||
pub fn config(&self) -> &MapObjectsStoreConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Suppression query used by `scan_controller`'s POI gate.
|
||||
pub fn is_ignored(&self, mgrs: &str, class_group: &str) -> bool {
|
||||
self.ignored.is_ignored(mgrs, class_group)
|
||||
}
|
||||
|
||||
/// Append an `IgnoredItem` (operator declined a POI, or a hydrate
|
||||
/// from `mission_client` pulled it down).
|
||||
pub fn append_ignored(&mut self, item: IgnoredItem) {
|
||||
self.ignored.append(item);
|
||||
}
|
||||
|
||||
/// Number of distinct ignored `(mgrs, class_group)` pairs.
|
||||
pub fn ignored_len(&self) -> usize {
|
||||
self.ignored.len()
|
||||
}
|
||||
|
||||
/// Open a scan pass over `bbox`. `scan_controller` / `mission_executor`
|
||||
/// call this when entering a region; the matching `end_of_pass`
|
||||
/// returns un-observed objects as `RemovedCandidate`s.
|
||||
pub fn pass_start(&mut self, bbox: RegionBbox, started_at: DateTime<Utc>) {
|
||||
self.passes.pass_start(bbox, started_at);
|
||||
}
|
||||
|
||||
/// Close the pass over `bbox` and return objects in the region that
|
||||
/// were not observed since the pass started, excluding ignored
|
||||
/// objects. Returns an empty vec if no pass was open.
|
||||
pub fn end_of_pass(&mut self, bbox: &RegionBbox) -> Vec<RemovedCandidate> {
|
||||
let Some(result) = self.passes.pass_end(bbox) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for objects in self.by_cell.values() {
|
||||
for obj in objects {
|
||||
if !bbox_contains(bbox, obj.gps_lat, obj.gps_lon) {
|
||||
continue;
|
||||
}
|
||||
if result.observed.contains(&obj.id) {
|
||||
continue;
|
||||
}
|
||||
// Filter out ignored — operator already said "no" on
|
||||
// this pair; surfacing it again would be noise.
|
||||
if self.ignored.is_ignored(&obj.mgrs, &obj.class_group) {
|
||||
continue;
|
||||
}
|
||||
// Pass started after the object's last_seen → object
|
||||
// was known at pass start.
|
||||
if obj.last_seen > result.started_at {
|
||||
continue;
|
||||
}
|
||||
out.push(RemovedCandidate {
|
||||
id: obj.id,
|
||||
mgrs: obj.mgrs.clone(),
|
||||
class: obj.class.clone(),
|
||||
class_group: obj.class_group.clone(),
|
||||
gps_lat: obj.gps_lat,
|
||||
gps_lon: obj.gps_lon,
|
||||
last_seen: obj.last_seen,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn open_passes(&self) -> usize {
|
||||
self.passes.open_passes()
|
||||
}
|
||||
|
||||
/// Resolve a raw class string to its canonical group key.
|
||||
///
|
||||
/// The first class listed in a `similar_classes` group is the group
|
||||
@@ -162,11 +248,20 @@ impl Store {
|
||||
|
||||
/// Classify a single detection input. Mutates the store on `New` /
|
||||
/// `Moved` / `Existing` (insert / position-update / last_seen-update
|
||||
/// respectively). Returns the classification.
|
||||
/// respectively). Returns `Ignored` and DOES NOT mutate when the
|
||||
/// resolved `(mgrs, class_group)` is in the ignored set.
|
||||
///
|
||||
/// Also notes the matched id into every open pass whose bbox
|
||||
/// contains the input GPS so end-of-pass sweeps see this object
|
||||
/// as observed.
|
||||
pub fn classify(&mut self, input: ClassifyInput) -> Result<Classification> {
|
||||
let query_cell = cell_of(input.gps_lat, input.gps_lon, self.config.h3_resolution)?;
|
||||
let group = self.group_key(&input.class);
|
||||
|
||||
if self.ignored.is_ignored(&input.mgrs, &group) {
|
||||
return Ok(Classification::Ignored);
|
||||
}
|
||||
|
||||
// Find the nearest matching object across the k-ring.
|
||||
let mut best: Option<(CellIndex, usize, f64)> = None;
|
||||
let disk = grid_disk(query_cell, self.config.k_ring);
|
||||
@@ -217,6 +312,7 @@ impl Store {
|
||||
..moved
|
||||
});
|
||||
}
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::Moved {
|
||||
id,
|
||||
from_mgrs,
|
||||
@@ -231,7 +327,9 @@ impl Store {
|
||||
.expect("cell present during best-match scan");
|
||||
let obj = &mut bucket[idx];
|
||||
obj.last_seen = input.observed_at;
|
||||
Ok(Classification::Existing { id: obj.id })
|
||||
let id = obj.id;
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::Existing { id })
|
||||
}
|
||||
None => {
|
||||
// NEW — insert.
|
||||
@@ -253,6 +351,7 @@ impl Store {
|
||||
};
|
||||
self.by_cell.entry(query_cell).or_default().push(stored);
|
||||
self.len += 1;
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::New { id })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user