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