use super::models::{ControlSource, GimbalCommand, GimbalState, PrimaryControl};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::warn;
const SOURCE_TRACKING_TTL: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, Copy)]
pub struct VehicleState {
pub pitch_deg: f32,
pub roll_deg: f32,
pub yaw_deg: f32,
pub yaw_rate_deg_s: Option<f32>,
pub ned_velocity_m_s: (f32, f32, f32),
}
const MAX_TRACKED_SOURCES: usize = 256;
#[derive(Clone)]
pub struct StateManager {
state: Arc<RwLock<GimbalState>>,
last_command: Arc<RwLock<Option<GimbalCommand>>>,
primary_control: Arc<RwLock<PrimaryControl>>,
last_command_at: Arc<RwLock<HashMap<ControlSource, Instant>>>,
last_rate_at: Arc<RwLock<HashMap<ControlSource, Instant>>>,
vehicle_state: Arc<RwLock<Option<(VehicleState, Instant)>>>,
min_command_interval: Duration,
}
impl StateManager {
pub fn new() -> Self {
Self::with_min_command_interval(Duration::ZERO)
}
pub fn with_min_command_interval(interval: Duration) -> Self {
Self {
state: Arc::new(RwLock::new(GimbalState::default())),
last_command: Arc::new(RwLock::new(None)),
primary_control: Arc::new(RwLock::new(PrimaryControl::default())),
last_command_at: Arc::new(RwLock::new(HashMap::new())),
last_rate_at: Arc::new(RwLock::new(HashMap::new())),
vehicle_state: Arc::new(RwLock::new(None)),
min_command_interval: interval,
}
}
pub fn update_vehicle_state(&self, state: VehicleState) {
*self.vehicle_state.write() = Some((state, Instant::now()));
}
pub fn vehicle_state(&self, max_age: Duration) -> Option<VehicleState> {
let snapshot = *self.vehicle_state.read();
snapshot.and_then(|(s, t)| (t.elapsed() <= max_age).then_some(s))
}
pub fn vehicle_yaw_deg(&self, max_age: Duration) -> Option<f32> {
self.vehicle_state(max_age).map(|s| s.yaw_deg)
}
pub fn axis_baseline(&self) -> (f32, f32, f32) {
let last = self.last_command.read().clone();
let measured = self.state.read();
let measured_valid = measured.measured_at.is_some();
let pick = |last_axis: Option<f32>, measured_axis: f32| -> f32 {
last_axis
.or_else(|| measured_valid.then_some(measured_axis))
.unwrap_or(0.0)
};
(
pick(last.as_ref().and_then(|c| c.pitch), measured.pitch),
pick(last.as_ref().and_then(|c| c.roll), measured.roll),
pick(last.as_ref().and_then(|c| c.yaw), measured.yaw),
)
}
pub fn last_rate_tick(&self, source: &ControlSource) -> Option<Instant> {
self.last_rate_at.read().get(source).copied()
}
pub fn record_rate_tick(&self, source: ControlSource, now: Instant) {
let mut map = self.last_rate_at.write();
evict_for_insert(&mut map, now);
map.insert(source, now);
}
pub fn get_state(&self) -> GimbalState {
self.state.read().clone()
}
pub fn update_measured_angles(&self, yaw: f32, pitch: f32, roll: f32) {
let mut state = self.state.write();
state.update_measured_angles(yaw, pitch, roll);
}
pub fn update_pan_mode(&self, mode: super::models::PanMode) {
let mut state = self.state.write();
state.update_pan_mode(mode);
}
pub fn update_standby(&self, standby: bool) {
let mut state = self.state.write();
state.update_standby(standby);
}
pub fn update_firmware_version(&self, version: String) {
let mut state = self.state.write();
state.firmware_version = Some(version);
}
pub fn get_last_command(&self) -> Option<GimbalCommand> {
self.last_command.read().clone()
}
pub fn set_command(&self, command: GimbalCommand) -> bool {
if !self.check_and_record_rate(&command.source) {
return false;
}
let mut last_cmd = self.last_command.write();
let should_accept = match &*last_cmd {
None => true, Some(prev) => {
if prev.is_stale() {
true
} else {
let new_priority = command.source.priority();
let prev_priority = prev.source.priority();
new_priority > prev_priority
|| (new_priority == prev_priority && command.timestamp > prev.timestamp)
}
}
};
if should_accept {
*last_cmd = Some(command);
true
} else {
false
}
}
fn check_and_record_rate(&self, source: &ControlSource) -> bool {
if self.min_command_interval.is_zero() {
return true;
}
let now = Instant::now();
let mut map = self.last_command_at.write();
evict_for_insert(&mut map, now);
if let Some(prev) = map.get(source) {
if now.duration_since(*prev) < self.min_command_interval {
warn!(
"Rate-limited command from {:?}: {} ms since last (min {} ms)",
source,
now.duration_since(*prev).as_millis(),
self.min_command_interval.as_millis(),
);
return false;
}
}
map.insert(source.clone(), now);
true
}
pub fn get_primary_control(&self) -> PrimaryControl {
*self.primary_control.read()
}
pub fn set_primary_control(&self, pc: PrimaryControl) {
*self.primary_control.write() = pc;
}
pub fn is_primary_allowed(&self, sysid: u8, compid: u8) -> bool {
self.get_primary_control().is_allowed(sysid, compid)
}
pub fn clear_source(&self, source: &ControlSource) {
self.last_command_at.write().remove(source);
self.last_rate_at.write().remove(source);
}
pub fn is_standby(&self) -> bool {
self.state.read().standby
}
}
fn evict_for_insert(map: &mut HashMap<ControlSource, Instant>, now: Instant) {
map.retain(|_, t| now.duration_since(*t) <= SOURCE_TRACKING_TTL);
if map.len() >= MAX_TRACKED_SOURCES {
if let Some(oldest_key) = map.iter().min_by_key(|(_, t)| *t).map(|(k, _)| k.clone()) {
map.remove(&oldest_key);
}
}
}
impl Default for StateManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "state/tests.rs"]
mod tests;