1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6use bincode::de::Decoder;
7use bincode::enc::Encoder;
8use bincode::error::{DecodeError, EncodeError};
9use bincode::{Decode, Encode};
10use core::marker::PhantomData;
11use cu29::prelude::*;
12use cu29::reflect::{Reflect, ReflectTypePath};
13use serde::{Deserialize, Serialize};
14
15#[cfg(not(feature = "std"))]
16use alloc::format;
17
18#[derive(Debug, Default, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
20pub struct PIDControlOutputPayload {
21 pub p: f32,
23 pub i: f32,
25 pub d: f32,
27 pub output: f32,
29}
30
31#[derive(Reflect)]
33pub struct PIDController {
34 kp: f32,
36 ki: f32,
37 kd: f32,
38 setpoint: f32,
39 p_limit: f32,
40 i_limit: f32,
41 d_limit: f32,
42 output_limit: f32,
43 sampling: CuDuration,
44 integral: f32,
46 last_error: f32,
47 elapsed: CuDuration,
48 last_output: PIDControlOutputPayload,
49}
50
51impl PIDController {
52 #[allow(clippy::too_many_arguments)]
53 pub fn new(
54 kp: f32,
55 ki: f32,
56 kd: f32,
57 setpoint: f32,
58 p_limit: f32,
59 i_limit: f32,
60 d_limit: f32,
61 output_limit: f32,
62 sampling: CuDuration, ) -> Self {
64 PIDController {
65 kp,
66 ki,
67 kd,
68 setpoint,
69 integral: 0.0,
70 last_error: 0.0,
71 p_limit,
72 i_limit,
73 d_limit,
74 output_limit,
75 elapsed: CuDuration::default(),
76 sampling,
77 last_output: PIDControlOutputPayload::default(),
78 }
79 }
80
81 pub fn reset(&mut self) {
82 self.integral = 0.0f32;
83 self.last_error = 0.0f32;
84 }
85
86 pub fn reset_integral(&mut self) {
87 self.integral = 0.0f32;
88 }
89
90 pub fn init_measurement(&mut self, measurement: f32) {
91 self.last_error = self.setpoint - measurement;
92 self.elapsed = self.sampling; }
94
95 pub fn next_control_output(
96 &mut self,
97 measurement: f32,
98 dt: CuDuration,
99 ) -> PIDControlOutputPayload {
100 self.elapsed += dt;
101
102 if self.elapsed < self.sampling {
103 return self.last_output.clone();
105 }
106
107 let error = self.setpoint - measurement;
108 let CuDuration(elapsed) = self.elapsed;
109 let dt = elapsed as f32 / 1_000_000f32; if dt == 0.0 {
111 return self.last_output.clone();
112 }
113
114 let p_unbounded = self.kp * error;
116 let p = p_unbounded.clamp(-self.p_limit, self.p_limit);
117
118 self.integral += error * dt;
120 let i_unbounded = self.ki * self.integral;
121 let i = i_unbounded.clamp(-self.i_limit, self.i_limit);
122
123 let derivative = (error - self.last_error) / dt;
125 let d_unbounded = self.kd * derivative;
126 let d = d_unbounded.clamp(-self.d_limit, self.d_limit);
127
128 self.last_error = error;
130
131 let output_unbounded = p + i + d;
133 let output = output_unbounded.clamp(-self.output_limit, self.output_limit);
134
135 let output = PIDControlOutputPayload { p, i, d, output };
136
137 self.last_output = output.clone();
138 self.elapsed = CuDuration::default();
139 output
140 }
141}
142
143impl Freezable for PIDController {
144 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
145 Encode::encode(&self.integral, encoder)?;
146 Encode::encode(&self.last_error, encoder)?;
147 Encode::encode(&self.elapsed, encoder)?;
148 Encode::encode(&self.last_output, encoder)?;
149 Ok(())
150 }
151
152 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
153 self.integral = Decode::decode(decoder)?;
154 self.last_error = Decode::decode(decoder)?;
155 self.elapsed = Decode::decode(decoder)?;
156 self.last_output = Decode::decode(decoder)?;
157 Ok(())
158 }
159}
160
161#[derive(Reflect)]
163pub struct GenericPIDTask<I>
164where
165 f32: for<'a> From<&'a I>,
166{
167 #[reflect(ignore)]
168 _marker: PhantomData<fn() -> I>,
169 pid: PIDController,
170 first_run: bool,
171 last_tov: CuTime,
172 setpoint: f32,
173 cutoff: f32,
174}
175
176impl<I> CuTask for GenericPIDTask<I>
177where
178 f32: for<'a> From<&'a I>,
179 I: CuMsgPayload + ReflectTypePath + 'static,
180{
181 type Resources<'r> = ();
182 type Input<'m> = input_msg!(I);
183 type Output<'m> = output_msg!(PIDControlOutputPayload);
184
185 fn new(config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
186 where
187 Self: Sized,
188 {
189 match config {
190 Some(config) => {
191 debug!("PIDTask config loaded");
192 let setpoint: f32 = config
193 .get::<f64>("setpoint")?
194 .ok_or("'setpoint' not found in config")?
195 as f32;
196
197 let cutoff: f32 = config.get::<f64>("cutoff")?.ok_or(
198 "'cutoff' not found in config, please set an operating +/- limit on the input.",
199 )? as f32;
200
201 let kp = match config.get::<f64>("kp")? {
203 Some(kp) => Ok(kp as f32),
204 None => Err(CuError::from(
205 "'kp' not found in the config. We need at least 'kp' to make the PID algorithm work.",
206 )),
207 }?;
208
209 let p_limit = getcfg(config, "pl", 2.0f32)?;
210 let ki = getcfg(config, "ki", 0.0f32)?;
211 let i_limit = getcfg(config, "il", 1.0f32)?;
212 let kd = getcfg(config, "kd", 0.0f32)?;
213 let d_limit = getcfg(config, "dl", 2.0f32)?;
214 let output_limit = getcfg(config, "ol", 1.0f32)?;
215
216 let sampling = if let Some(value) = config.get::<u32>("sampling_ms")? {
217 CuDuration::from(value as u64 * 1_000_000u64)
218 } else {
219 CuDuration::default()
220 };
221
222 let pid: PIDController = PIDController::new(
223 kp,
224 ki,
225 kd,
226 setpoint,
227 p_limit,
228 i_limit,
229 d_limit,
230 output_limit,
231 sampling,
232 );
233
234 Ok(Self {
235 _marker: PhantomData,
236 pid,
237 first_run: true,
238 last_tov: CuTime::default(),
239 setpoint,
240 cutoff,
241 })
242 }
243 None => Err(CuError::from("PIDTask needs a config.")),
244 }
245 }
246
247 fn process(
248 &mut self,
249 _ctx: &CuContext,
250 input: &Self::Input<'_>,
251 output: &mut Self::Output<'_>,
252 ) -> CuResult<()> {
253 output.tov = input.tov;
254 match input.payload() {
255 Some(payload) => {
256 let tov = match input.tov {
257 Tov::Time(single) => single,
258 _ => return Err("Unexpected variant for a TOV of PID".into()),
259 };
260
261 let measure: f32 = payload.into();
262
263 if self.first_run {
264 self.first_run = false;
265 self.last_tov = tov;
266 self.pid.init_measurement(measure);
267 output.clear_payload();
268 return Ok(());
269 }
270 let dt = tov - self.last_tov;
271 self.last_tov = tov;
272
273 let state = self.pid.next_control_output(measure, dt);
275 let upper_limit = self.setpoint + self.cutoff;
277 let lower_limit = self.setpoint - self.cutoff;
278 if measure > upper_limit {
279 return Err(format!("{} > {} (cutoff)", measure, upper_limit).into());
280 }
281 if measure < lower_limit {
282 return Err(format!("{} < {} (cutoff)", measure, lower_limit).into());
283 }
284 output.metadata.set_status(format!(
285 "{:>5.2} {:>5.2} {:>5.2} {:>5.2}",
286 &state.output, &state.p, &state.i, &state.d
287 ));
288 output.set_payload(state);
289 }
290 None => output.clear_payload(),
291 };
292 Ok(())
293 }
294
295 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
296 self.pid.reset();
297 self.first_run = true;
298 Ok(())
299 }
300}
301
302impl<I> Freezable for GenericPIDTask<I>
304where
305 f32: for<'a> From<&'a I>,
306{
307 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
308 self.pid.freeze(encoder)?;
309 Encode::encode(&self.first_run, encoder)?;
310 Encode::encode(&self.last_tov, encoder)?;
311 Ok(())
312 }
313
314 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
315 self.pid.thaw(decoder)?;
316 self.first_run = Decode::decode(decoder)?;
317 self.last_tov = Decode::decode(decoder)?;
318 Ok(())
319 }
320}
321
322fn getcfg(config: &ComponentConfig, key: &str, default: f32) -> Result<f32, ConfigError> {
324 Ok(config
325 .get::<f64>(key)?
326 .map(|value| value as f32)
327 .unwrap_or(default))
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use bincode::config::standard;
334 use bincode::de::DecoderImpl;
335 use bincode::de::read::SliceReader;
336 use bincode::encode_to_vec;
337
338 #[derive(Clone, Copy)]
339 struct TestInput;
340
341 impl From<&TestInput> for f32 {
342 fn from(_: &TestInput) -> Self {
343 0.0
344 }
345 }
346
347 fn sample_task() -> GenericPIDTask<TestInput> {
348 GenericPIDTask {
349 _marker: PhantomData,
350 pid: PIDController {
351 kp: 1.0,
352 ki: 2.0,
353 kd: 3.0,
354 setpoint: 4.0,
355 p_limit: 5.0,
356 i_limit: 6.0,
357 d_limit: 7.0,
358 output_limit: 8.0,
359 sampling: CuDuration::from(9),
360 integral: 10.0,
361 last_error: 11.0,
362 elapsed: CuDuration::from(12),
363 last_output: PIDControlOutputPayload {
364 p: 13.0,
365 i: 14.0,
366 d: 15.0,
367 output: 16.0,
368 },
369 },
370 first_run: false,
371 last_tov: CuTime::from(17_u64),
372 setpoint: 18.0,
373 cutoff: 19.0,
374 }
375 }
376
377 #[test]
378 fn freeze_thaw_restores_pid_timekeeping_state() {
379 let original = sample_task();
380 let bytes =
381 encode_to_vec(BincodeAdapter(&original), standard()).expect("encode pid task state");
382
383 let mut restored = sample_task();
384 restored.pid.integral = -1.0;
385 restored.pid.last_error = -2.0;
386 restored.pid.elapsed = CuDuration::from(999);
387 restored.pid.last_output = PIDControlOutputPayload {
388 p: -3.0,
389 i: -4.0,
390 d: -5.0,
391 output: -6.0,
392 };
393 restored.first_run = true;
394 restored.last_tov = CuTime::from(1_000_u64);
395
396 let reader = SliceReader::new(&bytes);
397 let mut decoder = DecoderImpl::new(reader, standard(), ());
398 restored.thaw(&mut decoder).expect("thaw pid task state");
399
400 assert_eq!(restored.pid.integral, original.pid.integral);
401 assert_eq!(restored.pid.last_error, original.pid.last_error);
402 assert_eq!(restored.pid.elapsed, original.pid.elapsed);
403 assert_eq!(restored.pid.last_output.p, original.pid.last_output.p);
404 assert_eq!(restored.pid.last_output.i, original.pid.last_output.i);
405 assert_eq!(restored.pid.last_output.d, original.pid.last_output.d);
406 assert_eq!(
407 restored.pid.last_output.output,
408 original.pid.last_output.output
409 );
410 assert_eq!(restored.first_run, original.first_run);
411 assert_eq!(restored.last_tov, original.last_tov);
412 }
413}