use crate::{MAX_SYNC_WORD_LEN, ModulationEncodeError, ModulationParseError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ModulationId {
LoRa = 0x01,
FskGfsk = 0x02,
LrFhss = 0x03,
Flrc = 0x04,
}
impl ModulationId {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0x01 => Self::LoRa,
0x02 => Self::FskGfsk,
0x03 => Self::LrFhss,
0x04 => Self::Flrc,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LoRaBandwidth {
Khz7 = 0,
Khz10 = 1,
Khz15 = 2,
Khz20 = 3,
Khz31 = 4,
Khz41 = 5,
Khz62 = 6,
Khz125 = 7,
Khz250 = 8,
Khz500 = 9,
Khz200 = 10,
Khz400 = 11,
Khz800 = 12,
Khz1600 = 13,
}
impl LoRaBandwidth {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Khz7,
1 => Self::Khz10,
2 => Self::Khz15,
3 => Self::Khz20,
4 => Self::Khz31,
5 => Self::Khz41,
6 => Self::Khz62,
7 => Self::Khz125,
8 => Self::Khz250,
9 => Self::Khz500,
10 => Self::Khz200,
11 => Self::Khz400,
12 => Self::Khz800,
13 => Self::Khz1600,
_ => return None,
})
}
pub const fn as_hz(self) -> u32 {
match self {
Self::Khz7 => 7_810,
Self::Khz10 => 10_420,
Self::Khz15 => 15_630,
Self::Khz20 => 20_830,
Self::Khz31 => 31_250,
Self::Khz41 => 41_670,
Self::Khz62 => 62_500,
Self::Khz125 => 125_000,
Self::Khz250 => 250_000,
Self::Khz500 => 500_000,
Self::Khz200 => 200_000,
Self::Khz400 => 400_000,
Self::Khz800 => 800_000,
Self::Khz1600 => 1_600_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LoRaCodingRate {
Cr4_5 = 0,
Cr4_6 = 1,
Cr4_7 = 2,
Cr4_8 = 3,
}
impl LoRaCodingRate {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Cr4_5,
1 => Self::Cr4_6,
2 => Self::Cr4_7,
3 => Self::Cr4_8,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LoRaHeaderMode {
Explicit = 0,
Implicit = 1,
}
impl LoRaHeaderMode {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Explicit,
1 => Self::Implicit,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct LoRaConfig {
pub freq_hz: u32,
pub sf: u8,
pub bw: LoRaBandwidth,
pub cr: LoRaCodingRate,
pub preamble_len: u16,
pub sync_word: u16,
pub tx_power_dbm: i8,
pub header_mode: LoRaHeaderMode,
pub payload_crc: bool,
pub iq_invert: bool,
}
impl LoRaConfig {
pub const WIRE_SIZE: usize = 15;
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
if buf.len() < Self::WIRE_SIZE {
return Err(ModulationEncodeError::BufferTooSmall);
}
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
buf[4] = self.sf;
buf[5] = self.bw.as_u8();
buf[6] = self.cr.as_u8();
buf[7..9].copy_from_slice(&self.preamble_len.to_le_bytes());
buf[9..11].copy_from_slice(&self.sync_word.to_le_bytes());
buf[11] = self.tx_power_dbm as u8;
buf[12] = self.header_mode.as_u8();
buf[13] = u8::from(self.payload_crc);
buf[14] = u8::from(self.iq_invert);
Ok(Self::WIRE_SIZE)
}
pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
if buf.len() != Self::WIRE_SIZE {
return Err(ModulationParseError::WrongLength {
expected: Self::WIRE_SIZE,
actual: buf.len(),
});
}
let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let sf = buf[4];
let bw = LoRaBandwidth::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
let cr = LoRaCodingRate::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
let preamble_len = u16::from_le_bytes([buf[7], buf[8]]);
let sync_word = u16::from_le_bytes([buf[9], buf[10]]);
let tx_power_dbm = buf[11] as i8;
let header_mode =
LoRaHeaderMode::from_u8(buf[12]).ok_or(ModulationParseError::InvalidField)?;
let payload_crc = match buf[13] {
0 => false,
1 => true,
_ => return Err(ModulationParseError::InvalidField),
};
let iq_invert = match buf[14] {
0 => false,
1 => true,
_ => return Err(ModulationParseError::InvalidField),
};
Ok(Self {
freq_hz,
sf,
bw,
cr,
preamble_len,
sync_word,
tx_power_dbm,
header_mode,
payload_crc,
iq_invert,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FskConfig {
pub freq_hz: u32,
pub bitrate_bps: u32,
pub freq_dev_hz: u32,
pub rx_bw: u8,
pub preamble_len: u16,
pub sync_word_len: u8,
pub sync_word: [u8; MAX_SYNC_WORD_LEN],
}
impl FskConfig {
pub const FIXED_WIRE_SIZE: usize = 16;
pub const fn wire_size_for(sync_word_len: u8) -> usize {
Self::FIXED_WIRE_SIZE + sync_word_len as usize
}
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
if self.sync_word_len as usize > MAX_SYNC_WORD_LEN {
return Err(ModulationEncodeError::SyncWordTooLong);
}
let total = Self::wire_size_for(self.sync_word_len);
if buf.len() < total {
return Err(ModulationEncodeError::BufferTooSmall);
}
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
buf[4..8].copy_from_slice(&self.bitrate_bps.to_le_bytes());
buf[8..12].copy_from_slice(&self.freq_dev_hz.to_le_bytes());
buf[12] = self.rx_bw;
buf[13..15].copy_from_slice(&self.preamble_len.to_le_bytes());
buf[15] = self.sync_word_len;
let n = self.sync_word_len as usize;
buf[16..16 + n].copy_from_slice(&self.sync_word[..n]);
Ok(total)
}
pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
if buf.len() < Self::FIXED_WIRE_SIZE {
return Err(ModulationParseError::TooShort);
}
let sync_word_len = buf[15];
if sync_word_len as usize > MAX_SYNC_WORD_LEN {
return Err(ModulationParseError::InvalidField);
}
let expected = Self::wire_size_for(sync_word_len);
if buf.len() != expected {
return Err(ModulationParseError::WrongLength {
expected,
actual: buf.len(),
});
}
let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let bitrate_bps = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
let freq_dev_hz = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let rx_bw = buf[12];
let preamble_len = u16::from_le_bytes([buf[13], buf[14]]);
let mut sync_word = [0u8; MAX_SYNC_WORD_LEN];
let n = sync_word_len as usize;
sync_word[..n].copy_from_slice(&buf[16..16 + n]);
Ok(Self {
freq_hz,
bitrate_bps,
freq_dev_hz,
rx_bw,
preamble_len,
sync_word_len,
sync_word,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LrFhssBandwidth {
Khz39 = 0,
Khz85 = 1,
Khz136 = 2,
Khz183 = 3,
Khz335 = 4,
Khz386 = 5,
Khz722 = 6,
Khz1523 = 7,
}
impl LrFhssBandwidth {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Khz39,
1 => Self::Khz85,
2 => Self::Khz136,
3 => Self::Khz183,
4 => Self::Khz335,
5 => Self::Khz386,
6 => Self::Khz722,
7 => Self::Khz1523,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LrFhssCodingRate {
Cr5_6 = 0,
Cr2_3 = 1,
Cr1_2 = 2,
Cr1_3 = 3,
}
impl LrFhssCodingRate {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Cr5_6,
1 => Self::Cr2_3,
2 => Self::Cr1_2,
3 => Self::Cr1_3,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LrFhssGrid {
Khz25 = 0,
Khz3_9 = 1,
}
impl LrFhssGrid {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Khz25,
1 => Self::Khz3_9,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct LrFhssConfig {
pub freq_hz: u32,
pub bw: LrFhssBandwidth,
pub cr: LrFhssCodingRate,
pub grid: LrFhssGrid,
pub hopping: bool,
pub tx_power_dbm: i8,
}
impl LrFhssConfig {
pub const WIRE_SIZE: usize = 10;
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
if buf.len() < Self::WIRE_SIZE {
return Err(ModulationEncodeError::BufferTooSmall);
}
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
buf[4] = self.bw.as_u8();
buf[5] = self.cr.as_u8();
buf[6] = self.grid.as_u8();
buf[7] = u8::from(self.hopping);
buf[8] = self.tx_power_dbm as u8;
buf[9] = 0; Ok(Self::WIRE_SIZE)
}
pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
if buf.len() != Self::WIRE_SIZE {
return Err(ModulationParseError::WrongLength {
expected: Self::WIRE_SIZE,
actual: buf.len(),
});
}
let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let bw = LrFhssBandwidth::from_u8(buf[4]).ok_or(ModulationParseError::InvalidField)?;
let cr = LrFhssCodingRate::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
let grid = LrFhssGrid::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
let hopping = match buf[7] {
0 => false,
1 => true,
_ => return Err(ModulationParseError::InvalidField),
};
let tx_power_dbm = buf[8] as i8;
if buf[9] != 0 {
return Err(ModulationParseError::InvalidField);
}
Ok(Self {
freq_hz,
bw,
cr,
grid,
hopping,
tx_power_dbm,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FlrcBitrate {
Kbps2600 = 0,
Kbps2080 = 1,
Kbps1300 = 2,
Kbps1040 = 3,
Kbps650 = 4,
Kbps520 = 5,
Kbps325 = 6,
Kbps260 = 7,
}
impl FlrcBitrate {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Kbps2600,
1 => Self::Kbps2080,
2 => Self::Kbps1300,
3 => Self::Kbps1040,
4 => Self::Kbps650,
5 => Self::Kbps520,
6 => Self::Kbps325,
7 => Self::Kbps260,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FlrcCodingRate {
Cr1_2 = 0,
Cr3_4 = 1,
Cr1_1 = 2,
}
impl FlrcCodingRate {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Cr1_2,
1 => Self::Cr3_4,
2 => Self::Cr1_1,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FlrcBt {
Off = 0,
Bt0_5 = 1,
Bt1_0 = 2,
}
impl FlrcBt {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Off,
1 => Self::Bt0_5,
2 => Self::Bt1_0,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FlrcPreambleLen {
Bits8 = 0,
Bits12 = 1,
Bits16 = 2,
Bits20 = 3,
Bits24 = 4,
Bits28 = 5,
Bits32 = 6,
}
impl FlrcPreambleLen {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Bits8,
1 => Self::Bits12,
2 => Self::Bits16,
3 => Self::Bits20,
4 => Self::Bits24,
5 => Self::Bits28,
6 => Self::Bits32,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FlrcConfig {
pub freq_hz: u32,
pub bitrate: FlrcBitrate,
pub cr: FlrcCodingRate,
pub bt: FlrcBt,
pub preamble_len: FlrcPreambleLen,
pub sync_word: u32,
pub tx_power_dbm: i8,
}
impl FlrcConfig {
pub const WIRE_SIZE: usize = 13;
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
if buf.len() < Self::WIRE_SIZE {
return Err(ModulationEncodeError::BufferTooSmall);
}
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
buf[4] = self.bitrate.as_u8();
buf[5] = self.cr.as_u8();
buf[6] = self.bt.as_u8();
buf[7] = self.preamble_len.as_u8();
buf[8..12].copy_from_slice(&self.sync_word.to_le_bytes());
buf[12] = self.tx_power_dbm as u8;
Ok(Self::WIRE_SIZE)
}
pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
if buf.len() != Self::WIRE_SIZE {
return Err(ModulationParseError::WrongLength {
expected: Self::WIRE_SIZE,
actual: buf.len(),
});
}
let freq_hz = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let bitrate = FlrcBitrate::from_u8(buf[4]).ok_or(ModulationParseError::InvalidField)?;
let cr = FlrcCodingRate::from_u8(buf[5]).ok_or(ModulationParseError::InvalidField)?;
let bt = FlrcBt::from_u8(buf[6]).ok_or(ModulationParseError::InvalidField)?;
let preamble_len =
FlrcPreambleLen::from_u8(buf[7]).ok_or(ModulationParseError::InvalidField)?;
let sync_word = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let tx_power_dbm = buf[12] as i8;
Ok(Self {
freq_hz,
bitrate,
cr,
bt,
preamble_len,
sync_word,
tx_power_dbm,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Modulation {
LoRa(LoRaConfig),
FskGfsk(FskConfig),
LrFhss(LrFhssConfig),
Flrc(FlrcConfig),
}
impl Modulation {
pub const fn id(&self) -> ModulationId {
match self {
Self::LoRa(_) => ModulationId::LoRa,
Self::FskGfsk(_) => ModulationId::FskGfsk,
Self::LrFhss(_) => ModulationId::LrFhss,
Self::Flrc(_) => ModulationId::Flrc,
}
}
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, ModulationEncodeError> {
if buf.is_empty() {
return Err(ModulationEncodeError::BufferTooSmall);
}
buf[0] = self.id().as_u8();
let n = match self {
Self::LoRa(c) => c.encode(&mut buf[1..])?,
Self::FskGfsk(c) => c.encode(&mut buf[1..])?,
Self::LrFhss(c) => c.encode(&mut buf[1..])?,
Self::Flrc(c) => c.encode(&mut buf[1..])?,
};
Ok(1 + n)
}
pub fn decode(buf: &[u8]) -> Result<Self, ModulationParseError> {
if buf.is_empty() {
return Err(ModulationParseError::TooShort);
}
let id = ModulationId::from_u8(buf[0]).ok_or(ModulationParseError::UnknownModulation)?;
let params = &buf[1..];
Ok(match id {
ModulationId::LoRa => Self::LoRa(LoRaConfig::decode(params)?),
ModulationId::FskGfsk => Self::FskGfsk(FskConfig::decode(params)?),
ModulationId::LrFhss => Self::LrFhss(LrFhssConfig::decode(params)?),
ModulationId::Flrc => Self::Flrc(FlrcConfig::decode(params)?),
})
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
fn sample_lora() -> LoRaConfig {
LoRaConfig {
freq_hz: 868_100_000,
sf: 7,
bw: LoRaBandwidth::Khz125,
cr: LoRaCodingRate::Cr4_5,
preamble_len: 8,
sync_word: 0x1424,
tx_power_dbm: 14,
header_mode: LoRaHeaderMode::Explicit,
payload_crc: true,
iq_invert: false,
}
}
#[test]
fn lora_wire_size() {
assert_eq!(LoRaConfig::WIRE_SIZE, 15);
}
#[test]
fn lora_roundtrip() {
let cfg = sample_lora();
let mut buf = [0u8; 32];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 15);
let decoded = LoRaConfig::decode(&buf[..n]).unwrap();
assert_eq!(decoded, cfg);
}
#[test]
fn lora_appendix_c23_bytes() {
let cfg = sample_lora();
let mut buf = [0u8; 15];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 15);
let expected: [u8; 15] = [
0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x00, 0x08, 0x00, 0x24, 0x14, 0x0E, 0x00, 0x01, 0x00, ];
assert_eq!(buf, expected);
}
#[test]
fn lora_rejects_wrong_length() {
assert!(matches!(
LoRaConfig::decode(&[0u8; 14]),
Err(ModulationParseError::WrongLength { .. })
));
assert!(matches!(
LoRaConfig::decode(&[0u8; 16]),
Err(ModulationParseError::WrongLength { .. })
));
}
#[test]
fn lora_rejects_bad_enum_values() {
let mut buf = [0u8; 15];
sample_lora().encode(&mut buf).unwrap();
let mut bad = buf;
bad[5] = 14; assert!(LoRaConfig::decode(&bad).is_err());
let mut bad = buf;
bad[6] = 4; assert!(LoRaConfig::decode(&bad).is_err());
let mut bad = buf;
bad[12] = 2; assert!(LoRaConfig::decode(&bad).is_err());
let mut bad = buf;
bad[13] = 2; assert!(LoRaConfig::decode(&bad).is_err());
let mut bad = buf;
bad[14] = 2; assert!(LoRaConfig::decode(&bad).is_err());
}
#[test]
fn fsk_roundtrip_empty_sync() {
let cfg = FskConfig {
freq_hz: 868_000_000,
bitrate_bps: 9_600,
freq_dev_hz: 5_000,
rx_bw: 0x0B,
preamble_len: 16,
sync_word_len: 0,
sync_word: [0u8; MAX_SYNC_WORD_LEN],
};
let mut buf = [0u8; 32];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 16);
let decoded = FskConfig::decode(&buf[..n]).unwrap();
assert_eq!(decoded, cfg);
}
#[test]
fn fsk_roundtrip_with_sync() {
let mut sync = [0u8; MAX_SYNC_WORD_LEN];
sync[..4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
let cfg = FskConfig {
freq_hz: 868_000_000,
bitrate_bps: 50_000,
freq_dev_hz: 25_000,
rx_bw: 0x1A,
preamble_len: 32,
sync_word_len: 4,
sync_word: sync,
};
let mut buf = [0u8; 32];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 20);
let decoded = FskConfig::decode(&buf[..n]).unwrap();
assert_eq!(decoded, cfg);
}
#[test]
fn fsk_rejects_oversized_sync() {
let mut cfg = FskConfig {
freq_hz: 0,
bitrate_bps: 0,
freq_dev_hz: 0,
rx_bw: 0,
preamble_len: 0,
sync_word_len: 9,
sync_word: [0u8; MAX_SYNC_WORD_LEN],
};
let mut buf = [0u8; 32];
assert!(cfg.encode(&mut buf).is_err());
cfg.sync_word_len = 0;
cfg.encode(&mut buf).unwrap();
let mut bad = [0u8; 16];
bad.copy_from_slice(&buf[..16]);
bad[15] = 9;
assert!(FskConfig::decode(&bad).is_err());
}
#[test]
fn lr_fhss_roundtrip() {
let cfg = LrFhssConfig {
freq_hz: 915_000_000,
bw: LrFhssBandwidth::Khz136,
cr: LrFhssCodingRate::Cr2_3,
grid: LrFhssGrid::Khz25,
hopping: true,
tx_power_dbm: 14,
};
let mut buf = [0u8; 10];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 10);
let decoded = LrFhssConfig::decode(&buf[..n]).unwrap();
assert_eq!(decoded, cfg);
assert_eq!(buf[9], 0, "reserved byte must serialize as 0");
}
#[test]
fn lr_fhss_rejects_nonzero_reserved() {
let cfg = LrFhssConfig {
freq_hz: 0,
bw: LrFhssBandwidth::Khz39,
cr: LrFhssCodingRate::Cr1_3,
grid: LrFhssGrid::Khz25,
hopping: false,
tx_power_dbm: 0,
};
let mut buf = [0u8; 10];
cfg.encode(&mut buf).unwrap();
buf[9] = 1;
assert!(LrFhssConfig::decode(&buf).is_err());
}
#[test]
fn flrc_roundtrip() {
let cfg = FlrcConfig {
freq_hz: 2_400_000_000,
bitrate: FlrcBitrate::Kbps1300,
cr: FlrcCodingRate::Cr3_4,
bt: FlrcBt::Bt0_5,
preamble_len: FlrcPreambleLen::Bits24,
sync_word: 0x1234_5678,
tx_power_dbm: 10,
};
let mut buf = [0u8; 13];
let n = cfg.encode(&mut buf).unwrap();
assert_eq!(n, 13);
let decoded = FlrcConfig::decode(&buf[..n]).unwrap();
assert_eq!(decoded, cfg);
}
#[test]
fn modulation_sum_roundtrip() {
let m = Modulation::LoRa(sample_lora());
let mut buf = [0u8; 64];
let n = m.encode(&mut buf).unwrap();
assert_eq!(n, 1 + LoRaConfig::WIRE_SIZE);
assert_eq!(buf[0], ModulationId::LoRa.as_u8());
let decoded = Modulation::decode(&buf[..n]).unwrap();
assert_eq!(decoded, m);
}
#[test]
fn modulation_id_rejects_unknown() {
assert!(matches!(
Modulation::decode(&[0x05, 0, 0, 0]),
Err(ModulationParseError::UnknownModulation)
));
assert!(matches!(
Modulation::decode(&[]),
Err(ModulationParseError::TooShort)
));
}
#[test]
fn lora_bandwidth_hz_table() {
assert_eq!(LoRaBandwidth::Khz125.as_hz(), 125_000);
assert_eq!(LoRaBandwidth::Khz500.as_hz(), 500_000);
assert_eq!(LoRaBandwidth::Khz7.as_hz(), 7_810);
assert_eq!(LoRaBandwidth::Khz1600.as_hz(), 1_600_000);
}
}