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