1use crate::CuTrajectoryInterpolation;
9use crate::CuTrajectoryPayload;
10use crate::MAX_TRAJECTORY_POINTS;
11use bincode::Decode;
12use bincode::Encode;
13use bincode::de::Decoder;
14use bincode::enc::Encoder;
15use bincode::error::DecodeError;
16use bincode::error::EncodeError;
17use cu29::prelude::*;
18use cu29::units::si::f64::Time;
19use cu29::units::si::time::second;
20use repose_kinematics::CuJointPosition;
21use repose_kinematics::CuJointVelocity;
22use serde::Deserialize;
23use serde::Serialize;
24
25#[derive(
27 Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
28)]
29#[serde(rename_all = "snake_case")]
30pub enum CuFollowStatus {
31 #[default]
33 Idle,
34 Running,
36 Finished,
38 ErrorEmptyTrajectory,
40}
41
42#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
47#[reflect(no_field_bounds, from_reflect = false)]
48pub struct CuTrajectoryFollowOutput<const N: usize> {
49 #[serde(with = "repose_kinematics::serde_arrays::position_array")]
50 pub positions: [CuJointPosition; N],
51 #[serde(with = "repose_kinematics::serde_arrays::velocity_array")]
52 pub velocities: [CuJointVelocity; N],
53 pub status: CuFollowStatus,
54}
55
56impl<const N: usize> Default for CuTrajectoryFollowOutput<N> {
57 fn default() -> Self {
58 Self {
59 positions: [CuJointPosition::default(); N],
60 velocities: [CuJointVelocity::default(); N],
61 status: CuFollowStatus::default(),
62 }
63 }
64}
65
66#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
73#[serde(default, deny_unknown_fields)]
74pub struct CuFollowTrajectoryParams {
75 pub interpolation: Option<CuTrajectoryInterpolation>,
78}
79
80impl CuFollowTrajectoryParams {
81 pub fn validate(&self) -> CuResult<()> {
83 Ok(())
84 }
85}
86
87#[derive(Reflect)]
100#[reflect(no_field_bounds, from_reflect = false)]
101pub struct CuFollowTrajectoryTask<const N: usize, const MAX_POINTS: usize = MAX_TRAJECTORY_POINTS> {
102 #[reflect(ignore)]
103 trajectory: Option<CuTrajectoryPayload<N, MAX_POINTS>>,
104 #[reflect(ignore)]
105 start: CuTime,
106 params: CuFollowTrajectoryParams,
107}
108
109impl<const N: usize, const MAX_POINTS: usize> CuFollowTrajectoryTask<N, MAX_POINTS> {
110 pub fn from_params(params: CuFollowTrajectoryParams) -> CuResult<Self> {
112 params.validate()?;
113 Ok(Self {
114 trajectory: None,
115 start: CuTime::MIN,
116 params,
117 })
118 }
119
120 pub const fn params(&self) -> &CuFollowTrajectoryParams {
122 &self.params
123 }
124}
125
126impl<const N: usize, const MAX_POINTS: usize> Default for CuFollowTrajectoryTask<N, MAX_POINTS> {
127 fn default() -> Self {
129 Self {
130 trajectory: None,
131 start: CuTime::MIN,
132 params: CuFollowTrajectoryParams::default(),
133 }
134 }
135}
136
137impl<const N: usize, const MAX_POINTS: usize> Freezable for CuFollowTrajectoryTask<N, MAX_POINTS> {
138 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
139 Encode::encode(&self.trajectory, encoder)?;
140 Encode::encode(&self.start, encoder)?;
141 Encode::encode(&self.params.interpolation, encoder)
142 }
143
144 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
145 self.trajectory = Decode::decode(decoder)?;
146 self.start = Decode::decode(decoder)?;
147 self.params.interpolation = Decode::decode(decoder)?;
148 Ok(())
149 }
150}
151
152impl<const N: usize, const MAX_POINTS: usize> CuTask for CuFollowTrajectoryTask<N, MAX_POINTS> {
153 type Resources<'r> = ();
154 type Input<'m> = input_msg!(CuTrajectoryPayload<N, MAX_POINTS>);
155 type Output<'m> = output_msg!(CuTrajectoryFollowOutput<N>);
156
157 fn new(cfg: Option<&ComponentConfig>, _res: Self::Resources<'_>) -> CuResult<Self>
158 where
159 Self: Sized,
160 {
161 let params = cfg
162 .map(ComponentConfig::deserialize_into::<CuFollowTrajectoryParams>)
163 .transpose()?
164 .unwrap_or_default();
165 Self::from_params(params)
166 }
167
168 fn process<'i, 'o>(
169 &mut self,
170 ctx: &CuContext,
171 input: &Self::Input<'i>,
172 output: &mut Self::Output<'o>,
173 ) -> CuResult<()> {
174 output.tov = input.tov;
175 let now = match input.tov {
176 Tov::Time(t) => t,
177 _ => ctx.now(),
178 };
179
180 if let Some(traj) = input.payload() {
181 if traj.is_empty() {
182 self.trajectory = None;
183 output.set_payload(CuTrajectoryFollowOutput {
184 status: CuFollowStatus::ErrorEmptyTrajectory,
185 ..Default::default()
186 });
187 return Ok(());
188 }
189 if self.trajectory.as_ref().is_none_or(|stored| stored != traj) {
190 self.trajectory = Some(traj.clone());
191 self.start = now;
192 }
193 }
194
195 let Some(stored) = self.trajectory.as_ref() else {
196 output.set_payload(CuTrajectoryFollowOutput::default());
197 return Ok(());
198 };
199
200 let elapsed = now - self.start;
201 let elapsed_s = elapsed.as_nanos() as f64 / 1_000_000_000.0;
202 let mut positions = [CuJointPosition::default(); N];
203 let mut velocities = [CuJointVelocity::default(); N];
204 let interpolation = self.params.interpolation.unwrap_or(stored.interpolation);
205 stored.sample_with(
206 interpolation,
207 Time::new::<second>(elapsed_s),
208 &mut positions,
209 &mut velocities,
210 )?;
211 let status = if elapsed_s >= stored.duration().get::<second>() {
212 CuFollowStatus::Finished
213 } else {
214 CuFollowStatus::Running
215 };
216 output.set_payload(CuTrajectoryFollowOutput {
217 positions,
218 velocities,
219 status,
220 });
221 Ok(())
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::CuTrajectoryPoint;
229 use cu29::units::si::angle::radian;
230 use cu29::units::si::angular_velocity::radian_per_second;
231 use cu29::units::si::f64::Angle;
232 use cu29::units::si::f64::AngularVelocity;
233
234 fn point(t: f64, q: f64, v: f64) -> CuTrajectoryPoint<1> {
235 CuTrajectoryPoint {
236 time_from_start: Time::new::<second>(t),
237 positions: [CuJointPosition::Revolute(Angle::new::<radian>(q))],
238 velocities: [CuJointVelocity::Revolute(AngularVelocity::new::<
239 radian_per_second,
240 >(v))],
241 ..Default::default()
242 }
243 }
244
245 fn cubic() -> CuFollowTrajectoryParams {
246 CuFollowTrajectoryParams {
247 interpolation: Some(CuTrajectoryInterpolation::Cubic),
248 }
249 }
250
251 fn two_point_traj() -> CuTrajectoryPayload<1> {
252 let mut traj = CuTrajectoryPayload::new();
253 traj.push(point(0.0, 0.0, 0.0)).unwrap();
254 traj.push(point(2.0, 2.0, 4.0)).unwrap();
255 traj
256 }
257
258 fn msg_at(traj: Option<CuTrajectoryPayload<1>>, secs: u64) -> CuMsg<CuTrajectoryPayload<1>> {
259 let mut msg = CuMsg::new(traj);
260 msg.tov = Tov::Time(CuTime::from_secs(secs));
261 msg
262 }
263
264 #[test]
265 fn test_idle_before_any_trajectory() {
266 let (ctx, _) = CuContext::new_mock_clock();
267 let mut task = CuFollowTrajectoryTask::<1>::default();
268 let input = CuMsg::new(None);
269 let mut output = CuMsg::new(None);
270 task.process(&ctx, &input, &mut output).unwrap();
271 let out = output.payload().unwrap();
272 assert_eq!(out.status, CuFollowStatus::Idle);
273 assert_eq!(out.positions[0].si_value(), 0.0);
274 }
275
276 #[test]
277 fn test_midpoint_is_running() {
278 let (ctx, _) = CuContext::new_mock_clock();
279 let mut task = CuFollowTrajectoryTask::<1>::default();
280 let mut output = CuMsg::new(None);
281 task.process(&ctx, &msg_at(Some(two_point_traj()), 100), &mut output)
282 .unwrap();
283 assert_eq!(output.payload().unwrap().status, CuFollowStatus::Running);
284 task.process(&ctx, &msg_at(Some(two_point_traj()), 101), &mut output)
285 .unwrap();
286 let out = output.payload().unwrap();
287 assert_eq!(out.status, CuFollowStatus::Running);
288 assert!((out.positions[0].si_value() - 1.0).abs() < 1e-12);
289 assert!((out.velocities[0].si_value() - 2.0).abs() < 1e-12);
290 }
291
292 #[test]
293 fn cubic_follower_samples_position_and_velocity_consistently() {
294 let (ctx, _) = CuContext::new_mock_clock();
295 let mut task = CuFollowTrajectoryTask::<1>::from_params(cubic()).unwrap();
296 let mut output = CuMsg::new(None);
297 task.process(&ctx, &msg_at(Some(two_point_traj()), 100), &mut output)
298 .unwrap();
299 task.process(&ctx, &msg_at(Some(two_point_traj()), 101), &mut output)
300 .unwrap();
301 let out = output.payload().unwrap();
302 assert!((out.positions[0].si_value() - 0.0).abs() < 1e-12);
303 assert!((out.velocities[0].si_value() - 0.5).abs() < 1e-12);
304 }
305
306 #[test]
307 fn freeze_thaw_preserves_cubic_mode_and_clock() {
308 let (ctx, _) = CuContext::new_mock_clock();
309 let mut original = CuFollowTrajectoryTask::<1>::from_params(cubic()).unwrap();
310 let mut output = CuMsg::new(None);
311 original
312 .process(&ctx, &msg_at(Some(two_point_traj()), 100), &mut output)
313 .unwrap();
314 let mut bytes = [0u8; 8192];
315 let mut encoder = bincode::enc::EncoderImpl::new(
316 bincode::enc::write::SliceWriter::new(&mut bytes),
317 bincode::config::standard(),
318 );
319 original.freeze(&mut encoder).unwrap();
320 let len = encoder.into_writer().bytes_written();
321 let mut decoder = bincode::de::DecoderImpl::new(
322 bincode::de::read::SliceReader::new(&bytes[..len]),
323 bincode::config::standard(),
324 (),
325 );
326 let mut restored = CuFollowTrajectoryTask::<1>::default();
327 restored.thaw(&mut decoder).unwrap();
328 restored
329 .process(&ctx, &msg_at(None, 101), &mut output)
330 .unwrap();
331 let out = output.payload().unwrap();
332 assert!((out.positions[0].si_value() - 0.0).abs() < 1e-12);
333 assert!((out.velocities[0].si_value() - 0.5).abs() < 1e-12);
334 }
335
336 #[test]
337 fn test_finished_holds_final_point() {
338 let (ctx, _) = CuContext::new_mock_clock();
339 let mut task = CuFollowTrajectoryTask::<1>::default();
340 let mut output = CuMsg::new(None);
341 task.process(&ctx, &msg_at(Some(two_point_traj()), 100), &mut output)
342 .unwrap();
343 task.process(&ctx, &msg_at(Some(two_point_traj()), 102), &mut output)
344 .unwrap();
345 assert_eq!(output.payload().unwrap().status, CuFollowStatus::Finished);
346 task.process(&ctx, &msg_at(Some(two_point_traj()), 110), &mut output)
347 .unwrap();
348 let out = output.payload().unwrap();
349 assert_eq!(out.status, CuFollowStatus::Finished);
350 assert_eq!(out.positions[0].si_value(), 2.0);
351 assert_eq!(out.velocities[0].si_value(), 4.0);
352 }
353
354 #[test]
355 fn test_new_trajectory_preempts_and_resets_clock() {
356 let (ctx, _) = CuContext::new_mock_clock();
357 let mut task = CuFollowTrajectoryTask::<1>::default();
358 let mut output = CuMsg::new(None);
359 task.process(&ctx, &msg_at(Some(two_point_traj()), 100), &mut output)
360 .unwrap();
361 task.process(&ctx, &msg_at(Some(two_point_traj()), 101), &mut output)
362 .unwrap();
363 let mut other = CuTrajectoryPayload::new();
364 other.push(point(0.0, 10.0, 0.0)).unwrap();
365 other.push(point(4.0, 14.0, 0.0)).unwrap();
366 task.process(&ctx, &msg_at(Some(other), 150), &mut output)
367 .unwrap();
368 let out = output.payload().unwrap();
369 assert_eq!(out.status, CuFollowStatus::Running);
370 assert_eq!(out.positions[0].si_value(), 10.0);
371 }
372
373 #[test]
374 fn test_empty_trajectory_reports_error() {
375 let (ctx, _) = CuContext::new_mock_clock();
376 let mut task = CuFollowTrajectoryTask::<1>::default();
377 let mut output = CuMsg::new(None);
378 task.process(
379 &ctx,
380 &msg_at(Some(CuTrajectoryPayload::new()), 100),
381 &mut output,
382 )
383 .unwrap();
384 assert_eq!(
385 output.payload().unwrap().status,
386 CuFollowStatus::ErrorEmptyTrajectory
387 );
388 task.process(&ctx, &CuMsg::new(None), &mut output).unwrap();
389 assert_eq!(output.payload().unwrap().status, CuFollowStatus::Idle);
390 }
391
392 #[test]
393 fn default_follower_uses_the_payload_interpolation() {
394 let (ctx, _) = CuContext::new_mock_clock();
395 let mut task = CuFollowTrajectoryTask::<1>::default();
396 let mut traj = two_point_traj();
397 traj.interpolation = CuTrajectoryInterpolation::Cubic;
398 let mut output = CuMsg::new(None);
399 task.process(&ctx, &msg_at(Some(traj.clone()), 100), &mut output)
400 .unwrap();
401 task.process(&ctx, &msg_at(Some(traj), 101), &mut output)
402 .unwrap();
403 let out = output.payload().unwrap();
404 assert!((out.positions[0].si_value() - 0.0).abs() < 1e-12);
405 assert!((out.velocities[0].si_value() - 0.5).abs() < 1e-12);
406 }
407
408 #[test]
409 fn explicit_interpolation_overrides_the_payload() {
410 let (ctx, _) = CuContext::new_mock_clock();
411 let mut task = CuFollowTrajectoryTask::<1>::from_params(CuFollowTrajectoryParams {
412 interpolation: Some(CuTrajectoryInterpolation::Linear),
413 })
414 .unwrap();
415 let mut traj = two_point_traj();
416 traj.interpolation = CuTrajectoryInterpolation::Cubic;
417 let mut output = CuMsg::new(None);
418 task.process(&ctx, &msg_at(Some(traj.clone()), 100), &mut output)
419 .unwrap();
420 task.process(&ctx, &msg_at(Some(traj), 101), &mut output)
421 .unwrap();
422 let out = output.payload().unwrap();
423 assert!((out.positions[0].si_value() - 1.0).abs() < 1e-12);
424 assert!((out.velocities[0].si_value() - 2.0).abs() < 1e-12);
425 }
426
427 #[cfg(feature = "std")]
428 #[test]
429 fn reads_config() {
430 let cfg: ComponentConfig =
431 ron::from_str(r#"#![enable(unwrap_newtypes)] {"interpolation": "cubic"}"#).unwrap();
432 let task = CuFollowTrajectoryTask::<1>::new(Some(&cfg), ()).unwrap();
433 assert_eq!(task.params(), &cubic());
434 let task = CuFollowTrajectoryTask::<1>::new(None, ()).unwrap();
435 assert_eq!(task.params().interpolation, None);
436 let typo: ComponentConfig =
437 ron::from_str(r#"#![enable(unwrap_newtypes)] {"interpolaton": "cubic"}"#).unwrap();
438 assert!(CuFollowTrajectoryTask::<1>::new(Some(&typo), ()).is_err());
439 }
440}