use crate::geo::{deg_to_e7, GeoOrigin, Ned};
pub const MAX_COMMAND_SPEED_MPS: f64 = 10.0;
pub const MAV_FRAME_GLOBAL_RELATIVE_ALT_INT: u8 = 6;
const X_IGNORE: u16 = 1;
const Y_IGNORE: u16 = 2;
const Z_IGNORE: u16 = 4;
const VX_IGNORE: u16 = 8;
const VY_IGNORE: u16 = 16;
const VZ_IGNORE: u16 = 32;
const AX_IGNORE: u16 = 64;
const AY_IGNORE: u16 = 128;
const AZ_IGNORE: u16 = 256;
const YAW_IGNORE: u16 = 1024;
const YAW_RATE_IGNORE: u16 = 2048;
const ALWAYS_IGNORED: u16 = AX_IGNORE | AY_IGNORE | AZ_IGNORE | YAW_IGNORE | YAW_RATE_IGNORE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetpointKind {
Velocity,
Position,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Setpoint {
pub kind: SetpointKind,
pub lat_e7: i32,
pub lon_e7: i32,
pub alt_m: f32,
pub vn: f32,
pub ve: f32,
pub vd: f32,
}
impl Setpoint {
pub fn velocity(cmd: Ned) -> Self {
let c = cmd.clamp_norm(MAX_COMMAND_SPEED_MPS);
Self {
kind: SetpointKind::Velocity,
lat_e7: 0,
lon_e7: 0,
alt_m: 0.0,
vn: c.n as f32,
ve: c.e as f32,
vd: c.d as f32,
}
}
pub fn position(lat_deg: f64, lon_deg: f64, alt_m: f64) -> Self {
Self {
kind: SetpointKind::Position,
lat_e7: deg_to_e7(lat_deg),
lon_e7: deg_to_e7(lon_deg),
alt_m: alt_m as f32,
vn: 0.0,
ve: 0.0,
vd: 0.0,
}
}
pub fn position_at(origin: &GeoOrigin, offset: Ned) -> Self {
let (lat, lon, alt) = origin.to_geo(offset);
Self::position(lat, lon, alt)
}
pub const fn type_mask(&self) -> u16 {
match self.kind {
SetpointKind::Velocity => X_IGNORE | Y_IGNORE | Z_IGNORE | ALWAYS_IGNORED,
SetpointKind::Position => VX_IGNORE | VY_IGNORE | VZ_IGNORE | ALWAYS_IGNORED,
}
}
pub const fn coordinate_frame(&self) -> u8 {
MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
}
pub fn is_valid(&self) -> bool {
match self.kind {
SetpointKind::Velocity => {
self.vn.is_finite() && self.ve.is_finite() && self.vd.is_finite()
}
SetpointKind::Position => {
self.alt_m.is_finite() && (self.lat_e7 != 0 || self.lon_e7 != 0)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn velocity_mask_ignores_position_and_commands_velocity() {
let s = Setpoint::velocity(Ned::new(1.0, 2.0, -3.0));
let m = s.type_mask();
assert_eq!(m & X_IGNORE, X_IGNORE);
assert_eq!(m & Y_IGNORE, Y_IGNORE);
assert_eq!(m & Z_IGNORE, Z_IGNORE);
assert_eq!(
m & (VX_IGNORE | VY_IGNORE | VZ_IGNORE),
0,
"velocity must be honoured"
);
assert_eq!(m, 3527, "the pinned wire value");
}
#[test]
fn position_mask_ignores_velocity_and_commands_position() {
let s = Setpoint::position(12.9716, 77.5946, 30.0);
let m = s.type_mask();
assert_eq!(
m & (VX_IGNORE | VY_IGNORE | VZ_IGNORE),
VX_IGNORE | VY_IGNORE | VZ_IGNORE
);
assert_eq!(
m & (X_IGNORE | Y_IGNORE | Z_IGNORE),
0,
"position must be honoured"
);
assert_eq!(m, 3576, "the pinned wire value");
}
#[test]
fn acceleration_and_yaw_are_never_commanded() {
for s in [
Setpoint::velocity(Ned::new(1.0, 0.0, 0.0)),
Setpoint::position(1.0, 2.0, 3.0),
] {
let m = s.type_mask();
assert_eq!(m & ALWAYS_IGNORED, ALWAYS_IGNORED, "{s:?}");
}
}
#[test]
fn every_setpoint_uses_the_relative_altitude_frame() {
assert_eq!(
Setpoint::velocity(Ned::ZERO).coordinate_frame(),
MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
);
assert_eq!(Setpoint::position(1.0, 2.0, 3.0).coordinate_frame(), 6);
}
#[test]
fn a_velocity_command_saturates_at_the_speed_ceiling() {
let s = Setpoint::velocity(Ned::new(300.0, 400.0, 0.0)); let m = (s.vn as f64).hypot(s.ve as f64);
assert!((m - MAX_COMMAND_SPEED_MPS).abs() < 1e-4, "{m}");
assert!((s.vn as f64 / m - 0.6).abs() < 1e-5);
}
#[test]
fn ignored_position_fields_are_zeroed_not_left_stale() {
let s = Setpoint::velocity(Ned::new(1.0, 1.0, 0.0));
assert_eq!((s.lat_e7, s.lon_e7, s.alt_m), (0, 0, 0.0));
let p = Setpoint::position(1.0, 2.0, 3.0);
assert_eq!((p.vn, p.ve, p.vd), (0.0, 0.0, 0.0));
}
#[test]
fn down_is_down_so_a_climb_is_negative_vd() {
let s = Setpoint::velocity(Ned::new(0.0, 0.0, -1.0));
assert!(s.vd < 0.0, "vd {} should be negative for a climb", s.vd);
}
#[test]
fn position_at_round_trips_through_the_local_frame() {
let o = GeoOrigin::new(12.9716, 77.5946, 100.0);
let s = Setpoint::position_at(&o, Ned::new(50.0, -20.0, -10.0));
let back = o.to_ned(
crate::geo::e7_to_deg(s.lat_e7),
crate::geo::e7_to_deg(s.lon_e7),
s.alt_m as f64,
);
assert!(
(back - Ned::new(50.0, -20.0, -10.0)).norm() < 0.02,
"{back:?}"
);
assert!(
(s.alt_m - 110.0).abs() < 1e-4,
"10 m of negative down is 10 m up"
);
}
#[test]
fn a_non_finite_velocity_never_validates() {
let bad = Setpoint {
kind: SetpointKind::Velocity,
lat_e7: 0,
lon_e7: 0,
alt_m: 0.0,
vn: f32::NAN,
ve: 0.0,
vd: 0.0,
};
assert!(!bad.is_valid());
assert!(Setpoint::velocity(Ned::new(1.0, 0.0, 0.0)).is_valid());
assert!(Setpoint::velocity(Ned::new(f64::NAN, 0.0, 0.0)).is_valid());
}
}