use crate::daemon::arbitrator::Outcome;
use crate::daemon::config::GimbalLimits;
use crate::daemon::mavlink_manager::math::{clamp_axis, earth_to_vehicle_yaw_deg};
use crate::daemon::mavlink_manager::RxCtx;
use crate::daemon::models::GimbalCommand;
use mavlink::ardupilotmega::GimbalManagerFlags;
use std::time::Duration;
use tracing::{error, info, warn};
pub(super) const VEHICLE_ATTITUDE_MAX_AGE: Duration = Duration::from_secs(1);
pub(super) fn addressed_to_us(
target_system: u8,
target_component: u8,
our_sysid: u8,
our_compid: u8,
) -> bool {
let sys_ok = target_system == 0 || target_system == our_sysid;
let comp_ok = target_component == 0 || target_component == our_compid;
sys_ok && comp_ok
}
pub(super) fn addressed_to_this_device(gimbal_device_id: u8, our_compid: u8) -> bool {
gimbal_device_id == 0 || gimbal_device_id == our_compid
}
pub(super) fn gimbal_device_id_from_param7(p: f32) -> Option<u8> {
if !p.is_finite() || p < 0.0 || p > u8::MAX as f32 {
return None;
}
let rounded = p.round();
if (rounded - p).abs() > 0.5 {
return None;
}
Some(rounded as u8)
}
pub(super) fn clamp_pry(pitch: f32, roll: f32, yaw: f32, limits: &GimbalLimits) -> (f32, f32, f32) {
(
clamp_axis(pitch, limits.pitch_min_deg, limits.pitch_max_deg, "pitch"),
clamp_axis(roll, limits.roll_min_deg, limits.roll_max_deg, "roll"),
clamp_axis(yaw, limits.yaw_min_deg, limits.yaw_max_deg, "yaw"),
)
}
pub(super) fn yaw_to_vehicle_frame(
raw_yaw_deg: f32,
flags: GimbalManagerFlags,
ctx: &RxCtx<'_>,
label: &str,
) -> f32 {
if !flags.contains(GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_YAW_IN_EARTH_FRAME) {
return raw_yaw_deg;
}
match ctx.state.vehicle_yaw_deg(VEHICLE_ATTITUDE_MAX_AGE) {
Some(vehicle_yaw) => earth_to_vehicle_yaw_deg(raw_yaw_deg, vehicle_yaw),
None => {
warn!(
"{}: YAW_IN_EARTH_FRAME requested but no fresh AUTOPILOT_STATE — \
falling back to vehicle-frame interpretation",
label
);
raw_yaw_deg
}
}
}
pub(super) async fn dispatch_gimbal_command(command: GimbalCommand, ctx: &RxCtx<'_>, label: &str) {
match ctx.arbitrator.arbitrate(command) {
Outcome::Execute { pitch, roll, yaw } => {
match ctx.gimbal.set_attitude(pitch, roll, yaw).await {
Ok(()) => info!("Executed MAVLink {}", label),
Err(e) => error!("Failed to execute MAVLink {}: {}", label, e),
}
}
Outcome::Rejected => warn!("MAVLink {} rejected (lower priority)", label),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn addressed_to_us_accepts_per_axis_broadcast_and_exact_match() {
assert!(addressed_to_us(0, 0, 1, 154));
assert!(addressed_to_us(0, 154, 1, 154));
assert!(addressed_to_us(1, 0, 1, 154));
assert!(addressed_to_us(1, 154, 1, 154));
}
#[test]
fn addressed_to_us_rejects_other_components_on_same_sysid() {
assert!(!addressed_to_us(1, 1, 1, 154));
}
#[test]
fn addressed_to_us_rejects_other_systems() {
assert!(!addressed_to_us(2, 154, 1, 154));
assert!(!addressed_to_us(2, 0, 1, 154));
}
#[test]
fn addressed_to_this_device_accepts_broadcast_and_exact_match() {
assert!(addressed_to_this_device(0, 154));
assert!(addressed_to_this_device(154, 154));
}
#[test]
fn addressed_to_this_device_rejects_other_devices() {
assert!(!addressed_to_this_device(155, 154));
for non_mavlink in 1..=6u8 {
assert!(
!addressed_to_this_device(non_mavlink, 154),
"non-MAVLink gimbal id {} must not match our compid 154",
non_mavlink
);
}
}
#[test]
fn gimbal_device_id_from_param7_decodes_valid_byte() {
assert_eq!(gimbal_device_id_from_param7(0.0), Some(0));
assert_eq!(gimbal_device_id_from_param7(154.0), Some(154));
assert_eq!(gimbal_device_id_from_param7(154.0001), Some(154));
assert_eq!(gimbal_device_id_from_param7(255.0), Some(255));
}
#[test]
fn gimbal_device_id_from_param7_rejects_garbage() {
assert_eq!(gimbal_device_id_from_param7(f32::NAN), None);
assert_eq!(gimbal_device_id_from_param7(-1.0), None);
assert_eq!(gimbal_device_id_from_param7(f32::INFINITY), None);
assert_eq!(gimbal_device_id_from_param7(f32::NEG_INFINITY), None);
assert_eq!(gimbal_device_id_from_param7(256.0), None);
assert_eq!(gimbal_device_id_from_param7(1e9), None);
}
}