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 pub speed :f64 ,
158 pub accel : f64
159}
160
161impl Default for PressureControl {
162 fn default() -> Self {
163 Self {
164 state: StateMachine::new(),
165 in_tolerance: false,
166 target_load: 0.0,
167 max_load: f64::INFINITY,
168 integral: 0.0,
169 prev_error: 0.0,
170 filtered_load: 0.0,
171 commanded_position: 0.0,
172 last_pid_error: 0.0,
173 settling_timer: 0.0,
174 is_first_run: false,
175 speed : 0.0,
176 accel : 0.0
177
178 }
179 }
180}
181
182impl PressureControl {
183 pub fn new() -> Self {
185 Self::default()
186 }
187
188 pub fn start(&mut self, target_load: f64, max_load: f64, speed : f64, accel: f64) {
196 self.state.clear_error();
197 self.target_load = target_load;
198 self.max_load = max_load;
199 self.integral = 0.0;
200 self.prev_error = 0.0;
201 self.last_pid_error = 0.0;
202 self.settling_timer = 0.0;
203 self.in_tolerance = false;
204 self.is_first_run = true;
205 self.state.index = PcState::Controlling as i32;
206 self.speed = speed;
207 self.accel = accel;
208 }
209
210 pub fn stop(&mut self, axis: &mut impl AxisHandle) {
213 let idx = self.state.index;
214 if idx == PcState::Idle as i32 || idx == PcState::Halted as i32 {
215 return;
216 }
217 axis.halt();
218 self.in_tolerance = false;
219 self.state.index = PcState::Halted as i32;
220 }
221
222 pub fn set_target(&mut self, target_load: f64) {
225 self.target_load = target_load;
226 }
227
228 pub fn set_max_load(&mut self, max_load: f64) {
230 self.max_load = max_load;
231 }
232
233 pub fn is_busy(&self) -> bool {
235 let idx = self.state.index;
236 idx == PcState::Controlling as i32 || idx == PcState::Halted as i32
237 }
238
239 pub fn is_error(&self) -> bool {
241 self.state.index == PcState::Error as i32 || self.state.is_error()
242 }
243
244 pub fn error_message(&self) -> String {
246 self.state.error_message.clone()
247 }
248
249 pub fn is_in_tolerance(&self) -> bool {
252 self.in_tolerance
253 }
254
255 pub fn filtered_load(&self) -> f64 {
257 self.filtered_load
258 }
259
260 pub fn commanded_position(&self) -> f64 {
263 self.commanded_position
264 }
265
266 pub fn pid_error(&self) -> f64 {
269 self.last_pid_error
270 }
271
272 pub fn tick(
274 &mut self,
275 axis: &mut impl AxisHandle,
276 current_load: f64,
277 config: &PressureControlConfig,
278 dt: f64,
279 ) {
280 if axis.is_error() && self.state.index != PcState::Idle as i32 {
282 self.state.set_error(100, "Axis is in error state");
283 self.state.index = PcState::Error as i32;
284 return;
285 }
286
287 match PcState::from_index(self.state.index) {
288 Some(PcState::Idle) | Some(PcState::Error) | None => {
289 }
292
293 Some(PcState::Halted) => {
294 if !axis.is_busy() {
297 self.state.index = PcState::Idle as i32;
298 }
299 }
300
301 Some(PcState::Controlling) => {
302 if current_load.abs() > self.max_load {
307 axis.halt();
308 self.state.set_error(
309 110,
310 format!(
311 "load {} exceeded max_load {}",
312 current_load, self.max_load,
313 ),
314 );
315 self.state.index = PcState::Error as i32;
316 return;
317 }
318
319 if self.is_first_run {
323 self.filtered_load = current_load;
324 self.commanded_position = axis.position();
325 self.is_first_run = false;
326 }
327
328 let alpha = config.filter_alpha.clamp(0.0, 1.0);
330 self.filtered_load = alpha * current_load
331 + (1.0 - alpha) * self.filtered_load;
332
333 let error = self.target_load - self.filtered_load;
335 self.last_pid_error = error;
336
337 let derivative = if dt > 0.0 {
338 (error - self.prev_error) / dt
339 } else {
340 0.0
341 };
342 self.prev_error = error;
343
344 let integral_candidate = (self.integral + error * dt)
348 .clamp(-config.max_integral, config.max_integral);
349 let raw_output = config.kp * error
350 + config.ki * integral_candidate
351 + config.kd * derivative
352 + config.feed_forward;
353
354 let signed_output = if config.invert_direction {
357 -raw_output
358 } else {
359 raw_output
360 };
361 let saturated = signed_output.abs() > config.max_step;
362 let step = signed_output.clamp(-config.max_step, config.max_step);
363
364 if !saturated {
369 self.integral = integral_candidate;
370 }
371
372 let next_cmd = self.commanded_position + step;
376 let clamped = match (config.position_limit_neg, config.position_limit_pos) {
377 (Some(lo), Some(hi)) => next_cmd.clamp(lo, hi),
378 (Some(lo), None) => next_cmd.max(lo),
379 (None, Some(hi)) => next_cmd.min(hi),
380 (None, None) => next_cmd,
381 };
382 if clamped != next_cmd {
383 self.integral = self.integral - error * dt;
386 self.integral = self.integral.clamp(-config.max_integral, config.max_integral);
387 }
388 self.commanded_position = clamped;
389
390 let issued_step = self.commanded_position - axis.position();
392 if issued_step.abs() > config.min_step {
393 let vel = self.speed;
394 let acc = self.accel;
395 let dec = self.accel;
396 axis.move_absolute(self.commanded_position, vel, acc, dec);
397 }
398
399 if error.abs() <= config.tolerance {
406 self.settling_timer += dt;
407 if self.settling_timer >= config.settling_time {
408 self.in_tolerance = true;
409 }
410 } else {
411 self.settling_timer = 0.0;
412 self.in_tolerance = false;
413 }
414 }
415 }
416
417 self.state.call();
418 }
419}
420
421#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::motion::axis_config::AxisConfig;
429
430 struct MockAxis {
431 position: f64,
432 busy: bool,
433 error: bool,
434 config: AxisConfig,
435 halt_calls: u32,
436 last_move_target: f64,
437 move_calls: u32,
438 spring_k: f64,
441 rest_position: f64,
442 }
443
444 impl MockAxis {
445 fn new() -> Self {
446 let mut cfg = AxisConfig::new(10_000);
447 cfg.jog_speed = 5.0;
448 cfg.jog_accel = 50.0;
449 cfg.jog_decel = 50.0;
450 Self {
451 position: 0.0, busy: false, error: false,
452 config: cfg,
453 halt_calls: 0, last_move_target: 0.0, move_calls: 0,
454 spring_k: 0.0, rest_position: 0.0,
455 }
456 }
457 fn with_spring(mut self, k: f64, rest: f64) -> Self {
458 self.spring_k = k;
459 self.rest_position = rest;
460 self
461 }
462 fn simulated_load(&self) -> f64 {
463 self.spring_k * (self.position - self.rest_position)
464 }
465 fn advance(&mut self) {
469 self.position = self.last_move_target;
470 }
471 }
472
473 impl AxisHandle for MockAxis {
474 fn position(&self) -> f64 { self.position }
475 fn config(&self) -> &AxisConfig { &self.config }
476 fn move_absolute(&mut self, p: f64, _v: f64, _a: f64, _d: f64) {
477 self.last_move_target = p;
478 self.move_calls += 1;
479 self.busy = true;
480 }
481 fn move_relative(&mut self, _: f64, _: f64, _: f64, _: f64) {}
482 fn halt(&mut self) {
483 self.halt_calls += 1;
484 self.busy = false;
485 }
486 fn is_busy(&self) -> bool { self.busy }
487 fn is_error(&self) -> bool { self.error }
488 fn motor_on(&self) -> bool { true }
489 }
490
491 fn zero_pid_config() -> PressureControlConfig {
492 PressureControlConfig { ..Default::default() }
493 }
494
495 #[test]
496 fn tick_is_noop_when_idle() {
497 let mut pc = PressureControl::new();
498 let mut axis = MockAxis::new();
499 let cfg = zero_pid_config();
500 pc.tick(&mut axis, 0.0, &cfg, 0.01);
501 assert!(!pc.is_busy());
502 assert_eq!(axis.move_calls, 0);
503 assert_eq!(axis.halt_calls, 0);
504 }
505
506 #[test]
507 fn start_then_tick_seeds_from_axis() {
508 let mut pc = PressureControl::new();
509 let mut axis = MockAxis::new();
510 axis.position = 3.25;
511 pc.start(10.0, 500.0, 10.0, 100.0);
512 assert!(pc.is_busy());
513 pc.tick(&mut axis, 0.0, &zero_pid_config(), 0.01);
515 assert_eq!(pc.commanded_position(), 3.25);
516 assert_eq!(pc.filtered_load(), 0.0);
517 }
518
519 #[test]
520 fn stop_halts_and_transitions_to_idle() {
521 let mut pc = PressureControl::new();
522 let mut axis = MockAxis::new();
523 pc.start(10.0, 500.0, 10.0, 100.0);
524 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
525 assert!(pc.is_busy());
526 pc.stop(&mut axis);
527 assert_eq!(axis.halt_calls, 1);
528 assert!(pc.is_busy(), "still busy until axis finishes halt");
529 axis.busy = false; pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
531 assert!(!pc.is_busy());
532 }
533
534 #[test]
535 fn max_load_triggers_error_and_halt() {
536 let mut pc = PressureControl::new();
537 let mut axis = MockAxis::new();
538 pc.start(10.0, 100.0, 10.0, 100.0);
539 pc.tick(&mut axis, 150.0, &zero_pid_config(), 0.01);
540 assert!(pc.is_error());
541 assert_eq!(axis.halt_calls, 1);
542 assert!(pc.error_message().contains("exceeded max_load"));
543 }
544
545 #[test]
546 fn loop_converges_toward_target() {
547 let mut pc = PressureControl::new();
549 let mut axis = MockAxis::new().with_spring(100.0, 0.0);
550 let cfg = PressureControlConfig {
551 kp: 0.01, ki: 0.0,
553 kd: 0.0,
554 max_step: 0.5,
555 min_step: 0.0,
556 filter_alpha: 1.0,
557 tolerance: 1.0,
558 settling_time: 0.0,
559 ..Default::default()
560 };
561 pc.start(50.0, 500.0, 10.0, 100.0);
562 for _ in 0..200 {
563 let load = axis.simulated_load();
564 pc.tick(&mut axis, load, &cfg, 0.01);
565 axis.advance();
566 }
567 let final_load = axis.simulated_load();
568 assert!(
569 (final_load - 50.0).abs() < 1.0,
570 "expected load near 50 N, got {}", final_load,
571 );
572 assert!(pc.is_in_tolerance());
573 }
574
575 #[test]
576 fn invert_direction_flips_step_sign() {
577 let mut pc = PressureControl::new();
583 let mut axis = MockAxis::new().with_spring(-100.0, 0.0);
584 let cfg = PressureControlConfig {
585 kp: 0.01,
586 invert_direction: true,
587 max_step: 0.5,
588 min_step: 0.0,
589 filter_alpha: 1.0,
590 tolerance: 1.0,
591 settling_time: 0.0,
592 ..Default::default()
593 };
594 pc.start(50.0, 500.0, 10.0, 100.0);
595 for _ in 0..200 {
596 let load = axis.simulated_load();
597 pc.tick(&mut axis, load, &cfg, 0.01);
598 axis.advance();
599 }
600 assert!(axis.position < 0.0, "expected negative position, got {}", axis.position);
601 }
602
603 #[test]
604 fn min_step_deadband_suppresses_move() {
605 let mut pc = PressureControl::new();
606 let mut axis = MockAxis::new();
607 let cfg = PressureControlConfig {
608 kp: 0.0001, max_step: 0.5,
610 min_step: 0.01, filter_alpha: 1.0,
612 ..Default::default()
613 };
614 pc.start(1.0, 500.0, 10.0, 100.0);
615 pc.tick(&mut axis, 0.0, &cfg, 0.01); let first_move_calls = axis.move_calls;
617 for _ in 0..10 {
619 pc.tick(&mut axis, 0.0, &cfg, 0.01);
620 }
621 assert_eq!(axis.move_calls, first_move_calls,
622 "min_step should have suppressed all these moves");
623 }
624
625 #[test]
626 fn position_limit_clamps_commanded() {
627 let mut pc = PressureControl::new();
628 let mut axis = MockAxis::new();
629 let cfg = PressureControlConfig {
630 kp: 1.0,
631 max_step: 0.5,
632 min_step: 0.0,
633 filter_alpha: 1.0,
634 position_limit_pos: Some(1.0), tolerance: 1.0,
636 settling_time: 0.0,
637 ..Default::default()
638 };
639 pc.start(100.0, 500.0, 10.0, 100.0); for _ in 0..20 {
641 pc.tick(&mut axis, 0.0, &cfg, 0.01);
642 }
643 assert!(pc.commanded_position() <= 1.0 + 1e-6,
644 "commanded position should be clamped, got {}", pc.commanded_position());
645 }
646
647 #[test]
648 fn axis_error_transitions_to_error_state() {
649 let mut pc = PressureControl::new();
650 let mut axis = MockAxis::new();
651 pc.start(10.0, 500.0, 10.0, 100.0);
652 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
653 assert!(pc.is_busy());
654 axis.error = true;
655 pc.tick(&mut axis, 5.0, &zero_pid_config(), 0.01);
656 assert!(pc.is_error());
657 }
658
659 #[test]
660 fn set_target_changes_setpoint_without_reset() {
661 let mut pc = PressureControl::new();
662 let mut axis = MockAxis::new().with_spring(100.0, 0.0);
663 let cfg = PressureControlConfig {
664 kp: 0.01, max_step: 0.5, min_step: 0.0, filter_alpha: 1.0,
665 tolerance: 0.5, settling_time: 0.0, ..Default::default()
666 };
667 pc.start(50.0, 500.0, 10.0, 100.0);
668 for _ in 0..200 {
669 let load = axis.simulated_load();
670 pc.tick(&mut axis, load, &cfg, 0.01);
671 axis.advance();
672 }
673 assert!((axis.simulated_load() - 50.0).abs() < 1.0);
674 pc.set_target(30.0);
675 for _ in 0..200 {
676 let load = axis.simulated_load();
677 pc.tick(&mut axis, load, &cfg, 0.01);
678 axis.advance();
679 }
680 assert!((axis.simulated_load() - 30.0).abs() < 1.0);
681 }
682}