use std::time::Duration;
use serde::{Deserialize, Serialize};
use nexosim::Message;
use nexosim::model::{Context, Model, schedulable};
use nexosim::ports::{EventSinkReader, EventSource, Output, SinkState, event_slot};
use nexosim::simulation::{EventKey, Mailbox, SimInit, SimulationError};
use nexosim::time::MonotonicTime;
#[derive(Serialize, Deserialize)]
pub struct Pump {
pub flow_rate: Output<f64>,
nominal_flow_rate: f64,
}
#[Model]
impl Pump {
pub fn new(nominal_flow_rate: f64) -> Self {
Self {
nominal_flow_rate,
flow_rate: Output::default(),
}
}
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;
}
}
#[derive(Serialize, Deserialize)]
pub struct Controller {
pub pump_cmd: Output<PumpCommand>,
brew_time: Duration,
water_sense: WaterSenseState,
stop_brew_key: Option<EventKey>,
}
#[Model]
impl Controller {
pub const DEFAULT_BREW_TIME: Duration = Duration::new(25, 0);
pub fn new() -> Self {
Self {
brew_time: Self::DEFAULT_BREW_TIME,
pump_cmd: Output::default(),
stop_brew_key: None,
water_sense: WaterSenseState::Empty, }
}
pub async fn water_sense(&mut self, state: WaterSenseState) {
if state == WaterSenseState::Empty && self.water_sense == WaterSenseState::NotEmpty {
if let Some(key) = self.stop_brew_key.take() {
key.cancel();
self.pump_cmd.send(PumpCommand::Off).await;
}
}
self.water_sense = state;
}
pub async fn brew_time(&mut self, brew_time: Duration) {
assert!(!brew_time.is_zero());
self.brew_time = brew_time;
}
pub async fn brew_cmd(&mut self, _: (), cx: &Context<Self>) {
if let Some(key) = self.stop_brew_key.take() {
self.pump_cmd.send(PumpCommand::Off).await;
key.cancel();
return;
}
if self.water_sense == WaterSenseState::Empty {
return;
}
self.stop_brew_key = Some(
cx.schedule_keyed_event(self.brew_time, schedulable!(Self::stop_brew), ())
.unwrap(),
);
self.pump_cmd.send(PumpCommand::On).await;
}
#[nexosim(schedulable)]
async fn stop_brew(&mut self) {
if self.stop_brew_key.take().is_some() {
self.pump_cmd.send(PumpCommand::Off).await;
}
}
}
#[derive(Copy, Clone, Eq, Message, PartialEq, Serialize, Deserialize)]
pub enum PumpCommand {
On,
Off,
}
#[derive(Serialize, Deserialize)]
pub struct Tank {
pub water_sense: Output<WaterSenseState>,
volume: f64,
dynamic_state: Option<TankDynamicState>,
}
#[Model]
impl Tank {
pub fn new(water_volume: f64) -> Self {
assert!(water_volume >= 0.0);
Self {
volume: water_volume,
dynamic_state: None,
water_sense: Output::default(),
}
}
#[nexosim(init)]
async fn init(&mut self) {
self.water_sense
.send(if self.volume == 0.0 {
WaterSenseState::Empty
} else {
WaterSenseState::NotEmpty
})
.await;
}
pub async fn fill(&mut self, added_volume: f64, cx: &Context<Self>) {
if added_volume <= 0.0 {
return;
}
let was_empty = self.volume == 0.0;
self.volume += added_volume;
if let Some(state) = self.dynamic_state.take() {
state.set_empty_key.cancel();
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;
return;
}
if was_empty {
self.water_sense.send(WaterSenseState::NotEmpty).await;
}
}
pub async fn volume(&mut self) -> f64 {
self.volume
}
pub async fn set_flow_rate(&mut self, flow_rate: f64, cx: &Context<Self>) {
assert!(flow_rate >= 0.0);
let time = cx.time();
if let Some(state) = self.dynamic_state.take() {
state.set_empty_key.cancel();
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;
}
async fn schedule_empty(&mut self, flow_rate: f64, time: MonotonicTime, cx: &Context<Self>) {
let duration_until_empty = if self.volume == 0.0 {
0.0
} else {
self.volume / flow_rate
};
if duration_until_empty.is_infinite() {
return;
}
let duration_until_empty = Duration::from_secs_f64(duration_until_empty);
match cx.schedule_keyed_event(duration_until_empty, schedulable!(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(_) => {
self.volume = 0.0;
self.water_sense.send(WaterSenseState::Empty).await;
}
}
}
#[nexosim(schedulable)]
async fn set_empty(&mut self) {
self.volume = 0.0;
self.dynamic_state = None;
self.water_sense.send(WaterSenseState::Empty).await;
}
}
#[derive(Serialize, Deserialize)]
struct TankDynamicState {
last_volume_update: MonotonicTime,
set_empty_key: EventKey,
flow_rate: f64,
}
#[derive(Copy, Clone, Eq, Message, PartialEq, Serialize, Deserialize)]
pub enum WaterSenseState {
Empty,
NotEmpty,
}
#[allow(dead_code)]
fn main() -> Result<(), SimulationError> {
let pump_flow_rate = 4.5e-6;
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);
let pump_mbox = Mailbox::new();
let controller_mbox = Mailbox::new();
let tank_mbox = Mailbox::new();
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);
let mut bench = SimInit::new();
let brew_cmd = EventSource::new()
.connect(Controller::brew_cmd, &controller_mbox)
.register(&mut bench);
let brew_time = EventSource::new()
.connect(Controller::brew_time, &controller_mbox)
.register(&mut bench);
let tank_fill = EventSource::new()
.connect(Tank::fill, &tank_mbox)
.register(&mut bench);
let (sink, mut flow_rate) = event_slot(SinkState::Enabled);
pump.flow_rate.connect_sink(sink);
let t0 = MonotonicTime::EPOCH; let mut simu = bench
.add_model(controller, controller_mbox, "controller")
.add_model(pump, pump_mbox, "pump")
.add_model(tank, tank_mbox, "tank")
.init(t0)?;
let scheduler = simu.scheduler();
let mut t = t0;
assert_eq!(simu.time(), t);
simu.process_event(&brew_cmd, ())?;
assert_eq!(flow_rate.try_read(), Some(pump_flow_rate));
simu.step()?;
t += Controller::DEFAULT_BREW_TIME;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.try_read(), Some(0.0));
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; for _ in 0..(shots_per_tank - 1) {
simu.process_event(&brew_cmd, ())?;
assert_eq!(flow_rate.try_read(), Some(pump_flow_rate));
simu.step()?;
t += Controller::DEFAULT_BREW_TIME;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.try_read(), Some(0.0));
}
simu.process_event(&brew_cmd, ())?;
simu.step()?;
assert!(simu.time() < t + Controller::DEFAULT_BREW_TIME);
t = simu.time();
let last_flow_rate = flow_rate.try_read();
assert_eq!(last_flow_rate, Some(0.0));
simu.process_event(&brew_cmd, ())?;
assert!(flow_rate.try_read().is_none());
let brew_duration = Duration::new(30, 0);
simu.process_event(&brew_time, brew_duration)?;
simu.process_event(&tank_fill, 1.0e-3)?;
simu.process_event(&brew_cmd, ())?;
assert_eq!(flow_rate.try_read(), Some(pump_flow_rate));
simu.step()?;
t += brew_duration;
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.try_read(), Some(0.0));
scheduler
.schedule_event(Duration::from_secs(15), &brew_cmd, ())
.unwrap();
simu.process_event(&brew_cmd, ())?;
assert_eq!(flow_rate.try_read(), Some(pump_flow_rate));
simu.step()?;
t += Duration::from_secs(15);
assert_eq!(simu.time(), t);
assert_eq!(flow_rate.try_read(), Some(0.0));
Ok(())
}