use crate::daemon::mavlink_manager::math::wrap_180_deg;
use std::time::Duration;
const SETTLE_AFTER: Duration = Duration::from_secs(2);
const ALPHA: f32 = 0.005;
const MAX_STEP_DEG: f32 = 0.05;
const MAX_PLAUSIBLE_ERROR_DEG: f32 = 30.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CorrectorStep {
pub error_deg: f32,
pub correction_deg: f32,
}
#[derive(Debug, Clone, Copy)]
pub struct YawCorrector {
pub mount_yaw_deg: f32,
}
impl YawCorrector {
pub const fn new(mount_yaw_deg: f32) -> Self {
Self { mount_yaw_deg }
}
pub fn step(
&self,
pv_op_yaw: f32,
last_sp_body: Option<f32>,
time_since_last_set: Option<Duration>,
) -> Option<CorrectorStep> {
let sp = last_sp_body?;
let time_since = time_since_last_set?;
if time_since < SETTLE_AFTER {
return None;
}
let error = wrap_180_deg(pv_op_yaw - sp);
if !error.is_finite() || error.abs() > MAX_PLAUSIBLE_ERROR_DEG {
return None;
}
let raw_correction = ALPHA * error;
let correction = raw_correction.clamp(-MAX_STEP_DEG, MAX_STEP_DEG);
Some(CorrectorStep {
error_deg: error,
correction_deg: correction,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() < eps
}
const SETTLED: Duration = Duration::from_secs(3);
#[test]
fn returns_none_without_recorded_sp() {
let c = YawCorrector::new(0.0);
assert!(c.step(0.0, None, Some(SETTLED)).is_none());
}
#[test]
fn returns_none_when_still_slewing() {
let c = YawCorrector::new(0.0);
assert!(c
.step(5.0, Some(0.0), Some(Duration::from_millis(500)))
.is_none());
}
#[test]
fn returns_none_with_no_set_yet() {
let c = YawCorrector::new(0.0);
assert!(c.step(0.0, Some(0.0), None).is_none());
}
#[test]
fn perfect_calibration_yields_zero_correction() {
let c = YawCorrector::new(0.0);
let step = c.step(10.0, Some(10.0), Some(SETTLED)).unwrap();
assert!(
approx_eq(step.error_deg, 0.0, 1e-6),
"error {}",
step.error_deg
);
assert!(approx_eq(step.correction_deg, 0.0, 1e-6));
}
#[test]
fn positive_error_yields_positive_correction() {
let c = YawCorrector::new(0.0);
let step = c.step(12.0, Some(10.0), Some(SETTLED)).unwrap();
assert!(
approx_eq(step.error_deg, 2.0, 1e-4),
"error {}",
step.error_deg
);
assert!(
approx_eq(step.correction_deg, 0.01, 1e-4),
"correction {}",
step.correction_deg
);
}
#[test]
fn negative_error_yields_negative_correction() {
let c = YawCorrector::new(0.0);
let step = c.step(5.0, Some(10.0), Some(SETTLED)).unwrap();
assert!(approx_eq(step.error_deg, -5.0, 1e-4));
assert!(approx_eq(step.correction_deg, -0.025, 1e-4));
}
#[test]
fn correction_is_capped_at_max_step() {
let c = YawCorrector::new(0.0);
let step = c.step(30.0, Some(10.0), Some(SETTLED)).unwrap();
assert!(approx_eq(step.error_deg, 20.0, 1e-4));
assert!(
approx_eq(step.correction_deg, MAX_STEP_DEG, 1e-6),
"correction {} should clamp to {MAX_STEP_DEG}",
step.correction_deg
);
}
#[test]
fn error_wraps_correctly_across_180_boundary() {
let c = YawCorrector::new(0.0);
let step = c.step(-179.0, Some(179.0), Some(SETTLED)).unwrap();
assert!(
approx_eq(step.error_deg, 2.0, 1e-3),
"error {} should wrap to ~+2",
step.error_deg
);
assert!(approx_eq(step.correction_deg, 0.01, 1e-4));
}
#[test]
fn rejects_implausibly_large_error() {
let c = YawCorrector::new(0.0);
let step = c.step(50.0, Some(0.0), Some(SETTLED));
assert!(step.is_none(), "expected None, got {step:?}");
}
#[test]
fn rejects_nan_inputs() {
let c = YawCorrector::new(0.0);
assert!(c.step(f32::NAN, Some(0.0), Some(SETTLED)).is_none());
assert!(c.step(0.0, Some(f32::NAN), Some(SETTLED)).is_none());
}
#[test]
fn many_steps_converge_to_correct_offset() {
let c = YawCorrector::new(0.0);
let true_drift_deg: f32 = 5.0;
let sp: f32 = 0.0;
let mut offset: f32 = 0.0;
for _ in 0..3000 {
let raw = sp + true_drift_deg;
let pv = raw - offset;
let Some(step) = c.step(pv, Some(sp), Some(SETTLED)) else {
panic!("corrector should fire when settled and SP is set");
};
offset += step.correction_deg;
}
assert!(
approx_eq(offset, true_drift_deg, 0.05),
"offset {} did not converge to {} within 0.05°",
offset,
true_drift_deg
);
}
}