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::cutask::CuMsgPack;
112
113use crate::cutask::{CuMsg, CuMsgPayload, CuSinkTask, CuSrcTask, Freezable};
114use crate::{input_msg, output_msg};
115use cu29_clock::RobotClock;
116use cu29_traits::CuResult;
117use std::marker::PhantomData;
118
119/// This is the state that will be passed to the simulation support to hook
120/// into the lifecycle of the tasks.
121pub enum CuTaskCallbackState<I, O> {
122 /// Callbacked when a task is created.
123 /// It gives you the opportunity to adapt the sim to the given config.
124 New(Option<ComponentConfig>),
125 /// Callbacked when a task is started.
126 Start,
127 /// Callbacked when a task is getting called on pre-process.
128 Preprocess,
129 /// Callbacked when a task is getting called on process.
130 /// I and O are the input and output messages of the task.
131 /// if this is a source task, I will be CuMsg<()>
132 /// if this is a sink task, O will be CuMsg<()>
133 Process(I, O),
134 /// Callbacked when a task is getting called on post-process.
135 Postprocess,
136 /// Callbacked when a task is stopped.
137 Stop,
138}
139
140/// This is the answer the simulator can give to control the simulation flow.
141#[derive(PartialEq)]
142pub enum SimOverride {
143 /// The callback took care of the logic on the simulation side and the actual
144 /// implementation needs to be skipped.
145 ExecutedBySim,
146 /// The actual implementation needs to be executed.
147 ExecuteByRuntime,
148 /// Emulated the behavior of an erroring task (same as return Err(..) in the normal tasks methods).
149 Errored(String),
150}
151
152/// This is a placeholder task for a source task for the simulations.
153/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
154pub struct CuSimSrcTask<T> {
155 boo: PhantomData<T>,
156}
157
158impl<T> Freezable for CuSimSrcTask<T> {}
159
160impl<T: CuMsgPayload> CuSrcTask for CuSimSrcTask<T> {
161 type Output<'m> = output_msg!(T);
162
163 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self>
164 where
165 Self: Sized,
166 {
167 Ok(Self { boo: PhantomData })
168 }
169
170 fn process(&mut self, _clock: &RobotClock, _new_msg: &mut Self::Output<'_>) -> CuResult<()> {
171 unimplemented!("A placeholder for sim was called for a source, you need answer SimOverride to ExecutedBySim for the Process step.")
172 }
173}
174
175/// Helper to map a payload type (or tuple of payload types) to the corresponding `input_msg!` form.
176pub trait CuSimSinkInput {
177 type With<'m>: CuMsgPack
178 where
179 Self: 'm;
180}
181
182macro_rules! impl_sim_sink_input_tuple {
183 ($($name:ident),+) => {
184 impl<$($name: CuMsgPayload),+> CuSimSinkInput for ($($name,)+) {
185 type With<'m> = input_msg!('m, $($name),+) where Self: 'm;
186 }
187 };
188}
189
190impl_sim_sink_input_tuple!(T1);
191impl_sim_sink_input_tuple!(T1, T2);
192impl_sim_sink_input_tuple!(T1, T2, T3);
193impl_sim_sink_input_tuple!(T1, T2, T3, T4);
194impl_sim_sink_input_tuple!(T1, T2, T3, T4, T5);
195
196/// This is a placeholder task for a sink task for the simulations.
197/// It basically does nothing in place of a real driver so it won't try to initialize any hardware.
198pub struct CuSimSinkTask<I> {
199 boo: PhantomData<I>,
200}
201
202impl<I> Freezable for CuSimSinkTask<I> {}
203
204impl<I: CuSimSinkInput + 'static> CuSinkTask for CuSimSinkTask<I> {
205 type Input<'m> = <I as CuSimSinkInput>::With<'m>;
206
207 fn new(_config: Option<&ComponentConfig>) -> CuResult<Self>
208 where
209 Self: Sized,
210 {
211 Ok(Self { boo: PhantomData })
212 }
213
214 fn process(&mut self, _clock: &RobotClock, _input: &Self::Input<'_>) -> CuResult<()> {
215 unimplemented!("A placeholder for sim was called for a sink, you need answer SimOverride to ExecutedBySim for the Process step.")
216 }
217}