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