use std::time::{Duration, Instant};
use crate::barrier::{self, BARRIER_GAIN_HZ};
use crate::config::SwarmControlConfig;
use crate::flocking::{self, FlockTuning};
use crate::formation::{self, Formation, FormationAnchor, FormationName, FORMATION_GAIN};
use crate::geo::{GeoOrigin, Ned};
use crate::neighbor::NeighborState;
use crate::precedence::ModePrecedence;
use crate::separation::{self, hard_override, HardBreach};
use crate::setpoint::{Setpoint, MAX_COMMAND_SPEED_MPS};
mod types;
pub use types::{
ControlOutcome, NeighborFix, OperatorDirective, OwnState, Suppression, SwarmControlCounters,
SwarmMode,
};
pub const CONTROL_HZ: f64 = 10.0;
pub const CONTROL_PERIOD: Duration = Duration::from_millis(100);
pub const OPERATOR_DIRECTIVE_TTL: Duration = Duration::from_secs(3);
pub const HARD_DWELL: Duration = Duration::from_secs(1);
pub const OPERATOR_GAIN: f64 = 1.0;
pub const COMMAND_DEADBAND_MPS: f64 = 0.05;
#[derive(Debug, Clone, Copy)]
struct HardLatch {
engaged_at: Instant,
target_alt_m: f64,
offender: u8,
}
pub struct SwarmController {
enabled: bool,
mode: SwarmMode,
formation: Formation,
flock: FlockTuning,
barrier_gain: f64,
target: Option<(f64, f64, f64)>,
operator: Option<(OperatorDirective, Instant)>,
hard: Option<HardLatch>,
last_neighbors_at: Option<Instant>,
precedence: ModePrecedence,
emergency: bool,
counters: SwarmControlCounters,
scratch: Vec<NeighborState>,
}
impl SwarmController {
pub fn new(cfg: &SwarmControlConfig, slots: &[u8]) -> Self {
let mut c = Self {
enabled: false,
mode: SwarmMode::Hold,
formation: Formation::built_in(
FormationName::Line,
slots,
formation::DEFAULT_SPACING_M,
FormationAnchor::Centroid,
),
flock: FlockTuning::default(),
barrier_gain: BARRIER_GAIN_HZ,
target: None,
operator: None,
hard: None,
last_neighbors_at: None,
precedence: ModePrecedence::Hold,
emergency: false,
counters: SwarmControlCounters::default(),
scratch: Vec::with_capacity(crate::neighbor::MAX_NEAREST),
};
c.apply_config(cfg, slots);
c
}
pub fn apply_config(&mut self, cfg: &SwarmControlConfig, slots: &[u8]) {
self.enabled = cfg.enabled;
self.mode = cfg.swarm_mode();
self.flock = cfg.flock_tuning();
self.formation = Formation::built_in(
cfg.formation_name(),
slots,
cfg.spacing_m(),
self.formation.anchor,
);
}
pub fn set_mode(&mut self, mode: SwarmMode) {
self.mode = mode;
}
pub fn mode(&self) -> SwarmMode {
self.mode
}
pub fn set_formation(
&mut self,
name: FormationName,
slots: &[u8],
spacing_m: f64,
anchor: FormationAnchor,
) {
self.formation = Formation::built_in(name, slots, spacing_m, anchor);
}
pub fn formation(&self) -> &Formation {
&self.formation
}
pub fn formation_anchor(&self) -> FormationAnchor {
self.formation.anchor
}
pub fn set_target(&mut self, target: Option<(f64, f64, f64)>) {
self.target = target;
}
pub fn set_operator_directive(&mut self, directive: OperatorDirective, now: Instant) {
self.operator = Some((directive, now));
}
pub fn clear_operator_directive(&mut self) {
self.operator = None;
}
pub fn precedence(&self) -> ModePrecedence {
self.precedence
}
pub fn emergency(&self) -> bool {
self.emergency
}
pub fn hard_latch(&self) -> Option<(u8, f64)> {
self.hard.map(|l| (l.offender, l.target_alt_m))
}
pub fn counters(&self) -> SwarmControlCounters {
self.counters
}
pub fn tick(&mut self, own: &OwnState, fixes: &[NeighborFix], now: Instant) -> ControlOutcome {
let fix_stale = own.fix_age.is_none_or(|age| age >= crate::OWN_STATE_STALE);
if fix_stale {
self.hard = None;
return self.suppress(Suppression::OwnStateStale, self.emergency);
}
let origin = GeoOrigin::new(own.lat_deg, own.lon_deg, own.alt_rel_m);
self.scratch.clear();
for f in fixes {
let pos = origin.to_ned(f.lat_deg, f.lon_deg, f.alt_m);
if !pos.is_finite() {
continue;
}
self.scratch.push(
NeighborState::new(f.slot, pos, Ned::new(f.vn, f.ve, f.vd), f.status)
.with_sender_order(f.sender_order),
);
}
if !self.scratch.is_empty() {
self.last_neighbors_at = Some(now);
} else if self.last_neighbors_at.is_none() {
self.last_neighbors_at = Some(now);
}
if !self.enabled {
return self.suppress(Suppression::Disabled, false);
}
let stale = self
.last_neighbors_at
.is_none_or(|t| now.saturating_duration_since(t) >= crate::NEIGHBOR_STALE);
if stale {
self.hard = None;
return self.suppress(Suppression::NeighborsStale, false);
}
if !own.armed {
self.hard = None;
return self.suppress(Suppression::NotArmed, false);
}
let sep = self.flock.separation;
let breach = separation::hard_breach(&self.scratch, &sep);
let repulsion = separation::repulsion(&self.scratch, &sep);
let (behaviour_level, behaviour) = self.behaviour_command(own, now);
let repelled = if matches!(behaviour_level, ModePrecedence::Flocking) {
behaviour
} else {
behaviour + repulsion
};
let bounded = barrier::constrain(
repelled,
Ned::new(own.vn, own.ve, own.vd),
&self.scratch,
&sep,
self.barrier_gain,
);
self.update_hard_latch(own, now, breach, bounded.pinned_by);
self.emergency = breach.is_some() || self.hard.is_some();
if !own.guided {
return self.suppress(Suppression::NotGuided, self.emergency);
}
if let Some(latch) = self.hard {
self.precedence = ModePrecedence::HardSeparation;
self.counters.setpoints_emitted += 1;
let escape = repulsion.horizontal() + hard_override(own.alt_rel_m, latch.target_alt_m);
let command = barrier::constrain(
escape,
Ned::new(own.vn, own.ve, own.vd),
&self.scratch,
&sep,
self.barrier_gain,
)
.command;
return ControlOutcome {
setpoint: Some(Setpoint::velocity(command)),
precedence: ModePrecedence::HardSeparation,
emergency: true,
suppressed: None,
};
}
if behaviour_level == ModePrecedence::Hold && bounded.command.norm() < COMMAND_DEADBAND_MPS
{
return self.suppress(Suppression::NothingToDo, self.emergency);
}
self.precedence = behaviour_level;
self.counters.setpoints_emitted += 1;
ControlOutcome {
setpoint: Some(Setpoint::velocity(bounded.command)),
precedence: behaviour_level,
emergency: self.emergency,
suppressed: None,
}
}
fn behaviour_command(&self, own: &OwnState, now: Instant) -> (ModePrecedence, Ned) {
let origin = GeoOrigin::new(own.lat_deg, own.lon_deg, own.alt_rel_m);
if let Some((directive, issued)) = self.operator {
if now.saturating_duration_since(issued) < OPERATOR_DIRECTIVE_TTL {
let cmd = match directive {
OperatorDirective::Goto {
lat_deg,
lon_deg,
alt_m,
} => origin
.to_ned(lat_deg, lon_deg, alt_m)
.scale(OPERATOR_GAIN)
.clamp_norm(MAX_COMMAND_SPEED_MPS),
OperatorDirective::Hold => Ned::ZERO,
};
return (ModePrecedence::Operator, cmd);
}
}
match self.mode {
SwarmMode::Formation => formation::command(
&self.formation,
own.slot,
&self.scratch,
FORMATION_GAIN,
MAX_COMMAND_SPEED_MPS,
)
.map(|c| (ModePrecedence::Formation, c))
.unwrap_or((ModePrecedence::Hold, Ned::ZERO)),
SwarmMode::Flocking => (
ModePrecedence::Flocking,
flocking::command(
Ned::new(own.vn, own.ve, own.vd),
self.target
.map(|(lat, lon, alt)| origin.to_ned(lat, lon, alt)),
&self.scratch,
&self.flock,
MAX_COMMAND_SPEED_MPS,
),
),
SwarmMode::Hold => (ModePrecedence::Hold, Ned::ZERO),
}
}
fn update_hard_latch(
&mut self,
own: &OwnState,
now: Instant,
breach: Option<HardBreach>,
pinned_by: Option<u8>,
) {
let trigger = breach
.map(|b| (b.slot, b.relative_alt_m, b.sender_order))
.or_else(|| {
pinned_by.and_then(|slot| {
self.scratch
.iter()
.find(|n| n.slot == slot)
.map(|n| (slot, -n.pos.d, n.sender_order))
})
});
match (self.hard, trigger) {
(None, Some((offender, relative_alt_m, sender_order))) => {
self.counters.hard_engagements += 1;
self.hard = Some(HardLatch {
engaged_at: now,
target_alt_m: separation::hard_climb_target_m(
own.alt_rel_m,
own.slot,
offender,
sender_order,
relative_alt_m,
),
offender,
});
}
(Some(latch), None)
if now.saturating_duration_since(latch.engaged_at) >= HARD_DWELL =>
{
self.hard = None;
}
_ => {}
}
}
fn suppress(&mut self, why: Suppression, emergency: bool) -> ControlOutcome {
self.precedence = ModePrecedence::Hold;
self.emergency = emergency;
self.counters.ticks_suppressed += 1;
ControlOutcome {
setpoint: None,
precedence: ModePrecedence::Hold,
emergency,
suppressed: Some(why),
}
}
}
#[cfg(test)]
mod tests;