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