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