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 && !self.data_buffer.is_empty() {
177                    let s = String::from_utf8_lossy(&self.data_buffer).into_owned();
178                    events.push(RNodeEvent::FirmwareDetail(s));
179                    self.data_buffer.clear();
180                }
181                // Start new frame (closing FLAG = opening FLAG of next)
182                self.in_frame = true;
183                self.command = kiss::CMD_UNKNOWN;
184                self.data_buffer.clear();
185                self.command_buffer.clear();
186                self.escape = false;
187            } else if byte == kiss::FEND {
188                // Opening frame
189                self.in_frame = true;
190                self.command = kiss::CMD_UNKNOWN;
191                self.data_buffer.clear();
192                self.command_buffer.clear();
193                self.escape = false;
194            } else if self.in_frame {
195                if self.data_buffer.is_empty()
196                    && self.command_buffer.is_empty()
197                    && self.command == kiss::CMD_UNKNOWN
198                {
199                    // First byte after FEND is the command
200                    self.command = byte;
201                } else if self.command == kiss::CMD_DATA
202                    || self.command == CMD_FW_DETAIL
203                    || data_cmd_to_index(self.command).is_some()
204                {
205                    // Data frame: accumulate with KISS unescaping
206                    if byte == kiss::FESC {
207                        self.escape = true;
208                    } else if self.escape {
209                        match byte {
210                            kiss::TFEND => self.data_buffer.push(kiss::FEND),
211                            kiss::TFESC => self.data_buffer.push(kiss::FESC),
212                            _ => self.data_buffer.push(byte),
213                        }
214                        self.escape = false;
215                    } else {
216                        self.data_buffer.push(byte);
217                    }
218                } else {
219                    // Command response: accumulate with KISS unescaping, then parse
220                    let val = if byte == kiss::FESC {
221                        self.escape = true;
222                        continue;
223                    } else if self.escape {
224                        self.escape = false;
225                        match byte {
226                            kiss::TFEND => kiss::FEND,
227                            kiss::TFESC => kiss::FESC,
228                            _ => byte,
229                        }
230                    } else {
231                        byte
232                    };
233
234                    self.command_buffer.push(val);
235                    self.parse_command(&mut events);
236                }
237            }
238        }
239
240        events
241    }
242
243    /// Check if a complete command response has been accumulated and emit event.
244    fn parse_command(&mut self, events: &mut Vec<RNodeEvent>) {
245        let buf = &self.command_buffer;
246        match self.command {
247            CMD_DETECT => {
248                if !buf.is_empty() {
249                    events.push(RNodeEvent::Detected(buf[0] == DETECT_RESP));
250                    self.command = kiss::CMD_UNKNOWN;
251                    self.in_frame = false;
252                }
253            }
254            CMD_FW_VERSION => {
255                if buf.len() >= 2 {
256                    events.push(RNodeEvent::FirmwareVersion {
257                        major: buf[0],
258                        minor: buf[1],
259                    });
260                    self.command = kiss::CMD_UNKNOWN;
261                    self.in_frame = false;
262                }
263            }
264            CMD_PLATFORM => {
265                if !buf.is_empty() {
266                    events.push(RNodeEvent::Platform(buf[0]));
267                    self.command = kiss::CMD_UNKNOWN;
268                    self.in_frame = false;
269                }
270            }
271            CMD_MCU => {
272                if !buf.is_empty() {
273                    events.push(RNodeEvent::Mcu(buf[0]));
274                    self.command = kiss::CMD_UNKNOWN;
275                    self.in_frame = false;
276                }
277            }
278            CMD_INTERFACES => {
279                if buf.len() >= 2 {
280                    events.push(RNodeEvent::InterfaceType {
281                        index: buf[0],
282                        type_byte: buf[1],
283                    });
284                    self.command = kiss::CMD_UNKNOWN;
285                    self.in_frame = false;
286                }
287            }
288            CMD_FREQUENCY => {
289                if buf.len() >= 4 {
290                    let freq = (buf[0] as u32) << 24
291                        | (buf[1] as u32) << 16
292                        | (buf[2] as u32) << 8
293                        | buf[3] as u32;
294                    events.push(RNodeEvent::Frequency(freq));
295                    self.command = kiss::CMD_UNKNOWN;
296                    self.in_frame = false;
297                }
298            }
299            CMD_BANDWIDTH => {
300                if buf.len() >= 4 {
301                    let bw = (buf[0] as u32) << 24
302                        | (buf[1] as u32) << 16
303                        | (buf[2] as u32) << 8
304                        | buf[3] as u32;
305                    events.push(RNodeEvent::Bandwidth(bw));
306                    self.command = kiss::CMD_UNKNOWN;
307                    self.in_frame = false;
308                }
309            }
310            CMD_TXPOWER => {
311                if !buf.is_empty() {
312                    events.push(RNodeEvent::TxPower(buf[0] as i8));
313                    self.command = kiss::CMD_UNKNOWN;
314                    self.in_frame = false;
315                }
316            }
317            CMD_SF => {
318                if !buf.is_empty() {
319                    events.push(RNodeEvent::SpreadingFactor(buf[0]));
320                    self.command = kiss::CMD_UNKNOWN;
321                    self.in_frame = false;
322                }
323            }
324            CMD_CR => {
325                if !buf.is_empty() {
326                    events.push(RNodeEvent::CodingRate(buf[0]));
327                    self.command = kiss::CMD_UNKNOWN;
328                    self.in_frame = false;
329                }
330            }
331            CMD_RADIO_STATE => {
332                if !buf.is_empty() {
333                    events.push(RNodeEvent::RadioState(buf[0]));
334                    self.command = kiss::CMD_UNKNOWN;
335                    self.in_frame = false;
336                }
337            }
338            CMD_STAT_RSSI => {
339                if !buf.is_empty() {
340                    events.push(RNodeEvent::StatRssi(buf[0]));
341                    self.command = kiss::CMD_UNKNOWN;
342                    self.in_frame = false;
343                }
344            }
345            CMD_STAT_SNR => {
346                if !buf.is_empty() {
347                    events.push(RNodeEvent::StatSnr(buf[0] as i8));
348                    self.command = kiss::CMD_UNKNOWN;
349                    self.in_frame = false;
350                }
351            }
352            CMD_ST_ALOCK => {
353                if buf.len() >= 2 {
354                    let val = (buf[0] as u16) << 8 | buf[1] as u16;
355                    events.push(RNodeEvent::StAlock(val));
356                    self.command = kiss::CMD_UNKNOWN;
357                    self.in_frame = false;
358                }
359            }
360            CMD_LT_ALOCK => {
361                if buf.len() >= 2 {
362                    let val = (buf[0] as u16) << 8 | buf[1] as u16;
363                    events.push(RNodeEvent::LtAlock(val));
364                    self.command = kiss::CMD_UNKNOWN;
365                    self.in_frame = false;
366                }
367            }
368            CMD_READY => {
369                events.push(RNodeEvent::Ready);
370                self.command = kiss::CMD_UNKNOWN;
371                self.in_frame = false;
372            }
373            CMD_SEL_INT => {
374                if !buf.is_empty() {
375                    self.selected_index = buf[0];
376                    events.push(RNodeEvent::SelectedInterface(buf[0]));
377                    self.command = kiss::CMD_UNKNOWN;
378                    self.in_frame = false;
379                }
380            }
381            CMD_ERROR if !buf.is_empty() => {
382                events.push(RNodeEvent::Error(buf[0]));
383                self.command = kiss::CMD_UNKNOWN;
384                self.in_frame = false;
385            }
386            _ => {
387                // Unknown command, ignore
388            }
389        }
390    }
391}
392
393impl Default for RNodeDecoder {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399// ── Command builders ────────────────────────────────────────────────────
400
401/// Build a KISS command frame: [FEND][cmd][escaped value][FEND].
402pub fn rnode_command(cmd: u8, value: &[u8]) -> Vec<u8> {
403    let escaped = kiss::escape(value);
404    let mut out = Vec::with_capacity(escaped.len() + 3);
405    out.push(kiss::FEND);
406    out.push(cmd);
407    out.extend_from_slice(&escaped);
408    out.push(kiss::FEND);
409    out
410}
411
412/// Build a command frame with subinterface selection prefix:
413/// [FEND][CMD_SEL_INT][index][FEND][cmd][escaped value][FEND].
414pub fn rnode_select_command(index: u8, cmd: u8, value: &[u8]) -> Vec<u8> {
415    let mut out = rnode_command(CMD_SEL_INT, &[index]);
416    out.extend_from_slice(&rnode_command(cmd, value));
417    out
418}
419
420/// Build the detect request frame.
421pub fn detect_request() -> Vec<u8> {
422    rnode_command(CMD_DETECT, &[DETECT_REQ])
423}
424
425/// Build a data frame for a subinterface:
426/// [FEND][CMD_INTn_DATA][escaped data][FEND].
427pub fn rnode_data_frame(index: u8, data: &[u8]) -> Vec<u8> {
428    let cmd = if (index as usize) < DATA_CMDS.len() {
429        DATA_CMDS[index as usize]
430    } else {
431        CMD_INT0_DATA
432    };
433    let escaped = kiss::escape(data);
434    let mut out = Vec::with_capacity(escaped.len() + 3);
435    out.push(kiss::FEND);
436    out.push(cmd);
437    out.extend_from_slice(&escaped);
438    out.push(kiss::FEND);
439    out
440}
441
442/// Build the Python `RNodeMultiInterface` transmit sequence. Outbound multi
443/// frames select the actual virtual port and then use `CMD_DATA`; the
444/// `CMD_INTn_DATA` values are device-to-host demultiplexing commands.
445pub fn rnode_multi_data_frame(index: u8, data: &[u8]) -> Vec<u8> {
446    rnode_select_command(index, kiss::CMD_DATA, data)
447}
448
449// ── Tests ───────────────────────────────────────────────────────────────
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn detect_request_format() {
457        let req = detect_request();
458        assert_eq!(req, vec![kiss::FEND, CMD_DETECT, DETECT_REQ, kiss::FEND]);
459    }
460
461    #[test]
462    fn decoder_detect_response() {
463        let response = vec![kiss::FEND, CMD_DETECT, DETECT_RESP, kiss::FEND];
464        let mut decoder = RNodeDecoder::new();
465        let events = decoder.feed(&response);
466        assert_eq!(events.len(), 1);
467        assert_eq!(events[0], RNodeEvent::Detected(true));
468    }
469
470    #[test]
471    fn decoder_firmware_version() {
472        // Version 1.52 with KISS escaping possible
473        let response = vec![kiss::FEND, CMD_FW_VERSION, 0x01, 0x34, kiss::FEND];
474        let mut decoder = RNodeDecoder::new();
475        let events = decoder.feed(&response);
476        assert_eq!(events.len(), 1);
477        assert_eq!(
478            events[0],
479            RNodeEvent::FirmwareVersion {
480                major: 1,
481                minor: 0x34
482            }
483        );
484    }
485
486    #[test]
487    fn decoder_platform() {
488        let response = vec![kiss::FEND, CMD_PLATFORM, 0x80, kiss::FEND]; // ESP32
489        let mut decoder = RNodeDecoder::new();
490        let events = decoder.feed(&response);
491        assert_eq!(events.len(), 1);
492        assert_eq!(events[0], RNodeEvent::Platform(0x80));
493    }
494
495    #[test]
496    fn decoder_interfaces() {
497        let response = vec![kiss::FEND, CMD_INTERFACES, 0x00, 0x01, kiss::FEND];
498        let mut decoder = RNodeDecoder::new();
499        let events = decoder.feed(&response);
500        assert_eq!(events.len(), 1);
501        assert_eq!(
502            events[0],
503            RNodeEvent::InterfaceType {
504                index: 0,
505                type_byte: 0x01
506            }
507        );
508    }
509
510    #[test]
511    fn decoder_frequency() {
512        // 868200000 Hz = 0x33C15740 (but let's use a simpler value)
513        // 867200000 Hz = 0x33B5_D100
514        let freq: u32 = 867_200_000;
515        let response = vec![
516            kiss::FEND,
517            CMD_FREQUENCY,
518            (freq >> 24) as u8,
519            (freq >> 16) as u8,
520            (freq >> 8) as u8,
521            (freq & 0xFF) as u8,
522            kiss::FEND,
523        ];
524        let mut decoder = RNodeDecoder::new();
525        let events = decoder.feed(&response);
526        assert_eq!(events.len(), 1);
527        assert_eq!(events[0], RNodeEvent::Frequency(867_200_000));
528    }
529
530    #[test]
531    fn decoder_data_frame_int0() {
532        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
533        // CMD_INT0_DATA = 0x00 (same as CMD_DATA)
534        let mut frame = vec![kiss::FEND, CMD_INT0_DATA];
535        frame.extend_from_slice(&kiss::escape(&payload));
536        frame.push(kiss::FEND);
537
538        let mut decoder = RNodeDecoder::new();
539        let events = decoder.feed(&frame);
540        assert_eq!(events.len(), 1);
541        assert_eq!(
542            events[0],
543            RNodeEvent::DataFrame {
544                index: 0,
545                data: payload
546            }
547        );
548    }
549
550    #[test]
551    fn decoder_multi_sub_data() {
552        let payload = vec![0xAA, 0xBB];
553        // CMD_INT1_DATA = 0x10
554        let mut frame = vec![kiss::FEND, CMD_INT1_DATA];
555        frame.extend_from_slice(&kiss::escape(&payload));
556        frame.push(kiss::FEND);
557
558        let mut decoder = RNodeDecoder::new();
559        let events = decoder.feed(&frame);
560        assert_eq!(events.len(), 1);
561        assert_eq!(
562            events[0],
563            RNodeEvent::DataFrame {
564                index: 1,
565                data: payload
566            }
567        );
568    }
569
570    #[test]
571    fn rnode_select_command_format() {
572        // Select subinterface 1, then set frequency
573        let freq: u32 = 868_000_000;
574        let freq_bytes = [
575            (freq >> 24) as u8,
576            (freq >> 16) as u8,
577            (freq >> 8) as u8,
578            (freq & 0xFF) as u8,
579        ];
580        let cmd = rnode_select_command(1, CMD_FREQUENCY, &freq_bytes);
581
582        // Should start with [FEND][CMD_SEL_INT][0x01][FEND]
583        assert_eq!(cmd[0], kiss::FEND);
584        assert_eq!(cmd[1], CMD_SEL_INT);
585        assert_eq!(cmd[2], 0x01);
586        assert_eq!(cmd[3], kiss::FEND);
587
588        // Then [FEND][CMD_FREQUENCY][escaped bytes][FEND]
589        assert_eq!(cmd[4], kiss::FEND);
590        assert_eq!(cmd[5], CMD_FREQUENCY);
591    }
592
593    #[test]
594    fn rnode_data_frame_format() {
595        let data = vec![0x01, 0x02, 0x03];
596        let frame = rnode_data_frame(0, &data);
597        assert_eq!(frame[0], kiss::FEND);
598        assert_eq!(frame[1], CMD_INT0_DATA);
599        assert_eq!(*frame.last().unwrap(), kiss::FEND);
600
601        // Subinterface 1
602        let frame1 = rnode_data_frame(1, &data);
603        assert_eq!(frame1[1], CMD_INT1_DATA);
604
605        // Subinterface 2
606        let frame2 = rnode_data_frame(2, &data);
607        assert_eq!(frame2[1], CMD_INT2_DATA);
608    }
609
610    #[test]
611    fn rnode_multi_data_frame_selects_actual_virtual_port() {
612        let frame = rnode_multi_data_frame(7, &[1, 2, 3]);
613        assert_eq!(
614            frame,
615            vec![
616                kiss::FEND,
617                CMD_SEL_INT,
618                7,
619                kiss::FEND,
620                kiss::FEND,
621                kiss::CMD_DATA,
622                1,
623                2,
624                3,
625                kiss::FEND,
626            ]
627        );
628    }
629}