diff --git a/Cargo.toml b/Cargo.toml index 836f6e7f..3f7b4700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ authors = [ "Jesse Braham ", ] 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" diff --git a/examples/reception_raw_blocking.rs b/examples/reception_raw_blocking.rs new file mode 100644 index 00000000..a5255f2f --- /dev/null +++ b/examples/reception_raw_blocking.rs @@ -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 = 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 = 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(); + }); +} diff --git a/examples/reception_raw_non_blocking.rs b/examples/reception_raw_non_blocking.rs new file mode 100644 index 00000000..544d3218 --- /dev/null +++ b/examples/reception_raw_non_blocking.rs @@ -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 = 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 = 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(); + }); +} diff --git a/src/lib.rs b/src/lib.rs index 1158902b..47301f6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/posix/tty.rs b/src/posix/tty.rs index 678c7c94..e3891fcb 100644 --- a/src/posix/tty.rs +++ b/src/posix/tty.rs @@ -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) { @@ -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 @@ -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 { + 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 { + 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 { + 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 { + 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 { @@ -466,21 +578,13 @@ impl FromRawFd for TTYPort { impl io::Read for TTYPort { fn read(&mut self, buf: &mut [u8]) -> io::Result { - 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 { - 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<()> {