1#![cfg_attr(not(feature = "std"), no_std)]
16
17mod cartesian_blend;
18mod parameterize;
19mod path_timing;
20mod quintic;
21mod serde_arrays;
22mod task;
23#[cfg(test)]
24mod test_support;
25
26pub use cartesian_blend::CuCartesianBlendParams;
27pub use cartesian_blend::CuCartesianBlendPath;
28pub use cartesian_blend::CuCartesianBlendStatus;
29pub use cartesian_blend::CuCartesianPathPoint;
30pub use cartesian_blend::CuCartesianWaypoint;
31pub use cartesian_blend::CuTcpZone;
32pub use cartesian_blend::blend_cartesian_path;
33pub use parameterize::CuJointTrajectoryLimits;
34pub use parameterize::CuTimingOverrides;
35pub use parameterize::CuTimingParams;
36pub use parameterize::CuTimingStatus;
37pub use parameterize::CuTrajectoryProfile;
38pub use parameterize::parameterize;
39pub use path_timing::CuPathTimingParams;
40pub use path_timing::parameterize_cartesian_path;
41pub use task::CuFollowStatus;
42pub use task::CuFollowTrajectoryParams;
43pub use task::CuFollowTrajectoryTask;
44pub use task::CuTrajectoryFollowOutput;
45
46use bincode::Decode;
47use bincode::Encode;
48use cu29::prelude::*;
49use cu29::units::si::angular_velocity::radian_per_second;
50use cu29::units::si::f64::AngularVelocity;
51use cu29::units::si::f64::Time;
52use cu29::units::si::f64::Velocity;
53use cu29::units::si::time::second;
54use cu29::units::si::velocity::meter_per_second;
55use repose_kinematics::CuJointAcceleration;
56use repose_kinematics::CuJointPosition;
57use repose_kinematics::CuJointVelocity;
58use serde::Deserialize;
59use serde::Serialize;
60
61pub const MAX_TRAJECTORY_POINTS: usize = 64;
63
64#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
66#[reflect(no_field_bounds, from_reflect = false)]
67pub struct CuTrajectoryPoint<const N: usize> {
68 pub time_from_start: Time,
70 #[serde(with = "repose_kinematics::serde_arrays::position_array")]
71 pub positions: [CuJointPosition; N],
72 #[serde(with = "repose_kinematics::serde_arrays::velocity_array")]
73 pub velocities: [CuJointVelocity; N],
74 #[serde(with = "repose_kinematics::serde_arrays::acceleration_array")]
79 pub accelerations: [CuJointAcceleration; N],
80}
81
82impl<const N: usize> Default for CuTrajectoryPoint<N> {
83 fn default() -> Self {
84 Self {
85 time_from_start: Time::default(),
86 positions: [CuJointPosition::default(); N],
87 velocities: [CuJointVelocity::default(); N],
88 accelerations: [CuJointAcceleration::default(); N],
89 }
90 }
91}
92
93#[derive(
98 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
99)]
100#[serde(rename_all = "snake_case")]
101pub enum CuTrajectoryInterpolation {
102 #[default]
105 Linear,
106 Cubic,
109 Quintic,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
117#[reflect(no_field_bounds, from_reflect = false)]
118pub struct CuTrajectoryPayload<const N: usize, const MAX_POINTS: usize = MAX_TRAJECTORY_POINTS> {
119 #[serde(with = "crate::serde_arrays::trajectory_point_array")]
120 pub points: [CuTrajectoryPoint<N>; MAX_POINTS],
121 pub len: u32,
123 #[serde(default)]
128 pub interpolation: CuTrajectoryInterpolation,
129}
130
131impl<const N: usize, const MAX_POINTS: usize> Encode for CuTrajectoryPayload<N, MAX_POINTS> {
133 fn encode<E: bincode::enc::Encoder>(
134 &self,
135 encoder: &mut E,
136 ) -> Result<(), bincode::error::EncodeError> {
137 Encode::encode(&self.points, encoder)?;
138 Encode::encode(&self.len, encoder)?;
139 Encode::encode(&self.interpolation, encoder)
140 }
141}
142impl<C, const N: usize, const MAX_POINTS: usize> Decode<C> for CuTrajectoryPayload<N, MAX_POINTS> {
143 fn decode<D: bincode::de::Decoder<Context = C>>(
144 decoder: &mut D,
145 ) -> Result<Self, bincode::error::DecodeError> {
146 Ok(Self {
147 points: Decode::decode(decoder)?,
148 len: Decode::decode(decoder)?,
149 interpolation: Decode::decode(decoder)?,
150 })
151 }
152}
153impl<'de, C, const N: usize, const MAX_POINTS: usize> bincode::BorrowDecode<'de, C>
154 for CuTrajectoryPayload<N, MAX_POINTS>
155{
156 fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = C>>(
157 decoder: &mut D,
158 ) -> Result<Self, bincode::error::DecodeError> {
159 Decode::decode(decoder)
160 }
161}
162
163impl<const N: usize, const MAX_POINTS: usize> Default for CuTrajectoryPayload<N, MAX_POINTS> {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl<const N: usize, const MAX_POINTS: usize> CuTrajectoryPayload<N, MAX_POINTS> {
170 pub fn new() -> Self {
172 Self {
173 points: core::array::from_fn(|_| CuTrajectoryPoint::default()),
174 len: 0,
175 interpolation: CuTrajectoryInterpolation::Linear,
176 }
177 }
178
179 pub const fn is_empty(&self) -> bool {
181 self.len == 0
182 }
183
184 pub const fn len(&self) -> usize {
186 self.len as usize
187 }
188
189 pub fn duration(&self) -> Time {
191 if self.is_empty() {
192 Time::default()
193 } else {
194 self.points[self.len as usize - 1].time_from_start
195 }
196 }
197
198 pub fn points(&self) -> &[CuTrajectoryPoint<N>] {
200 &self.points[..self.len as usize]
201 }
202
203 pub fn push(&mut self, point: CuTrajectoryPoint<N>) -> CuResult<()> {
205 if self.len as usize >= MAX_POINTS {
206 return Err(CuError::from("Trajectory is full"));
207 }
208 if !self.is_empty()
209 && point.time_from_start <= self.points[self.len as usize - 1].time_from_start
210 {
211 return Err(CuError::from(
212 "Trajectory times must be strictly increasing",
213 ));
214 }
215 self.points[self.len as usize] = point;
216 self.len += 1;
217 Ok(())
218 }
219
220 pub fn clear(&mut self) {
222 self.len = 0;
223 }
224
225 pub fn sample(
233 &self,
234 t: Time,
235 out_positions: &mut [CuJointPosition; N],
236 out_velocities: &mut [CuJointVelocity; N],
237 ) -> CuResult<()> {
238 self.sample_with(self.interpolation, t, out_positions, out_velocities)
239 }
240
241 pub fn sample_with(
243 &self,
244 interpolation: CuTrajectoryInterpolation,
245 t: Time,
246 out_positions: &mut [CuJointPosition; N],
247 out_velocities: &mut [CuJointVelocity; N],
248 ) -> CuResult<()> {
249 match interpolation {
250 CuTrajectoryInterpolation::Linear => {
251 self.sample_linear(t, out_positions, out_velocities)
252 }
253 CuTrajectoryInterpolation::Cubic => self.sample_cubic(t, out_positions, out_velocities),
254 CuTrajectoryInterpolation::Quintic => {
255 self.sample_quintic(t, out_positions, out_velocities)
256 }
257 }
258 }
259
260 pub fn sample_linear(
267 &self,
268 t: Time,
269 out_positions: &mut [CuJointPosition; N],
270 out_velocities: &mut [CuJointVelocity; N],
271 ) -> CuResult<()> {
272 if self.is_empty() {
273 return Err(CuError::from("Cannot sample an empty trajectory"));
274 }
275 if self.len() == 1 {
276 *out_positions = self.points[0].positions;
277 *out_velocities = self.points[0].velocities;
278 return Ok(());
279 }
280 let pts = self.points();
281 let duration = self.duration();
282 let zero = Time::default();
283 let clamped = if t <= zero {
284 zero
285 } else if t >= duration {
286 duration
287 } else {
288 t
289 };
290 let t_s = clamped.get::<second>();
291 let last = pts.len() - 1;
292 if t_s <= pts[0].time_from_start.get::<second>() {
293 *out_positions = pts[0].positions;
294 *out_velocities = pts[0].velocities;
295 return Ok(());
296 }
297 if t_s >= pts[last].time_from_start.get::<second>() {
298 *out_positions = pts[last].positions;
299 *out_velocities = pts[last].velocities;
300 return Ok(());
301 }
302 let mut i = 0;
303 while i < last && pts[i + 1].time_from_start.get::<second>() < t_s {
304 i += 1;
305 }
306 let t0 = pts[i].time_from_start.get::<second>();
307 let t1 = pts[i + 1].time_from_start.get::<second>();
308 let fraction = (t_s - t0) / (t1 - t0);
309 let mut j = 0;
310 while j < N {
311 out_positions[j] =
312 pts[i].positions[j].interpolate(pts[i + 1].positions[j], fraction)?;
313 out_velocities[j] =
314 lerp_velocity(pts[i].velocities[j], pts[i + 1].velocities[j], fraction)?;
315 j += 1;
316 }
317 Ok(())
318 }
319
320 pub fn sample_cubic(
327 &self,
328 t: Time,
329 out_positions: &mut [CuJointPosition; N],
330 out_velocities: &mut [CuJointVelocity; N],
331 ) -> CuResult<()> {
332 if self.is_empty() || self.len() > MAX_POINTS || !t.get::<second>().is_finite() {
333 return Err(CuError::from("Invalid cubic trajectory or sample time"));
334 }
335 let pts = self.points();
336 let ts = t.get::<second>();
337 if ts <= pts[0].time_from_start.get::<second>() {
338 *out_positions = pts[0].positions;
339 *out_velocities = pts[0].velocities;
340 return Ok(());
341 }
342 let last = pts.len() - 1;
343 if ts >= pts[last].time_from_start.get::<second>() {
344 *out_positions = pts[last].positions;
345 *out_velocities = pts[last].velocities;
346 return Ok(());
347 }
348 let mut i = 0;
349 while i < last && pts[i + 1].time_from_start.get::<second>() < ts {
350 i += 1;
351 }
352 let a = &pts[i];
353 let b = &pts[i + 1];
354 let t0 = a.time_from_start.get::<second>();
355 let duration = b.time_from_start.get::<second>() - t0;
356 if !duration.is_finite() || duration <= 0.0 {
357 return Err(CuError::from("Invalid trajectory segment duration"));
358 }
359 let u = (ts - t0) / duration;
360 for j in 0..N {
361 if core::mem::discriminant(&a.positions[j]) != core::mem::discriminant(&b.positions[j])
362 || core::mem::discriminant(&a.velocities[j])
363 != core::mem::discriminant(&b.velocities[j])
364 || !matches!(
365 (a.positions[j], a.velocities[j]),
366 (CuJointPosition::Revolute(_), CuJointVelocity::Revolute(_))
367 | (CuJointPosition::Prismatic(_), CuJointVelocity::Prismatic(_))
368 | (CuJointPosition::Fixed, CuJointVelocity::Fixed)
369 )
370 {
371 return Err(CuError::from("Trajectory joint kinds disagree"));
372 }
373 let q0 = a.positions[j].si_value();
374 let q1 = b.positions[j].si_value();
375 let v0 = a.velocities[j].si_value();
376 let v1 = b.velocities[j].si_value();
377 if !q0.is_finite() || !q1.is_finite() || !v0.is_finite() || !v1.is_finite() {
378 return Err(CuError::from("Non-finite trajectory knot"));
379 }
380 let d = q1 - q0;
381 let aa = v0 * duration;
382 let bb = 3.0 * d - 2.0 * aa - v1 * duration;
383 let cc = -2.0 * d + aa + v1 * duration;
384 out_positions[j] = a.positions[j].with_si_value(q0 + u * (aa + u * (bb + u * cc)));
385 out_velocities[j] = CuJointVelocity::from_coordinate(
386 a.positions[j],
387 (aa + u * (2.0 * bb + 3.0 * cc * u)) / duration,
388 );
389 }
390 Ok(())
391 }
392
393 pub fn sample_quintic(
401 &self,
402 t: Time,
403 out_positions: &mut [CuJointPosition; N],
404 out_velocities: &mut [CuJointVelocity; N],
405 ) -> CuResult<()> {
406 if self.is_empty() || self.len() > MAX_POINTS || !t.get::<second>().is_finite() {
407 return Err(CuError::from("Invalid quintic trajectory or sample time"));
408 }
409 let pts = self.points();
410 let ts = t.get::<second>();
411 let last = pts.len() - 1;
412 if ts <= pts[0].time_from_start.get::<second>() {
413 *out_positions = pts[0].positions;
414 *out_velocities = pts[0].velocities;
415 return Ok(());
416 }
417 if ts >= pts[last].time_from_start.get::<second>() {
418 *out_positions = pts[last].positions;
419 *out_velocities = pts[last].velocities;
420 return Ok(());
421 }
422 let mut i = 0;
423 while i < last && pts[i + 1].time_from_start.get::<second>() < ts {
424 i += 1;
425 }
426 let a = &pts[i];
427 let b = &pts[i + 1];
428 let t0 = a.time_from_start.get::<second>();
429 let duration = b.time_from_start.get::<second>() - t0;
430 if !duration.is_finite() || duration <= 0.0 {
431 return Err(CuError::from("Invalid trajectory segment duration"));
432 }
433 let u = (ts - t0) / duration;
434 for j in 0..N {
435 let kinds_agree = matches!(
436 (
437 a.positions[j],
438 b.positions[j],
439 a.velocities[j],
440 b.velocities[j]
441 ),
442 (
443 CuJointPosition::Revolute(_),
444 CuJointPosition::Revolute(_),
445 CuJointVelocity::Revolute(_),
446 CuJointVelocity::Revolute(_)
447 ) | (
448 CuJointPosition::Prismatic(_),
449 CuJointPosition::Prismatic(_),
450 CuJointVelocity::Prismatic(_),
451 CuJointVelocity::Prismatic(_)
452 ) | (
453 CuJointPosition::Fixed,
454 CuJointPosition::Fixed,
455 CuJointVelocity::Fixed,
456 CuJointVelocity::Fixed
457 )
458 );
459 if !kinds_agree {
460 return Err(CuError::from("Trajectory joint kinds disagree"));
461 }
462 let quintic = quintic::Quintic::new(
463 [a.positions[j].si_value(), b.positions[j].si_value()],
464 [a.velocities[j].si_value(), b.velocities[j].si_value()],
465 [a.accelerations[j].si_value(), b.accelerations[j].si_value()],
466 duration,
467 );
468 let (q, v, _) = quintic.eval(u);
469 if !q.is_finite() || !v.is_finite() {
470 return Err(CuError::from("Non-finite trajectory knot"));
471 }
472 out_positions[j] = a.positions[j].with_si_value(q);
473 out_velocities[j] = CuJointVelocity::from_coordinate(a.positions[j], v);
474 }
475 Ok(())
476 }
477}
478
479fn lerp_velocity(
481 a: CuJointVelocity,
482 b: CuJointVelocity,
483 fraction: f64,
484) -> CuResult<CuJointVelocity> {
485 if !fraction.is_finite() {
486 return Err(CuError::from("Non-finite interpolation fraction"));
487 }
488 match (a, b) {
489 (CuJointVelocity::Revolute(x), CuJointVelocity::Revolute(y)) => {
490 let xv = x.get::<radian_per_second>();
491 let yv = y.get::<radian_per_second>();
492 Ok(CuJointVelocity::Revolute(AngularVelocity::new::<
493 radian_per_second,
494 >(
495 xv + fraction * (yv - xv)
496 )))
497 }
498 (CuJointVelocity::Prismatic(x), CuJointVelocity::Prismatic(y)) => {
499 let xv = x.get::<meter_per_second>();
500 let yv = y.get::<meter_per_second>();
501 Ok(CuJointVelocity::Prismatic(
502 Velocity::new::<meter_per_second>(xv + fraction * (yv - xv)),
503 ))
504 }
505 (CuJointVelocity::Fixed, CuJointVelocity::Fixed) => Ok(CuJointVelocity::Fixed),
506 _ => Err(CuError::from("Mismatched joint velocity types")),
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use cu29::units::si::angle::radian;
514
515 fn point(t: f64, q: f64, v: f64) -> CuTrajectoryPoint<1> {
516 CuTrajectoryPoint {
517 time_from_start: Time::new::<second>(t),
518 positions: [CuJointPosition::Revolute(
519 cu29::units::si::f64::Angle::new::<radian>(q),
520 )],
521 velocities: [CuJointVelocity::Revolute(AngularVelocity::new::<
522 radian_per_second,
523 >(v))],
524 ..Default::default()
525 }
526 }
527
528 fn two_point_traj() -> CuTrajectoryPayload<1> {
529 let mut traj = CuTrajectoryPayload::new();
530 traj.push(point(0.0, 0.0, 0.0)).unwrap();
531 traj.push(point(2.0, 2.0, 4.0)).unwrap();
532 traj
533 }
534
535 #[test]
536 fn test_push_duration_and_points() {
537 let traj = two_point_traj();
538 assert_eq!(traj.len(), 2);
539 assert!(!traj.is_empty());
540 assert_eq!(traj.duration().get::<second>(), 2.0);
541 assert_eq!(traj.points().len(), 2);
542 }
543
544 #[test]
545 fn test_push_rejects_full_and_non_monotonic() {
546 let mut traj = CuTrajectoryPayload::<1, 2>::new();
547 traj.push(point(0.0, 0.0, 0.0)).unwrap();
548 traj.push(point(1.0, 1.0, 1.0)).unwrap();
549 assert!(traj.push(point(2.0, 2.0, 2.0)).is_err());
550 let mut traj = CuTrajectoryPayload::<1>::new();
551 traj.push(point(1.0, 0.0, 0.0)).unwrap();
552 assert!(traj.push(point(1.0, 1.0, 1.0)).is_err());
553 assert!(traj.push(point(0.5, 1.0, 1.0)).is_err());
554 assert_eq!(traj.len(), 1);
555 traj.clear();
556 assert!(traj.is_empty());
557 assert_eq!(traj.duration().get::<second>(), 0.0);
558 }
559
560 #[test]
561 fn test_sample_endpoints_midpoint_and_clamping() {
562 let traj = two_point_traj();
563 let mut q = [CuJointPosition::default(); 1];
564 let mut qd = [CuJointVelocity::default(); 1];
565 traj.sample(Time::new::<second>(0.0), &mut q, &mut qd)
566 .unwrap();
567 assert_eq!(q[0].si_value(), 0.0);
568 traj.sample(Time::new::<second>(1.0), &mut q, &mut qd)
569 .unwrap();
570 assert!((q[0].si_value() - 1.0).abs() < 1e-12);
571 assert!((qd[0].si_value() - 2.0).abs() < 1e-12);
572 traj.sample(Time::new::<second>(2.0), &mut q, &mut qd)
573 .unwrap();
574 assert_eq!(q[0].si_value(), 2.0);
575 traj.sample(Time::new::<second>(-5.0), &mut q, &mut qd)
576 .unwrap();
577 assert_eq!(q[0].si_value(), 0.0);
578 traj.sample(Time::new::<second>(99.0), &mut q, &mut qd)
579 .unwrap();
580 assert_eq!(q[0].si_value(), 2.0);
581 assert_eq!(qd[0].si_value(), 4.0);
582 }
583
584 #[test]
585 fn test_sample_empty_errors() {
586 let traj = CuTrajectoryPayload::<1>::new();
587 let mut q = [CuJointPosition::default(); 1];
588 let mut qd = [CuJointVelocity::default(); 1];
589 assert!(
590 traj.sample(Time::new::<second>(0.0), &mut q, &mut qd)
591 .is_err()
592 );
593 }
594
595 #[test]
596 fn test_sample_dispatches_on_payload_interpolation() {
597 let mut traj = two_point_traj();
598 let mut q = [CuJointPosition::default(); 1];
599 let mut qd = [CuJointVelocity::default(); 1];
600 let mut q2 = q;
601 let mut qd2 = qd;
602 let t = Time::new::<second>(1.0);
603 for interpolation in [
604 CuTrajectoryInterpolation::Linear,
605 CuTrajectoryInterpolation::Cubic,
606 CuTrajectoryInterpolation::Quintic,
607 ] {
608 traj.interpolation = interpolation;
609 traj.sample(t, &mut q, &mut qd).unwrap();
610 match interpolation {
611 CuTrajectoryInterpolation::Linear => traj.sample_linear(t, &mut q2, &mut qd2),
612 CuTrajectoryInterpolation::Cubic => traj.sample_cubic(t, &mut q2, &mut qd2),
613 CuTrajectoryInterpolation::Quintic => traj.sample_quintic(t, &mut q2, &mut qd2),
614 }
615 .unwrap();
616 assert_eq!((q, qd), (q2, qd2), "{interpolation:?}");
617 }
618 }
619
620 #[test]
621 fn test_bincode_round_trip_keeps_interpolation() {
622 let mut traj = CuTrajectoryPayload::<1, 4>::new();
623 traj.push(point(0.0, 0.0, 0.0)).unwrap();
624 traj.push(point(1.0, 1.0, 0.0)).unwrap();
625 traj.interpolation = CuTrajectoryInterpolation::Quintic;
626 let mut bytes = [0u8; 1024];
627 let len =
628 bincode::encode_into_slice(&traj, &mut bytes, bincode::config::standard()).unwrap();
629 let (decoded, _): (CuTrajectoryPayload<1, 4>, usize) =
630 bincode::decode_from_slice(&bytes[..len], bincode::config::standard()).unwrap();
631 assert_eq!(decoded, traj);
632 }
633
634 #[test]
635 fn test_stretch_time_scales_and_limit_ratio_drops() {
636 let mut traj = CuTrajectoryPayload::<1, 4>::new();
637 traj.push(point(0.0, 0.0, 0.0)).unwrap();
638 traj.push(point(1.0, 1.0, 0.0)).unwrap();
639 let limits = CuJointTrajectoryLimits::new(
640 [CuJointVelocity::Revolute(AngularVelocity::new::<
641 radian_per_second,
642 >(1.0))],
643 [CuJointAcceleration::from_coordinate(
644 traj.points()[0].positions[0],
645 10.0,
646 )],
647 );
648 let before = traj.quintic_limit_ratio(&limits).unwrap();
649 traj.stretch_time(2.0).unwrap();
650 assert_eq!(traj.duration().get::<second>(), 2.0);
651 let after = traj.quintic_limit_ratio(&limits).unwrap();
652 assert!(after < before);
653 assert!(traj.stretch_time(0.0).is_err());
654 }
655}