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