Skip to main content

donglora_protocol/
modulation.rs

1//! Per-modulation parameter structs for `SET_CONFIG` (`PROTOCOL.md §10`).
2//!
3//! `SET_CONFIG`'s payload is a one-byte modulation identifier followed
4//! by a modulation-specific struct. Each struct is fixed-layout
5//! little-endian; FSK/GFSK alone carries a variable-length sync word at
6//! its tail.
7//!
8//! The crate models the whole universe of DongLoRa Protocol modulations even on
9//! devices that physically can't drive them. Firmware ultimately returns
10//! `EMODULATION` for anything the chip doesn't support, but the wire
11//! codec stays uniform across all 1.0-compliant implementations.
12
13use crate::{MAX_SYNC_WORD_LEN, ModulationEncodeError, ModulationParseError};
14
15// ── Modulation identifier ───────────────────────────────────────────
16
17/// One-byte modulation selector (`PROTOCOL.md §10`). Prefixes every
18/// `SET_CONFIG` payload.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "defmt", derive(defmt::Format))]
21#[repr(u8)]
22pub enum ModulationId {
23    LoRa = 0x01,
24    FskGfsk = 0x02,
25    LrFhss = 0x03,
26    Flrc = 0x04,
27}
28
29impl ModulationId {
30    pub const fn as_u8(self) -> u8 {
31        self as u8
32    }
33
34    pub const fn from_u8(v: u8) -> Option<Self> {
35        Some(match v {
36            0x01 => Self::LoRa,
37            0x02 => Self::FskGfsk,
38            0x03 => Self::LrFhss,
39            0x04 => Self::Flrc,
40            _ => return None,
41        })
42    }
43}
44
45// ── LoRa enums ──────────────────────────────────────────────────────
46
47/// LoRa signal bandwidth enum (`PROTOCOL.md §10.1`). The small integer
48/// is what goes on the wire, NOT the kHz value.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[cfg_attr(feature = "defmt", derive(defmt::Format))]
51#[repr(u8)]
52pub enum LoRaBandwidth {
53    /// 7.81 kHz — sub-GHz chips only.
54    Khz7 = 0,
55    /// 10.42 kHz — sub-GHz chips only.
56    Khz10 = 1,
57    /// 15.63 kHz — sub-GHz chips only.
58    Khz15 = 2,
59    /// 20.83 kHz — sub-GHz chips only.
60    Khz20 = 3,
61    /// 31.25 kHz — sub-GHz chips only.
62    Khz31 = 4,
63    /// 41.67 kHz — sub-GHz chips only.
64    Khz41 = 5,
65    /// 62.5 kHz — sub-GHz chips only.
66    Khz62 = 6,
67    /// 125 kHz — all LoRa chips.
68    Khz125 = 7,
69    /// 250 kHz — all LoRa chips.
70    Khz250 = 8,
71    /// 500 kHz — all LoRa chips.
72    Khz500 = 9,
73    /// 200 kHz — SX128x (2.4 GHz) only.
74    Khz200 = 10,
75    /// 400 kHz — SX128x (2.4 GHz) only.
76    Khz400 = 11,
77    /// 800 kHz — SX128x (2.4 GHz) only.
78    Khz800 = 12,
79    /// 1600 kHz — SX128x (2.4 GHz) only.
80    Khz1600 = 13,
81}
82
83impl LoRaBandwidth {
84    pub const fn as_u8(self) -> u8 {
85        self as u8
86    }
87
88    pub const fn from_u8(v: u8) -> Option<Self> {
89        Some(match v {
90            0 => Self::Khz7,
91            1 => Self::Khz10,
92            2 => Self::Khz15,
93            3 => Self::Khz20,
94            4 => Self::Khz31,
95            5 => Self::Khz41,
96            6 => Self::Khz62,
97            7 => Self::Khz125,
98            8 => Self::Khz250,
99            9 => Self::Khz500,
100            10 => Self::Khz200,
101            11 => Self::Khz400,
102            12 => Self::Khz800,
103            13 => Self::Khz1600,
104            _ => return None,
105        })
106    }
107
108    /// Nominal bandwidth in Hz. Useful for the auto-LDRO rule
109    /// `(2^SF)/BW_Hz > 16 ms` and for display.
110    pub const fn as_hz(self) -> u32 {
111        match self {
112            Self::Khz7 => 7_810,
113            Self::Khz10 => 10_420,
114            Self::Khz15 => 15_630,
115            Self::Khz20 => 20_830,
116            Self::Khz31 => 31_250,
117            Self::Khz41 => 41_670,
118            Self::Khz62 => 62_500,
119            Self::Khz125 => 125_000,
120            Self::Khz250 => 250_000,
121            Self::Khz500 => 500_000,
122            Self::Khz200 => 200_000,
123            Self::Khz400 => 400_000,
124            Self::Khz800 => 800_000,
125            Self::Khz1600 => 1_600_000,
126        }
127    }
128}
129
130/// LoRa coding rate enum (`PROTOCOL.md §10.1`).
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[cfg_attr(feature = "defmt", derive(defmt::Format))]
133#[repr(u8)]
134pub enum LoRaCodingRate {
135    /// 4/5.
136    Cr4_5 = 0,
137    /// 4/6.
138    Cr4_6 = 1,
139    /// 4/7.
140    Cr4_7 = 2,
141    /// 4/8.
142    Cr4_8 = 3,
143}
144
145impl LoRaCodingRate {
146    pub const fn as_u8(self) -> u8 {
147        self as u8
148    }
149
150    pub const fn from_u8(v: u8) -> Option<Self> {
151        Some(match v {
152            0 => Self::Cr4_5,
153            1 => Self::Cr4_6,
154            2 => Self::Cr4_7,
155            3 => Self::Cr4_8,
156            _ => return None,
157        })
158    }
159}
160
161/// LoRa header mode (`PROTOCOL.md §10.1`).
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[cfg_attr(feature = "defmt", derive(defmt::Format))]
164#[repr(u8)]
165pub enum LoRaHeaderMode {
166    Explicit = 0,
167    Implicit = 1,
168}
169
170impl LoRaHeaderMode {
171    pub const fn as_u8(self) -> u8 {
172        self as u8
173    }
174
175    pub const fn from_u8(v: u8) -> Option<Self> {
176        Some(match v {
177            0 => Self::Explicit,
178            1 => Self::Implicit,
179            _ => return None,
180        })
181    }
182}
183
184/// LoRa configuration payload (15 bytes after `modulation_id`).
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "defmt", derive(defmt::Format))]
187pub struct LoRaConfig {
188    pub freq_hz: u32,
189    /// Spreading factor 5–12 (chip-dependent: SX127x is 6–12).
190    pub sf: u8,
191    pub bw: LoRaBandwidth,
192    pub cr: LoRaCodingRate,
193    pub preamble_len: u16,
194    pub sync_word: u16,
195    pub tx_power_dbm: i8,
196    pub header_mode: LoRaHeaderMode,
197    /// Payload CRC: `false` disabled, `true` enabled.
198    pub payload_crc: bool,
199    /// IQ inversion: `false` normal, `true` inverted.
200    pub iq_invert: bool,
201}
202
203impl LoRaConfig {
204    /// Fixed wire size of the per-modulation params.
205    pub const WIRE_SIZE: usize = 15;
206
207    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
208        if buf.len() < Self::WIRE_SIZE {
209            return Err(ModulationEncodeError::BufferTooSmall);
210        }
211        buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
212        buf[4] = self.sf;
213        buf[5] = self.bw.as_u8();
214        buf[6] = self.cr.as_u8();
215        buf[7..9].copy_from_slice(&self.preamble_len.to_le_bytes());
216        buf[9..11].copy_from_slice(&self.sync_word.to_le_bytes());
217        buf[11] = self.tx_power_dbm as u8;
218        buf[12] = self.header_mode.as_u8();
219        buf[13] = u8::from(self.payload_crc);
220        buf[14] = u8::from(self.iq_invert);
221        Ok(Self::WIRE_SIZE)
222    }
223
224    pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
225        if buf.len() != Self::WIRE_SIZE {
226            return Err(ModulationParseError::WrongLength {
227                expected: Self::WIRE_SIZE,
228                actual: buf.len(),
229            });
230        }
231        let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
232        let sf = buf[4];
233        let bw = LoRaBandwidth::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
234        let cr = LoRaCodingRate::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
235        let preamble_len = u16::from_le_bytes([buf[7], buf[8]]);
236        let sync_word = u16::from_le_bytes([buf[9], buf[10]]);
237        let tx_power_dbm = buf[11] as i8;
238        let header_mode =
239            LoRaHeaderMode::from_u8(buf[12]).ok_or(ModulationParseError::InvalidField)?;
240        let payload_crc = match buf[13] {
241            0 => false,
242            1 => true,
243            _ => return Err(ModulationParseError::InvalidField),
244        };
245        let iq_invert = match buf[14] {
246            0 => false,
247            1 => true,
248            _ => return Err(ModulationParseError::InvalidField),
249        };
250        Ok(Self {
251            freq_hz,
252            sf,
253            bw,
254            cr,
255            preamble_len,
256            sync_word,
257            tx_power_dbm,
258            header_mode,
259            payload_crc,
260            iq_invert,
261        })
262    }
263}
264
265// ── FSK / GFSK ──────────────────────────────────────────────────────
266
267/// FSK / GFSK configuration payload (`PROTOCOL.md §10.2`).
268///
269/// Wire size is `16 + sync_word_len` bytes. `sync_word_len` is 0–8 and
270/// indicates how many leading bytes of the `sync_word` array are
271/// transmitted (MSB-first, per FSK convention). Extra trailing bytes
272/// are ignored.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274#[cfg_attr(feature = "defmt", derive(defmt::Format))]
275pub struct FskConfig {
276    pub freq_hz: u32,
277    pub bitrate_bps: u32,
278    pub freq_dev_hz: u32,
279    /// Chip-specific RX-filter bandwidth index.
280    pub rx_bw: u8,
281    pub preamble_len: u16,
282    pub sync_word_len: u8,
283    pub sync_word: [u8; MAX_SYNC_WORD_LEN],
284}
285
286impl FskConfig {
287    /// Fixed part of the payload preceding the variable-length sync word.
288    pub const FIXED_WIRE_SIZE: usize = 16;
289
290    /// Total wire size for the given `sync_word_len`. `sync_word_len`
291    /// larger than 8 is rejected by `encode`.
292    pub const fn wire_size_for(sync_word_len: u8) -> usize {
293        Self::FIXED_WIRE_SIZE + sync_word_len as usize
294    }
295
296    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
297        if self.sync_word_len as usize > MAX_SYNC_WORD_LEN {
298            return Err(ModulationEncodeError::SyncWordTooLong);
299        }
300        let total = Self::wire_size_for(self.sync_word_len);
301        if buf.len() < total {
302            return Err(ModulationEncodeError::BufferTooSmall);
303        }
304        buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
305        buf[4..8].copy_from_slice(&self.bitrate_bps.to_le_bytes());
306        buf[8..12].copy_from_slice(&self.freq_dev_hz.to_le_bytes());
307        buf[12] = self.rx_bw;
308        buf[13..15].copy_from_slice(&self.preamble_len.to_le_bytes());
309        buf[15] = self.sync_word_len;
310        let n = self.sync_word_len as usize;
311        buf[16..16 + n].copy_from_slice(&self.sync_word[..n]);
312        Ok(total)
313    }
314
315    pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
316        if buf.len() < Self::FIXED_WIRE_SIZE {
317            return Err(ModulationParseError::TooShort);
318        }
319        let sync_word_len = buf[15];
320        if sync_word_len as usize > MAX_SYNC_WORD_LEN {
321            return Err(ModulationParseError::InvalidField);
322        }
323        let expected = Self::wire_size_for(sync_word_len);
324        if buf.len() != expected {
325            return Err(ModulationParseError::WrongLength {
326                expected,
327                actual: buf.len(),
328            });
329        }
330        let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
331        let bitrate_bps = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
332        let freq_dev_hz = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
333        let rx_bw = buf[12];
334        let preamble_len = u16::from_le_bytes([buf[13], buf[14]]);
335        let mut sync_word = [0u8; MAX_SYNC_WORD_LEN];
336        let n = sync_word_len as usize;
337        sync_word[..n].copy_from_slice(&buf[16..16 + n]);
338        Ok(Self {
339            freq_hz,
340            bitrate_bps,
341            freq_dev_hz,
342            rx_bw,
343            preamble_len,
344            sync_word_len,
345            sync_word,
346        })
347    }
348}
349
350// ── LR-FHSS ─────────────────────────────────────────────────────────
351
352/// LR-FHSS occupied-bandwidth enum (`PROTOCOL.md §10.3`).
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354#[cfg_attr(feature = "defmt", derive(defmt::Format))]
355#[repr(u8)]
356pub enum LrFhssBandwidth {
357    /// 39.06 kHz.
358    Khz39 = 0,
359    /// 85.94 kHz.
360    Khz85 = 1,
361    /// 136.72 kHz.
362    Khz136 = 2,
363    /// 183.59 kHz.
364    Khz183 = 3,
365    /// 335.94 kHz.
366    Khz335 = 4,
367    /// 386.72 kHz.
368    Khz386 = 5,
369    /// 722.66 kHz.
370    Khz722 = 6,
371    /// 1523.44 kHz.
372    Khz1523 = 7,
373}
374
375impl LrFhssBandwidth {
376    pub const fn as_u8(self) -> u8 {
377        self as u8
378    }
379    pub const fn from_u8(v: u8) -> Option<Self> {
380        Some(match v {
381            0 => Self::Khz39,
382            1 => Self::Khz85,
383            2 => Self::Khz136,
384            3 => Self::Khz183,
385            4 => Self::Khz335,
386            5 => Self::Khz386,
387            6 => Self::Khz722,
388            7 => Self::Khz1523,
389            _ => return None,
390        })
391    }
392}
393
394/// LR-FHSS coding rate enum (`PROTOCOL.md §10.3`).
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396#[cfg_attr(feature = "defmt", derive(defmt::Format))]
397#[repr(u8)]
398pub enum LrFhssCodingRate {
399    /// 5/6.
400    Cr5_6 = 0,
401    /// 2/3.
402    Cr2_3 = 1,
403    /// 1/2.
404    Cr1_2 = 2,
405    /// 1/3.
406    Cr1_3 = 3,
407}
408
409impl LrFhssCodingRate {
410    pub const fn as_u8(self) -> u8 {
411        self as u8
412    }
413    pub const fn from_u8(v: u8) -> Option<Self> {
414        Some(match v {
415            0 => Self::Cr5_6,
416            1 => Self::Cr2_3,
417            2 => Self::Cr1_2,
418            3 => Self::Cr1_3,
419            _ => return None,
420        })
421    }
422}
423
424/// LR-FHSS grid selection (`PROTOCOL.md §10.3`).
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426#[cfg_attr(feature = "defmt", derive(defmt::Format))]
427#[repr(u8)]
428pub enum LrFhssGrid {
429    /// 25.39 kHz grid.
430    Khz25 = 0,
431    /// 3.9 kHz grid.
432    Khz3_9 = 1,
433}
434
435impl LrFhssGrid {
436    pub const fn as_u8(self) -> u8 {
437        self as u8
438    }
439    pub const fn from_u8(v: u8) -> Option<Self> {
440        Some(match v {
441            0 => Self::Khz25,
442            1 => Self::Khz3_9,
443            _ => return None,
444        })
445    }
446}
447
448/// LR-FHSS configuration payload (10 bytes after `modulation_id`).
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450#[cfg_attr(feature = "defmt", derive(defmt::Format))]
451pub struct LrFhssConfig {
452    pub freq_hz: u32,
453    pub bw: LrFhssBandwidth,
454    pub cr: LrFhssCodingRate,
455    pub grid: LrFhssGrid,
456    pub hopping: bool,
457    pub tx_power_dbm: i8,
458}
459
460impl LrFhssConfig {
461    pub const WIRE_SIZE: usize = 10;
462
463    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
464        if buf.len() < Self::WIRE_SIZE {
465            return Err(ModulationEncodeError::BufferTooSmall);
466        }
467        buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
468        buf[4] = self.bw.as_u8();
469        buf[5] = self.cr.as_u8();
470        buf[6] = self.grid.as_u8();
471        buf[7] = u8::from(self.hopping);
472        buf[8] = self.tx_power_dbm as u8;
473        buf[9] = 0; // reserved
474        Ok(Self::WIRE_SIZE)
475    }
476
477    pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
478        if buf.len() != Self::WIRE_SIZE {
479            return Err(ModulationParseError::WrongLength {
480                expected: Self::WIRE_SIZE,
481                actual: buf.len(),
482            });
483        }
484        let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
485        let bw = LrFhssBandwidth::from_u8(buf[4]).ok_or(ModulationParseError::InvalidField)?;
486        let cr = LrFhssCodingRate::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
487        let grid = LrFhssGrid::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
488        let hopping = match buf[7] {
489            0 => false,
490            1 => true,
491            _ => return Err(ModulationParseError::InvalidField),
492        };
493        let tx_power_dbm = buf[8] as i8;
494        // Reserved byte MUST be zero per §10.3.
495        if buf[9] != 0 {
496            return Err(ModulationParseError::InvalidField);
497        }
498        Ok(Self {
499            freq_hz,
500            bw,
501            cr,
502            grid,
503            hopping,
504            tx_power_dbm,
505        })
506    }
507}
508
509// ── FLRC ────────────────────────────────────────────────────────────
510
511/// FLRC bitrate enum (`PROTOCOL.md §10.4`).
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513#[cfg_attr(feature = "defmt", derive(defmt::Format))]
514#[repr(u8)]
515pub enum FlrcBitrate {
516    /// 2600 kbps.
517    Kbps2600 = 0,
518    /// 2080 kbps.
519    Kbps2080 = 1,
520    /// 1300 kbps.
521    Kbps1300 = 2,
522    /// 1040 kbps.
523    Kbps1040 = 3,
524    /// 650 kbps.
525    Kbps650 = 4,
526    /// 520 kbps.
527    Kbps520 = 5,
528    /// 325 kbps.
529    Kbps325 = 6,
530    /// 260 kbps.
531    Kbps260 = 7,
532}
533
534impl FlrcBitrate {
535    pub const fn as_u8(self) -> u8 {
536        self as u8
537    }
538    pub const fn from_u8(v: u8) -> Option<Self> {
539        Some(match v {
540            0 => Self::Kbps2600,
541            1 => Self::Kbps2080,
542            2 => Self::Kbps1300,
543            3 => Self::Kbps1040,
544            4 => Self::Kbps650,
545            5 => Self::Kbps520,
546            6 => Self::Kbps325,
547            7 => Self::Kbps260,
548            _ => return None,
549        })
550    }
551}
552
553/// FLRC coding rate enum (`PROTOCOL.md §10.4`).
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555#[cfg_attr(feature = "defmt", derive(defmt::Format))]
556#[repr(u8)]
557pub enum FlrcCodingRate {
558    /// 1/2.
559    Cr1_2 = 0,
560    /// 3/4.
561    Cr3_4 = 1,
562    /// 1/1 (no coding).
563    Cr1_1 = 2,
564}
565
566impl FlrcCodingRate {
567    pub const fn as_u8(self) -> u8 {
568        self as u8
569    }
570    pub const fn from_u8(v: u8) -> Option<Self> {
571        Some(match v {
572            0 => Self::Cr1_2,
573            1 => Self::Cr3_4,
574            2 => Self::Cr1_1,
575            _ => return None,
576        })
577    }
578}
579
580/// FLRC Gaussian BT-product enum (`PROTOCOL.md §10.4`).
581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
582#[cfg_attr(feature = "defmt", derive(defmt::Format))]
583#[repr(u8)]
584pub enum FlrcBt {
585    /// Gaussian filtering off.
586    Off = 0,
587    /// BT = 0.5.
588    Bt0_5 = 1,
589    /// BT = 1.0.
590    Bt1_0 = 2,
591}
592
593impl FlrcBt {
594    pub const fn as_u8(self) -> u8 {
595        self as u8
596    }
597    pub const fn from_u8(v: u8) -> Option<Self> {
598        Some(match v {
599            0 => Self::Off,
600            1 => Self::Bt0_5,
601            2 => Self::Bt1_0,
602            _ => return None,
603        })
604    }
605}
606
607/// FLRC preamble length enum (`PROTOCOL.md §10.4`).
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609#[cfg_attr(feature = "defmt", derive(defmt::Format))]
610#[repr(u8)]
611pub enum FlrcPreambleLen {
612    Bits8 = 0,
613    Bits12 = 1,
614    Bits16 = 2,
615    Bits20 = 3,
616    Bits24 = 4,
617    Bits28 = 5,
618    Bits32 = 6,
619}
620
621impl FlrcPreambleLen {
622    pub const fn as_u8(self) -> u8 {
623        self as u8
624    }
625    pub const fn from_u8(v: u8) -> Option<Self> {
626        Some(match v {
627            0 => Self::Bits8,
628            1 => Self::Bits12,
629            2 => Self::Bits16,
630            3 => Self::Bits20,
631            4 => Self::Bits24,
632            5 => Self::Bits28,
633            6 => Self::Bits32,
634            _ => return None,
635        })
636    }
637}
638
639/// FLRC configuration payload (13 bytes after `modulation_id`).
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641#[cfg_attr(feature = "defmt", derive(defmt::Format))]
642pub struct FlrcConfig {
643    pub freq_hz: u32,
644    pub bitrate: FlrcBitrate,
645    pub cr: FlrcCodingRate,
646    pub bt: FlrcBt,
647    pub preamble_len: FlrcPreambleLen,
648    /// 32-bit sync word, transmitted MSB-first on air.
649    pub sync_word: u32,
650    pub tx_power_dbm: i8,
651}
652
653impl FlrcConfig {
654    pub const WIRE_SIZE: usize = 13;
655
656    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
657        if buf.len() < Self::WIRE_SIZE {
658            return Err(ModulationEncodeError::BufferTooSmall);
659        }
660        buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
661        buf[4] = self.bitrate.as_u8();
662        buf[5] = self.cr.as_u8();
663        buf[6] = self.bt.as_u8();
664        buf[7] = self.preamble_len.as_u8();
665        buf[8..12].copy_from_slice(&self.sync_word.to_le_bytes());
666        buf[12] = self.tx_power_dbm as u8;
667        Ok(Self::WIRE_SIZE)
668    }
669
670    pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
671        if buf.len() != Self::WIRE_SIZE {
672            return Err(ModulationParseError::WrongLength {
673                expected: Self::WIRE_SIZE,
674                actual: buf.len(),
675            });
676        }
677        let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
678        let bitrate = FlrcBitrate::from_u8(buf[4]).ok_or(ModulationParseError::InvalidField)?;
679        let cr = FlrcCodingRate::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
680        let bt = FlrcBt::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
681        let preamble_len =
682            FlrcPreambleLen::from_u8(buf[7]).ok_or(ModulationParseError::InvalidField)?;
683        let sync_word = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
684        let tx_power_dbm = buf[12] as i8;
685        Ok(Self {
686            freq_hz,
687            bitrate,
688            cr,
689            bt,
690            preamble_len,
691            sync_word,
692            tx_power_dbm,
693        })
694    }
695}
696
697// ── Modulation sum type ─────────────────────────────────────────────
698
699/// Any of the four modulation configs, tagged by `ModulationId`.
700///
701/// This is the type carried inside `Command::SetConfig`. Encoding writes
702/// the `modulation_id` byte first, then delegates to the per-modulation
703/// `encode`. Decoding dispatches on the leading byte and calls the
704/// matching `decode`.
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
706#[cfg_attr(feature = "defmt", derive(defmt::Format))]
707pub enum Modulation {
708    LoRa(LoRaConfig),
709    FskGfsk(FskConfig),
710    LrFhss(LrFhssConfig),
711    Flrc(FlrcConfig),
712}
713
714impl Modulation {
715    pub const fn id(&self) -> ModulationId {
716        match self {
717            Self::LoRa(_) => ModulationId::LoRa,
718            Self::FskGfsk(_) => ModulationId::FskGfsk,
719            Self::LrFhss(_) => ModulationId::LrFhss,
720            Self::Flrc(_) => ModulationId::Flrc,
721        }
722    }
723
724    /// Encode as `[modulation_id][params]` into `buf`. Returns the total
725    /// number of bytes written.
726    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
727        if buf.is_empty() {
728            return Err(ModulationEncodeError::BufferTooSmall);
729        }
730        buf[0] = self.id().as_u8();
731        let n = match self {
732            Self::LoRa(c) => c.encode(&mut buf[1..])?,
733            Self::FskGfsk(c) => c.encode(&mut buf[1..])?,
734            Self::LrFhss(c) => c.encode(&mut buf[1..])?,
735            Self::Flrc(c) => c.encode(&mut buf[1..])?,
736        };
737        Ok(1 + n)
738    }
739
740    /// Decode from `[modulation_id][params]`. The full slice is the
741    /// `SET_CONFIG` payload; length errors are propagated as
742    /// `ModulationParseError::WrongLength`.
743    pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
744        if buf.is_empty() {
745            return Err(ModulationParseError::TooShort);
746        }
747        let id = ModulationId::from_u8(buf[0]).ok_or(ModulationParseError::UnknownModulation)?;
748        let params = &buf[1..];
749        Ok(match id {
750            ModulationId::LoRa => Self::LoRa(LoRaConfig::decode(params)?),
751            ModulationId::FskGfsk => Self::FskGfsk(FskConfig::decode(params)?),
752            ModulationId::LrFhss => Self::LrFhss(LrFhssConfig::decode(params)?),
753            ModulationId::Flrc => Self::Flrc(FlrcConfig::decode(params)?),
754        })
755    }
756}
757
758#[cfg(test)]
759#[allow(clippy::panic, clippy::unwrap_used)]
760mod tests {
761    use super::*;
762
763    fn sample_lora() -> LoRaConfig {
764        LoRaConfig {
765            freq_hz: 868_100_000,
766            sf: 7,
767            bw: LoRaBandwidth::Khz125,
768            cr: LoRaCodingRate::Cr4_5,
769            preamble_len: 8,
770            sync_word: 0x1424,
771            tx_power_dbm: 14,
772            header_mode: LoRaHeaderMode::Explicit,
773            payload_crc: true,
774            iq_invert: false,
775        }
776    }
777
778    #[test]
779    fn lora_wire_size() {
780        assert_eq!(LoRaConfig::WIRE_SIZE, 15);
781    }
782
783    #[test]
784    fn lora_roundtrip() {
785        let cfg = sample_lora();
786        let mut buf = [0u8; 32];
787        let n = cfg.encode(&mut buf).unwrap();
788        assert_eq!(n, 15);
789        let decoded = LoRaConfig::decode(&buf[..n]).unwrap();
790        assert_eq!(decoded, cfg);
791    }
792
793    #[test]
794    fn lora_appendix_c23_bytes() {
795        // PROTOCOL.md §C.2.3 — EU868 SF7 BW125 CR4/5, preamble=8, sync=0x1424,
796        // power=14 dBm, explicit, crc on, iq normal.
797        let cfg = sample_lora();
798        let mut buf = [0u8; 15];
799        let n = cfg.encode(&mut buf).unwrap();
800        assert_eq!(n, 15);
801        let expected: [u8; 15] = [
802            0xA0, 0x27, 0xBE, 0x33, // freq_hz = 868_100_000 LE
803            0x07, // sf
804            0x07, // bw = Khz125
805            0x00, // cr = 4/5
806            0x08, 0x00, // preamble_len = 8
807            0x24, 0x14, // sync_word = 0x1424
808            0x0E, // tx_power_dbm = 14
809            0x00, // header explicit
810            0x01, // payload_crc on
811            0x00, // iq normal
812        ];
813        assert_eq!(buf, expected);
814    }
815
816    #[test]
817    fn lora_rejects_wrong_length() {
818        assert!(matches!(
819            LoRaConfig::decode(&[0u8; 14]),
820            Err(ModulationParseError::WrongLength { .. })
821        ));
822        assert!(matches!(
823            LoRaConfig::decode(&[0u8; 16]),
824            Err(ModulationParseError::WrongLength { .. })
825        ));
826    }
827
828    #[test]
829    fn lora_rejects_bad_enum_values() {
830        let mut buf = [0u8; 15];
831        sample_lora().encode(&mut buf).unwrap();
832
833        let mut bad = buf;
834        bad[5] = 14; // BW 14 is undefined
835        assert!(LoRaConfig::decode(&bad).is_err());
836
837        let mut bad = buf;
838        bad[6] = 4; // CR 4 is undefined
839        assert!(LoRaConfig::decode(&bad).is_err());
840
841        let mut bad = buf;
842        bad[12] = 2; // header_mode 2 undefined
843        assert!(LoRaConfig::decode(&bad).is_err());
844
845        let mut bad = buf;
846        bad[13] = 2; // payload_crc must be 0 or 1
847        assert!(LoRaConfig::decode(&bad).is_err());
848
849        let mut bad = buf;
850        bad[14] = 2; // iq_invert must be 0 or 1
851        assert!(LoRaConfig::decode(&bad).is_err());
852    }
853
854    #[test]
855    fn fsk_roundtrip_empty_sync() {
856        let cfg = FskConfig {
857            freq_hz: 868_000_000,
858            bitrate_bps: 9_600,
859            freq_dev_hz: 5_000,
860            rx_bw: 0x0B,
861            preamble_len: 16,
862            sync_word_len: 0,
863            sync_word: [0u8; MAX_SYNC_WORD_LEN],
864        };
865        let mut buf = [0u8; 32];
866        let n = cfg.encode(&mut buf).unwrap();
867        assert_eq!(n, 16);
868        let decoded = FskConfig::decode(&buf[..n]).unwrap();
869        assert_eq!(decoded, cfg);
870    }
871
872    #[test]
873    fn fsk_roundtrip_with_sync() {
874        let mut sync = [0u8; MAX_SYNC_WORD_LEN];
875        sync[..4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
876        let cfg = FskConfig {
877            freq_hz: 868_000_000,
878            bitrate_bps: 50_000,
879            freq_dev_hz: 25_000,
880            rx_bw: 0x1A,
881            preamble_len: 32,
882            sync_word_len: 4,
883            sync_word: sync,
884        };
885        let mut buf = [0u8; 32];
886        let n = cfg.encode(&mut buf).unwrap();
887        assert_eq!(n, 20);
888        let decoded = FskConfig::decode(&buf[..n]).unwrap();
889        assert_eq!(decoded, cfg);
890    }
891
892    #[test]
893    fn fsk_rejects_oversized_sync() {
894        let mut cfg = FskConfig {
895            freq_hz: 0,
896            bitrate_bps: 0,
897            freq_dev_hz: 0,
898            rx_bw: 0,
899            preamble_len: 0,
900            sync_word_len: 9,
901            sync_word: [0u8; MAX_SYNC_WORD_LEN],
902        };
903        let mut buf = [0u8; 32];
904        assert!(cfg.encode(&mut buf).is_err());
905
906        // Decoder rejects the same: sync_word_len > 8.
907        cfg.sync_word_len = 0;
908        cfg.encode(&mut buf).unwrap();
909        let mut bad = [0u8; 16];
910        bad.copy_from_slice(&buf[..16]);
911        bad[15] = 9;
912        assert!(FskConfig::decode(&bad).is_err());
913    }
914
915    #[test]
916    fn lr_fhss_roundtrip() {
917        let cfg = LrFhssConfig {
918            freq_hz: 915_000_000,
919            bw: LrFhssBandwidth::Khz136,
920            cr: LrFhssCodingRate::Cr2_3,
921            grid: LrFhssGrid::Khz25,
922            hopping: true,
923            tx_power_dbm: 14,
924        };
925        let mut buf = [0u8; 10];
926        let n = cfg.encode(&mut buf).unwrap();
927        assert_eq!(n, 10);
928        let decoded = LrFhssConfig::decode(&buf[..n]).unwrap();
929        assert_eq!(decoded, cfg);
930        assert_eq!(buf[9], 0, "reserved byte must serialize as 0");
931    }
932
933    #[test]
934    fn lr_fhss_rejects_nonzero_reserved() {
935        let cfg = LrFhssConfig {
936            freq_hz: 0,
937            bw: LrFhssBandwidth::Khz39,
938            cr: LrFhssCodingRate::Cr1_3,
939            grid: LrFhssGrid::Khz25,
940            hopping: false,
941            tx_power_dbm: 0,
942        };
943        let mut buf = [0u8; 10];
944        cfg.encode(&mut buf).unwrap();
945        buf[9] = 1;
946        assert!(LrFhssConfig::decode(&buf).is_err());
947    }
948
949    #[test]
950    fn flrc_roundtrip() {
951        let cfg = FlrcConfig {
952            freq_hz: 2_400_000_000,
953            bitrate: FlrcBitrate::Kbps1300,
954            cr: FlrcCodingRate::Cr3_4,
955            bt: FlrcBt::Bt0_5,
956            preamble_len: FlrcPreambleLen::Bits24,
957            sync_word: 0x1234_5678,
958            tx_power_dbm: 10,
959        };
960        let mut buf = [0u8; 13];
961        let n = cfg.encode(&mut buf).unwrap();
962        assert_eq!(n, 13);
963        let decoded = FlrcConfig::decode(&buf[..n]).unwrap();
964        assert_eq!(decoded, cfg);
965    }
966
967    #[test]
968    fn modulation_sum_roundtrip() {
969        let m = Modulation::LoRa(sample_lora());
970        let mut buf = [0u8; 64];
971        let n = m.encode(&mut buf).unwrap();
972        // id byte + LoRa wire size
973        assert_eq!(n, 1 + LoRaConfig::WIRE_SIZE);
974        assert_eq!(buf[0], ModulationId::LoRa.as_u8());
975        let decoded = Modulation::decode(&buf[..n]).unwrap();
976        assert_eq!(decoded, m);
977    }
978
979    #[test]
980    fn modulation_id_rejects_unknown() {
981        assert!(matches!(
982            Modulation::decode(&[0x05, 0, 0, 0]),
983            Err(ModulationParseError::UnknownModulation)
984        ));
985        assert!(matches!(
986            Modulation::decode(&[]),
987            Err(ModulationParseError::TooShort)
988        ));
989    }
990
991    #[test]
992    fn lora_bandwidth_hz_table() {
993        // Cross-check §10.1 table values.
994        assert_eq!(LoRaBandwidth::Khz125.as_hz(), 125_000);
995        assert_eq!(LoRaBandwidth::Khz500.as_hz(), 500_000);
996        assert_eq!(LoRaBandwidth::Khz7.as_hz(), 7_810);
997        assert_eq!(LoRaBandwidth::Khz1600.as_hz(), 1_600_000);
998    }
999}