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}