Skip to main content

rns_net/
rnode_kiss.rs

1//! RNode-specific KISS protocol commands and streaming decoder.
2//!
3//! Extends `kiss.rs` with RNode command constants, multi-byte responses,
4//! and subinterface routing for multi-radio RNode devices.
5//! Matches Python `RNodeInterface.py` and `RNodeMultiInterface.py`.
6
7use crate::kiss;
8
9// ── RNode KISS command bytes ────────────────────────────────────────────
10
11pub const CMD_FREQUENCY: u8 = 0x01;
12pub const CMD_BANDWIDTH: u8 = 0x02;
13pub const CMD_TXPOWER: u8 = 0x03;
14pub const CMD_SF: u8 = 0x04;
15pub const CMD_CR: u8 = 0x05;
16pub const CMD_RADIO_STATE: u8 = 0x06;
17pub const CMD_RADIO_LOCK: u8 = 0x07;
18pub const CMD_DETECT: u8 = 0x08;
19pub const CMD_LEAVE: u8 = 0x0A;
20pub const CMD_ST_ALOCK: u8 = 0x0B;
21pub const CMD_LT_ALOCK: u8 = 0x0C;
22pub const CMD_READY: u8 = 0x0F;
23pub const CMD_SEL_INT: u8 = 0x1F;
24pub const CMD_STAT_RSSI: u8 = 0x23;
25pub const CMD_STAT_SNR: u8 = 0x24;
26pub const CMD_RANDOM: u8 = 0x40;
27pub const CMD_PLATFORM: u8 = 0x48;
28pub const CMD_MCU: u8 = 0x49;
29pub const CMD_FW_VERSION: u8 = 0x50;
30pub const CMD_FW_DETAIL: u8 = 0x51;
31pub const CMD_RESET: u8 = 0x55;
32pub const CMD_INTERFACES: u8 = 0x71;
33pub const CMD_ERROR: u8 = 0x90;
34
35pub const DETECT_REQ: u8 = 0x73;
36pub const DETECT_RESP: u8 = 0x46;
37
38pub const RADIO_STATE_OFF: u8 = 0x00;
39pub const RADIO_STATE_ON: u8 = 0x01;
40
41// Subinterface data command bytes (from RNodeMultiInterface.py)
42const CMD_INT0_DATA: u8 = 0x00;
43const CMD_INT1_DATA: u8 = 0x10;
44const CMD_INT2_DATA: u8 = 0x20;
45const CMD_INT3_DATA: u8 = 0x70;
46const CMD_INT4_DATA: u8 = 0x75;
47const CMD_INT5_DATA: u8 = 0x90;
48const CMD_INT6_DATA: u8 = 0xA0;
49const CMD_INT7_DATA: u8 = 0xB0;
50const CMD_INT8_DATA: u8 = 0xC0;
51const CMD_INT9_DATA: u8 = 0xD0;
52const CMD_INT10_DATA: u8 = 0xE0;
53const CMD_INT11_DATA: u8 = 0xF0;
54
55/// All subinterface data command bytes, indexed by subinterface number.
56const DATA_CMDS: [u8; 12] = [
57    CMD_INT0_DATA,
58    CMD_INT1_DATA,
59    CMD_INT2_DATA,
60    CMD_INT3_DATA,
61    CMD_INT4_DATA,
62    CMD_INT5_DATA,
63    CMD_INT6_DATA,
64    CMD_INT7_DATA,
65    CMD_INT8_DATA,
66    CMD_INT9_DATA,
67    CMD_INT10_DATA,
68    CMD_INT11_DATA,
69];
70
71/// Map a command byte to a subinterface data index, or None.
72fn data_cmd_to_index(cmd: u8) -> Option<usize> {
73    DATA_CMDS.iter().position(|&c| c == cmd)
74}
75
76// ── Events ──────────────────────────────────────────────────────────────
77
78/// Events yielded by the RNode decoder.
79#[derive(Debug, Clone, PartialEq)]
80pub enum RNodeEvent {
81    /// A data frame was received on the given subinterface.
82    DataFrame { index: usize, data: Vec<u8> },
83    /// Device detection response.
84    Detected(bool),
85    /// Firmware version reported.
86    FirmwareVersion { major: u8, minor: u8 },
87    /// Platform byte reported.
88    Platform(u8),
89    /// MCU byte reported.
90    Mcu(u8),
91    /// Interface type for a given index.
92    InterfaceType { index: u8, type_byte: u8 },
93    /// Reported frequency (Hz).
94    Frequency(u32),
95    /// Reported bandwidth (Hz).
96    Bandwidth(u32),
97    /// Reported TX power (dBm, signed).
98    TxPower(i8),
99    /// Reported spreading factor.
100    SpreadingFactor(u8),
101    /// Reported coding rate.
102    CodingRate(u8),
103    /// Reported radio state.
104    RadioState(u8),
105    /// Reported RSSI (raw byte, caller subtracts RSSI_OFFSET=157).
106    StatRssi(u8),
107    /// Reported SNR (signed, multiply by 0.25 for dB).
108    StatSnr(i8),
109    /// Reported short-term airtime lock (percent * 100).
110    StAlock(u16),
111    /// Reported long-term airtime lock (percent * 100).
112    LtAlock(u16),
113    /// Flow control: device ready for next packet.
114    Ready,
115    /// Selected subinterface changed.
116    SelectedInterface(u8),
117    /// Detailed firmware version string (e.g. "0.1.142-f63fb02").
118    FirmwareDetail(String),
119    /// Error code from device.
120    Error(u8),
121}
122
123// ── Decoder ─────────────────────────────────────────────────────────────
124
125/// Streaming RNode KISS decoder.
126///
127/// Handles KISS framing, KISS escape sequences, multi-byte command
128/// responses, and subinterface data routing.
129pub struct RNodeDecoder {
130    in_frame: bool,
131    escape: bool,
132    command: u8,
133    data_buffer: Vec<u8>,
134    command_buffer: Vec<u8>,
135    selected_index: u8,
136}
137
138impl RNodeDecoder {
139    pub fn new() -> Self {
140        RNodeDecoder {
141            in_frame: false,
142            escape: false,
143            command: kiss::CMD_UNKNOWN,
144            data_buffer: Vec::new(),
145            command_buffer: Vec::new(),
146            selected_index: 0,
147        }
148    }
149
150    /// Current selected subinterface index.
151    pub fn selected_index(&self) -> u8 {
152        self.selected_index
153    }
154
155    /// Feed raw bytes and return decoded events.
156    pub fn feed(&mut self, bytes: &[u8]) -> Vec<RNodeEvent> {
157        let mut events = Vec::new();
158
159        for &byte in bytes {
160            if self.in_frame && byte == kiss::FEND {
161                // End of frame — check if we have buffered data for a data command
162                if let Some(idx) = data_cmd_to_index(self.command) {
163                    if !self.data_buffer.is_empty() {
164                        events.push(RNodeEvent::DataFrame {
165                            index: idx,
166                            data: core::mem::take(&mut self.data_buffer),
167                        });
168                    }
169                } else if self.command == kiss::CMD_DATA {
170                    if !self.data_buffer.is_empty() {
171                        events.push(RNodeEvent::DataFrame {
172                            index: self.selected_index as usize,
173                            data: core::mem::take(&mut self.data_buffer),
174                        });
175                    }
176                } else if self.command == CMD_FW_DETAIL {
177                    if !self.data_buffer.is_empty() {
178                        let s = String::from_utf8_lossy(&self.data_buffer).into_owned();
179                        events.push(RNodeEvent::FirmwareDetail(s));
180                        self.data_buffer.clear();
181                    }
182                }
183                // Start new frame (closing FLAG = opening FLAG of next)
184                self.in_frame = true;
185                self.command = kiss::CMD_UNKNOWN;
186                self.data_buffer.clear();
187                self.command_buffer.clear();
188                self.escape = false;
189            } else if byte == kiss::FEND {
190                // Opening frame
191                self.in_frame = true;
192                self.command = kiss::CMD_UNKNOWN;
193                self.data_buffer.clear();
194                self.command_buffer.clear();
195                self.escape = false;
196            } else if self.in_frame {
197                if self.data_buffer.is_empty()
198                    && self.command_buffer.is_empty()
199                    && self.command == kiss::CMD_UNKNOWN
200                {
201                    // First byte after FEND is the command
202                    self.command = byte;
203                } else if self.command == kiss::CMD_DATA
204                    || self.command == CMD_FW_DETAIL
205                    || data_cmd_to_index(self.command).is_some()
206                {
207                    // Data frame: accumulate with KISS unescaping
208                    if byte == kiss::FESC {
209                        self.escape = true;
210                    } else if self.escape {
211                        match byte {
212                            kiss::TFEND => self.data_buffer.push(kiss::FEND),
213                            kiss::TFESC => self.data_buffer.push(kiss::FESC),
214                            _ => self.data_buffer.push(byte),
215                        }
216                        self.escape = false;
217                    } else {
218                        self.data_buffer.push(byte);
219                    }
220                } else {
221                    // Command response: accumulate with KISS unescaping, then parse
222                    let val = if byte == kiss::FESC {
223                        self.escape = true;
224                        continue;
225                    } else if self.escape {
226                        self.escape = false;
227                        match byte {
228                            kiss::TFEND => kiss::FEND,
229                            kiss::TFESC => kiss::FESC,
230                            _ => byte,
231                        }
232                    } else {
233                        byte
234                    };
235
236                    self.command_buffer.push(val);
237                    self.parse_command(&mut events);
238                }
239            }
240        }
241
242        events
243    }
244
245    /// Check if a complete command response has been accumulated and emit event.
246    fn parse_command(&mut self, events: &mut Vec<RNodeEvent>) {
247        let buf = &self.command_buffer;
248        match self.command {
249            CMD_DETECT => {
250                if buf.len() >= 1 {
251                    events.push(RNodeEvent::Detected(buf[0] == DETECT_RESP));
252                    self.command = kiss::CMD_UNKNOWN;
253                    self.in_frame = false;
254                }
255            }
256            CMD_FW_VERSION => {
257                if buf.len() >= 2 {
258                    events.push(RNodeEvent::FirmwareVersion {
259                        major: buf[0],
260                        minor: buf[1],
261                    });
262                    self.command = kiss::CMD_UNKNOWN;
263                    self.in_frame = false;
264                }
265            }
266            CMD_PLATFORM => {
267                if buf.len() >= 1 {
268                    events.push(RNodeEvent::Platform(buf[0]));
269                    self.command = kiss::CMD_UNKNOWN;
270                    self.in_frame = false;
271                }
272            }
273            CMD_MCU => {
274                if buf.len() >= 1 {
275                    events.push(RNodeEvent::Mcu(buf[0]));
276                    self.command = kiss::CMD_UNKNOWN;
277                    self.in_frame = false;
278                }
279            }
280            CMD_INTERFACES => {
281                if buf.len() >= 2 {
282                    events.push(RNodeEvent::InterfaceType {
283                        index: buf[0],
284                        type_byte: buf[1],
285                    });
286                    self.command = kiss::CMD_UNKNOWN;
287                    self.in_frame = false;
288                }
289            }
290            CMD_FREQUENCY => {
291                if buf.len() >= 4 {
292                    let freq = (buf[0] as u32) << 24
293                        | (buf[1] as u32) << 16
294                        | (buf[2] as u32) << 8
295                        | buf[3] as u32;
296                    events.push(RNodeEvent::Frequency(freq));
297                    self.command = kiss::CMD_UNKNOWN;
298                    self.in_frame = false;
299                }
300            }
301            CMD_BANDWIDTH => {
302                if buf.len() >= 4 {
303                    let bw = (buf[0] as u32) << 24
304                        | (buf[1] as u32) << 16
305                        | (buf[2] as u32) << 8
306                        | buf[3] as u32;
307                    events.push(RNodeEvent::Bandwidth(bw));
308                    self.command = kiss::CMD_UNKNOWN;
309                    self.in_frame = false;
310                }
311            }
312            CMD_TXPOWER => {
313                if buf.len() >= 1 {
314                    events.push(RNodeEvent::TxPower(buf[0] as i8));
315                    self.command = kiss::CMD_UNKNOWN;
316                    self.in_frame = false;
317                }
318            }
319            CMD_SF => {
320                if buf.len() >= 1 {
321                    events.push(RNodeEvent::SpreadingFactor(buf[0]));
322                    self.command = kiss::CMD_UNKNOWN;
323                    self.in_frame = false;
324                }
325            }
326            CMD_CR => {
327                if buf.len() >= 1 {
328                    events.push(RNodeEvent::CodingRate(buf[0]));
329                    self.command = kiss::CMD_UNKNOWN;
330                    self.in_frame = false;
331                }
332            }
333            CMD_RADIO_STATE => {
334                if buf.len() >= 1 {
335                    events.push(RNodeEvent::RadioState(buf[0]));
336                    self.command = kiss::CMD_UNKNOWN;
337                    self.in_frame = false;
338                }
339            }
340            CMD_STAT_RSSI => {
341                if buf.len() >= 1 {
342                    events.push(RNodeEvent::StatRssi(buf[0]));
343                    self.command = kiss::CMD_UNKNOWN;
344                    self.in_frame = false;
345                }
346            }
347            CMD_STAT_SNR => {
348                if buf.len() >= 1 {
349                    events.push(RNodeEvent::StatSnr(buf[0] as i8));
350                    self.command = kiss::CMD_UNKNOWN;
351                    self.in_frame = false;
352                }
353            }
354            CMD_ST_ALOCK => {
355                if buf.len() >= 2 {
356                    let val = (buf[0] as u16) << 8 | buf[1] as u16;
357                    events.push(RNodeEvent::StAlock(val));
358                    self.command = kiss::CMD_UNKNOWN;
359                    self.in_frame = false;
360                }
361            }
362            CMD_LT_ALOCK => {
363                if buf.len() >= 2 {
364                    let val = (buf[0] as u16) << 8 | buf[1] as u16;
365                    events.push(RNodeEvent::LtAlock(val));
366                    self.command = kiss::CMD_UNKNOWN;
367                    self.in_frame = false;
368                }
369            }
370            CMD_READY => {
371                events.push(RNodeEvent::Ready);
372                self.command = kiss::CMD_UNKNOWN;
373                self.in_frame = false;
374            }
375            CMD_SEL_INT => {
376                if buf.len() >= 1 {
377                    self.selected_index = buf[0];
378                    events.push(RNodeEvent::SelectedInterface(buf[0]));
379                    self.command = kiss::CMD_UNKNOWN;
380                    self.in_frame = false;
381                }
382            }
383            CMD_ERROR => {
384                if buf.len() >= 1 {
385                    events.push(RNodeEvent::Error(buf[0]));
386                    self.command = kiss::CMD_UNKNOWN;
387                    self.in_frame = false;
388                }
389            }
390            _ => {
391                // Unknown command, ignore
392            }
393        }
394    }
395}
396
397// ── Command builders ────────────────────────────────────────────────────
398
399/// Build a KISS command frame: [FEND][cmd][escaped value][FEND].
400pub fn rnode_command(cmd: u8, value: &[u8]) -> Vec<u8> {
401    let escaped = kiss::escape(value);
402    let mut out = Vec::with_capacity(escaped.len() + 3);
403    out.push(kiss::FEND);
404    out.push(cmd);
405    out.extend_from_slice(&escaped);
406    out.push(kiss::FEND);
407    out
408}
409
410/// Build a command frame with subinterface selection prefix:
411/// [FEND][CMD_SEL_INT][index][FEND][cmd][escaped value][FEND].
412pub fn rnode_select_command(index: u8, cmd: u8, value: &[u8]) -> Vec<u8> {
413    let mut out = rnode_command(CMD_SEL_INT, &[index]);
414    out.extend_from_slice(&rnode_command(cmd, value));
415    out
416}
417
418/// Build the detect request frame.
419pub fn detect_request() -> Vec<u8> {
420    rnode_command(CMD_DETECT, &[DETECT_REQ])
421}
422
423/// Build a data frame for a subinterface:
424/// [FEND][CMD_INTn_DATA][escaped data][FEND].
425pub fn rnode_data_frame(index: u8, data: &[u8]) -> Vec<u8> {
426    let cmd = if (index as usize) < DATA_CMDS.len() {
427        DATA_CMDS[index as usize]
428    } else {
429        CMD_INT0_DATA
430    };
431    let escaped = kiss::escape(data);
432    let mut out = Vec::with_capacity(escaped.len() + 3);
433    out.push(kiss::FEND);
434    out.push(cmd);
435    out.extend_from_slice(&escaped);
436    out.push(kiss::FEND);
437    out
438}
439
440// ── Tests ───────────────────────────────────────────────────────────────
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn detect_request_format() {
448        let req = detect_request();
449        assert_eq!(req, vec![kiss::FEND, CMD_DETECT, DETECT_REQ, kiss::FEND]);
450    }
451
452    #[test]
453    fn decoder_detect_response() {
454        let response = vec![kiss::FEND, CMD_DETECT, DETECT_RESP, kiss::FEND];
455        let mut decoder = RNodeDecoder::new();
456        let events = decoder.feed(&response);
457        assert_eq!(events.len(), 1);
458        assert_eq!(events[0], RNodeEvent::Detected(true));
459    }
460
461    #[test]
462    fn decoder_firmware_version() {
463        // Version 1.52 with KISS escaping possible
464        let response = vec![kiss::FEND, CMD_FW_VERSION, 0x01, 0x34, kiss::FEND];
465        let mut decoder = RNodeDecoder::new();
466        let events = decoder.feed(&response);
467        assert_eq!(events.len(), 1);
468        assert_eq!(
469            events[0],
470            RNodeEvent::FirmwareVersion {
471                major: 1,
472                minor: 0x34
473            }
474        );
475    }
476
477    #[test]
478    fn decoder_platform() {
479        let response = vec![kiss::FEND, CMD_PLATFORM, 0x80, kiss::FEND]; // ESP32
480        let mut decoder = RNodeDecoder::new();
481        let events = decoder.feed(&response);
482        assert_eq!(events.len(), 1);
483        assert_eq!(events[0], RNodeEvent::Platform(0x80));
484    }
485
486    #[test]
487    fn decoder_interfaces() {
488        let response = vec![kiss::FEND, CMD_INTERFACES, 0x00, 0x01, kiss::FEND];
489        let mut decoder = RNodeDecoder::new();
490        let events = decoder.feed(&response);
491        assert_eq!(events.len(), 1);
492        assert_eq!(
493            events[0],
494            RNodeEvent::InterfaceType {
495                index: 0,
496                type_byte: 0x01
497            }
498        );
499    }
500
501    #[test]
502    fn decoder_frequency() {
503        // 868200000 Hz = 0x33C15740 (but let's use a simpler value)
504        // 867200000 Hz = 0x33B5_D100
505        let freq: u32 = 867_200_000;
506        let response = vec![
507            kiss::FEND,
508            CMD_FREQUENCY,
509            (freq >> 24) as u8,
510            (freq >> 16) as u8,
511            (freq >> 8) as u8,
512            (freq & 0xFF) as u8,
513            kiss::FEND,
514        ];
515        let mut decoder = RNodeDecoder::new();
516        let events = decoder.feed(&response);
517        assert_eq!(events.len(), 1);
518        assert_eq!(events[0], RNodeEvent::Frequency(867_200_000));
519    }
520
521    #[test]
522    fn decoder_data_frame_int0() {
523        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
524        // CMD_INT0_DATA = 0x00 (same as CMD_DATA)
525        let mut frame = vec![kiss::FEND, CMD_INT0_DATA];
526        frame.extend_from_slice(&kiss::escape(&payload));
527        frame.push(kiss::FEND);
528
529        let mut decoder = RNodeDecoder::new();
530        let events = decoder.feed(&frame);
531        assert_eq!(events.len(), 1);
532        assert_eq!(
533            events[0],
534            RNodeEvent::DataFrame {
535                index: 0,
536                data: payload
537            }
538        );
539    }
540
541    #[test]
542    fn decoder_multi_sub_data() {
543        let payload = vec![0xAA, 0xBB];
544        // CMD_INT1_DATA = 0x10
545        let mut frame = vec![kiss::FEND, CMD_INT1_DATA];
546        frame.extend_from_slice(&kiss::escape(&payload));
547        frame.push(kiss::FEND);
548
549        let mut decoder = RNodeDecoder::new();
550        let events = decoder.feed(&frame);
551        assert_eq!(events.len(), 1);
552        assert_eq!(
553            events[0],
554            RNodeEvent::DataFrame {
555                index: 1,
556                data: payload
557            }
558        );
559    }
560
561    #[test]
562    fn rnode_select_command_format() {
563        // Select subinterface 1, then set frequency
564        let freq: u32 = 868_000_000;
565        let freq_bytes = [
566            (freq >> 24) as u8,
567            (freq >> 16) as u8,
568            (freq >> 8) as u8,
569            (freq & 0xFF) as u8,
570        ];
571        let cmd = rnode_select_command(1, CMD_FREQUENCY, &freq_bytes);
572
573        // Should start with [FEND][CMD_SEL_INT][0x01][FEND]
574        assert_eq!(cmd[0], kiss::FEND);
575        assert_eq!(cmd[1], CMD_SEL_INT);
576        assert_eq!(cmd[2], 0x01);
577        assert_eq!(cmd[3], kiss::FEND);
578
579        // Then [FEND][CMD_FREQUENCY][escaped bytes][FEND]
580        assert_eq!(cmd[4], kiss::FEND);
581        assert_eq!(cmd[5], CMD_FREQUENCY);
582    }
583
584    #[test]
585    fn rnode_data_frame_format() {
586        let data = vec![0x01, 0x02, 0x03];
587        let frame = rnode_data_frame(0, &data);
588        assert_eq!(frame[0], kiss::FEND);
589        assert_eq!(frame[1], CMD_INT0_DATA);
590        assert_eq!(*frame.last().unwrap(), kiss::FEND);
591
592        // Subinterface 1
593        let frame1 = rnode_data_frame(1, &data);
594        assert_eq!(frame1[1], CMD_INT1_DATA);
595
596        // Subinterface 2
597        let frame2 = rnode_data_frame(2, &data);
598        assert_eq!(frame2[1], CMD_INT2_DATA);
599    }
600}