forked from ROMEO/nexosim
Introduce ProtoModel trait, remove Model::setup
The external_input example has been as well adapted and (at least temporarily) simplifiedi/modified to remove the dependencies on `atomic_wait` and `mio`.
This commit is contained in:
@ -3,27 +3,29 @@
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * model prototypes,
|
||||
//! * submodels,
|
||||
//! * outputs cloning,
|
||||
//! * self-scheduling methods,
|
||||
//! * model setup,
|
||||
//! * model initialization,
|
||||
//! * simulation monitoring with event streams.
|
||||
//! * simulation monitoring with buffered event sinks.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌──────────────────────────────────────────────┐
|
||||
//! │ Assembly │
|
||||
//! │ ┌──────────┐ ┌──────────┐ │
|
||||
//! PPS │ │ │ coil currents │ │ │position
|
||||
//! Pulse rate ●───────►│──►│ Driver ├───────────────►│ Motor ├──►│─────────►
|
||||
//! (±freq)│ │ │ (IA, IB) │ │ │(0:199)
|
||||
//! │ └──────────┘ └──────────┘ │
|
||||
//! └──────────────────────────────────────────────┘
|
||||
//! ┌────────────────────────────────────────────┐
|
||||
//! │ Assembly │
|
||||
//! │ ┌──────────┐ │
|
||||
//! PPS │ │ │ coil currents ┌─────────┐ │
|
||||
//! Pulse rate ●──────────┼──►│ Driver ├───────────────►│ │ │
|
||||
//! (±freq) │ │ │ (IA, IB) │ │ │ position
|
||||
//! │ └──────────┘ │ Motor ├──┼──────────►
|
||||
//! torque │ │ │ │ (0:199)
|
||||
//! Load ●──────────┼──────────────────────────────►│ │ │
|
||||
//! │ └─────────┘ │
|
||||
//! └────────────────────────────────────────────┘
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use asynchronix::model::{Model, SetupContext};
|
||||
use asynchronix::model::{BuildContext, Model, ProtoModel};
|
||||
use asynchronix::ports::{EventBuffer, Output};
|
||||
use asynchronix::simulation::{Mailbox, SimInit, SimulationError};
|
||||
use asynchronix::time::MonotonicTime;
|
||||
@ -32,36 +34,59 @@ mod stepper_motor;
|
||||
|
||||
pub use stepper_motor::{Driver, Motor};
|
||||
|
||||
pub struct MotorAssembly {
|
||||
/// A prototype for `MotorAssembly`.
|
||||
pub struct ProtoMotorAssembly {
|
||||
pub position: Output<u16>,
|
||||
init_pos: u16,
|
||||
load: Output<f64>,
|
||||
pps: Output<f64>,
|
||||
}
|
||||
|
||||
impl MotorAssembly {
|
||||
impl ProtoMotorAssembly {
|
||||
/// The prototype has a public constructor.
|
||||
pub fn new(init_pos: u16) -> Self {
|
||||
Self {
|
||||
position: Default::default(),
|
||||
init_pos,
|
||||
load: Default::default(),
|
||||
pps: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the pulse rate (sign = direction) [Hz] -- input port.
|
||||
// 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;
|
||||
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;
|
||||
self.load.send(torque).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for MotorAssembly {
|
||||
fn setup(&mut self, setup_context: &SetupContext<Self>) {
|
||||
impl Model for MotorAssembly {}
|
||||
|
||||
impl ProtoModel for ProtoMotorAssembly {
|
||||
type Model = MotorAssembly;
|
||||
|
||||
fn build(self, ctx: &BuildContext<Self>) -> MotorAssembly {
|
||||
let mut assembly = MotorAssembly::new();
|
||||
let mut motor = Motor::new(self.init_pos);
|
||||
let mut driver = Driver::new(1.0);
|
||||
|
||||
@ -70,17 +95,20 @@ impl Model for MotorAssembly {
|
||||
let driver_mbox = Mailbox::new();
|
||||
|
||||
// Connections.
|
||||
self.pps.connect(Driver::pulse_rate, &driver_mbox);
|
||||
self.load.connect(Motor::load, &motor_mbox);
|
||||
assembly.pps.connect(Driver::pulse_rate, &driver_mbox);
|
||||
assembly.load.connect(Motor::load, &motor_mbox);
|
||||
driver.current_out.connect(Motor::current_in, &motor_mbox);
|
||||
// Note: it is important to clone `position` from the parent to the
|
||||
// submodel so that all connections made by the user to the parent model
|
||||
// are preserved. Connections added after cloning are reflected in all
|
||||
// clones.
|
||||
motor.position = self.position.clone();
|
||||
|
||||
setup_context.add_model(driver, driver_mbox, "driver");
|
||||
setup_context.add_model(motor, motor_mbox, "motor");
|
||||
// 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.
|
||||
ctx.add_submodel(driver, driver_mbox, "driver");
|
||||
ctx.add_submodel(motor, motor_mbox, "motor");
|
||||
|
||||
assembly
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +119,7 @@ fn main() -> Result<(), SimulationError> {
|
||||
|
||||
// Models.
|
||||
let init_pos = 123;
|
||||
let mut assembly = MotorAssembly::new(init_pos);
|
||||
let mut assembly = ProtoMotorAssembly::new(init_pos);
|
||||
|
||||
// Mailboxes.
|
||||
let assembly_mbox = Mailbox::new();
|
||||
|
@ -1,80 +1,97 @@
|
||||
//! Example: a model that reads data from the external world.
|
||||
//! Example: a model that reads data external to the simulation.
|
||||
//!
|
||||
//! This example demonstrates in particular:
|
||||
//!
|
||||
//! * external world inputs (useful in cosimulation),
|
||||
//! * processing of external inputs (useful in co-simulation),
|
||||
//! * system clock,
|
||||
//! * periodic scheduling.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌────────────────────────────────┐
|
||||
//! │ Simulation │
|
||||
//! ┌────────────┐ ┌────────────┐ │ ┌──────────┐ │
|
||||
//! │ │ UDP │ │ message │ message │ │ message │ ┌─────────────┐
|
||||
//! │ UDP Client ├─────────►│ UDP Server ├──────────►├─────────►│ Listener ├─────────►├──►│ EventBuffer │
|
||||
//! │ │ message │ │ │ │ │ │ └─────────────┘
|
||||
//! └────────────┘ └────────────┘ │ └──────────┘ │
|
||||
//! └────────────────────────────────┘
|
||||
//! ┏━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
//! ┃ Simulation ┃
|
||||
//! ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┌╌╌╌╌╌╌╌╌╌╌╌╌┐ ┃ ┌──────────┐ ┃
|
||||
//! ┆ ┆ message ┆ ┆ message ┃ │ │ message ┃
|
||||
//! ┆ UDP Client ├╌╌╌╌╌╌╌╌►┆ UDP Server ├╌╌╌╌╌╌╌╌╌╌╌╂╌╌►│ Listener ├─────────╂─►
|
||||
//! ┆ ┆ [UDP] ┆ ┆ [channel] ┃ │ │ ┃
|
||||
//! └╌╌╌╌╌╌╌╌╌╌╌╌┘ └╌╌╌╌╌╌╌╌╌╌╌╌┘ ┃ └──────────┘ ┃
|
||||
//! ┗━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//! ```
|
||||
|
||||
use std::io::ErrorKind;
|
||||
use std::net::UdpSocket;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::net::{Ipv4Addr, UdpSocket};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread::{self, sleep, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
use atomic_wait::{wait, wake_one};
|
||||
|
||||
use mio::net::UdpSocket as MioUdpSocket;
|
||||
use mio::{Events, Interest, Poll, Token};
|
||||
|
||||
use asynchronix::model::{Context, InitializedModel, Model, SetupContext};
|
||||
use asynchronix::model::{BuildContext, Context, InitializedModel, Model, ProtoModel};
|
||||
use asynchronix::ports::{EventBuffer, Output};
|
||||
use asynchronix::simulation::{Mailbox, SimInit, SimulationError};
|
||||
use asynchronix::time::{AutoSystemClock, MonotonicTime};
|
||||
|
||||
const DELTA: Duration = Duration::from_millis(2);
|
||||
const PERIOD: Duration = Duration::from_millis(20);
|
||||
const N: u32 = 10;
|
||||
const SENDER: &str = "127.0.0.1:8000";
|
||||
const RECEIVER: &str = "127.0.0.1:9000";
|
||||
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);
|
||||
|
||||
/// Model that receives external input.
|
||||
pub struct Listener {
|
||||
/// 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, _: &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>,
|
||||
|
||||
/// External sender.
|
||||
tx: Option<Sender<String>>,
|
||||
|
||||
/// Synchronization with client.
|
||||
start: Arc<AtomicU32>,
|
||||
|
||||
/// Synchronization with simulation.
|
||||
stop: Arc<AtomicBool>,
|
||||
|
||||
/// Handle to UDP Server.
|
||||
external_handle: Option<JoinHandle<()>>,
|
||||
server_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
/// Creates a Listener.
|
||||
pub fn new(start: Arc<AtomicU32>) -> Self {
|
||||
start.store(0, Ordering::Relaxed);
|
||||
|
||||
let (tx, rx) = channel();
|
||||
pub fn new(
|
||||
message: Output<String>,
|
||||
rx: Receiver<String>,
|
||||
server_handle: JoinHandle<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: Output::default(),
|
||||
message,
|
||||
rx,
|
||||
tx: Some(tx),
|
||||
start,
|
||||
stop: Arc::new(AtomicBool::new(false)),
|
||||
external_handle: None,
|
||||
server_handle: Some(server_handle),
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,82 +102,39 @@ impl Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP server.
|
||||
///
|
||||
/// Code is based on the MIO UDP example.
|
||||
fn listener(tx: Sender<String>, start: Arc<AtomicU32>, stop: Arc<AtomicBool>) {
|
||||
const UDP_SOCKET: Token = Token(0);
|
||||
let mut poll = Poll::new().unwrap();
|
||||
let mut events = Events::with_capacity(10);
|
||||
let mut socket = MioUdpSocket::bind(RECEIVER.parse().unwrap()).unwrap();
|
||||
poll.registry()
|
||||
.register(&mut socket, UDP_SOCKET, Interest::READABLE)
|
||||
.unwrap();
|
||||
/// 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.store(1, Ordering::Relaxed);
|
||||
wake_one(&*start);
|
||||
start.notify();
|
||||
|
||||
'process: loop {
|
||||
// Wait for UDP packet or end of simulation.
|
||||
if let Err(err) = poll.poll(&mut events, Some(Duration::from_secs(1))) {
|
||||
if err.kind() == ErrorKind::Interrupted {
|
||||
// Exit if simulation is finished.
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
break 'process;
|
||||
}
|
||||
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 'process;
|
||||
}
|
||||
|
||||
for event in events.iter() {
|
||||
match event.token() {
|
||||
UDP_SOCKET => loop {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((packet_size, _)) => {
|
||||
if let Ok(message) = std::str::from_utf8(&buf[..packet_size]) {
|
||||
// Inject external message into simulation.
|
||||
if tx.send(message.into()).is_err() {
|
||||
break 'process;
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
break 'process;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
panic!("Got event for unexpected token: {:?}", event);
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Exit if simulation is finished.
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
break 'process;
|
||||
}
|
||||
}
|
||||
|
||||
poll.registry().deregister(&mut socket).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Listener {
|
||||
/// Start UDP Server on model setup.
|
||||
fn setup(&mut self, _: &SetupContext<Self>) {
|
||||
let tx = self.tx.take().unwrap();
|
||||
let start = Arc::clone(&self.start);
|
||||
let stop = Arc::clone(&self.stop);
|
||||
self.external_handle = Some(thread::spawn(move || {
|
||||
Self::listener(tx, start, stop);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Initialize model.
|
||||
async fn init(self, context: &Context<Self>) -> InitializedModel<Self> {
|
||||
// Schedule periodic function that processes external events.
|
||||
@ -174,13 +148,40 @@ impl Model for Listener {
|
||||
}
|
||||
|
||||
impl Drop for Listener {
|
||||
/// Notify UDP Server that simulation is over and wait for server shutdown.
|
||||
/// Wait for UDP Server shutdown.
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
let handle = self.external_handle.take();
|
||||
if let Some(handle) = handle {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,16 +192,17 @@ fn main() -> Result<(), SimulationError> {
|
||||
|
||||
// Models.
|
||||
|
||||
// Client-server synchronization.
|
||||
let start = Arc::new(AtomicU32::new(0));
|
||||
// Synchronization barrier for the UDP client.
|
||||
let start = WaitBarrier::new();
|
||||
|
||||
let mut listener = Listener::new(Arc::clone(&start));
|
||||
// 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::new();
|
||||
let mut message = EventBuffer::with_capacity(N + 1);
|
||||
listener.message.connect_sink(&message);
|
||||
|
||||
// Start time (arbitrary since models do not depend on absolute time).
|
||||
@ -218,32 +220,39 @@ fn main() -> Result<(), SimulationError> {
|
||||
|
||||
// External client that sends UDP messages.
|
||||
let sender_handle = thread::spawn(move || {
|
||||
// Wait until UDP Server is ready.
|
||||
wait(&start, 0);
|
||||
let socket = UdpSocket::bind(SENDER).unwrap();
|
||||
|
||||
// Wait until the UDP Server is ready.
|
||||
start.wait();
|
||||
|
||||
for i in 0..N {
|
||||
let socket = UdpSocket::bind(SENDER).unwrap();
|
||||
socket.send_to(i.to_string().as_bytes(), RECEIVER).unwrap();
|
||||
if i % 3 == 0 {
|
||||
sleep(PERIOD * i)
|
||||
sleep(PERIOD * i as u32)
|
||||
}
|
||||
}
|
||||
|
||||
socket
|
||||
});
|
||||
|
||||
// Advance simulation, external messages will be collected.
|
||||
simu.step_by(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 {
|
||||
// UDP can reorder packages, we are expecting that on not too loaded
|
||||
// localhost packages would not be dropped
|
||||
// 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);
|
||||
|
||||
sender_handle.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -8,19 +8,19 @@
|
||||
//! ```text
|
||||
//! ┌────────┐
|
||||
//! │ │
|
||||
//! ┌───►│ Load ├───► Power
|
||||
//! ┌──◄►│ Load ├───► Power
|
||||
//! │ │ │
|
||||
//! │ └────────┘
|
||||
//! │
|
||||
//! │ ┌────────┐
|
||||
//! │ │ │
|
||||
//! ├───►│ Load ├───► Power
|
||||
//! ├──◄►│ Load ├───► Power
|
||||
//! │ │ │
|
||||
//! │ └────────┘
|
||||
//! │
|
||||
//! │ ┌────────┐
|
||||
//! ┌──────────┐ voltage► │ │ │
|
||||
//! Voltage setting ●────►│ │◄────────────┴───►│ Load ├───► Power
|
||||
//! Voltage setting ●────►│ │►◄───────────┴──◄►│ Load ├───► Power
|
||||
//! │ Power │ ◄current │ │
|
||||
//! │ supply │ └────────┘
|
||||
//! │ ├───────────────────────────────► Total power
|
||||
|
@ -4,14 +4,17 @@
|
||||
//!
|
||||
//! * self-scheduling methods,
|
||||
//! * model initialization,
|
||||
//! * simulation monitoring with event streams.
|
||||
//! * simulation monitoring with buffered event sinks.
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌──────────┐ ┌──────────┐
|
||||
//! PPS │ │ coil currents │ │ position
|
||||
//! Pulse rate ●─────────►│ Driver ├───────────────►│ Motor ├──────────►
|
||||
//! (±freq) │ │ (IA, IB) │ │ (0:199)
|
||||
//! └──────────┘ └──────────┘
|
||||
//! ┌──────────┐
|
||||
//! PPS │ │ coil currents ┌─────────┐
|
||||
//! Pulse rate ●─────────►│ Driver ├───────────────►│ │
|
||||
//! (±freq) │ │ (IA, IB) │ │ position
|
||||
//! └──────────┘ │ Motor ├──────────►
|
||||
//! torque │ │ (0:199)
|
||||
//! Load ●─────────────────────────────────────►│ │
|
||||
//! └─────────┘
|
||||
//! ```
|
||||
|
||||
use std::future::Future;
|
||||
@ -136,7 +139,7 @@ impl Driver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the pulse rate (sign = direction) [Hz] -- input port.
|
||||
/// Pulse rate (sign = direction) [Hz] -- input port.
|
||||
pub async fn pulse_rate(&mut self, pps: f64, context: &Context<Self>) {
|
||||
println!(
|
||||
"Model instance {} at time {}: setting pps: {:.2}",
|
||||
|
Reference in New Issue
Block a user