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