1use crate::fb::StateMachine;
33use super::axis_view::AxisHandle;
34
35#[derive(Debug, Clone)]
37pub struct PressureControlConfig {
38 pub kp: f64,
40 pub ki: f64,
42 pub kd: f64,
44 pub feed_forward: f64,
46 pub max_step: f64,
50 pub min_step: f64,
55 pub max_integral: f64,
57 pub filter_alpha: f64,
61 pub invert_direction: bool,
64 pub tolerance: f64,
66 pub settling_time: f64,
68 pub position_limit_pos: Option<f64>,
73 pub position_limit_neg: Option<f64>,
76}
77
78impl Default for PressureControlConfig {
79 fn default() -> Self {
80 Self {
81 kp: 0.0,
82 ki: 0.0,
83 kd: 0.0,
84 feed_forward: 0.0,
85 max_step: 0.005, min_step: 0.0, max_integral: 100.0,
88 filter_alpha: 0.5,
89 invert_direction: false,
90 tolerance: 1.0,
91 settling_time: 0.1,
92 position_limit_pos: None,
93 position_limit_neg: None,
94 }
95 }
96}
97
98#[repr(i32)]
99#[derive(Copy, Clone, PartialEq, Debug)]
100enum PcState {
101 Idle = 0,
102 Controlling = 10,
103 Halted = 20,
106 Error = 30,
107}
108
109impl PcState {
110 fn from_index(idx: i32) -> Option<Self> {
111 match idx {
112 x if x == Self::Idle as i32 => Some(Self::Idle),
113 x if x == Self::Controlling as i32 => Some(Self::Controlling),
114 x if x == Self::Halted as i32 => Some(Self::Halted),
115 x if x == Self::Error as i32 => Some(Self::Error),
116 _ => None,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct PressureControl {
124 state: StateMachine,
126
127 in_tolerance: bool,
131
132 target_load: f64,
134 max_load: f64,
135
136 integral: f64,
138 prev_error: f64,
139 filtered_load: f64,
140
141 commanded_position: f64,
145
146 last_pid_error: f64,
148
149 settling_timer: f64,
151
152 is_first_run: bool,
156}
157
158impl Default for PressureControl {
159 fn default() -> Self {
160 Self {
161 state: StateMachine::new(),
162 in_tolerance: false,
163 target_load: 0.0,
164 max_load: f64::INFINITY,
165 integral: 0.0,
166 prev_error: 0.0,
167 filtered_load: 0.0,
168 commanded_position: 0.0,
169 last_pid_error: 0.0,
170 settling_timer: 0.0,
171 is_first_run: false,
172 }
173 }
174}
175
176impl PressureControl {
177 pub fn new() -> Self {
179 Self::default()
180 }
181
182 pub fn start(&mut self, target_load: f64, max_load: f64) {
190 self.state.clear_error();
191 self.target_load = target_load;
192 self.max_load = max_load;
193 self.integral = 0.0;
194 self.prev_error = 0.0;
195 self.last_pid_error = 0.0;
196 self.settling_timer = 0.0;
197 self.in_tolerance = false;
198 self.is_first_run = true;
199 self.state.index = PcState::Controlling as i32;
200 }
201
202 pub fn stop(&mut self, axis: &mut impl AxisHandle) {
205 let idx = self.state.index;
206 if idx == PcState::Idle as i32 || idx == PcState::Halted as i32 {
207 return;
208 }
209 axis.halt();
210 self.in_tolerance = false;
211 self.state.index = PcState::Halted as i32;
212 }
213
214 pub fn set_target(&mut self, target_load: f64) {
217 self.target_load = target_load;
218 }
219
220 pub fn set_max_load(&mut self, max_load: f64) {
222 self.max_load = max_load;
223 }
224
225 pub fn is_busy(&self) -> bool {
227 let idx = self.state.index;
228 idx == PcState::Controlling as i32 || idx == PcState::Halted as i32
229 }
230
231 pub fn is_error(&self) -> bool {
233 self.state.index == PcState::Error as i32 || self.state.is_error()
234 }
235
236 pub fn error_message(&self) -> String {
238 self.state.error_message.clone()
239 }
240
241 pub fn is_in_tolerance(&self) -> bool {
244 self.in_tolerance
245 }
246
247 pub fn filtered_load(&self) -> f64 {
249 self.filtered_load
250 }
251
252 pub fn commanded_position(&self) -> f64 {
255 self.commanded_position
256 }
257
258 pub fn pid_error(&self) -> f64 {
261 self.last_pid_error
262 }
263
264 pub fn tick(
266 &mut self,
267 axis: &mut impl AxisHandle,
268 current_load: f64,
269 config: &PressureControlConfig,
270 dt: f64,
271 ) {
272 if axis.is_error() && self.state.index != PcState::Idle as i32 {
274 self.state.set_error(100, "Axis is in error state");
275 self.state.index = PcState::Error as i32;
276 return;
277 }
278
279 match PcState::from_index(self.state.index) {
280 Some(PcState::Idle) | Some(PcState::Error) | None => {
281 }
284
285 Some(PcState::Halted) => {
286 if !axis.is_busy() {
289 self.state.index = PcState::Idle as i32;
290 }
291 }
292
293 Some(PcState::Controlling) => {
294 if current_load.abs() > self.max_load {
299 axis.halt();
300 self.state.set_error(
301 110,
302 format!(
303 "load {} exceeded max_load {}",
304 current_load, self.max_load,
305 ),
306 );
307 self.state.index = PcState::Error as i32;
308 return;
309 }
310
311 if self.is_first_run {
315 self.filtered_load = current_load;
316 self.commanded_position = axis.position();
317 self.is_first_run = false;
318 }
319
320 let alpha = config.filter_alpha.clamp(0.0, 1.0);
322 self.filtered_load = alpha * current_load
323 + (1.0 - alpha) * self.filtered_load;
324
325 let error = self.target_load - self.filtered_load;
327 self.last_pid_error = error;
328
329 let derivative = if dt > 0.0 {
330 (error - self.prev_error) / dt
331 } else {
332 0.0
333 };
334 self.prev_error = error;
335
336 let integral_candidate = (self.integral + error * dt)
340 .clamp(-config.max_integral, config.max_integral);
341 let raw_output = config.kp * error
342 + config.ki * integral_candidate
343 + config.kd * derivative
344 + config.feed_forward;
345
346 let signed_output = if config.invert_direction {
349 -raw_output
350 } else {
351 raw_output
352 };
353 let saturated = signed_output.abs() > config.max_step;
354 let step = signed_output.clamp(-config.max_step, config.max_step);
355
356 if !saturated {
361 self.integral = integral_candidate;
362 }
363
364 let next_cmd = self.commanded_position + step;
368 let clamped = match (config.position_limit_neg, config.position_limit_pos) {
369 (Some(lo), Some(hi)) => next_cmd.clamp(lo, hi),
370 (Some(lo), None) => next_cmd.max(lo),
371 (None, Some(hi)) => next_cmd.min(hi),
372 (None, None) => next_cmd,
373 };
374 if clamped != next_cmd {
375 self.integral = self.integral - error * dt;
378 self.integral = self.integral.clamp(-config.max_integral, config.max_integral);
379 }
380 self.commanded_position = clamped;
381
382 let issued_step = self.commanded_position - axis.position();
384 if issued_step.abs() > config.min_step {
385 let vel = axis.config().jog_speed;
386 let acc = axis.config().jog_accel;
387 let dec = axis.config().jog_decel;
388 axis.move_absolute(self.commanded_position, vel, acc, dec);
389 }
390
391 if error.abs() <= config.tolerance {
393 self.settling_timer += dt;
394 if self.settling_timer >= config.settling_time {
395 self.in_tolerance = true;
396 }
397 } else {
398 self.settling_timer = 0.0;
399 self.in_tolerance = false;
400 }
401 }
402 }
403
404 self.state.call();
405 }
406}
407
408#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::motion::axis_config::AxisConfig;
416
417 struct MockAxis {
418 position: f64,
419 busy: bool,
420 error: bool,
421 config: AxisConfig,
422 halt_calls: u32,
423 last_move_target: f64,
424 move_calls: u32,
425 spring_k: f64,
428 rest_position: f64,
429 }
430
431 impl MockAxis {
432 fn new() -> Self {
433 let mut cfg = AxisConfig::new(10_000);
434 cfg.jog_speed = 5.0;
435 cfg.jog_accel = 50.0;
436 cfg.jog_decel = 50.0;
437 Self {
438 position: 0.0, busy: false, error: false,
439 config: cfg,
440 halt_calls: 0, last_move_target: 0.0, move_calls: 0,
441 spring_k: 0.0, rest_position: 0.0,
442 }
443 }
444 fn with_spring(mut self, k: f64, rest: f64) -> Self {
445 self.spring_k = k;
446 self.rest_position = rest;
447 self
448 }
449 fn simulated_load(&self) -> f64 {
450 self.spring_k * (self.position - self.rest_position)
451 }
452 fn advance(&mut self) {
456 self.position = self.last_move_target;
457 }
458 }
459
460 impl AxisHandle for MockAxis {
461 fn position(&self) -> f64 { self.position }
462 fn config(&self) -> &AxisConfig { &self.config }
463 fn move_absolute(&mut self, p: f64, _v: f64, _a: f64, _d: f64) {
464 self.last_move_target = p;
465 self.move_calls += 1;
466 self.busy = true;
467 }
468 fn move_relative(&mut self, _: f64, _: f64, _: f64, _: f64) {}
469 fn halt(&mut self) {
470 self.halt_calls += 1;
471 self.busy = false;
472 }
473 fn is_busy(&self) -> bool { self.busy }
474 fn is_error(&self) -> bool { self.error }
475 fn motor_on(&self) -> bool { true }
476 }
477
478 fn zero_pid_config() -> PressureControlConfig {
479 PressureControlConfig { ..Default::default() }
480 }
481
482 #[test]
483 fn tick_is_noop_when_idle() {
484 let mut pc = PressureControl::new();
485 let mut axis = MockAxis::new();
486 let cfg = zero_pid_config();
487 pc.tick(&mut axis, 0.0, &cfg, 0.01);
488 assert!(!pc.is_busy());
489 assert_eq!(axis.move_calls, 0);
490 assert_eq!(axis.halt_calls, 0);
491 }
492
493 #[test]
494 fn start_then_tick_seeds_from_axis() {
495 let mut pc = PressureControl::new();
496 let mut axis = MockAxis::new();
497 axis.position = 3.25;
498 pc.start(10.0, 500.0);
499 assert!(pc.is_busy());
500 pc.tick(&mut axis, 0.0, &zero_pid_config(), 0.01);
502 assert_eq!(pc.commanded_position(), 3.25);
503 assert_eq!(pc.filtered_load(), 0.0);
504 }
505
506 #[test]
507 fn stop_halts_and_transitions_to_idle() {
508 let mut pc = PressureControl::new();
509 let mut axis = MockAxis::new();
510 pc.start(10.0, 500.0);
511 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
512 assert!(pc.is_busy());
513 pc.stop(&mut axis);
514 assert_eq!(axis.halt_calls, 1);
515 assert!(pc.is_busy(), "still busy until axis finishes halt");
516 axis.busy = false; pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
518 assert!(!pc.is_busy());
519 }
520
521 #[test]
522 fn max_load_triggers_error_and_halt() {
523 let mut pc = PressureControl::new();
524 let mut axis = MockAxis::new();
525 pc.start(10.0, 100.0);
526 pc.tick(&mut axis, 150.0, &zero_pid_config(), 0.01);
527 assert!(pc.is_error());
528 assert_eq!(axis.halt_calls, 1);
529 assert!(pc.error_message().contains("exceeded max_load"));
530 }
531
532 #[test]
533 fn loop_converges_toward_target() {
534 let mut pc = PressureControl::new();
536 let mut axis = MockAxis::new().with_spring(100.0, 0.0);
537 let cfg = PressureControlConfig {
538 kp: 0.01, ki: 0.0,
540 kd: 0.0,
541 max_step: 0.5,
542 min_step: 0.0,
543 filter_alpha: 1.0,
544 tolerance: 1.0,
545 settling_time: 0.0,
546 ..Default::default()
547 };
548 pc.start(50.0, 500.0);
549 for _ in 0..200 {
550 let load = axis.simulated_load();
551 pc.tick(&mut axis, load, &cfg, 0.01);
552 axis.advance();
553 }
554 let final_load = axis.simulated_load();
555 assert!(
556 (final_load - 50.0).abs() < 1.0,
557 "expected load near 50 N, got {}", final_load,
558 );
559 assert!(pc.is_in_tolerance());
560 }
561
562 #[test]
563 fn invert_direction_flips_step_sign() {
564 let mut pc = PressureControl::new();
570 let mut axis = MockAxis::new().with_spring(-100.0, 0.0);
571 let cfg = PressureControlConfig {
572 kp: 0.01,
573 invert_direction: true,
574 max_step: 0.5,
575 min_step: 0.0,
576 filter_alpha: 1.0,
577 tolerance: 1.0,
578 settling_time: 0.0,
579 ..Default::default()
580 };
581 pc.start(50.0, 500.0);
582 for _ in 0..200 {
583 let load = axis.simulated_load();
584 pc.tick(&mut axis, load, &cfg, 0.01);
585 axis.advance();
586 }
587 assert!(axis.position < 0.0, "expected negative position, got {}", axis.position);
588 }
589
590 #[test]
591 fn min_step_deadband_suppresses_move() {
592 let mut pc = PressureControl::new();
593 let mut axis = MockAxis::new();
594 let cfg = PressureControlConfig {
595 kp: 0.0001, max_step: 0.5,
597 min_step: 0.01, filter_alpha: 1.0,
599 ..Default::default()
600 };
601 pc.start(1.0, 500.0);
602 pc.tick(&mut axis, 0.0, &cfg, 0.01); let first_move_calls = axis.move_calls;
604 for _ in 0..10 {
606 pc.tick(&mut axis, 0.0, &cfg, 0.01);
607 }
608 assert_eq!(axis.move_calls, first_move_calls,
609 "min_step should have suppressed all these moves");
610 }
611
612 #[test]
613 fn position_limit_clamps_commanded() {
614 let mut pc = PressureControl::new();
615 let mut axis = MockAxis::new();
616 let cfg = PressureControlConfig {
617 kp: 1.0,
618 max_step: 0.5,
619 min_step: 0.0,
620 filter_alpha: 1.0,
621 position_limit_pos: Some(1.0), tolerance: 1.0,
623 settling_time: 0.0,
624 ..Default::default()
625 };
626 pc.start(100.0, 500.0); for _ in 0..20 {
628 pc.tick(&mut axis, 0.0, &cfg, 0.01);
629 }
630 assert!(pc.commanded_position() <= 1.0 + 1e-6,
631 "commanded position should be clamped, got {}", pc.commanded_position());
632 }
633
634 #[test]
635 fn axis_error_transitions_to_error_state() {
636 let mut pc = PressureControl::new();
637 let mut axis = MockAxis::new();
638 pc.start(10.0, 500.0);
639 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
640 assert!(pc.is_busy());
641 axis.error = true;
642 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
643 assert!(pc.is_error());
644 }
645
646 #[test]
647 fn set_target_changes_setpoint_without_reset() {
648 let mut pc = PressureControl::new();
649 let mut axis = MockAxis::new().with_spring(100.0, 0.0);
650 let cfg = PressureControlConfig {
651 kp: 0.01, max_step: 0.5, min_step: 0.0, filter_alpha: 1.0,
652 tolerance: 0.5, settling_time: 0.0, ..Default::default()
653 };
654 pc.start(50.0, 500.0);
655 for _ in 0..200 {
656 let load = axis.simulated_load();
657 pc.tick(&mut axis, load, &cfg, 0.01);
658 axis.advance();
659 }
660 assert!((axis.simulated_load() - 50.0).abs() < 1.0);
661 pc.set_target(30.0);
662 for _ in 0..200 {
663 let load = axis.simulated_load();
664 pc.tick(&mut axis, load, &cfg, 0.01);
665 axis.advance();
666 }
667 assert!((axis.simulated_load() - 30.0).abs() < 1.0);
668 }
669}