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;
14use serde_derive::{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 + 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 + 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 bytes = self.0.as_bytes();
77 bytes.encode(encoder)
78 }
79}
80
81impl Decode for CuCompactString {
82 fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
83 let bytes = <Vec<u8> as Decode>::decode(decoder)?; // Decode into a byte buffer
84 let compact_string =
85 CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
86 Ok(CuCompactString(compact_string))
87 }
88}
89
90impl<'de> BorrowDecode<'de> for CuCompactString {
91 fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
92 CuCompactString::decode(decoder)
93 }
94}
95
96/// CuMsgMetadata is a structure that contains metadata common to all CuMsgs.
97#[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize)]
98pub struct CuMsgMetadata {
99 /// The time range used for the processing of this message
100 pub process_time: PartialCuTimeRange,
101 /// The time of validity of the message.
102 /// It can be undefined (None), one measure point or a range of measures (TimeRange).
103 pub tov: Tov,
104 /// A small string for real time feedback purposes.
105 /// This is useful for to display on the field when the tasks are operating correctly.
106 pub status_txt: CuCompactString,
107}
108
109impl CuMsgMetadata {
110 pub fn set_status(&mut self, status: impl ToCompactString) {
111 self.status_txt = CuCompactString(status.to_compact_string());
112 }
113}
114
115impl Display for CuMsgMetadata {
116 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117 write!(
118 f,
119 "process_time start: {}, process_time end: {}",
120 self.process_time.start, self.process_time.end
121 )
122 }
123}
124
125/// CuMsg is the envelope holding the msg payload and the metadata between tasks.
126#[derive(Default, Debug, Clone, bincode::Encode, bincode::Decode)]
127pub struct CuMsg<T>
128where
129 T: CuMsgPayload,
130{
131 /// This payload is the actual data exchanged between tasks.
132 payload: Option<T>,
133
134 /// This metadata is the data that is common to all messages.
135 pub metadata: CuMsgMetadata,
136}
137
138impl Default for CuMsgMetadata {
139 fn default() -> Self {
140 CuMsgMetadata {
141 process_time: PartialCuTimeRange::default(),
142 tov: Tov::default(),
143 status_txt: CuCompactString(CompactString::with_capacity(COMPACT_STRING_CAPACITY)),
144 }
145 }
146}
147
148impl<T> CuMsg<T>
149where
150 T: CuMsgPayload,
151{
152 pub fn new(payload: Option<T>) -> Self {
153 CuMsg {
154 payload,
155 metadata: CuMsgMetadata::default(),
156 }
157 }
158 pub fn payload(&self) -> Option<&T> {
159 self.payload.as_ref()
160 }
161
162 pub fn set_payload(&mut self, payload: T) {
163 self.payload = Some(payload);
164 }
165
166 pub fn clear_payload(&mut self) {
167 self.payload = None;
168 }
169
170 pub fn payload_mut(&mut self) -> &mut Option<T> {
171 &mut self.payload
172 }
173}
174
175/// The internal state of a task needs to be serializable
176/// so the framework can take a snapshot of the task graph.
177pub trait Freezable {
178 /// This method is called by the framework when it wants to save the task state.
179 /// The default implementation is to encode nothing (stateless).
180 /// If you have a state, you need to implement this method.
181 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
182 Encode::encode(&(), encoder) // default is stateless
183 }
184
185 /// This method is called by the framework when it wants to restore the task to a specific state.
186 /// Here it is similar to Decode but the framework will give you a new instance of the task (the new method will be called)
187 #[allow(unused_variables)]
188 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
189 Ok(())
190 }
191}
192
193/// A Src Task is a task that only produces messages. For example drivers for sensors are Src Tasks.
194/// They are in push mode from the runtime.
195/// To set the frequency of the pulls and align them to any hw, see the runtime configuration.
196/// Note: A source has the privilege to have a clock passed to it vs a frozen clock.
197pub trait CuSrcTask<'cl>: Freezable {
198 type Output: CuMsgPack<'cl>;
199
200 /// Here you need to initialize everything your task will need for the duration of its lifetime.
201 /// The config allows you to access the configuration of the task.
202 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self>
203 where
204 Self: Sized;
205
206 /// Start is called between the creation of the task and the first call to pre/process.
207 fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
208 Ok(())
209 }
210
211 /// This is a method called by the runtime before "process". This is a kind of best effort,
212 /// as soon as possible call to give a chance for the task to do some work before to prepare
213 /// to make "process" as short as possible.
214 fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
215 Ok(())
216 }
217
218 /// Process is the most critical execution of the task.
219 /// The goal will be to produce the output message as soon as possible.
220 /// Use preprocess to prepare the task to make this method as short as possible.
221 fn process(&mut self, clock: &RobotClock, new_msg: Self::Output) -> CuResult<()>;
222
223 /// This is a method called by the runtime after "process". It is best effort a chance for
224 /// the task to update some state after process is out of the way.
225 /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
226 fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
227 Ok(())
228 }
229
230 /// Called to stop the task. It signals that the *process method won't be called until start is called again.
231 fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
232 Ok(())
233 }
234}
235
236/// This is the most generic Task of copper. It is a "transform" task deriving an output from an input.
237pub trait CuTask<'cl>: Freezable {
238 type Input: CuMsgPack<'cl>;
239 type Output: CuMsgPack<'cl>;
240
241 /// Here you need to initialize everything your task will need for the duration of its lifetime.
242 /// The config allows you to access the configuration of the task.
243 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self>
244 where
245 Self: Sized;
246
247 /// Start is called between the creation of the task and the first call to pre/process.
248 fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
249 Ok(())
250 }
251
252 /// This is a method called by the runtime before "process". This is a kind of best effort,
253 /// as soon as possible call to give a chance for the task to do some work before to prepare
254 /// to make "process" as short as possible.
255 fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
256 Ok(())
257 }
258
259 /// Process is the most critical execution of the task.
260 /// The goal will be to produce the output message as soon as possible.
261 /// Use preprocess to prepare the task to make this method as short as possible.
262 fn process(
263 &mut self,
264 _clock: &RobotClock,
265 input: Self::Input,
266 output: Self::Output,
267 ) -> CuResult<()>;
268
269 /// This is a method called by the runtime after "process". It is best effort a chance for
270 /// the task to update some state after process is out of the way.
271 /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
272 fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
273 Ok(())
274 }
275
276 /// Called to stop the task. It signals that the *process method won't be called until start is called again.
277 fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
278 Ok(())
279 }
280}
281
282/// A Sink Task is a task that only consumes messages. For example drivers for actuators are Sink Tasks.
283pub trait CuSinkTask<'cl>: Freezable {
284 type Input: CuMsgPack<'cl>;
285
286 /// Here you need to initialize everything your task will need for the duration of its lifetime.
287 /// The config allows you to access the configuration of the task.
288 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self>
289 where
290 Self: Sized;
291
292 /// Start is called between the creation of the task and the first call to pre/process.
293 fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
294 Ok(())
295 }
296
297 /// This is a method called by the runtime before "process". This is a kind of best effort,
298 /// as soon as possible call to give a chance for the task to do some work before to prepare
299 /// to make "process" as short as possible.
300 fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
301 Ok(())
302 }
303
304 /// Process is the most critical execution of the task.
305 /// The goal will be to produce the output message as soon as possible.
306 /// Use preprocess to prepare the task to make this method as short as possible.
307 fn process(&mut self, _clock: &RobotClock, input: Self::Input) -> CuResult<()>;
308
309 /// This is a method called by the runtime after "process". It is best effort a chance for
310 /// the task to update some state after process is out of the way.
311 /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
312 fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
313 Ok(())
314 }
315
316 /// Called to stop the task. It signals that the *process method won't be called until start is called again.
317 fn stop(&mut self, _clock: &RobotClock) -> CuResult<()> {
318 Ok(())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use bincode::{config, decode_from_slice, encode_to_vec};
326
327 #[test]
328 fn test_cucompactstr_encode_decode() {
329 let cstr = CuCompactString(CompactString::from("hello"));
330 let config = config::standard();
331 let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
332 let (decoded, _): (CuCompactString, usize) =
333 decode_from_slice(&encoded, config).expect("Decoding failed");
334 assert_eq!(cstr.0, decoded.0);
335 }
336}