From f8c0bdadf3bef443447befbdf20f043dc89cdaef Mon Sep 17 00:00:00 2001 From: mlsvrts Date: Sun, 6 Nov 2022 12:32:06 -0700 Subject: [PATCH 1/2] Add a loopback test example Adds a new example with support for running loopback tests on a port and printing some timing statistics. Loopback test supports running in split or standard serial port modes, and specifiying the data and iterations of test to perform. Signed-off-by: mlsvrts --- Cargo.toml | 2 +- examples/loopback.rs | 262 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 examples/loopback.rs diff --git a/Cargo.toml b/Cargo.toml index 98092a75..7c1404e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ features = [ serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] -clap = "3.1.6" +clap = { version = "3.1.6", features = ["derive"] } [features] default = ["libudev"] diff --git a/examples/loopback.rs b/examples/loopback.rs new file mode 100644 index 00000000..0aebc5ff --- /dev/null +++ b/examples/loopback.rs @@ -0,0 +1,262 @@ +//! This example performs a loopback test using real hardware ports +//! +//! Additionally, some data will be collected and logged during the test to provide some +//! rudimentary benchmarking information. When 'split-port' is specified, the serial port will +//! be split into two channels that read/write "simultaneously" from multiple threads. +//! +//! You can also provide the length (in bytes) of data to test with, and the number of iterations to perform or +//! a list of raw bytes to transmit. +//! +//! To run this example: +//! +//! 1) `cargo run --example loopback /dev/ttyUSB0` +//! +//! 2) `cargo run --example loopback /dev/ttyUSB0 --split-port` +//! +//! 3) `cargo run --example loopback /dev/ttyUSB0 -i 100 -l 32 -b 9600` +//! +//! 4) `cargo run --example loopback /dev/ttyUSB8 --bytes 222,173,190,239` + +use std::time::{Duration, Instant}; + +use clap::Parser; +use serialport::SerialPort; + +/// Serialport Example - Loopback +#[derive(Parser)] +struct Args { + /// The device path to a serialport + port: String, + + /// The number of read/write iterations to perform + #[clap(short, long, default_value = "100")] + iterations: usize, + + /// The number of bytes written per transaction + /// + /// Ignored when bytes are passed directly from the command-line + #[clap(short, long, default_value = "8")] + length: usize, + + /// The baudrate to open the port with + #[clap(short, long, default_value = "115200")] + baudrate: u32, + + /// Bytes to write to the serial port + /// + /// When not specified, the bytes transmitted count up + #[clap(short, long, use_value_delimiter = true)] + bytes: Option>, + + /// Split the port to read/write from multiple threads + #[clap(long)] + split_port: bool, +} + +fn main() { + let args = Args::parse(); + + // Open the serial port + let mut port = match serialport::new(&args.port, args.baudrate) + .timeout(Duration::MAX) + .open() + { + Err(e) => { + eprintln!("Failed to open \"{}\". Error: {}", args.port, e); + ::std::process::exit(1); + } + Ok(p) => p, + }; + + // Setup stat-tracking + let length = args.length; + let data: Vec = args + .bytes + .unwrap_or_else(|| (0..length).map(|i| i as u8).collect()); + + let (mut read_stats, mut write_stats) = Stats::new(args.iterations, &data); + + // Run the tests + if args.split_port { + loopback_split(&mut port, &mut read_stats, &mut write_stats); + } else { + loopback_standard(&mut port, &mut read_stats, &mut write_stats); + } + + // Print the results + println!("Loopback {}:", args.port); + println!(" data-length: {} bytes", read_stats.data.len()); + println!(" iterations: {}", read_stats.iterations); + println!(" read:"); + println!(" total: {:.6}s", read_stats.total()); + println!(" average: {:.6}s", read_stats.average()); + println!(" max: {:.6}s", read_stats.max()); + println!(" write:"); + println!(" total: {:.6}s", write_stats.total()); + println!(" average: {:.6}s", write_stats.average()); + println!(" max: {:.6}s", write_stats.max()); + println!(" total: {:.6}s", read_stats.total() + write_stats.total()); + println!( + " bytes/s: {:.6}", + (read_stats.data.len() as f32) / (read_stats.average() + write_stats.average()) + ) +} + +/// Capture read/write times to calculate average durations +#[derive(Clone)] +struct Stats<'a> { + pub data: &'a [u8], + pub times: Vec, + pub iterations: usize, + now: Instant, +} + +impl<'a> Stats<'a> { + /// Create new read/write stats + fn new(iterations: usize, data: &'a [u8]) -> (Self, Self) { + ( + Self { + data, + times: Vec::with_capacity(iterations), + iterations, + now: Instant::now(), + }, + Self { + data, + times: Vec::with_capacity(iterations), + iterations, + now: Instant::now(), + }, + ) + } + + /// Start a duration timer + fn start(&mut self) { + self.now = Instant::now(); + } + + /// Store a duration + fn stop(&mut self) { + self.times.push(self.now.elapsed()); + } + + /// Provides the total time elapsed + fn total(&self) -> f32 { + self.times.iter().map(|d| d.as_secs_f32()).sum() + } + + /// Provides average time per transaction + fn average(&self) -> f32 { + self.total() / (self.times.len() as f32) + } + + /// Provides the maximum transation time + fn max(&self) -> f32 { + self.times + .iter() + .max() + .map(|d| d.as_secs_f32()) + .unwrap_or(0.0) + } +} + +fn loopback_standard<'a>( + port: &mut Box, + read_stats: &mut Stats<'a>, + write_stats: &mut Stats<'a>, +) { + let mut buf = vec![0u8; read_stats.data.len()]; + + for _ in 0..read_stats.iterations { + // Write data to the port + write_stats.start(); + port.write_all(write_stats.data) + .expect("failed to write to serialport"); + write_stats.stop(); + + // Read data back from the port + read_stats.start(); + port.read_exact(&mut buf) + .expect("failed to read from serialport"); + read_stats.stop(); + + // Crash on error + for (i, x) in buf.iter().enumerate() { + if read_stats.data[i] != *x { + eprintln!( + "Expected byte '{:02X}' but got '{:02X}'", + read_stats.data[i], x + ); + ::std::process::exit(2); + } + } + } +} + +fn loopback_split<'a>( + port: &mut Box, + read_stats: &mut Stats<'a>, + write_stats: &mut Stats<'a>, +) { + let mut buf = vec![0u8; read_stats.data.len()]; + let mut rport = match port.try_clone() { + Ok(p) => p, + Err(e) => { + eprintln!("Failed to clone port: {}", e); + ::std::process::exit(3); + } + }; + + // Manage threads for read/writing; port usage is not async, so threads can easily deadlock: + // + // 1. Read Thread: Park -> Read -> Unpark Write ──────┐ + // └──────────────────────────────────┘ + // 2. Write Thread: Write -> Unpark Read -> Park ──────┐ + // └──────────────────────────────────┘ + std::thread::scope(|scope| { + // Get handle for writing thread + let wr_thread = std::thread::current(); + + // Spawn a thread that reads data for n iterations + let handle = scope.spawn(move || { + for _ in 0..read_stats.iterations { + // Wait for the write to complete + std::thread::park(); + + read_stats.start(); + rport + .read_exact(&mut buf) + .expect("failed to read from serialport"); + read_stats.stop(); + + // Crash on error + for (i, x) in buf.iter().enumerate() { + if read_stats.data[i] != *x { + eprintln!( + "Expected byte '{:02X}' but got '{:02X}'", + read_stats.data[i], x + ); + ::std::process::exit(2); + } + } + + // Allow the writing thread to start + wr_thread.unpark(); + } + }); + + // Write data to the port for n iterations + for _ in 0..write_stats.iterations { + write_stats.start(); + port.write_all(write_stats.data) + .expect("failed to write to serialport"); + write_stats.stop(); + + // Notify that the write completed + handle.thread().unpark(); + + // Wait for read to complete + std::thread::park(); + } + }); +} From 774493f6672a363e9dbaab508f466c12925bf04d Mon Sep 17 00:00:00 2001 From: mlsvrts Date: Sun, 4 Dec 2022 14:43:51 -0700 Subject: [PATCH 2/2] Remove 'short' from loopback test 'bytes' argument. Supporting '-b' for both 'bytes' and 'baudrate' could cause a collision, this update prevents the issue. Signed-off-by: mlsvrts --- examples/loopback.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/loopback.rs b/examples/loopback.rs index 0aebc5ff..ba753e82 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -45,7 +45,7 @@ struct Args { /// Bytes to write to the serial port /// /// When not specified, the bytes transmitted count up - #[clap(short, long, use_value_delimiter = true)] + #[clap(long, use_value_delimiter = true)] bytes: Option>, /// Split the port to read/write from multiple threads