1#![cfg_attr(not(feature = "std"), no_std)]
17
18use bincode::Decode;
19use bincode::Encode;
20use bincode::de::Decoder;
21use bincode::enc::Encoder;
22use bincode::error::DecodeError;
23use bincode::error::EncodeError;
24use cu29::prelude::*;
25use cu29::units::si::f64::Time;
26use cu29::units::si::time::second;
27use serde::Deserialize;
28use serde::Serialize;
29
30mod serde_arrays;
31
32#[derive(
34 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
35)]
36#[serde(rename_all = "snake_case")]
37pub enum CuIoStatus {
38 #[default]
40 NotReady,
41 Ready,
43 Fault(u32),
45}
46
47#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
49#[reflect(no_field_bounds, from_reflect = false)]
50pub struct CuDigitalInputPayload<const N: usize> {
51 pub status: CuIoStatus,
52 #[serde(with = "crate::serde_arrays::bool_array")]
54 pub values: [bool; N],
55}
56
57impl<const N: usize> Default for CuDigitalInputPayload<N> {
58 fn default() -> Self {
59 Self {
60 status: CuIoStatus::NotReady,
61 values: [false; N],
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
72#[reflect(no_field_bounds, from_reflect = false)]
73pub struct CuDigitalOutputPayload<const N: usize> {
74 #[serde(with = "crate::serde_arrays::bool_array")]
75 pub values: [bool; N],
76 #[serde(with = "crate::serde_arrays::bool_array")]
77 pub mask: [bool; N],
78}
79
80impl<const N: usize> Default for CuDigitalOutputPayload<N> {
81 fn default() -> Self {
82 Self {
83 values: [false; N],
84 mask: [false; N],
85 }
86 }
87}
88
89#[derive(
91 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
92)]
93#[serde(rename_all = "snake_case")]
94pub enum CuGripperAction {
95 #[default]
96 Open,
97 Close,
98}
99
100#[derive(Default, Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
102pub struct CuGripperCommandPayload {
103 pub action: CuGripperAction,
104}
105
106#[derive(
108 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
109)]
110#[serde(rename_all = "snake_case")]
111pub enum CuGripperState {
112 #[default]
114 Unknown,
115 Opening,
116 Open,
117 Closing,
118 Closed,
119 Fault,
122}
123
124#[derive(Default, Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
126pub struct CuGripperStatePayload {
127 pub state: CuGripperState,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
167#[serde(default, deny_unknown_fields)]
168pub struct CuGripperParams {
169 pub close_output: Option<usize>,
171 pub open_output: Option<usize>,
173 pub closed_input: Option<usize>,
175 pub open_input: Option<usize>,
177 pub settle: Time,
179 pub motion_timeout: Time,
181}
182
183impl Default for CuGripperParams {
184 fn default() -> Self {
187 Self {
188 close_output: None,
189 open_output: None,
190 closed_input: None,
191 open_input: None,
192 settle: Time::new::<second>(0.3),
193 motion_timeout: Time::new::<second>(2.0),
194 }
195 }
196}
197
198impl CuGripperParams {
199 pub fn single_acting(close_output: usize) -> Self {
202 Self {
203 close_output: Some(close_output),
204 ..Self::default()
205 }
206 }
207
208 pub fn validate<const IN: usize, const OUT: usize>(&self) -> CuResult<()> {
211 let close_output = self
212 .close_output
213 .ok_or_else(|| CuError::from("repose-io: gripper needs 'close_output'"))?;
214 let output_ok = |i: usize| i < OUT;
215 let input_ok = |i: usize| i < IN;
216 let positive = |t: Time| t.get::<second>() > 0.0;
217 if !output_ok(close_output)
218 || self
219 .open_output
220 .is_some_and(|o| !output_ok(o) || o == close_output)
221 || self.closed_input.is_some_and(|i| !input_ok(i))
222 || self.open_input.is_some_and(|i| !input_ok(i))
223 || self
224 .closed_input
225 .is_some_and(|i| self.open_input == Some(i))
226 || !positive(self.settle)
227 || !positive(self.motion_timeout)
228 {
229 return Err(CuError::from(
230 "repose-io: gripper indices out of range, shared, or non-positive timings",
231 ));
232 }
233 Ok(())
234 }
235}
236
237#[derive(Reflect)]
245#[reflect(no_field_bounds, from_reflect = false)]
246pub struct CuGripperTask<const IN: usize, const OUT: usize> {
247 #[reflect(ignore)]
248 params: CuGripperParams,
249 #[reflect(ignore)]
250 close_output: usize,
251 #[reflect(ignore)]
252 settle_ns: u64,
253 #[reflect(ignore)]
254 motion_timeout_ns: u64,
255 #[reflect(ignore)]
256 action: Option<CuGripperAction>,
257 #[reflect(ignore)]
258 state: CuGripperState,
259 motion_start: Option<CuTime>,
260}
261
262fn to_nanos(t: Time) -> u64 {
265 ((t.get::<second>() * 1e9 + 0.5) as u64).max(1)
267}
268
269impl<const IN: usize, const OUT: usize> CuGripperTask<IN, OUT> {
270 pub fn from_params(params: CuGripperParams) -> CuResult<Self> {
272 params.validate::<IN, OUT>()?;
273 Ok(Self {
274 close_output: params.close_output.unwrap_or_default(),
275 settle_ns: to_nanos(params.settle),
276 motion_timeout_ns: to_nanos(params.motion_timeout),
277 params,
278 action: None,
279 state: CuGripperState::Unknown,
280 motion_start: None,
281 })
282 }
283
284 pub fn params(&self) -> &CuGripperParams {
286 &self.params
287 }
288
289 pub fn state(&self) -> CuGripperState {
291 self.state
292 }
293
294 fn sensor(&self, input: Option<usize>, values: &[bool; IN]) -> Option<bool> {
295 input.map(|i| values[i])
296 }
297}
298
299impl<const IN: usize, const OUT: usize> Freezable for CuGripperTask<IN, OUT> {
300 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
301 Encode::encode(&self.action, encoder)?;
302 Encode::encode(&self.state, encoder)?;
303 Encode::encode(&self.motion_start, encoder)
304 }
305
306 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
307 self.action = Decode::decode(decoder)?;
308 self.state = Decode::decode(decoder)?;
309 self.motion_start = Decode::decode(decoder)?;
310 Ok(())
311 }
312}
313
314impl<const IN: usize, const OUT: usize> CuTask for CuGripperTask<IN, OUT> {
315 type Resources<'r> = ();
316 type Input<'m> = input_msg!('m, CuGripperCommandPayload, CuDigitalInputPayload<IN>);
317 type Output<'m> = output_msg!(CuDigitalOutputPayload<OUT>, CuGripperStatePayload);
318
319 fn new(cfg: Option<&ComponentConfig>, _res: Self::Resources<'_>) -> CuResult<Self>
320 where
321 Self: Sized,
322 {
323 let params = cfg
324 .map(ComponentConfig::deserialize_into::<CuGripperParams>)
325 .transpose()?
326 .unwrap_or_default();
327 Self::from_params(params)
328 }
329
330 fn process<'i, 'o>(
331 &mut self,
332 ctx: &CuContext,
333 input: &Self::Input<'i>,
334 output: &mut Self::Output<'o>,
335 ) -> CuResult<()> {
336 let (command, io) = *input;
337 let (digital_out, state_out) = output;
338 digital_out.tov = io.tov;
339 state_out.tov = io.tov;
340 let now = ctx.now();
341
342 if let Some(command) = command.payload()
344 && self.action != Some(command.action)
345 {
346 self.action = Some(command.action);
347 self.motion_start = Some(now);
348 self.state = match command.action {
349 CuGripperAction::Open => CuGripperState::Opening,
350 CuGripperAction::Close => CuGripperState::Closing,
351 };
352 }
353
354 let io_ready = io.payload().filter(|io| io.status == CuIoStatus::Ready);
355 self.state = match (self.action, io_ready) {
356 (None, _) => CuGripperState::Unknown,
357 (Some(_), None) => CuGripperState::Fault,
358 (Some(_), Some(_)) if self.state == CuGripperState::Fault => CuGripperState::Fault,
360 (Some(action), Some(io)) => {
361 let closed = self.sensor(self.params.closed_input, &io.values);
362 let open = self.sensor(self.params.open_input, &io.values);
363 let (reached, other, done, moving) = match action {
364 CuGripperAction::Close => (
365 closed,
366 open,
367 CuGripperState::Closed,
368 CuGripperState::Closing,
369 ),
370 CuGripperAction::Open => {
371 (open, closed, CuGripperState::Open, CuGripperState::Opening)
372 }
373 };
374 let elapsed = self
375 .motion_start
376 .map_or(0, |start| now.as_nanos().saturating_sub(start.as_nanos()));
377 if closed == Some(true) && open == Some(true) {
378 CuGripperState::Fault
379 } else {
380 match reached {
381 Some(true) => done,
382 Some(false) if elapsed >= self.motion_timeout_ns => CuGripperState::Fault,
383 Some(false) => moving,
384 None if elapsed >= self.settle_ns => {
387 if other == Some(true) {
388 CuGripperState::Fault
389 } else {
390 done
391 }
392 }
393 None => moving,
394 }
395 }
396 }
397 };
398
399 match self.action {
400 None => digital_out.clear_payload(),
401 Some(action) => {
402 let close = action == CuGripperAction::Close;
403 let mut command = CuDigitalOutputPayload::<OUT>::default();
404 command.values[self.close_output] = close;
405 command.mask[self.close_output] = true;
406 if let Some(open_output) = self.params.open_output {
407 command.values[open_output] = !close;
408 command.mask[open_output] = true;
409 }
410 digital_out.set_payload(command);
411 }
412 }
413 state_out.set_payload(CuGripperStatePayload { state: self.state });
414 Ok(())
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 fn params() -> CuGripperParams {
423 CuGripperParams {
424 open_output: Some(1),
425 closed_input: Some(0),
426 ..CuGripperParams::single_acting(0)
427 }
428 }
429
430 fn step(
431 task: &mut CuGripperTask<2, 2>,
432 ctx: &CuContext,
433 action: Option<CuGripperAction>,
434 io: Option<CuDigitalInputPayload<2>>,
435 ) -> (Option<CuDigitalOutputPayload<2>>, CuGripperState) {
436 let command = CuMsg::new(action.map(|action| CuGripperCommandPayload { action }));
437 let io = CuMsg::new(io);
438 let input = (&command, &io);
439 let mut output = (
440 CuMsg::<CuDigitalOutputPayload<2>>::default(),
441 CuMsg::<CuGripperStatePayload>::default(),
442 );
443 task.process(ctx, &input, &mut output).unwrap();
444 (
445 output.0.payload().cloned(),
446 output.1.payload().unwrap().state,
447 )
448 }
449
450 fn ready(closed: bool) -> Option<CuDigitalInputPayload<2>> {
451 Some(CuDigitalInputPayload {
452 status: CuIoStatus::Ready,
453 values: [closed, false],
454 })
455 }
456
457 #[test]
458 fn close_drives_masked_outputs_until_sensor_confirms() {
459 let mut task = CuGripperTask::<2, 2>::from_params(params()).unwrap();
460 let (ctx, _clock) = CuContext::new_mock_clock();
461 let (out, state) = step(&mut task, &ctx, None, ready(false));
462 assert!(out.is_none());
463 assert_eq!(state, CuGripperState::Unknown);
464 let (out, state) = step(&mut task, &ctx, Some(CuGripperAction::Close), ready(false));
465 let out = out.unwrap();
466 assert_eq!(out.values, [true, false]);
467 assert_eq!(out.mask, [true, true]);
468 assert_eq!(state, CuGripperState::Closing);
469 let (_, state) = step(&mut task, &ctx, None, ready(true));
470 assert_eq!(state, CuGripperState::Closed);
471 }
472
473 #[test]
474 fn missing_sensor_times_out_and_io_fault_latches() {
475 let mut task = CuGripperTask::<2, 2>::from_params(CuGripperParams {
476 motion_timeout: Time::new::<second>(1e-6),
477 ..params()
478 })
479 .unwrap();
480 let (ctx, clock) = CuContext::new_mock_clock();
481 step(&mut task, &ctx, Some(CuGripperAction::Close), ready(false));
482 clock.increment(CuDuration::from_nanos(2_000));
483 let (_, state) = step(&mut task, &ctx, None, ready(false));
484 assert_eq!(state, CuGripperState::Fault);
485 let (out, state) = step(&mut task, &ctx, None, ready(true));
486 assert_eq!(state, CuGripperState::Fault);
487 assert_eq!(out.unwrap().values, [true, false]);
488 let (_, state) = step(&mut task, &ctx, Some(CuGripperAction::Open), None);
489 assert_eq!(state, CuGripperState::Fault);
490 }
491
492 #[test]
493 fn rejects_out_of_range_wiring() {
494 assert!(CuGripperTask::<2, 2>::from_params(CuGripperParams::single_acting(2)).is_err());
495 assert!(
496 CuGripperTask::<2, 2>::from_params(CuGripperParams {
497 open_output: Some(0),
498 ..CuGripperParams::single_acting(0)
499 })
500 .is_err()
501 );
502 }
503
504 #[test]
505 fn missing_close_output_is_rejected() {
506 assert!(CuGripperTask::<2, 2>::new(None, ()).is_err());
507 assert!(CuGripperParams::default().validate::<2, 2>().is_err());
508 assert!(
509 CuGripperTask::<2, 2>::from_params(CuGripperParams {
510 settle: Time::new::<second>(0.0),
511 ..CuGripperParams::single_acting(0)
512 })
513 .is_err()
514 );
515 }
516
517 #[test]
518 fn default_timings_match_previous_nanoseconds() {
519 let task = CuGripperTask::<2, 2>::from_params(params()).unwrap();
520 assert_eq!(task.settle_ns, 300_000_000);
521 assert_eq!(task.motion_timeout_ns, 2_000_000_000);
522 }
523}