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