//! 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(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 transaction 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); } } } } #[rustversion::before(1.63)] fn loopback_split<'a>( _port: &mut Box, _read_stats: &mut Stats<'a>, _write_stats: &mut Stats<'a>, ) { unimplemented!("requires Rust 1.63 or later"); } #[rustversion::since(1.63)] 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(); } }); }