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    /// Open and configure a serial port.
45    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        // Configure termios
60        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        // cfmakeraw equivalent
67        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        // Data bits
81        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        // Parity
90        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        // Stop bits
105        if config.stop_bits == 2 {
106            termios.c_cflag |= libc::CSTOPB;
107        } else {
108            termios.c_cflag &= !libc::CSTOPB;
109        }
110
111        // Disable flow control and hangup-on-close (HUPCL drops DTR on
112        // close, which resets devices that wire DTR to RST like Heltec V3).
113        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        // Baud rate
118        let speed = baud_to_speed(config.baud)?;
119        unsafe {
120            libc::cfsetispeed(&mut termios, speed);
121            libc::cfsetospeed(&mut termios, speed);
122        }
123
124        // Blocking read with VMIN=1, VTIME=0
125        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        // Clear O_NONBLOCK for blocking reads
134        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    /// Get the raw fd.
148    pub fn as_raw_fd(&self) -> RawFd {
149        self.fd
150    }
151
152    /// Get a Read handle (File wrapping a dup'd fd).
153    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    /// Get a Write handle (File wrapping a dup'd fd).
162    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
185/// Map baud rate u32 to libc speed_t constant.
186fn 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/// Create a pseudo-terminal pair for testing. Returns (master_fd, slave_fd).
227///
228/// The master and slave are configured for raw mode to avoid terminal processing.
229#[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    // Set both sides to raw mode to avoid terminal character processing
247    for fd in [master, slave] {
248        let mut termios: libc::termios = unsafe { std::mem::zeroed() };
249        unsafe { libc::tcgetattr(fd, &mut termios) };
250        // cfmakeraw equivalent
251        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        // Poll with timeout to avoid blocking forever
299        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        // Verify common baud rates map successfully
315        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}