Skip to content

Commit

Permalink
Implement custom baud rates on Apple with the IOSSIOSPEED ioctl.
Browse files Browse the repository at this point in the history
  • Loading branch information
de-vri-es committed Jun 20, 2024
1 parent df6fa61 commit 4d374e3
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 48 deletions.
47 changes: 47 additions & 0 deletions src/sys/unix/apple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::path::PathBuf;
use std::os::unix::io::RawFd;

const IOCTL_IOSSIOSPEED: u64 = 0x80045402;

/// Set the baud rate of a serial port using the IOSSIOSPEED ioctl.
///
/// The speed set this way applied to the input and the output speed.
/// It is *not* reflected back in the `termios` struct.
/// There is no ioctl to retrieve the real baud rate -.-
///
/// This is Apple, so there is no public documentation (why would you?).
/// This is the best I could find:
/// * https://opensource.apple.com/source/IOSerialFamily/IOSerialFamily-91/tests/IOSerialTestLib.c.auto.html
/// * https://developer.apple.com/library/archive/samplecode/SerialPortSample/Listings/SerialPortSample_SerialPortSample_c.html#//apple_ref/doc/uid/DTS10000454-SerialPortSample_SerialPortSample_c-DontLinkElementID_4
pub fn ioctl_iossiospeed(fd: RawFd, baud_rate: libc::speed_t) -> Result<(), std::io::Error> {
unsafe {
super::check(libc::ioctl(fd, IOCTL_IOSSIOSPEED, &baud_rate))?;
Ok(())
}
}

pub fn enumerate() -> std::io::Result<Vec<PathBuf>> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::FileTypeExt;

let serial_ports = std::fs::read_dir("/dev")?
.filter_map(|entry| {
let entry = entry.ok()?;
let kind = entry.metadata().ok()?.file_type();
if kind.is_char_device() && is_tty_name(entry.file_name().as_bytes()) {
Some(entry.path())
} else {
None
}
})
.collect();
Ok(serial_ports)
}

fn is_tty_name(name: &[u8]) -> bool {
// Sigh, closed source doesn't have to mean undocumented.
// Anyway:
// https://stackoverflow.com/questions/14074413/serial-port-names-on-mac-os-x
// https://learn.adafruit.com/ftdi-friend/com-slash-serial-port-name
name.starts_with(b"tty.") || name.starts_with(b"cu.")
}
71 changes: 30 additions & 41 deletions src/sys/unix/bsd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,37 @@ pub fn enumerate() -> std::io::Result<Vec<PathBuf>> {
}

fn is_tty_name(name: &[u8]) -> bool {
cfg_if! {
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
// Sigh, closed source doesn't have to mean undocumented.
// Anyway:
// https://stackoverflow.com/questions/14074413/serial-port-names-on-mac-os-x
// https://learn.adafruit.com/ftdi-friend/com-slash-serial-port-name
name.starts_with(b"tty.") || name.starts_with(b"cu.")

} else {
// For BSD variants, we simply report all entries in /dev that look like a TTY.
// This may contain a lot of false positives for pseudo-terminals or other fake terminals.
// If anyone can improve this for a specific BSD they love, by all means send a PR.

// https://man.dragonflybsd.org/?command=sio&section=4
// https://leaf.dragonflybsd.org/cgi/web-man?command=ucom&section=ANY
#[cfg(target_os = "dragonfly")]
const PREFIXES: [&[u8]; 4] = [b"ttyd", b"cuaa", b"ttyU", b"cuaU"];

// https://www.freebsd.org/cgi/man.cgi?query=uart&sektion=4&apropos=0&manpath=FreeBSD+13.0-RELEASE+and+Ports
// https://www.freebsd.org/cgi/man.cgi?query=ucom&sektion=4&apropos=0&manpath=FreeBSD+13.0-RELEASE+and+Ports
#[cfg(target_os = "freebsd")]
const PREFIXES: [&[u8]; 5] = [b"ttyu", b"cuau", b"cuad", b"ttyU", b"cuaU"];

// https://man.netbsd.org/com.4
// https://man.netbsd.org/ucom.4
#[cfg(target_os = "netbsd")]
const PREFIXES: [&[u8]; 4] = [b"tty", b"dty", b"ttyU", b"dtyU"];

// https://man.openbsd.org/com
// https://man.openbsd.org/ucom
#[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;
}
// For BSD variants, we simply report all entries in /dev that look like a TTY.
// This may contain a lot of false positives for pseudo-terminals or other fake terminals.
// If anyone can improve this for a specific BSD they love, by all means send a PR.

// https://man.dragonflybsd.org/?command=sio&section=4
// https://leaf.dragonflybsd.org/cgi/web-man?command=ucom&section=ANY
#[cfg(target_os = "dragonfly")]
const PREFIXES: [&[u8]; 4] = [b"ttyd", b"cuaa", b"ttyU", b"cuaU"];

// https://www.freebsd.org/cgi/man.cgi?query=uart&sektion=4&apropos=0&manpath=FreeBSD+13.0-RELEASE+and+Ports
// https://www.freebsd.org/cgi/man.cgi?query=ucom&sektion=4&apropos=0&manpath=FreeBSD+13.0-RELEASE+and+Ports
#[cfg(target_os = "freebsd")]
const PREFIXES: [&[u8]; 5] = [b"ttyu", b"cuau", b"cuad", b"ttyU", b"cuaU"];

// https://man.netbsd.org/com.4
// https://man.netbsd.org/ucom.4
#[cfg(target_os = "netbsd")]
const PREFIXES: [&[u8]; 4] = [b"tty", b"dty", b"ttyU", b"dtyU"];

// https://man.openbsd.org/com
// https://man.openbsd.org/ucom
#[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
}
}

false
}
69 changes: 62 additions & 7 deletions src/sys/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,32 @@ pub struct SerialPort {
pub file: std::fs::File,
pub read_timeout_ms: u32,
pub write_timeout_ms: u32,

/// On iOS and macOS, custom baud rates are set through a IOSSIOSPEED ioclt.
/// The speed set this way is not reported back by `tcgetattr()` and there does
/// not appear to be an ioctl to get the real baud rate.
///
/// So.. we have to cache the value from the last time we set it.
/// This is fragile but it seems to be the best we can do :|
///
/// Set to `None` until we set the baud rate.
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub baud_rate: Option<libc::speed_t>,
}

cfg_if! {
if #[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
target_os = "ios",
target_os = "macos",
))] {
mod apple;
pub use apple::*;

} else if #[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))] {
mod bsd;
pub use bsd::*;
Expand Down Expand Up @@ -132,6 +148,8 @@ impl SerialPort {
file,
read_timeout_ms: super::DEFAULT_TIMEOUT_MS,
write_timeout_ms: super::DEFAULT_TIMEOUT_MS,
#[cfg(any(target_os = "ios", target_os = "macos"))]
baud_rate: None,
}
}

Expand All @@ -140,16 +158,53 @@ impl SerialPort {
file: self.file.try_clone()?,
read_timeout_ms: self.read_timeout_ms,
write_timeout_ms: self.write_timeout_ms,
#[cfg(any(target_os = "ios", target_os = "macos"))]
baud_rate: self.baud_rate,
})
}

pub fn get_configuration(&self) -> std::io::Result<Settings> {
Settings::get_from_file(&self.file)
cfg_if! {
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
let mut settings = Settings::get_from_file(&self.file)?;
if let Some(baud_rate) = self.baud_rate {
settings.termios.c_ispeed = baud_rate;
settings.termios.c_ospeed = baud_rate;
}
Ok(settings)
} else {
Settings::get_from_file(&self.file)
}
}
}

pub fn set_configuration(&mut self, settings: &Settings) -> std::io::Result<()> {
// On iOS and macOS we set the baud rate with the IOSSIOSPEED ioctl.
// But we also need to ensure the `set_on_file()` doesn't fail.
// So fill in a safe speed in the termios struct which we will override shortly after.
#[cfg(any(target_os = "ios", target_os = "macos"))]
let (settings, baud_rate) = {
let baud_rate = settings.termios.c_ospeed;
let mut settings = settings.clone();
settings.termios.c_ispeed = 9600;
settings.termios.c_ospeed = 9600;
(settings, baud_rate)
};
#[cfg(any(target_os = "ios", target_os = "macos"))]
let settings = &settings;

settings.set_on_file(&mut self.file)?;

// On iOS and macOS, override the speed with the IOSSIOSPEED ioctl.
#[cfg(any(target_os = "ios", target_os = "macos"))]
{
// Since `set_on_file` succeeded, I guess the baud rate is now set by `tcsetattr`.
// So in case `ioctl_iossiospeed()` fails, lets clear the cached baud rate first.
self.baud_rate = None;
ioctl_iossiospeed(self.file.as_raw_fd(), baud_rate)?;
self.baud_rate = Some(baud_rate);
}

let applied_settings = self.get_configuration()?;
if applied_settings != *settings {
Err(other_error("failed to apply some or all settings"))
Expand Down

0 comments on commit 4d374e3

Please sign in to comment.