Skip to main content

autocore_std/motion/
pressure_control.rs

1use crate::fb::StateMachine;
2use super::axis_view::AxisHandle;
3
4/// Configuration for the Pressure Control function block.
5#[derive(Debug, Clone)]
6pub struct PressureControlConfig {
7    /// Proportional gain (Kp).
8    pub kp: f64,
9    /// Integral gain (Ki).
10    pub ki: f64,
11    /// Derivative gain (Kd).
12    pub kd: f64,
13    /// Feed forward value added directly to the output.
14    pub feed_forward: f64,
15    /// Maximum allowed position delta (in user units) per call/tick.
16    /// Critical for safety to prevent crushing the load cell.
17    /// E.g., 0.001 inches per ms tick.
18    pub max_step: f64,
19    /// Maximum accumulated integral windup.
20    pub max_integral: f64,
21    /// Exponential Moving Average filter coefficient for the load cell (0.0 to 1.0).
22    /// 1.0 = No filtering (raw data).
23    /// 0.1 = Heavy filtering.
24    pub filter_alpha: f64,
25    /// If true, a positive error (need more pressure) results in a *negative* move.
26    /// Set to true if moving the axis down (negative) increases compression.
27    pub invert_direction: bool,
28    /// The acceptable load error window to be considered "in tolerance" (e.g., +/- 2.0 lbs).
29    pub tolerance: f64,
30    /// How long the load must remain within `tolerance` before reporting `in_tolerance = true`.
31    pub settling_time: f64,
32}
33
34impl Default for PressureControlConfig {
35    fn default() -> Self {
36        Self {
37            kp: 0.0,
38            ki: 0.0,
39            kd: 0.0,
40            feed_forward: 0.0,
41            max_step: 0.005, // Conservative 5 thousandths max step
42            max_integral: 100.0,
43            filter_alpha: 0.5,
44            invert_direction: false,
45            tolerance: 1.0,
46            settling_time: 0.1,
47        }
48    }
49}
50
51/// A closed-loop PID pressure/force controller for Profile Position (PP) axes.
52///
53/// This function block uses an Exponential Moving Average (EMA) filter to smooth
54/// incoming load cell data. It calculates a PID output which is clamped to a safe
55/// maximum step size and issued as a small, incremental `move_absolute` target
56/// to the drive every cycle.
57#[derive(Debug, Clone)]
58pub struct PressureControl {
59    /// True when the block is actively executing and controlling the axis.
60    pub active: bool,
61    /// True when the current load has been within `config.tolerance` of the target
62    /// for at least `config.settling_time` seconds.
63    pub in_tolerance: bool,
64    /// True if a fault occurred (e.g., axis error). Check `state.error_code`.
65    pub error: bool,
66    /// Internal state machine for operation sequencing and error reporting.
67    pub state: StateMachine,
68
69    // Internal PID state
70    integral: f64,
71    prev_error: f64,
72    filtered_load: f64,
73    
74    // Internal Setpoint state (to prevent read-modify-write jitter on actual position)
75    commanded_position: f64,
76
77    // Tolerance timer
78    settling_timer: f64,
79    
80    // First execution flag
81    is_first_run: bool,
82}
83
84impl Default for PressureControl {
85    fn default() -> Self {
86        Self {
87            active: false,
88            in_tolerance: false,
89            error: false,
90            state: StateMachine::new(),
91            integral: 0.0,
92            prev_error: 0.0,
93            filtered_load: 0.0,
94            commanded_position: 0.0,
95            settling_timer: 0.0,
96            is_first_run: true,
97        }
98    }
99}
100
101impl PressureControl {
102    /// Creates a new, uninitialized PressureControl block.
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// Execute the pressure control loop.
108    ///
109    /// - `axis`: The handle to the PP axis.
110    /// - `execute`: Set to true to engage the controller. On a falling edge, the axis halts.
111    /// - `target_load`: The desired pressure/force (e.g., lbs or Newtons).
112    /// - `current_load`: The raw, instantaneous reading from the load cell.
113    /// - `config`: Tuning and safety parameters.
114    /// - `dt`: The cycle time delta in seconds (e.g., 0.001 for 1ms).
115    pub fn call(
116        &mut self,
117        axis: &mut impl AxisHandle,
118        execute: bool,
119        target_load: f64,
120        current_load: f64,
121        config: &PressureControlConfig,
122        dt: f64,
123    ) {
124        // Handle abort/halt on falling edge of execute
125        if !execute {
126            if self.active {
127                axis.halt();
128                self.active = false;
129                self.in_tolerance = false;
130                self.state.index = 0; // Idle
131            }
132            self.is_first_run = true;
133            self.integral = 0.0;
134            self.prev_error = 0.0;
135            self.settling_timer = 0.0;
136            self.error = false;
137            return;
138        }
139
140        // Safety: Check for axis faults
141        if axis.is_error() {
142            self.error = true;
143            self.active = false;
144            self.in_tolerance = false;
145            self.state.set_error(100, "Axis is in error state");
146            return;
147        }
148
149        // 1. Initialize on rising edge
150        if self.is_first_run {
151            self.filtered_load = current_load; // Seed filter
152            self.commanded_position = axis.position(); // Seed trajectory from current reality
153            self.is_first_run = false;
154            self.active = true;
155            self.error = false;
156            self.state.index = 10; // Controlling
157        }
158
159        // 2. Exponential Moving Average (EMA) Filter
160        let alpha = config.filter_alpha.clamp(0.0, 1.0);
161        self.filtered_load = (alpha * current_load) + ((1.0 - alpha) * self.filtered_load);
162
163        // 3. PID Calculation
164        let error = target_load - self.filtered_load;
165        
166        // Anti-windup Integral
167        self.integral += error * dt;
168        self.integral = self.integral.clamp(-config.max_integral, config.max_integral);
169
170        // Derivative (change in error over time)
171        let derivative = if dt > 0.0 { (error - self.prev_error) / dt } else { 0.0 };
172        self.prev_error = error;
173
174        // Calculate raw output
175        let mut output = (config.kp * error) + (config.ki * self.integral) + (config.kd * derivative) + config.feed_forward;
176
177        // 4. Direction & Clamping
178        if config.invert_direction {
179            output = -output;
180        }
181        
182        // Clamp output to the maximum safe step size
183        let step = output.clamp(-config.max_step, config.max_step);
184
185        // 5. Issue PP Move
186        // We accumulate the internal commanded_position rather than reading axis.position()
187        // every tick. Reading actual position every tick to calculate the next target
188        // creates a positive feedback loop of lag that destabilizes the PID.
189        self.commanded_position += step;
190        
191        // We use move_absolute to our internal tracking position rather than move_relative
192        // to prevent compounding loss of precision over thousands of micro-moves.
193        let vel = axis.config().jog_speed;
194        let acc = axis.config().jog_accel;
195        let dec = axis.config().jog_decel;
196        
197        // Only command motion if we actually need to move a non-zero distance.
198        if step.abs() > f64::EPSILON {
199            // Note: If the axis options include dynamic max/min position limits, 
200            // the DriveHandle's internal move_absolute implementation will automatically
201            // reject this move if it violates the safety envelope.
202            axis.move_absolute(self.commanded_position, vel, acc, dec);
203        }
204
205        // 6. Tolerance Window Evaluation
206        if error.abs() <= config.tolerance {
207            self.settling_timer += dt;
208            if self.settling_timer >= config.settling_time {
209                self.in_tolerance = true;
210            }
211        } else {
212            self.settling_timer = 0.0;
213            self.in_tolerance = false;
214        }
215    }
216}