Skip to main content

rns_net/
serial.rs

1//! Serial port abstraction using libc termios.
2//!
3//! Provides raw serial I/O without external crate dependencies.
4
5use std::io;
6use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
7
8/// Serial port parity setting.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum Parity {
11    None,
12    Even,
13    Odd,
14}
15
16/// Configuration for a serial port.
17#[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
38/// A serial port backed by a file descriptor.
39pub struct SerialPort {
40    fd: RawFd,
41}
42
43impl SerialPort {
44    /// Wrap a pre-opened file descriptor (e.g. from a USB bridge socketpair).
45    /// No termios configuration is applied — the fd is used as-is.
46    pub fn from_raw_fd(fd: RawFd) -> Self {
47        SerialPort { fd }
48    }
49
50    /// Open and configure a serial port.
51    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        // Configure termios
66        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        // cfmakeraw equivalent
73        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        // Data bits
87        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        // Parity
96        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        // Stop bits
111        if config.stop_bits == 2 {
112            termios.c_cflag |= libc::CSTOPB;
113        } else {
114            termios.c_cflag &= !libc::CSTOPB;
115        }
116
117        // Disable flow control and hangup-on-close (HUPCL drops DTR on
118        // close, which resets devices that wire DTR to RST like Heltec V3).
119        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        // Baud rate
124        let speed = baud_to_speed(config.baud)?;
125        unsafe {
126            libc::cfsetispeed(&mut termios, speed);
127            libc::cfsetospeed(&mut termios, speed);
128        }
129
130        // Blocking read with VMIN=1, VTIME=0
131        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        // Clear O_NONBLOCK for blocking reads
140        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    /// Get the raw fd.
154    pub fn as_raw_fd(&self) -> RawFd {
155        self.fd
156    }
157
158    /// Get a Read handle (File wrapping a dup'd fd).
159    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    /// Get a Write handle (File wrapping a dup'd fd).
168    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
191/// Map baud rate u32 to libc speed_t constant.
192fn 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/// Create a pseudo-terminal pair for testing. Returns (master_fd, slave_fd).
245///
246/// The master and slave are configured for raw mode to avoid terminal processing.
247#[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    // Set both sides to raw mode to avoid terminal character processing
265    for fd in [master, slave] {
266        let mut termios: libc::termios = unsafe { std::mem::zeroed() };
267        unsafe { libc::tcgetattr(fd, &mut termios) };
268        // cfmakeraw equivalent
269        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        // Poll with timeout to avoid blocking forever
317        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        // Verify common baud rates map successfully
333        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}