blob: dc519345f218ff70f88021dbbe2a329cf08a7290 [file] [log] [blame]
//! 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<Vec<u8>>,
/// 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<u8> = 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<Duration>,
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<dyn SerialPort>,
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<dyn SerialPort>,
_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<dyn SerialPort>,
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();
}
});
}