1
0
forked from ROMEO/nexosim

First release candidate for v0.1.0

This commit is contained in:
Serge Barral
2023-01-16 23:05:46 +01:00
parent fe00ee0743
commit 31520d461a
58 changed files with 9731 additions and 1401 deletions

View File

@ -0,0 +1,480 @@
//! Example: espresso coffee machine.
//!
//! This example demonstrates in particular:
//!
//! * non-trivial state machines,
//! * cancellation of calls scheduled at the current time step using epochs,
//! * 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::future::Future;
use std::pin::Pin;
use std::time::Duration;
use asynchronix::model::{InitializedModel, Model, Output};
use asynchronix::simulation::{Mailbox, SimInit};
use asynchronix::time::{MonotonicTime, Scheduler, SchedulerKey};
/// 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,
/// Scheduler key, which if present indicates that the machine is current
/// brewing -- internal state.
stop_brew_key: Option<SchedulerKey>,
/// An epoch incremented when the scheduled 'stop_brew` callback must be
/// ignored -- internal state.
stop_brew_epoch: u64,
}
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
stop_brew_epoch: 0,
}
}
/// Signals a change in the water sensing state -- input port.
pub async fn water_sense(&mut self, state: WaterSenseState, scheduler: &Scheduler<Self>) {
// 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() {
// Try to abort the scheduled call to `stop_brew()`. If this will
// fails, increment the epoch so that the call is ignored.
if scheduler.cancel(key).is_err() {
self.stop_brew_epoch = self.stop_brew_epoch.wrapping_add(1);
};
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, _: (), scheduler: &Scheduler<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;
// Try to abort the scheduled call to `stop_brew()`. If this will
// fails, increment the epoch so that the call is ignored.
if scheduler.cancel(key).is_err() {
self.stop_brew_epoch = self.stop_brew_epoch.wrapping_add(1);
};
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(
scheduler
.schedule_in(self.brew_time, Self::stop_brew, self.stop_brew_epoch)
.unwrap(),
);
self.pump_cmd.send(PumpCommand::On).await;
}
/// Stops brewing.
async fn stop_brew(&mut self, epoch: u64) {
// Ignore this call if the epoch has been incremented.
if self.stop_brew_epoch != epoch {
return;
}
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>,
/// An epoch incremented when the pending call to `set_empty()` must be
/// ignored -- internal state.
set_empty_epoch: u64,
}
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,
set_empty_epoch: 0,
water_sense: Output::default(),
}
}
/// Water volume added [m³] -- input port.
pub async fn fill(&mut self, added_volume: f64, scheduler: &Scheduler<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() {
// Try to abort the scheduled call to `set_empty()`. If this will
// fails, increment the epoch so that the call is ignored.
if scheduler.cancel(state.set_empty_key).is_err() {
self.set_empty_epoch = self.set_empty_epoch.wrapping_add(1);
}
// Update the volume, saturating at 0 in case of rounding errors.
let time = scheduler.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, scheduler).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, scheduler: &Scheduler<Self>) {
assert!(flow_rate >= 0.0);
let time = scheduler.time();
// If the flow rate was non-zero up to now, update the volume.
if let Some(state) = self.dynamic_state.take() {
// Try to abort the scheduled call to `set_empty()`. If this will
// fails, increment the epoch so that the call is ignored.
if scheduler.cancel(state.set_empty_key).is_err() {
self.set_empty_epoch = self.set_empty_epoch.wrapping_add(1);
}
// 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, scheduler).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,
scheduler: &Scheduler<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 scheduler.schedule_in(duration_until_empty, Self::set_empty, self.set_empty_epoch) {
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, epoch: u64) {
// Ignore this call if the epoch has been incremented.
if epoch != self.set_empty_epoch {
return;
}
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.
fn init(
mut self,
_scheduler: &Scheduler<Self>,
) -> Pin<Box<dyn Future<Output = InitializedModel<Self>> + Send + '_>> {
Box::pin(async move {
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: SchedulerKey,
flow_rate: f64,
}
/// Water level in the tank.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum WaterSenseState {
Empty,
NotEmpty,
}
fn main() {
// ---------------
// 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 = pump.flow_rate.connect_slot().0;
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 = SimInit::new()
.add_model(controller, controller_mbox)
.add_model(pump, pump_mbox)
.add_model(tank, tank_mbox)
.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.send_event(Controller::brew_cmd, (), &controller_addr);
assert_eq!(flow_rate.take(), Some(pump_flow_rate));
simu.step();
t += Controller::DEFAULT_BREW_TIME;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.take(), 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 care about floating-point rounding errors?
for _ in 0..(shots_per_tank - 1) {
simu.send_event(Controller::brew_cmd, (), &controller_addr);
assert_eq!(flow_rate.take(), Some(pump_flow_rate));
simu.step();
t += Controller::DEFAULT_BREW_TIME;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.take(), Some(0.0));
}
// Check that the tank becomes empty before the completion of the next shot.
simu.send_event(Controller::brew_cmd, (), &controller_addr);
simu.step();
assert!(simu.time() < t + Controller::DEFAULT_BREW_TIME);
t = simu.time();
assert_eq!(flow_rate.take(), Some(0.0));
// Try to brew another shot while the tank is still empty.
simu.send_event(Controller::brew_cmd, (), &controller_addr);
assert!(flow_rate.take().is_none());
// Change the brew time and fill up the tank.
let brew_time = Duration::new(30, 0);
simu.send_event(Controller::brew_time, brew_time, &controller_addr);
simu.send_event(Tank::fill, 1.0e-3, tank_addr);
simu.send_event(Controller::brew_cmd, (), &controller_addr);
assert_eq!(flow_rate.take(), Some(pump_flow_rate));
simu.step();
t += brew_time;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.take(), Some(0.0));
// Interrupt the brew after 15s by pressing again the brew button.
simu.schedule_in(
Duration::from_secs(15),
Controller::brew_cmd,
(),
&controller_addr,
)
.unwrap();
simu.send_event(Controller::brew_cmd, (), &controller_addr);
assert_eq!(flow_rate.take(), Some(pump_flow_rate));
simu.step();
t += Duration::from_secs(15);
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.take(), Some(0.0));
}

View File

@ -0,0 +1,167 @@
//! 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 asynchronix::model::{Model, Output, Requestor};
use asynchronix::simulation::{Mailbox, SimInit};
use asynchronix::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() {
// ---------------
// 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 = psu.power.connect_slot().0;
let mut load1_power = load1.power.connect_slot().0;
let mut load2_power = load2.power.connect_slot().0;
let mut load3_power = load3.power.connect_slot().0;
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)
.add_model(load1, load1_mbox)
.add_model(load2, load2_mbox)
.add_model(load3, load3_mbox)
.init(t0);
// ----------
// 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.send_event(PowerSupply::voltage_setting, voltage, &psu_addr);
let v_square = voltage * voltage;
assert!(same_power(load1_power.take().unwrap(), v_square / r1));
assert!(same_power(load2_power.take().unwrap(), v_square / r2));
assert!(same_power(load3_power.take().unwrap(), v_square / r3));
assert!(same_power(
psu_power.take().unwrap(),
v_square * (1.0 / r1 + 1.0 / r2 + 1.0 / r3)
));
}
}

View File

@ -0,0 +1,315 @@
//! Example: current-controlled stepper motor and its driver.
//!
//! This example demonstrates in particular:
//!
//! * self-scheduling methods,
//! * model initialization,
//! * simulation monitoring with event streams.
//!
//! ```text
//! ┌──────────┐ ┌──────────┐
//! PPS │ │ coil currents │ │ position
//! Pulse rate ●─────────▶│ Driver ├───────────────▶│ Motor ├──────────▶
//! (±freq) │ │ (IA, IB) │ │ (0:199)
//! └──────────┘ └──────────┘
//! ```
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use asynchronix::model::{InitializedModel, Model, Output};
use asynchronix::simulation::{Mailbox, SimInit};
use asynchronix::time::{MonotonicTime, Scheduler};
/// 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.
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.
fn init(
mut self,
_scheduler: &Scheduler<Self>,
) -> Pin<Box<dyn Future<Output = InitializedModel<Self>> + Send + '_>> {
Box::pin(async move {
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,
}
}
/// Sets the pulse rate (sign = direction) [Hz] -- input port.
pub async fn pulse_rate(&mut self, pps: f64, scheduler: &Scheduler<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((), scheduler).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,
_: (),
scheduler: &'a Scheduler<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.
scheduler
.schedule_in(pulse_duration, Self::send_pulse, ())
.unwrap();
}
}
}
impl Model for Driver {}
fn main() {
// ---------------
// 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 = motor.position.connect_stream().0;
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 = SimInit::new()
.add_model(driver, driver_mbox)
.add_model(motor, motor_mbox)
.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.
simu.schedule_in(
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_by(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.send_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.send_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_by(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.send_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_by(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));
}