#![allow(clippy::excessive_precision)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct SincosEncoder<S: ControlScalar> {
pub sin_offset: S,
pub cos_offset: S,
pub sin_gain: S,
pub cos_gain: S,
sin_min: S,
sin_max: S,
cos_min: S,
cos_max: S,
pub angle: S,
pub velocity: S,
pub bandwidth: S,
prev_angle: S,
}
impl<S: ControlScalar> SincosEncoder<S> {
pub fn new(bandwidth: S) -> Self {
let big = S::from_f64(1e10);
Self {
sin_offset: S::ZERO,
cos_offset: S::ZERO,
sin_gain: S::ONE,
cos_gain: S::ONE,
sin_min: big,
sin_max: -big,
cos_min: big,
cos_max: -big,
angle: S::ZERO,
velocity: S::ZERO,
bandwidth,
prev_angle: S::ZERO,
}
}
pub fn update(&mut self, sin_raw: S, cos_raw: S, dt: S) {
let (sin_cal, cos_cal) = self.calibrate_sample(sin_raw, cos_raw);
self.update_minmax(sin_cal, cos_cal);
let new_angle = sin_cal.atan2(cos_cal);
let two_pi = S::TWO * S::PI;
let mut delta = new_angle - self.prev_angle;
if delta > S::PI {
delta -= two_pi;
} else if delta < -S::PI {
delta += two_pi;
}
self.angle = new_angle;
self.prev_angle = new_angle;
if dt > S::EPSILON {
let raw_vel = delta / dt;
let alpha = (self.bandwidth * dt).min(S::ONE);
self.velocity = self.velocity + alpha * (raw_vel - self.velocity);
}
}
fn calibrate_sample(&self, sin_raw: S, cos_raw: S) -> (S, S) {
let sin_cal = (sin_raw - self.sin_offset) * self.sin_gain;
let cos_cal = (cos_raw - self.cos_offset) * self.cos_gain;
(sin_cal, cos_cal)
}
fn update_minmax(&mut self, sin_cal: S, cos_cal: S) {
if sin_cal < self.sin_min {
self.sin_min = sin_cal;
}
if sin_cal > self.sin_max {
self.sin_max = sin_cal;
}
if cos_cal < self.cos_min {
self.cos_min = cos_cal;
}
if cos_cal > self.cos_max {
self.cos_max = cos_cal;
}
let sin_range = self.sin_max - self.sin_min;
let cos_range = self.cos_max - self.cos_min;
if sin_range > S::from_f64(0.1) {
let sin_center = (self.sin_max + self.sin_min) * S::HALF;
self.sin_offset += sin_center / self.sin_gain;
if sin_range > S::EPSILON {
let target_range = S::TWO;
self.sin_gain *= target_range / sin_range;
}
let big = S::from_f64(1e10);
self.sin_min = big;
self.sin_max = -big;
}
if cos_range > S::from_f64(0.1) {
let cos_center = (self.cos_max + self.cos_min) * S::HALF;
self.cos_offset += cos_center / self.cos_gain;
if cos_range > S::EPSILON {
let target_range = S::TWO;
self.cos_gain *= target_range / cos_range;
}
let big = S::from_f64(1e10);
self.cos_min = big;
self.cos_max = -big;
}
}
pub fn angle_deg(&self) -> S {
self.angle * S::from_f64(180.0 / core::f64::consts::PI)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_angle_at_zero() {
let mut enc = SincosEncoder::<f32>::new(100.0);
enc.update(0.0, 1.0, 0.001);
assert!(enc.angle.abs() < 1e-5, "angle={}", enc.angle);
}
#[test]
fn test_angle_at_90_degrees() {
let mut enc = SincosEncoder::<f32>::new(100.0);
enc.update(1.0, 0.0, 0.001);
let expected = core::f32::consts::PI / 2.0;
assert!(
(enc.angle - expected).abs() < 1e-5,
"angle={}, expected={expected}",
enc.angle
);
}
#[test]
fn test_velocity_estimation() {
let mut enc = SincosEncoder::<f32>::new(10.0);
let dt = 0.001_f32;
let omega = 100.0_f32; let mut theta = 0.0_f32;
for _ in 0..10000 {
theta += omega * dt;
enc.update(theta.sin(), theta.cos(), dt);
}
let v = enc.velocity;
assert!((v - omega).abs() < 5.0, "velocity={v}, expected≈{omega}");
}
#[test]
fn test_angle_at_180_degrees() {
let mut enc = SincosEncoder::<f32>::new(100.0);
enc.update(0.0, -1.0, 0.001);
assert!(
(enc.angle.abs() - core::f32::consts::PI).abs() < 1e-4,
"angle={}",
enc.angle
);
}
}