Skip to main content

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 crate::context::CuContext;
6use crate::reflect::{GetTypeRegistration, Reflect, TypePath, TypeRegistry};
7#[cfg(feature = "reflect")]
8use bevy_reflect;
9use bincode::de::{Decode, Decoder};
10use bincode::enc::{Encode, Encoder};
11use bincode::error::{DecodeError, EncodeError};
12use compact_str::{CompactString, ToCompactString};
13use core::any::{TypeId, type_name};
14use cu29_clock::{PartialCuTimeRange, Tov};
15use cu29_traits::{
16    COMPACT_STRING_CAPACITY, CuCompactString, CuError, CuMsgMetadataTrait, CuMsgOrigin, CuResult,
17    ErasedCuStampedData, Metadata,
18};
19use serde::de::DeserializeOwned;
20use serde::{Deserialize, Serialize};
21
22use alloc::format;
23use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
24
25/// The state of a task.
26// Everything that is stateful in copper for zero copy constraints need to be restricted to this trait.
27#[cfg(feature = "reflect")]
28pub trait CuMsgPayload:
29    Default
30    + Debug
31    + Clone
32    + Encode
33    + Decode<()>
34    + Serialize
35    + DeserializeOwned
36    + Reflect
37    + TypePath
38    + Sized
39{
40}
41
42#[cfg(not(feature = "reflect"))]
43pub trait CuMsgPayload:
44    Default + Debug + Clone + Encode + Decode<()> + Serialize + DeserializeOwned + Reflect + Sized
45{
46}
47
48pub trait CuMsgPack {}
49
50// Also anything that follows this contract can be a payload (blanket implementation)
51#[cfg(feature = "reflect")]
52impl<T> CuMsgPayload for T where
53    T: Default
54        + Debug
55        + Clone
56        + Encode
57        + Decode<()>
58        + Serialize
59        + DeserializeOwned
60        + Reflect
61        + TypePath
62        + Sized
63{
64}
65
66#[cfg(not(feature = "reflect"))]
67impl<T> CuMsgPayload for T where
68    T: Default
69        + Debug
70        + Clone
71        + Encode
72        + Decode<()>
73        + Serialize
74        + DeserializeOwned
75        + Reflect
76        + Sized
77{
78}
79
80macro_rules! impl_cu_msg_pack {
81    ($($name:ident),+) => {
82        impl<'cl, $($name),+> CuMsgPack for ($(&CuMsg<$name>,)+)
83        where
84            $($name: CuMsgPayload),+
85        {}
86    };
87}
88
89macro_rules! impl_cu_msg_pack_up_to {
90    ($first:ident, $second:ident $(, $rest:ident)* $(,)?) => {
91        impl_cu_msg_pack!($first, $second);
92        impl_cu_msg_pack_up_to!(@accumulate ($first, $second); $($rest),*);
93    };
94    (@accumulate ($($acc:ident),+);) => {};
95    (@accumulate ($($acc:ident),+); $next:ident $(, $rest:ident)*) => {
96        impl_cu_msg_pack!($($acc),+, $next);
97        impl_cu_msg_pack_up_to!(@accumulate ($($acc),+, $next); $($rest),*);
98    };
99}
100
101impl<T: CuMsgPayload> CuMsgPack for CuMsg<T> {}
102impl<T: CuMsgPayload> CuMsgPack for &CuMsg<T> {}
103impl<T: CuMsgPayload> CuMsgPack for (&CuMsg<T>,) {}
104impl CuMsgPack for () {}
105
106// Apply the macro to generate implementations for tuple sizes up to 12.
107impl_cu_msg_pack_up_to!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
108
109// A convenience macro to get from a payload or a list of payloads to a proper CuMsg or CuMsgPack
110// declaration for your tasks used for input messages.
111#[macro_export]
112macro_rules! input_msg {
113    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
114        ( & $lt CuMsg<$first>, $( & $lt CuMsg<$rest> ),+ )
115    };
116    ($ty:ty) => {
117        CuMsg<$ty>
118    };
119}
120
121// A convenience macro to get from a payload to a proper CuMsg used as output.
122#[macro_export]
123macro_rules! output_msg {
124    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
125        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
126    };
127    ($first:ty, $($rest:ty),+) => {
128        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
129    };
130    ($ty:ty) => {
131        CuMsg<$ty>
132    };
133}
134
135/// Helper trait used by codegen when Copper needs to treat a task output as a
136/// single message slot without relying on config-declared output edges.
137pub trait CuSingleOutputMsg {
138    type Payload: CuMsgPayload;
139}
140
141impl<T: CuMsgPayload> CuSingleOutputMsg for CuMsg<T> {
142    type Payload = T;
143}
144
145/// CuMsgMetadata is a structure that contains metadata common to all CuStampedDataSet.
146#[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize, Reflect)]
147#[reflect(opaque, from_reflect = false, no_field_bounds)]
148pub struct CuMsgMetadata {
149    /// The time range used for the processing of this message
150    pub process_time: PartialCuTimeRange,
151    /// A small string for real time feedback purposes.
152    /// This is useful for to display on the field when the tasks are operating correctly.
153    pub status_txt: CuCompactString,
154    /// Remote Copper provenance captured on receive, when available.
155    pub origin: Option<CuMsgOrigin>,
156}
157
158impl Metadata for CuMsgMetadata {}
159
160impl CuMsgMetadata {
161    pub fn set_status(&mut self, status: impl ToCompactString) {
162        self.status_txt = CuCompactString(status.to_compact_string());
163    }
164
165    pub fn set_origin(&mut self, origin: CuMsgOrigin) {
166        self.origin = Some(origin);
167    }
168
169    pub fn clear_origin(&mut self) {
170        self.origin = None;
171    }
172}
173
174impl CuMsgMetadataTrait for CuMsgMetadata {
175    fn process_time(&self) -> PartialCuTimeRange {
176        self.process_time
177    }
178
179    fn status_txt(&self) -> &CuCompactString {
180        &self.status_txt
181    }
182
183    fn origin(&self) -> Option<&CuMsgOrigin> {
184        self.origin.as_ref()
185    }
186}
187
188impl Display for CuMsgMetadata {
189    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
190        write!(
191            f,
192            "process_time start: {}, process_time end: {}",
193            self.process_time.start, self.process_time.end
194        )
195    }
196}
197
198/// CuMsg is the envelope holding the msg payload and the metadata between tasks.
199#[derive(Default, Debug, Clone, bincode::Decode, Serialize, Deserialize, Reflect)]
200#[reflect(opaque, from_reflect = false, no_field_bounds)]
201#[serde(bound(
202    serialize = "T: Serialize, M: Serialize",
203    deserialize = "T: DeserializeOwned, M: DeserializeOwned"
204))]
205pub struct CuStampedData<T, M>
206where
207    T: CuMsgPayload,
208    M: Metadata,
209{
210    /// This payload is the actual data exchanged between tasks.
211    payload: Option<T>,
212
213    /// The time of validity of the message.
214    /// It can be undefined (None), one measure point or a range of measures (TimeRange).
215    pub tov: Tov,
216
217    /// This metadata is the data that is common to all messages.
218    pub metadata: M,
219}
220
221impl<T, M> Encode for CuStampedData<T, M>
222where
223    T: CuMsgPayload,
224    M: Metadata,
225{
226    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
227        // NOTE: the `HandleContent` policy decision (TouchedOnly / None) is NOT made
228        // here. It can't be: this impl is generic over `T`, so method resolution at
229        // the `payload_should_log()` call site would always pick the trait blanket
230        // default (true) — autoref-specialization only works at concrete-type sites.
231        // The codegen-emitted per-slot encoder in cu29_derive consults the policy at
232        // the concrete payload type and routes to `encode_metadata_only` when the
233        // bytes should be skipped. This generic impl just writes the full payload.
234        match &self.payload {
235            None => {
236                0u8.encode(encoder)?;
237            }
238            Some(payload) => {
239                1u8.encode(encoder)?;
240                let encoded_start = cu29_traits::observed_encode_bytes();
241                let handle_start = crate::monitoring::current_payload_handle_bytes();
242                payload.encode(encoder)?;
243                let encoded_bytes =
244                    cu29_traits::observed_encode_bytes().saturating_sub(encoded_start);
245                let handle_bytes =
246                    crate::monitoring::current_payload_handle_bytes().saturating_sub(handle_start);
247                crate::monitoring::record_current_slot_payload_io_stats(
248                    core::mem::size_of::<T>(),
249                    encoded_bytes,
250                    handle_bytes,
251                );
252            }
253        }
254        self.tov.encode(encoder)?;
255        self.metadata.encode(encoder)?;
256        Ok(())
257    }
258}
259
260/// Write a metadata-only record for a stamped message: presence tag = 0u8 (no payload),
261/// followed by `tov` and `metadata`. Wire-compatible with the existing decode path —
262/// a reader sees `payload: None` for the frame, same as if the source had been
263/// disabled entirely, but the surrounding timestamp/status are preserved.
264///
265/// Codegen emits a call to this helper when a slot's producing task is configured with
266/// `HandleContent::None` or `HandleContent::TouchedOnly` and the handle wasn't touched.
267pub fn encode_metadata_only<T, M, E>(
268    msg: &CuStampedData<T, M>,
269    encoder: &mut E,
270) -> Result<(), EncodeError>
271where
272    T: CuMsgPayload,
273    M: Metadata,
274    E: Encoder,
275{
276    0u8.encode(encoder)?;
277    msg.tov.encode(encoder)?;
278    msg.metadata.encode(encoder)?;
279    Ok(())
280}
281
282impl Default for CuMsgMetadata {
283    fn default() -> Self {
284        CuMsgMetadata {
285            process_time: PartialCuTimeRange::default(),
286            status_txt: CuCompactString(CompactString::with_capacity(COMPACT_STRING_CAPACITY)),
287            origin: None,
288        }
289    }
290}
291
292impl<T, M> CuStampedData<T, M>
293where
294    T: CuMsgPayload,
295    M: Metadata,
296{
297    /// Initializes an empty message directly in preallocated pool storage.
298    ///
299    /// # Safety
300    /// `dst` must point to aligned, writable storage for one `Self`. Its previous
301    /// contents are overwritten without being dropped.
302    #[doc(hidden)]
303    pub unsafe fn init_in_place(dst: *mut Self) {
304        // SAFETY: Each field is written independently before a reference to the
305        // message is formed. An empty payload does not construct a T.
306        unsafe {
307            // Copy from a promoted constant so debug builds do not materialize
308            // a potentially large Option<T> temporary on the startup stack.
309            core::ptr::copy_nonoverlapping(
310                const { &None::<T> },
311                core::ptr::addr_of_mut!((*dst).payload),
312                1,
313            );
314            core::ptr::addr_of_mut!((*dst).tov).write(Tov::default());
315            core::ptr::addr_of_mut!((*dst).metadata).write(M::default());
316        }
317    }
318
319    pub(crate) fn from_parts(payload: Option<T>, tov: Tov, metadata: M) -> Self {
320        CuStampedData {
321            payload,
322            tov,
323            metadata,
324        }
325    }
326
327    pub fn new(payload: Option<T>) -> Self {
328        Self::from_parts(payload, Tov::default(), M::default())
329    }
330    pub fn payload(&self) -> Option<&T> {
331        self.payload.as_ref()
332    }
333
334    pub fn set_payload(&mut self, payload: T) {
335        self.payload = Some(payload);
336    }
337
338    pub fn clear_payload(&mut self) {
339        self.payload = None;
340    }
341
342    pub fn payload_mut(&mut self) -> &mut Option<T> {
343        &mut self.payload
344    }
345}
346
347impl<T, M> ErasedCuStampedData for CuStampedData<T, M>
348where
349    T: CuMsgPayload,
350    M: CuMsgMetadataTrait + Metadata,
351{
352    fn payload(&self) -> Option<&dyn erased_serde::Serialize> {
353        self.payload
354            .as_ref()
355            .map(|p| p as &dyn erased_serde::Serialize)
356    }
357
358    #[cfg(feature = "reflect")]
359    fn payload_reflect(&self) -> Option<&dyn cu29_traits::Reflect> {
360        self.payload
361            .as_ref()
362            .map(|p| p as &dyn cu29_traits::Reflect)
363    }
364
365    fn tov(&self) -> Tov {
366        self.tov
367    }
368
369    fn metadata(&self) -> &dyn CuMsgMetadataTrait {
370        &self.metadata
371    }
372}
373
374/// This is the robotics message type for Copper with the correct Metadata type
375/// that will be used by the runtime.
376pub type CuMsg<T> = CuStampedData<T, CuMsgMetadata>;
377
378impl<T: CuMsgPayload> CuStampedData<T, CuMsgMetadata> {
379    /// Reinterprets the payload type carried by this message.
380    ///
381    /// # Safety
382    ///
383    /// The caller must guarantee that the message really contains a payload of type `U`. Failing
384    /// to do so is undefined behaviour.
385    pub unsafe fn assume_payload<U: CuMsgPayload>(&self) -> &CuMsg<U> {
386        // SAFETY: Caller guarantees that the underlying payload is of type U.
387        unsafe { &*(self as *const CuMsg<T> as *const CuMsg<U>) }
388    }
389
390    /// Mutable variant of [`assume_payload`](Self::assume_payload).
391    ///
392    /// # Safety
393    ///
394    /// The caller must guarantee that mutating the returned message is sound for the actual
395    /// payload type stored in the buffer.
396    pub unsafe fn assume_payload_mut<U: CuMsgPayload>(&mut self) -> &mut CuMsg<U> {
397        // SAFETY: Caller guarantees that the underlying payload is of type U.
398        unsafe { &mut *(self as *mut CuMsg<T> as *mut CuMsg<U>) }
399    }
400}
401
402impl<T: CuMsgPayload + 'static> CuStampedData<T, CuMsgMetadata> {
403    fn downcast_err<U: CuMsgPayload + 'static>() -> CuError {
404        CuError::from(format!(
405            "CuMsg payload mismatch: {} cannot be reinterpreted as {}",
406            type_name::<T>(),
407            type_name::<U>()
408        ))
409    }
410
411    /// Attempts to view this message as carrying payload `U`.
412    pub fn downcast_ref<U: CuMsgPayload + 'static>(&self) -> CuResult<&CuMsg<U>> {
413        if TypeId::of::<T>() == TypeId::of::<U>() {
414            // SAFETY: We just proved that T == U.
415            Ok(unsafe { self.assume_payload::<U>() })
416        } else {
417            Err(Self::downcast_err::<U>())
418        }
419    }
420
421    /// Mutable variant of [`downcast_ref`](Self::downcast_ref).
422    pub fn downcast_mut<U: CuMsgPayload + 'static>(&mut self) -> CuResult<&mut CuMsg<U>> {
423        if TypeId::of::<T>() == TypeId::of::<U>() {
424            // SAFETY: We just proved that T == U.
425            Ok(unsafe { self.assume_payload_mut::<U>() })
426        } else {
427            Err(Self::downcast_err::<U>())
428        }
429    }
430}
431
432/// The internal state of a task needs to be serializable
433/// so the framework can take a snapshot of the task graph.
434pub trait Freezable {
435    /// This method is called by the framework when it wants to save the task state.
436    /// The default implementation is to encode nothing (stateless).
437    /// If you have a state, you need to implement this method.
438    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
439        Encode::encode(&(), encoder) // default is stateless
440    }
441
442    /// This method is called by the framework when it wants to restore the task to a specific state.
443    /// Here it is similar to Decode but the framework will give you a new instance of the task (the new method will be called)
444    fn thaw<D: Decoder>(&mut self, _decoder: &mut D) -> Result<(), DecodeError> {
445        Ok(())
446    }
447}
448
449/// Bincode Adapter for Freezable tasks
450/// This allows the use of the bincode API directly to freeze and thaw tasks.
451pub struct BincodeAdapter<'a, T: Freezable + ?Sized>(pub &'a T);
452
453impl<'a, T: Freezable + ?Sized> Encode for BincodeAdapter<'a, T> {
454    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
455        self.0.freeze(encoder)
456    }
457}
458
459/// A Src Task is a task that only produces messages. For example drivers for sensors are Src Tasks.
460/// They are in push mode from the runtime.
461/// To set the frequency of the pulls and align them to any hw, see the runtime configuration.
462/// Note: A source has the privilege to have a clock passed to it vs a frozen clock.
463pub trait CuSrcTask: Freezable + Reflect {
464    type Output<'m>: CuMsgPayload;
465    /// Resources required by the task.
466    type Resources<'r>;
467
468    /// Registers the reflected type used as this task's debug-state contract.
469    ///
470    /// The default exposes the task struct itself. Override this when the task
471    /// contains ignored, third-party, hardware, or otherwise non-inspectable
472    /// internals and should expose a purpose-built debug-state view instead.
473    fn register_debug_state_types(registry: &mut TypeRegistry)
474    where
475        Self: GetTypeRegistration + Sized,
476    {
477        registry.register::<Self>();
478    }
479
480    /// Returns the reflected type path used as this task's debug-state schema.
481    fn debug_state_type_path() -> &'static str
482    where
483        Self: TypePath + Sized,
484    {
485        Self::type_path()
486    }
487
488    /// Borrows this task's current debug-state view.
489    ///
490    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
491    /// when the debug state is a projected view rather than the task struct.
492    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
493    where
494        Self: Sized,
495    {
496        f(self)
497    }
498
499    /// Here you need to initialize everything your task will need for the duration of its lifetime.
500    /// The config allows you to access the configuration of the task.
501    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
502    where
503        Self: Sized;
504
505    /// Start is called between the creation of the task and the first call to pre/process.
506    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
507        Ok(())
508    }
509
510    /// This is a method called by the runtime before "process". This is a kind of best effort,
511    /// as soon as possible call to give a chance for the task to do some work before to prepare
512    /// to make "process" as short as possible.
513    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
514        Ok(())
515    }
516
517    /// Process is the most critical execution of the task.
518    /// The goal will be to produce the output message as soon as possible.
519    /// Use preprocess to prepare the task to make this method as short as possible.
520    fn process<'o>(&mut self, ctx: &CuContext, new_msg: &mut Self::Output<'o>) -> CuResult<()>;
521
522    /// This is a method called by the runtime after "process". It is best effort a chance for
523    /// the task to update some state after process is out of the way.
524    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
525    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
526        Ok(())
527    }
528
529    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
530    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
531        Ok(())
532    }
533}
534
535/// This is the most generic Task of copper. It is a "transform" task deriving an output from an input.
536pub trait CuTask: Freezable + Reflect {
537    type Input<'m>: CuMsgPack;
538    type Output<'m>: CuMsgPayload;
539    /// Resources required by the task.
540    type Resources<'r>;
541
542    /// Registers the reflected type used as this task's debug-state contract.
543    ///
544    /// The default exposes the task struct itself. Override this when the task
545    /// contains ignored, third-party, hardware, or otherwise non-inspectable
546    /// internals and should expose a purpose-built debug-state view instead.
547    fn register_debug_state_types(registry: &mut TypeRegistry)
548    where
549        Self: GetTypeRegistration + Sized,
550    {
551        registry.register::<Self>();
552    }
553
554    /// Returns the reflected type path used as this task's debug-state schema.
555    fn debug_state_type_path() -> &'static str
556    where
557        Self: TypePath + Sized,
558    {
559        Self::type_path()
560    }
561
562    /// Borrows this task's current debug-state view.
563    ///
564    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
565    /// when the debug state is a projected view rather than the task struct.
566    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
567    where
568        Self: Sized,
569    {
570        f(self)
571    }
572
573    /// Here you need to initialize everything your task will need for the duration of its lifetime.
574    /// The config allows you to access the configuration of the task.
575    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
576    where
577        Self: Sized;
578
579    /// Start is called between the creation of the task and the first call to pre/process.
580    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
581        Ok(())
582    }
583
584    /// This is a method called by the runtime before "process". This is a kind of best effort,
585    /// as soon as possible call to give a chance for the task to do some work before to prepare
586    /// to make "process" as short as possible.
587    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
588        Ok(())
589    }
590
591    /// Process is the most critical execution of the task.
592    /// The goal will be to produce the output message as soon as possible.
593    /// Use preprocess to prepare the task to make this method as short as possible.
594    fn process<'i, 'o>(
595        &mut self,
596        _ctx: &CuContext,
597        input: &Self::Input<'i>,
598        output: &mut Self::Output<'o>,
599    ) -> CuResult<()>;
600
601    /// This is a method called by the runtime after "process". It is best effort a chance for
602    /// the task to update some state after process is out of the way.
603    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
604    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
605        Ok(())
606    }
607
608    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
609    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
610        Ok(())
611    }
612}
613
614/// A Sink Task is a task that only consumes messages. For example drivers for actuators are Sink Tasks.
615pub trait CuSinkTask: Freezable + Reflect {
616    type Input<'m>: CuMsgPack;
617    /// Resources required by the task.
618    type Resources<'r>;
619
620    /// Registers the reflected type used as this task's debug-state contract.
621    ///
622    /// The default exposes the task struct itself. Override this when the task
623    /// contains ignored, third-party, hardware, or otherwise non-inspectable
624    /// internals and should expose a purpose-built debug-state view instead.
625    fn register_debug_state_types(registry: &mut TypeRegistry)
626    where
627        Self: GetTypeRegistration + Sized,
628    {
629        registry.register::<Self>();
630    }
631
632    /// Returns the reflected type path used as this task's debug-state schema.
633    fn debug_state_type_path() -> &'static str
634    where
635        Self: TypePath + Sized,
636    {
637        Self::type_path()
638    }
639
640    /// Borrows this task's current debug-state view.
641    ///
642    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
643    /// when the debug state is a projected view rather than the task struct.
644    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
645    where
646        Self: Sized,
647    {
648        f(self)
649    }
650
651    /// Here you need to initialize everything your task will need for the duration of its lifetime.
652    /// The config allows you to access the configuration of the task.
653    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
654    where
655        Self: Sized;
656
657    /// Start is called between the creation of the task and the first call to pre/process.
658    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
659        Ok(())
660    }
661
662    /// This is a method called by the runtime before "process". This is a kind of best effort,
663    /// as soon as possible call to give a chance for the task to do some work before to prepare
664    /// to make "process" as short as possible.
665    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
666        Ok(())
667    }
668
669    /// Process is the most critical execution of the task.
670    /// The goal will be to produce the output message as soon as possible.
671    /// Use preprocess to prepare the task to make this method as short as possible.
672    fn process<'i>(&mut self, _ctx: &CuContext, input: &Self::Input<'i>) -> CuResult<()>;
673
674    /// This is a method called by the runtime after "process". It is best effort a chance for
675    /// the task to update some state after process is out of the way.
676    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
677    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
678        Ok(())
679    }
680
681    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
682    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
683        Ok(())
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use bincode::{config, decode_from_slice, encode_to_vec};
691
692    #[test]
693    fn test_cucompactstr_encode_decode() {
694        let cstr = CuCompactString(CompactString::from("hello"));
695        let config = config::standard();
696        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
697        let (decoded, _): (CuCompactString, usize) =
698            decode_from_slice(&encoded, config).expect("Decoding failed");
699        assert_eq!(cstr.0, decoded.0);
700    }
701
702    /// Test wrapper proving that a composite payload can forward `payload_should_log`
703    /// to an inner [`CuHandle`] via an inherent method — exactly the pattern real
704    /// composite payloads like `CuImage` will use.
705    ///
706    /// Gated on the default (non-bevy_reflect) feature configuration where
707    /// `Reflect` is auto-impl'd for any `'static`, so the wrapper satisfies
708    /// `CuMsgPayload` without needing `#[derive(Reflect)]`.
709    #[cfg(not(feature = "reflect"))]
710    #[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize)]
711    struct TestHandlePayload {
712        handle: crate::pool::CuHandle<Vec<u8>>,
713    }
714
715    #[cfg(not(feature = "reflect"))]
716    impl Default for TestHandlePayload {
717        fn default() -> Self {
718            Self {
719                handle: crate::pool::CuHandle::new_detached(Vec::new()),
720            }
721        }
722    }
723
724    #[cfg(not(feature = "reflect"))]
725    impl TestHandlePayload {
726        // Inherent specialization arm: real composite payloads (e.g. CuImage) provide
727        // an identical method that forwards to their inner CuHandle.
728        fn payload_should_log(&self) -> bool {
729            self.handle.payload_should_log()
730        }
731    }
732
733    /// Encoding a CuMsg whose payload wraps a CuHandle in `TouchedOnly` mode must:
734    /// * skip the payload bytes when no consumer marked the handle touched, and
735    /// * include them once `mark_touched` was called.
736    /// The wire shape stays compatible with the existing `Option<T>` decode path
737    /// (presence tag is 0u8 for skip, 1u8 + payload otherwise).
738    #[cfg(not(feature = "reflect"))]
739    #[test]
740    fn test_encode_skips_payload_for_untouched_handle() {
741        use crate::pool::{CuHandle, HandleContent};
742        let cfg = config::standard();
743
744        let untouched = TestHandlePayload {
745            handle: CuHandle::new_detached_with_mode(
746                vec![0xAA, 0xBB, 0xCC, 0xDD],
747                HandleContent::TouchedOnly,
748            ),
749        };
750        let msg_skip: CuMsg<TestHandlePayload> = CuMsg::new(Some(untouched));
751        let skip_bytes = encode_to_vec(&msg_skip, cfg).expect("encode");
752
753        let touched_payload = TestHandlePayload {
754            handle: CuHandle::new_detached_with_mode(
755                vec![0xAA, 0xBB, 0xCC, 0xDD],
756                HandleContent::TouchedOnly,
757            ),
758        };
759        touched_payload.handle.mark_touched();
760        let msg_keep: CuMsg<TestHandlePayload> = CuMsg::new(Some(touched_payload));
761        let keep_bytes = encode_to_vec(&msg_keep, cfg).expect("encode");
762
763        assert_eq!(
764            skip_bytes[0], 0u8,
765            "first byte must be the no-payload presence tag for an untouched TouchedOnly handle"
766        );
767        assert_eq!(
768            keep_bytes[0], 1u8,
769            "first byte must be the payload-present tag once the handle was touched"
770        );
771        assert!(
772            keep_bytes.len() > skip_bytes.len(),
773            "touched encoding ({} bytes) must include payload content; skip is {} bytes",
774            keep_bytes.len(),
775            skip_bytes.len()
776        );
777    }
778
779    /// `HandleContent::All` (the default for every existing source) must never drop
780    /// payload bytes — regardless of whether the handle was touched.
781    #[cfg(not(feature = "reflect"))]
782    #[test]
783    fn test_encode_keeps_payload_for_default_mode() {
784        use crate::pool::{CuHandle, HandleContent};
785        let cfg = config::standard();
786
787        let payload = TestHandlePayload {
788            handle: CuHandle::new_detached_with_mode(vec![1, 2, 3], HandleContent::All),
789        };
790        let msg: CuMsg<TestHandlePayload> = CuMsg::new(Some(payload));
791        let bytes = encode_to_vec(&msg, cfg).expect("encode");
792        assert_eq!(
793            bytes[0], 1u8,
794            "default (HandleContent::All) must keep emitting the payload"
795        );
796    }
797}