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
397impl Default for RNodeDecoder {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403// ── Command builders ────────────────────────────────────────────────────
404
405/// Build a KISS command frame: [FEND][cmd][escaped value][FEND].
406pub fn rnode_command(cmd: u8, value: &[u8]) -> Vec<u8> {
407    let escaped = kiss::escape(value);
408    let mut out = Vec::with_capacity(escaped.len() + 3);
409    out.push(kiss::FEND);
410    out.push(cmd);
411    out.extend_from_slice(&escaped);
412    out.push(kiss::FEND);
413    out
414}
415
416/// Build a command frame with subinterface selection prefix:
417/// [FEND][CMD_SEL_INT][index][FEND][cmd][escaped value][FEND].
418pub fn rnode_select_command(index: u8, cmd: u8, value: &[u8]) -> Vec<u8> {
419    let mut out = rnode_command(CMD_SEL_INT, &[index]);
420    out.extend_from_slice(&rnode_command(cmd, value));
421    out
422}
423
424/// Build the detect request frame.
425pub fn detect_request() -> Vec<u8> {
426    rnode_command(CMD_DETECT, &[DETECT_REQ])
427}
428
429/// Build a data frame for a subinterface:
430/// [FEND][CMD_INTn_DATA][escaped data][FEND].
431pub fn rnode_data_frame(index: u8, data: &[u8]) -> Vec<u8> {
432    let cmd = if (index as usize) < DATA_CMDS.len() {
433        DATA_CMDS[index as usize]
434    } else {
435        CMD_INT0_DATA
436    };
437    let escaped = kiss::escape(data);
438    let mut out = Vec::with_capacity(escaped.len() + 3);
439    out.push(kiss::FEND);
440    out.push(cmd);
441    out.extend_from_slice(&escaped);
442    out.push(kiss::FEND);
443    out
444}
445
446// ── Tests ───────────────────────────────────────────────────────────────
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn detect_request_format() {
454        let req = detect_request();
455        assert_eq!(req, vec![kiss::FEND, CMD_DETECT, DETECT_REQ, kiss::FEND]);
456    }
457
458    #[test]
459    fn decoder_detect_response() {
460        let response = vec![kiss::FEND, CMD_DETECT, DETECT_RESP, kiss::FEND];
461        let mut decoder = RNodeDecoder::new();
462        let events = decoder.feed(&response);
463        assert_eq!(events.len(), 1);
464        assert_eq!(events[0], RNodeEvent::Detected(true));
465    }
466
467    #[test]
468    fn decoder_firmware_version() {
469        // Version 1.52 with KISS escaping possible
470        let response = vec![kiss::FEND, CMD_FW_VERSION, 0x01, 0x34, kiss::FEND];
471        let mut decoder = RNodeDecoder::new();
472        let events = decoder.feed(&response);
473        assert_eq!(events.len(), 1);
474        assert_eq!(
475            events[0],
476            RNodeEvent::FirmwareVersion {
477                major: 1,
478                minor: 0x34
479            }
480        );
481    }
482
483    #[test]
484    fn decoder_platform() {
485        let response = vec![kiss::FEND, CMD_PLATFORM, 0x80, kiss::FEND]; // ESP32
486        let mut decoder = RNodeDecoder::new();
487        let events = decoder.feed(&response);
488        assert_eq!(events.len(), 1);
489        assert_eq!(events[0], RNodeEvent::Platform(0x80));
490    }
491
492    #[test]
493    fn decoder_interfaces() {
494        let response = vec![kiss::FEND, CMD_INTERFACES, 0x00, 0x01, kiss::FEND];
495        let mut decoder = RNodeDecoder::new();
496        let events = decoder.feed(&response);
497        assert_eq!(events.len(), 1);
498        assert_eq!(
499            events[0],
500            RNodeEvent::InterfaceType {
501                index: 0,
502                type_byte: 0x01
503            }
504        );
505    }
506
507    #[test]
508    fn decoder_frequency() {
509        // 868200000 Hz = 0x33C15740 (but let's use a simpler value)
510        // 867200000 Hz = 0x33B5_D100
511        let freq: u32 = 867_200_000;
512        let response = vec![
513            kiss::FEND,
514            CMD_FREQUENCY,
515            (freq >> 24) as u8,
516            (freq >> 16) as u8,
517            (freq >> 8) as u8,
518            (freq & 0xFF) as u8,
519            kiss::FEND,
520        ];
521        let mut decoder = RNodeDecoder::new();
522        let events = decoder.feed(&response);
523        assert_eq!(events.len(), 1);
524        assert_eq!(events[0], RNodeEvent::Frequency(867_200_000));
525    }
526
527    #[test]
528    fn decoder_data_frame_int0() {
529        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
530        // CMD_INT0_DATA = 0x00 (same as CMD_DATA)
531        let mut frame = vec![kiss::FEND, CMD_INT0_DATA];
532        frame.extend_from_slice(&kiss::escape(&payload));
533        frame.push(kiss::FEND);
534
535        let mut decoder = RNodeDecoder::new();
536        let events = decoder.feed(&frame);
537        assert_eq!(events.len(), 1);
538        assert_eq!(
539            events[0],
540            RNodeEvent::DataFrame {
541                index: 0,
542                data: payload
543            }
544        );
545    }
546
547    #[test]
548    fn decoder_multi_sub_data() {
549        let payload = vec![0xAA, 0xBB];
550        // CMD_INT1_DATA = 0x10
551        let mut frame = vec![kiss::FEND, CMD_INT1_DATA];
552        frame.extend_from_slice(&kiss::escape(&payload));
553        frame.push(kiss::FEND);
554
555        let mut decoder = RNodeDecoder::new();
556        let events = decoder.feed(&frame);
557        assert_eq!(events.len(), 1);
558        assert_eq!(
559            events[0],
560            RNodeEvent::DataFrame {
561                index: 1,
562                data: payload
563            }
564        );
565    }
566
567    #[test]
568    fn rnode_select_command_format() {
569        // Select subinterface 1, then set frequency
570        let freq: u32 = 868_000_000;
571        let freq_bytes = [
572            (freq >> 24) as u8,
573            (freq >> 16) as u8,
574            (freq >> 8) as u8,
575            (freq & 0xFF) as u8,
576        ];
577        let cmd = rnode_select_command(1, CMD_FREQUENCY, &freq_bytes);
578
579        // Should start with [FEND][CMD_SEL_INT][0x01][FEND]
580        assert_eq!(cmd[0], kiss::FEND);
581        assert_eq!(cmd[1], CMD_SEL_INT);
582        assert_eq!(cmd[2], 0x01);
583        assert_eq!(cmd[3], kiss::FEND);
584
585        // Then [FEND][CMD_FREQUENCY][escaped bytes][FEND]
586        assert_eq!(cmd[4], kiss::FEND);
587        assert_eq!(cmd[5], CMD_FREQUENCY);
588    }
589
590    #[test]
591    fn rnode_data_frame_format() {
592        let data = vec![0x01, 0x02, 0x03];
593        let frame = rnode_data_frame(0, &data);
594        assert_eq!(frame[0], kiss::FEND);
595        assert_eq!(frame[1], CMD_INT0_DATA);
596        assert_eq!(*frame.last().unwrap(), kiss::FEND);
597
598        // Subinterface 1
599        let frame1 = rnode_data_frame(1, &data);
600        assert_eq!(frame1[1], CMD_INT1_DATA);
601
602        // Subinterface 2
603        let frame2 = rnode_data_frame(2, &data);
604        assert_eq!(frame2[1], CMD_INT2_DATA);
605    }
606}