#[derive(Debug, Clone)]
pub struct CorePID {
pub target: f64,
pub kp: f64,
pub ki: f64,
pub kd: f64,
pub max: f64,
pub prev_error: f64,
pub integral: f64,
pub tolerance: f64,
}
impl CorePID {
pub fn new(kp: f64, ki: f64, kd: f64, target: f64, max: f64, tolerance: f64) -> Self {
Self {
target,
kp,
ki,
kd,
max,
tolerance,
prev_error: 0.0,
integral: 0.0,
}
}
pub fn tick(&mut self, reading: f64, mut dt: f64) -> f64 {
dt = dt.max(1e-6);
let error = self.target - reading;
self.integral += error * dt;
if self.ki != 0.0 {
let integral_limit = self.max / self.ki.abs();
self.integral = self.integral.clamp(-integral_limit, integral_limit);
}
let p = self.kp * error;
let i = self.ki * self.integral;
let d = self.kd * (error - self.prev_error) / dt;
let output = (p + i + d).clamp(-self.max, self.max);
self.prev_error = error;
if error.abs() > self.tolerance {
output
} else {
0.0
}
}
pub fn reset_integral(&mut self) { self.integral = 0.0 }
pub fn set_target(&mut self, target: f64) { self.target = target; }
pub fn is_active(&self, reading: f64) -> bool {
let error = self.target - reading;
error.abs() > self.tolerance
}
}
#[cfg(test)]
mod tests {
use super::CorePID;
fn approx_eq(a: f64, b: f64, eps: f64) {
assert!(
(a - b).abs() <= eps,
"left={} right={} diff={} eps={}",
a,
b,
(a - b).abs(),
eps
);
}
#[test]
fn new_initializes_fields() {
let pid = CorePID::new(1.2, 0.3, 0.4, 10.0, 100.0, 0.5);
approx_eq(pid.kp, 1.2, 1e-12);
approx_eq(pid.ki, 0.3, 1e-12);
approx_eq(pid.kd, 0.4, 1e-12);
approx_eq(pid.target, 10.0, 1e-12);
approx_eq(pid.max, 100.0, 1e-12);
approx_eq(pid.tolerance, 0.5, 1e-12);
approx_eq(pid.prev_error, 0.0, 1e-12);
approx_eq(pid.integral, 0.0, 1e-12);
}
#[test]
fn tick_proportional_only() {
let mut pid = CorePID::new(2.0, 0.0, 0.0, 10.0, 100.0, 0.0);
let out = pid.tick(7.0, 0.1);
approx_eq(out, 6.0, 1e-12);
approx_eq(pid.prev_error, 3.0, 1e-12);
}
#[test]
fn tick_output_is_zero_inside_tolerance() {
let mut pid = CorePID::new(10.0, 0.0, 0.0, 10.0, 100.0, 0.5);
let out = pid.tick(9.7, 0.1);
approx_eq(out, 0.0, 1e-12);
assert!(!pid.is_active(9.7));
}
#[test]
fn tick_output_is_clamped_by_max() {
let mut pid = CorePID::new(100.0, 0.0, 0.0, 10.0, 5.0, 0.0);
let out = pid.tick(0.0, 0.1);
approx_eq(out, 5.0, 1e-12);
}
#[test]
fn integral_is_clamped_to_prevent_windup() {
let mut pid = CorePID::new(0.0, 2.0, 0.0, 10.0, 10.0, 0.0);
let out = pid.tick(0.0, 10.0);
approx_eq(pid.integral, 5.0, 1e-12);
approx_eq(out, 10.0, 1e-12);
}
#[test]
fn derivative_uses_dt_with_lower_bound() {
let mut pid = CorePID::new(0.0, 0.0, 1.0, 10.0, 7.0, 0.0);
let out = pid.tick(0.0, 0.0);
approx_eq(out, 7.0, 1e-12);
}
#[test]
fn reset_integral_and_set_target_work() {
let mut pid = CorePID::new(0.0, 1.0, 0.0, 10.0, 100.0, 0.0);
let _ = pid.tick(0.0, 1.0);
assert!(pid.integral > 0.0);
pid.reset_integral();
approx_eq(pid.integral, 0.0, 1e-12);
pid.set_target(42.0);
approx_eq(pid.target, 42.0, 1e-12);
}
#[test]
fn is_active_reflects_error_vs_tolerance() {
let pid = CorePID::new(1.0, 0.0, 0.0, 10.0, 100.0, 0.25);
assert!(pid.is_active(9.0)); assert!(!pid.is_active(9.9)); }
}