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