Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
"Jesse Braham <[email protected]>",
]
edition = "2021"
rust-version = "1.59.0"
rust-version = "1.63.0"
description = "A cross-platform low-level serial port library."
documentation = "https://docs.rs/serialport"
repository = "https://github.com/serialport/serialport-rs"
Expand Down
86 changes: 86 additions & 0 deletions examples/reception_raw_blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Shows a low-level approach where VMIN, VTIME and raw reads are used for blocking reads.
#[cfg(unix)]
fn main() {
use serialport::{SerialPort, TTYPort};
use std::io::{self, Write};
use std::os::unix::prelude::*;
use std::time::Duration;

let (mut master, mut slave) = TTYPort::pair().expect("Unable to create pseudo-terminal pair");

// Master ptty has no associated path on the filesystem.
println!(
"Master ptty fd: {}, path: {:?}",
master.as_raw_fd(),
master.name()
);
println!(
"Slave ptty fd: {}, path: {:?}",
slave.as_raw_fd(),
slave.name()
);

std::thread::scope(|s| {
let jh_master = s.spawn(move || {
std::thread::sleep(Duration::from_millis(100));
master.write(b"hello slave\n").unwrap();
let mut serial_buf: Vec<u8> = vec![0; 1000];
loop {
match master.read_raw(serial_buf.as_mut_slice()) {
Ok(t) => {
if t == 0 {
std::thread::sleep(Duration::from_millis(100));
continue;
}

println!("received {t} bytes on master:");
io::stdout().write_all(&serial_buf[..t]).unwrap();
io::stdout().flush().unwrap();
assert_eq!(&serial_buf[..t], b"hello master\n");
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
panic!("unexpected timeout error");
}
Err(e) => eprintln!("{:?}", e),
}
}
});
let jh_slave = s.spawn(move || {
slave
.set_read_mode(serialport::ReadMode::Blocking {
inter_byte_timeout: 10,
minimal_bytes: 1,
})
.unwrap();
let mut serial_buf: Vec<u8> = vec![0; 1000];
let now = std::time::Instant::now();
loop {
match slave.read_raw(serial_buf.as_mut_slice()) {
Ok(t) => {
let elapsed = now.elapsed();
// The read is delayed by at least 100ms.
assert!(
elapsed >= Duration::from_millis(100),
"read returned too quickly: {:?}",
elapsed
);
println!("received {t} bytes on slave:");
io::stdout().write_all(&serial_buf[..t]).unwrap();
io::stdout().flush().unwrap();
assert_eq!(&serial_buf[..t], b"hello slave\n");
slave.write(b"hello master\n").unwrap();
slave.flush().ok();
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
panic!("unexpected timeout error");
}
Err(e) => eprintln!("{:?}", e),
}
}
});
jh_master.join().unwrap();
jh_slave.join().unwrap();
});
}
106 changes: 106 additions & 0 deletions examples/reception_raw_non_blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Shows a low-level approach where VMIN, VTIME and raw reads are used for non-blocking reads.
#[cfg(unix)]
fn main() {
use serialport::{SerialPort, TTYPort};
use std::io::{self, Write};
use std::os::unix::prelude::*;
use std::sync::atomic::AtomicBool;
use std::time::Duration;

static MASTER_DONE: AtomicBool = AtomicBool::new(false);

let (mut master, mut slave) = TTYPort::pair().expect("Unable to create pseudo-terminal pair");

// Master ptty has no associated path on the filesystem.
println!(
"Master ptty fd: {}, path: {:?}",
master.as_raw_fd(),
master.name()
);
println!(
"Slave ptty fd: {}, path: {:?}",
slave.as_raw_fd(),
slave.name()
);
std::thread::scope(|s| {
let jh_master = s.spawn(move || {
let mut serial_buf: Vec<u8> = vec![0; 1000];
std::thread::sleep(Duration::from_millis(50));
master.write(b"hello slave\n").unwrap();
master.flush().ok();
loop {
match master.read(&mut serial_buf) {
Ok(t) => {
println!("received {t} bytes on master:");
assert_eq!(&serial_buf[..t], b"hello master\n");
io::stdout().write_all(&serial_buf[..t]).unwrap();
io::stdout().flush().unwrap();
MASTER_DONE.store(true, std::sync::atomic::Ordering::Relaxed);
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
std::thread::sleep(Duration::from_millis(20));
},
Err(e) => {
panic!("master error: {:?}", e);
}
}
}
});
let jh_slave = s.spawn(move || {
slave
.set_read_mode(serialport::ReadMode::Immediate)
.unwrap();
let now = std::time::Instant::now();
let mut serial_buf: Vec<u8> = vec![0; 1000];

// This is non-blocking.
let read_result = slave.read_raw(&mut serial_buf);
assert!(read_result.is_ok(), "non-blocking read should not throw error");
assert!(now.elapsed() < Duration::from_millis(1), "slave read has blocked");
assert_eq!(read_result.unwrap(), 0, "no data should be available");

let mut current_write_index = 0;
let now = std::time::Instant::now();
loop {
match slave.read_raw(&mut serial_buf[current_write_index..]) {
Ok(t) => {
if t > 0 {
current_write_index += t;
} else {
std::thread::sleep(Duration::from_millis(20));
}
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
// This is the difference to a regular read: Reads will return Ok(0)
// instead of this error.
panic!("unexpected timeout error");
}
Err(e) => {
panic!("slave error: {:?}", e);
}
}
if now.elapsed() > Duration::from_millis(100) {
println!("received {current_write_index} bytes on slave:");
io::stdout()
.write_all(&serial_buf[..current_write_index])
.unwrap();
io::stdout().flush().unwrap();
assert_eq!(
&serial_buf[0..current_write_index],
"hello slave\n".as_bytes()
);
slave.write(b"hello master\n").unwrap();
slave.flush().ok();
break;
}
}
// Need to keep the slave alive to avoid pipe errors.
while !MASTER_DONE.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(10));
}
});
jh_master.join().unwrap();
jh_slave.join().unwrap();
});
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use std::time::Duration;
#[cfg(unix)]
mod posix;
#[cfg(unix)]
pub use posix::{BreakDuration, TTYPort};
pub use posix::{BreakDuration, ReadMode, TTYPort};

#[cfg(windows)]
mod windows;
Expand Down
124 changes: 114 additions & 10 deletions src/posix/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,48 @@ use crate::{
SerialPortBuilder, StopBits,
};

/// Read mode enumeration.
///
/// The documentation is strongly based on [this documentation](http://www.unixwiz.net/techtips/termios-vmin-vtime.html).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadMode {
/// If data are available in the input queue, it's transferred to the caller's buffer up to a
/// maximum of nbytes, and returned immediately to the caller. Otherwise the driver blocks
/// until data arrives, or when VTIME tenths expire from the start of the call. If the timer
/// expires without data, zero is returned. A single byte is sufficient to satisfy this read
/// call, but if more is available in the input queue, it's returned to the caller.
TimedRead {
/// Timeout value VTIME in tenths of a second.
timeout: u8,
},
/// This is a counted read that is satisfied only when at least VMIN characters have been
/// transferred to the caller's buffer - there is no timing component involved. This read can
/// be satisfied from the driver's input queue (where the call could return immediately), or
/// by waiting for new data to arrive: in this respect the call could block indefinitely.
BlockUntilMinRead {
/// VMIN character. This is the minimum number of character required to satisfy a read
/// operation.
minimal_bytes: u8,
},
/// Read calls are satisfied when either VMIN characters have been transferred to the caller's
/// buffer, or when VTIME tenths expire between characters. Since this timer is not started
/// until the first character arrives, this call can block indefinitely if the serial line is
/// idle. This is the most common mode of operation, and we consider VTIME to be an
/// intercharacter timeout, not an overall one. This call should never return zero bytes read.
Blocking {
/// VTIME value in tenths of a second.
inter_byte_timeout: u8,
/// VMIN character. Receiving this number of chracters satisfies the read operation.
minimal_bytes: u8,
},
/// This is a completely non-blocking read - the call is satisfied immediately directly from
/// the driver's input queue. If data are available, it's transferred to the caller's buffer up
/// to nbytes and returned. Otherwise zero is immediately returned to indicate "no data".
/// This essentially polls the serial port and should be used with care. If done
/// repeatedly, it can consume enormous amounts of processor time and is highly inefficient.
Immediate,
}

/// Convenience method for removing exclusive access from
/// a fd and closing it.
fn close(fd: RawFd) {
Expand Down Expand Up @@ -375,6 +417,37 @@ impl TTYPort {
.map_err(|e| e.into())
}

/// Set the VMIN and VTIME attributes of the port which determine the behavior
/// of the port on read calls.
///
/// See [this documentation](http://www.unixwiz.net/techtips/termios-vmin-vtime.html) for
/// more details.
pub fn set_vmin_vtime(&mut self, vmin: u8, vtime: u8) -> Result<()> {
let mut termios = termios::get_termios(self.fd)?;
termios.c_cc[nix::sys::termios::SpecialCharacterIndices::VMIN as usize] = vmin;
termios.c_cc[nix::sys::termios::SpecialCharacterIndices::VTIME as usize] = vtime;
termios::set_termios(self.fd, &termios)
}

/// Set the TTY port read mode which configures the VMIN and VTIME parameters.
///
/// It should be noted that the regular [std::io::Read] implementation
/// still relies on the `ppoll` API to determine whether a serial port is readable.
/// This means that a read might still return a timeout error even if the read mode
/// is set to [ReadMode::Immediate]. This can be circumvented by using [Self::read_raw]
/// directly.
pub fn set_read_mode(&mut self, read_mode: ReadMode) -> Result<()> {
match read_mode {
ReadMode::TimedRead { timeout } => self.set_vmin_vtime(0, timeout),
ReadMode::BlockUntilMinRead { minimal_bytes } => self.set_vmin_vtime(minimal_bytes, 0),
ReadMode::Blocking {
inter_byte_timeout,
minimal_bytes,
} => self.set_vmin_vtime(minimal_bytes, inter_byte_timeout),
ReadMode::Immediate => self.set_vmin_vtime(0, 0),
}
}

/// Attempts to clone the `SerialPort`. This allow you to write and read simultaneously from the
/// same serial connection. Please note that if you want a real asynchronous serial port you
/// should look at [mio-serial](https://crates.io/crates/mio-serial) or
Expand All @@ -400,6 +473,45 @@ impl TTYPort {
baud_rate: self.baud_rate,
})
}

/// Raw read call which directly calls [nix::unistd::read] without polling the file
/// descriptor first.
///
/// Can be used with [Self::set_read_mode] to use the
/// [read mode specified by VMIN and VTIME](http://www.unixwiz.net/techtips/termios-vmin-vtime.html).
pub fn read_raw(&mut self, buf: &mut [u8]) -> io::Result<usize> {
nix::unistd::read(self.fd, buf).map_err(|e| io::Error::from(Error::from(e)))
}

/// Raw read call which directly calls [nix::unistd::write] without polling the file
/// descriptor first.
pub fn write_raw(&mut self, buf: &[u8]) -> io::Result<usize> {
nix::unistd::write(self.fd, buf).map_err(|e| io::Error::from(Error::from(e)))
}

/// Read implementation which is also used by [std::io::Read].
///
/// This implementation uses the OS `ppoll` mechanism to determine whether bytes are available.
/// It will return an IO error with [io::ErrorKind::TimedOut] if no bytes are available.
pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if let Err(e) = super::poll::wait_read_fd(self.fd, self.timeout) {
return Err(io::Error::from(Error::from(e)));
}

self.read_raw(buf)
}

/// Write implementation which is also used by [std::io::Write].
///
/// This implementation uses the OS `ppoll` mechanism to determine whether bytes can be
/// written.
pub fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Err(e) = super::poll::wait_write_fd(self.fd, self.timeout) {
return Err(io::Error::from(Error::from(e)));
}

self.write_raw(buf)
}
}

impl Drop for TTYPort {
Expand Down Expand Up @@ -466,21 +578,13 @@ impl FromRawFd for TTYPort {

impl io::Read for TTYPort {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if let Err(e) = super::poll::wait_read_fd(self.fd, self.timeout) {
return Err(io::Error::from(Error::from(e)));
}

nix::unistd::read(self.fd, buf).map_err(|e| io::Error::from(Error::from(e)))
self.read(buf)
}
}

impl io::Write for TTYPort {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Err(e) = super::poll::wait_write_fd(self.fd, self.timeout) {
return Err(io::Error::from(Error::from(e)));
}

nix::unistd::write(self.fd, buf).map_err(|e| io::Error::from(Error::from(e)))
self.write(buf)
}

fn flush(&mut self) -> io::Result<()> {
Expand Down
Loading