use crate::underlying::sys::DEFAULT_TIMEOUT_MS;
use libc::{O_NOCTTY, O_NONBLOCK};
use std::{
fs::{File, OpenOptions},
io::{Error as IoError, ErrorKind as IoErrorKind, IoSlice, IoSliceMut, Result as IoResult},
os::{
raw::{c_int, c_short},
unix::{fs::OpenOptionsExt, io::AsRawFd},
},
path::{Path, PathBuf},
time::Duration,
};
use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
#[cfg(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
))]
pub type RawTermios = libc::termios2;
#[cfg(not(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
)))]
pub type RawTermios = libc::termios;
#[derive(Debug)]
pub struct RawSyncSerialPort {
pub fd: File,
pub read_timeout_ms: u32,
pub write_timeout_ms: u32,
}
impl RawSyncSerialPort {
pub fn new(path: impl AsRef<Path>) -> IoResult<Self> {
let this = Self {
fd: OpenOptions::new()
.read(true)
.write(true)
.create(false)
.custom_flags(O_NONBLOCK | O_NOCTTY)
.open(path)?,
read_timeout_ms: DEFAULT_TIMEOUT_MS,
write_timeout_ms: DEFAULT_TIMEOUT_MS,
};
let mut termios = Self::get_termios_from_fd(&this.fd)?;
Self::set_baud_rate(&mut termios)?;
termios.c_cflag = (termios.c_cflag & !libc::CSIZE) | libc::CS8;
termios.c_cflag = termios.c_cflag & !libc::PARODD & !libc::PARENB;
termios.c_cflag &= !libc::CSTOPB;
Self::set_termios_on_fd(&this.fd, &termios)?;
Ok(this)
}
pub fn try_clone(&self) -> IoResult<Self> {
Ok(Self {
fd: self.fd.try_clone()?,
read_timeout_ms: self.read_timeout_ms,
write_timeout_ms: self.write_timeout_ms,
})
}
#[allow(
// Windows needs a result type here, and we want to match signatures.
clippy::unnecessary_wraps,
)]
pub fn get_read_timeout(&self) -> IoResult<Duration> {
Ok(Duration::from_millis(self.read_timeout_ms.into()))
}
#[allow(
// Windows needs a result type here, and we want to match signatures.
clippy::unnecessary_wraps,
)]
pub fn get_write_timeout(&self) -> IoResult<Duration> {
Ok(Duration::from_millis(self.write_timeout_ms.into()))
}
pub fn set_read_timeout(&mut self, new_timeout: Duration) -> IoResult<()> {
let timeout_ms = new_timeout.as_millis();
if timeout_ms < 1 || timeout_ms > (u32::MAX - 1).into() {
return Err(IoError::other(
"read timeout must be between 1, and 4294967294",
));
}
self.read_timeout_ms = timeout_ms.try_into().unwrap_or(u32::MAX);
Ok(())
}
pub fn set_write_timeout(&mut self, new_timeout: Duration) -> IoResult<()> {
let timeout_ms = new_timeout.as_millis();
if timeout_ms < 1 || timeout_ms > (u32::MAX - 1).into() {
return Err(IoError::other(
"write timeout must be between 1, and 4294967294",
));
}
self.write_timeout_ms = timeout_ms.try_into().unwrap_or(u32::MAX);
Ok(())
}
#[allow(
// We manually check that sign loss isn't an issue.
clippy::cast_sign_loss,
)]
pub fn read(&self, buff: &mut [u8]) -> IoResult<usize> {
if !Self::poll(&self.fd, libc::POLLIN, self.read_timeout_ms)? {
return Err(IoErrorKind::TimedOut.into());
}
loop {
match unsafe {
Self::check_isize(libc::read(
self.fd.as_raw_fd(),
buff.as_mut_ptr().cast(),
buff.len() as _,
))
} {
Ok(size) => return Ok(size as usize),
Err(cause) => {
if cause.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(cause);
}
}
}
}
#[must_use]
pub const fn can_read_vectored() -> bool {
true
}
#[allow(
// Truncation is okay in this api.
clippy::cast_possible_truncation,
// Wrapping is okay in this API.
clippy::cast_possible_wrap,
// We manually check to ensure sign loss isn't a big deal.
clippy::cast_sign_loss,
)]
pub fn read_vectored(&self, buff: &mut [IoSliceMut<'_>]) -> IoResult<usize> {
if !Self::poll(&self.fd, libc::POLLIN, self.read_timeout_ms)? {
return Err(IoErrorKind::TimedOut.into());
}
loop {
match unsafe {
Self::check_isize(libc::readv(
self.fd.as_raw_fd(),
buff.as_mut_ptr().cast(),
buff.len() as _,
))
} {
Ok(read) => return Ok(read as usize),
Err(cause) => {
if cause.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(cause);
}
}
}
}
#[allow(
// Truncation is okay for this API.
clippy::cast_possible_truncation,
// Wrapping is okay for this API.
clippy::cast_possible_wrap,
// We validate manually the sign loss isn't an issue.
clippy::cast_sign_loss,
)]
pub fn write(&self, buff: &[u8]) -> IoResult<usize> {
if !Self::poll(&self.fd, libc::POLLOUT, self.write_timeout_ms)? {
return Err(IoErrorKind::TimedOut.into());
}
loop {
match unsafe {
Self::check_isize(libc::write(
self.fd.as_raw_fd(),
buff.as_ptr().cast(),
buff.len() as _,
))
} {
Ok(size) => return Ok(size as usize),
Err(cause) => {
if cause.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(cause);
}
}
}
}
#[must_use]
pub const fn can_write_vectored() -> bool {
true
}
#[allow(
// Wrapping is okay for this API.
clippy::cast_possible_wrap,
// Truncation is okay in this API.
clippy::cast_possible_truncation,
// We validate sign loss isn't an issue manually.
clippy::cast_sign_loss,
)]
pub fn write_vectored(&self, buff: &[IoSlice<'_>]) -> IoResult<usize> {
if !Self::poll(&self.fd, libc::POLLOUT, self.write_timeout_ms)? {
return Err(IoErrorKind::TimedOut.into());
}
loop {
match unsafe {
Self::check_isize(libc::writev(
self.fd.as_raw_fd(),
buff.as_ptr().cast(),
buff.len() as _,
))
} {
Ok(read) => return Ok(read as usize),
Err(cause) => {
if cause.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(cause);
}
}
}
}
pub fn flush_output(&self) -> IoResult<()> {
unsafe {
Self::check(libc::tcdrain(self.fd.as_raw_fd()))?;
}
Ok(())
}
pub fn discard_buffers(&self, discard_input: bool, discard_output: bool) -> IoResult<()> {
let mut flags = 0;
if discard_input && discard_output {
flags |= libc::TCIOFLUSH;
} else if discard_input {
flags |= libc::TCIFLUSH;
} else if discard_output {
flags |= libc::TCOFLUSH;
}
unsafe {
Self::check(libc::tcflush(self.fd.as_raw_fd(), flags))?;
}
Ok(())
}
pub fn set_rts(&self, state: bool) -> IoResult<()> {
Self::set_pin(&self.fd, libc::TIOCM_RTS, state)
}
pub fn read_cts(&self) -> IoResult<bool> {
Self::read_pin(&self.fd, libc::TIOCM_CTS)
}
pub fn set_dtr(&self, state: bool) -> IoResult<()> {
Self::set_pin(&self.fd, libc::TIOCM_DTR, state)
}
pub fn read_dsr(&self) -> IoResult<bool> {
Self::read_pin(&self.fd, libc::TIOCM_DSR)
}
pub fn read_ri(&self) -> IoResult<bool> {
Self::read_pin(&self.fd, libc::TIOCM_RI)
}
pub fn read_cd(&self) -> IoResult<bool> {
Self::read_pin(&self.fd, libc::TIOCM_CD)
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
))]
pub fn enumerate() -> IoResult<Vec<PathBuf>> {
use std::os::unix::{ffi::OsStrExt, fs::FileTypeExt};
Ok(std::fs::read_dir("/dev")?
.filter_map(|resulting_entry| {
let entry = resulting_entry.ok()?;
let kind = entry.metadata().ok()?.file_type();
if kind.is_char_device() && Self::is_tty_name(entry.file_name().as_bytes()) {
Some(entry.path())
} else {
None
}
})
.collect())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn enumerate() -> IoResult<Vec<PathBuf>> {
use std::os::unix::{ffi::OsStrExt, fs::FileTypeExt};
Ok(std::fs::read_dir("/sys/class/tty")?
.filter_map(|resulting_entry| {
let entry = resulting_entry.ok()?;
let base_name = entry.file_name();
let dev_path = Path::new("/dev").join(&base_name);
let metadata = dev_path.metadata().ok()?;
if !metadata.file_type().is_char_device() {
return None;
}
match base_name.as_bytes().strip_prefix(b"tty") {
Some(b"") | None => return None,
Some(&[c, ..]) if c.is_ascii_digit() => return None,
Some(_) => (),
}
if let Ok(driver_override) =
std::fs::read(entry.path().join("device/driver_override"))
&& driver_override == b"(null)\n"
{
return None;
}
Some(dev_path)
})
.collect())
}
#[cfg(any(target_os = "illumos", target_os = "solaris"))]
pub fn enumerate() -> IoResult<Vec<PathBuf>> {
use std::os::unix::fs::FileTypeExt;
let cua = std::fs::read_dir("/dev/cua")?;
let term = std::fs::read_dir("/dev/cua")?;
Ok(cua
.chain(term)
.filter_map(|entry| {
let entry = entry.ok()?;
let kind = entry.metadata().ok()?.file_type();
if kind.is_char_device() {
Some(entry.path())
} else {
None
}
})
.collect())
}
#[cfg(not(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos",
target_os = "solaris",
target_os = "linux",
target_os = "android",
)))]
pub fn enumerate() -> IoResult<Vec<PathBuf>> {
Err(IoError::new(
IoErrorKind::Other,
"port enumeration is not implemented for this platform",
))
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn is_tty_name(name: &[u8]) -> bool {
name.starts_with(b"tty.") || name.starts_with(b"cu.")
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
fn is_tty_name(name: &[u8]) -> bool {
#[cfg(target_os = "dragonfly")]
const PREFIXES: [&[u8]; 4] = [b"ttyd", b"cuaa", b"ttyU", b"cuaU"];
#[cfg(target_os = "freebsd")]
const PREFIXES: [&[u8]; 5] = [b"ttyu", b"cuau", b"cuad", b"ttyU", b"cuaU"];
#[cfg(target_os = "netbsd")]
const PREFIXES: [&[u8]; 4] = [b"tty", b"dty", b"ttyU", b"dtyU"];
#[cfg(target_os = "openbsd")]
const PREFIXES: [&[u8]; 4] = [b"tty", b"cua", b"ttyU", b"cuaU"];
for prefix in PREFIXES {
if let Some(suffix) = name.strip_prefix(prefix) {
if !suffix.is_empty() && suffix.iter().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
false
}
#[allow(
// Wrapping is okay here.
clippy::cast_possible_wrap,
)]
fn poll(fd: &File, events: c_short, timeout_ms: u32) -> IoResult<bool> {
let mut poll_fd = libc::pollfd {
fd: fd.as_raw_fd(),
events,
revents: 0,
};
unsafe {
Self::check(libc::poll(&raw mut poll_fd, 1, timeout_ms as i32))?;
}
Ok(poll_fd.revents != 0)
}
fn read_pin(fd: &File, pin: c_int) -> IoResult<bool> {
let mut bits: c_int = 0;
unsafe {
Self::check(libc::ioctl(fd.as_raw_fd(), libc::TIOCMGET as _, &mut bits))?;
}
Ok(bits & pin != 0)
}
fn set_pin(fd: &File, pin: c_int, state: bool) -> IoResult<()> {
unsafe {
Self::check(libc::ioctl(
fd.as_raw_fd(),
if state {
libc::TIOCMBIS
} else {
libc::TIOCMBIC
} as _,
&pin,
))?;
}
Ok(())
}
#[allow(
// Certain paths need the return type to be a result, others do not.
clippy::unnecessary_wraps,
)]
fn set_baud_rate(termios: &mut RawTermios) -> IoResult<()> {
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
))]
unsafe {
Self::check(libc::cfsetospeed(termios, 57600 as _))?;
Self::check(libc::cfsetispeed(termios, 57600 as _))?;
Ok(())
}
#[cfg(all(
not(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
)),
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
))]
{
termios.c_cflag &= !(libc::CBAUD | libc::CIBAUD);
termios.c_cflag |= libc::BOTHER;
termios.c_cflag |= libc::BOTHER << libc::IBSHIFT;
termios.c_ospeed = 57600;
termios.c_ispeed = 57600;
Ok(())
}
#[cfg(all(
not(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
)),
not(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
)),
))]
unsafe {
Self::check(libc::cfsetospeed(termios, libc::B57600))?;
Self::check(libc::cfsetispeed(termios, libc::B57600))?;
Ok(())
}
}
fn get_termios_from_fd(fd: &File) -> IoResult<RawTermios> {
#[cfg(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
))]
unsafe {
let mut termios = std::mem::zeroed();
Self::check(libc::ioctl(
fd.as_raw_fd(),
libc::TCGETS2 as _,
&mut termios,
))?;
Ok(termios)
}
#[cfg(not(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
)))]
unsafe {
let mut termios = std::mem::zeroed();
Self::check(libc::tcgetattr(fd.as_raw_fd(), &mut termios))?;
Ok(termios)
}
}
fn set_termios_on_fd(fd: &File, termios: &RawTermios) -> IoResult<()> {
#[cfg(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
))]
unsafe {
Self::check(libc::ioctl(fd.as_raw_fd(), libc::TCSETSW2 as _, termios))?;
Ok(())
}
#[cfg(not(all(
any(target_os = "android", target_os = "linux"),
not(any(target_arch = "powerpc", target_arch = "powerpc64"))
)))]
unsafe {
Self::check(libc::tcsetattr(fd.as_raw_fd(), libc::TCSADRAIN, termios))?;
Ok(())
}
}
fn check(ret: i32) -> IoResult<i32> {
if ret == -1 {
Err(IoError::last_os_error())
} else {
Ok(ret)
}
}
fn check_isize(ret: isize) -> IoResult<isize> {
if ret == -1 {
Err(IoError::last_os_error())
} else {
Ok(ret)
}
}
}
const RAW_SYNC_SERIAL_PORT_FIELDS: &[NamedField<'static>] = &[
NamedField::new("fd"),
NamedField::new("read_timeout_ms"),
NamedField::new("write_timeout_ms"),
];
impl Structable for RawSyncSerialPort {
fn definition(&self) -> StructDef<'_> {
StructDef::new_static(
"RawSyncSerialPortFields",
Fields::Named(RAW_SYNC_SERIAL_PORT_FIELDS),
)
}
}
impl Valuable for RawSyncSerialPort {
fn as_value(&self) -> Value<'_> {
Value::Structable(self)
}
fn visit(&self, visitor: &mut dyn Visit) {
visitor.visit_named_fields(&NamedValues::new(
RAW_SYNC_SERIAL_PORT_FIELDS,
&[
Valuable::as_value(&format!("{:?}", self.fd)),
Valuable::as_value(&self.read_timeout_ms),
Valuable::as_value(&self.write_timeout_ms),
],
));
}
}