Skip to main content

rns_net/interface/rnode/
mod.rs

1//! RNode LoRa radio interface.
2//!
3//! Manages serial connection to RNode device, detects firmware,
4//! configures radio parameters per subinterface. Supports single-radio
5//! (RNodeInterface) and multi-radio (RNodeMultiInterface) devices.
6//!
7//! Matches Python `RNodeInterface.py` and `RNodeMultiInterface.py`.
8
9mod folded;
10mod transport;
11
12use std::io::{self, Read, Write};
13use std::os::fd::AsRawFd;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Mutex};
16use std::thread;
17use std::time::Duration;
18use transport::Transport;
19
20/// The shared write side of an RNode connection.
21pub type DeviceWriter = crate::interface::device_writer::DeviceWriter<Transport>;
22
23use rns_core::transport::types::{AirtimeProfile, InterfaceId};
24
25use crate::event::{Event, EventSender, InterfaceTelemetry};
26use crate::interface::{lock_or_recover, Writer};
27use crate::rnode_kiss;
28use crate::serial::{Parity, SerialConfig};
29
30/// Validation limits matching Python RNodeInterface.
31pub const FREQ_MIN: u32 = 137_000_000;
32pub const FREQ_MAX: u32 = 3_000_000_000;
33pub const BW_MIN: u32 = 7_800;
34pub const BW_MAX: u32 = 1_625_000;
35pub const SF_MIN: u8 = 5;
36pub const SF_MAX: u8 = 12;
37pub const CR_MIN: u8 = 5;
38pub const CR_MAX: u8 = 8;
39pub const TXPOWER_MIN: i8 = 0;
40pub const TXPOWER_MAX: i8 = 37;
41pub const HW_MTU: u32 = 508;
42pub const LORA_PREAMBLE_SYMBOLS: u16 = 8;
43pub const LORA_EXPLICIT_HEADER: bool = true;
44pub const LORA_CRC: bool = true;
45
46/// Configuration for one RNode subinterface (radio).
47#[derive(Debug, Clone)]
48pub struct RNodeSubConfig {
49    pub name: String,
50    pub vport: u8,
51    pub outgoing: bool,
52    pub frequency: u32,
53    pub bandwidth: u32,
54    pub txpower: i8,
55    pub spreading_factor: u8,
56    pub coding_rate: u8,
57    pub flow_control: bool,
58    pub st_alock: Option<f32>,
59    pub lt_alock: Option<f32>,
60}
61
62/// Configuration for the RNode device.
63#[derive(Debug, Clone)]
64pub struct RNodeConfig {
65    pub name: String,
66    pub port: String,
67    pub speed: u32,
68    pub subinterfaces: Vec<RNodeSubConfig>,
69    pub multi: bool,
70    pub id_interval: Option<u32>,
71    pub id_callsign: Option<Vec<u8>>,
72    pub base_interface_id: InterfaceId,
73    /// Pre-opened file descriptor (e.g. from a USB bridge socketpair on Android).
74    /// When set, `start()` uses this fd directly instead of opening `port`.
75    pub pre_opened_fd: Option<i32>,
76    pub underlay_mark: Option<u32>,
77    pub runtime: Arc<Mutex<RNodeRuntime>>,
78}
79
80#[derive(Debug, Clone)]
81pub struct RNodeRuntime {
82    pub sub: RNodeSubConfig,
83    pub writer: Option<Arc<Mutex<DeviceWriter>>>,
84}
85
86impl RNodeRuntime {
87    pub fn from_config(config: &RNodeConfig) -> Self {
88        Self {
89            sub: config
90                .subinterfaces
91                .first()
92                .cloned()
93                .unwrap_or_else(|| RNodeSubConfig {
94                    name: config.name.clone(),
95                    vport: 0,
96                    outgoing: true,
97                    frequency: 868_000_000,
98                    bandwidth: 125_000,
99                    txpower: 7,
100                    spreading_factor: 8,
101                    coding_rate: 5,
102                    flow_control: false,
103                    st_alock: None,
104                    lt_alock: None,
105                }),
106            writer: None,
107        }
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct RNodeRuntimeConfigHandle {
113    pub interface_name: String,
114    pub runtime: Arc<Mutex<RNodeRuntime>>,
115    pub startup: RNodeRuntime,
116}
117
118impl Default for RNodeConfig {
119    fn default() -> Self {
120        let mut config = RNodeConfig {
121            name: String::new(),
122            port: String::new(),
123            speed: 115200,
124            subinterfaces: Vec::new(),
125            multi: false,
126            id_interval: None,
127            id_callsign: None,
128            base_interface_id: InterfaceId(0),
129            pre_opened_fd: None,
130            underlay_mark: None,
131            runtime: Arc::new(Mutex::new(RNodeRuntime {
132                sub: RNodeSubConfig {
133                    name: String::new(),
134                    vport: 0,
135                    outgoing: true,
136                    frequency: 868_000_000,
137                    bandwidth: 125_000,
138                    txpower: 7,
139                    spreading_factor: 8,
140                    coding_rate: 5,
141                    flow_control: false,
142                    st_alock: None,
143                    lt_alock: None,
144                },
145                writer: None,
146            })),
147        };
148        let startup = RNodeRuntime::from_config(&config);
149        config.runtime = Arc::new(Mutex::new(startup));
150        config
151    }
152}
153
154/// Validate subinterface configuration. Returns error message if invalid.
155pub fn validate_sub_config(sub: &RNodeSubConfig) -> Option<String> {
156    if sub.frequency < FREQ_MIN || sub.frequency > FREQ_MAX {
157        return Some(format!(
158            "Invalid frequency {} for {}",
159            sub.frequency, sub.name
160        ));
161    }
162    if sub.bandwidth < BW_MIN || sub.bandwidth > BW_MAX {
163        return Some(format!(
164            "Invalid bandwidth {} for {}",
165            sub.bandwidth, sub.name
166        ));
167    }
168    if sub.spreading_factor < SF_MIN || sub.spreading_factor > SF_MAX {
169        return Some(format!(
170            "Invalid SF {} for {}",
171            sub.spreading_factor, sub.name
172        ));
173    }
174    if sub.coding_rate < CR_MIN || sub.coding_rate > CR_MAX {
175        return Some(format!("Invalid CR {} for {}", sub.coding_rate, sub.name));
176    }
177    if sub.txpower < TXPOWER_MIN || sub.txpower > TXPOWER_MAX {
178        return Some(format!("Invalid TX power {} for {}", sub.txpower, sub.name));
179    }
180    if let Some(st) = sub.st_alock {
181        if !(0.0..=100.0).contains(&st) {
182            return Some(format!("Invalid ST airtime limit {} for {}", st, sub.name));
183        }
184    }
185    if let Some(lt) = sub.lt_alock {
186        if !(0.0..=100.0).contains(&lt) {
187            return Some(format!("Invalid LT airtime limit {} for {}", lt, sub.name));
188        }
189    }
190    None
191}
192
193/// Estimate the LoRa physical bit rate for announce bandwidth gating.
194///
195/// Formula: SF * (bandwidth / 2^SF) * (4 / coding_rate).
196pub fn estimate_lora_bitrate_bps(sub: &RNodeSubConfig) -> u64 {
197    let symbols_per_second = sub.bandwidth as f64 / (1u64 << sub.spreading_factor) as f64;
198    let bits_per_symbol = sub.spreading_factor as f64 * (4.0 / sub.coding_rate as f64);
199    (symbols_per_second * bits_per_symbol).round().max(1.0) as u64
200}
201
202pub fn lora_airtime_profile(sub: &RNodeSubConfig) -> AirtimeProfile {
203    AirtimeProfile::Lora {
204        bandwidth: sub.bandwidth,
205        spreading_factor: sub.spreading_factor,
206        coding_rate: sub.coding_rate,
207        preamble_symbols: LORA_PREAMBLE_SYMBOLS,
208        explicit_header: LORA_EXPLICIT_HEADER,
209        crc: LORA_CRC,
210    }
211}
212
213/// Writer for a specific RNode subinterface.
214/// Wraps a shared serial writer with subinterface-specific data framing.
215struct RNodeSubWriter {
216    confirmed_pending: Option<Vec<u8>>,
217    writer: Arc<Mutex<DeviceWriter>>,
218    index: u8,
219    flow_control: bool,
220    flow_state: Arc<Mutex<SubFlowState>>,
221    multi: bool,
222}
223
224struct SubFlowState {
225    ready: bool,
226    queue: std::collections::VecDeque<Vec<u8>>,
227}
228
229fn make_sub_writer(
230    writer: Arc<Mutex<DeviceWriter>>,
231    index: u8,
232    flow_control: bool,
233    flow_state: Arc<Mutex<SubFlowState>>,
234    multi: bool,
235) -> Box<dyn Writer> {
236    Box::new(RNodeSubWriter {
237        confirmed_pending: None,
238        writer,
239        index,
240        flow_control,
241        flow_state,
242        multi,
243    })
244}
245
246impl Writer for RNodeSubWriter {
247    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
248        if !self.flow_control {
249            return self.send_frame(data);
250        }
251        self.confirmed_pending = Some(if self.multi {
252            rnode_kiss::rnode_multi_data_frame(self.index, data)
253        } else {
254            rnode_kiss::rnode_data_frame(self.index, data)
255        });
256        self.flush()
257    }
258
259    fn flush(&mut self) -> io::Result<()> {
260        if self.confirmed_pending.is_none() {
261            return Ok(());
262        }
263        let mut state = lock_or_recover(&self.flow_state, "rnode flow state");
264        if !state.ready || !state.queue.is_empty() {
265            return Err(io::ErrorKind::WouldBlock.into());
266        }
267        state.ready = false;
268        drop(state);
269        lock_or_recover(&self.writer, "rnode shared writer")
270            .write_all(&self.confirmed_pending.take().unwrap())
271    }
272    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
273        let frame = if self.multi {
274            rnode_kiss::rnode_multi_data_frame(self.index, data)
275        } else {
276            rnode_kiss::rnode_data_frame(self.index, data)
277        };
278        if self.flow_control {
279            let mut state = lock_or_recover(&self.flow_state, "rnode flow state");
280            if state.ready {
281                state.ready = false;
282                drop(state);
283                lock_or_recover(&self.writer, "rnode shared writer").write_all(&frame)
284            } else {
285                state.queue.push_back(data.to_vec());
286                Ok(())
287            }
288        } else {
289            lock_or_recover(&self.writer, "rnode shared writer").write_all(&frame)
290        }
291    }
292
293    fn enqueue_confirmed(
294        &mut self,
295        data: &[u8],
296        completion: &mut Option<crate::link_send::Completion>,
297    ) -> io::Result<()> {
298        // Driver-run (see `sends_inline`): a confirmed frame is taken only
299        // when it is written at once; else WouldBlock leaves the completion
300        // untouched and the driver retries.
301        let frame = self.frame(data);
302        let mut state = lock_or_recover(&self.flow_state, "rnode flow state");
303        let mut writer = lock_or_recover(&self.writer, "rnode shared writer");
304        if (self.flow_control && (!state.ready || !state.queue.is_empty()))
305            || !writer.fits(frame.len())
306        {
307            return Err(io::Error::new(
308                io::ErrorKind::WouldBlock,
309                "RNode cannot take a frame yet",
310            ));
311        }
312        writer.write_all(&frame)?;
313        if self.flow_control {
314            state.ready = false;
315        }
316        drop((writer, state));
317        if let Some(completion) = completion.take() {
318            completion.finish(Ok(()));
319        }
320        Ok(())
321    }
322
323    fn sends_inline(&self) -> bool {
324        // With interface I/O folded the shared device writer never blocks.
325        crate::engine::FOLD_IO
326    }
327
328    fn flush_pending(&mut self) {
329        lock_or_recover(&self.writer, "rnode shared writer").drain();
330    }
331}
332
333impl RNodeSubWriter {
334    fn frame(&self, data: &[u8]) -> Vec<u8> {
335        if self.multi {
336            rnode_kiss::rnode_multi_data_frame(self.index, data)
337        } else {
338            rnode_kiss::rnode_data_frame(self.index, data)
339        }
340    }
341}
342
343type RNodeWriters = Vec<(InterfaceId, Box<dyn Writer>)>;
344
345/// Start the RNode interface.
346///
347/// Opens serial port, spawns reader thread which performs detect+configure,
348/// then enters data relay mode.
349///
350/// Returns one `(InterfaceId, Box<dyn Writer>)` per subinterface.
351pub fn start(
352    config: RNodeConfig,
353    tx: EventSender,
354) -> io::Result<Vec<(InterfaceId, Box<dyn Writer>)>> {
355    start_with_control(config, tx).map(|(writers, _)| writers)
356}
357
358fn start_with_control(
359    config: RNodeConfig,
360    tx: EventSender,
361) -> io::Result<(RNodeWriters, super::ListenerControl)> {
362    // Validate all subinterface configs upfront
363    for sub in &config.subinterfaces {
364        if let Some(err) = validate_sub_config(sub) {
365            return Err(io::Error::new(io::ErrorKind::InvalidInput, err));
366        }
367    }
368
369    if config.subinterfaces.is_empty() {
370        return Err(io::Error::new(
371            io::ErrorKind::InvalidInput,
372            "No subinterfaces configured",
373        ));
374    }
375
376    let (reader_file, writer_transport) = if let Some(fd) = config.pre_opened_fd {
377        // Pre-opened fd from USB bridge — dup for independent reader/writer handles
378        Transport::open_from_fd(fd)?
379    } else {
380        let serial_config = SerialConfig {
381            path: config.port.clone(),
382            baud: config.speed,
383            data_bits: 8,
384            parity: Parity::None,
385            stop_bits: 1,
386        };
387        Transport::open(&serial_config, config.underlay_mark)?
388    };
389    let shared_writer = Arc::new(Mutex::new(if crate::engine::FOLD_IO {
390        DeviceWriter::driven(
391            writer_transport,
392            Arc::clone(tx.poller()),
393            crate::interface::loop_writer::write_key(config.base_interface_id),
394            &config.name,
395        )
396    } else {
397        DeviceWriter::new(writer_transport)
398    }));
399
400    // Build per-subinterface writers and IDs
401    let num_subs = config.subinterfaces.len();
402    let mut writers: Vec<(InterfaceId, Box<dyn Writer>)> = Vec::with_capacity(num_subs);
403    let mut flow_states: Vec<Arc<Mutex<SubFlowState>>> = Vec::with_capacity(num_subs);
404
405    for (i, sub) in config.subinterfaces.iter().enumerate() {
406        let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
407        let flow_state = Arc::new(Mutex::new(SubFlowState {
408            ready: true,
409            queue: std::collections::VecDeque::new(),
410        }));
411        flow_states.push(flow_state.clone());
412        let sub_writer = make_sub_writer(
413            shared_writer.clone(),
414            sub.vport,
415            sub.flow_control,
416            flow_state,
417            config.multi,
418        );
419        writers.push((sub_id, sub_writer));
420    }
421
422    // Spawn reader thread
423    let reader_shared_writer = shared_writer.clone();
424    {
425        let mut runtime = lock_or_recover(&config.runtime, "rnode runtime");
426        runtime.writer = Some(shared_writer.clone());
427        runtime.sub = config
428            .subinterfaces
429            .first()
430            .cloned()
431            .unwrap_or(runtime.sub.clone());
432    }
433    let running = Arc::new(AtomicBool::new(true));
434    if crate::engine::FOLD_IO {
435        folded::RNodeLink::new(config, tx, shared_writer, flow_states, Arc::clone(&running))
436            .start(reader_file)?;
437        return Ok((writers, super::ListenerControl::with_running_flag(running)));
438    }
439    let reader_config = config.clone();
440    let reader_flow_states = flow_states;
441    let reader_running = Arc::clone(&running);
442    thread::Builder::new()
443        .name(format!("rnode-reader-{}", config.base_interface_id.0))
444        .spawn(move || {
445            reader_loop(
446                reader_file,
447                reader_shared_writer,
448                reader_config,
449                tx,
450                reader_flow_states,
451                reader_running,
452            );
453        })?;
454
455    // Spawn keepalive thread — sends periodic DETECT to prevent firmware
456    // bridge idle timeout (ESP32 RNode reverts to standalone after 30s idle).
457    let keepalive_writer = shared_writer.clone();
458    let keepalive_name = config.name.clone();
459    let keepalive_running = Arc::clone(&running);
460    thread::Builder::new()
461        .name(format!("rnode-keepalive-{}", config.base_interface_id.0))
462        .spawn(move || {
463            let detect = rnode_kiss::detect_request();
464            while keepalive_running.load(Ordering::Relaxed) {
465                if !wait_while_running(&keepalive_running, Duration::from_secs(15)) {
466                    break;
467                }
468                if let Err(e) =
469                    lock_or_recover(&keepalive_writer, "rnode shared writer").write_all(&detect)
470                {
471                    log::debug!("[{}] keepalive write failed: {}", keepalive_name, e);
472                }
473            }
474        })?;
475
476    Ok((writers, super::ListenerControl::with_running_flag(running)))
477}
478
479fn wait_while_running(running: &AtomicBool, duration: Duration) -> bool {
480    let deadline = std::time::Instant::now() + duration;
481    while running.load(Ordering::Relaxed) {
482        let remaining = deadline.saturating_duration_since(std::time::Instant::now());
483        if remaining.is_zero() {
484            return true;
485        }
486        thread::sleep(remaining.min(Duration::from_millis(100)));
487    }
488    false
489}
490
491/// Reader loop: detect device, configure radios, then relay data frames.
492fn reader_loop(
493    mut reader: Transport,
494    writer: Arc<Mutex<DeviceWriter>>,
495    config: RNodeConfig,
496    tx: EventSender,
497    flow_states: Vec<Arc<Mutex<SubFlowState>>>,
498    running: Arc<AtomicBool>,
499) {
500    const RECONNECT_INITIAL_DELAY: Duration = Duration::from_millis(200);
501    const RECONNECT_MAX_DELAY: Duration = Duration::from_secs(2);
502    // Initial delay for hardware init (matches Python: sleep(2.0))
503    if !wait_while_running(&running, Duration::from_secs(2)) {
504        return;
505    }
506    let mut connected_once = false;
507    // RNode is the PHY-stat-capable interface: retain each modem report until
508    // the matching frame is enqueued, attach it to that frame, then clear it
509    // so later frames cannot inherit stale RSSI or SNR.
510    let mut last_rssi: Option<i16> = None;
511    let mut last_snr: Option<f32> = None;
512    if let Err(e) = detect_and_configure(&mut reader, &writer, &config) {
513        log::error!("[{}] initial RNode setup failed: {}", config.name, e);
514        return;
515    }
516    signal_interface_up(&tx, &config, &writer, &flow_states, connected_once);
517    connected_once = true;
518    while running.load(Ordering::Relaxed) {
519        let mut decoder = rnode_kiss::RNodeDecoder::new();
520        let mut buf = [0u8; 4096];
521        let disconnected = loop {
522            if !running.load(Ordering::Relaxed) {
523                return;
524            }
525            if !reader_ready(&reader, Duration::from_millis(200)) {
526                continue;
527            }
528            match reader.read(&mut buf) {
529                Ok(0) => {
530                    if !running.load(Ordering::Relaxed) {
531                        return;
532                    }
533                    log::warn!("[{}] serial port closed", config.name);
534                    signal_interface_down(&tx, &config);
535                    break true;
536                }
537                Ok(n) => {
538                    for event in decoder.feed(&buf[..n]) {
539                        let phy_stat = matches!(
540                            &event,
541                            rnode_kiss::RNodeEvent::StatRssi(_)
542                                | rnode_kiss::RNodeEvent::StatSnr(_)
543                        );
544                        if update_pending_rx_metadata(&event, &mut last_rssi, &mut last_snr) {
545                            if phy_stat {
546                                publish_phy_telemetry(&tx, &config, last_rssi, last_snr);
547                            }
548                            continue;
549                        }
550                        match event {
551                            rnode_kiss::RNodeEvent::DataFrame { index, data } => {
552                                let Some(position) = config
553                                    .subinterfaces
554                                    .iter()
555                                    .position(|sub| usize::from(sub.vport) == index)
556                                else {
557                                    continue;
558                                };
559                                let sub_id =
560                                    InterfaceId(config.base_interface_id.0 + position as u64);
561                                if tx
562                                    .send(Event::Frame {
563                                        interface_id: sub_id,
564                                        data: data.into(),
565                                        rssi: last_rssi,
566                                        snr: last_snr,
567                                    })
568                                    .is_err()
569                                {
570                                    return;
571                                }
572                                last_rssi = None;
573                                last_snr = None;
574                            }
575                            rnode_kiss::RNodeEvent::Ready => {
576                                // Flow control: unlock all subinterfaces that have flow_control
577                                for (i, fs) in flow_states.iter().enumerate() {
578                                    if config.subinterfaces[i].flow_control {
579                                        process_flow_queue(
580                                            fs,
581                                            &writer,
582                                            config.subinterfaces[i].vport,
583                                            config.multi,
584                                        );
585                                    }
586                                }
587                            }
588                            rnode_kiss::RNodeEvent::Error(code) => {
589                                log::error!("[{}] RNode error: 0x{:02X}", config.name, code);
590                            }
591                            _ => {
592                                // Status updates logged but not acted on
593                            }
594                        }
595                    }
596                }
597                Err(e) => {
598                    if !running.load(Ordering::Relaxed) {
599                        return;
600                    }
601                    log::error!("[{}] serial read error: {}", config.name, e);
602                    signal_interface_down(&tx, &config);
603                    break true;
604                }
605            }
606        };
607
608        clear_pending_rx_metadata(&mut last_rssi, &mut last_snr);
609
610        if !running.load(Ordering::Relaxed) || !disconnected || config.pre_opened_fd.is_some() {
611            return;
612        }
613
614        let mut backoff = RECONNECT_INITIAL_DELAY;
615        while running.load(Ordering::Relaxed) {
616            match reopen_connection(&config, &writer) {
617                Ok(new_reader) => {
618                    reset_flow_states(&flow_states);
619                    reader = new_reader;
620                    if let Err(e) = detect_and_configure(&mut reader, &writer, &config) {
621                        log::warn!("[{}] reconnect configure failed: {}", config.name, e);
622                        if !wait_while_running(&running, backoff) {
623                            return;
624                        }
625                        backoff = std::cmp::min(backoff.saturating_mul(2), RECONNECT_MAX_DELAY);
626                        continue;
627                    }
628                    signal_interface_up(&tx, &config, &writer, &flow_states, connected_once);
629                    break;
630                }
631                Err(e) => {
632                    log::warn!("[{}] reconnect open failed: {}", config.name, e);
633                    if !wait_while_running(&running, backoff) {
634                        return;
635                    }
636                    backoff = std::cmp::min(backoff.saturating_mul(2), RECONNECT_MAX_DELAY);
637                }
638            }
639        }
640    }
641}
642
643fn reader_ready(reader: &Transport, timeout: Duration) -> bool {
644    let mut pfd = libc::pollfd {
645        fd: reader.as_raw_fd(),
646        events: libc::POLLIN,
647        revents: 0,
648    };
649    let timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
650    unsafe { libc::poll(&mut pfd, 1, timeout_ms) > 0 }
651}
652
653fn detect_and_configure(
654    reader: &mut Transport,
655    writer: &Arc<Mutex<DeviceWriter>>,
656    config: &RNodeConfig,
657) -> io::Result<()> {
658    let detect_cmd = rnode_kiss::detect_request();
659    let mut cmd = detect_cmd;
660    cmd.extend_from_slice(&rnode_kiss::rnode_command(
661        rnode_kiss::CMD_FW_VERSION,
662        &[0x00],
663    ));
664    cmd.extend_from_slice(&rnode_kiss::rnode_command(
665        rnode_kiss::CMD_FW_DETAIL,
666        &[0x00],
667    ));
668    cmd.extend_from_slice(&rnode_kiss::rnode_command(
669        rnode_kiss::CMD_PLATFORM,
670        &[0x00],
671    ));
672    cmd.extend_from_slice(&rnode_kiss::rnode_command(rnode_kiss::CMD_MCU, &[0x00]));
673
674    lock_or_recover(writer, "rnode shared writer").write_all(&cmd)?;
675
676    let mut decoder = rnode_kiss::RNodeDecoder::new();
677    let mut buf = [0u8; 4096];
678    let mut detected = false;
679    let detect_start = std::time::Instant::now();
680    let detect_timeout = Duration::from_secs(5);
681
682    while !detected && detect_start.elapsed() < detect_timeout {
683        match reader.read(&mut buf) {
684            Ok(0) => {
685                return Err(io::Error::new(
686                    io::ErrorKind::UnexpectedEof,
687                    "serial port closed during detect",
688                ));
689            }
690            Ok(n) => {
691                for event in decoder.feed(&buf[..n]) {
692                    match event {
693                        rnode_kiss::RNodeEvent::Detected(true) => {
694                            detected = true;
695                            log::info!("[{}] RNode device detected", config.name);
696                        }
697                        rnode_kiss::RNodeEvent::FirmwareVersion { major, minor } => {
698                            log::info!("[{}] firmware version {}.{}", config.name, major, minor);
699                        }
700                        rnode_kiss::RNodeEvent::FirmwareDetail(ref detail) => {
701                            log::info!("[{}] firmware detail: {}", config.name, detail);
702                        }
703                        rnode_kiss::RNodeEvent::Platform(p) => {
704                            log::info!("[{}] platform: 0x{:02X}", config.name, p);
705                        }
706                        rnode_kiss::RNodeEvent::Mcu(m) => {
707                            log::info!("[{}] MCU: 0x{:02X}", config.name, m);
708                        }
709                        _ => {}
710                    }
711                }
712            }
713            Err(e) => {
714                return Err(io::Error::new(
715                    e.kind(),
716                    format!("serial read error during detect: {}", e),
717                ));
718            }
719        }
720    }
721
722    if !detected {
723        return Err(io::Error::new(
724            io::ErrorKind::TimedOut,
725            "RNode detection timed out",
726        ));
727    }
728
729    for sub in &config.subinterfaces {
730        configure_subinterface(writer, sub.vport, sub, config.multi)?;
731    }
732
733    thread::sleep(Duration::from_millis(300));
734    log::info!(
735        "[{}] RNode configured with {} subinterface(s)",
736        config.name,
737        config.subinterfaces.len()
738    );
739    Ok(())
740}
741
742fn signal_interface_down(tx: &EventSender, config: &RNodeConfig) {
743    for i in 0..config.subinterfaces.len() {
744        let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
745        let _ = tx.send(Event::InterfaceDown(sub_id));
746    }
747}
748
749fn update_pending_rx_metadata(
750    event: &rnode_kiss::RNodeEvent,
751    last_rssi: &mut Option<i16>,
752    last_snr: &mut Option<f32>,
753) -> bool {
754    match event {
755        rnode_kiss::RNodeEvent::StatRssi(rssi) => {
756            *last_rssi = Some(*rssi as i16 - 157);
757            true
758        }
759        rnode_kiss::RNodeEvent::StatSnr(snr) => {
760            *last_snr = Some(*snr as f32 * 0.25);
761            true
762        }
763        _ => false,
764    }
765}
766
767fn signal_interface_up(
768    tx: &EventSender,
769    config: &RNodeConfig,
770    writer: &Arc<Mutex<DeviceWriter>>,
771    flow_states: &[Arc<Mutex<SubFlowState>>],
772    reconnected: bool,
773) {
774    for (i, flow_state) in flow_states.iter().enumerate() {
775        let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
776        let new_writer = reconnected.then(|| {
777            make_sub_writer(
778                writer.clone(),
779                config.subinterfaces[i].vport,
780                config.subinterfaces[i].flow_control,
781                flow_state.clone(),
782                config.multi,
783            )
784        });
785        let _ = tx.send(Event::InterfaceUp(sub_id, new_writer, None));
786    }
787    publish_phy_telemetry(tx, config, None, None);
788}
789
790/// Python RNodeInterface's signal-quality mapping, clamped to 0..100 and
791/// rounded to one decimal place.
792fn signal_quality(spreading_factor: u8, snr: f32) -> f64 {
793    let sfs = spreading_factor.saturating_sub(7) as f64;
794    let q_snr_min = -9.0 - sfs * 2.0;
795    let q_snr_max = 6.0;
796    let span = q_snr_max - q_snr_min;
797    if span <= 0.0 {
798        return 0.0;
799    }
800    let quality = (((f64::from(snr) - q_snr_min) / span) * 100.0).clamp(0.0, 100.0);
801    (quality * 10.0).round() / 10.0
802}
803
804fn publish_phy_telemetry(
805    tx: &EventSender,
806    config: &RNodeConfig,
807    rssi: Option<i16>,
808    snr: Option<f32>,
809) {
810    for event in phy_telemetry(config, rssi, snr) {
811        let _ = tx.send(event);
812    }
813}
814
815/// One telemetry event per subinterface.
816fn phy_telemetry(
817    config: &RNodeConfig,
818    rssi: Option<i16>,
819    snr: Option<f32>,
820) -> impl Iterator<Item = Event> + '_ {
821    config
822        .subinterfaces
823        .iter()
824        .enumerate()
825        .map(move |(i, sub)| {
826            let telemetry = InterfaceTelemetry {
827                rssi,
828                snr,
829                signal_quality: snr.map(|snr| signal_quality(sub.spreading_factor, snr)),
830                frequency: Some(sub.frequency),
831                bandwidth: Some(sub.bandwidth),
832                txpower: Some(sub.txpower),
833                spreading_factor: Some(sub.spreading_factor),
834                coding_rate: Some(sub.coding_rate),
835                channel: None,
836                modulation: Some("LoRa".to_string()),
837                ..Default::default()
838            };
839            Event::InterfaceTelemetry {
840                interface_id: InterfaceId(config.base_interface_id.0 + i as u64),
841                telemetry: Box::new(telemetry),
842            }
843        })
844}
845
846fn reset_flow_states(flow_states: &[Arc<Mutex<SubFlowState>>]) {
847    for flow_state in flow_states {
848        let mut state = lock_or_recover(flow_state, "rnode flow state");
849        state.ready = true;
850        state.queue.clear();
851    }
852}
853
854fn clear_pending_rx_metadata(last_rssi: &mut Option<i16>, last_snr: &mut Option<f32>) {
855    *last_rssi = None;
856    *last_snr = None;
857}
858
859fn reopen_connection(
860    config: &RNodeConfig,
861    writer: &Arc<Mutex<DeviceWriter>>,
862) -> io::Result<Transport> {
863    let serial_config = SerialConfig {
864        path: config.port.clone(),
865        baud: config.speed,
866        data_bits: 8,
867        parity: Parity::None,
868        stop_bits: 1,
869    };
870
871    let (reader, new_writer) = Transport::open(&serial_config, config.underlay_mark)?;
872    lock_or_recover(writer, "rnode shared writer").replace(new_writer);
873    Ok(reader)
874}
875
876/// Configure a single subinterface on the RNode device.
877pub(crate) fn configure_subinterface(
878    writer: &Arc<Mutex<DeviceWriter>>,
879    index: u8,
880    sub: &RNodeSubConfig,
881    multi: bool,
882) -> io::Result<()> {
883    let mut w = lock_or_recover(writer, "rnode shared writer");
884
885    // For multi-radio, send select command before each parameter
886    let freq_bytes = [
887        (sub.frequency >> 24) as u8,
888        (sub.frequency >> 16) as u8,
889        (sub.frequency >> 8) as u8,
890        (sub.frequency & 0xFF) as u8,
891    ];
892    let bw_bytes = [
893        (sub.bandwidth >> 24) as u8,
894        (sub.bandwidth >> 16) as u8,
895        (sub.bandwidth >> 8) as u8,
896        (sub.bandwidth & 0xFF) as u8,
897    ];
898    let txp = if sub.txpower < 0 {
899        (sub.txpower as i16 + 256) as u8
900    } else {
901        sub.txpower as u8
902    };
903
904    if multi {
905        w.write_all(&rnode_kiss::rnode_select_command(
906            index,
907            rnode_kiss::CMD_FREQUENCY,
908            &freq_bytes,
909        ))?;
910        w.write_all(&rnode_kiss::rnode_select_command(
911            index,
912            rnode_kiss::CMD_BANDWIDTH,
913            &bw_bytes,
914        ))?;
915        w.write_all(&rnode_kiss::rnode_select_command(
916            index,
917            rnode_kiss::CMD_TXPOWER,
918            &[txp],
919        ))?;
920        w.write_all(&rnode_kiss::rnode_select_command(
921            index,
922            rnode_kiss::CMD_SF,
923            &[sub.spreading_factor],
924        ))?;
925        w.write_all(&rnode_kiss::rnode_select_command(
926            index,
927            rnode_kiss::CMD_CR,
928            &[sub.coding_rate],
929        ))?;
930    } else {
931        w.write_all(&rnode_kiss::rnode_command(
932            rnode_kiss::CMD_FREQUENCY,
933            &freq_bytes,
934        ))?;
935        w.write_all(&rnode_kiss::rnode_command(
936            rnode_kiss::CMD_BANDWIDTH,
937            &bw_bytes,
938        ))?;
939        w.write_all(&rnode_kiss::rnode_command(rnode_kiss::CMD_TXPOWER, &[txp]))?;
940        w.write_all(&rnode_kiss::rnode_command(
941            rnode_kiss::CMD_SF,
942            &[sub.spreading_factor],
943        ))?;
944        w.write_all(&rnode_kiss::rnode_command(
945            rnode_kiss::CMD_CR,
946            &[sub.coding_rate],
947        ))?;
948    }
949
950    // Airtime locks
951    if let Some(st) = sub.st_alock {
952        let st_val = (st * 100.0) as u16;
953        let st_bytes = [(st_val >> 8) as u8, (st_val & 0xFF) as u8];
954        if multi {
955            w.write_all(&rnode_kiss::rnode_select_command(
956                index,
957                rnode_kiss::CMD_ST_ALOCK,
958                &st_bytes,
959            ))?;
960        } else {
961            w.write_all(&rnode_kiss::rnode_command(
962                rnode_kiss::CMD_ST_ALOCK,
963                &st_bytes,
964            ))?;
965        }
966    }
967    if let Some(lt) = sub.lt_alock {
968        let lt_val = (lt * 100.0) as u16;
969        let lt_bytes = [(lt_val >> 8) as u8, (lt_val & 0xFF) as u8];
970        if multi {
971            w.write_all(&rnode_kiss::rnode_select_command(
972                index,
973                rnode_kiss::CMD_LT_ALOCK,
974                &lt_bytes,
975            ))?;
976        } else {
977            w.write_all(&rnode_kiss::rnode_command(
978                rnode_kiss::CMD_LT_ALOCK,
979                &lt_bytes,
980            ))?;
981        }
982    }
983
984    // Turn on radio
985    if multi {
986        w.write_all(&rnode_kiss::rnode_select_command(
987            index,
988            rnode_kiss::CMD_RADIO_STATE,
989            &[rnode_kiss::RADIO_STATE_ON],
990        ))?;
991    } else {
992        w.write_all(&rnode_kiss::rnode_command(
993            rnode_kiss::CMD_RADIO_STATE,
994            &[rnode_kiss::RADIO_STATE_ON],
995        ))?;
996    }
997
998    Ok(())
999}
1000
1001/// Process flow control queue for a subinterface.
1002fn process_flow_queue(
1003    flow_state: &Arc<Mutex<SubFlowState>>,
1004    writer: &Arc<Mutex<DeviceWriter>>,
1005    index: u8,
1006    multi: bool,
1007) {
1008    let mut state = lock_or_recover(flow_state, "rnode flow state");
1009    if let Some(data) = state.queue.pop_front() {
1010        state.ready = false;
1011        drop(state);
1012        let frame = if multi {
1013            rnode_kiss::rnode_multi_data_frame(index, &data)
1014        } else {
1015            rnode_kiss::rnode_data_frame(index, &data)
1016        };
1017        let _ = lock_or_recover(writer, "rnode shared writer").write_all(&frame);
1018    } else {
1019        state.ready = true;
1020    }
1021}
1022
1023// --- Factory implementation ---
1024
1025use super::{
1026    ConfigSection, InterfaceConfigData, InterfaceFactory, StartContext, StartResult, SubInterface,
1027};
1028use rns_core::transport::types::InterfaceInfo;
1029use std::collections::HashMap;
1030
1031/// Factory for `RNodeInterface`.
1032pub struct RNodeFactory;
1033
1034impl InterfaceFactory for RNodeFactory {
1035    fn type_name(&self) -> &str {
1036        "RNodeInterface"
1037    }
1038
1039    fn default_ifac_size(&self) -> usize {
1040        8
1041    }
1042
1043    fn parse_config(
1044        &self,
1045        name: &str,
1046        id: InterfaceId,
1047        params: &HashMap<String, String>,
1048    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1049        let pre_opened_fd = params.get("fd").and_then(|v| v.parse::<i32>().ok());
1050
1051        let port = params
1052            .get("port")
1053            .cloned()
1054            .or_else(|| pre_opened_fd.map(|_| "usb-bridge".to_string()))
1055            .ok_or_else(|| "RNodeInterface requires 'port' or 'fd'".to_string())?;
1056
1057        let speed = params
1058            .get("speed")
1059            .and_then(|v| v.parse().ok())
1060            .unwrap_or(115200u32);
1061
1062        let frequency = params
1063            .get("frequency")
1064            .and_then(|v| v.parse().ok())
1065            .unwrap_or(868_000_000u32);
1066
1067        let bandwidth = params
1068            .get("bandwidth")
1069            .and_then(|v| v.parse().ok())
1070            .unwrap_or(125_000u32);
1071
1072        let txpower = params
1073            .get("txpower")
1074            .and_then(|v| v.parse().ok())
1075            .unwrap_or(7i8);
1076
1077        let spreading_factor = params
1078            .get("spreadingfactor")
1079            .or_else(|| params.get("spreading_factor"))
1080            .and_then(|v| v.parse().ok())
1081            .unwrap_or(8u8);
1082
1083        let coding_rate = params
1084            .get("codingrate")
1085            .or_else(|| params.get("coding_rate"))
1086            .and_then(|v| v.parse().ok())
1087            .unwrap_or(5u8);
1088
1089        let flow_control = params
1090            .get("flow_control")
1091            .and_then(|v| crate::config::parse_bool_pub(v))
1092            .unwrap_or(false);
1093
1094        let st_alock = params.get("st_alock").and_then(|v| v.parse().ok());
1095
1096        let lt_alock = params.get("lt_alock").and_then(|v| v.parse().ok());
1097
1098        let id_interval = params.get("id_interval").and_then(|v| v.parse().ok());
1099
1100        let id_callsign = params.get("id_callsign").map(|v| v.as_bytes().to_vec());
1101
1102        let sub = RNodeSubConfig {
1103            name: name.to_string(),
1104            vport: 0,
1105            outgoing: true,
1106            frequency,
1107            bandwidth,
1108            txpower,
1109            spreading_factor,
1110            coding_rate,
1111            flow_control,
1112            st_alock,
1113            lt_alock,
1114        };
1115
1116        Ok(Box::new(RNodeConfig {
1117            name: name.to_string(),
1118            port,
1119            speed,
1120            subinterfaces: vec![sub],
1121            multi: false,
1122            id_interval,
1123            id_callsign,
1124            base_interface_id: id,
1125            pre_opened_fd,
1126            underlay_mark: None,
1127            runtime: Arc::new(Mutex::new(RNodeRuntime {
1128                sub: RNodeSubConfig {
1129                    name: name.to_string(),
1130                    vport: 0,
1131                    outgoing: true,
1132                    frequency,
1133                    bandwidth,
1134                    txpower,
1135                    spreading_factor,
1136                    coding_rate,
1137                    flow_control,
1138                    st_alock,
1139                    lt_alock,
1140                },
1141                writer: None,
1142            })),
1143        }))
1144    }
1145
1146    fn start(
1147        &self,
1148        config: Box<dyn InterfaceConfigData>,
1149        ctx: StartContext,
1150    ) -> std::io::Result<StartResult> {
1151        let mut rnode_config = *config.into_any().downcast::<RNodeConfig>().map_err(|_| {
1152            std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
1153        })?;
1154        rnode_config.underlay_mark = ctx.underlay_mark;
1155
1156        let sub_bitrates: Vec<u64> = rnode_config
1157            .subinterfaces
1158            .iter()
1159            .map(estimate_lora_bitrate_bps)
1160            .collect();
1161        let airtime_profiles: Vec<AirtimeProfile> = rnode_config
1162            .subinterfaces
1163            .iter()
1164            .map(lora_airtime_profile)
1165            .collect();
1166
1167        let (pairs, control) = start_with_control(rnode_config.clone(), ctx.tx)?;
1168
1169        let mut subs = Vec::with_capacity(pairs.len());
1170        for (index, (sub_id, writer)) in pairs.into_iter().enumerate() {
1171            let sub_name = rnode_config.subinterfaces[index].name.clone();
1172
1173            let info = InterfaceInfo {
1174                stamp_costs: Default::default(),
1175                id: sub_id,
1176                name: sub_name,
1177                mode: ctx.mode,
1178                gravity: ctx.gravity,
1179                recursive_prs: ctx.recursive_prs,
1180                announces_from_internal: ctx.announces_from_internal,
1181                announces_to_internal: ctx.announces_to_internal,
1182                out_capable: rnode_config.subinterfaces[index].outgoing,
1183                in_capable: true,
1184                bitrate: sub_bitrates.get(index).copied(),
1185                airtime_profile: airtime_profiles.get(index).copied(),
1186                announce_rate_target: None,
1187                announce_rate_grace: 0,
1188                announce_rate_penalty: 0.0,
1189                announce_cap: rns_core::constants::ANNOUNCE_CAP,
1190                is_local_client: false,
1191                wants_tunnel: false,
1192                tunnel_id: None,
1193                mtu: rns_core::constants::MTU as u32,
1194                ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
1195                ia_freq: 0.0,
1196                ip_freq: 0.0,
1197                op_freq: 0.0,
1198                op_samples: 0,
1199                started: crate::time::now(),
1200            };
1201
1202            subs.push(SubInterface {
1203                id: sub_id,
1204                info,
1205                writer,
1206                interface_type_name: if rnode_config.multi {
1207                    "RNodeMultiInterface"
1208                } else {
1209                    "RNodeInterface"
1210                }
1211                .to_string(),
1212            });
1213        }
1214
1215        Ok(StartResult::Multi {
1216            subinterfaces: subs,
1217            control: Some(control),
1218        })
1219    }
1220}
1221
1222/// Factory for ConfigObj-driven `RNodeMultiInterface` devices.
1223pub struct RNodeMultiFactory;
1224
1225impl InterfaceFactory for RNodeMultiFactory {
1226    fn type_name(&self) -> &str {
1227        "RNodeMultiInterface"
1228    }
1229
1230    fn default_ifac_size(&self) -> usize {
1231        8
1232    }
1233
1234    fn parse_config(
1235        &self,
1236        name: &str,
1237        id: InterfaceId,
1238        params: &HashMap<String, String>,
1239    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1240        self.parse_config_section(
1241            name,
1242            id,
1243            ConfigSection {
1244                params,
1245                children: &[],
1246            },
1247        )
1248    }
1249
1250    fn parse_config_section(
1251        &self,
1252        name: &str,
1253        id: InterfaceId,
1254        section: ConfigSection<'_>,
1255    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1256        let base = RNodeFactory.parse_config(name, id, section.params)?;
1257        let mut config = *base
1258            .into_any()
1259            .downcast::<RNodeConfig>()
1260            .map_err(|_| "wrong RNode config type".to_string())?;
1261
1262        let mut subinterfaces = Vec::new();
1263        for child in section.children {
1264            let enabled = child
1265                .params
1266                .get("interface_enabled")
1267                .or_else(|| child.params.get("enabled"))
1268                .and_then(|value| crate::config::parse_bool_pub(value))
1269                .unwrap_or(true);
1270            if !enabled {
1271                continue;
1272            }
1273            if subinterfaces.len() >= 11 {
1274                return Err("RNodeMultiInterface supports at most 11 enabled subinterfaces".into());
1275            }
1276            let vport = child
1277                .params
1278                .get("vport")
1279                .ok_or_else(|| format!("subinterface '{}' requires 'vport'", child.name))?
1280                .parse::<u8>()
1281                .map_err(|_| format!("invalid RNode virtual port for '{}'", child.name))?;
1282            if vport > 10
1283                || subinterfaces
1284                    .iter()
1285                    .any(|sub: &RNodeSubConfig| sub.vport == vport)
1286            {
1287                return Err(format!("invalid or duplicate RNode virtual port {}", vport));
1288            }
1289            let parse_u32 = |key: &str, default| {
1290                child
1291                    .params
1292                    .get(key)
1293                    .and_then(|v| v.parse().ok())
1294                    .unwrap_or(default)
1295            };
1296            let parse_u8_alias = |first: &str, second: &str, default| {
1297                child
1298                    .params
1299                    .get(first)
1300                    .or_else(|| child.params.get(second))
1301                    .and_then(|v| v.parse().ok())
1302                    .unwrap_or(default)
1303            };
1304            let sub = RNodeSubConfig {
1305                name: child.name.clone(),
1306                vport,
1307                outgoing: child
1308                    .params
1309                    .get("outgoing")
1310                    .and_then(|v| crate::config::parse_bool_pub(v))
1311                    .unwrap_or(true),
1312                frequency: parse_u32("frequency", 868_000_000),
1313                bandwidth: parse_u32("bandwidth", 125_000),
1314                txpower: child
1315                    .params
1316                    .get("txpower")
1317                    .and_then(|v| v.parse().ok())
1318                    .unwrap_or(7),
1319                spreading_factor: parse_u8_alias("spreadingfactor", "spreading_factor", 8),
1320                coding_rate: parse_u8_alias("codingrate", "coding_rate", 5),
1321                flow_control: child
1322                    .params
1323                    .get("flow_control")
1324                    .and_then(|v| crate::config::parse_bool_pub(v))
1325                    .unwrap_or(false),
1326                st_alock: child
1327                    .params
1328                    .get("airtime_limit_short")
1329                    .or_else(|| child.params.get("st_alock"))
1330                    .and_then(|v| v.parse().ok()),
1331                lt_alock: child
1332                    .params
1333                    .get("airtime_limit_long")
1334                    .or_else(|| child.params.get("lt_alock"))
1335                    .and_then(|v| v.parse().ok()),
1336            };
1337            if let Some(error) = validate_sub_config(&sub) {
1338                return Err(error);
1339            }
1340            subinterfaces.push(sub);
1341        }
1342        if subinterfaces.is_empty() {
1343            return Err("RNodeMultiInterface requires at least one enabled subinterface".into());
1344        }
1345        config.multi = true;
1346        config.subinterfaces = subinterfaces;
1347        *lock_or_recover(&config.runtime, "rnode runtime") = RNodeRuntime::from_config(&config);
1348        Ok(Box::new(config))
1349    }
1350
1351    fn start(
1352        &self,
1353        config: Box<dyn InterfaceConfigData>,
1354        ctx: StartContext,
1355    ) -> io::Result<StartResult> {
1356        RNodeFactory.start(config, ctx)
1357    }
1358}
1359
1360pub(crate) fn rnode_runtime_handle_from_config(config: &RNodeConfig) -> RNodeRuntimeConfigHandle {
1361    RNodeRuntimeConfigHandle {
1362        interface_name: config.name.clone(),
1363        runtime: Arc::clone(&config.runtime),
1364        startup: RNodeRuntime::from_config(config),
1365    }
1366}
1367
1368#[cfg(test)]
1369mod tests {
1370    use super::*;
1371    use crate::event;
1372    use crate::kiss;
1373    use crate::serial::open_pty_pair;
1374    use std::os::unix::io::{AsRawFd, FromRawFd};
1375    use std::path::PathBuf;
1376    use std::sync::mpsc::RecvTimeoutError;
1377    use tempfile::tempdir;
1378    /// Helper: poll an fd for reading with timeout (ms).
1379    fn poll_read(fd: i32, timeout_ms: i32) -> bool {
1380        let mut pfd = libc::pollfd {
1381            fd,
1382            events: libc::POLLIN,
1383            revents: 0,
1384        };
1385        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
1386        ret > 0
1387    }
1388
1389    /// Read all available bytes from an fd.
1390    fn read_available(file: &mut Transport) -> Vec<u8> {
1391        let mut all = Vec::new();
1392        let mut buf = [0u8; 4096];
1393        while poll_read(file.as_raw_fd(), 100) {
1394            match file.read(&mut buf) {
1395                Ok(n) if n > 0 => all.extend_from_slice(&buf[..n]),
1396                _ => break,
1397            }
1398        }
1399        all
1400    }
1401
1402    fn slave_tty_path(fd: i32) -> PathBuf {
1403        let mut buf = [0u8; 256];
1404        let rc = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr().cast(), buf.len()) };
1405        assert_eq!(rc, 0, "ttyname_r failed for fd {}", fd);
1406        let nul = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
1407        PathBuf::from(std::str::from_utf8(&buf[..nul]).unwrap())
1408    }
1409
1410    unsafe fn transport_from_raw_fd(fd: i32) -> Transport {
1411        Transport::Serial(std::fs::File::from_raw_fd(fd))
1412    }
1413
1414    fn wait_for_interface_event<F>(
1415        rx: &crate::event::EventReceiver,
1416        timeout: Duration,
1417        predicate: F,
1418    ) -> Event
1419    where
1420        F: Fn(&Event) -> bool,
1421    {
1422        let deadline = std::time::Instant::now() + timeout;
1423        loop {
1424            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1425            match rx.recv_timeout(remaining) {
1426                Ok(event) if predicate(&event) => return event,
1427                Ok(_) => continue,
1428                Err(RecvTimeoutError::Timeout) => panic!("timed out waiting for interface event"),
1429                Err(RecvTimeoutError::Disconnected) => panic!("event channel disconnected"),
1430            }
1431        }
1432    }
1433
1434    /// Mock RNode: respond to detect with DETECT_RESP, FW version, platform
1435    fn mock_respond_detect(master: &mut Transport) {
1436        // Respond: DETECT_RESP
1437        master
1438            .write_all(&rnode_kiss::rnode_command(
1439                rnode_kiss::CMD_DETECT,
1440                &[rnode_kiss::DETECT_RESP],
1441            ))
1442            .unwrap();
1443        // FW version 1.74
1444        master
1445            .write_all(&rnode_kiss::rnode_command(
1446                rnode_kiss::CMD_FW_VERSION,
1447                &[0x01, 0x4A],
1448            ))
1449            .unwrap();
1450        // Platform ESP32
1451        master
1452            .write_all(&rnode_kiss::rnode_command(
1453                rnode_kiss::CMD_PLATFORM,
1454                &[0x80],
1455            ))
1456            .unwrap();
1457        // MCU
1458        master
1459            .write_all(&rnode_kiss::rnode_command(rnode_kiss::CMD_MCU, &[0x01]))
1460            .unwrap();
1461        master.flush().unwrap();
1462    }
1463
1464    #[test]
1465    fn rnode_detect_over_pty() {
1466        // Test that the RNode decoder can parse detect responses from a PTY
1467        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1468        let mut master = unsafe { transport_from_raw_fd(master_fd) };
1469        let mut slave = unsafe { transport_from_raw_fd(slave_fd) };
1470
1471        // Write detect response to master
1472        mock_respond_detect(&mut master);
1473
1474        let mut decoder = rnode_kiss::RNodeDecoder::new();
1475        let mut events = Vec::new();
1476        let deadline = std::time::Instant::now() + Duration::from_secs(2);
1477
1478        while std::time::Instant::now() < deadline {
1479            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1480            let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
1481            if !poll_read(slave.as_raw_fd(), timeout_ms) {
1482                break;
1483            }
1484
1485            let chunk = read_available(&mut slave);
1486            if chunk.is_empty() {
1487                continue;
1488            }
1489
1490            events.extend(decoder.feed(&chunk));
1491
1492            let saw_detect = events
1493                .iter()
1494                .any(|e| matches!(e, rnode_kiss::RNodeEvent::Detected(true)));
1495            let saw_firmware = events
1496                .iter()
1497                .any(|e| matches!(e, rnode_kiss::RNodeEvent::FirmwareVersion { .. }));
1498            if saw_detect && saw_firmware {
1499                break;
1500            }
1501        }
1502
1503        assert!(
1504            events
1505                .iter()
1506                .any(|e| matches!(e, rnode_kiss::RNodeEvent::Detected(true))),
1507            "should detect device"
1508        );
1509        assert!(
1510            events
1511                .iter()
1512                .any(|e| matches!(e, rnode_kiss::RNodeEvent::FirmwareVersion { .. })),
1513            "should get firmware version"
1514        );
1515    }
1516
1517    #[test]
1518    fn rnode_configure_commands() {
1519        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1520        let mut master = unsafe { transport_from_raw_fd(master_fd) };
1521        let writer_file = unsafe { transport_from_raw_fd(libc::dup(slave_fd)) };
1522        let writer = Arc::new(Mutex::new(DeviceWriter::new(writer_file)));
1523
1524        let sub = RNodeSubConfig {
1525            name: "test".into(),
1526            vport: 0,
1527            outgoing: true,
1528            frequency: 868_000_000,
1529            bandwidth: 125_000,
1530            txpower: 7,
1531            spreading_factor: 8,
1532            coding_rate: 5,
1533            flow_control: false,
1534            st_alock: None,
1535            lt_alock: None,
1536        };
1537
1538        configure_subinterface(&writer, 0, &sub, false).unwrap();
1539
1540        // Read what was sent
1541        let data = read_available(&mut master);
1542
1543        // Should contain frequency command
1544        assert!(
1545            data.windows(2)
1546                .any(|w| w[0] == kiss::FEND && w[1] == rnode_kiss::CMD_FREQUENCY),
1547            "should contain FREQUENCY command"
1548        );
1549        // Should contain bandwidth command
1550        assert!(
1551            data.windows(2)
1552                .any(|w| w[0] == kiss::FEND && w[1] == rnode_kiss::CMD_BANDWIDTH),
1553            "should contain BANDWIDTH command"
1554        );
1555        // Should contain RADIO_STATE ON
1556        assert!(
1557            data.windows(3).any(|w| w[0] == kiss::FEND
1558                && w[1] == rnode_kiss::CMD_RADIO_STATE
1559                && w[2] == rnode_kiss::RADIO_STATE_ON),
1560            "should contain RADIO_STATE ON command"
1561        );
1562
1563        unsafe { libc::close(slave_fd) };
1564    }
1565
1566    #[test]
1567    fn rnode_data_roundtrip() {
1568        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1569        let mut master = unsafe { transport_from_raw_fd(master_fd) };
1570        let slave = unsafe { transport_from_raw_fd(slave_fd) };
1571
1572        // Write a data frame (subinterface 0) to master
1573        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
1574        let frame = rnode_kiss::rnode_data_frame(0, &payload);
1575        master.write_all(&frame).unwrap();
1576        master.flush().unwrap();
1577
1578        // Read from slave with RNode decoder
1579        assert!(poll_read(slave.as_raw_fd(), 2000));
1580        let mut decoder = rnode_kiss::RNodeDecoder::new();
1581        let mut buf = [0u8; 4096];
1582        let mut slave_file = slave;
1583        let n = slave_file.read(&mut buf).unwrap();
1584        let events = decoder.feed(&buf[..n]);
1585
1586        assert_eq!(events.len(), 1);
1587        match &events[0] {
1588            rnode_kiss::RNodeEvent::DataFrame { index, data } => {
1589                assert_eq!(*index, 0);
1590                assert_eq!(data, &payload);
1591            }
1592            other => panic!("expected DataFrame, got {:?}", other),
1593        }
1594    }
1595
1596    #[test]
1597    fn rnode_flow_control() {
1598        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1599        let writer_file = unsafe { transport_from_raw_fd(slave_fd) };
1600        let shared_writer = Arc::new(Mutex::new(DeviceWriter::new(writer_file)));
1601
1602        let flow_state = Arc::new(Mutex::new(SubFlowState {
1603            ready: true,
1604            queue: std::collections::VecDeque::new(),
1605        }));
1606
1607        let mut sub_writer = RNodeSubWriter {
1608            confirmed_pending: None,
1609            writer: shared_writer.clone(),
1610            index: 0,
1611            flow_control: true,
1612            flow_state: flow_state.clone(),
1613            multi: false,
1614        };
1615
1616        // First send: should go through (ready=true) and set ready=false
1617        sub_writer.send_frame(b"hello").unwrap();
1618        assert!(!flow_state.lock().unwrap().ready);
1619
1620        // Second send: should be queued
1621        sub_writer.send_frame(b"world").unwrap();
1622        assert_eq!(flow_state.lock().unwrap().queue.len(), 1);
1623
1624        // Process flow queue (simulates CMD_READY)
1625        process_flow_queue(&flow_state, &shared_writer, 0, false);
1626        assert_eq!(flow_state.lock().unwrap().queue.len(), 0);
1627        assert!(!flow_state.lock().unwrap().ready); // sent queued, still locked
1628
1629        // Process again with empty queue: sets ready=true
1630        process_flow_queue(&flow_state, &shared_writer, 0, false);
1631        assert!(flow_state.lock().unwrap().ready);
1632
1633        unsafe { libc::close(master_fd) };
1634    }
1635
1636    #[test]
1637    fn signal_quality_matches_python_mapping_and_clamps() {
1638        assert_eq!(signal_quality(8, -11.0), 0.0);
1639        assert_eq!(signal_quality(8, -2.5), 50.0);
1640        assert_eq!(signal_quality(8, 6.0), 100.0);
1641        assert_eq!(signal_quality(8, 30.0), 100.0);
1642        assert_eq!(signal_quality(8, -40.0), 0.0);
1643        assert_eq!(signal_quality(10, -2.5), 59.5);
1644    }
1645
1646    #[test]
1647    fn rnode_reset_clears_pending_rx_metadata() {
1648        let mut last_rssi = Some(-101);
1649        let mut last_snr = Some(7.25);
1650
1651        clear_pending_rx_metadata(&mut last_rssi, &mut last_snr);
1652
1653        assert_eq!(last_rssi, None);
1654        assert_eq!(last_snr, None);
1655    }
1656
1657    #[test]
1658    fn confirmed_rnode_frame_waits_for_ready_and_flushes_once() {
1659        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1660        let mut master = unsafe { transport_from_raw_fd(master_fd) };
1661        let state = Arc::new(Mutex::new(SubFlowState {
1662            ready: false,
1663            queue: std::collections::VecDeque::new(),
1664        }));
1665        let mut writer = RNodeSubWriter {
1666            confirmed_pending: None,
1667            writer: Arc::new(Mutex::new(DeviceWriter::new(unsafe {
1668                transport_from_raw_fd(slave_fd)
1669            }))),
1670            index: 0,
1671            flow_control: true,
1672            flow_state: state.clone(),
1673            multi: false,
1674        };
1675        assert_eq!(
1676            writer
1677                .send_frame_confirmed(b"confirmed")
1678                .unwrap_err()
1679                .kind(),
1680            io::ErrorKind::WouldBlock
1681        );
1682        assert!(state.lock().unwrap().queue.is_empty());
1683        assert!(!poll_read(master.as_raw_fd(), 20));
1684        state.lock().unwrap().ready = true;
1685        Writer::flush(&mut writer).unwrap();
1686        let expected = rnode_kiss::rnode_data_frame(0, b"confirmed");
1687        let mut actual = vec![0; expected.len()];
1688        master.read_exact(&mut actual).unwrap();
1689        assert_eq!(actual, expected);
1690        Writer::flush(&mut writer).unwrap();
1691        assert!(!poll_read(master.as_raw_fd(), 20));
1692    }
1693
1694    #[test]
1695    fn rnode_snr_metadata_does_not_depend_on_rssi() {
1696        let mut last_rssi = None;
1697        let mut last_snr = None;
1698
1699        assert!(update_pending_rx_metadata(
1700            &rnode_kiss::RNodeEvent::StatSnr(-12),
1701            &mut last_rssi,
1702            &mut last_snr,
1703        ));
1704
1705        assert_eq!(last_rssi, None);
1706        assert_eq!(last_snr, Some(-3.0));
1707    }
1708
1709    #[test]
1710    fn rnode_rx_metadata_reports_rssi_and_snr() {
1711        let mut last_rssi = None;
1712        let mut last_snr = None;
1713
1714        assert!(update_pending_rx_metadata(
1715            &rnode_kiss::RNodeEvent::StatRssi(57),
1716            &mut last_rssi,
1717            &mut last_snr,
1718        ));
1719        assert!(update_pending_rx_metadata(
1720            &rnode_kiss::RNodeEvent::StatSnr(-12),
1721            &mut last_rssi,
1722            &mut last_snr,
1723        ));
1724
1725        assert_eq!(last_rssi, Some(-100));
1726        assert_eq!(last_snr, Some(-3.0));
1727    }
1728
1729    #[test]
1730    fn rnode_sub_writer_format() {
1731        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1732        let mut master = unsafe { transport_from_raw_fd(master_fd) };
1733        let writer_file = unsafe { transport_from_raw_fd(slave_fd) };
1734        let shared_writer = Arc::new(Mutex::new(DeviceWriter::new(writer_file)));
1735
1736        let flow_state = Arc::new(Mutex::new(SubFlowState {
1737            ready: true,
1738            queue: std::collections::VecDeque::new(),
1739        }));
1740
1741        let mut sub_writer = RNodeSubWriter {
1742            confirmed_pending: None,
1743            writer: shared_writer,
1744            index: 1, // subinterface 1
1745            flow_control: false,
1746            flow_state,
1747            multi: false,
1748        };
1749
1750        let payload = vec![0xAA, 0xBB, 0xCC];
1751        sub_writer.send_frame(&payload).unwrap();
1752
1753        // Read from master
1754        assert!(poll_read(master.as_raw_fd(), 2000));
1755        let mut buf = [0u8; 256];
1756        let n = master.read(&mut buf).unwrap();
1757
1758        // Should start with FEND, CMD_INT1_DATA (0x10)
1759        assert_eq!(buf[0], kiss::FEND);
1760        assert_eq!(buf[1], 0x10); // CMD_INT1_DATA
1761        assert_eq!(buf[n - 1], kiss::FEND);
1762    }
1763
1764    #[test]
1765    fn rnode_multi_sub_routing() {
1766        // Test that data frames on different CMD_INTn route to different indices
1767        let mut decoder = rnode_kiss::RNodeDecoder::new();
1768
1769        let payload0 = vec![0x01, 0x02];
1770        let frame0 = rnode_kiss::rnode_data_frame(0, &payload0);
1771        let events0 = decoder.feed(&frame0);
1772        assert_eq!(events0.len(), 1);
1773        assert_eq!(
1774            events0[0],
1775            rnode_kiss::RNodeEvent::DataFrame {
1776                index: 0,
1777                data: payload0
1778            }
1779        );
1780
1781        let payload1 = vec![0x03, 0x04];
1782        let frame1 = rnode_kiss::rnode_data_frame(1, &payload1);
1783        let events1 = decoder.feed(&frame1);
1784        assert_eq!(events1.len(), 1);
1785        assert_eq!(
1786            events1[0],
1787            rnode_kiss::RNodeEvent::DataFrame {
1788                index: 1,
1789                data: payload1
1790            }
1791        );
1792    }
1793
1794    #[test]
1795    fn rnode_error_handling() {
1796        // NOTE: CMD_ERROR (0x90) == CMD_INT5_DATA (0x90) — they share the same
1797        // byte value. In multi-radio mode, 0x90 is treated as INT5_DATA.
1798        // Error events only appear on single-radio devices where there's no
1799        // INT5_DATA. The decoder treats 0x90 as INT5_DATA (data wins).
1800        // For real error detection, we rely on the data frame being invalid
1801        // or the device sending a different error indicator.
1802        // Test that cmd 0x90 is correctly handled as a data frame.
1803        let mut decoder = rnode_kiss::RNodeDecoder::new();
1804        let frame = rnode_kiss::rnode_command(rnode_kiss::CMD_ERROR, &[0x02]);
1805        let events = decoder.feed(&frame);
1806        assert_eq!(events.len(), 1);
1807        // 0x90 = CMD_INT5_DATA, so it's a DataFrame with index 5
1808        assert_eq!(
1809            events[0],
1810            rnode_kiss::RNodeEvent::DataFrame {
1811                index: 5,
1812                data: vec![0x02]
1813            }
1814        );
1815    }
1816
1817    #[test]
1818    fn rnode_config_validation() {
1819        let good = RNodeSubConfig {
1820            name: "test".into(),
1821            vport: 0,
1822            outgoing: true,
1823            frequency: 868_000_000,
1824            bandwidth: 125_000,
1825            txpower: 7,
1826            spreading_factor: 8,
1827            coding_rate: 5,
1828            flow_control: false,
1829            st_alock: None,
1830            lt_alock: None,
1831        };
1832        assert!(validate_sub_config(&good).is_none());
1833
1834        // Bad frequency
1835        let mut bad = good.clone();
1836        bad.frequency = 100;
1837        assert!(validate_sub_config(&bad).is_some());
1838
1839        // Bad SF
1840        bad = good.clone();
1841        bad.spreading_factor = 13;
1842        assert!(validate_sub_config(&bad).is_some());
1843
1844        // Bad CR
1845        bad = good.clone();
1846        bad.coding_rate = 9;
1847        assert!(validate_sub_config(&bad).is_some());
1848
1849        // Bad BW
1850        bad = good.clone();
1851        bad.bandwidth = 5;
1852        assert!(validate_sub_config(&bad).is_some());
1853
1854        // Bad TX power
1855        bad = good.clone();
1856        bad.txpower = 50;
1857        assert!(validate_sub_config(&bad).is_some());
1858    }
1859
1860    #[test]
1861    fn multi_factory_preserves_nonsequential_virtual_ports_and_disabled_children() {
1862        let parent = HashMap::from([
1863            ("port".to_string(), "/dev/ttyUSB0".to_string()),
1864            ("speed".to_string(), "115200".to_string()),
1865        ]);
1866        let children = vec![
1867            crate::config::ParsedSubinterface {
1868                name: "disabled".into(),
1869                params: HashMap::from([
1870                    ("interface_enabled".into(), "no".into()),
1871                    ("vport".into(), "2".into()),
1872                ]),
1873            },
1874            crate::config::ParsedSubinterface {
1875                name: "high".into(),
1876                params: HashMap::from([
1877                    ("interface_enabled".into(), "yes".into()),
1878                    ("vport".into(), "7".into()),
1879                    ("frequency".into(), "915000000".into()),
1880                    ("airtime_limit_short".into(), "12.5".into()),
1881                    ("outgoing".into(), "no".into()),
1882                ]),
1883            },
1884            crate::config::ParsedSubinterface {
1885                name: "low".into(),
1886                params: HashMap::from([
1887                    ("enabled".into(), "yes".into()),
1888                    ("vport".into(), "1".into()),
1889                ]),
1890            },
1891        ];
1892        let parsed = RNodeMultiFactory
1893            .parse_config_section(
1894                "multi",
1895                InterfaceId(40),
1896                ConfigSection {
1897                    params: &parent,
1898                    children: &children,
1899                },
1900            )
1901            .unwrap();
1902        let config = parsed.into_any().downcast::<RNodeConfig>().unwrap();
1903        assert_eq!(config.subinterfaces.len(), 2);
1904        assert_eq!(config.subinterfaces[0].vport, 7);
1905        assert_eq!(config.subinterfaces[1].vport, 1);
1906        assert!(!config.subinterfaces[0].outgoing);
1907        assert_eq!(config.subinterfaces[0].st_alock, Some(12.5));
1908    }
1909
1910    #[test]
1911    fn rnode_lora_bitrate_estimate_uses_radio_params() {
1912        let mut sub = RNodeSubConfig {
1913            name: "test".into(),
1914            vport: 0,
1915            outgoing: true,
1916            frequency: 868_000_000,
1917            bandwidth: 125_000,
1918            txpower: 7,
1919            spreading_factor: 8,
1920            coding_rate: 5,
1921            flow_control: false,
1922            st_alock: None,
1923            lt_alock: None,
1924        };
1925
1926        assert_eq!(estimate_lora_bitrate_bps(&sub), 3125);
1927
1928        sub.spreading_factor = 12;
1929        assert_eq!(estimate_lora_bitrate_bps(&sub), 293);
1930
1931        sub.bandwidth = 250_000;
1932        assert_eq!(estimate_lora_bitrate_bps(&sub), 586);
1933    }
1934
1935    #[test]
1936    fn rnode_lora_airtime_profile_uses_radio_params() {
1937        let sub = RNodeSubConfig {
1938            name: "test".into(),
1939            vport: 0,
1940            outgoing: true,
1941            frequency: 868_000_000,
1942            bandwidth: 125_000,
1943            txpower: 7,
1944            spreading_factor: 8,
1945            coding_rate: 5,
1946            flow_control: false,
1947            st_alock: None,
1948            lt_alock: None,
1949        };
1950
1951        let profile = lora_airtime_profile(&sub);
1952        assert!((profile.transmit_time_secs(100) - 0.307712).abs() < 0.000001);
1953        assert_eq!(
1954            profile,
1955            AirtimeProfile::Lora {
1956                bandwidth: 125_000,
1957                spreading_factor: 8,
1958                coding_rate: 5,
1959                preamble_symbols: LORA_PREAMBLE_SYMBOLS,
1960                explicit_header: LORA_EXPLICIT_HEADER,
1961                crc: LORA_CRC,
1962            }
1963        );
1964    }
1965
1966    fn thread_named(prefix: &str) -> bool {
1967        std::fs::read_dir("/proc/self/task").unwrap().any(|task| {
1968            let comm = task.unwrap().path().join("comm");
1969            std::fs::read_to_string(comm).is_ok_and(|comm| comm.trim().starts_with(prefix))
1970        })
1971    }
1972
1973    /// Data frames the device side reads within `timeout_ms`, past the
1974    /// host's commands.
1975    fn device_reads(master: &mut Transport, want: usize, timeout_ms: i32) -> Vec<Vec<u8>> {
1976        let mut decoder = rnode_kiss::RNodeDecoder::new();
1977        let mut frames = Vec::new();
1978        let mut buf = [0u8; 1024];
1979        while frames.len() < want && poll_read(master.as_raw_fd(), timeout_ms) {
1980            let n = master.read(&mut buf).unwrap();
1981            for event in decoder.feed(&buf[..n]) {
1982                if let rnode_kiss::RNodeEvent::DataFrame { data, .. } = event {
1983                    frames.push(data);
1984                }
1985            }
1986        }
1987        frames
1988    }
1989
1990    #[test]
1991    fn folded_rnode_runs_on_the_driver_loop() {
1992        if !crate::engine::FOLD_IO {
1993            return;
1994        }
1995        let tempdir = tempdir().unwrap();
1996        let port_path = tempdir.path().join("rnode-port");
1997        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1998        std::os::unix::fs::symlink(slave_tty_path(slave_fd), &port_path).unwrap();
1999        let mut master = unsafe { transport_from_raw_fd(master_fd) };
2000        let slave = unsafe { transport_from_raw_fd(slave_fd) };
2001
2002        let (tx, rx) = event::channel();
2003        let mut config = RNodeConfig {
2004            name: "folded-rnode".into(),
2005            port: port_path.display().to_string(),
2006            subinterfaces: vec![RNodeSubConfig {
2007                name: "folded-rnode".into(),
2008                vport: 0,
2009                outgoing: true,
2010                frequency: 868_000_000,
2011                bandwidth: 125_000,
2012                txpower: 7,
2013                spreading_factor: 8,
2014                coding_rate: 5,
2015                flow_control: true,
2016                st_alock: None,
2017                lt_alock: None,
2018            }],
2019            base_interface_id: InterfaceId(43),
2020            ..Default::default()
2021        };
2022        config.runtime = Arc::new(Mutex::new(RNodeRuntime::from_config(&config)));
2023        let (mut writers, control) = start_with_control(config, tx.clone()).unwrap();
2024        let (_, writer) = writers.remove(0);
2025        assert!(writer.sends_inline());
2026        let (mut writer, _) =
2027            crate::interface::wrap_async_writer(writer, InterfaceId(43), "rnode", tx, 16);
2028
2029        thread::sleep(Duration::from_millis(2500));
2030        mock_respond_detect(&mut master);
2031        wait_for_interface_event(&rx, Duration::from_secs(4), |event| {
2032            matches!(event, Event::InterfaceUp(InterfaceId(43), None, None))
2033        });
2034        // Setup is done: only the driver serves the device now.
2035        thread::sleep(Duration::from_millis(100));
2036        assert!(!thread_named("rnode-setup-43"));
2037        assert!(!thread_named("rnode-reader-43"));
2038        assert!(!thread_named("rnode-keepaliv"));
2039        assert!(!thread_named("iface-writer-43"));
2040
2041        // Inbound, with the modem's report attached.
2042        master
2043            .write_all(&rnode_kiss::rnode_command(
2044                rnode_kiss::CMD_STAT_RSSI,
2045                &[100],
2046            ))
2047            .unwrap();
2048        let payload: Vec<u8> = (0..32).collect();
2049        master
2050            .write_all(&rnode_kiss::rnode_data_frame(0, &payload))
2051            .unwrap();
2052        match wait_for_interface_event(&rx, Duration::from_secs(2), |event| {
2053            matches!(event, Event::Frame { .. })
2054        }) {
2055            Event::Frame {
2056                interface_id,
2057                data,
2058                rssi,
2059                ..
2060            } => {
2061                assert_eq!(interface_id, InterfaceId(43));
2062                assert_eq!(&*data, &payload[..]);
2063                assert_eq!(rssi, Some(-57));
2064            }
2065            _ => unreachable!(),
2066        }
2067
2068        // Outbound: the second frame waits for the device to report ready.
2069        writer.send_frame(b"first frame").unwrap();
2070        writer.send_frame(b"second frame").unwrap();
2071        writer.flush_pending();
2072        assert_eq!(
2073            device_reads(&mut master, 2, 200),
2074            vec![b"first frame".to_vec()]
2075        );
2076        master
2077            .write_all(&rnode_kiss::rnode_command(rnode_kiss::CMD_READY, &[0x01]))
2078            .unwrap();
2079        assert!(rx.recv_timeout(Duration::from_millis(200)).is_err());
2080        assert_eq!(
2081            device_reads(&mut master, 1, 2000),
2082            vec![b"second frame".to_vec()]
2083        );
2084
2085        // The device going away is reported.
2086        drop(master);
2087        drop(slave);
2088        wait_for_interface_event(&rx, Duration::from_secs(2), |event| {
2089            matches!(event, Event::InterfaceDown(InterfaceId(43)))
2090        });
2091        control.request_stop();
2092    }
2093
2094    #[test]
2095    fn rnode_reconnects_after_serial_disconnect() {
2096        let tempdir = tempdir().unwrap();
2097        let port_path = tempdir.path().join("rnode-port");
2098
2099        let (master1_fd, slave1_fd) = open_pty_pair().unwrap();
2100        let slave1_path = slave_tty_path(slave1_fd);
2101        std::os::unix::fs::symlink(&slave1_path, &port_path).unwrap();
2102
2103        let mut master1 = unsafe { transport_from_raw_fd(master1_fd) };
2104        let slave1 = unsafe { transport_from_raw_fd(slave1_fd) };
2105
2106        let (tx, rx) = event::channel();
2107        let sub = RNodeSubConfig {
2108            name: "test-rnode".into(),
2109            vport: 0,
2110            outgoing: true,
2111            frequency: 868_000_000,
2112            bandwidth: 125_000,
2113            txpower: 7,
2114            spreading_factor: 8,
2115            coding_rate: 5,
2116            flow_control: false,
2117            st_alock: None,
2118            lt_alock: None,
2119        };
2120        let mut config = RNodeConfig {
2121            name: "test-rnode".into(),
2122            port: port_path.display().to_string(),
2123            speed: 115200,
2124            subinterfaces: vec![sub],
2125            multi: false,
2126            id_interval: None,
2127            id_callsign: None,
2128            base_interface_id: InterfaceId(41),
2129            pre_opened_fd: None,
2130            underlay_mark: None,
2131            runtime: Arc::new(Mutex::new(RNodeRuntime {
2132                sub: RNodeSubConfig {
2133                    name: String::new(),
2134                    vport: 0,
2135                    outgoing: true,
2136                    frequency: 868_000_000,
2137                    bandwidth: 125_000,
2138                    txpower: 7,
2139                    spreading_factor: 8,
2140                    coding_rate: 5,
2141                    flow_control: false,
2142                    st_alock: None,
2143                    lt_alock: None,
2144                },
2145                writer: None,
2146            })),
2147        };
2148        config.runtime = Arc::new(Mutex::new(RNodeRuntime::from_config(&config)));
2149
2150        let (_writers, control) = start_with_control(config, tx).unwrap();
2151
2152        thread::sleep(Duration::from_secs(3));
2153        mock_respond_detect(&mut master1);
2154        let up = wait_for_interface_event(&rx, Duration::from_secs(4), |event| {
2155            matches!(event, Event::InterfaceUp(InterfaceId(41), _, _))
2156        });
2157        assert!(matches!(
2158            up,
2159            Event::InterfaceUp(InterfaceId(41), None, None)
2160        ));
2161
2162        drop(master1);
2163        drop(slave1);
2164
2165        let down = wait_for_interface_event(&rx, Duration::from_secs(4), |event| {
2166            matches!(event, Event::InterfaceDown(InterfaceId(41)))
2167        });
2168        assert!(matches!(down, Event::InterfaceDown(InterfaceId(41))));
2169
2170        let (master2_fd, slave2_fd) = open_pty_pair().unwrap();
2171        let slave2_path = slave_tty_path(slave2_fd);
2172        std::fs::remove_file(&port_path).unwrap();
2173        std::os::unix::fs::symlink(&slave2_path, &port_path).unwrap();
2174
2175        let mut master2 = unsafe { transport_from_raw_fd(master2_fd) };
2176        let _slave2 = unsafe { transport_from_raw_fd(slave2_fd) };
2177
2178        thread::sleep(Duration::from_secs(3));
2179        mock_respond_detect(&mut master2);
2180        let up = wait_for_interface_event(&rx, Duration::from_secs(4), |event| {
2181            matches!(event, Event::InterfaceUp(InterfaceId(41), _, _))
2182        });
2183        assert!(matches!(
2184            up,
2185            Event::InterfaceUp(InterfaceId(41), Some(_), None)
2186        ));
2187
2188        // signal_interface_up publishes telemetry right after InterfaceUp;
2189        // wait for it so it cannot land in the post-stop silence window.
2190        let telemetry = rx.recv_timeout(Duration::from_secs(1)).unwrap();
2191        assert!(matches!(
2192            telemetry,
2193            Event::InterfaceTelemetry {
2194                interface_id: InterfaceId(41),
2195                ..
2196            }
2197        ));
2198
2199        control.request_stop();
2200        assert!(control.should_stop());
2201        // A stopped reader must not report the intentional serial close as a
2202        // hardware failure or start another reconnect attempt.
2203        drop(master2);
2204        assert!(rx.recv_timeout(Duration::from_millis(500)).is_err());
2205    }
2206}