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 {
let (base_pitch, base_roll, base_yaw) = self.state_manager.axis_baseline();
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(base_pitch),
roll: command.roll.unwrap_or(base_roll),
yaw: command.yaw.unwrap_or(base_yaw),
},
CommandMode::Rate => {
warn!("Rate-mode command reached arbitrator unmapped — 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 none_axis_resolves_to_baseline_not_zero() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager.clone());
let prime = GimbalCommand::new(
ControlSource::Mavlink(1),
CommandMode::Position,
Some(20.0), Some(10.0), Some(15.0), );
assert!(matches!(
arbitrator.arbitrate(prime),
Outcome::Execute { .. }
));
let yaw_only = GimbalCommand::new(
ControlSource::Mavlink(1),
CommandMode::Position,
Some(30.0), None, None, );
match arbitrator.arbitrate(yaw_only) {
Outcome::Execute { pitch, roll, yaw } => {
assert_eq!(yaw, 30.0);
assert_eq!(pitch, 10.0, "pitch must preserve baseline, not snap to 0");
assert_eq!(roll, 15.0, "roll must preserve baseline, not snap to 0");
}
other => panic!("expected Execute, got {other:?}"),
}
}
#[test]
fn none_axis_at_cold_boot_falls_back_to_zero() {
let state_manager = StateManager::new();
let arbitrator = Arbitrator::new(state_manager);
let cmd = GimbalCommand::new(
ControlSource::Cli,
CommandMode::Position,
Some(15.0), None,
None,
);
match arbitrator.arbitrate(cmd) {
Outcome::Execute { pitch, roll, yaw } => {
assert_eq!(yaw, 15.0);
assert_eq!(pitch, 0.0);
assert_eq!(roll, 0.0);
}
other => panic!("expected Execute, got {other:?}"),
}
}
}