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 open(config: &SerialConfig) -> io::Result<Self> {
46 let c_path = std::ffi::CString::new(config.path.as_str())
47 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
48
49 let fd = unsafe {
50 libc::open(
51 c_path.as_ptr(),
52 libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK,
53 )
54 };
55 if fd < 0 {
56 return Err(io::Error::last_os_error());
57 }
58
59 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
61 if unsafe { libc::tcgetattr(fd, &mut termios) } != 0 {
62 unsafe { libc::close(fd) };
63 return Err(io::Error::last_os_error());
64 }
65
66 termios.c_iflag &= !(libc::IGNBRK
68 | libc::BRKINT
69 | libc::PARMRK
70 | libc::ISTRIP
71 | libc::INLCR
72 | libc::IGNCR
73 | libc::ICRNL
74 | libc::IXON);
75 termios.c_oflag &= !libc::OPOST;
76 termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
77 termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
78 termios.c_cflag |= libc::CS8;
79
80 termios.c_cflag &= !libc::CSIZE;
82 termios.c_cflag |= match config.data_bits {
83 5 => libc::CS5,
84 6 => libc::CS6,
85 7 => libc::CS7,
86 _ => libc::CS8,
87 };
88
89 match config.parity {
91 Parity::None => {
92 termios.c_cflag &= !libc::PARENB;
93 }
94 Parity::Even => {
95 termios.c_cflag |= libc::PARENB;
96 termios.c_cflag &= !libc::PARODD;
97 }
98 Parity::Odd => {
99 termios.c_cflag |= libc::PARENB;
100 termios.c_cflag |= libc::PARODD;
101 }
102 }
103
104 if config.stop_bits == 2 {
106 termios.c_cflag |= libc::CSTOPB;
107 } else {
108 termios.c_cflag &= !libc::CSTOPB;
109 }
110
111 termios.c_cflag |= libc::CLOCAL | libc::CREAD;
114 termios.c_cflag &= !(libc::CRTSCTS | libc::HUPCL);
115 termios.c_iflag &= !(libc::IXON | libc::IXOFF | libc::IXANY);
116
117 let speed = baud_to_speed(config.baud)?;
119 unsafe {
120 libc::cfsetispeed(&mut termios, speed);
121 libc::cfsetospeed(&mut termios, speed);
122 }
123
124 termios.c_cc[libc::VMIN] = 1;
126 termios.c_cc[libc::VTIME] = 0;
127
128 if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) } != 0 {
129 unsafe { libc::close(fd) };
130 return Err(io::Error::last_os_error());
131 }
132
133 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
135 if flags < 0 {
136 unsafe { libc::close(fd) };
137 return Err(io::Error::last_os_error());
138 }
139 if unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) } < 0 {
140 unsafe { libc::close(fd) };
141 return Err(io::Error::last_os_error());
142 }
143
144 Ok(SerialPort { fd })
145 }
146
147 pub fn as_raw_fd(&self) -> RawFd {
149 self.fd
150 }
151
152 pub fn reader(&self) -> io::Result<std::fs::File> {
154 let new_fd = unsafe { libc::dup(self.fd) };
155 if new_fd < 0 {
156 return Err(io::Error::last_os_error());
157 }
158 Ok(unsafe { std::fs::File::from_raw_fd(new_fd) })
159 }
160
161 pub fn writer(&self) -> io::Result<std::fs::File> {
163 let new_fd = unsafe { libc::dup(self.fd) };
164 if new_fd < 0 {
165 return Err(io::Error::last_os_error());
166 }
167 Ok(unsafe { std::fs::File::from_raw_fd(new_fd) })
168 }
169}
170
171impl AsRawFd for SerialPort {
172 fn as_raw_fd(&self) -> RawFd {
173 self.fd
174 }
175}
176
177impl Drop for SerialPort {
178 fn drop(&mut self) {
179 unsafe {
180 libc::close(self.fd);
181 }
182 }
183}
184
185fn baud_to_speed(baud: u32) -> io::Result<libc::speed_t> {
187 match baud {
188 0 => Ok(libc::B0),
189 50 => Ok(libc::B50),
190 75 => Ok(libc::B75),
191 110 => Ok(libc::B110),
192 134 => Ok(libc::B134),
193 150 => Ok(libc::B150),
194 200 => Ok(libc::B200),
195 300 => Ok(libc::B300),
196 600 => Ok(libc::B600),
197 1200 => Ok(libc::B1200),
198 1800 => Ok(libc::B1800),
199 2400 => Ok(libc::B2400),
200 4800 => Ok(libc::B4800),
201 9600 => Ok(libc::B9600),
202 19200 => Ok(libc::B19200),
203 38400 => Ok(libc::B38400),
204 57600 => Ok(libc::B57600),
205 115200 => Ok(libc::B115200),
206 230400 => Ok(libc::B230400),
207 460800 => Ok(libc::B460800),
208 500000 => Ok(libc::B500000),
209 576000 => Ok(libc::B576000),
210 921600 => Ok(libc::B921600),
211 1000000 => Ok(libc::B1000000),
212 1152000 => Ok(libc::B1152000),
213 1500000 => Ok(libc::B1500000),
214 2000000 => Ok(libc::B2000000),
215 2500000 => Ok(libc::B2500000),
216 3000000 => Ok(libc::B3000000),
217 3500000 => Ok(libc::B3500000),
218 4000000 => Ok(libc::B4000000),
219 _ => Err(io::Error::new(
220 io::ErrorKind::InvalidInput,
221 format!("unsupported baud rate: {}", baud),
222 )),
223 }
224}
225
226#[cfg(test)]
230pub fn open_pty_pair() -> io::Result<(RawFd, RawFd)> {
231 let mut master: RawFd = -1;
232 let mut slave: RawFd = -1;
233 let ret = unsafe {
234 libc::openpty(
235 &mut master,
236 &mut slave,
237 std::ptr::null_mut(),
238 std::ptr::null_mut(),
239 std::ptr::null_mut(),
240 )
241 };
242 if ret != 0 {
243 return Err(io::Error::last_os_error());
244 }
245
246 for fd in [master, slave] {
248 let mut termios: libc::termios = unsafe { std::mem::zeroed() };
249 unsafe { libc::tcgetattr(fd, &mut termios) };
250 termios.c_iflag &= !(libc::IGNBRK
252 | libc::BRKINT
253 | libc::PARMRK
254 | libc::ISTRIP
255 | libc::INLCR
256 | libc::IGNCR
257 | libc::ICRNL
258 | libc::IXON);
259 termios.c_oflag &= !libc::OPOST;
260 termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
261 termios.c_cflag &= !(libc::CSIZE | libc::PARENB);
262 termios.c_cflag |= libc::CS8;
263 termios.c_cc[libc::VMIN] = 1;
264 termios.c_cc[libc::VTIME] = 0;
265 unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) };
266 }
267
268 Ok((master, slave))
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::io::{Read, Write};
275
276 #[test]
277 fn open_pty_pair_works() {
278 let (master, slave) = open_pty_pair().unwrap();
279 assert!(master >= 0);
280 assert!(slave >= 0);
281 unsafe {
282 libc::close(master);
283 libc::close(slave);
284 }
285 }
286
287 #[test]
288 fn write_read_roundtrip() {
289 let (master, slave) = open_pty_pair().unwrap();
290
291 let mut master_file = unsafe { std::fs::File::from_raw_fd(master) };
292 let mut slave_file = unsafe { std::fs::File::from_raw_fd(slave) };
293
294 let data = b"hello serial";
295 master_file.write_all(data).unwrap();
296 master_file.flush().unwrap();
297
298 let mut pfd = libc::pollfd {
300 fd: slave,
301 events: libc::POLLIN,
302 revents: 0,
303 };
304 let ret = unsafe { libc::poll(&mut pfd, 1, 2000) };
305 assert!(ret > 0, "should have data available on slave");
306
307 let mut buf = [0u8; 64];
308 let n = slave_file.read(&mut buf).unwrap();
309 assert_eq!(&buf[..n], data);
310 }
311
312 #[test]
313 fn config_baud_rates() {
314 for &baud in &[9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600] {
316 let speed = baud_to_speed(baud);
317 assert!(speed.is_ok(), "baud {} should be supported", baud);
318 }
319 }
320
321 #[test]
322 fn invalid_path_fails() {
323 let config = SerialConfig {
324 path: "/dev/nonexistent_serial_port_xyz".into(),
325 ..Default::default()
326 };
327 let result = SerialPort::open(&config);
328 assert!(result.is_err());
329 }
330}