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