1use std::io;
6use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum Parity {
11 None,
12 Even,
13 Odd,
14}
15
16#[derive(Debug, Clone)]
18pub struct SerialConfig {
19 pub path: String,
20 pub baud: u32,
21 pub data_bits: u8,
22 pub parity: Parity,
23 pub stop_bits: u8,
24}
25
26impl Default for SerialConfig {
27 fn default() -> Self {
28 SerialConfig {
29 path: String::new(),
30 baud: 9600,
31 data_bits: 8,
32 parity: Parity::None,
33 stop_bits: 1,
34 }
35 }
36}
37
38pub struct SerialPort {
40 fd: RawFd,
41}
42
43impl SerialPort {
44 pub fn from_raw_fd(fd: RawFd) -> Self {
47 SerialPort { fd }
48 }
49
50 pub fn open(config: &SerialConfig) -> io::Result<Self> {
52 let c_path = std::ffi::CString::new(config.path.as_str())
53 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
54
55 let fd = unsafe {
56 libc::open(
57 c_path.as_ptr(),
58 libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK,
59 )
60 };
61 if fd < 0 {
62 return Err(io::Error::last_os_error());
63 }
64
65 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
67 if unsafe { libc::tcgetattr(fd, &mut termios) } != 0 {
68 unsafe { libc::close(fd) };
69 return Err(io::Error::last_os_error());
70 }
71
72 termios.c_iflag &= !(libc::IGNBRK
74 | libc::BRKINT
75 | libc::PARMRK
76 | libc::ISTRIP
77 | libc::INLCR
78 | libc::IGNCR
79 | libc::ICRNL
80 | libc::IXON);
81 termios.c_oflag &= !libc::OPOST;
82 termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
83 termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
84 termios.c_cflag |= libc::CS8;
85
86 termios.c_cflag &= !libc::CSIZE;
88 termios.c_cflag |= match config.data_bits {
89 5 => libc::CS5,
90 6 => libc::CS6,
91 7 => libc::CS7,
92 _ => libc::CS8,
93 };
94
95 match config.parity {
97 Parity::None => {
98 termios.c_cflag &= !libc::PARENB;
99 }
100 Parity::Even => {
101 termios.c_cflag |= libc::PARENB;
102 termios.c_cflag &= !libc::PARODD;
103 }
104 Parity::Odd => {
105 termios.c_cflag |= libc::PARENB;
106 termios.c_cflag |= libc::PARODD;
107 }
108 }
109
110 if config.stop_bits == 2 {
112 termios.c_cflag |= libc::CSTOPB;
113 } else {
114 termios.c_cflag &= !libc::CSTOPB;
115 }
116
117 termios.c_cflag |= libc::CLOCAL | libc::CREAD;
120 termios.c_cflag &= !(libc::CRTSCTS | libc::HUPCL);
121 termios.c_iflag &= !(libc::IXON | libc::IXOFF | libc::IXANY);
122
123 let speed = baud_to_speed(config.baud)?;
125 unsafe {
126 libc::cfsetispeed(&mut termios, speed);
127 libc::cfsetospeed(&mut termios, speed);
128 }
129
130 termios.c_cc[libc::VMIN] = 1;
132 termios.c_cc[libc::VTIME] = 0;
133
134 if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) } != 0 {
135 unsafe { libc::close(fd) };
136 return Err(io::Error::last_os_error());
137 }
138
139 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
141 if flags < 0 {
142 unsafe { libc::close(fd) };
143 return Err(io::Error::last_os_error());
144 }
145 if unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) } < 0 {
146 unsafe { libc::close(fd) };
147 return Err(io::Error::last_os_error());
148 }
149
150 Ok(SerialPort { fd })
151 }
152
153 pub fn as_raw_fd(&self) -> RawFd {
155 self.fd
156 }
157
158 pub fn reader(&self) -> io::Result<std::fs::File> {
160 let new_fd = unsafe { libc::dup(self.fd) };
161 if new_fd < 0 {
162 return Err(io::Error::last_os_error());
163 }
164 Ok(unsafe { std::fs::File::from_raw_fd(new_fd) })
165 }
166
167 pub fn writer(&self) -> io::Result<std::fs::File> {
169 let new_fd = unsafe { libc::dup(self.fd) };
170 if new_fd < 0 {
171 return Err(io::Error::last_os_error());
172 }
173 Ok(unsafe { std::fs::File::from_raw_fd(new_fd) })
174 }
175}
176
177impl AsRawFd for SerialPort {
178 fn as_raw_fd(&self) -> RawFd {
179 self.fd
180 }
181}
182
183impl Drop for SerialPort {
184 fn drop(&mut self) {
185 unsafe {
186 libc::close(self.fd);
187 }
188 }
189}
190
191fn baud_to_speed(baud: u32) -> io::Result<libc::speed_t> {
193 match baud {
194 0 => Ok(libc::B0),
195 50 => Ok(libc::B50),
196 75 => Ok(libc::B75),
197 110 => Ok(libc::B110),
198 134 => Ok(libc::B134),
199 150 => Ok(libc::B150),
200 200 => Ok(libc::B200),
201 300 => Ok(libc::B300),
202 600 => Ok(libc::B600),
203 1200 => Ok(libc::B1200),
204 1800 => Ok(libc::B1800),
205 2400 => Ok(libc::B2400),
206 4800 => Ok(libc::B4800),
207 9600 => Ok(libc::B9600),
208 19200 => Ok(libc::B19200),
209 38400 => Ok(libc::B38400),
210 57600 => Ok(libc::B57600),
211 115200 => Ok(libc::B115200),
212 230400 => Ok(libc::B230400),
213 #[cfg(target_os = "linux")]
214 460800 => Ok(libc::B460800),
215 #[cfg(target_os = "linux")]
216 500000 => Ok(libc::B500000),
217 #[cfg(target_os = "linux")]
218 576000 => Ok(libc::B576000),
219 #[cfg(target_os = "linux")]
220 921600 => Ok(libc::B921600),
221 #[cfg(target_os = "linux")]
222 1000000 => Ok(libc::B1000000),
223 #[cfg(target_os = "linux")]
224 1152000 => Ok(libc::B1152000),
225 #[cfg(target_os = "linux")]
226 1500000 => Ok(libc::B1500000),
227 #[cfg(target_os = "linux")]
228 2000000 => Ok(libc::B2000000),
229 #[cfg(target_os = "linux")]
230 2500000 => Ok(libc::B2500000),
231 #[cfg(target_os = "linux")]
232 3000000 => Ok(libc::B3000000),
233 #[cfg(target_os = "linux")]
234 3500000 => Ok(libc::B3500000),
235 #[cfg(target_os = "linux")]
236 4000000 => Ok(libc::B4000000),
237 _ => Err(io::Error::new(
238 io::ErrorKind::InvalidInput,
239 format!("unsupported baud rate: {}", baud),
240 )),
241 }
242}
243
244#[cfg(test)]
248pub fn open_pty_pair() -> io::Result<(RawFd, RawFd)> {
249 let mut master: RawFd = -1;
250 let mut slave: RawFd = -1;
251 let ret = unsafe {
252 libc::openpty(
253 &mut master,
254 &mut slave,
255 std::ptr::null_mut(),
256 std::ptr::null_mut(),
257 std::ptr::null_mut(),
258 )
259 };
260 if ret != 0 {
261 return Err(io::Error::last_os_error());
262 }
263
264 for fd in [master, slave] {
266 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
267 unsafe { libc::tcgetattr(fd, &mut termios) };
268 termios.c_iflag &= !(libc::IGNBRK
270 | libc::BRKINT
271 | libc::PARMRK
272 | libc::ISTRIP
273 | libc::INLCR
274 | libc::IGNCR
275 | libc::ICRNL
276 | libc::IXON);
277 termios.c_oflag &= !libc::OPOST;
278 termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
279 termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
280 termios.c_cflag |= libc::CS8;
281 termios.c_cc[libc::VMIN] = 1;
282 termios.c_cc[libc::VTIME] = 0;
283 unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) };
284 }
285
286 Ok((master, slave))
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::io::{Read, Write};
293
294 #[test]
295 fn open_pty_pair_works() {
296 let (master, slave) = open_pty_pair().unwrap();
297 assert!(master >= 0);
298 assert!(slave >= 0);
299 unsafe {
300 libc::close(master);
301 libc::close(slave);
302 }
303 }
304
305 #[test]
306 fn write_read_roundtrip() {
307 let (master, slave) = open_pty_pair().unwrap();
308
309 let mut master_file = unsafe { std::fs::File::from_raw_fd(master) };
310 let mut slave_file = unsafe { std::fs::File::from_raw_fd(slave) };
311
312 let data = b"hello serial";
313 master_file.write_all(data).unwrap();
314 master_file.flush().unwrap();
315
316 let mut pfd = libc::pollfd {
318 fd: slave,
319 events: libc::POLLIN,
320 revents: 0,
321 };
322 let ret = unsafe { libc::poll(&mut pfd, 1, 2000) };
323 assert!(ret > 0, "should have data available on slave");
324
325 let mut buf = [0u8; 64];
326 let n = slave_file.read(&mut buf).unwrap();
327 assert_eq!(&buf[..n], data);
328 }
329
330 #[test]
331 fn config_baud_rates() {
332 for &baud in &[9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600] {
334 let speed = baud_to_speed(baud);
335 assert!(speed.is_ok(), "baud {} should be supported", baud);
336 }
337 }
338
339 #[test]
340 fn from_raw_fd_works() {
341 let (master, slave) = open_pty_pair().unwrap();
342
343 let port = SerialPort::from_raw_fd(slave);
344 let mut writer = port.writer().unwrap();
345 let mut reader_file = unsafe { std::fs::File::from_raw_fd(master) };
346
347 let data = b"from_raw_fd test";
348 writer.write_all(data).unwrap();
349 writer.flush().unwrap();
350
351 let mut pfd = libc::pollfd {
352 fd: master,
353 events: libc::POLLIN,
354 revents: 0,
355 };
356 let ret = unsafe { libc::poll(&mut pfd, 1, 2000) };
357 assert!(ret > 0, "should have data available");
358
359 let mut buf = [0u8; 64];
360 let n = reader_file.read(&mut buf).unwrap();
361 assert_eq!(&buf[..n], data);
362 }
363
364 #[test]
365 fn invalid_path_fails() {
366 let config = SerialConfig {
367 path: "/dev/nonexistent_serial_port_xyz".into(),
368 ..Default::default()
369 };
370 let result = SerialPort::open(&config);
371 assert!(result.is_err());
372 }
373}