forked from ROMEO/nexosim
Rename crate to NeXosim
This commit is contained in:
186
nexosim/examples/assembly.rs
Normal file
186
nexosim/examples/assembly.rs
Normal file
@ -0,0 +1,186 @@
|
||||
//! Example: an assembly consisting of a current-controlled stepper motor and
|
||||
//! its driver.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * model prototypes,
|
||||
//! * submodels,
|
||||
//! * self-scheduling methods,
|
||||
//! * model initialization,
|
||||
//! * simulation monitoring with buffered event sinks.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────────────────────────────────────────┐
|
||||
//! │ Assembly │
|
||||
//! │ ┌──────────┐ │
|
||||
//! PPS │ │ │ coil currents ┌─────────┐ │
|
||||
//! Pulse rate ●──────────┼──►│ Driver ├───────────────►│ │ │
|
||||
//! (±freq) │ │ │ (IA, IB) │ │ │ position
|
||||
//! │ └──────────┘ │ Motor ├──┼──────────►
|
||||
//! torque │ │ │ │ (0:199)
|
||||
//! Load ●──────────┼──────────────────────────────►│ │ │
|
||||
//! │ └─────────┘ │
|
||||
//! └────────────────────────────────────────────┘
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use nexosim::model::{BuildContext, Model, ProtoModel};
|
||||
use nexosim::ports::{EventBuffer, Output};
|
||||
use nexosim::simulation::{Mailbox, SimInit, SimulationError};
|
||||
use nexosim::time::MonotonicTime;
|
||||
|
||||
mod stepper_motor;
|
||||
|
||||
pub use stepper_motor::{Driver, Motor};
|
||||
|
||||
/// A prototype for `MotorAssembly`.
|
||||
pub struct ProtoMotorAssembly {
|
||||
pub position: Output<u16>,
|
||||
init_pos: u16,
|
||||
}
|
||||
|
||||
impl ProtoMotorAssembly {
|
||||
/// The prototype has a public constructor.
|
||||
pub fn new(init_pos: u16) -> Self {
|
||||
Self {
|
||||
position: Default::default(),
|
||||
init_pos,
|
||||
}
|
||||
}
|
||||
|
||||
// Input methods are in the model itself.
|
||||
}
|
||||
|
||||
/// The parent model which submodels are the driver and the motor.
|
||||
pub struct MotorAssembly {
|
||||
/// Private output for submodel connection.
|
||||
pps: Output<f64>,
|
||||
/// Private output for submodel connection.
|
||||
load: Output<f64>,
|
||||
}
|
||||
|
||||
impl MotorAssembly {
|
||||
/// The model now has a module-private constructor.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
pps: Default::default(),
|
||||
load: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pulse rate (sign = direction) [Hz] -- input port.
|
||||
pub async fn pulse_rate(&mut self, pps: f64) {
|
||||
self.pps.send(pps).await
|
||||
}
|
||||
|
||||
/// Torque applied by the load [N·m] -- input port.
|
||||
pub async fn load(&mut self, torque: f64) {
|
||||
self.load.send(torque).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for MotorAssembly {}
|
||||
|
||||
impl ProtoModel for ProtoMotorAssembly {
|
||||
type Model = MotorAssembly;
|
||||
|
||||
fn build(self, cx: &mut BuildContext<Self>) -> MotorAssembly {
|
||||
let mut assembly = MotorAssembly::new();
|
||||
let mut motor = Motor::new(self.init_pos);
|
||||
let mut driver = Driver::new(1.0);
|
||||
|
||||
// Mailboxes.
|
||||
let motor_mbox = Mailbox::new();
|
||||
let driver_mbox = Mailbox::new();
|
||||
|
||||
// Connections.
|
||||
assembly.pps.connect(Driver::pulse_rate, &driver_mbox);
|
||||
assembly.load.connect(Motor::load, &motor_mbox);
|
||||
driver.current_out.connect(Motor::current_in, &motor_mbox);
|
||||
|
||||
// Move the prototype's output to the submodel. The `self.position`
|
||||
// output can be cloned if necessary if several submodels need access to
|
||||
// it.
|
||||
motor.position = self.position;
|
||||
|
||||
// Add the submodels to the simulation.
|
||||
cx.add_submodel(driver, driver_mbox, "driver");
|
||||
cx.add_submodel(motor, motor_mbox, "motor");
|
||||
|
||||
assembly
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), SimulationError> {
|
||||
// ---------------
|
||||
// Bench assembly.
|
||||
// ---------------
|
||||
|
||||
// Models.
|
||||
let init_pos = 123;
|
||||
let mut assembly = ProtoMotorAssembly::new(init_pos);
|
||||
|
||||
// Mailboxes.
|
||||
let assembly_mbox = Mailbox::new();
|
||||
let assembly_addr = assembly_mbox.address();
|
||||
|
||||
// Model handles for simulation.
|
||||
let mut position = EventBuffer::new();
|
||||
assembly.position.connect_sink(&position);
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
// Assembly and initialization.
|
||||
let (mut simu, scheduler) = SimInit::new()
|
||||
.add_model(assembly, assembly_mbox, "assembly")
|
||||
.init(t0)?;
|
||||
|
||||
// ----------
|
||||
// Simulation.
|
||||
// ----------
|
||||
|
||||
// Check initial conditions.
|
||||
let mut t = t0;
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(position.next(), Some(init_pos));
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Start the motor in 2s with a PPS of 10Hz.
|
||||
scheduler
|
||||
.schedule_event(
|
||||
Duration::from_secs(2),
|
||||
MotorAssembly::pulse_rate,
|
||||
10.0,
|
||||
&assembly_addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Advance simulation time to two next events.
|
||||
simu.step()?;
|
||||
t += Duration::new(2, 0);
|
||||
assert_eq!(simu.time(), t);
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
|
||||
// Whichever the starting position, after two phase increments from the
|
||||
// driver the rotor should have synchronized with the driver, with a
|
||||
// position given by this beautiful formula.
|
||||
let mut pos = (((init_pos + 1) / 4) * 4 + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.by_ref().last().unwrap(), pos);
|
||||
|
||||
// Advance simulation time by 0.9s, which with a 10Hz PPS should correspond to
|
||||
// 9 position increments.
|
||||
simu.step_until(Duration::new(0, 900_000_000))?;
|
||||
t += Duration::new(0, 900_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
for _ in 0..9 {
|
||||
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.next(), Some(pos));
|
||||
}
|
||||
assert!(position.next().is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
445
nexosim/examples/espresso_machine.rs
Normal file
445
nexosim/examples/espresso_machine.rs
Normal file
@ -0,0 +1,445 @@
|
||||
//! Example: espresso coffee machine.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * non-trivial state machines,
|
||||
//! * cancellation of events,
|
||||
//! * model initialization,
|
||||
//! * simulation monitoring with event slots.
|
||||
//!
|
||||
//! ```text
|
||||
//! flow rate
|
||||
//! ┌─────────────────────────────────────────────┐
|
||||
//! │ (≥0) │
|
||||
//! │ ┌────────────┐ │
|
||||
//! └───►│ │ │
|
||||
//! added volume │ Water tank ├────┐ │
|
||||
//! Water fill ●───────────────────►│ │ │ │
|
||||
//! (>0) └────────────┘ │ │
|
||||
//! │ │
|
||||
//! water sense │ │
|
||||
//! ┌──────────────────────┘ │
|
||||
//! │ (empty|not empty) │
|
||||
//! │ │
|
||||
//! │ ┌────────────┐ ┌────────────┐ │
|
||||
//! brew time └───►│ │ command │ │ │
|
||||
//! Brew time dial ●───────────────────►│ Controller ├─────────►│ Water pump ├───┘
|
||||
//! (>0) ┌───►│ │ (on|off) │ │
|
||||
//! │ └────────────┘ └────────────┘
|
||||
//! trigger │
|
||||
//! Brew command ●───────────────┘
|
||||
//! (-)
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use nexosim::model::{Context, InitializedModel, Model};
|
||||
use nexosim::ports::{EventSlot, Output};
|
||||
use nexosim::simulation::{ActionKey, Mailbox, SimInit, SimulationError};
|
||||
use nexosim::time::MonotonicTime;
|
||||
|
||||
/// Water pump.
|
||||
pub struct Pump {
|
||||
/// Actual volumetric flow rate [m³·s⁻¹] -- output port.
|
||||
pub flow_rate: Output<f64>,
|
||||
|
||||
/// Nominal volumetric flow rate in operation [m³·s⁻¹] -- constant.
|
||||
nominal_flow_rate: f64,
|
||||
}
|
||||
|
||||
impl Pump {
|
||||
/// Creates a pump with the specified nominal flow rate [m³·s⁻¹].
|
||||
pub fn new(nominal_flow_rate: f64) -> Self {
|
||||
Self {
|
||||
nominal_flow_rate,
|
||||
flow_rate: Output::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Main ON/OFF command -- input port.
|
||||
pub async fn command(&mut self, cmd: PumpCommand) {
|
||||
let flow_rate = match cmd {
|
||||
PumpCommand::On => self.nominal_flow_rate,
|
||||
PumpCommand::Off => 0.0,
|
||||
};
|
||||
|
||||
self.flow_rate.send(flow_rate).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Pump {}
|
||||
|
||||
/// Espresso machine controller.
|
||||
pub struct Controller {
|
||||
/// Pump command -- output port.
|
||||
pub pump_cmd: Output<PumpCommand>,
|
||||
|
||||
/// Brew time setting [s] -- internal state.
|
||||
brew_time: Duration,
|
||||
/// Current water sense state.
|
||||
water_sense: WaterSenseState,
|
||||
/// Event key, which if present indicates that the machine is currently
|
||||
/// brewing -- internal state.
|
||||
stop_brew_key: Option<ActionKey>,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
/// Default brew time [s].
|
||||
const DEFAULT_BREW_TIME: Duration = Duration::new(25, 0);
|
||||
|
||||
/// Creates an espresso machine controller.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
brew_time: Self::DEFAULT_BREW_TIME,
|
||||
pump_cmd: Output::default(),
|
||||
stop_brew_key: None,
|
||||
water_sense: WaterSenseState::Empty, // will be overridden during init
|
||||
}
|
||||
}
|
||||
|
||||
/// Signals a change in the water sensing state -- input port.
|
||||
pub async fn water_sense(&mut self, state: WaterSenseState) {
|
||||
// Check if the tank just got empty.
|
||||
if state == WaterSenseState::Empty && self.water_sense == WaterSenseState::NotEmpty {
|
||||
// If a brew was ongoing, we must cancel it.
|
||||
if let Some(key) = self.stop_brew_key.take() {
|
||||
key.cancel();
|
||||
self.pump_cmd.send(PumpCommand::Off).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.water_sense = state;
|
||||
}
|
||||
|
||||
/// Sets the timing for the next brews [s] -- input port.
|
||||
pub async fn brew_time(&mut self, brew_time: Duration) {
|
||||
// Panic if the duration is null.
|
||||
assert!(!brew_time.is_zero());
|
||||
|
||||
self.brew_time = brew_time;
|
||||
}
|
||||
|
||||
/// Starts brewing or cancels the current brew -- input port.
|
||||
pub async fn brew_cmd(&mut self, _: (), cx: &mut Context<Self>) {
|
||||
// If a brew was ongoing, sending the brew command is interpreted as a
|
||||
// request to cancel it.
|
||||
if let Some(key) = self.stop_brew_key.take() {
|
||||
self.pump_cmd.send(PumpCommand::Off).await;
|
||||
|
||||
// Abort the scheduled call to `stop_brew()`.
|
||||
key.cancel();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no water, do nothing.
|
||||
if self.water_sense == WaterSenseState::Empty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the `stop_brew()` method and turn on the pump.
|
||||
self.stop_brew_key = Some(
|
||||
cx.schedule_keyed_event(self.brew_time, Self::stop_brew, ())
|
||||
.unwrap(),
|
||||
);
|
||||
self.pump_cmd.send(PumpCommand::On).await;
|
||||
}
|
||||
|
||||
/// Stops brewing.
|
||||
async fn stop_brew(&mut self) {
|
||||
if self.stop_brew_key.take().is_some() {
|
||||
self.pump_cmd.send(PumpCommand::Off).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Controller {}
|
||||
|
||||
/// ON/OFF pump command.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum PumpCommand {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Water tank.
|
||||
pub struct Tank {
|
||||
/// Water sensor -- output port.
|
||||
pub water_sense: Output<WaterSenseState>,
|
||||
|
||||
/// Volume of water [m³] -- internal state.
|
||||
volume: f64,
|
||||
/// State that exists when the mass flow rate is non-zero -- internal state.
|
||||
dynamic_state: Option<TankDynamicState>,
|
||||
}
|
||||
impl Tank {
|
||||
/// Creates a new tank with the specified amount of water [m³].
|
||||
///
|
||||
/// The initial flow rate is assumed to be zero.
|
||||
pub fn new(water_volume: f64) -> Self {
|
||||
assert!(water_volume >= 0.0);
|
||||
|
||||
Self {
|
||||
volume: water_volume,
|
||||
dynamic_state: None,
|
||||
water_sense: Output::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Water volume added [m³] -- input port.
|
||||
pub async fn fill(&mut self, added_volume: f64, cx: &mut Context<Self>) {
|
||||
// Ignore zero and negative values. We could also impose a maximum based
|
||||
// on tank capacity.
|
||||
if added_volume <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let was_empty = self.volume == 0.0;
|
||||
|
||||
// Account for the added water.
|
||||
self.volume += added_volume;
|
||||
|
||||
// If the current flow rate is non-zero, compute the current volume and
|
||||
// schedule a new update.
|
||||
if let Some(state) = self.dynamic_state.take() {
|
||||
// Abort the scheduled call to `set_empty()`.
|
||||
state.set_empty_key.cancel();
|
||||
|
||||
// Update the volume, saturating at 0 in case of rounding errors.
|
||||
let time = cx.time();
|
||||
let elapsed_time = time.duration_since(state.last_volume_update).as_secs_f64();
|
||||
self.volume = (self.volume - state.flow_rate * elapsed_time).max(0.0);
|
||||
|
||||
self.schedule_empty(state.flow_rate, time, cx).await;
|
||||
|
||||
// There is no need to broadcast the state of the water sense since
|
||||
// it could not be previously `Empty` (otherwise the dynamic state
|
||||
// would not exist).
|
||||
return;
|
||||
}
|
||||
|
||||
if was_empty {
|
||||
self.water_sense.send(WaterSenseState::NotEmpty).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Flow rate [m³·s⁻¹] -- input port.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method will panic if the flow rate is negative.
|
||||
pub async fn set_flow_rate(&mut self, flow_rate: f64, cx: &mut Context<Self>) {
|
||||
assert!(flow_rate >= 0.0);
|
||||
|
||||
let time = cx.time();
|
||||
|
||||
// If the flow rate was non-zero up to now, update the volume.
|
||||
if let Some(state) = self.dynamic_state.take() {
|
||||
// Abort the scheduled call to `set_empty()`.
|
||||
state.set_empty_key.cancel();
|
||||
|
||||
// Update the volume, saturating at 0 in case of rounding errors.
|
||||
let elapsed_time = time.duration_since(state.last_volume_update).as_secs_f64();
|
||||
self.volume = (self.volume - state.flow_rate * elapsed_time).max(0.0);
|
||||
}
|
||||
|
||||
self.schedule_empty(flow_rate, time, cx).await;
|
||||
}
|
||||
|
||||
/// Schedules a callback for when the tank becomes empty.
|
||||
///
|
||||
/// Pre-conditions:
|
||||
/// - `flow_rate` cannot be negative.
|
||||
/// - `self.volume` should be up to date,
|
||||
/// - `self.dynamic_state` should be `None`.
|
||||
async fn schedule_empty(
|
||||
&mut self,
|
||||
flow_rate: f64,
|
||||
time: MonotonicTime,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Determine when the tank will be empty at the current flow rate.
|
||||
let duration_until_empty = if self.volume == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
self.volume / flow_rate
|
||||
};
|
||||
if duration_until_empty.is_infinite() {
|
||||
// The flow rate is zero or very close to zero, so there is not
|
||||
// need to plan an update since the tank will never become
|
||||
// empty.
|
||||
return;
|
||||
}
|
||||
let duration_until_empty = Duration::from_secs_f64(duration_until_empty);
|
||||
|
||||
// Schedule the next update.
|
||||
match cx.schedule_keyed_event(duration_until_empty, Self::set_empty, ()) {
|
||||
Ok(set_empty_key) => {
|
||||
let state = TankDynamicState {
|
||||
last_volume_update: time,
|
||||
set_empty_key,
|
||||
flow_rate,
|
||||
};
|
||||
self.dynamic_state = Some(state);
|
||||
}
|
||||
Err(_) => {
|
||||
// The duration was null so the tank is already empty.
|
||||
self.volume = 0.0;
|
||||
self.water_sense.send(WaterSenseState::Empty).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the state of the tank to indicate that there is no more water.
|
||||
async fn set_empty(&mut self) {
|
||||
self.volume = 0.0;
|
||||
self.dynamic_state = None;
|
||||
self.water_sense.send(WaterSenseState::Empty).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Tank {
|
||||
/// Broadcasts the initial state of the water sense.
|
||||
async fn init(mut self, _: &mut Context<Self>) -> InitializedModel<Self> {
|
||||
self.water_sense
|
||||
.send(if self.volume == 0.0 {
|
||||
WaterSenseState::Empty
|
||||
} else {
|
||||
WaterSenseState::NotEmpty
|
||||
})
|
||||
.await;
|
||||
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic state of the tank that exists when and only when the mass flow rate
|
||||
/// is non-zero.
|
||||
struct TankDynamicState {
|
||||
last_volume_update: MonotonicTime,
|
||||
set_empty_key: ActionKey,
|
||||
flow_rate: f64,
|
||||
}
|
||||
|
||||
/// Water level in the tank.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum WaterSenseState {
|
||||
Empty,
|
||||
NotEmpty,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), SimulationError> {
|
||||
// ---------------
|
||||
// Bench assembly.
|
||||
// ---------------
|
||||
|
||||
// Models.
|
||||
|
||||
// The constant mass flow rate assumption is of course a gross
|
||||
// simplification, so the flow rate is set to an expected average over the
|
||||
// whole extraction [m³·s⁻¹].
|
||||
let pump_flow_rate = 4.5e-6;
|
||||
// Start with 1.5l in the tank [m³].
|
||||
let init_tank_volume = 1.5e-3;
|
||||
|
||||
let mut pump = Pump::new(pump_flow_rate);
|
||||
let mut controller = Controller::new();
|
||||
let mut tank = Tank::new(init_tank_volume);
|
||||
|
||||
// Mailboxes.
|
||||
let pump_mbox = Mailbox::new();
|
||||
let controller_mbox = Mailbox::new();
|
||||
let tank_mbox = Mailbox::new();
|
||||
|
||||
// Connections.
|
||||
controller.pump_cmd.connect(Pump::command, &pump_mbox);
|
||||
tank.water_sense
|
||||
.connect(Controller::water_sense, &controller_mbox);
|
||||
pump.flow_rate.connect(Tank::set_flow_rate, &tank_mbox);
|
||||
|
||||
// Model handles for simulation.
|
||||
let mut flow_rate = EventSlot::new();
|
||||
pump.flow_rate.connect_sink(&flow_rate);
|
||||
let controller_addr = controller_mbox.address();
|
||||
let tank_addr = tank_mbox.address();
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
// Assembly and initialization.
|
||||
let (mut simu, scheduler) = SimInit::new()
|
||||
.add_model(controller, controller_mbox, "controller")
|
||||
.add_model(pump, pump_mbox, "pump")
|
||||
.add_model(tank, tank_mbox, "tank")
|
||||
.init(t0)?;
|
||||
|
||||
// ----------
|
||||
// Simulation.
|
||||
// ----------
|
||||
|
||||
// Check initial conditions.
|
||||
let mut t = t0;
|
||||
assert_eq!(simu.time(), t);
|
||||
|
||||
// Brew one espresso shot with the default brew time.
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
assert_eq!(flow_rate.next(), Some(pump_flow_rate));
|
||||
|
||||
simu.step()?;
|
||||
t += Controller::DEFAULT_BREW_TIME;
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(flow_rate.next(), Some(0.0));
|
||||
|
||||
// Drink too much coffee.
|
||||
let volume_per_shot = pump_flow_rate * Controller::DEFAULT_BREW_TIME.as_secs_f64();
|
||||
let shots_per_tank = (init_tank_volume / volume_per_shot) as u64; // YOLO--who cares about floating-point rounding errors?
|
||||
for _ in 0..(shots_per_tank - 1) {
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
assert_eq!(flow_rate.next(), Some(pump_flow_rate));
|
||||
simu.step()?;
|
||||
t += Controller::DEFAULT_BREW_TIME;
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(flow_rate.next(), Some(0.0));
|
||||
}
|
||||
|
||||
// Check that the tank becomes empty before the completion of the next shot.
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
simu.step()?;
|
||||
assert!(simu.time() < t + Controller::DEFAULT_BREW_TIME);
|
||||
t = simu.time();
|
||||
assert_eq!(flow_rate.next(), Some(0.0));
|
||||
|
||||
// Try to brew another shot while the tank is still empty.
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
assert!(flow_rate.next().is_none());
|
||||
|
||||
// Change the brew time and fill up the tank.
|
||||
let brew_time = Duration::new(30, 0);
|
||||
simu.process_event(Controller::brew_time, brew_time, &controller_addr)?;
|
||||
simu.process_event(Tank::fill, 1.0e-3, tank_addr)?;
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
assert_eq!(flow_rate.next(), Some(pump_flow_rate));
|
||||
|
||||
simu.step()?;
|
||||
t += brew_time;
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(flow_rate.next(), Some(0.0));
|
||||
|
||||
// Interrupt the brew after 15s by pressing again the brew button.
|
||||
scheduler
|
||||
.schedule_event(
|
||||
Duration::from_secs(15),
|
||||
Controller::brew_cmd,
|
||||
(),
|
||||
&controller_addr,
|
||||
)
|
||||
.unwrap();
|
||||
simu.process_event(Controller::brew_cmd, (), &controller_addr)?;
|
||||
assert_eq!(flow_rate.next(), Some(pump_flow_rate));
|
||||
|
||||
simu.step()?;
|
||||
t += Duration::from_secs(15);
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(flow_rate.next(), Some(0.0));
|
||||
|
||||
Ok(())
|
||||
}
|
257
nexosim/examples/external_input.rs
Normal file
257
nexosim/examples/external_input.rs
Normal file
@ -0,0 +1,257 @@
|
||||
//! Example: a model that reads data external to the simulation.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * processing of external inputs (useful in co-simulation),
|
||||
//! * system clock,
|
||||
//! * periodic scheduling.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┏━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
//! ┃ Simulation ┃
|
||||
//! ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┃ ┌──────────┐ ┃
|
||||
//! ┆ ┆ message ┆ ┆ message ┃ │ │ message ┃
|
||||
//! ┆ UDP Client ├╌╌╌╌╌╌╌╌►┆ UDP Server ├╌╌╌╌╌╌╌╌╌╌╌╂╌╌►│ Listener ├─────────╂─►
|
||||
//! ┆ ┆ [UDP] ┆ ┆ [channel] ┃ │ │ ┃
|
||||
//! └╌╌╌╌╌╌╌╌╌╌╌╌┘ └╌╌╌╌╌╌╌╌╌╌╌╌┘ ┃ └──────────┘ ┃
|
||||
//! ┗━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//! ```
|
||||
|
||||
use std::io::ErrorKind;
|
||||
use std::net::{Ipv4Addr, UdpSocket};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread::{self, sleep, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
use nexosim::model::{BuildContext, Context, InitializedModel, Model, ProtoModel};
|
||||
use nexosim::ports::{EventBuffer, Output};
|
||||
use nexosim::simulation::{Mailbox, SimInit, SimulationError};
|
||||
use nexosim::time::{AutoSystemClock, MonotonicTime};
|
||||
|
||||
const DELTA: Duration = Duration::from_millis(2);
|
||||
const PERIOD: Duration = Duration::from_millis(20);
|
||||
const N: usize = 10;
|
||||
const SHUTDOWN_SIGNAL: &str = "<SHUTDOWN>";
|
||||
const SENDER: (Ipv4Addr, u16) = (Ipv4Addr::new(127, 0, 0, 1), 8000);
|
||||
const RECEIVER: (Ipv4Addr, u16) = (Ipv4Addr::new(127, 0, 0, 1), 9000);
|
||||
|
||||
/// Prototype for the `Listener` Model.
|
||||
pub struct ProtoListener {
|
||||
/// Received message.
|
||||
pub message: Output<String>,
|
||||
|
||||
/// Notifier to start the UDP client.
|
||||
start: Notifier,
|
||||
}
|
||||
|
||||
impl ProtoListener {
|
||||
fn new(start: Notifier) -> Self {
|
||||
Self {
|
||||
message: Output::default(),
|
||||
start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoModel for ProtoListener {
|
||||
type Model = Listener;
|
||||
|
||||
/// Start the UDP Server immediately upon model construction.
|
||||
fn build(self, _: &mut BuildContext<Self>) -> Listener {
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let external_handle = thread::spawn(move || {
|
||||
Listener::listen(tx, self.start);
|
||||
});
|
||||
|
||||
Listener::new(self.message, rx, external_handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Model that asynchronously receives messages external to the simulation.
|
||||
pub struct Listener {
|
||||
/// Received message.
|
||||
message: Output<String>,
|
||||
|
||||
/// Receiver of external messages.
|
||||
rx: Receiver<String>,
|
||||
|
||||
/// Handle to UDP Server.
|
||||
server_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
/// Creates a Listener.
|
||||
pub fn new(
|
||||
message: Output<String>,
|
||||
rx: Receiver<String>,
|
||||
server_handle: JoinHandle<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
message,
|
||||
rx,
|
||||
server_handle: Some(server_handle),
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodically scheduled function that processes external events.
|
||||
async fn process(&mut self) {
|
||||
while let Ok(message) = self.rx.try_recv() {
|
||||
self.message.send(message).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the UDP server.
|
||||
fn listen(tx: Sender<String>, start: Notifier) {
|
||||
let socket = UdpSocket::bind(RECEIVER).unwrap();
|
||||
let mut buf = [0; 1 << 16];
|
||||
|
||||
// Wake up the client.
|
||||
start.notify();
|
||||
|
||||
loop {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((packet_size, _)) => {
|
||||
if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) {
|
||||
if message == SHUTDOWN_SIGNAL {
|
||||
break;
|
||||
}
|
||||
// Inject external message into simulation.
|
||||
if tx.send(message.into()).is_err() {
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Listener {
|
||||
/// Initialize model.
|
||||
async fn init(self, cx: &mut Context<Self>) -> InitializedModel<Self> {
|
||||
// Schedule periodic function that processes external events.
|
||||
cx.schedule_periodic_event(DELTA, PERIOD, Listener::process, ())
|
||||
.unwrap();
|
||||
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Listener {
|
||||
/// Wait for UDP Server shutdown.
|
||||
fn drop(&mut self) {
|
||||
self.server_handle.take().map(|handle| {
|
||||
let _ = handle.join();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A synchronization barrier that can be unblocked by a notifier.
|
||||
struct WaitBarrier(Arc<(Mutex<bool>, Condvar)>);
|
||||
|
||||
impl WaitBarrier {
|
||||
fn new() -> Self {
|
||||
Self(Arc::new((Mutex::new(false), Condvar::new())))
|
||||
}
|
||||
fn notifier(&self) -> Notifier {
|
||||
Notifier(self.0.clone())
|
||||
}
|
||||
fn wait(self) {
|
||||
let _unused = self
|
||||
.0
|
||||
.1
|
||||
.wait_while(self.0 .0.lock().unwrap(), |pending| *pending)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A notifier for the associated synchronization barrier.
|
||||
struct Notifier(Arc<(Mutex<bool>, Condvar)>);
|
||||
|
||||
impl Notifier {
|
||||
fn notify(self) {
|
||||
*self.0 .0.lock().unwrap() = false;
|
||||
self.0 .1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), SimulationError> {
|
||||
// ---------------
|
||||
// Bench assembly.
|
||||
// ---------------
|
||||
|
||||
// Models.
|
||||
|
||||
// Synchronization barrier for the UDP client.
|
||||
let start = WaitBarrier::new();
|
||||
|
||||
// Prototype of the listener model.
|
||||
let mut listener = ProtoListener::new(start.notifier());
|
||||
|
||||
// Mailboxes.
|
||||
let listener_mbox = Mailbox::new();
|
||||
|
||||
// Model handles for simulation.
|
||||
let mut message = EventBuffer::with_capacity(N + 1);
|
||||
listener.message.connect_sink(&message);
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
// Assembly and initialization.
|
||||
let mut simu = SimInit::new()
|
||||
.add_model(listener, listener_mbox, "listener")
|
||||
.set_clock(AutoSystemClock::new())
|
||||
.init(t0)?
|
||||
.0;
|
||||
|
||||
// ----------
|
||||
// Simulation.
|
||||
// ----------
|
||||
|
||||
// External client that sends UDP messages.
|
||||
let sender_handle = thread::spawn(move || {
|
||||
let socket = UdpSocket::bind(SENDER).unwrap();
|
||||
|
||||
// Wait until the UDP Server is ready.
|
||||
start.wait();
|
||||
|
||||
for i in 0..N {
|
||||
socket.send_to(i.to_string().as_bytes(), RECEIVER).unwrap();
|
||||
if i % 3 == 0 {
|
||||
sleep(PERIOD * i as u32)
|
||||
}
|
||||
}
|
||||
|
||||
socket
|
||||
});
|
||||
|
||||
// Advance simulation, external messages will be collected.
|
||||
simu.step_until(Duration::from_secs(2))?;
|
||||
|
||||
// Shut down the server.
|
||||
let socket = sender_handle.join().unwrap();
|
||||
socket
|
||||
.send_to(SHUTDOWN_SIGNAL.as_bytes(), RECEIVER)
|
||||
.unwrap();
|
||||
|
||||
// Check collected external messages.
|
||||
let mut packets = 0_u32;
|
||||
for _ in 0..N {
|
||||
// Check all messages accounting for possible UDP packet re-ordering,
|
||||
// but assuming no packet loss.
|
||||
packets |= 1 << message.next().unwrap().parse::<u8>().unwrap();
|
||||
}
|
||||
assert_eq!(packets, u32::MAX >> 22);
|
||||
assert_eq!(message.next(), None);
|
||||
|
||||
Ok(())
|
||||
}
|
175
nexosim/examples/power_supply.rs
Normal file
175
nexosim/examples/power_supply.rs
Normal file
@ -0,0 +1,175 @@
|
||||
//! Example: power supply with parallel resistive loads.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * the use of requestor and replier ports,
|
||||
//! * simulation monitoring with event slots.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────┐
|
||||
//! │ │
|
||||
//! ┌──◄►│ Load ├───► Power
|
||||
//! │ │ │
|
||||
//! │ └────────┘
|
||||
//! │
|
||||
//! │ ┌────────┐
|
||||
//! │ │ │
|
||||
//! ├──◄►│ Load ├───► Power
|
||||
//! │ │ │
|
||||
//! │ └────────┘
|
||||
//! │
|
||||
//! │ ┌────────┐
|
||||
//! ┌──────────┐ voltage► │ │ │
|
||||
//! Voltage setting ●────►│ │►◄───────────┴──◄►│ Load ├───► Power
|
||||
//! │ Power │ ◄current │ │
|
||||
//! │ supply │ └────────┘
|
||||
//! │ ├───────────────────────────────► Total power
|
||||
//! └──────────┘
|
||||
//! ```
|
||||
use nexosim::model::Model;
|
||||
use nexosim::ports::{EventSlot, Output, Requestor};
|
||||
use nexosim::simulation::{Mailbox, SimInit, SimulationError};
|
||||
use nexosim::time::MonotonicTime;
|
||||
|
||||
/// Power supply.
|
||||
pub struct PowerSupply {
|
||||
/// Electrical output [V → A] -- requestor port.
|
||||
pub pwr_out: Requestor<f64, f64>,
|
||||
/// Power consumption [W] -- output port.
|
||||
pub power: Output<f64>,
|
||||
}
|
||||
|
||||
impl PowerSupply {
|
||||
/// Creates a power supply.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
pwr_out: Default::default(),
|
||||
power: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Voltage setting [V] -- input port.
|
||||
pub async fn voltage_setting(&mut self, voltage: f64) {
|
||||
// Ignore negative values.
|
||||
if voltage < 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sum all load currents.
|
||||
let mut total_current = 0.0;
|
||||
for current in self.pwr_out.send(voltage).await {
|
||||
total_current += current;
|
||||
}
|
||||
|
||||
self.power.send(voltage * total_current).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for PowerSupply {}
|
||||
|
||||
/// Power supply.
|
||||
pub struct Load {
|
||||
/// Power consumption [W] -- output port.
|
||||
pub power: Output<f64>,
|
||||
|
||||
/// Load conductance [S] -- internal state.
|
||||
conductance: f64,
|
||||
}
|
||||
|
||||
impl Load {
|
||||
/// Creates a load with the specified resistance [Ω].
|
||||
fn new(resistance: f64) -> Self {
|
||||
assert!(resistance > 0.0);
|
||||
Self {
|
||||
power: Default::default(),
|
||||
conductance: 1.0 / resistance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Electrical input [V → A] -- replier port.
|
||||
///
|
||||
/// This port receives the applied voltage and returns the load current.
|
||||
pub async fn pwr_in(&mut self, voltage: f64) -> f64 {
|
||||
let current = voltage * self.conductance;
|
||||
self.power.send(voltage * current).await;
|
||||
|
||||
current
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Load {}
|
||||
|
||||
fn main() -> Result<(), SimulationError> {
|
||||
// ---------------
|
||||
// Bench assembly.
|
||||
// ---------------
|
||||
|
||||
// Models.
|
||||
let r1 = 5.0;
|
||||
let r2 = 10.0;
|
||||
let r3 = 20.0;
|
||||
let mut psu = PowerSupply::new();
|
||||
let mut load1 = Load::new(r1);
|
||||
let mut load2 = Load::new(r2);
|
||||
let mut load3 = Load::new(r3);
|
||||
|
||||
// Mailboxes.
|
||||
let psu_mbox = Mailbox::new();
|
||||
let load1_mbox = Mailbox::new();
|
||||
let load2_mbox = Mailbox::new();
|
||||
let load3_mbox = Mailbox::new();
|
||||
|
||||
// Connections.
|
||||
psu.pwr_out.connect(Load::pwr_in, &load1_mbox);
|
||||
psu.pwr_out.connect(Load::pwr_in, &load2_mbox);
|
||||
psu.pwr_out.connect(Load::pwr_in, &load3_mbox);
|
||||
|
||||
// Model handles for simulation.
|
||||
let mut psu_power = EventSlot::new();
|
||||
let mut load1_power = EventSlot::new();
|
||||
let mut load2_power = EventSlot::new();
|
||||
let mut load3_power = EventSlot::new();
|
||||
psu.power.connect_sink(&psu_power);
|
||||
load1.power.connect_sink(&load1_power);
|
||||
load2.power.connect_sink(&load2_power);
|
||||
load3.power.connect_sink(&load3_power);
|
||||
let psu_addr = psu_mbox.address();
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
// Assembly and initialization.
|
||||
let mut simu = SimInit::new()
|
||||
.add_model(psu, psu_mbox, "psu")
|
||||
.add_model(load1, load1_mbox, "load1")
|
||||
.add_model(load2, load2_mbox, "load2")
|
||||
.add_model(load3, load3_mbox, "load3")
|
||||
.init(t0)?
|
||||
.0;
|
||||
|
||||
// ----------
|
||||
// Simulation.
|
||||
// ----------
|
||||
|
||||
// Compare two electrical powers for equality [W].
|
||||
fn same_power(a: f64, b: f64) -> bool {
|
||||
// Use an absolute floating-point epsilon of 1 pW.
|
||||
(a - b).abs() < 1e-12
|
||||
}
|
||||
|
||||
// Vary the supply voltage, check the load and power supply consumptions.
|
||||
for voltage in [10.0, 15.0, 20.0] {
|
||||
simu.process_event(PowerSupply::voltage_setting, voltage, &psu_addr)?;
|
||||
|
||||
let v_square = voltage * voltage;
|
||||
assert!(same_power(load1_power.next().unwrap(), v_square / r1));
|
||||
assert!(same_power(load2_power.next().unwrap(), v_square / r2));
|
||||
assert!(same_power(load3_power.next().unwrap(), v_square / r3));
|
||||
assert!(same_power(
|
||||
psu_power.next().unwrap(),
|
||||
v_square * (1.0 / r1 + 1.0 / r2 + 1.0 / r3)
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
316
nexosim/examples/stepper_motor.rs
Normal file
316
nexosim/examples/stepper_motor.rs
Normal file
@ -0,0 +1,316 @@
|
||||
//! Example: current-controlled stepper motor and its driver.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * self-scheduling methods,
|
||||
//! * model initialization,
|
||||
//! * simulation monitoring with buffered event sinks.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌──────────┐
|
||||
//! PPS │ │ coil currents ┌─────────┐
|
||||
//! Pulse rate ●─────────►│ Driver ├───────────────►│ │
|
||||
//! (±freq) │ │ (IA, IB) │ │ position
|
||||
//! └──────────┘ │ Motor ├──────────►
|
||||
//! torque │ │ (0:199)
|
||||
//! Load ●─────────────────────────────────────►│ │
|
||||
//! └─────────┘
|
||||
//! ```
|
||||
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use nexosim::model::{Context, InitializedModel, Model};
|
||||
use nexosim::ports::{EventBuffer, Output};
|
||||
use nexosim::simulation::{Mailbox, SimInit};
|
||||
use nexosim::time::MonotonicTime;
|
||||
|
||||
/// Stepper motor.
|
||||
pub struct Motor {
|
||||
/// Position [-] -- output port.
|
||||
pub position: Output<u16>,
|
||||
|
||||
/// Position [-] -- internal state.
|
||||
pos: u16,
|
||||
/// Torque applied by the load [N·m] -- internal state.
|
||||
torque: f64,
|
||||
}
|
||||
|
||||
impl Motor {
|
||||
/// Number of steps per revolution.
|
||||
pub const STEPS_PER_REV: u16 = 200;
|
||||
/// Torque constant of the motor [N·m·A⁻¹].
|
||||
pub const TORQUE_CONSTANT: f64 = 1.0;
|
||||
|
||||
/// Creates a motor with the specified initial position.
|
||||
pub fn new(position: u16) -> Self {
|
||||
Self {
|
||||
position: Default::default(),
|
||||
pos: position % Self::STEPS_PER_REV,
|
||||
torque: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Coil currents [A] -- input port.
|
||||
///
|
||||
/// For the sake of simplicity, we do as if the rotor rotates
|
||||
/// instantaneously. If the current is too weak to overcome the load or when
|
||||
/// attempting to move to an opposite phase, the position remains unchanged.
|
||||
pub async fn current_in(&mut self, current: (f64, f64)) {
|
||||
assert!(!current.0.is_nan() && !current.1.is_nan());
|
||||
|
||||
let (target_phase, abs_current) = match (current.0 != 0.0, current.1 != 0.0) {
|
||||
(false, false) => return,
|
||||
(true, false) => (if current.0 > 0.0 { 0 } else { 2 }, current.0.abs()),
|
||||
(false, true) => (if current.1 > 0.0 { 1 } else { 3 }, current.1.abs()),
|
||||
_ => panic!("current detected in both coils"),
|
||||
};
|
||||
|
||||
if abs_current < Self::TORQUE_CONSTANT * self.torque {
|
||||
return;
|
||||
}
|
||||
let pos_delta = match target_phase - (self.pos % 4) as i8 {
|
||||
0 | 2 | -2 => return,
|
||||
1 | -3 => 1,
|
||||
-1 | 3 => Self::STEPS_PER_REV - 1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.pos = (self.pos + pos_delta) % Self::STEPS_PER_REV;
|
||||
self.position.send(self.pos).await;
|
||||
}
|
||||
|
||||
/// Torque applied by the load [N·m] -- input port.
|
||||
pub fn load(&mut self, torque: f64) {
|
||||
assert!(torque >= 0.0);
|
||||
|
||||
self.torque = torque;
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Motor {
|
||||
/// Broadcasts the initial position of the motor.
|
||||
async fn init(mut self, _: &mut Context<Self>) -> InitializedModel<Self> {
|
||||
self.position.send(self.pos).await;
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stepper motor driver.
|
||||
pub struct Driver {
|
||||
/// Coil A and coil B currents [A] -- output port.
|
||||
pub current_out: Output<(f64, f64)>,
|
||||
|
||||
/// Requested pulse rate (pulse per second) [Hz] -- internal state.
|
||||
pps: f64,
|
||||
/// Phase for the next pulse (= 0, 1, 2 or 3) -- internal state.
|
||||
next_phase: u8,
|
||||
/// Nominal coil current (absolute value) [A] -- constant.
|
||||
current: f64,
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
/// Minimum supported pulse rate [Hz].
|
||||
const MIN_PPS: f64 = 1.0;
|
||||
/// Maximum supported pulse rate [Hz].
|
||||
const MAX_PPS: f64 = 1_000.0;
|
||||
|
||||
/// Creates a new driver with the specified nominal current.
|
||||
pub fn new(nominal_current: f64) -> Self {
|
||||
Self {
|
||||
current_out: Default::default(),
|
||||
pps: 0.0,
|
||||
next_phase: 0,
|
||||
current: nominal_current,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pulse rate (sign = direction) [Hz] -- input port.
|
||||
pub async fn pulse_rate(&mut self, pps: f64, cx: &mut Context<Self>) {
|
||||
let pps = pps.signum() * pps.abs().clamp(Self::MIN_PPS, Self::MAX_PPS);
|
||||
if pps == self.pps {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_idle = self.pps == 0.0;
|
||||
self.pps = pps;
|
||||
|
||||
// Trigger the rotation if the motor is currently idle. Otherwise the
|
||||
// new value will be accounted for at the next pulse.
|
||||
if is_idle {
|
||||
self.send_pulse((), cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a pulse and schedules the next one.
|
||||
///
|
||||
/// Note: self-scheduling async methods must be for now defined with an
|
||||
/// explicit signature instead of `async fn` due to a rustc issue.
|
||||
fn send_pulse<'a>(
|
||||
&'a mut self,
|
||||
_: (),
|
||||
cx: &'a mut Context<Self>,
|
||||
) -> impl Future<Output = ()> + Send + 'a {
|
||||
async move {
|
||||
let current_out = match self.next_phase {
|
||||
0 => (self.current, 0.0),
|
||||
1 => (0.0, self.current),
|
||||
2 => (-self.current, 0.0),
|
||||
3 => (0.0, -self.current),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
self.current_out.send(current_out).await;
|
||||
|
||||
if self.pps == 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.next_phase = (self.next_phase + (self.pps.signum() + 4.0) as u8) % 4;
|
||||
|
||||
let pulse_duration = Duration::from_secs_f64(1.0 / self.pps.abs());
|
||||
|
||||
// Schedule the next pulse.
|
||||
cx.schedule_event(pulse_duration, Self::send_pulse, ())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Driver {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn main() -> Result<(), nexosim::simulation::SimulationError> {
|
||||
// ---------------
|
||||
// Bench assembly.
|
||||
// ---------------
|
||||
|
||||
// Models.
|
||||
let init_pos = 123;
|
||||
let mut motor = Motor::new(init_pos);
|
||||
let mut driver = Driver::new(1.0);
|
||||
|
||||
// Mailboxes.
|
||||
let motor_mbox = Mailbox::new();
|
||||
let driver_mbox = Mailbox::new();
|
||||
|
||||
// Connections.
|
||||
driver.current_out.connect(Motor::current_in, &motor_mbox);
|
||||
|
||||
// Model handles for simulation.
|
||||
let mut position = EventBuffer::new();
|
||||
motor.position.connect_sink(&position);
|
||||
let motor_addr = motor_mbox.address();
|
||||
let driver_addr = driver_mbox.address();
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
// Assembly and initialization.
|
||||
let (mut simu, scheduler) = SimInit::new()
|
||||
.add_model(driver, driver_mbox, "driver")
|
||||
.add_model(motor, motor_mbox, "motor")
|
||||
.init(t0)?;
|
||||
|
||||
// ----------
|
||||
// Simulation.
|
||||
// ----------
|
||||
|
||||
// Check initial conditions.
|
||||
let mut t = t0;
|
||||
assert_eq!(simu.time(), t);
|
||||
assert_eq!(position.next(), Some(init_pos));
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Start the motor in 2s with a PPS of 10Hz.
|
||||
scheduler
|
||||
.schedule_event(
|
||||
Duration::from_secs(2),
|
||||
Driver::pulse_rate,
|
||||
10.0,
|
||||
&driver_addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Advance simulation time to two next events.
|
||||
simu.step()?;
|
||||
t += Duration::new(2, 0);
|
||||
assert_eq!(simu.time(), t);
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
|
||||
// Whichever the starting position, after two phase increments from the
|
||||
// driver the rotor should have synchronized with the driver, with a
|
||||
// position given by this beautiful formula.
|
||||
let mut pos = (((init_pos + 1) / 4) * 4 + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.by_ref().last().unwrap(), pos);
|
||||
|
||||
// Advance simulation time by 0.9s, which with a 10Hz PPS should correspond to
|
||||
// 9 position increments.
|
||||
simu.step_until(Duration::new(0, 900_000_000))?;
|
||||
t += Duration::new(0, 900_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
for _ in 0..9 {
|
||||
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.next(), Some(pos));
|
||||
}
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Increase the load beyond the torque limit for a 1A driver current.
|
||||
simu.process_event(Motor::load, 2.0, &motor_addr)?;
|
||||
|
||||
// Advance simulation time and check that the motor is blocked.
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Do it again.
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Decrease the load below the torque limit for a 1A driver current and
|
||||
// advance simulation time.
|
||||
simu.process_event(Motor::load, 0.5, &motor_addr)?;
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
|
||||
// The motor should start moving again, but since the phase was incremented
|
||||
// 3 times (out of 4 phases) while the motor was blocked, the motor actually
|
||||
// makes a step backward before it moves forward again.
|
||||
assert_eq!(simu.time(), t);
|
||||
pos = (pos + Motor::STEPS_PER_REV - 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.next(), Some(pos));
|
||||
|
||||
// Advance simulation time by 0.7s, which with a 10Hz PPS should correspond to
|
||||
// 7 position increments.
|
||||
simu.step_until(Duration::new(0, 700_000_000))?;
|
||||
t += Duration::new(0, 700_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
for _ in 0..7 {
|
||||
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.next(), Some(pos));
|
||||
}
|
||||
assert!(position.next().is_none());
|
||||
|
||||
// Now make the motor rotate in the opposite direction. Note that this
|
||||
// driver only accounts for a new PPS at the next pulse.
|
||||
simu.process_event(Driver::pulse_rate, -10.0, &driver_addr)?;
|
||||
simu.step()?;
|
||||
t += Duration::new(0, 100_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.next(), Some(pos));
|
||||
|
||||
// Advance simulation time by 1.9s, which with a -10Hz PPS should correspond
|
||||
// to 19 position decrements.
|
||||
simu.step_until(Duration::new(1, 900_000_000))?;
|
||||
t += Duration::new(1, 900_000_000);
|
||||
assert_eq!(simu.time(), t);
|
||||
pos = (pos + Motor::STEPS_PER_REV - 19) % Motor::STEPS_PER_REV;
|
||||
assert_eq!(position.by_ref().last(), Some(pos));
|
||||
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user