1use crate::{MAX_SYNC_WORD_LEN, ModulationEncodeError, ModulationParseError};
14
15#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[cfg_attr(feature = "defmt", derive(defmt::Format))]
51#[repr(u8)]
52pub enum LoRaBandwidth {
53 Khz7 = 0,
55 Khz10 = 1,
57 Khz15 = 2,
59 Khz20 = 3,
61 Khz31 = 4,
63 Khz41 = 5,
65 Khz62 = 6,
67 Khz125 = 7,
69 Khz250 = 8,
71 Khz500 = 9,
73 Khz200 = 10,
75 Khz400 = 11,
77 Khz800 = 12,
79 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[cfg_attr(feature = "defmt", derive(defmt::Format))]
133#[repr(u8)]
134pub enum LoRaCodingRate {
135 Cr4_5 = 0,
137 Cr4_6 = 1,
139 Cr4_7 = 2,
141 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "defmt", derive(defmt::Format))]
187pub struct LoRaConfig {
188 pub freq_hz: u32,
189 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 pub payload_crc: bool,
199 pub iq_invert: bool,
201}
202
203impl LoRaConfig {
204 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#[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 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 pub const FIXED_WIRE_SIZE: usize = 16;
289
290 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354#[cfg_attr(feature = "defmt", derive(defmt::Format))]
355#[repr(u8)]
356pub enum LrFhssBandwidth {
357 Khz39 = 0,
359 Khz85 = 1,
361 Khz136 = 2,
363 Khz183 = 3,
365 Khz335 = 4,
367 Khz386 = 5,
369 Khz722 = 6,
371 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396#[cfg_attr(feature = "defmt", derive(defmt::Format))]
397#[repr(u8)]
398pub enum LrFhssCodingRate {
399 Cr5_6 = 0,
401 Cr2_3 = 1,
403 Cr1_2 = 2,
405 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426#[cfg_attr(feature = "defmt", derive(defmt::Format))]
427#[repr(u8)]
428pub enum LrFhssGrid {
429 Khz25 = 0,
431 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#[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; 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513#[cfg_attr(feature = "defmt", derive(defmt::Format))]
514#[repr(u8)]
515pub enum FlrcBitrate {
516 Kbps2600 = 0,
518 Kbps2080 = 1,
520 Kbps1300 = 2,
522 Kbps1040 = 3,
524 Kbps650 = 4,
526 Kbps520 = 5,
528 Kbps325 = 6,
530 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555#[cfg_attr(feature = "defmt", derive(defmt::Format))]
556#[repr(u8)]
557pub enum FlrcCodingRate {
558 Cr1_2 = 0,
560 Cr3_4 = 1,
562 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
582#[cfg_attr(feature = "defmt", derive(defmt::Format))]
583#[repr(u8)]
584pub enum FlrcBt {
585 Off = 0,
587 Bt0_5 = 1,
589 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#[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#[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 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#[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 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 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 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, 0x07, 0x07, 0x00, 0x08, 0x00, 0x24, 0x14, 0x0E, 0x00, 0x01, 0x00, ];
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; assert!(LoRaConfig::decode(&bad).is_err());
836
837 let mut bad = buf;
838 bad[6] = 4; assert!(LoRaConfig::decode(&bad).is_err());
840
841 let mut bad = buf;
842 bad[12] = 2; assert!(LoRaConfig::decode(&bad).is_err());
844
845 let mut bad = buf;
846 bad[13] = 2; assert!(LoRaConfig::decode(&bad).is_err());
848
849 let mut bad = buf;
850 bad[14] = 2; 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 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 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 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}