forked from ROMEO/nexosim
Merge pull request #82 from asynchronics/feature-blocking-event-queue
Add blocking event queue.
This commit is contained in:
commit
0804ba714e
@ -5,3 +5,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nexosim = { version = "0.3.0", path = "../nexosim" }
|
nexosim = { version = "0.3.0", path = "../nexosim" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
@ -40,8 +40,9 @@ impl Default for Hk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Processor mode ID.
|
/// Processor mode ID.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub enum ModeId {
|
pub enum ModeId {
|
||||||
|
#[default]
|
||||||
Off,
|
Off,
|
||||||
Idle,
|
Idle,
|
||||||
Processing,
|
Processing,
|
||||||
|
274
nexosim-util/examples/simulation_driven.rs
Normal file
274
nexosim-util/examples/simulation_driven.rs
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
//! Example: a simulation that runs infinitely until stopped. This setup is
|
||||||
|
//! typical for hardware-in-the-loop use case. The test scenario is driven by
|
||||||
|
//! simulation events.
|
||||||
|
//!
|
||||||
|
//! This example demonstrates in particular:
|
||||||
|
//!
|
||||||
|
//! * infinite simulation,
|
||||||
|
//! * blocking event queue,
|
||||||
|
//! * simulation halting,
|
||||||
|
//! * system clock,
|
||||||
|
//! * periodic scheduling,
|
||||||
|
//! * observable state.
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
//! ┃ Simulation ┃
|
||||||
|
//! ┃ ┌──────────┐ ┌──────────┐mode ┃
|
||||||
|
//! ┃ │ │pulses │ ├──────╂┐BlockingEventQueue
|
||||||
|
//! ┃ │ Detector ├──────►│ Counter │count ┃├───────────────────►
|
||||||
|
//! ┃ │ │ │ ├──────╂┘
|
||||||
|
//! ┃ └──────────┘ └──────────┘ ┃
|
||||||
|
//! ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use nexosim::model::{Context, Model};
|
||||||
|
use nexosim::ports::{BlockingEventQueue, Output};
|
||||||
|
use nexosim::simulation::{ActionKey, ExecutionError, Mailbox, SimInit, SimulationError};
|
||||||
|
use nexosim::time::{AutoSystemClock, MonotonicTime};
|
||||||
|
use nexosim_util::helper_models::Ticker;
|
||||||
|
use nexosim_util::observables::ObservableValue;
|
||||||
|
|
||||||
|
const SWITCH_ON_DELAY: Duration = Duration::from_secs(1);
|
||||||
|
const MAX_PULSE_PERIOD: u64 = 100;
|
||||||
|
const TICK: Duration = Duration::from_millis(100);
|
||||||
|
const N: u64 = 10;
|
||||||
|
|
||||||
|
/// Counter mode.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum Mode {
|
||||||
|
#[default]
|
||||||
|
Off,
|
||||||
|
On,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulation event.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Event {
|
||||||
|
Mode(Mode),
|
||||||
|
Count(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Counter` Model.
|
||||||
|
pub struct Counter {
|
||||||
|
/// Operation mode.
|
||||||
|
pub mode: Output<Mode>,
|
||||||
|
|
||||||
|
/// Pulses count.
|
||||||
|
pub count: Output<u64>,
|
||||||
|
|
||||||
|
/// Internal state.
|
||||||
|
state: ObservableValue<Mode>,
|
||||||
|
|
||||||
|
/// Counter.
|
||||||
|
acc: ObservableValue<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
/// Creates a new `Counter` model.
|
||||||
|
fn new() -> Self {
|
||||||
|
let mode = Output::default();
|
||||||
|
let count = Output::default();
|
||||||
|
Self {
|
||||||
|
mode: mode.clone(),
|
||||||
|
count: count.clone(),
|
||||||
|
state: ObservableValue::new(mode),
|
||||||
|
acc: ObservableValue::new(count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Power -- input port.
|
||||||
|
pub async fn power_in(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||||
|
match *self.state {
|
||||||
|
Mode::Off if on => cx
|
||||||
|
.schedule_event(SWITCH_ON_DELAY, Self::switch_on, ())
|
||||||
|
.unwrap(),
|
||||||
|
Mode::On if !on => self.switch_off().await,
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pulse -- input port.
|
||||||
|
pub async fn pulse(&mut self) {
|
||||||
|
self.acc.modify(|x| *x += 1).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches `Counter` on.
|
||||||
|
async fn switch_on(&mut self) {
|
||||||
|
self.state.set(Mode::On).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches `Counter` off.
|
||||||
|
async fn switch_off(&mut self) {
|
||||||
|
self.state.set(Mode::Off).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for Counter {}
|
||||||
|
|
||||||
|
/// Detector model that produces pulses.
|
||||||
|
pub struct Detector {
|
||||||
|
/// Output pulse.
|
||||||
|
pub pulse: Output<()>,
|
||||||
|
|
||||||
|
/// `ActionKey` of the next scheduled detection.
|
||||||
|
next: Option<ActionKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Detector {
|
||||||
|
/// Creates a new `Detector` model.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pulse: Output::default(),
|
||||||
|
next: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches `Detector` on -- input port.
|
||||||
|
pub async fn switch_on(&mut self, _: (), cx: &mut Context<Self>) {
|
||||||
|
self.schedule_next(cx).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches `Detector` off -- input port.
|
||||||
|
pub async fn switch_off(&mut self) {
|
||||||
|
self.next = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a pulse.
|
||||||
|
///
|
||||||
|
/// Note: self-scheduling async methods must be for now defined with an
|
||||||
|
/// explicit signature instead of `async fn` due to a rustc issue.
|
||||||
|
fn pulse<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
_: (),
|
||||||
|
cx: &'a mut Context<Self>,
|
||||||
|
) -> impl Future<Output = ()> + Send + 'a {
|
||||||
|
async move {
|
||||||
|
self.pulse.send(()).await;
|
||||||
|
self.schedule_next(cx).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedules the next detection.
|
||||||
|
async fn schedule_next(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let next = {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
rng.gen_range(1..MAX_PULSE_PERIOD)
|
||||||
|
};
|
||||||
|
self.next = Some(
|
||||||
|
cx.schedule_keyed_event(Duration::from_millis(next), Self::pulse, ())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for Detector {}
|
||||||
|
|
||||||
|
fn main() -> Result<(), SimulationError> {
|
||||||
|
// ---------------
|
||||||
|
// Bench assembly.
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
// Models.
|
||||||
|
|
||||||
|
// The detector model that produces pulses.
|
||||||
|
let mut detector = Detector::new();
|
||||||
|
|
||||||
|
// The counter model.
|
||||||
|
let mut counter = Counter::new();
|
||||||
|
|
||||||
|
// The ticker model that keeps simulation alive.
|
||||||
|
let ticker = Ticker::new(TICK);
|
||||||
|
|
||||||
|
// Mailboxes.
|
||||||
|
let detector_mbox = Mailbox::new();
|
||||||
|
let counter_mbox = Mailbox::new();
|
||||||
|
let ticker_mbox = Mailbox::new();
|
||||||
|
|
||||||
|
// Connections.
|
||||||
|
detector.pulse.connect(Counter::pulse, &counter_mbox);
|
||||||
|
|
||||||
|
// Model handles for simulation.
|
||||||
|
let detector_addr = detector_mbox.address();
|
||||||
|
let counter_addr = counter_mbox.address();
|
||||||
|
let observer = BlockingEventQueue::new();
|
||||||
|
counter
|
||||||
|
.mode
|
||||||
|
.map_connect_sink(|m| Event::Mode(*m), &observer);
|
||||||
|
counter
|
||||||
|
.count
|
||||||
|
.map_connect_sink(|c| Event::Count(*c), &observer);
|
||||||
|
let mut observer = observer.into_reader();
|
||||||
|
|
||||||
|
// Start time (arbitrary since models do not depend on absolute time).
|
||||||
|
let t0 = MonotonicTime::EPOCH;
|
||||||
|
|
||||||
|
// Assembly and initialization.
|
||||||
|
let (mut simu, mut scheduler) = SimInit::new()
|
||||||
|
.add_model(detector, detector_mbox, "detector")
|
||||||
|
.add_model(counter, counter_mbox, "counter")
|
||||||
|
.add_model(ticker, ticker_mbox, "ticker")
|
||||||
|
.set_clock(AutoSystemClock::new())
|
||||||
|
.init(t0)?;
|
||||||
|
|
||||||
|
// Simulation thread.
|
||||||
|
let simulation_handle = thread::spawn(move || {
|
||||||
|
// ---------- Simulation. ----------
|
||||||
|
// Infinitely kept alive by the ticker model until halted.
|
||||||
|
simu.step_unbounded()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch the counter on.
|
||||||
|
scheduler.schedule_event(
|
||||||
|
Duration::from_millis(1),
|
||||||
|
Counter::power_in,
|
||||||
|
true,
|
||||||
|
counter_addr,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wait until counter mode is `On`.
|
||||||
|
loop {
|
||||||
|
let event = observer.next();
|
||||||
|
match event {
|
||||||
|
Some(Event::Mode(Mode::On)) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => panic!("Simulation exited unexpectedly"),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch the detector on.
|
||||||
|
scheduler.schedule_event(
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Detector::switch_on,
|
||||||
|
(),
|
||||||
|
detector_addr,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wait until `N` detections.
|
||||||
|
loop {
|
||||||
|
let event = observer.next();
|
||||||
|
match event {
|
||||||
|
Some(Event::Count(c)) if c >= N => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => panic!("Simulation exited unexpectedly"),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the simulation.
|
||||||
|
scheduler.halt();
|
||||||
|
match simulation_handle.join().unwrap() {
|
||||||
|
Err(ExecutionError::Halted) => Ok(()),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
35
nexosim-util/src/helper_models.rs
Normal file
35
nexosim-util/src/helper_models.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//! Helper models.
|
||||||
|
//!
|
||||||
|
//! This module contains helper models useful for simulation bench assembly.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use nexosim::model::{Context, InitializedModel, Model};
|
||||||
|
|
||||||
|
/// A ticker model.
|
||||||
|
///
|
||||||
|
/// This model self-schedules at the specified period, which can be used to keep
|
||||||
|
/// the simulation alive.
|
||||||
|
pub struct Ticker {
|
||||||
|
/// Tick period.
|
||||||
|
tick: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ticker {
|
||||||
|
/// Creates a new `Ticker` with the specified self-scheduling period.
|
||||||
|
pub fn new(tick: Duration) -> Self {
|
||||||
|
Self { tick }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Self-scheduled function.
|
||||||
|
async fn tick(&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for Ticker {
|
||||||
|
async fn init(self, cx: &mut Context<Self>) -> InitializedModel<Self> {
|
||||||
|
cx.schedule_periodic_event(self.tick, self.tick, Self::tick, ())
|
||||||
|
.unwrap();
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
pub mod combinators;
|
pub mod combinators;
|
||||||
|
pub mod helper_models;
|
||||||
pub mod observables;
|
pub mod observables;
|
||||||
|
@ -273,6 +273,9 @@ pub use input::markers;
|
|||||||
pub use input::{InputFn, ReplierFn};
|
pub use input::{InputFn, ReplierFn};
|
||||||
pub use output::{Output, Requestor, UniRequestor};
|
pub use output::{Output, Requestor, UniRequestor};
|
||||||
pub use sink::{
|
pub use sink::{
|
||||||
event_buffer::EventBuffer, event_slot::EventSlot, EventSink, EventSinkStream, EventSinkWriter,
|
blocking_event_queue::{BlockingEventQueue, BlockingEventQueueReader},
|
||||||
|
event_buffer::EventBuffer,
|
||||||
|
event_slot::EventSlot,
|
||||||
|
EventSink, EventSinkStream, EventSinkWriter,
|
||||||
};
|
};
|
||||||
pub use source::{EventSource, QuerySource, ReplyReceiver};
|
pub use source::{EventSource, QuerySource, ReplyReceiver};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub(crate) mod blocking_event_queue;
|
||||||
pub(crate) mod event_buffer;
|
pub(crate) mod event_buffer;
|
||||||
pub(crate) mod event_slot;
|
pub(crate) mod event_slot;
|
||||||
|
|
||||||
|
145
nexosim/src/ports/sink/blocking_event_queue.rs
Normal file
145
nexosim/src/ports/sink/blocking_event_queue.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::{EventSink, EventSinkStream, EventSinkWriter};
|
||||||
|
|
||||||
|
/// A blocking event queue with an unbounded size.
|
||||||
|
///
|
||||||
|
/// Implements [`EventSink`].
|
||||||
|
///
|
||||||
|
/// Note that [`EventSinkStream`] is implemented by
|
||||||
|
/// [`BlockingEventQueueReader`], created with the
|
||||||
|
/// [`BlockingEventQueue::into_reader`] method.
|
||||||
|
pub struct BlockingEventQueue<T> {
|
||||||
|
is_open: Arc<AtomicBool>,
|
||||||
|
sender: Sender<T>,
|
||||||
|
receiver: Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BlockingEventQueue<T> {
|
||||||
|
/// Creates an open `BlockingEventQueue`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::new_with_state(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a closed `BlockingEventQueue`.
|
||||||
|
pub fn new_closed() -> Self {
|
||||||
|
Self::new_with_state(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a consumer handle.
|
||||||
|
pub fn into_reader(self) -> BlockingEventQueueReader<T> {
|
||||||
|
BlockingEventQueueReader {
|
||||||
|
is_open: self.is_open,
|
||||||
|
receiver: self.receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `BlockingEventQueue` in the specified state.
|
||||||
|
fn new_with_state(is_open: bool) -> Self {
|
||||||
|
let (sender, receiver) = channel();
|
||||||
|
Self {
|
||||||
|
is_open: Arc::new(AtomicBool::new(is_open)),
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> EventSink<T> for BlockingEventQueue<T> {
|
||||||
|
type Writer = BlockingEventQueueWriter<T>;
|
||||||
|
|
||||||
|
fn writer(&self) -> Self::Writer {
|
||||||
|
BlockingEventQueueWriter {
|
||||||
|
is_open: self.is_open.clone(),
|
||||||
|
sender: self.sender.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for BlockingEventQueue<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for BlockingEventQueue<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("BlockingEventQueue").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A consumer handle of a `BlockingEventQueue`.
|
||||||
|
///
|
||||||
|
/// Implements [`EventSinkStream`]. Calls to the iterator's `next` method are
|
||||||
|
/// blocking. `None` is returned when all writer handles have been dropped.
|
||||||
|
pub struct BlockingEventQueueReader<T> {
|
||||||
|
is_open: Arc<AtomicBool>,
|
||||||
|
receiver: Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Iterator for BlockingEventQueueReader<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.receiver.recv() {
|
||||||
|
Ok(event) => Some(event),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FusedIterator for BlockingEventQueueReader<T> {}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> EventSinkStream for BlockingEventQueueReader<T> {
|
||||||
|
fn open(&mut self) {
|
||||||
|
self.is_open.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.is_open.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for BlockingEventQueueReader<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("BlockingEventQueueReader")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A producer handle of a `BlockingEventQueue`.
|
||||||
|
pub struct BlockingEventQueueWriter<T> {
|
||||||
|
is_open: Arc<AtomicBool>,
|
||||||
|
sender: Sender<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> EventSinkWriter<T> for BlockingEventQueueWriter<T> {
|
||||||
|
/// Pushes an event onto the queue.
|
||||||
|
fn write(&self, event: T) {
|
||||||
|
if !self.is_open.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ignore sending failure.
|
||||||
|
let _ = self.sender.send(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for BlockingEventQueueWriter<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
is_open: self.is_open.clone(),
|
||||||
|
sender: self.sender.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for BlockingEventQueueWriter<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct("BlockingEventQueueWriter")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user