Skip to main content

cu29_runtime/
simulation.rs

1//! # `cu29::simulation` Module
2//!
3//! The `cu29::simulation` module provides an interface to simulate tasks in Copper-based systems.
4//! It offers structures, traits, and enums that enable hooking into the lifecycle of tasks, adapting
5//! their behavior, and integrating them with simulated hardware environments.
6//!
7//! ## Overview
8//!
9//! This module is specifically designed to manage the lifecycle of tasks during simulation, allowing
10//! users to override specific simulation steps and simulate sensor data or hardware interaction using
11//! placeholders for real drivers. It includes the following components:
12//!
13//! - **`CuTaskCallbackState`**: Represents the lifecycle states of tasks during simulation.
14//! - **`SimOverride`**: Defines how the simulator should handle specific task callbacks, either
15//!   executing the logic in the simulator or deferring to the real implementation.
16//!
17//! ## Hooking Simulation Events
18//!
19//! You can control and simulate task behavior using a callback mechanism. A task in the Copper framework
20//! has a lifecycle, and for each stage of the lifecycle, a corresponding callback state is passed to
21//! the simulation. This allows you to inject custom logic for each task stage.
22//!
23//! ### `CuTaskCallbackState` Enum
24//!
25//! The `CuTaskCallbackState` enum represents different stages in the lifecycle of a Copper task during a simulation:
26//!
27//! - **`New(Option<ComponentConfig>)`**: Triggered when a task is created. Use this state to adapt the simulation
28//!   to a specific component configuration if needed.
29//! - **`Start`**: Triggered when a task starts. This state allows you to initialize or set up any necessary data
30//!   before the task processes any input.
31//! - **`Preprocess`**: Called before the main processing step. Useful for preparing or validating data.
32//! - **`Process(I, O)`**: The core processing state, where you can handle the input (`I`) and output (`O`) of
33//!   the task. For source tasks, `I` is `CuMsg<()>`, and for sink tasks, `O` is `CuMsg<()>`.
34//! - **`Postprocess`**: Called after the main processing step. Allows for cleanup or final adjustments.
35//! - **`Stop`**: Triggered when a task is stopped. Use this to finalize any data or state before task termination.
36//!
37//! ### Example Usage: Callback
38//!
39//! You can combine the expressiveness of the enum matching to intercept and override the task lifecycle for the simulation.
40//!
41//! ```rust,ignore
42//! let mut sim_callback = move |step: SimStep<'_>| -> SimOverride {
43//!     match step {
44//!         // Handle the creation of source tasks, potentially adapting the simulation based on configuration
45//!         SimStep::SourceTask(CuTaskCallbackState::New(Some(config))) => {
46//!             println!("Creating Source Task with configuration: {:?}", config);
47//!             // You can adapt the simulation using the configuration here
48//!             SimOverride::ExecuteByRuntime
49//!         }
50//!         SimStep::SourceTask(CuTaskCallbackState::New(None)) => {
51//!             println!("Creating Source Task without configuration.");
52//!             SimOverride::ExecuteByRuntime
53//!         }
54//!         // Handle the processing step for sink tasks, simulating the response
55//!         SimStep::SinkTask(CuTaskCallbackState::Process(input, output)) => {
56//!             println!("Processing Sink Task...");
57//!             println!("Received input: {:?}", input);
58//!
59//!             // Simulate a response by setting the output payload
60//!             output.set_payload(your_simulated_response());
61//!             println!("Set simulated output for Sink Task.");
62//!
63//!             SimOverride::ExecutedBySim
64//!         }
65//!         // Generic handling for other phases like Start, Preprocess, Postprocess, or Stop
66//!         SimStep::SourceTask(CuTaskCallbackState::Start)
67//!         | SimStep::SinkTask(CuTaskCallbackState::Start) => {
68//!             println!("Task started.");
69//!             SimOverride::ExecuteByRuntime
70//!         }
71//!         SimStep::SourceTask(CuTaskCallbackState::Stop)
72//!         | SimStep::SinkTask(CuTaskCallbackState::Stop) => {
73//!             println!("Task stopped.");
74//!             SimOverride::ExecuteByRuntime
75//!         }
76//!         // Default fallback for any unhandled cases
77//!         _ => {
78//!             println!("Unhandled simulation step: {:?}", step);
79//!             SimOverride::ExecuteByRuntime
80//!         }
81//!     }
82//! };
83//! ```
84//!
85//! In this example, `example_callback` is a function that matches against the current step in the simulation and
86//! determines if the simulation should handle it (`SimOverride::ExecutedBySim`) or defer to the runtime's real
87//! implementation (`SimOverride::ExecuteByRuntime`).
88//!
89//! ## Task Simulation with `CuSimSrcTask` and `CuSimSinkTask`
90//!
91//! The module provides placeholder tasks for source and sink tasks, which do not interact with real hardware but
92//! instead simulate the presence of it.
93//!
94//! - **`CuSimSrcTask<T>`**: A placeholder for a source task that simulates a sensor or data acquisition hardware.
95//!   This task provides the ability to simulate incoming data without requiring actual hardware initialization.
96//!
97//! - **`CuSimSinkTask<T>`**: A placeholder for a sink task that simulates sending data to hardware. It serves as a
98//!   mock for hardware actuators or output devices during simulations.
99//!
100//! ## Controlling Simulation Flow: `SimOverride` Enum
101//!
102//! The `SimOverride` enum is used to control how the simulator should proceed at each step. This allows
103//! for fine-grained control of task behavior in the simulation context:
104//!
105//! - **`ExecutedBySim`**: Indicates that the simulator has handled the task logic, and the real implementation
106//!   should be skipped.
107//! - **`ExecuteByRuntime`**: Indicates that the real implementation should proceed as normal.
108//!
109
110use crate::config::ComponentConfig;
111use crate::context::CuContext;
112use crate::cubridge::{
113    BridgeChannel, BridgeChannelConfig, BridgeChannelInfo, BridgeChannelSet, CuBridge,
114};
115use crate::cutask::CuMsgPack;
116
117use crate::cutask::{CuMsg, CuMsgPayload, CuSinkTask, CuSrcTask, Freezable};
118use crate::reflect::{Reflect, TypePath};
119use crate::{input_msg, output_msg};
120use bincode::de::Decoder;
121use bincode::enc::Encoder;
122use bincode::error::{DecodeError, EncodeError};
123use bincode::{Decode, Encode};
124use core::marker::PhantomData;
125use cu29_traits::CuResult;
126
127/// This is the state that will be passed to the simulation support to hook
128/// into the lifecycle of the tasks.
129pub enum CuTaskCallbackState<I, O> {
130    /// Callbacked when a task is created.
131    /// It gives you the opportunity to adapt the sim to the given config.
132    New(Option<ComponentConfig>),
133    /// Callbacked when a task is started.
134    Start,
135    /// Callbacked when a task is getting called on pre-process.
136    Preprocess,
137    /// Callbacked when a task is getting called on process.
138    /// I and O are the input and output messages of the task.
139    /// if this is a source task, I will be CuMsg<()>
140    /// if this is a sink task, O will be CuMsg<()>
141    Process(I, O),
142    /// Callbacked when a task is getting called on post-process.
143    Postprocess,
144    /// Callbacked when a task is stopped.
145    Stop,
146}
147
148/// This is the answer the simulator can give to control the simulation flow.
149#[derive(PartialEq)]
150pub enum SimOverride {
151    /// The callback took care of the logic on the simulation side and the actual
152    /// implementation needs to be skipped.
153    ExecutedBySim,
154    /// The actual implementation needs to be executed.
155    ExecuteByRuntime,
156    /// Emulated the behavior of an erroring task (same as return Err(..) in the normal tasks methods).
157    Errored(String),
158}
159
160/// Lifecycle callbacks for bridges when running in simulation.
161///
162/// These mirror the CuBridge trait hooks so a simulator can choose to
163/// bypass the real implementation (e.g. to avoid opening hardware) or
164/// inject faults.
165pub enum CuBridgeLifecycleState {
166    /// The bridge is about to be constructed. Gives access to config.
167    New(Option<ComponentConfig>),
168    /// The bridge is starting.
169    Start,
170    /// Called before the I/O cycle.
171    Preprocess,
172    /// Called after the I/O cycle.
173    Postprocess,
174    /// The bridge is stopping.
175    Stop,
176}
177
178/// This is a placeholder task for a source task for the simulations.
179/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
180#[derive(Reflect)]
181#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
182pub struct CuSimSrcTask<T> {
183    #[reflect(ignore)]
184    boo: PhantomData<fn() -> T>,
185    state: bool,
186}
187
188impl<T: 'static> TypePath for CuSimSrcTask<T> {
189    fn type_path() -> &'static str {
190        "cu29_runtime::simulation::CuSimSrcTask"
191    }
192
193    fn short_type_path() -> &'static str {
194        "CuSimSrcTask"
195    }
196
197    fn type_ident() -> Option<&'static str> {
198        Some("CuSimSrcTask")
199    }
200
201    fn crate_name() -> Option<&'static str> {
202        Some("cu29_runtime")
203    }
204
205    fn module_path() -> Option<&'static str> {
206        Some("simulation")
207    }
208}
209
210impl<T> Freezable for CuSimSrcTask<T> {
211    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
212        Encode::encode(&self.state, encoder)
213    }
214
215    fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
216        self.state = Decode::decode(decoder)?;
217        Ok(())
218    }
219}
220
221impl<T: CuMsgPayload + 'static> CuSrcTask for CuSimSrcTask<T> {
222    type Resources<'r> = ();
223    type Output<'m> = output_msg!(T);
224
225    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
226    where
227        Self: Sized,
228    {
229        // Default to true to mirror typical source initial state; deterministic across runs.
230        Ok(Self {
231            boo: PhantomData,
232            state: true,
233        })
234    }
235
236    fn process(&mut self, _ctx: &CuContext, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
237        unimplemented!(
238            "A placeholder for sim was called for a source, you need answer SimOverride to ExecutedBySim for the Process step."
239        )
240    }
241}
242
243impl<T> CuSimSrcTask<T> {
244    /// Placeholder hook for simulation-driven sources.
245    ///
246    /// In the sim placeholder we don't advance any internal state because the
247    /// simulator is responsible for providing deterministic outputs and state
248    /// snapshots are carried by the real task (when run_in_sim = true).
249    /// Keeping this as a no-op avoids baking any fake behavior into keyframes.
250    pub fn sim_tick(&mut self) {}
251}
252
253/// Helper to map a payload type (or tuple of payload types) to the corresponding `input_msg!` form.
254pub trait CuSimSinkInput {
255    type With<'m>: CuMsgPack
256    where
257        Self: 'm;
258}
259
260macro_rules! impl_sim_sink_input_tuple {
261    ($($name:ident),+) => {
262        impl<$($name: CuMsgPayload),+> CuSimSinkInput for ($($name,)+) {
263            type With<'m> = input_msg!('m, $($name),+) where Self: 'm;
264        }
265    };
266}
267
268macro_rules! impl_sim_sink_input_up_to {
269    ($first:ident $(, $rest:ident)* $(,)?) => {
270        impl_sim_sink_input_tuple!($first);
271        impl_sim_sink_input_up_to!(@accumulate ($first); $($rest),*);
272    };
273    (@accumulate ($($acc:ident),+);) => {};
274    (@accumulate ($($acc:ident),+); $next:ident $(, $rest:ident)*) => {
275        impl_sim_sink_input_tuple!($($acc),+, $next);
276        impl_sim_sink_input_up_to!(@accumulate ($($acc),+, $next); $($rest),*);
277    };
278}
279
280impl_sim_sink_input_up_to!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
281
282/// This is a placeholder task for a sink task for the simulations.
283/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
284#[derive(Reflect)]
285#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
286pub struct CuSimSinkTask<I> {
287    #[reflect(ignore)]
288    boo: PhantomData<fn() -> I>,
289}
290
291impl<I: 'static> TypePath for CuSimSinkTask<I> {
292    fn type_path() -> &'static str {
293        "cu29_runtime::simulation::CuSimSinkTask"
294    }
295
296    fn short_type_path() -> &'static str {
297        "CuSimSinkTask"
298    }
299
300    fn type_ident() -> Option<&'static str> {
301        Some("CuSimSinkTask")
302    }
303
304    fn crate_name() -> Option<&'static str> {
305        Some("cu29_runtime")
306    }
307
308    fn module_path() -> Option<&'static str> {
309        Some("simulation")
310    }
311}
312
313impl<I> Freezable for CuSimSinkTask<I> {}
314
315impl<I: CuSimSinkInput + 'static> CuSinkTask for CuSimSinkTask<I> {
316    type Resources<'r> = ();
317    type Input<'m> = <I as CuSimSinkInput>::With<'m>;
318
319    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
320    where
321        Self: Sized,
322    {
323        Ok(Self { boo: PhantomData })
324    }
325
326    fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
327        unimplemented!(
328            "A placeholder for sim was called for a sink, you need answer SimOverride to ExecutedBySim for the Process step."
329        )
330    }
331}
332
333/// Empty channel-id enum used when a simulated bridge has no channel on one side.
334#[derive(Copy, Clone, Debug, Eq, PartialEq)]
335pub enum CuNoBridgeChannelId {}
336
337/// Empty channel set used when a simulated bridge has no channel on one side.
338pub struct CuNoBridgeChannels;
339
340impl BridgeChannelSet for CuNoBridgeChannels {
341    type Id = CuNoBridgeChannelId;
342
343    const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] = &[];
344}
345
346/// Placeholder bridge used in simulation when a bridge is configured with
347/// `run_in_sim: false`.
348///
349/// This bridge is parameterized directly by the Tx/Rx channel sets generated
350/// from configuration, so the original bridge type does not need to compile in
351/// simulation mode.
352#[derive(Reflect)]
353#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
354pub struct CuSimBridge<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> {
355    #[reflect(ignore)]
356    boo: PhantomData<fn() -> (Tx, Rx)>,
357}
358
359impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> TypePath
360    for CuSimBridge<Tx, Rx>
361{
362    fn type_path() -> &'static str {
363        "cu29_runtime::simulation::CuSimBridge"
364    }
365
366    fn short_type_path() -> &'static str {
367        "CuSimBridge"
368    }
369
370    fn type_ident() -> Option<&'static str> {
371        Some("CuSimBridge")
372    }
373
374    fn crate_name() -> Option<&'static str> {
375        Some("cu29_runtime")
376    }
377
378    fn module_path() -> Option<&'static str> {
379        Some("simulation")
380    }
381}
382
383impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> Freezable
384    for CuSimBridge<Tx, Rx>
385{
386}
387
388impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> CuBridge
389    for CuSimBridge<Tx, Rx>
390{
391    type Tx = Tx;
392    type Rx = Rx;
393    type Resources<'r> = ();
394
395    fn new(
396        _config: Option<&ComponentConfig>,
397        _tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
398        _rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
399        _resources: Self::Resources<'_>,
400    ) -> CuResult<Self>
401    where
402        Self: Sized,
403    {
404        Ok(Self { boo: PhantomData })
405    }
406
407    fn send<'a, Payload>(
408        &mut self,
409        _ctx: &CuContext,
410        _channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
411        _msg: &CuMsg<Payload>,
412    ) -> CuResult<()>
413    where
414        Payload: CuMsgPayload + 'a,
415    {
416        Ok(())
417    }
418
419    fn receive<'a, Payload>(
420        &mut self,
421        _ctx: &CuContext,
422        _channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
423        _msg: &mut CuMsg<Payload>,
424    ) -> CuResult<()>
425    where
426        Payload: CuMsgPayload + 'a,
427    {
428        Ok(())
429    }
430}