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//! - **`CuSimSrcTaskPack<O>`**: The corresponding placeholder for multi-output sources.
97//!
98//! - **`CuSimSinkTask<T>`**: A placeholder for a sink task that simulates sending data to hardware. It serves as a
99//!   mock for hardware actuators or output devices during simulations.
100//!
101//! ## Controlling Simulation Flow: `SimOverride` Enum
102//!
103//! The `SimOverride` enum is used to control how the simulator should proceed at each step. This allows
104//! for fine-grained control of task behavior in the simulation context:
105//!
106//! - **`ExecutedBySim`**: Indicates that the simulator has handled the task logic, and the real implementation
107//!   should be skipped.
108//! - **`ExecuteByRuntime`**: Indicates that the real implementation should proceed as normal.
109//!
110//! ## Recorded Replay Helpers
111//!
112//! Simulation-enabled generated runtimes expose two recorded replay callbacks:
113//!
114//! - `recorded_replay_step` is exact-output replay. It copies recorded outputs
115//!   from a CopperList and skips the runtime implementation for deterministic
116//!   log reproduction.
117//! - `recorded_debug_replay_step` is debugger state replay. It injects recorded
118//!   external inputs, suppresses external side effects, and lets regular Copper
119//!   tasks execute so restored keyframe state advances to the inspected CL.
120//!
121
122use crate::config::ComponentConfig;
123use crate::context::CuContext;
124use crate::copperlist::CopperList;
125use crate::cubridge::{
126    BridgeChannel, BridgeChannelConfig, BridgeChannelInfo, BridgeChannelSet, CuBridge,
127};
128use crate::cutask::CuMsgPack;
129
130use crate::cutask::{CuMsg, CuMsgPayload, CuSinkTask, CuSrcTask, Freezable};
131use crate::reflect::{Reflect, TypePath};
132use crate::{input_msg, output_msg};
133use bincode::de::Decoder;
134use bincode::enc::Encoder;
135use bincode::error::{DecodeError, EncodeError};
136use bincode::{Decode, Encode};
137use core::marker::PhantomData;
138use cu29_clock::CuTime;
139use cu29_traits::{CopperListTuple, CuResult, ErasedCuStampedDataSet};
140
141/// Returns the earliest recorded `process_time.start` found in a CopperList.
142///
143/// This is the default timestamp used by exact-output replay when no matching
144/// recorded keyframe is being injected for the current CL.
145pub fn recorded_copperlist_timestamp<P: CopperListTuple>(
146    copperlist: &CopperList<P>,
147) -> Option<CuTime> {
148    <CopperList<P> as ErasedCuStampedDataSet>::cumsgs(copperlist)
149        .into_iter()
150        .filter_map(|msg| Option::<CuTime>::from(msg.metadata().process_time().start))
151        .min()
152}
153
154/// This is the state that will be passed to the simulation support to hook
155/// into the lifecycle of the tasks.
156pub enum CuTaskCallbackState<I, O> {
157    /// Callbacked when a task is created.
158    /// It gives you the opportunity to adapt the sim to the given config.
159    New(Option<ComponentConfig>),
160    /// Callbacked when a task is started.
161    Start,
162    /// Callbacked when a task is getting called on pre-process.
163    Preprocess,
164    /// Callbacked when a task is getting called on process.
165    /// I and O are the input and output messages of the task.
166    /// if this is a source task, I will be CuMsg<()>
167    /// if this is a sink task, O will be CuMsg<()>
168    Process(I, O),
169    /// Callbacked when a task is getting called on post-process.
170    Postprocess,
171    /// Callbacked when a task is stopped.
172    Stop,
173}
174
175/// This is the answer the simulator can give to control the simulation flow.
176#[derive(PartialEq)]
177pub enum SimOverride {
178    /// The callback took care of the logic on the simulation side and the actual
179    /// implementation needs to be skipped.
180    ExecutedBySim,
181    /// The actual implementation needs to be executed.
182    ExecuteByRuntime,
183    /// Emulated the behavior of an erroring task (same as return Err(..) in the normal tasks methods).
184    Errored(String),
185}
186
187/// Lifecycle callbacks for bridges when running in simulation.
188///
189/// These mirror the CuBridge trait hooks so a simulator can choose to
190/// bypass the real implementation (e.g. to avoid opening hardware) or
191/// inject faults.
192pub enum CuBridgeLifecycleState {
193    /// The bridge is about to be constructed. Gives access to config.
194    New(Option<ComponentConfig>),
195    /// The bridge is starting.
196    Start,
197    /// Called before the I/O cycle.
198    Preprocess,
199    /// Called after the I/O cycle.
200    Postprocess,
201    /// The bridge is stopping.
202    Stop,
203}
204
205/// This is a placeholder task for a source task for the simulations.
206/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
207#[derive(Reflect)]
208#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
209pub struct CuSimSrcTask<T> {
210    #[reflect(ignore)]
211    boo: PhantomData<fn() -> T>,
212    state: bool,
213}
214
215impl<T: 'static> TypePath for CuSimSrcTask<T> {
216    fn type_path() -> &'static str {
217        "cu29_runtime::simulation::CuSimSrcTask"
218    }
219
220    fn short_type_path() -> &'static str {
221        "CuSimSrcTask"
222    }
223
224    fn type_ident() -> Option<&'static str> {
225        Some("CuSimSrcTask")
226    }
227
228    fn crate_name() -> Option<&'static str> {
229        Some("cu29_runtime")
230    }
231
232    fn module_path() -> Option<&'static str> {
233        Some("simulation")
234    }
235}
236
237impl<T> Freezable for CuSimSrcTask<T> {
238    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
239        Encode::encode(&self.state, encoder)
240    }
241
242    fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
243        self.state = Decode::decode(decoder)?;
244        Ok(())
245    }
246}
247
248impl<T: CuMsgPayload + 'static> CuSrcTask for CuSimSrcTask<T> {
249    type Resources<'r> = ();
250    type Output<'m> = output_msg!(T);
251
252    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
253    where
254        Self: Sized,
255    {
256        // Default to true to mirror typical source initial state; deterministic across runs.
257        Ok(Self {
258            boo: PhantomData,
259            state: true,
260        })
261    }
262
263    fn process(&mut self, _ctx: &CuContext, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
264        unimplemented!(
265            "A placeholder for sim was called for a source, you need answer SimOverride to ExecutedBySim for the Process step."
266        )
267    }
268}
269
270impl<T> CuSimSrcTask<T> {
271    /// Placeholder hook for simulation-driven sources.
272    ///
273    /// In the sim placeholder we don't advance any internal state because the
274    /// simulator is responsible for providing deterministic outputs and state
275    /// snapshots are carried by the real task (when run_in_sim = true).
276    /// Keeping this as a no-op avoids baking any fake behavior into keyframes.
277    pub fn sim_tick(&mut self) {}
278}
279
280/// Simulation placeholder preserving the complete output tuple of a multi-output source.
281#[derive(Reflect)]
282#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
283pub struct CuSimSrcTaskPack<O> {
284    #[reflect(ignore)]
285    output: PhantomData<fn() -> O>,
286    state: bool,
287}
288
289impl<O: 'static> TypePath for CuSimSrcTaskPack<O> {
290    fn type_path() -> &'static str {
291        "cu29_runtime::simulation::CuSimSrcTaskPack"
292    }
293
294    fn short_type_path() -> &'static str {
295        "CuSimSrcTaskPack"
296    }
297
298    fn type_ident() -> Option<&'static str> {
299        Some("CuSimSrcTaskPack")
300    }
301
302    fn crate_name() -> Option<&'static str> {
303        Some("cu29_runtime")
304    }
305
306    fn module_path() -> Option<&'static str> {
307        Some("simulation")
308    }
309}
310
311impl<O> Freezable for CuSimSrcTaskPack<O> {
312    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
313        Encode::encode(&self.state, encoder)
314    }
315
316    fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
317        self.state = Decode::decode(decoder)?;
318        Ok(())
319    }
320}
321
322impl<O: CuMsgPayload + 'static> CuSrcTask for CuSimSrcTaskPack<O> {
323    type Resources<'r> = ();
324    type Output<'m> = O;
325
326    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
327    where
328        Self: Sized,
329    {
330        Ok(Self {
331            output: PhantomData,
332            state: true,
333        })
334    }
335
336    fn process(&mut self, _ctx: &CuContext, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
337        unimplemented!(
338            "A placeholder for sim was called for a multi-output source, you need answer SimOverride to ExecutedBySim for the Process step."
339        )
340    }
341}
342
343impl<O> CuSimSrcTaskPack<O> {
344    /// Placeholder hook for simulation-driven multi-output sources.
345    pub fn sim_tick(&mut self) {}
346}
347
348/// Helper to map a payload type (or tuple of payload types) to the corresponding `input_msg!` form.
349pub trait CuSimSinkInput {
350    type With<'m>: CuMsgPack
351    where
352        Self: 'm;
353}
354
355macro_rules! impl_sim_sink_input_tuple {
356    ($name:ident) => {
357        impl<$name: CuMsgPayload> CuSimSinkInput for ($name,) {
358            type With<'m> = CuMsg<$name> where Self: 'm;
359        }
360    };
361    ($($name:ident),+) => {
362        impl<$($name: CuMsgPayload),+> CuSimSinkInput for ($($name,)+) {
363            type With<'m> = input_msg!('m, $($name),+) where Self: 'm;
364        }
365    };
366}
367
368macro_rules! impl_sim_sink_input_up_to {
369    ($first:ident $(, $rest:ident)* $(,)?) => {
370        impl_sim_sink_input_tuple!($first);
371        impl_sim_sink_input_up_to!(@accumulate ($first); $($rest),*);
372    };
373    (@accumulate ($($acc:ident),+);) => {};
374    (@accumulate ($($acc:ident),+); $next:ident $(, $rest:ident)*) => {
375        impl_sim_sink_input_tuple!($($acc),+, $next);
376        impl_sim_sink_input_up_to!(@accumulate ($($acc),+, $next); $($rest),*);
377    };
378}
379
380impl_sim_sink_input_up_to!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
381
382/// This is a placeholder task for a sink task for the simulations.
383/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
384#[derive(Reflect)]
385#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
386pub struct CuSimSinkTask<I> {
387    #[reflect(ignore)]
388    boo: PhantomData<fn() -> I>,
389}
390
391impl<I: 'static> TypePath for CuSimSinkTask<I> {
392    fn type_path() -> &'static str {
393        "cu29_runtime::simulation::CuSimSinkTask"
394    }
395
396    fn short_type_path() -> &'static str {
397        "CuSimSinkTask"
398    }
399
400    fn type_ident() -> Option<&'static str> {
401        Some("CuSimSinkTask")
402    }
403
404    fn crate_name() -> Option<&'static str> {
405        Some("cu29_runtime")
406    }
407
408    fn module_path() -> Option<&'static str> {
409        Some("simulation")
410    }
411}
412
413impl<I> Freezable for CuSimSinkTask<I> {}
414
415impl<I: CuSimSinkInput + 'static> CuSinkTask for CuSimSinkTask<I> {
416    type Resources<'r> = ();
417    type Input<'m> = <I as CuSimSinkInput>::With<'m>;
418
419    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
420    where
421        Self: Sized,
422    {
423        Ok(Self { boo: PhantomData })
424    }
425
426    fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
427        unimplemented!(
428            "A placeholder for sim was called for a sink, you need answer SimOverride to ExecutedBySim for the Process step."
429        )
430    }
431}
432
433/// Empty channel-id enum used when a simulated bridge has no channel on one side.
434#[derive(Copy, Clone, Debug, Eq, PartialEq)]
435pub enum CuNoBridgeChannelId {}
436
437/// Empty channel set used when a simulated bridge has no channel on one side.
438pub struct CuNoBridgeChannels;
439
440impl BridgeChannelSet for CuNoBridgeChannels {
441    type Id = CuNoBridgeChannelId;
442
443    const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] = &[];
444}
445
446/// Placeholder bridge used in simulation when a bridge is configured with
447/// `run_in_sim: false`.
448///
449/// This bridge is parameterized directly by the Tx/Rx channel sets generated
450/// from configuration, so the original bridge type does not need to compile in
451/// simulation mode.
452#[derive(Reflect)]
453#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
454pub struct CuSimBridge<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> {
455    #[reflect(ignore)]
456    boo: PhantomData<fn() -> (Tx, Rx)>,
457}
458
459impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> TypePath
460    for CuSimBridge<Tx, Rx>
461{
462    fn type_path() -> &'static str {
463        "cu29_runtime::simulation::CuSimBridge"
464    }
465
466    fn short_type_path() -> &'static str {
467        "CuSimBridge"
468    }
469
470    fn type_ident() -> Option<&'static str> {
471        Some("CuSimBridge")
472    }
473
474    fn crate_name() -> Option<&'static str> {
475        Some("cu29_runtime")
476    }
477
478    fn module_path() -> Option<&'static str> {
479        Some("simulation")
480    }
481}
482
483impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> Freezable
484    for CuSimBridge<Tx, Rx>
485{
486}
487
488impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> CuBridge
489    for CuSimBridge<Tx, Rx>
490{
491    type Tx = Tx;
492    type Rx = Rx;
493    type Resources<'r> = ();
494
495    fn new(
496        _config: Option<&ComponentConfig>,
497        _tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
498        _rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
499        _resources: Self::Resources<'_>,
500    ) -> CuResult<Self>
501    where
502        Self: Sized,
503    {
504        Ok(Self { boo: PhantomData })
505    }
506
507    fn send<'a, Payload>(
508        &mut self,
509        _ctx: &CuContext,
510        _channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
511        _msg: &CuMsg<Payload>,
512    ) -> CuResult<()>
513    where
514        Payload: CuMsgPayload + 'a,
515    {
516        Ok(())
517    }
518
519    fn receive<'a, Payload>(
520        &mut self,
521        _ctx: &CuContext,
522        _channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
523        _msg: &mut CuMsg<Payload>,
524    ) -> CuResult<()>
525    where
526        Payload: CuMsgPayload + 'a,
527    {
528        Ok(())
529    }
530}