Skip to main content

cu29_runtime/
cutask.rs

1//! This module contains all the main definition of the traits you need to implement
2//! or interact with to create a Copper task.
3
4use crate::config::ComponentConfig;
5use crate::reflect::Reflect;
6use bincode::de::{Decode, Decoder};
7use bincode::enc::{Encode, Encoder};
8use bincode::error::{DecodeError, EncodeError};
9use compact_str::{CompactString, ToCompactString};
10use core::any::{TypeId, type_name};
11use cu29_clock::{PartialCuTimeRange, RobotClock, Tov};
12use cu29_traits::{
13    COMPACT_STRING_CAPACITY, CuCompactString, CuError, CuMsgMetadataTrait, CuResult,
14    ErasedCuStampedData, Metadata,
15};
16use serde::de::DeserializeOwned;
17use serde::{Deserialize, Serialize};
18
19use alloc::format;
20use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
21
22/// The state of a task.
23// Everything that is stateful in copper for zero copy constraints need to be restricted to this trait.
24pub trait CuMsgPayload:
25    Default + Debug + Clone + Encode + Decode<()> + Serialize + DeserializeOwned + Sized
26{
27}
28
29pub trait CuMsgPack {}
30
31// Also anything that follows this contract can be a payload (blanket implementation)
32impl<T> CuMsgPayload for T where
33    T: Default + Debug + Clone + Encode + Decode<()> + Serialize + DeserializeOwned + Sized
34{
35}
36
37macro_rules! impl_cu_msg_pack {
38    ($($name:ident),+) => {
39        impl<'cl, $($name),+> CuMsgPack for ($(&CuMsg<$name>,)+)
40        where
41            $($name: CuMsgPayload),+
42        {}
43    };
44}
45
46impl<T: CuMsgPayload> CuMsgPack for CuMsg<T> {}
47impl<T: CuMsgPayload> CuMsgPack for &CuMsg<T> {}
48impl<T: CuMsgPayload> CuMsgPack for (&CuMsg<T>,) {}
49impl CuMsgPack for () {}
50
51// Apply the macro to generate implementations for tuple sizes up to 5
52impl_cu_msg_pack!(T1, T2);
53impl_cu_msg_pack!(T1, T2, T3);
54impl_cu_msg_pack!(T1, T2, T3, T4);
55impl_cu_msg_pack!(T1, T2, T3, T4, T5);
56
57// A convenience macro to get from a payload or a list of payloads to a proper CuMsg or CuMsgPack
58// declaration for your tasks used for input messages.
59#[macro_export]
60macro_rules! input_msg {
61    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
62        ( & $lt CuMsg<$first>, $( & $lt CuMsg<$rest> ),+ )
63    };
64    ($lt:lifetime, $ty:ty) => {
65        CuMsg<$ty>   // This is for backward compatibility
66    };
67    ($ty:ty) => {
68        CuMsg<$ty>
69    };
70}
71
72// A convenience macro to get from a payload to a proper CuMsg used as output.
73#[macro_export]
74macro_rules! output_msg {
75    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
76        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
77    };
78    ($first:ty, $($rest:ty),+) => {
79        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
80    };
81    ($ty:ty) => {
82        CuMsg<$ty>
83    };
84    ($lt:lifetime, $ty:ty) => {
85        CuMsg<$ty>  // This is for backward compatibility
86    };
87}
88
89/// CuMsgMetadata is a structure that contains metadata common to all CuStampedDataSet.
90#[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize)]
91pub struct CuMsgMetadata {
92    /// The time range used for the processing of this message
93    pub process_time: PartialCuTimeRange,
94    /// A small string for real time feedback purposes.
95    /// This is useful for to display on the field when the tasks are operating correctly.
96    pub status_txt: CuCompactString,
97}
98
99impl Metadata for CuMsgMetadata {}
100
101impl CuMsgMetadata {
102    pub fn set_status(&mut self, status: impl ToCompactString) {
103        self.status_txt = CuCompactString(status.to_compact_string());
104    }
105}
106
107impl CuMsgMetadataTrait for CuMsgMetadata {
108    fn process_time(&self) -> PartialCuTimeRange {
109        self.process_time
110    }
111
112    fn status_txt(&self) -> &CuCompactString {
113        &self.status_txt
114    }
115}
116
117impl Display for CuMsgMetadata {
118    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
119        write!(
120            f,
121            "process_time start: {}, process_time end: {}",
122            self.process_time.start, self.process_time.end
123        )
124    }
125}
126
127/// CuMsg is the envelope holding the msg payload and the metadata between tasks.
128#[derive(Default, Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize)]
129#[serde(bound(
130    serialize = "T: Serialize, M: Serialize",
131    deserialize = "T: DeserializeOwned, M: DeserializeOwned"
132))]
133pub struct CuStampedData<T, M>
134where
135    T: CuMsgPayload,
136    M: Metadata,
137{
138    /// This payload is the actual data exchanged between tasks.
139    payload: Option<T>,
140
141    /// The time of validity of the message.
142    /// It can be undefined (None), one measure point or a range of measures (TimeRange).
143    pub tov: Tov,
144
145    /// This metadata is the data that is common to all messages.
146    pub metadata: M,
147}
148
149impl Default for CuMsgMetadata {
150    fn default() -> Self {
151        CuMsgMetadata {
152            process_time: PartialCuTimeRange::default(),
153            status_txt: CuCompactString(CompactString::with_capacity(COMPACT_STRING_CAPACITY)),
154        }
155    }
156}
157
158impl<T, M> CuStampedData<T, M>
159where
160    T: CuMsgPayload,
161    M: Metadata,
162{
163    pub fn new(payload: Option<T>) -> Self {
164        CuStampedData {
165            payload,
166            tov: Tov::default(),
167            metadata: M::default(),
168        }
169    }
170    pub fn payload(&self) -> Option<&T> {
171        self.payload.as_ref()
172    }
173
174    pub fn set_payload(&mut self, payload: T) {
175        self.payload = Some(payload);
176    }
177
178    pub fn clear_payload(&mut self) {
179        self.payload = None;
180    }
181
182    pub fn payload_mut(&mut self) -> &mut Option<T> {
183        &mut self.payload
184    }
185}
186
187impl<T, M> ErasedCuStampedData for CuStampedData<T, M>
188where
189    T: CuMsgPayload,
190    M: CuMsgMetadataTrait + Metadata,
191{
192    fn payload(&self) -> Option<&dyn erased_serde::Serialize> {
193        self.payload
194            .as_ref()
195            .map(|p| p as &dyn erased_serde::Serialize)
196    }
197
198    fn tov(&self) -> Tov {
199        self.tov
200    }
201
202    fn metadata(&self) -> &dyn CuMsgMetadataTrait {
203        &self.metadata
204    }
205}
206
207/// This is the robotics message type for Copper with the correct Metadata type
208/// that will be used by the runtime.
209pub type CuMsg<T> = CuStampedData<T, CuMsgMetadata>;
210
211impl<T: CuMsgPayload> CuStampedData<T, CuMsgMetadata> {
212    /// Reinterprets the payload type carried by this message.
213    ///
214    /// # Safety
215    ///
216    /// The caller must guarantee that the message really contains a payload of type `U`. Failing
217    /// to do so is undefined behaviour.
218    pub unsafe fn assume_payload<U: CuMsgPayload>(&self) -> &CuMsg<U> {
219        // SAFETY: Caller guarantees that the underlying payload is of type U.
220        unsafe { &*(self as *const CuMsg<T> as *const CuMsg<U>) }
221    }
222
223    /// Mutable variant of [`assume_payload`](Self::assume_payload).
224    ///
225    /// # Safety
226    ///
227    /// The caller must guarantee that mutating the returned message is sound for the actual
228    /// payload type stored in the buffer.
229    pub unsafe fn assume_payload_mut<U: CuMsgPayload>(&mut self) -> &mut CuMsg<U> {
230        // SAFETY: Caller guarantees that the underlying payload is of type U.
231        unsafe { &mut *(self as *mut CuMsg<T> as *mut CuMsg<U>) }
232    }
233}
234
235impl<T: CuMsgPayload + 'static> CuStampedData<T, CuMsgMetadata> {
236    fn downcast_err<U: CuMsgPayload + 'static>() -> CuError {
237        CuError::from(format!(
238            "CuMsg payload mismatch: {} cannot be reinterpreted as {}",
239            type_name::<T>(),
240            type_name::<U>()
241        ))
242    }
243
244    /// Attempts to view this message as carrying payload `U`.
245    pub fn downcast_ref<U: CuMsgPayload + 'static>(&self) -> CuResult<&CuMsg<U>> {
246        if TypeId::of::<T>() == TypeId::of::<U>() {
247            // SAFETY: We just proved that T == U.
248            Ok(unsafe { self.assume_payload::<U>() })
249        } else {
250            Err(Self::downcast_err::<U>())
251        }
252    }
253
254    /// Mutable variant of [`downcast_ref`](Self::downcast_ref).
255    pub fn downcast_mut<U: CuMsgPayload + 'static>(&mut self) -> CuResult<&mut CuMsg<U>> {
256        if TypeId::of::<T>() == TypeId::of::<U>() {
257            // SAFETY: We just proved that T == U.
258            Ok(unsafe { self.assume_payload_mut::<U>() })
259        } else {
260            Err(Self::downcast_err::<U>())
261        }
262    }
263}
264
265/// The internal state of a task needs to be serializable
266/// so the framework can take a snapshot of the task graph.
267pub trait Freezable {
268    /// This method is called by the framework when it wants to save the task state.
269    /// The default implementation is to encode nothing (stateless).
270    /// If you have a state, you need to implement this method.
271    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
272        Encode::encode(&(), encoder) // default is stateless
273    }
274
275    /// This method is called by the framework when it wants to restore the task to a specific state.
276    /// Here it is similar to Decode but the framework will give you a new instance of the task (the new method will be called)
277    fn thaw<D: Decoder>(&mut self, _decoder: &mut D) -> Result<(), DecodeError> {
278        Ok(())
279    }
280}
281
282/// Bincode Adapter for Freezable tasks
283/// This allows the use of the bincode API directly to freeze and thaw tasks.
284pub struct BincodeAdapter<'a, T: Freezable + ?Sized>(pub &'a T);
285
286impl<'a, T: Freezable + ?Sized> Encode for BincodeAdapter<'a, T> {
287    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
288        self.0.freeze(encoder)
289    }
290}
291
292/// A Src Task is a task that only produces messages. For example drivers for sensors are Src Tasks.
293/// They are in push mode from the runtime.
294/// To set the frequency of the pulls and align them to any hw, see the runtime configuration.
295/// Note: A source has the privilege to have a clock passed to it vs a frozen clock.
296pub trait CuSrcTask: Freezable + Reflect {
297    type Output<'m>: CuMsgPayload;
298    /// Resources required by the task.
299    type Resources<'r>;
300
301    /// Here you need to initialize everything your task will need for the duration of its lifetime.
302    /// The config allows you to access the configuration of the task.
303    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
304    where
305        Self: Sized;
306
307    /// Start is called between the creation of the task and the first call to pre/process.
308    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
309        Ok(())
310    }
311
312    /// This is a method called by the runtime before "process". This is a kind of best effort,
313    /// as soon as possible call to give a chance for the task to do some work before to prepare
314    /// to make "process" as short as possible.
315    fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
316        Ok(())
317    }
318
319    /// Process is the most critical execution of the task.
320    /// The goal will be to produce the output message as soon as possible.
321    /// Use preprocess to prepare the task to make this method as short as possible.
322    fn process<'o>(&mut self, clock: &RobotClock, new_msg: &mut Self::Output<'o>) -> CuResult<()>;
323
324    /// This is a method called by the runtime after "process". It is best effort a chance for
325    /// the task to update some state after process is out of the way.
326    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
327    fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
328        Ok(())
329    }
330
331    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
332    fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
333        Ok(())
334    }
335}
336
337/// This is the most generic Task of copper. It is a "transform" task deriving an output from an input.
338pub trait CuTask: Freezable + Reflect {
339    type Input<'m>: CuMsgPack;
340    type Output<'m>: CuMsgPayload;
341    /// Resources required by the task.
342    type Resources<'r>;
343
344    /// Here you need to initialize everything your task will need for the duration of its lifetime.
345    /// The config allows you to access the configuration of the task.
346    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
347    where
348        Self: Sized;
349
350    /// Start is called between the creation of the task and the first call to pre/process.
351    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
352        Ok(())
353    }
354
355    /// This is a method called by the runtime before "process". This is a kind of best effort,
356    /// as soon as possible call to give a chance for the task to do some work before to prepare
357    /// to make "process" as short as possible.
358    fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
359        Ok(())
360    }
361
362    /// Process is the most critical execution of the task.
363    /// The goal will be to produce the output message as soon as possible.
364    /// Use preprocess to prepare the task to make this method as short as possible.
365    fn process<'i, 'o>(
366        &mut self,
367        _clock: &RobotClock,
368        input: &Self::Input<'i>,
369        output: &mut Self::Output<'o>,
370    ) -> CuResult<()>;
371
372    /// This is a method called by the runtime after "process". It is best effort a chance for
373    /// the task to update some state after process is out of the way.
374    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
375    fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
376        Ok(())
377    }
378
379    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
380    fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
381        Ok(())
382    }
383}
384
385/// A Sink Task is a task that only consumes messages. For example drivers for actuators are Sink Tasks.
386pub trait CuSinkTask: Freezable + Reflect {
387    type Input<'m>: CuMsgPack;
388    /// Resources required by the task.
389    type Resources<'r>;
390
391    /// Here you need to initialize everything your task will need for the duration of its lifetime.
392    /// The config allows you to access the configuration of the task.
393    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
394    where
395        Self: Sized;
396
397    /// Start is called between the creation of the task and the first call to pre/process.
398    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
399        Ok(())
400    }
401
402    /// This is a method called by the runtime before "process". This is a kind of best effort,
403    /// as soon as possible call to give a chance for the task to do some work before to prepare
404    /// to make "process" as short as possible.
405    fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
406        Ok(())
407    }
408
409    /// Process is the most critical execution of the task.
410    /// The goal will be to produce the output message as soon as possible.
411    /// Use preprocess to prepare the task to make this method as short as possible.
412    fn process<'i>(&mut self, _clock: &RobotClock, input: &Self::Input<'i>) -> CuResult<()>;
413
414    /// This is a method called by the runtime after "process". It is best effort a chance for
415    /// the task to update some state after process is out of the way.
416    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
417    fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
418        Ok(())
419    }
420
421    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
422    fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
423        Ok(())
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use bincode::{config, decode_from_slice, encode_to_vec};
431
432    #[test]
433    fn test_cucompactstr_encode_decode() {
434        let cstr = CuCompactString(CompactString::from("hello"));
435        let config = config::standard();
436        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
437        let (decoded, _): (CuCompactString, usize) =
438            decode_from_slice(&encoded, config).expect("Decoding failed");
439        assert_eq!(cstr.0, decoded.0);
440    }
441}