Skip to main content

rns_net/interface/
rnode.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
9use std::io::{self, Read, Write};
10use std::sync::{Arc, Mutex};
11use std::thread;
12use std::time::Duration;
13
14use rns_core::transport::types::InterfaceId;
15
16use crate::event::{Event, EventSender};
17use crate::interface::Writer;
18use crate::rnode_kiss;
19use crate::serial::{Parity, SerialConfig, SerialPort};
20
21/// Validation limits matching Python RNodeInterface.
22pub const FREQ_MIN: u32 = 137_000_000;
23pub const FREQ_MAX: u32 = 3_000_000_000;
24pub const BW_MIN: u32 = 7_800;
25pub const BW_MAX: u32 = 1_625_000;
26pub const SF_MIN: u8 = 5;
27pub const SF_MAX: u8 = 12;
28pub const CR_MIN: u8 = 5;
29pub const CR_MAX: u8 = 8;
30pub const TXPOWER_MIN: i8 = 0;
31pub const TXPOWER_MAX: i8 = 37;
32pub const HW_MTU: u32 = 508;
33
34/// Configuration for one RNode subinterface (radio).
35#[derive(Debug, Clone)]
36pub struct RNodeSubConfig {
37    pub name: String,
38    pub frequency: u32,
39    pub bandwidth: u32,
40    pub txpower: i8,
41    pub spreading_factor: u8,
42    pub coding_rate: u8,
43    pub flow_control: bool,
44    pub st_alock: Option<f32>,
45    pub lt_alock: Option<f32>,
46}
47
48/// Configuration for the RNode device.
49#[derive(Debug, Clone)]
50pub struct RNodeConfig {
51    pub name: String,
52    pub port: String,
53    pub speed: u32,
54    pub subinterfaces: Vec<RNodeSubConfig>,
55    pub id_interval: Option<u32>,
56    pub id_callsign: Option<Vec<u8>>,
57    pub base_interface_id: InterfaceId,
58    /// Pre-opened file descriptor (e.g. from a USB bridge socketpair on Android).
59    /// When set, `start()` uses this fd directly instead of opening `port`.
60    pub pre_opened_fd: Option<i32>,
61    pub runtime: Arc<Mutex<RNodeRuntime>>,
62}
63
64#[derive(Debug, Clone)]
65pub struct RNodeRuntime {
66    pub sub: RNodeSubConfig,
67    pub writer: Option<Arc<Mutex<std::fs::File>>>,
68}
69
70impl RNodeRuntime {
71    pub fn from_config(config: &RNodeConfig) -> Self {
72        Self {
73            sub: config
74                .subinterfaces
75                .first()
76                .cloned()
77                .unwrap_or_else(|| RNodeSubConfig {
78                    name: config.name.clone(),
79                    frequency: 868_000_000,
80                    bandwidth: 125_000,
81                    txpower: 7,
82                    spreading_factor: 8,
83                    coding_rate: 5,
84                    flow_control: false,
85                    st_alock: None,
86                    lt_alock: None,
87                }),
88            writer: None,
89        }
90    }
91}
92
93#[derive(Debug, Clone)]
94pub struct RNodeRuntimeConfigHandle {
95    pub interface_name: String,
96    pub runtime: Arc<Mutex<RNodeRuntime>>,
97    pub startup: RNodeRuntime,
98}
99
100impl Default for RNodeConfig {
101    fn default() -> Self {
102        let mut config = RNodeConfig {
103            name: String::new(),
104            port: String::new(),
105            speed: 115200,
106            subinterfaces: Vec::new(),
107            id_interval: None,
108            id_callsign: None,
109            base_interface_id: InterfaceId(0),
110            pre_opened_fd: None,
111            runtime: Arc::new(Mutex::new(RNodeRuntime {
112                sub: RNodeSubConfig {
113                    name: String::new(),
114                    frequency: 868_000_000,
115                    bandwidth: 125_000,
116                    txpower: 7,
117                    spreading_factor: 8,
118                    coding_rate: 5,
119                    flow_control: false,
120                    st_alock: None,
121                    lt_alock: None,
122                },
123                writer: None,
124            })),
125        };
126        let startup = RNodeRuntime::from_config(&config);
127        config.runtime = Arc::new(Mutex::new(startup));
128        config
129    }
130}
131
132/// Validate subinterface configuration. Returns error message if invalid.
133pub fn validate_sub_config(sub: &RNodeSubConfig) -> Option<String> {
134    if sub.frequency < FREQ_MIN || sub.frequency > FREQ_MAX {
135        return Some(format!(
136            "Invalid frequency {} for {}",
137            sub.frequency, sub.name
138        ));
139    }
140    if sub.bandwidth < BW_MIN || sub.bandwidth > BW_MAX {
141        return Some(format!(
142            "Invalid bandwidth {} for {}",
143            sub.bandwidth, sub.name
144        ));
145    }
146    if sub.spreading_factor < SF_MIN || sub.spreading_factor > SF_MAX {
147        return Some(format!(
148            "Invalid SF {} for {}",
149            sub.spreading_factor, sub.name
150        ));
151    }
152    if sub.coding_rate < CR_MIN || sub.coding_rate > CR_MAX {
153        return Some(format!("Invalid CR {} for {}", sub.coding_rate, sub.name));
154    }
155    if sub.txpower < TXPOWER_MIN || sub.txpower > TXPOWER_MAX {
156        return Some(format!("Invalid TX power {} for {}", sub.txpower, sub.name));
157    }
158    if let Some(st) = sub.st_alock {
159        if st < 0.0 || st > 100.0 {
160            return Some(format!("Invalid ST airtime limit {} for {}", st, sub.name));
161        }
162    }
163    if let Some(lt) = sub.lt_alock {
164        if lt < 0.0 || lt > 100.0 {
165            return Some(format!("Invalid LT airtime limit {} for {}", lt, sub.name));
166        }
167    }
168    None
169}
170
171/// Writer for a specific RNode subinterface.
172/// Wraps a shared serial writer with subinterface-specific data framing.
173struct RNodeSubWriter {
174    writer: Arc<Mutex<std::fs::File>>,
175    index: u8,
176    flow_control: bool,
177    flow_state: Arc<Mutex<SubFlowState>>,
178}
179
180struct SubFlowState {
181    ready: bool,
182    queue: std::collections::VecDeque<Vec<u8>>,
183}
184
185impl Writer for RNodeSubWriter {
186    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
187        let frame = rnode_kiss::rnode_data_frame(self.index, data);
188        if self.flow_control {
189            let mut state = self.flow_state.lock().unwrap();
190            if state.ready {
191                state.ready = false;
192                drop(state);
193                self.writer.lock().unwrap().write_all(&frame)
194            } else {
195                state.queue.push_back(data.to_vec());
196                Ok(())
197            }
198        } else {
199            self.writer.lock().unwrap().write_all(&frame)
200        }
201    }
202}
203
204/// Start the RNode interface.
205///
206/// Opens serial port, spawns reader thread which performs detect+configure,
207/// then enters data relay mode.
208///
209/// Returns one `(InterfaceId, Box<dyn Writer>)` per subinterface.
210pub fn start(
211    config: RNodeConfig,
212    tx: EventSender,
213) -> io::Result<Vec<(InterfaceId, Box<dyn Writer>)>> {
214    // Validate all subinterface configs upfront
215    for sub in &config.subinterfaces {
216        if let Some(err) = validate_sub_config(sub) {
217            return Err(io::Error::new(io::ErrorKind::InvalidInput, err));
218        }
219    }
220
221    if config.subinterfaces.is_empty() {
222        return Err(io::Error::new(
223            io::ErrorKind::InvalidInput,
224            "No subinterfaces configured",
225        ));
226    }
227
228    let (reader_file, shared_writer) = if let Some(fd) = config.pre_opened_fd {
229        // Pre-opened fd from USB bridge — dup for independent reader/writer handles
230        let port = SerialPort::from_raw_fd(fd);
231        let r = port.reader()?;
232        let w = port.writer()?;
233        std::mem::forget(port); // don't close the original fd — bridge owns it
234        (r, Arc::new(Mutex::new(w)))
235    } else {
236        let serial_config = SerialConfig {
237            path: config.port.clone(),
238            baud: config.speed,
239            data_bits: 8,
240            parity: Parity::None,
241            stop_bits: 1,
242        };
243        let port = SerialPort::open(&serial_config)?;
244        let r = port.reader()?;
245        let w = port.writer()?;
246        (r, Arc::new(Mutex::new(w)))
247    };
248
249    // Build per-subinterface writers and IDs
250    let num_subs = config.subinterfaces.len();
251    let mut writers: Vec<(InterfaceId, Box<dyn Writer>)> = Vec::with_capacity(num_subs);
252    let mut flow_states: Vec<Arc<Mutex<SubFlowState>>> = Vec::with_capacity(num_subs);
253
254    for (i, sub) in config.subinterfaces.iter().enumerate() {
255        let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
256        let flow_state = Arc::new(Mutex::new(SubFlowState {
257            ready: true,
258            queue: std::collections::VecDeque::new(),
259        }));
260        flow_states.push(flow_state.clone());
261
262        let sub_writer: Box<dyn Writer> = Box::new(RNodeSubWriter {
263            writer: shared_writer.clone(),
264            index: i as u8,
265            flow_control: sub.flow_control,
266            flow_state,
267        });
268        writers.push((sub_id, sub_writer));
269    }
270
271    // Spawn reader thread
272    let reader_shared_writer = shared_writer.clone();
273    {
274        let mut runtime = config.runtime.lock().unwrap();
275        runtime.writer = Some(shared_writer.clone());
276        runtime.sub = config
277            .subinterfaces
278            .first()
279            .cloned()
280            .unwrap_or(runtime.sub.clone());
281    }
282    let reader_config = config.clone();
283    let reader_flow_states = flow_states;
284    thread::Builder::new()
285        .name(format!("rnode-reader-{}", config.base_interface_id.0))
286        .spawn(move || {
287            reader_loop(
288                reader_file,
289                reader_shared_writer,
290                reader_config,
291                tx,
292                reader_flow_states,
293            );
294        })?;
295
296    // Spawn keepalive thread — sends periodic DETECT to prevent firmware
297    // bridge idle timeout (ESP32 RNode reverts to standalone after 30s idle).
298    let keepalive_writer = shared_writer.clone();
299    let keepalive_name = config.name.clone();
300    thread::Builder::new()
301        .name(format!("rnode-keepalive-{}", config.base_interface_id.0))
302        .spawn(move || {
303            let detect = rnode_kiss::detect_request();
304            loop {
305                thread::sleep(Duration::from_secs(15));
306                if let Err(e) = keepalive_writer.lock().unwrap().write_all(&detect) {
307                    log::debug!("[{}] keepalive write failed: {}", keepalive_name, e);
308                    return;
309                }
310            }
311        })?;
312
313    Ok(writers)
314}
315
316/// Reader loop: detect device, configure radios, then relay data frames.
317fn reader_loop(
318    mut reader: std::fs::File,
319    writer: Arc<Mutex<std::fs::File>>,
320    config: RNodeConfig,
321    tx: EventSender,
322    flow_states: Vec<Arc<Mutex<SubFlowState>>>,
323) {
324    // Initial delay for hardware init (matches Python: sleep(2.0))
325    thread::sleep(Duration::from_secs(2));
326
327    // Send detect request
328    {
329        let detect_cmd = rnode_kiss::detect_request();
330        // Also request FW version, platform, MCU (matches Python detect())
331        let mut cmd = detect_cmd;
332        cmd.extend_from_slice(&rnode_kiss::rnode_command(
333            rnode_kiss::CMD_FW_VERSION,
334            &[0x00],
335        ));
336        cmd.extend_from_slice(&rnode_kiss::rnode_command(
337            rnode_kiss::CMD_FW_DETAIL,
338            &[0x00],
339        ));
340        cmd.extend_from_slice(&rnode_kiss::rnode_command(
341            rnode_kiss::CMD_PLATFORM,
342            &[0x00],
343        ));
344        cmd.extend_from_slice(&rnode_kiss::rnode_command(rnode_kiss::CMD_MCU, &[0x00]));
345
346        if let Err(e) = writer.lock().unwrap().write_all(&cmd) {
347            log::error!("[{}] failed to send detect: {}", config.name, e);
348            return;
349        }
350    }
351
352    let mut decoder = rnode_kiss::RNodeDecoder::new();
353    let mut buf = [0u8; 4096];
354    let mut detected = false;
355
356    // Detection phase: read until we get Detected or timeout
357    let detect_start = std::time::Instant::now();
358    let detect_timeout = Duration::from_secs(5);
359
360    while !detected && detect_start.elapsed() < detect_timeout {
361        match reader.read(&mut buf) {
362            Ok(0) => {
363                log::warn!("[{}] serial port closed during detect", config.name);
364                return;
365            }
366            Ok(n) => {
367                for event in decoder.feed(&buf[..n]) {
368                    match event {
369                        rnode_kiss::RNodeEvent::Detected(true) => {
370                            detected = true;
371                            log::info!("[{}] RNode device detected", config.name);
372                        }
373                        rnode_kiss::RNodeEvent::FirmwareVersion { major, minor } => {
374                            log::info!("[{}] firmware version {}.{}", config.name, major, minor);
375                        }
376                        rnode_kiss::RNodeEvent::FirmwareDetail(ref detail) => {
377                            log::info!("[{}] firmware detail: {}", config.name, detail);
378                        }
379                        rnode_kiss::RNodeEvent::Platform(p) => {
380                            log::info!("[{}] platform: 0x{:02X}", config.name, p);
381                        }
382                        rnode_kiss::RNodeEvent::Mcu(m) => {
383                            log::info!("[{}] MCU: 0x{:02X}", config.name, m);
384                        }
385                        _ => {}
386                    }
387                }
388            }
389            Err(e) => {
390                log::error!("[{}] serial read error during detect: {}", config.name, e);
391                return;
392            }
393        }
394    }
395
396    if !detected {
397        log::error!("[{}] RNode detection timed out", config.name);
398        return;
399    }
400
401    // Configure each subinterface
402    for (i, sub) in config.subinterfaces.iter().enumerate() {
403        if let Err(e) =
404            configure_subinterface(&writer, i as u8, sub, config.subinterfaces.len() > 1)
405        {
406            log::error!(
407                "[{}] failed to configure subinterface {}: {}",
408                config.name,
409                i,
410                e
411            );
412            return;
413        }
414    }
415
416    // Brief delay for configuration to settle (matches Python: sleep(0.3))
417    thread::sleep(Duration::from_millis(300));
418
419    // Signal all subinterfaces as online
420    for i in 0..config.subinterfaces.len() {
421        let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
422        let _ = tx.send(Event::InterfaceUp(sub_id, None, None));
423    }
424
425    log::info!(
426        "[{}] RNode configured with {} subinterface(s)",
427        config.name,
428        config.subinterfaces.len()
429    );
430
431    // Data relay loop
432    loop {
433        match reader.read(&mut buf) {
434            Ok(0) => {
435                log::warn!("[{}] serial port closed", config.name);
436                for i in 0..config.subinterfaces.len() {
437                    let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
438                    let _ = tx.send(Event::InterfaceDown(sub_id));
439                }
440                // TODO: reconnect logic
441                return;
442            }
443            Ok(n) => {
444                for event in decoder.feed(&buf[..n]) {
445                    match event {
446                        rnode_kiss::RNodeEvent::DataFrame { index, data } => {
447                            let sub_id = InterfaceId(config.base_interface_id.0 + index as u64);
448                            if tx
449                                .send(Event::Frame {
450                                    interface_id: sub_id,
451                                    data,
452                                })
453                                .is_err()
454                            {
455                                return;
456                            }
457                        }
458                        rnode_kiss::RNodeEvent::Ready => {
459                            // Flow control: unlock all subinterfaces that have flow_control
460                            for (i, fs) in flow_states.iter().enumerate() {
461                                if config.subinterfaces[i].flow_control {
462                                    process_flow_queue(fs, &writer, i as u8);
463                                }
464                            }
465                        }
466                        rnode_kiss::RNodeEvent::Error(code) => {
467                            log::error!("[{}] RNode error: 0x{:02X}", config.name, code);
468                        }
469                        _ => {
470                            // Status updates logged but not acted on
471                        }
472                    }
473                }
474            }
475            Err(e) => {
476                log::error!("[{}] serial read error: {}", config.name, e);
477                for i in 0..config.subinterfaces.len() {
478                    let sub_id = InterfaceId(config.base_interface_id.0 + i as u64);
479                    let _ = tx.send(Event::InterfaceDown(sub_id));
480                }
481                return;
482            }
483        }
484    }
485}
486
487/// Configure a single subinterface on the RNode device.
488pub(crate) fn configure_subinterface(
489    writer: &Arc<Mutex<std::fs::File>>,
490    index: u8,
491    sub: &RNodeSubConfig,
492    multi: bool,
493) -> io::Result<()> {
494    let mut w = writer.lock().unwrap();
495
496    // For multi-radio, send select command before each parameter
497    let freq_bytes = [
498        (sub.frequency >> 24) as u8,
499        (sub.frequency >> 16) as u8,
500        (sub.frequency >> 8) as u8,
501        (sub.frequency & 0xFF) as u8,
502    ];
503    let bw_bytes = [
504        (sub.bandwidth >> 24) as u8,
505        (sub.bandwidth >> 16) as u8,
506        (sub.bandwidth >> 8) as u8,
507        (sub.bandwidth & 0xFF) as u8,
508    ];
509    let txp = if sub.txpower < 0 {
510        (sub.txpower as i16 + 256) as u8
511    } else {
512        sub.txpower as u8
513    };
514
515    if multi {
516        w.write_all(&rnode_kiss::rnode_select_command(
517            index,
518            rnode_kiss::CMD_FREQUENCY,
519            &freq_bytes,
520        ))?;
521        w.write_all(&rnode_kiss::rnode_select_command(
522            index,
523            rnode_kiss::CMD_BANDWIDTH,
524            &bw_bytes,
525        ))?;
526        w.write_all(&rnode_kiss::rnode_select_command(
527            index,
528            rnode_kiss::CMD_TXPOWER,
529            &[txp],
530        ))?;
531        w.write_all(&rnode_kiss::rnode_select_command(
532            index,
533            rnode_kiss::CMD_SF,
534            &[sub.spreading_factor],
535        ))?;
536        w.write_all(&rnode_kiss::rnode_select_command(
537            index,
538            rnode_kiss::CMD_CR,
539            &[sub.coding_rate],
540        ))?;
541    } else {
542        w.write_all(&rnode_kiss::rnode_command(
543            rnode_kiss::CMD_FREQUENCY,
544            &freq_bytes,
545        ))?;
546        w.write_all(&rnode_kiss::rnode_command(
547            rnode_kiss::CMD_BANDWIDTH,
548            &bw_bytes,
549        ))?;
550        w.write_all(&rnode_kiss::rnode_command(rnode_kiss::CMD_TXPOWER, &[txp]))?;
551        w.write_all(&rnode_kiss::rnode_command(
552            rnode_kiss::CMD_SF,
553            &[sub.spreading_factor],
554        ))?;
555        w.write_all(&rnode_kiss::rnode_command(
556            rnode_kiss::CMD_CR,
557            &[sub.coding_rate],
558        ))?;
559    }
560
561    // Airtime locks
562    if let Some(st) = sub.st_alock {
563        let st_val = (st * 100.0) as u16;
564        let st_bytes = [(st_val >> 8) as u8, (st_val & 0xFF) as u8];
565        if multi {
566            w.write_all(&rnode_kiss::rnode_select_command(
567                index,
568                rnode_kiss::CMD_ST_ALOCK,
569                &st_bytes,
570            ))?;
571        } else {
572            w.write_all(&rnode_kiss::rnode_command(
573                rnode_kiss::CMD_ST_ALOCK,
574                &st_bytes,
575            ))?;
576        }
577    }
578    if let Some(lt) = sub.lt_alock {
579        let lt_val = (lt * 100.0) as u16;
580        let lt_bytes = [(lt_val >> 8) as u8, (lt_val & 0xFF) as u8];
581        if multi {
582            w.write_all(&rnode_kiss::rnode_select_command(
583                index,
584                rnode_kiss::CMD_LT_ALOCK,
585                &lt_bytes,
586            ))?;
587        } else {
588            w.write_all(&rnode_kiss::rnode_command(
589                rnode_kiss::CMD_LT_ALOCK,
590                &lt_bytes,
591            ))?;
592        }
593    }
594
595    // Turn on radio
596    if multi {
597        w.write_all(&rnode_kiss::rnode_select_command(
598            index,
599            rnode_kiss::CMD_RADIO_STATE,
600            &[rnode_kiss::RADIO_STATE_ON],
601        ))?;
602    } else {
603        w.write_all(&rnode_kiss::rnode_command(
604            rnode_kiss::CMD_RADIO_STATE,
605            &[rnode_kiss::RADIO_STATE_ON],
606        ))?;
607    }
608
609    Ok(())
610}
611
612/// Process flow control queue for a subinterface.
613fn process_flow_queue(
614    flow_state: &Arc<Mutex<SubFlowState>>,
615    writer: &Arc<Mutex<std::fs::File>>,
616    index: u8,
617) {
618    let mut state = flow_state.lock().unwrap();
619    if let Some(data) = state.queue.pop_front() {
620        state.ready = false;
621        drop(state);
622        let frame = rnode_kiss::rnode_data_frame(index, &data);
623        let _ = writer.lock().unwrap().write_all(&frame);
624    } else {
625        state.ready = true;
626    }
627}
628
629// --- Factory implementation ---
630
631use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult, SubInterface};
632use rns_core::transport::types::InterfaceInfo;
633use std::collections::HashMap;
634
635/// Factory for `RNodeInterface`.
636pub struct RNodeFactory;
637
638impl InterfaceFactory for RNodeFactory {
639    fn type_name(&self) -> &str {
640        "RNodeInterface"
641    }
642
643    fn default_ifac_size(&self) -> usize {
644        8
645    }
646
647    fn parse_config(
648        &self,
649        name: &str,
650        id: InterfaceId,
651        params: &HashMap<String, String>,
652    ) -> Result<Box<dyn InterfaceConfigData>, String> {
653        let pre_opened_fd = params.get("fd").and_then(|v| v.parse::<i32>().ok());
654
655        let port = params
656            .get("port")
657            .cloned()
658            .or_else(|| pre_opened_fd.map(|_| "usb-bridge".to_string()))
659            .ok_or_else(|| "RNodeInterface requires 'port' or 'fd'".to_string())?;
660
661        let speed = params
662            .get("speed")
663            .and_then(|v| v.parse().ok())
664            .unwrap_or(115200u32);
665
666        let frequency = params
667            .get("frequency")
668            .and_then(|v| v.parse().ok())
669            .unwrap_or(868_000_000u32);
670
671        let bandwidth = params
672            .get("bandwidth")
673            .and_then(|v| v.parse().ok())
674            .unwrap_or(125_000u32);
675
676        let txpower = params
677            .get("txpower")
678            .and_then(|v| v.parse().ok())
679            .unwrap_or(7i8);
680
681        let spreading_factor = params
682            .get("spreadingfactor")
683            .or_else(|| params.get("spreading_factor"))
684            .and_then(|v| v.parse().ok())
685            .unwrap_or(8u8);
686
687        let coding_rate = params
688            .get("codingrate")
689            .or_else(|| params.get("coding_rate"))
690            .and_then(|v| v.parse().ok())
691            .unwrap_or(5u8);
692
693        let flow_control = params
694            .get("flow_control")
695            .and_then(|v| crate::config::parse_bool_pub(v))
696            .unwrap_or(false);
697
698        let st_alock = params.get("st_alock").and_then(|v| v.parse().ok());
699
700        let lt_alock = params.get("lt_alock").and_then(|v| v.parse().ok());
701
702        let id_interval = params.get("id_interval").and_then(|v| v.parse().ok());
703
704        let id_callsign = params.get("id_callsign").map(|v| v.as_bytes().to_vec());
705
706        let sub = RNodeSubConfig {
707            name: name.to_string(),
708            frequency,
709            bandwidth,
710            txpower,
711            spreading_factor,
712            coding_rate,
713            flow_control,
714            st_alock,
715            lt_alock,
716        };
717
718        Ok(Box::new(RNodeConfig {
719            name: name.to_string(),
720            port,
721            speed,
722            subinterfaces: vec![sub],
723            id_interval,
724            id_callsign,
725            base_interface_id: id,
726            pre_opened_fd,
727            runtime: Arc::new(Mutex::new(RNodeRuntime {
728                sub: RNodeSubConfig {
729                    name: name.to_string(),
730                    frequency,
731                    bandwidth,
732                    txpower,
733                    spreading_factor,
734                    coding_rate,
735                    flow_control,
736                    st_alock,
737                    lt_alock,
738                },
739                writer: None,
740            })),
741        }))
742    }
743
744    fn start(
745        &self,
746        config: Box<dyn InterfaceConfigData>,
747        ctx: StartContext,
748    ) -> std::io::Result<StartResult> {
749        let rnode_config = *config.into_any().downcast::<RNodeConfig>().map_err(|_| {
750            std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
751        })?;
752
753        let name = rnode_config.name.clone();
754
755        let pairs = start(rnode_config, ctx.tx)?;
756
757        let mut subs = Vec::with_capacity(pairs.len());
758        for (index, (sub_id, writer)) in pairs.into_iter().enumerate() {
759            let sub_name = if index == 0 {
760                name.clone()
761            } else {
762                format!("{}/{}", name, index)
763            };
764
765            let info = InterfaceInfo {
766                id: sub_id,
767                name: sub_name,
768                mode: ctx.mode,
769                out_capable: true,
770                in_capable: true,
771                bitrate: None,
772                announce_rate_target: None,
773                announce_rate_grace: 0,
774                announce_rate_penalty: 0.0,
775                announce_cap: rns_core::constants::ANNOUNCE_CAP,
776                is_local_client: false,
777                wants_tunnel: false,
778                tunnel_id: None,
779                mtu: rns_core::constants::MTU as u32,
780                ingress_control: false,
781                ia_freq: 0.0,
782                started: crate::time::now(),
783            };
784
785            subs.push(SubInterface {
786                id: sub_id,
787                info,
788                writer,
789                interface_type_name: "RNodeInterface".to_string(),
790            });
791        }
792
793        Ok(StartResult::Multi(subs))
794    }
795}
796
797pub(crate) fn rnode_runtime_handle_from_config(config: &RNodeConfig) -> RNodeRuntimeConfigHandle {
798    RNodeRuntimeConfigHandle {
799        interface_name: config.name.clone(),
800        runtime: Arc::clone(&config.runtime),
801        startup: RNodeRuntime::from_config(config),
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use crate::kiss;
809    use crate::serial::open_pty_pair;
810    use std::os::unix::io::{AsRawFd, FromRawFd};
811    /// Helper: poll an fd for reading with timeout (ms).
812    fn poll_read(fd: i32, timeout_ms: i32) -> bool {
813        let mut pfd = libc::pollfd {
814            fd,
815            events: libc::POLLIN,
816            revents: 0,
817        };
818        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
819        ret > 0
820    }
821
822    /// Read all available bytes from an fd.
823    fn read_available(file: &mut std::fs::File) -> Vec<u8> {
824        let mut all = Vec::new();
825        let mut buf = [0u8; 4096];
826        while poll_read(file.as_raw_fd(), 100) {
827            match file.read(&mut buf) {
828                Ok(n) if n > 0 => all.extend_from_slice(&buf[..n]),
829                _ => break,
830            }
831        }
832        all
833    }
834
835    /// Mock RNode: respond to detect with DETECT_RESP, FW version, platform
836    fn mock_respond_detect(master: &mut std::fs::File) {
837        // Respond: DETECT_RESP
838        master
839            .write_all(&rnode_kiss::rnode_command(
840                rnode_kiss::CMD_DETECT,
841                &[rnode_kiss::DETECT_RESP],
842            ))
843            .unwrap();
844        // FW version 1.74
845        master
846            .write_all(&rnode_kiss::rnode_command(
847                rnode_kiss::CMD_FW_VERSION,
848                &[0x01, 0x4A],
849            ))
850            .unwrap();
851        // Platform ESP32
852        master
853            .write_all(&rnode_kiss::rnode_command(
854                rnode_kiss::CMD_PLATFORM,
855                &[0x80],
856            ))
857            .unwrap();
858        // MCU
859        master
860            .write_all(&rnode_kiss::rnode_command(rnode_kiss::CMD_MCU, &[0x01]))
861            .unwrap();
862        master.flush().unwrap();
863    }
864
865    #[test]
866    fn rnode_detect_over_pty() {
867        // Test that the RNode decoder can parse detect responses from a PTY
868        let (master_fd, slave_fd) = open_pty_pair().unwrap();
869        let mut master = unsafe { std::fs::File::from_raw_fd(master_fd) };
870        let mut slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
871
872        // Write detect response to master
873        mock_respond_detect(&mut master);
874
875        // Read from slave with RNode decoder
876        assert!(poll_read(slave.as_raw_fd(), 2000));
877        let mut decoder = rnode_kiss::RNodeDecoder::new();
878        let mut buf = [0u8; 4096];
879        let n = slave.read(&mut buf).unwrap();
880        let events = decoder.feed(&buf[..n]);
881
882        assert!(
883            events
884                .iter()
885                .any(|e| matches!(e, rnode_kiss::RNodeEvent::Detected(true))),
886            "should detect device"
887        );
888        assert!(
889            events
890                .iter()
891                .any(|e| matches!(e, rnode_kiss::RNodeEvent::FirmwareVersion { .. })),
892            "should get firmware version"
893        );
894    }
895
896    #[test]
897    fn rnode_configure_commands() {
898        let (master_fd, slave_fd) = open_pty_pair().unwrap();
899        let mut master = unsafe { std::fs::File::from_raw_fd(master_fd) };
900        let writer_file = unsafe { std::fs::File::from_raw_fd(libc::dup(slave_fd)) };
901        let writer = Arc::new(Mutex::new(writer_file));
902
903        let sub = RNodeSubConfig {
904            name: "test".into(),
905            frequency: 868_000_000,
906            bandwidth: 125_000,
907            txpower: 7,
908            spreading_factor: 8,
909            coding_rate: 5,
910            flow_control: false,
911            st_alock: None,
912            lt_alock: None,
913        };
914
915        configure_subinterface(&writer, 0, &sub, false).unwrap();
916
917        // Read what was sent
918        let data = read_available(&mut master);
919
920        // Should contain frequency command
921        assert!(
922            data.windows(2)
923                .any(|w| w[0] == kiss::FEND && w[1] == rnode_kiss::CMD_FREQUENCY),
924            "should contain FREQUENCY command"
925        );
926        // Should contain bandwidth command
927        assert!(
928            data.windows(2)
929                .any(|w| w[0] == kiss::FEND && w[1] == rnode_kiss::CMD_BANDWIDTH),
930            "should contain BANDWIDTH command"
931        );
932        // Should contain RADIO_STATE ON
933        assert!(
934            data.windows(3).any(|w| w[0] == kiss::FEND
935                && w[1] == rnode_kiss::CMD_RADIO_STATE
936                && w[2] == rnode_kiss::RADIO_STATE_ON),
937            "should contain RADIO_STATE ON command"
938        );
939
940        unsafe { libc::close(slave_fd) };
941    }
942
943    #[test]
944    fn rnode_data_roundtrip() {
945        let (master_fd, slave_fd) = open_pty_pair().unwrap();
946        let mut master = unsafe { std::fs::File::from_raw_fd(master_fd) };
947        let slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
948
949        // Write a data frame (subinterface 0) to master
950        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
951        let frame = rnode_kiss::rnode_data_frame(0, &payload);
952        master.write_all(&frame).unwrap();
953        master.flush().unwrap();
954
955        // Read from slave with RNode decoder
956        assert!(poll_read(slave.as_raw_fd(), 2000));
957        let mut decoder = rnode_kiss::RNodeDecoder::new();
958        let mut buf = [0u8; 4096];
959        let mut slave_file = slave;
960        let n = slave_file.read(&mut buf).unwrap();
961        let events = decoder.feed(&buf[..n]);
962
963        assert_eq!(events.len(), 1);
964        match &events[0] {
965            rnode_kiss::RNodeEvent::DataFrame { index, data } => {
966                assert_eq!(*index, 0);
967                assert_eq!(data, &payload);
968            }
969            other => panic!("expected DataFrame, got {:?}", other),
970        }
971    }
972
973    #[test]
974    fn rnode_flow_control() {
975        let (master_fd, slave_fd) = open_pty_pair().unwrap();
976        let writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
977        let shared_writer = Arc::new(Mutex::new(writer_file));
978
979        let flow_state = Arc::new(Mutex::new(SubFlowState {
980            ready: true,
981            queue: std::collections::VecDeque::new(),
982        }));
983
984        let mut sub_writer = RNodeSubWriter {
985            writer: shared_writer.clone(),
986            index: 0,
987            flow_control: true,
988            flow_state: flow_state.clone(),
989        };
990
991        // First send: should go through (ready=true) and set ready=false
992        sub_writer.send_frame(b"hello").unwrap();
993        assert!(!flow_state.lock().unwrap().ready);
994
995        // Second send: should be queued
996        sub_writer.send_frame(b"world").unwrap();
997        assert_eq!(flow_state.lock().unwrap().queue.len(), 1);
998
999        // Process flow queue (simulates CMD_READY)
1000        process_flow_queue(&flow_state, &shared_writer, 0);
1001        assert_eq!(flow_state.lock().unwrap().queue.len(), 0);
1002        assert!(!flow_state.lock().unwrap().ready); // sent queued, still locked
1003
1004        // Process again with empty queue: sets ready=true
1005        process_flow_queue(&flow_state, &shared_writer, 0);
1006        assert!(flow_state.lock().unwrap().ready);
1007
1008        unsafe { libc::close(master_fd) };
1009    }
1010
1011    #[test]
1012    fn rnode_sub_writer_format() {
1013        let (master_fd, slave_fd) = open_pty_pair().unwrap();
1014        let mut master = unsafe { std::fs::File::from_raw_fd(master_fd) };
1015        let writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
1016        let shared_writer = Arc::new(Mutex::new(writer_file));
1017
1018        let flow_state = Arc::new(Mutex::new(SubFlowState {
1019            ready: true,
1020            queue: std::collections::VecDeque::new(),
1021        }));
1022
1023        let mut sub_writer = RNodeSubWriter {
1024            writer: shared_writer,
1025            index: 1, // subinterface 1
1026            flow_control: false,
1027            flow_state,
1028        };
1029
1030        let payload = vec![0xAA, 0xBB, 0xCC];
1031        sub_writer.send_frame(&payload).unwrap();
1032
1033        // Read from master
1034        assert!(poll_read(master.as_raw_fd(), 2000));
1035        let mut buf = [0u8; 256];
1036        let n = master.read(&mut buf).unwrap();
1037
1038        // Should start with FEND, CMD_INT1_DATA (0x10)
1039        assert_eq!(buf[0], kiss::FEND);
1040        assert_eq!(buf[1], 0x10); // CMD_INT1_DATA
1041        assert_eq!(buf[n - 1], kiss::FEND);
1042    }
1043
1044    #[test]
1045    fn rnode_multi_sub_routing() {
1046        // Test that data frames on different CMD_INTn route to different indices
1047        let mut decoder = rnode_kiss::RNodeDecoder::new();
1048
1049        let payload0 = vec![0x01, 0x02];
1050        let frame0 = rnode_kiss::rnode_data_frame(0, &payload0);
1051        let events0 = decoder.feed(&frame0);
1052        assert_eq!(events0.len(), 1);
1053        assert_eq!(
1054            events0[0],
1055            rnode_kiss::RNodeEvent::DataFrame {
1056                index: 0,
1057                data: payload0
1058            }
1059        );
1060
1061        let payload1 = vec![0x03, 0x04];
1062        let frame1 = rnode_kiss::rnode_data_frame(1, &payload1);
1063        let events1 = decoder.feed(&frame1);
1064        assert_eq!(events1.len(), 1);
1065        assert_eq!(
1066            events1[0],
1067            rnode_kiss::RNodeEvent::DataFrame {
1068                index: 1,
1069                data: payload1
1070            }
1071        );
1072    }
1073
1074    #[test]
1075    fn rnode_error_handling() {
1076        // NOTE: CMD_ERROR (0x90) == CMD_INT5_DATA (0x90) — they share the same
1077        // byte value. In multi-radio mode, 0x90 is treated as INT5_DATA.
1078        // Error events only appear on single-radio devices where there's no
1079        // INT5_DATA. The decoder treats 0x90 as INT5_DATA (data wins).
1080        // For real error detection, we rely on the data frame being invalid
1081        // or the device sending a different error indicator.
1082        // Test that cmd 0x90 is correctly handled as a data frame.
1083        let mut decoder = rnode_kiss::RNodeDecoder::new();
1084        let frame = rnode_kiss::rnode_command(rnode_kiss::CMD_ERROR, &[0x02]);
1085        let events = decoder.feed(&frame);
1086        assert_eq!(events.len(), 1);
1087        // 0x90 = CMD_INT5_DATA, so it's a DataFrame with index 5
1088        assert_eq!(
1089            events[0],
1090            rnode_kiss::RNodeEvent::DataFrame {
1091                index: 5,
1092                data: vec![0x02]
1093            }
1094        );
1095    }
1096
1097    #[test]
1098    fn rnode_config_validation() {
1099        let good = RNodeSubConfig {
1100            name: "test".into(),
1101            frequency: 868_000_000,
1102            bandwidth: 125_000,
1103            txpower: 7,
1104            spreading_factor: 8,
1105            coding_rate: 5,
1106            flow_control: false,
1107            st_alock: None,
1108            lt_alock: None,
1109        };
1110        assert!(validate_sub_config(&good).is_none());
1111
1112        // Bad frequency
1113        let mut bad = good.clone();
1114        bad.frequency = 100;
1115        assert!(validate_sub_config(&bad).is_some());
1116
1117        // Bad SF
1118        bad = good.clone();
1119        bad.spreading_factor = 13;
1120        assert!(validate_sub_config(&bad).is_some());
1121
1122        // Bad CR
1123        bad = good.clone();
1124        bad.coding_rate = 9;
1125        assert!(validate_sub_config(&bad).is_some());
1126
1127        // Bad BW
1128        bad = good.clone();
1129        bad.bandwidth = 5;
1130        assert!(validate_sub_config(&bad).is_some());
1131
1132        // Bad TX power
1133        bad = good.clone();
1134        bad.txpower = 50;
1135        assert!(validate_sub_config(&bad).is_some());
1136    }
1137}