use crate::error::PlantError;
use crate::linear_algebra::{Matrix, Vector, Vector3D};
use crate::scalar::Numeric;
use crate::spatial::Wrench;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RotorSpin {
Clockwise,
CounterClockwise,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RotorCommands<const ROTOR_COUNT: usize, T: Numeric = f64> {
thrusts: Vector<ROTOR_COUNT, T>,
saturated: bool,
}
impl<const ROTOR_COUNT: usize, T: Numeric> RotorCommands<ROTOR_COUNT, T> {
#[inline]
pub fn thrusts(self) -> Vector<ROTOR_COUNT, T> {
self.thrusts
}
#[inline]
#[must_use]
pub fn saturated(self) -> bool {
self.saturated
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MultirotorMixer<const ROTOR_COUNT: usize, T: Numeric = f64> {
allocation: Matrix<4, ROTOR_COUNT, T>,
distribution: Matrix<ROTOR_COUNT, 4, T>,
minimum_thrust: T,
maximum_thrust: T,
}
impl<const ROTOR_COUNT: usize, T: Numeric> MultirotorMixer<ROTOR_COUNT, T> {
pub fn new(
positions: [Vector3D<T>; ROTOR_COUNT],
spins: [RotorSpin; ROTOR_COUNT],
torque_per_thrust: T,
minimum_thrust: T,
maximum_thrust: T,
) -> Result<Self, PlantError> {
if !torque_per_thrust.is_finite()
|| !minimum_thrust.is_finite()
|| !maximum_thrust.is_finite()
|| positions.iter().any(|p| !p.is_finite())
{
return Err(PlantError::NonFinite);
}
if torque_per_thrust <= T::ZERO {
return Err(PlantError::NonPositiveTorqueRatio);
}
if maximum_thrust <= minimum_thrust {
return Err(PlantError::InvalidThrustLimits);
}
let allocation = Matrix::<4, ROTOR_COUNT, T>::from_fn(|row, rotor| {
let position = positions[rotor];
match row {
0 => T::ONE,
1 => position[1],
2 => -position[0],
_ => match spins[rotor] {
RotorSpin::Clockwise => torque_per_thrust,
RotorSpin::CounterClockwise => -torque_per_thrust,
},
}
});
let distribution = allocation.pseudo_inverse()?;
let round_trip = allocation * distribution;
let bar = T::from_f64(1e-4);
for row in 0..4 {
for col in 0..4 {
let wanted = if row == col { T::ONE } else { T::ZERO };
if (round_trip[(row, col)] - wanted).abs() > bar {
return Err(PlantError::RotorLayoutNotIndependent);
}
}
}
Ok(MultirotorMixer {
allocation,
distribution,
minimum_thrust,
maximum_thrust,
})
}
#[inline]
pub fn allocation(self) -> Matrix<4, ROTOR_COUNT, T> {
self.allocation
}
#[inline]
#[must_use]
pub fn minimum_thrust(self) -> T {
self.minimum_thrust
}
#[inline]
#[must_use]
pub fn maximum_thrust(self) -> T {
self.maximum_thrust
}
#[must_use]
pub fn rotor_thrusts(
self,
collective_thrust: T,
torque: Vector3D<T>,
) -> RotorCommands<ROTOR_COUNT, T> {
let wanted = Vector::new([collective_thrust, torque[0], torque[1], torque[2]]);
let share = self.distribution * wanted;
let mut saturated = false;
let thrusts = Vector::from_fn(|rotor| {
let value = share[rotor];
if value < self.minimum_thrust {
saturated = true;
self.minimum_thrust
} else if value > self.maximum_thrust {
saturated = true;
self.maximum_thrust
} else {
value
}
});
RotorCommands { thrusts, saturated }
}
#[must_use]
pub fn wrench(self, thrusts: Vector<ROTOR_COUNT, T>) -> Wrench<T> {
let produced = self.allocation * thrusts;
Wrench::new(
Vector::new([T::ZERO, T::ZERO, produced[0]]),
Vector::new([produced[1], produced[2], produced[3]]),
)
}
}
impl<T: Numeric> MultirotorMixer<4, T> {
pub fn quadrotor_x(
arm_length: T,
torque_per_thrust: T,
minimum_thrust: T,
maximum_thrust: T,
) -> Result<Self, PlantError> {
if !arm_length.is_finite() {
return Err(PlantError::NonFinite);
}
if arm_length <= T::ZERO {
return Err(PlantError::NonPositiveArmLength);
}
let half = arm_length / T::TWO.sqrt();
let positions = [
Vector::new([half, -half, T::ZERO]),
Vector::new([-half, half, T::ZERO]),
Vector::new([half, half, T::ZERO]),
Vector::new([-half, -half, T::ZERO]),
];
let spins = [
RotorSpin::Clockwise,
RotorSpin::Clockwise,
RotorSpin::CounterClockwise,
RotorSpin::CounterClockwise,
];
Self::new(
positions,
spins,
torque_per_thrust,
minimum_thrust,
maximum_thrust,
)
}
}