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 /// Simulation-only observation after the runtime executed a task's process.
170 /// Generated live replay restores sender metadata before downstream tasks run.
171 ProcessCompleted(O),
172 /// Callbacked when a task is getting called on post-process.
173 Postprocess,
174 /// Callbacked when a task is stopped.
175 Stop,
176}
177
178/// This is the answer the simulator can give to control the simulation flow.
179#[derive(PartialEq)]
180pub enum SimOverride {
181 /// The callback took care of the logic on the simulation side and the actual
182 /// implementation needs to be skipped.
183 ExecutedBySim,
184 /// The actual implementation needs to be executed.
185 ExecuteByRuntime,
186 /// Emulated the behavior of an erroring task (same as return Err(..) in the normal tasks methods).
187 Errored(String),
188}
189
190/// Lifecycle callbacks for bridges when running in simulation.
191///
192/// These mirror the CuBridge trait hooks so a simulator can choose to
193/// bypass the real implementation (e.g. to avoid opening hardware) or
194/// inject faults.
195pub enum CuBridgeLifecycleState {
196 /// The bridge is about to be constructed. Gives access to config.
197 New(Option<ComponentConfig>),
198 /// The bridge is starting.
199 Start,
200 /// Called before the I/O cycle.
201 Preprocess,
202 /// Called after the I/O cycle.
203 Postprocess,
204 /// The bridge is stopping.
205 Stop,
206}
207
208/// This is a placeholder task for a source task for the simulations.
209/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
210#[derive(Reflect)]
211#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
212pub struct CuSimSrcTask<T> {
213 #[reflect(ignore)]
214 boo: PhantomData<fn() -> T>,
215 state: bool,
216}
217
218impl<T: 'static> TypePath for CuSimSrcTask<T> {
219 fn type_path() -> &'static str {
220 "cu29_runtime::simulation::CuSimSrcTask"
221 }
222
223 fn short_type_path() -> &'static str {
224 "CuSimSrcTask"
225 }
226
227 fn type_ident() -> Option<&'static str> {
228 Some("CuSimSrcTask")
229 }
230
231 fn crate_name() -> Option<&'static str> {
232 Some("cu29_runtime")
233 }
234
235 fn module_path() -> Option<&'static str> {
236 Some("simulation")
237 }
238}
239
240impl<T> Freezable for CuSimSrcTask<T> {
241 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
242 Encode::encode(&self.state, encoder)
243 }
244
245 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
246 self.state = Decode::decode(decoder)?;
247 Ok(())
248 }
249}
250
251impl<T: CuMsgPayload + 'static> CuSrcTask for CuSimSrcTask<T> {
252 type Resources<'r> = ();
253 type Output<'m> = output_msg!(T);
254
255 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
256 where
257 Self: Sized,
258 {
259 // Default to true to mirror typical source initial state; deterministic across runs.
260 Ok(Self {
261 boo: PhantomData,
262 state: true,
263 })
264 }
265
266 fn process(&mut self, _ctx: &CuContext, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
267 unimplemented!(
268 "A placeholder for sim was called for a source, you need answer SimOverride to ExecutedBySim for the Process step."
269 )
270 }
271}
272
273impl<T> CuSimSrcTask<T> {
274 /// Placeholder hook for simulation-driven sources.
275 ///
276 /// In the sim placeholder we don't advance any internal state because the
277 /// simulator is responsible for providing deterministic outputs and state
278 /// snapshots are carried by the real task (when run_in_sim = true).
279 /// Keeping this as a no-op avoids baking any fake behavior into keyframes.
280 pub fn sim_tick(&mut self) {}
281}
282
283/// Simulation placeholder preserving the complete output tuple of a multi-output source.
284#[derive(Reflect)]
285#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
286pub struct CuSimSrcTaskPack<O> {
287 #[reflect(ignore)]
288 output: PhantomData<fn() -> O>,
289 state: bool,
290}
291
292impl<O: 'static> TypePath for CuSimSrcTaskPack<O> {
293 fn type_path() -> &'static str {
294 "cu29_runtime::simulation::CuSimSrcTaskPack"
295 }
296
297 fn short_type_path() -> &'static str {
298 "CuSimSrcTaskPack"
299 }
300
301 fn type_ident() -> Option<&'static str> {
302 Some("CuSimSrcTaskPack")
303 }
304
305 fn crate_name() -> Option<&'static str> {
306 Some("cu29_runtime")
307 }
308
309 fn module_path() -> Option<&'static str> {
310 Some("simulation")
311 }
312}
313
314impl<O> Freezable for CuSimSrcTaskPack<O> {
315 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
316 Encode::encode(&self.state, encoder)
317 }
318
319 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
320 self.state = Decode::decode(decoder)?;
321 Ok(())
322 }
323}
324
325impl<O: CuMsgPayload + 'static> CuSrcTask for CuSimSrcTaskPack<O> {
326 type Resources<'r> = ();
327 type Output<'m> = O;
328
329 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
330 where
331 Self: Sized,
332 {
333 Ok(Self {
334 output: PhantomData,
335 state: true,
336 })
337 }
338
339 fn process(&mut self, _ctx: &CuContext, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
340 unimplemented!(
341 "A placeholder for sim was called for a multi-output source, you need answer SimOverride to ExecutedBySim for the Process step."
342 )
343 }
344}
345
346impl<O> CuSimSrcTaskPack<O> {
347 /// Placeholder hook for simulation-driven multi-output sources.
348 pub fn sim_tick(&mut self) {}
349}
350
351/// Helper to map a payload type (or tuple of payload types) to the corresponding `input_msg!` form.
352pub trait CuSimSinkInput {
353 type With<'m>: CuMsgPack
354 where
355 Self: 'm;
356}
357
358macro_rules! impl_sim_sink_input_tuple {
359 ($name:ident) => {
360 impl<$name: CuMsgPayload> CuSimSinkInput for ($name,) {
361 type With<'m> = CuMsg<$name> where Self: 'm;
362 }
363 };
364 ($($name:ident),+) => {
365 impl<$($name: CuMsgPayload),+> CuSimSinkInput for ($($name,)+) {
366 type With<'m> = input_msg!('m, $($name),+) where Self: 'm;
367 }
368 };
369}
370
371macro_rules! impl_sim_sink_input_up_to {
372 ($first:ident $(, $rest:ident)* $(,)?) => {
373 impl_sim_sink_input_tuple!($first);
374 impl_sim_sink_input_up_to!(@accumulate ($first); $($rest),*);
375 };
376 (@accumulate ($($acc:ident),+);) => {};
377 (@accumulate ($($acc:ident),+); $next:ident $(, $rest:ident)*) => {
378 impl_sim_sink_input_tuple!($($acc),+, $next);
379 impl_sim_sink_input_up_to!(@accumulate ($($acc),+, $next); $($rest),*);
380 };
381}
382
383impl_sim_sink_input_up_to!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
384
385/// This is a placeholder task for a sink task for the simulations.
386/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
387#[derive(Reflect)]
388#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
389pub struct CuSimSinkTask<I> {
390 #[reflect(ignore)]
391 boo: PhantomData<fn() -> I>,
392}
393
394impl<I: 'static> TypePath for CuSimSinkTask<I> {
395 fn type_path() -> &'static str {
396 "cu29_runtime::simulation::CuSimSinkTask"
397 }
398
399 fn short_type_path() -> &'static str {
400 "CuSimSinkTask"
401 }
402
403 fn type_ident() -> Option<&'static str> {
404 Some("CuSimSinkTask")
405 }
406
407 fn crate_name() -> Option<&'static str> {
408 Some("cu29_runtime")
409 }
410
411 fn module_path() -> Option<&'static str> {
412 Some("simulation")
413 }
414}
415
416impl<I> Freezable for CuSimSinkTask<I> {}
417
418impl<I: CuSimSinkInput + 'static> CuSinkTask for CuSimSinkTask<I> {
419 type Resources<'r> = ();
420 type Input<'m> = <I as CuSimSinkInput>::With<'m>;
421
422 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
423 where
424 Self: Sized,
425 {
426 Ok(Self { boo: PhantomData })
427 }
428
429 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
430 unimplemented!(
431 "A placeholder for sim was called for a sink, you need answer SimOverride to ExecutedBySim for the Process step."
432 )
433 }
434}
435
436/// Empty channel-id enum used when a simulated bridge has no channel on one side.
437#[derive(Copy, Clone, Debug, Eq, PartialEq)]
438pub enum CuNoBridgeChannelId {}
439
440/// Empty channel set used when a simulated bridge has no channel on one side.
441pub struct CuNoBridgeChannels;
442
443impl BridgeChannelSet for CuNoBridgeChannels {
444 type Id = CuNoBridgeChannelId;
445
446 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] = &[];
447}
448
449/// Placeholder bridge used in simulation when a bridge is configured with
450/// `run_in_sim: false`.
451///
452/// This bridge is parameterized directly by the Tx/Rx channel sets generated
453/// from configuration, so the original bridge type does not need to compile in
454/// simulation mode.
455#[derive(Reflect)]
456#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
457pub struct CuSimBridge<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> {
458 #[reflect(ignore)]
459 boo: PhantomData<fn() -> (Tx, Rx)>,
460}
461
462impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> TypePath
463 for CuSimBridge<Tx, Rx>
464{
465 fn type_path() -> &'static str {
466 "cu29_runtime::simulation::CuSimBridge"
467 }
468
469 fn short_type_path() -> &'static str {
470 "CuSimBridge"
471 }
472
473 fn type_ident() -> Option<&'static str> {
474 Some("CuSimBridge")
475 }
476
477 fn crate_name() -> Option<&'static str> {
478 Some("cu29_runtime")
479 }
480
481 fn module_path() -> Option<&'static str> {
482 Some("simulation")
483 }
484}
485
486impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> Freezable
487 for CuSimBridge<Tx, Rx>
488{
489}
490
491impl<Tx: BridgeChannelSet + 'static, Rx: BridgeChannelSet + 'static> CuBridge
492 for CuSimBridge<Tx, Rx>
493{
494 type Tx = Tx;
495 type Rx = Rx;
496 type Resources<'r> = ();
497
498 fn new(
499 _config: Option<&ComponentConfig>,
500 _tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
501 _rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
502 _resources: Self::Resources<'_>,
503 ) -> CuResult<Self>
504 where
505 Self: Sized,
506 {
507 Ok(Self { boo: PhantomData })
508 }
509
510 fn send<'a, Payload>(
511 &mut self,
512 _ctx: &CuContext,
513 _channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
514 _msg: &CuMsg<Payload>,
515 ) -> CuResult<()>
516 where
517 Payload: CuMsgPayload + 'a,
518 {
519 Ok(())
520 }
521
522 fn receive<'a, Payload>(
523 &mut self,
524 _ctx: &CuContext,
525 _channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
526 _msg: &mut CuMsg<Payload>,
527 ) -> CuResult<()>
528 where
529 Payload: CuMsgPayload + 'a,
530 {
531 Ok(())
532 }
533}