use super::common::{
addressed_to_this_device, addressed_to_us, clamp_pry, dispatch_gimbal_command,
yaw_to_vehicle_frame,
};
use super::rate::{any_rate_active, apply_rate_increment};
use crate::daemon::mavlink_manager::math::quaternion_wxyz_to_euler_deg;
use crate::daemon::mavlink_manager::RxCtx;
use crate::daemon::models::{CommandMode, ControlSource, GimbalCommand};
use tracing::{debug, warn};
pub(super) async fn on_set_attitude(
sender_sysid: u8,
sender_compid: u8,
m: mavlink::ardupilotmega::GIMBAL_MANAGER_SET_ATTITUDE_DATA,
ctx: &RxCtx<'_>,
) {
debug!("Received GIMBAL_MANAGER_SET_ATTITUDE: {:?}", m);
if !addressed_to_us(
m.target_system,
m.target_component,
ctx.config.sysid,
ctx.config.compid,
) {
return;
}
if !addressed_to_this_device(m.gimbal_device_id, ctx.config.compid) {
debug!(
"Ignoring GIMBAL_MANAGER_SET_ATTITUDE for gimbal_device_id={} (we are {})",
m.gimbal_device_id, ctx.config.compid
);
return;
}
if !ctx.state.is_primary_allowed(sender_sysid, sender_compid) {
warn!(
"Ignoring GIMBAL_MANAGER_SET_ATTITUDE from non-primary ({},{})",
sender_sysid, sender_compid
);
return;
}
if m.q.iter().any(|c| c.is_nan()) {
let rates_deg_per_s = (
m.angular_velocity_y.to_degrees(),
m.angular_velocity_x.to_degrees(),
m.angular_velocity_z.to_degrees(),
);
if any_rate_active(rates_deg_per_s) {
apply_rate_increment(
ControlSource::Mavlink(sender_sysid),
rates_deg_per_s,
ctx,
"SET_ATTITUDE rate",
)
.await;
} else {
debug!("GIMBAL_MANAGER_SET_ATTITUDE with NaN quaternion and no rate — dropping");
}
return;
}
let (pitch_deg, roll_deg, yaw_deg) = quaternion_wxyz_to_euler_deg(m.q);
let yaw_deg = yaw_to_vehicle_frame(yaw_deg, m.flags, ctx, "SET_ATTITUDE");
let (pitch, roll, yaw) = clamp_pry(pitch_deg, roll_deg, yaw_deg, &ctx.config.limits);
let command = GimbalCommand::new(
ControlSource::Mavlink(sender_sysid),
CommandMode::Position,
Some(yaw),
Some(pitch),
Some(roll),
);
dispatch_gimbal_command(command, ctx, "SET_ATTITUDE").await;
}
pub(super) async fn on_set_pitchyaw(
sender_sysid: u8,
sender_compid: u8,
m: mavlink::ardupilotmega::GIMBAL_MANAGER_SET_PITCHYAW_DATA,
ctx: &RxCtx<'_>,
) {
debug!("Received GIMBAL_MANAGER_SET_PITCHYAW: {:?}", m);
if !addressed_to_us(
m.target_system,
m.target_component,
ctx.config.sysid,
ctx.config.compid,
) {
return;
}
if !addressed_to_this_device(m.gimbal_device_id, ctx.config.compid) {
debug!(
"Ignoring GIMBAL_MANAGER_SET_PITCHYAW for gimbal_device_id={} (we are {})",
m.gimbal_device_id, ctx.config.compid
);
return;
}
if !ctx.state.is_primary_allowed(sender_sysid, sender_compid) {
warn!(
"Ignoring GIMBAL_MANAGER_SET_PITCHYAW from non-primary ({},{})",
sender_sysid, sender_compid
);
return;
}
if m.pitch.is_nan() && m.yaw.is_nan() {
let rates_deg_per_s = (
m.pitch_rate.to_degrees(),
0.0, m.yaw_rate.to_degrees(),
);
if any_rate_active(rates_deg_per_s) {
apply_rate_increment(
ControlSource::Mavlink(sender_sysid),
rates_deg_per_s,
ctx,
"SET_PITCHYAW rate",
)
.await;
} else {
debug!("GIMBAL_MANAGER_SET_PITCHYAW with NaN angles and no rate — dropping");
}
return;
}
let (base_pitch, base_roll, base_yaw) = ctx.state.axis_baseline();
let pitch_deg = if m.pitch.is_nan() {
base_pitch
} else {
m.pitch.to_degrees()
};
let yaw_deg = if m.yaw.is_nan() {
base_yaw
} else {
m.yaw.to_degrees()
};
let yaw_deg = yaw_to_vehicle_frame(yaw_deg, m.flags, ctx, "SET_PITCHYAW");
let (pitch, roll, yaw) = clamp_pry(pitch_deg, base_roll, yaw_deg, &ctx.config.limits);
let command = GimbalCommand::new(
ControlSource::Mavlink(sender_sysid),
CommandMode::Position,
Some(yaw),
Some(pitch),
Some(roll),
);
dispatch_gimbal_command(command, ctx, "SET_PITCHYAW").await;
}
pub(super) const MANUAL_RATE_FULL_DEG_S: f32 = 90.0;
pub(super) async fn on_set_manual(
sender_sysid: u8,
sender_compid: u8,
m: mavlink::ardupilotmega::GIMBAL_MANAGER_SET_MANUAL_CONTROL_DATA,
ctx: &RxCtx<'_>,
) {
debug!("Received GIMBAL_MANAGER_SET_MANUAL_CONTROL: {:?}", m);
if !addressed_to_us(
m.target_system,
m.target_component,
ctx.config.sysid,
ctx.config.compid,
) {
return;
}
if !addressed_to_this_device(m.gimbal_device_id, ctx.config.compid) {
debug!(
"Ignoring GIMBAL_MANAGER_SET_MANUAL_CONTROL for gimbal_device_id={} (we are {})",
m.gimbal_device_id, ctx.config.compid
);
return;
}
if !ctx.state.is_primary_allowed(sender_sysid, sender_compid) {
debug!(
"Ignoring GIMBAL_MANAGER_SET_MANUAL_CONTROL from non-primary ({},{})",
sender_sysid, sender_compid
);
return;
}
if m.pitch.is_nan() && m.yaw.is_nan() {
let pitch_rate = manual_rate_to_deg_per_s(m.pitch_rate);
let yaw_rate = manual_rate_to_deg_per_s(m.yaw_rate);
let rates_deg_per_s = (pitch_rate, 0.0, yaw_rate);
if any_rate_active(rates_deg_per_s) {
apply_rate_increment(
ControlSource::Mavlink(sender_sysid),
rates_deg_per_s,
ctx,
"SET_MANUAL_CONTROL rate",
)
.await;
} else {
debug!("GIMBAL_MANAGER_SET_MANUAL_CONTROL with NaN angles and no rate — dropping");
}
return;
}
let (base_pitch, base_roll, base_yaw) = ctx.state.axis_baseline();
let pitch_deg = if m.pitch.is_nan() {
base_pitch
} else {
m.pitch * 90.0
};
let yaw_deg = if m.yaw.is_nan() {
base_yaw
} else {
m.yaw * 90.0
};
let yaw_deg = yaw_to_vehicle_frame(yaw_deg, m.flags, ctx, "SET_MANUAL_CONTROL");
let (pitch, roll, yaw) = clamp_pry(pitch_deg, base_roll, yaw_deg, &ctx.config.limits);
let command = GimbalCommand::new(
ControlSource::Mavlink(sender_sysid),
CommandMode::Position,
Some(yaw),
Some(pitch),
Some(roll),
);
dispatch_gimbal_command(command, ctx, "SET_MANUAL_CONTROL").await;
}
fn manual_rate_to_deg_per_s(rate_normalized: f32) -> f32 {
if rate_normalized.is_nan() {
0.0
} else {
rate_normalized * MANUAL_RATE_FULL_DEG_S
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::float_cmp
)]
mod tests {
use super::*;
#[test]
fn manual_rate_full_stick_maps_to_full_scale_deg_per_s() {
assert_eq!(manual_rate_to_deg_per_s(1.0), MANUAL_RATE_FULL_DEG_S);
assert_eq!(manual_rate_to_deg_per_s(-1.0), -MANUAL_RATE_FULL_DEG_S);
}
#[test]
fn manual_rate_half_stick_maps_to_half_scale() {
assert_eq!(manual_rate_to_deg_per_s(0.5), MANUAL_RATE_FULL_DEG_S * 0.5);
assert_eq!(
manual_rate_to_deg_per_s(-0.25),
MANUAL_RATE_FULL_DEG_S * -0.25
);
}
#[test]
fn manual_rate_zero_maps_to_zero() {
assert_eq!(manual_rate_to_deg_per_s(0.0), 0.0);
}
#[test]
fn manual_rate_nan_maps_to_zero() {
assert_eq!(manual_rate_to_deg_per_s(f32::NAN), 0.0);
}
#[test]
fn manual_rate_passes_through_out_of_range_values() {
assert_eq!(manual_rate_to_deg_per_s(2.0), MANUAL_RATE_FULL_DEG_S * 2.0);
}
}