use crate::fb::StateMachine;
use super::axis_view::AxisHandle;
#[derive(Debug, Clone)]
pub struct PressureControlConfig {
pub kp: f64,
pub ki: f64,
pub kd: f64,
pub feed_forward: f64,
pub max_step: f64,
pub min_step: f64,
pub max_integral: f64,
pub filter_alpha: f64,
pub invert_direction: bool,
pub tolerance: f64,
pub settling_time: f64,
pub position_limit_pos: Option<f64>,
pub position_limit_neg: Option<f64>,
}
impl Default for PressureControlConfig {
fn default() -> Self {
Self {
kp: 0.0,
ki: 0.0,
kd: 0.0,
feed_forward: 0.0,
max_step: 0.005, min_step: 0.0, max_integral: 100.0,
filter_alpha: 0.5,
invert_direction: false,
tolerance: 1.0,
settling_time: 0.1,
position_limit_pos: None,
position_limit_neg: None,
}
}
}
#[repr(i32)]
#[derive(Copy, Clone, PartialEq, Debug)]
enum PcState {
Idle = 0,
Controlling = 10,
Halted = 20,
Error = 30,
}
impl PcState {
fn from_index(idx: i32) -> Option<Self> {
match idx {
x if x == Self::Idle as i32 => Some(Self::Idle),
x if x == Self::Controlling as i32 => Some(Self::Controlling),
x if x == Self::Halted as i32 => Some(Self::Halted),
x if x == Self::Error as i32 => Some(Self::Error),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct PressureControl {
state: StateMachine,
in_tolerance: bool,
target_load: f64,
max_load: f64,
integral: f64,
prev_error: f64,
filtered_load: f64,
commanded_position: f64,
last_pid_error: f64,
settling_timer: f64,
is_first_run: bool,
}
impl Default for PressureControl {
fn default() -> Self {
Self {
state: StateMachine::new(),
in_tolerance: false,
target_load: 0.0,
max_load: f64::INFINITY,
integral: 0.0,
prev_error: 0.0,
filtered_load: 0.0,
commanded_position: 0.0,
last_pid_error: 0.0,
settling_timer: 0.0,
is_first_run: false,
}
}
}
impl PressureControl {
pub fn new() -> Self {
Self::default()
}
pub fn start(&mut self, target_load: f64, max_load: f64) {
self.state.clear_error();
self.target_load = target_load;
self.max_load = max_load;
self.integral = 0.0;
self.prev_error = 0.0;
self.last_pid_error = 0.0;
self.settling_timer = 0.0;
self.in_tolerance = false;
self.is_first_run = true;
self.state.index = PcState::Controlling as i32;
}
pub fn stop(&mut self, axis: &mut impl AxisHandle) {
let idx = self.state.index;
if idx == PcState::Idle as i32 || idx == PcState::Halted as i32 {
return;
}
axis.halt();
self.in_tolerance = false;
self.state.index = PcState::Halted as i32;
}
pub fn set_target(&mut self, target_load: f64) {
self.target_load = target_load;
}
pub fn set_max_load(&mut self, max_load: f64) {
self.max_load = max_load;
}
pub fn is_busy(&self) -> bool {
let idx = self.state.index;
idx == PcState::Controlling as i32 || idx == PcState::Halted as i32
}
pub fn is_error(&self) -> bool {
self.state.index == PcState::Error as i32 || self.state.is_error()
}
pub fn error_message(&self) -> String {
self.state.error_message.clone()
}
pub fn is_in_tolerance(&self) -> bool {
self.in_tolerance
}
pub fn filtered_load(&self) -> f64 {
self.filtered_load
}
pub fn commanded_position(&self) -> f64 {
self.commanded_position
}
pub fn pid_error(&self) -> f64 {
self.last_pid_error
}
pub fn tick(
&mut self,
axis: &mut impl AxisHandle,
current_load: f64,
config: &PressureControlConfig,
dt: f64,
) {
if axis.is_error() && self.state.index != PcState::Idle as i32 {
self.state.set_error(100, "Axis is in error state");
self.state.index = PcState::Error as i32;
return;
}
match PcState::from_index(self.state.index) {
Some(PcState::Idle) | Some(PcState::Error) | None => {
}
Some(PcState::Halted) => {
if !axis.is_busy() {
self.state.index = PcState::Idle as i32;
}
}
Some(PcState::Controlling) => {
if current_load.abs() > self.max_load {
axis.halt();
self.state.set_error(
110,
format!(
"load {} exceeded max_load {}",
current_load, self.max_load,
),
);
self.state.index = PcState::Error as i32;
return;
}
if self.is_first_run {
self.filtered_load = current_load;
self.commanded_position = axis.position();
self.is_first_run = false;
}
let alpha = config.filter_alpha.clamp(0.0, 1.0);
self.filtered_load = alpha * current_load
+ (1.0 - alpha) * self.filtered_load;
let error = self.target_load - self.filtered_load;
self.last_pid_error = error;
let derivative = if dt > 0.0 {
(error - self.prev_error) / dt
} else {
0.0
};
self.prev_error = error;
let integral_candidate = (self.integral + error * dt)
.clamp(-config.max_integral, config.max_integral);
let raw_output = config.kp * error
+ config.ki * integral_candidate
+ config.kd * derivative
+ config.feed_forward;
let signed_output = if config.invert_direction {
-raw_output
} else {
raw_output
};
let saturated = signed_output.abs() > config.max_step;
let step = signed_output.clamp(-config.max_step, config.max_step);
if !saturated {
self.integral = integral_candidate;
}
let next_cmd = self.commanded_position + step;
let clamped = match (config.position_limit_neg, config.position_limit_pos) {
(Some(lo), Some(hi)) => next_cmd.clamp(lo, hi),
(Some(lo), None) => next_cmd.max(lo),
(None, Some(hi)) => next_cmd.min(hi),
(None, None) => next_cmd,
};
if clamped != next_cmd {
self.integral = self.integral - error * dt;
self.integral = self.integral.clamp(-config.max_integral, config.max_integral);
}
self.commanded_position = clamped;
let issued_step = self.commanded_position - axis.position();
if issued_step.abs() > config.min_step {
let vel = axis.config().jog_speed;
let acc = axis.config().jog_accel;
let dec = axis.config().jog_decel;
axis.move_absolute(self.commanded_position, vel, acc, dec);
}
if error.abs() <= config.tolerance {
self.settling_timer += dt;
if self.settling_timer >= config.settling_time {
self.in_tolerance = true;
}
} else {
self.settling_timer = 0.0;
self.in_tolerance = false;
}
}
}
self.state.call();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::motion::axis_config::AxisConfig;
struct MockAxis {
position: f64,
busy: bool,
error: bool,
config: AxisConfig,
halt_calls: u32,
last_move_target: f64,
move_calls: u32,
spring_k: f64,
rest_position: f64,
}
impl MockAxis {
fn new() -> Self {
let mut cfg = AxisConfig::new(10_000);
cfg.jog_speed = 5.0;
cfg.jog_accel = 50.0;
cfg.jog_decel = 50.0;
Self {
position: 0.0, busy: false, error: false,
config: cfg,
halt_calls: 0, last_move_target: 0.0, move_calls: 0,
spring_k: 0.0, rest_position: 0.0,
}
}
fn with_spring(mut self, k: f64, rest: f64) -> Self {
self.spring_k = k;
self.rest_position = rest;
self
}
fn simulated_load(&self) -> f64 {
self.spring_k * (self.position - self.rest_position)
}
fn advance(&mut self) {
self.position = self.last_move_target;
}
}
impl AxisHandle for MockAxis {
fn position(&self) -> f64 { self.position }
fn config(&self) -> &AxisConfig { &self.config }
fn move_absolute(&mut self, p: f64, _v: f64, _a: f64, _d: f64) {
self.last_move_target = p;
self.move_calls += 1;
self.busy = true;
}
fn move_relative(&mut self, _: f64, _: f64, _: f64, _: f64) {}
fn halt(&mut self) {
self.halt_calls += 1;
self.busy = false;
}
fn is_busy(&self) -> bool { self.busy }
fn is_error(&self) -> bool { self.error }
fn motor_on(&self) -> bool { true }
}
fn zero_pid_config() -> PressureControlConfig {
PressureControlConfig { ..Default::default() }
}
#[test]
fn tick_is_noop_when_idle() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
let cfg = zero_pid_config();
pc.tick(&mut axis, 0.0, &cfg, 0.01);
assert!(!pc.is_busy());
assert_eq!(axis.move_calls, 0);
assert_eq!(axis.halt_calls, 0);
}
#[test]
fn start_then_tick_seeds_from_axis() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
axis.position = 3.25;
pc.start(10.0, 500.0);
assert!(pc.is_busy());
pc.tick(&mut axis, 0.0, &zero_pid_config(), 0.01);
assert_eq!(pc.commanded_position(), 3.25);
assert_eq!(pc.filtered_load(), 0.0);
}
#[test]
fn stop_halts_and_transitions_to_idle() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
pc.start(10.0, 500.0);
pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
assert!(pc.is_busy());
pc.stop(&mut axis);
assert_eq!(axis.halt_calls, 1);
assert!(pc.is_busy(), "still busy until axis finishes halt");
axis.busy = false; pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
assert!(!pc.is_busy());
}
#[test]
fn max_load_triggers_error_and_halt() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
pc.start(10.0, 100.0);
pc.tick(&mut axis, 150.0, &zero_pid_config(), 0.01);
assert!(pc.is_error());
assert_eq!(axis.halt_calls, 1);
assert!(pc.error_message().contains("exceeded max_load"));
}
#[test]
fn loop_converges_toward_target() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new().with_spring(100.0, 0.0);
let cfg = PressureControlConfig {
kp: 0.01, ki: 0.0,
kd: 0.0,
max_step: 0.5,
min_step: 0.0,
filter_alpha: 1.0,
tolerance: 1.0,
settling_time: 0.0,
..Default::default()
};
pc.start(50.0, 500.0);
for _ in 0..200 {
let load = axis.simulated_load();
pc.tick(&mut axis, load, &cfg, 0.01);
axis.advance();
}
let final_load = axis.simulated_load();
assert!(
(final_load - 50.0).abs() < 1.0,
"expected load near 50 N, got {}", final_load,
);
assert!(pc.is_in_tolerance());
}
#[test]
fn invert_direction_flips_step_sign() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new().with_spring(-100.0, 0.0);
let cfg = PressureControlConfig {
kp: 0.01,
invert_direction: true,
max_step: 0.5,
min_step: 0.0,
filter_alpha: 1.0,
tolerance: 1.0,
settling_time: 0.0,
..Default::default()
};
pc.start(50.0, 500.0);
for _ in 0..200 {
let load = axis.simulated_load();
pc.tick(&mut axis, load, &cfg, 0.01);
axis.advance();
}
assert!(axis.position < 0.0, "expected negative position, got {}", axis.position);
}
#[test]
fn min_step_deadband_suppresses_move() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
let cfg = PressureControlConfig {
kp: 0.0001, max_step: 0.5,
min_step: 0.01, filter_alpha: 1.0,
..Default::default()
};
pc.start(1.0, 500.0);
pc.tick(&mut axis, 0.0, &cfg, 0.01); let first_move_calls = axis.move_calls;
for _ in 0..10 {
pc.tick(&mut axis, 0.0, &cfg, 0.01);
}
assert_eq!(axis.move_calls, first_move_calls,
"min_step should have suppressed all these moves");
}
#[test]
fn position_limit_clamps_commanded() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
let cfg = PressureControlConfig {
kp: 1.0,
max_step: 0.5,
min_step: 0.0,
filter_alpha: 1.0,
position_limit_pos: Some(1.0), tolerance: 1.0,
settling_time: 0.0,
..Default::default()
};
pc.start(100.0, 500.0); for _ in 0..20 {
pc.tick(&mut axis, 0.0, &cfg, 0.01);
}
assert!(pc.commanded_position() <= 1.0 + 1e-6,
"commanded position should be clamped, got {}", pc.commanded_position());
}
#[test]
fn axis_error_transitions_to_error_state() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new();
pc.start(10.0, 500.0);
pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
assert!(pc.is_busy());
axis.error = true;
pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
assert!(pc.is_error());
}
#[test]
fn set_target_changes_setpoint_without_reset() {
let mut pc = PressureControl::new();
let mut axis = MockAxis::new().with_spring(100.0, 0.0);
let cfg = PressureControlConfig {
kp: 0.01, max_step: 0.5, min_step: 0.0, filter_alpha: 1.0,
tolerance: 0.5, settling_time: 0.0, ..Default::default()
};
pc.start(50.0, 500.0);
for _ in 0..200 {
let load = axis.simulated_load();
pc.tick(&mut axis, load, &cfg, 0.01);
axis.advance();
}
assert!((axis.simulated_load() - 50.0).abs() < 1.0);
pc.set_target(30.0);
for _ in 0..200 {
let load = axis.simulated_load();
pc.tick(&mut axis, load, &cfg, 0.01);
axis.advance();
}
assert!((axis.simulated_load() - 30.0).abs() < 1.0);
}
}