use super::models::{CommandMode, GimbalCommand};
use super::state::StateManager;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Outcome {
Execute {
pitch: f32,
roll: f32,
yaw: f32,
},
Rejected,
}
pub struct Arbitrator {
state_manager: StateManager,
}
impl Arbitrator {
pub fn new(state_manager: StateManager) -> Self {
Self { state_manager }
}
#[tracing::instrument(
level = "debug",
skip(self),
fields(source = ?command.source, mode = ?command.mode)
)]
pub fn arbitrate(&self, command: GimbalCommand) -> Outcome {
if !self.state_manager.set_command(command.clone()) {
debug!(
"Command rejected by state arbitration (source: {:?})",
command.source
);
return Outcome::Rejected;
}
info!(
"Accepted command from {:?} with priority {}",
command.source,
command.source.priority()
);
match command.mode {
CommandMode::Position => Outcome::Execute {
pitch: command.pitch.unwrap_or(0.0),
roll: command.roll.unwrap_or(0.0),
yaw: command.yaw.unwrap_or(0.0),
},
CommandMode::Rate => {
warn!("Rate-mode command reached arbitrator unmapped — rejecting");
Outcome::Rejected
}
CommandMode::Manual => {
warn!("Manual control mode not yet implemented — rejecting");
Outcome::Rejected
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::daemon::models::{CommandMode, ControlSource, GimbalCommand};
#[test]
fn position_command_decodes_to_execute() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager.clone());
let cmd = GimbalCommand::new(
ControlSource::Cli,
CommandMode::Position,
Some(10.0), Some(20.0), Some(30.0), );
match arbitrator.arbitrate(cmd) {
Outcome::Execute { pitch, roll, yaw } => {
assert_eq!(pitch, 20.0);
assert_eq!(roll, 30.0);
assert_eq!(yaw, 10.0);
}
other => panic!("expected Execute, got {other:?}"),
}
let last = state_manager.get_last_command().unwrap();
assert_eq!(last.yaw, Some(10.0));
assert_eq!(last.pitch, Some(20.0));
assert_eq!(last.roll, Some(30.0));
let state = state_manager.get_state();
assert_eq!(state.yaw, 0.0);
assert_eq!(state.pitch, 0.0);
assert_eq!(state.roll, 0.0);
assert!(state.measured_at.is_none());
}
#[test]
fn lower_priority_command_is_rejected() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager.clone());
let cmd1 = GimbalCommand::new(
ControlSource::Mavlink(1),
CommandMode::Position,
Some(10.0),
Some(10.0),
Some(10.0),
);
assert!(matches!(
arbitrator.arbitrate(cmd1),
Outcome::Execute { .. }
));
let cmd2 = GimbalCommand::new(
ControlSource::Cli,
CommandMode::Position,
Some(20.0),
Some(20.0),
Some(20.0),
);
assert_eq!(arbitrator.arbitrate(cmd2), Outcome::Rejected);
let last = state_manager.get_last_command().unwrap();
assert_eq!(last.yaw, Some(10.0));
}
#[test]
fn rate_mode_at_arbitrator_is_rejected() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager);
let cmd = GimbalCommand::new(
ControlSource::Cli,
CommandMode::Rate,
Some(0.0),
Some(0.0),
Some(0.0),
);
assert_eq!(arbitrator.arbitrate(cmd), Outcome::Rejected);
}
#[test]
fn manual_mode_is_rejected() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager);
let cmd = GimbalCommand::new(
ControlSource::Cli,
CommandMode::Manual,
Some(0.0),
Some(0.0),
Some(0.0),
);
assert_eq!(arbitrator.arbitrate(cmd), Outcome::Rejected);
}
}