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