use super::super::math::euler_deg_to_quaternion_wxyz;
use crate::daemon::config::MavlinkConfig;
use crate::daemon::mavlink_manager::{Peers, RxCtx};
use crate::daemon::state::StateManager;
use crate::gimbal::Attitude;
use mavlink::ardupilotmega::{
GimbalDeviceErrorFlags, GimbalDeviceFlags, GimbalManagerFlags, MavCmd, MavMessage, MavResult,
COMMAND_ACK_DATA, GIMBAL_DEVICE_ATTITUDE_STATUS_DATA, GIMBAL_MANAGER_STATUS_DATA,
};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tokio::net::UdpSocket;
use tracing::{debug, error, warn};
pub(super) mod msg_id {
pub(in crate::daemon::mavlink_manager) const GIMBAL_MANAGER_INFORMATION: u32 = 280;
pub(in crate::daemon::mavlink_manager) const GIMBAL_MANAGER_STATUS: u32 = 281;
}
pub(super) fn message_id_from_param(p: f32) -> Option<u32> {
if !p.is_finite() || p < 0.0 || p > u32::MAX as f32 {
return None;
}
Some(p.round() as u32)
}
pub(super) fn build_gimbal_manager_status_data(
state_manager: &StateManager,
config: &MavlinkConfig,
start_time: Instant,
) -> GIMBAL_MANAGER_STATUS_DATA {
let pc = state_manager.get_primary_control();
let flags = if state_manager.is_standby() {
GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_RETRACT
} else {
GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_YAW_IN_VEHICLE_FRAME
};
GIMBAL_MANAGER_STATUS_DATA {
time_boot_ms: start_time.elapsed().as_millis() as u32,
flags,
gimbal_device_id: config.compid,
primary_control_sysid: pc.primary_sysid,
primary_control_compid: pc.primary_compid,
secondary_control_sysid: pc.secondary_sysid,
secondary_control_compid: pc.secondary_compid,
}
}
pub(super) fn build_attitude_status_data(
attitude: &Attitude,
start_time: Instant,
failure_flags: GimbalDeviceErrorFlags,
) -> GIMBAL_DEVICE_ATTITUDE_STATUS_DATA {
let q = euler_deg_to_quaternion_wxyz(attitude.pitch, attitude.roll, attitude.yaw);
GIMBAL_DEVICE_ATTITUDE_STATUS_DATA {
time_boot_ms: start_time.elapsed().as_millis() as u32,
target_system: 0,
target_component: 0,
flags: GimbalDeviceFlags::GIMBAL_DEVICE_FLAGS_YAW_IN_VEHICLE_FRAME,
q,
angular_velocity_x: f32::NAN,
angular_velocity_y: f32::NAN,
angular_velocity_z: f32::NAN,
failure_flags,
}
}
pub(super) async fn send_mavlink_message(
socket: &Arc<UdpSocket>,
dest_addr: SocketAddr,
sysid: u8,
compid: u8,
seq: &Arc<AtomicU8>,
msg: MavMessage,
) -> crate::error::Result<()> {
let header = mavlink::MavHeader {
system_id: sysid,
component_id: compid,
sequence: seq.fetch_add(1, Ordering::Relaxed),
};
let mut buf = Vec::new();
mavlink::write_v2_msg(&mut buf, header, &msg)?;
socket.send_to(&buf, dest_addr).await?;
Ok(())
}
pub(super) async fn broadcast_to_peers(
socket: &Arc<UdpSocket>,
peers: &Peers,
config: &MavlinkConfig,
seq: &Arc<AtomicU8>,
msg: MavMessage,
label: &'static str,
) {
let targets: Vec<SocketAddr> = peers.lock().await.keys().copied().collect();
if targets.is_empty() {
return;
}
for peer in targets {
if let Err(e) =
send_mavlink_message(socket, peer, config.sysid, config.compid, seq, msg.clone()).await
{
warn!("{} to {} failed: {}", label, peer, e);
}
}
}
pub(super) async fn send_gimbal_manager_information(
socket: &Arc<UdpSocket>,
dest_addr: SocketAddr,
config: &MavlinkConfig,
seq: &Arc<AtomicU8>,
start_time: Instant,
) {
use mavlink::ardupilotmega::GimbalManagerCapFlags as Caps;
let cap_flags = Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_RETRACT
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_NEUTRAL
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_AXIS
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_FOLLOW
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_LOCK
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_AXIS
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_FOLLOW
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_LOCK
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_AXIS
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_FOLLOW
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_LOCK
| Caps::GIMBAL_MANAGER_CAP_FLAGS_HAS_RC_INPUTS;
let limits = &config.limits;
let info = mavlink::ardupilotmega::GIMBAL_MANAGER_INFORMATION_DATA {
time_boot_ms: start_time.elapsed().as_millis() as u32,
cap_flags,
gimbal_device_id: config.compid,
roll_min: limits.roll_min_deg.to_radians(),
roll_max: limits.roll_max_deg.to_radians(),
pitch_min: limits.pitch_min_deg.to_radians(),
pitch_max: limits.pitch_max_deg.to_radians(),
yaw_min: limits.yaw_min_deg.to_radians(),
yaw_max: limits.yaw_max_deg.to_radians(),
};
let msg = MavMessage::GIMBAL_MANAGER_INFORMATION(info);
match send_mavlink_message(socket, dest_addr, config.sysid, config.compid, seq, msg).await {
Ok(_) => debug!("Sent GIMBAL_MANAGER_INFORMATION"),
Err(e) => error!("Failed to send GIMBAL_MANAGER_INFORMATION: {}", e),
}
}
pub(super) async fn send_command_ack(
dest_addr: SocketAddr,
ctx: &RxCtx<'_>,
command: MavCmd,
result: MavResult,
) {
let ack = COMMAND_ACK_DATA { command, result };
let msg = MavMessage::COMMAND_ACK(ack);
if let Err(e) = send_mavlink_message(
ctx.socket,
dest_addr,
ctx.config.sysid,
ctx.config.compid,
ctx.seq,
msg,
)
.await
{
error!("Failed to send COMMAND_ACK: {}", e);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::super::super::math::quaternion_wxyz_to_euler_deg;
use super::*;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() < eps
}
#[test]
fn build_attitude_status_round_trips_quaternion() {
let attitude = Attitude {
pitch: 12.0,
roll: -7.5,
yaw: 33.25,
};
let data =
build_attitude_status_data(&attitude, Instant::now(), GimbalDeviceErrorFlags::empty());
let (p, r, y) = quaternion_wxyz_to_euler_deg(data.q);
assert!(approx_eq(p, attitude.pitch, 1e-3), "pitch: got {p}");
assert!(approx_eq(r, attitude.roll, 1e-3), "roll: got {r}");
assert!(approx_eq(y, attitude.yaw, 1e-3), "yaw: got {y}");
}
#[test]
fn build_attitude_status_uses_broadcast_addressing_and_vehicle_frame() {
let data = build_attitude_status_data(
&Attitude {
pitch: 0.0,
roll: 0.0,
yaw: 0.0,
},
Instant::now(),
GimbalDeviceErrorFlags::empty(),
);
assert_eq!(data.target_system, 0);
assert_eq!(data.target_component, 0);
assert!(data
.flags
.contains(GimbalDeviceFlags::GIMBAL_DEVICE_FLAGS_YAW_IN_VEHICLE_FRAME));
assert!(data.angular_velocity_x.is_nan());
assert!(data.angular_velocity_y.is_nan());
assert!(data.angular_velocity_z.is_nan());
assert!(data.failure_flags.is_empty());
}
fn test_mavlink_config() -> MavlinkConfig {
MavlinkConfig {
enabled: true,
transport: Default::default(),
bind_addr: "0.0.0.0".parse().unwrap(),
udp_port: 14550,
sysid: 1,
compid: 154,
limits: Default::default(),
}
}
#[test]
fn build_manager_status_active_advertises_yaw_in_vehicle_frame() {
let cfg = test_mavlink_config();
let state = StateManager::new();
let data = build_gimbal_manager_status_data(&state, &cfg, Instant::now());
assert!(
!data
.flags
.contains(GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_RETRACT),
"flags must not advertise RETRACT while active (got {:?})",
data.flags
);
assert!(
data.flags
.contains(GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_YAW_IN_VEHICLE_FRAME),
"flags should mirror GIMBAL_DEVICE_ATTITUDE_STATUS frame (got {:?})",
data.flags
);
}
#[test]
fn build_manager_status_standby_advertises_retract_only() {
let cfg = test_mavlink_config();
let state = StateManager::new();
state.update_standby(true);
let data = build_gimbal_manager_status_data(&state, &cfg, Instant::now());
assert!(
data.flags
.contains(GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_RETRACT),
"standby=on must advertise RETRACT (got {:?})",
data.flags
);
assert!(
!data
.flags
.contains(GimbalManagerFlags::GIMBAL_MANAGER_FLAGS_YAW_IN_VEHICLE_FRAME),
"standby=on must not advertise YAW_IN_VEHICLE_FRAME alongside \
RETRACT — RETRACT takes precedence (got {:?})",
data.flags
);
}
#[test]
fn build_attitude_status_propagates_caller_failure_flags() {
let data = build_attitude_status_data(
&Attitude {
pitch: 0.0,
roll: 0.0,
yaw: 0.0,
},
Instant::now(),
GimbalDeviceErrorFlags::GIMBAL_DEVICE_ERROR_FLAGS_COMMS_ERROR,
);
assert!(data
.failure_flags
.contains(GimbalDeviceErrorFlags::GIMBAL_DEVICE_ERROR_FLAGS_COMMS_ERROR));
}
}