use heapless::Vec as HVec;
use crate::{
CommandEncodeError, CommandParseError, MAX_OTA_PAYLOAD, Modulation, ModulationEncodeError,
ModulationParseError,
};
pub const TYPE_PING: u8 = 0x01;
pub const TYPE_GET_INFO: u8 = 0x02;
pub const TYPE_SET_CONFIG: u8 = 0x03;
pub const TYPE_TX: u8 = 0x04;
pub const TYPE_RX_START: u8 = 0x05;
pub const TYPE_RX_STOP: u8 = 0x06;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TxFlags {
pub skip_cad: bool,
}
impl TxFlags {
pub const fn as_byte(self) -> u8 {
if self.skip_cad { 0b0000_0001 } else { 0 }
}
pub const fn from_byte(b: u8) -> Result<Self, CommandParseError> {
if b & !0b0000_0001 != 0 {
return Err(CommandParseError::ReservedBitSet);
}
Ok(Self {
skip_cad: b & 0b0000_0001 != 0,
})
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Command {
Ping,
GetInfo,
SetConfig(Modulation),
Tx {
flags: TxFlags,
data: HVec<u8, MAX_OTA_PAYLOAD>,
},
RxStart,
RxStop,
}
impl Command {
pub const fn type_id(&self) -> u8 {
match self {
Self::Ping => TYPE_PING,
Self::GetInfo => TYPE_GET_INFO,
Self::SetConfig(_) => TYPE_SET_CONFIG,
Self::Tx { .. } => TYPE_TX,
Self::RxStart => TYPE_RX_START,
Self::RxStop => TYPE_RX_STOP,
}
}
pub fn encode_payload(&self, buf: &mut [u8]) -> Result<usize, CommandEncodeError> {
match self {
Self::Ping | Self::GetInfo | Self::RxStart | Self::RxStop => Ok(0),
Self::SetConfig(m) => m.encode(buf).map_err(CommandEncodeError::from),
Self::Tx { flags, data } => {
if data.is_empty() {
return Err(CommandEncodeError::EmptyTxPayload);
}
if data.len() > MAX_OTA_PAYLOAD {
return Err(CommandEncodeError::PayloadTooLarge);
}
let total = 1 + data.len();
if buf.len() < total {
return Err(CommandEncodeError::BufferTooSmall);
}
buf[0] = flags.as_byte();
buf[1..total].copy_from_slice(data);
Ok(total)
}
}
}
pub fn parse(type_id: u8, payload: &[u8]) -> Result<Self, CommandParseError> {
match type_id {
TYPE_PING => {
if !payload.is_empty() {
return Err(CommandParseError::WrongLength);
}
Ok(Self::Ping)
}
TYPE_GET_INFO => {
if !payload.is_empty() {
return Err(CommandParseError::WrongLength);
}
Ok(Self::GetInfo)
}
TYPE_SET_CONFIG => Modulation::decode(payload)
.map(Self::SetConfig)
.map_err(CommandParseError::from),
TYPE_TX => {
if payload.is_empty() {
return Err(CommandParseError::WrongLength);
}
let flags = TxFlags::from_byte(payload[0])?;
let body = &payload[1..];
if body.is_empty() {
return Err(CommandParseError::WrongLength);
}
if body.len() > MAX_OTA_PAYLOAD {
return Err(CommandParseError::WrongLength);
}
let mut data = HVec::new();
data.extend_from_slice(body)
.map_err(|_| CommandParseError::WrongLength)?;
Ok(Self::Tx { flags, data })
}
TYPE_RX_START => {
if !payload.is_empty() {
return Err(CommandParseError::WrongLength);
}
Ok(Self::RxStart)
}
TYPE_RX_STOP => {
if !payload.is_empty() {
return Err(CommandParseError::WrongLength);
}
Ok(Self::RxStop)
}
_ => Err(CommandParseError::UnknownType),
}
}
}
impl From<ModulationEncodeError> for CommandEncodeError {
fn from(e: ModulationEncodeError) -> Self {
match e {
ModulationEncodeError::BufferTooSmall => Self::BufferTooSmall,
ModulationEncodeError::SyncWordTooLong => Self::SyncWordTooLong,
}
}
}
impl From<ModulationParseError> for CommandParseError {
fn from(e: ModulationParseError) -> Self {
match e {
ModulationParseError::WrongLength { .. } | ModulationParseError::TooShort => {
Self::WrongLength
}
ModulationParseError::InvalidField => Self::InvalidField,
ModulationParseError::UnknownModulation => Self::UnknownModulation,
}
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode};
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 type_ids_match_spec() {
assert_eq!(TYPE_PING, 0x01);
assert_eq!(TYPE_GET_INFO, 0x02);
assert_eq!(TYPE_SET_CONFIG, 0x03);
assert_eq!(TYPE_TX, 0x04);
assert_eq!(TYPE_RX_START, 0x05);
assert_eq!(TYPE_RX_STOP, 0x06);
}
#[test]
fn tx_flags_roundtrip() {
assert_eq!(TxFlags::default().as_byte(), 0);
assert_eq!(TxFlags { skip_cad: true }.as_byte(), 1);
assert_eq!(TxFlags::from_byte(0).unwrap(), TxFlags::default());
assert_eq!(TxFlags::from_byte(1).unwrap(), TxFlags { skip_cad: true });
}
#[test]
fn tx_flags_reject_reserved_bits() {
assert!(matches!(
TxFlags::from_byte(0x02),
Err(CommandParseError::ReservedBitSet)
));
assert!(matches!(
TxFlags::from_byte(0x80),
Err(CommandParseError::ReservedBitSet)
));
}
#[test]
fn roundtrip_empty_commands() {
for cmd in [
Command::Ping,
Command::GetInfo,
Command::RxStart,
Command::RxStop,
] {
let mut buf = [0u8; 4];
let n = cmd.encode_payload(&mut buf).unwrap();
assert_eq!(n, 0);
assert_eq!(Command::parse(cmd.type_id(), &buf[..n]).unwrap(), cmd);
}
}
#[test]
fn roundtrip_set_config_lora() {
let cmd = Command::SetConfig(Modulation::LoRa(sample_lora()));
let mut buf = [0u8; 32];
let n = cmd.encode_payload(&mut buf).unwrap();
assert_eq!(n, 16);
assert_eq!(Command::parse(cmd.type_id(), &buf[..n]).unwrap(), cmd);
}
#[test]
fn roundtrip_tx_with_cad() {
let mut data = HVec::new();
data.extend_from_slice(b"Hello").unwrap();
let cmd = Command::Tx {
flags: TxFlags { skip_cad: false },
data,
};
let mut buf = [0u8; 8];
let n = cmd.encode_payload(&mut buf).unwrap();
assert_eq!(n, 6); assert_eq!(buf[0], 0x00);
assert_eq!(&buf[1..n], b"Hello");
assert_eq!(Command::parse(cmd.type_id(), &buf[..n]).unwrap(), cmd);
}
#[test]
fn roundtrip_tx_skip_cad() {
let mut data = HVec::new();
data.extend_from_slice(b"URGENT").unwrap();
let cmd = Command::Tx {
flags: TxFlags { skip_cad: true },
data,
};
let mut buf = [0u8; 8];
let n = cmd.encode_payload(&mut buf).unwrap();
assert_eq!(n, 7);
assert_eq!(buf[0], 0x01);
assert_eq!(&buf[1..n], b"URGENT");
assert_eq!(Command::parse(cmd.type_id(), &buf[..n]).unwrap(), cmd);
}
#[test]
fn tx_rejects_empty_payload() {
let cmd = Command::Tx {
flags: TxFlags::default(),
data: HVec::new(),
};
let mut buf = [0u8; 2];
assert!(matches!(
cmd.encode_payload(&mut buf),
Err(CommandEncodeError::EmptyTxPayload)
));
}
#[test]
fn tx_parse_rejects_empty_payload() {
assert!(matches!(
Command::parse(TYPE_TX, &[0x00]),
Err(CommandParseError::WrongLength)
));
}
#[test]
fn ping_rejects_nonempty_payload() {
assert!(matches!(
Command::parse(TYPE_PING, &[0x00]),
Err(CommandParseError::WrongLength)
));
}
#[test]
fn unknown_type_rejects() {
assert!(matches!(
Command::parse(0x10, &[]),
Err(CommandParseError::UnknownType)
));
assert!(matches!(
Command::parse(0xFF, &[0xDE, 0xAD]),
Err(CommandParseError::UnknownType)
));
}
}