Skip to main content

rns_net/interface/
serial_iface.rs

1//! Serial interface with HDLC framing.
2//!
3//! Matches Python `SerialInterface.py` — opens a serial port,
4//! reads with HDLC framing, reconnects on failure.
5
6use std::io::{self, Read, Write};
7use std::os::fd::AsRawFd;
8use std::thread;
9use std::time::Duration;
10
11use rns_core::transport::types::InterfaceId;
12
13use crate::event::{Event, EventSender};
14use crate::hdlc;
15use crate::interface::{
16    poll_readable, wait_while_active, ListenerControl, UnavailableWriter, Writer,
17};
18use crate::serial::{Parity, SerialConfig, SerialPort};
19
20/// Configuration for a Serial interface.
21#[derive(Debug, Clone)]
22pub struct SerialIfaceConfig {
23    pub name: String,
24    pub port: String,
25    pub speed: u32,
26    pub data_bits: u8,
27    pub parity: Parity,
28    pub stop_bits: u8,
29    pub interface_id: InterfaceId,
30}
31
32impl Default for SerialIfaceConfig {
33    fn default() -> Self {
34        SerialIfaceConfig {
35            name: String::new(),
36            port: String::new(),
37            speed: 9600,
38            data_bits: 8,
39            parity: Parity::None,
40            stop_bits: 1,
41            interface_id: InterfaceId(0),
42        }
43    }
44}
45
46/// Writer that sends HDLC-framed data over a serial port.
47struct SerialWriter {
48    file: std::fs::File,
49}
50
51impl Writer for SerialWriter {
52    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
53        self.file.write_all(&hdlc::frame(data))
54    }
55}
56
57/// Start the serial interface. Opens the port, spawns reader thread.
58/// Returns the writer for the driver.
59pub fn start(config: SerialIfaceConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
60    start_with_control(config, tx).map(|(writer, _)| writer)
61}
62
63fn start_with_control(
64    config: SerialIfaceConfig,
65    tx: EventSender,
66) -> io::Result<(Box<dyn Writer>, ListenerControl)> {
67    let serial_config = SerialConfig {
68        path: config.port.clone(),
69        baud: config.speed,
70        data_bits: config.data_bits,
71        parity: config.parity,
72        stop_bits: config.stop_bits,
73    };
74
75    let control = ListenerControl::new();
76    let port = match SerialPort::open(&serial_config) {
77        Ok(port) => port,
78        Err(error) => {
79            log::warn!(
80                "[{}] could not open serial port {}: {}; retrying",
81                config.name,
82                config.port,
83                error
84            );
85            let retry_control = control.clone();
86            thread::Builder::new()
87                .name(format!("serial-reconnect-{}", config.interface_id.0))
88                .spawn(move || {
89                    if let Some(reader) = reconnect(&config, &tx, &retry_control) {
90                        let id = config.interface_id;
91                        reader_loop(reader, id, config, tx, retry_control);
92                    }
93                })?;
94            return Ok((Box::new(UnavailableWriter), control));
95        }
96    };
97    let reader_file = port.reader()?;
98    let writer_file = port.writer()?;
99
100    let id = config.interface_id;
101
102    // Signal interface up
103    let _ = tx.send(Event::InterfaceUp(id, None, None));
104
105    // Short delay matching Python's configure_device sleep
106    thread::sleep(Duration::from_millis(500));
107
108    // Spawn reader thread
109    let reader_control = control.clone();
110    thread::Builder::new()
111        .name(format!("serial-reader-{}", id.0))
112        .spawn(move || {
113            reader_loop(reader_file, id, config, tx, reader_control);
114        })?;
115
116    Ok((Box::new(SerialWriter { file: writer_file }), control))
117}
118
119/// Reader thread: reads from serial, HDLC-decodes, sends frames to driver.
120fn reader_loop(
121    mut reader: std::fs::File,
122    id: InterfaceId,
123    config: SerialIfaceConfig,
124    tx: EventSender,
125    control: ListenerControl,
126) {
127    let mut decoder = hdlc::Decoder::new();
128    let mut buf = [0u8; 4096];
129
130    loop {
131        if control.should_stop() {
132            return;
133        }
134        match poll_readable(reader.as_raw_fd(), Duration::from_millis(200)) {
135            Ok(false) => continue,
136            Ok(true) => {}
137            Err(e) => {
138                if control.should_stop() {
139                    return;
140                }
141                log::warn!("[{}] serial poll error: {}", config.name, e);
142                let _ = tx.send(Event::InterfaceDown(id));
143                match reconnect(&config, &tx, &control) {
144                    Some(new_reader) => {
145                        reader = new_reader;
146                        decoder = hdlc::Decoder::new();
147                        continue;
148                    }
149                    None => return,
150                }
151            }
152        }
153        match reader.read(&mut buf) {
154            Ok(0) => {
155                if control.should_stop() {
156                    return;
157                }
158                // EOF — port closed
159                log::warn!("[{}] serial port closed", config.name);
160                let _ = tx.send(Event::InterfaceDown(id));
161                match reconnect(&config, &tx, &control) {
162                    Some(new_reader) => {
163                        reader = new_reader;
164                        decoder = hdlc::Decoder::new();
165                        continue;
166                    }
167                    None => return,
168                }
169            }
170            Ok(n) => {
171                for frame in decoder.feed(&buf[..n]) {
172                    if tx
173                        .send(Event::Frame {
174                            interface_id: id,
175                            data: frame,
176                            rssi: None,
177                            snr: None,
178                        })
179                        .is_err()
180                    {
181                        return; // driver shut down
182                    }
183                }
184            }
185            Err(e) => {
186                if control.should_stop() {
187                    return;
188                }
189                log::warn!("[{}] serial read error: {}", config.name, e);
190                let _ = tx.send(Event::InterfaceDown(id));
191                match reconnect(&config, &tx, &control) {
192                    Some(new_reader) => {
193                        reader = new_reader;
194                        decoder = hdlc::Decoder::new();
195                        continue;
196                    }
197                    None => return,
198                }
199            }
200        }
201    }
202}
203
204/// Attempt to reconnect the serial port. Returns new reader file on success.
205fn reconnect(
206    config: &SerialIfaceConfig,
207    tx: &EventSender,
208    control: &ListenerControl,
209) -> Option<std::fs::File> {
210    loop {
211        if !wait_while_active(control, Duration::from_secs(5)) {
212            return None;
213        }
214        log::info!(
215            "[{}] attempting to reconnect serial port {}...",
216            config.name,
217            config.port
218        );
219
220        let serial_config = SerialConfig {
221            path: config.port.clone(),
222            baud: config.speed,
223            data_bits: config.data_bits,
224            parity: config.parity,
225            stop_bits: config.stop_bits,
226        };
227
228        match SerialPort::open(&serial_config) {
229            Ok(port) => match (port.reader(), port.writer()) {
230                (Ok(reader), Ok(writer_file)) => {
231                    if control.should_stop() {
232                        return None;
233                    }
234                    log::info!("[{}] serial port reconnected", config.name);
235                    let new_writer: Box<dyn Writer> = Box::new(SerialWriter { file: writer_file });
236                    let _ = tx.send(Event::InterfaceUp(
237                        config.interface_id,
238                        Some(new_writer),
239                        None,
240                    ));
241                    if !wait_while_active(control, Duration::from_millis(500)) {
242                        return None;
243                    }
244                    return Some(reader);
245                }
246                _ => {
247                    log::warn!(
248                        "[{}] failed to get reader/writer from serial port",
249                        config.name
250                    );
251                }
252            },
253            Err(e) => {
254                log::warn!("[{}] serial reconnect failed: {}", config.name, e);
255            }
256        }
257    }
258}
259
260// --- Factory implementation ---
261
262use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
263use rns_core::transport::types::InterfaceInfo;
264use std::collections::HashMap;
265
266/// Factory for `SerialInterface`.
267pub struct SerialFactory;
268
269impl InterfaceFactory for SerialFactory {
270    fn type_name(&self) -> &str {
271        "SerialInterface"
272    }
273
274    fn default_ifac_size(&self) -> usize {
275        8
276    }
277
278    fn parse_config(
279        &self,
280        name: &str,
281        id: InterfaceId,
282        params: &HashMap<String, String>,
283    ) -> Result<Box<dyn InterfaceConfigData>, String> {
284        let port = params
285            .get("port")
286            .cloned()
287            .ok_or_else(|| "SerialInterface requires 'port'".to_string())?;
288
289        let speed = params
290            .get("speed")
291            .and_then(|v| v.parse().ok())
292            .unwrap_or(9600u32);
293
294        let data_bits = params
295            .get("databits")
296            .and_then(|v| v.parse().ok())
297            .unwrap_or(8u8);
298
299        let parity = params
300            .get("parity")
301            .map(|v| match v.to_lowercase().as_str() {
302                "e" | "even" => crate::serial::Parity::Even,
303                "o" | "odd" => crate::serial::Parity::Odd,
304                _ => crate::serial::Parity::None,
305            })
306            .unwrap_or(crate::serial::Parity::None);
307
308        let stop_bits = params
309            .get("stopbits")
310            .and_then(|v| v.parse().ok())
311            .unwrap_or(1u8);
312
313        Ok(Box::new(SerialIfaceConfig {
314            name: name.to_string(),
315            port,
316            speed,
317            data_bits,
318            parity,
319            stop_bits,
320            interface_id: id,
321        }))
322    }
323
324    fn start(
325        &self,
326        config: Box<dyn InterfaceConfigData>,
327        ctx: StartContext,
328    ) -> std::io::Result<StartResult> {
329        let serial_config = *config
330            .into_any()
331            .downcast::<SerialIfaceConfig>()
332            .map_err(|_| {
333                std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
334            })?;
335
336        let id = serial_config.interface_id;
337        let name = serial_config.name.clone();
338        let bitrate = Some(serial_config.speed as u64);
339
340        let info = InterfaceInfo {
341            id,
342            name,
343            mode: ctx.mode,
344            gravity: ctx.gravity,
345            recursive_prs: ctx.recursive_prs,
346            announces_from_internal: ctx.announces_from_internal,
347            announces_to_internal: ctx.announces_to_internal,
348            out_capable: true,
349            in_capable: true,
350            bitrate,
351            airtime_profile: None,
352            announce_rate_target: None,
353            announce_rate_grace: 0,
354            announce_rate_penalty: 0.0,
355            announce_cap: rns_core::constants::ANNOUNCE_CAP,
356            is_local_client: false,
357            wants_tunnel: false,
358            tunnel_id: None,
359            mtu: rns_core::constants::MTU as u32,
360            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
361            ia_freq: 0.0,
362            ip_freq: 0.0,
363            op_freq: 0.0,
364            op_samples: 0,
365            started: crate::time::now(),
366        };
367
368        let (writer, control) = start_with_control(serial_config, ctx.tx)?;
369
370        Ok(StartResult::Simple {
371            control: Some(control),
372            id,
373            info,
374            writer,
375            interface_type_name: "SerialInterface".to_string(),
376        })
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::serial::open_pty_pair;
384    use std::os::unix::io::{AsRawFd, FromRawFd};
385    use std::sync::mpsc;
386    use std::time::Duration;
387
388    /// Helper: poll an fd for reading with timeout (ms). Returns true if data available.
389    fn poll_read(fd: i32, timeout_ms: i32) -> bool {
390        let mut pfd = libc::pollfd {
391            fd,
392            events: libc::POLLIN,
393            revents: 0,
394        };
395        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
396        ret > 0
397    }
398
399    #[test]
400    fn serial_hdlc_roundtrip() {
401        let (master_fd, slave_fd) = open_pty_pair().unwrap();
402        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
403        let mut slave_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
404
405        // Write an HDLC frame to the master side
406        let payload: Vec<u8> = (0..32).collect();
407        let framed = hdlc::frame(&payload);
408        master_file.write_all(&framed).unwrap();
409        master_file.flush().unwrap();
410
411        // Read from slave using HDLC decoder
412        assert!(
413            poll_read(slave_file.as_raw_fd(), 2000),
414            "should have data available"
415        );
416
417        let mut decoder = hdlc::Decoder::new();
418        let mut buf = [0u8; 4096];
419        let n = slave_file.read(&mut buf).unwrap();
420        let frames = decoder.feed(&buf[..n]);
421        assert_eq!(frames.len(), 1);
422        assert_eq!(frames[0], payload);
423    }
424
425    #[test]
426    fn serial_writer_frames() {
427        let (master_fd, slave_fd) = open_pty_pair().unwrap();
428
429        let writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
430        let mut writer = SerialWriter { file: writer_file };
431
432        let payload = vec![0x01, 0x02, 0x7E, 0x7D, 0x03]; // includes bytes that need HDLC escaping
433        writer.send_frame(&payload).unwrap();
434
435        // Read from master
436        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
437        assert!(poll_read(master_file.as_raw_fd(), 2000), "should have data");
438
439        let expected = hdlc::frame(&payload);
440        let mut buf = [0u8; 256];
441        let n = master_file.read(&mut buf).unwrap();
442        assert_eq!(&buf[..n], &expected[..]);
443    }
444
445    #[test]
446    fn serial_fragmented_read() {
447        let (master_fd, slave_fd) = open_pty_pair().unwrap();
448        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
449        let slave_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
450
451        let (tx, rx) = mpsc::channel::<Event>();
452
453        // Write a frame in two parts to the master
454        let payload: Vec<u8> = (0..32).collect();
455        let framed = hdlc::frame(&payload);
456        let mid = framed.len() / 2;
457
458        // Spawn reader thread first (it will block waiting for data)
459        let reader_thread = thread::spawn(move || {
460            let mut reader = slave_file;
461            let mut decoder = hdlc::Decoder::new();
462            let mut buf = [0u8; 4096];
463
464            loop {
465                match reader.read(&mut buf) {
466                    Ok(n) if n > 0 => {
467                        if let Some(frame) = decoder.feed(&buf[..n]).into_iter().next() {
468                            let _ = tx.send(Event::Frame {
469                                interface_id: InterfaceId(0),
470                                data: frame,
471                                rssi: None,
472                                snr: None,
473                            });
474                            return;
475                        }
476                    }
477                    _ => return,
478                }
479            }
480        });
481
482        // Write first half
483        master_file.write_all(&framed[..mid]).unwrap();
484        master_file.flush().unwrap();
485
486        // Small delay then write second half
487        thread::sleep(Duration::from_millis(50));
488        master_file.write_all(&framed[mid..]).unwrap();
489        master_file.flush().unwrap();
490
491        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
492        match event {
493            Event::Frame { data, .. } => assert_eq!(data, payload),
494            other => panic!("expected Frame, got {:?}", other),
495        }
496
497        let _ = reader_thread.join();
498    }
499
500    #[test]
501    fn serial_reconnect_on_close() {
502        let (master_fd, slave_fd) = open_pty_pair().unwrap();
503
504        let (tx, rx) = mpsc::channel::<Event>();
505        let id = InterfaceId(42);
506
507        // Spawn a reader thread on the slave side
508        let slave_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
509        let reader_thread = thread::spawn(move || {
510            let mut reader = slave_file;
511            let mut buf = [0u8; 4096];
512            let mut decoder = hdlc::Decoder::new();
513            loop {
514                match reader.read(&mut buf) {
515                    Ok(0) => {
516                        let _ = tx.send(Event::InterfaceDown(id));
517                        return;
518                    }
519                    Ok(n) => {
520                        for frame in decoder.feed(&buf[..n]) {
521                            let _ = tx.send(Event::Frame {
522                                interface_id: id,
523                                data: frame,
524                                rssi: None,
525                                snr: None,
526                            });
527                        }
528                    }
529                    Err(_) => {
530                        let _ = tx.send(Event::InterfaceDown(id));
531                        return;
532                    }
533                }
534            }
535        });
536
537        // Close the master fd — this should cause the reader to get EOF
538        unsafe { libc::close(master_fd) };
539
540        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
541        assert!(matches!(event, Event::InterfaceDown(InterfaceId(42))));
542
543        let _ = reader_thread.join();
544    }
545
546    #[test]
547    fn detach_stops_idle_serial_reader_without_reconnect() {
548        let (master_fd, slave_fd) = open_pty_pair().unwrap();
549        let master = unsafe { std::fs::File::from_raw_fd(master_fd) };
550        let slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
551        let (tx, rx) = crate::event::channel();
552        let control = ListenerControl::new();
553        let reader_control = control.clone();
554        let config = SerialIfaceConfig {
555            name: "detach-test".into(),
556            interface_id: InterfaceId(71),
557            ..Default::default()
558        };
559
560        let reader =
561            thread::spawn(move || reader_loop(slave, InterfaceId(71), config, tx, reader_control));
562        thread::sleep(Duration::from_millis(50));
563        control.request_stop();
564        reader.join().unwrap();
565        drop(master);
566        assert!(rx.try_recv().is_err());
567    }
568
569    #[test]
570    fn missing_serial_port_recovers_after_it_appears() {
571        let tempdir = tempfile::tempdir().unwrap();
572        let port_path = tempdir.path().join("serial-port");
573        let (tx, rx) = crate::event::channel();
574        let config = SerialIfaceConfig {
575            name: "late-port".into(),
576            port: port_path.display().to_string(),
577            interface_id: InterfaceId(73),
578            ..Default::default()
579        };
580
581        let (_writer, control) = start_with_control(config, tx).unwrap();
582        assert!(rx.try_recv().is_err());
583
584        let (master_fd, slave_fd) = open_pty_pair().unwrap();
585        let master = unsafe { std::fs::File::from_raw_fd(master_fd) };
586        let slave_path = std::fs::read_link(format!("/proc/self/fd/{slave_fd}")).unwrap();
587        std::os::unix::fs::symlink(slave_path, &port_path).unwrap();
588        let slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
589
590        let event = rx.recv_timeout(Duration::from_secs(8)).unwrap();
591        assert!(matches!(
592            event,
593            Event::InterfaceUp(InterfaceId(73), Some(_), None)
594        ));
595        control.request_stop();
596        drop(slave);
597        drop(master);
598    }
599}