use heapless::Vec as HVec;
use crate::{
DeviceMessageEncodeError, DeviceMessageParseError, ErrorCode, Info, InfoParseError,
MAX_OTA_PAYLOAD, Modulation, ModulationEncodeError, ModulationParseError, commands,
};
pub const TYPE_OK: u8 = 0x80;
pub const TYPE_ERR: u8 = 0x81;
pub const TYPE_RX: u8 = 0xC0;
pub const TYPE_TX_DONE: u8 = 0xC1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum SetConfigResultCode {
Applied = 0,
AlreadyMatched = 1,
LockedMismatch = 2,
}
impl SetConfigResultCode {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Applied,
1 => Self::AlreadyMatched,
2 => Self::LockedMismatch,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Owner {
None = 0,
Mine = 1,
Other = 2,
}
impl Owner {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::None,
1 => Self::Mine,
2 => Self::Other,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct SetConfigResult {
pub result: SetConfigResultCode,
pub owner: Owner,
pub current: Modulation,
}
impl SetConfigResult {
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
if buf.len() < 2 {
return Err(DeviceMessageEncodeError::BufferTooSmall);
}
buf[0] = self.result.as_u8();
buf[1] = self.owner.as_u8();
let rest = self.current.encode(&mut buf[2..]).map_err(|e| match e {
ModulationEncodeError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
ModulationEncodeError::SyncWordTooLong => DeviceMessageEncodeError::SyncWordTooLong,
})?;
Ok(2 + rest)
}
pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
if buf.len() < 2 {
return Err(DeviceMessageParseError::TooShort);
}
let result =
SetConfigResultCode::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
let owner = Owner::from_u8(buf[1]).ok_or(DeviceMessageParseError::InvalidField)?;
let current = Modulation::decode(&buf[2..]).map_err(DeviceMessageParseError::from)?;
Ok(Self {
result,
owner,
current,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OkPayload {
Empty,
Info(Info),
SetConfig(SetConfigResult),
}
impl OkPayload {
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
match self {
Self::Empty => Ok(0),
Self::Info(info) => info.encode(buf).map_err(|e| match e {
InfoParseError::TooShort | InfoParseError::InvalidField => {
DeviceMessageEncodeError::InvalidField
}
InfoParseError::BufferTooSmall => DeviceMessageEncodeError::BufferTooSmall,
}),
Self::SetConfig(r) => r.encode(buf),
}
}
pub fn parse_for(cmd_type: u8, payload: &[u8]) -> Result<Self, DeviceMessageParseError> {
match cmd_type {
commands::TYPE_PING
| commands::TYPE_TX
| commands::TYPE_RX_START
| commands::TYPE_RX_STOP => {
if !payload.is_empty() {
return Err(DeviceMessageParseError::WrongLength);
}
Ok(Self::Empty)
}
commands::TYPE_GET_INFO => Info::decode(payload)
.map(Self::Info)
.map_err(DeviceMessageParseError::from),
commands::TYPE_SET_CONFIG => SetConfigResult::decode(payload).map(Self::SetConfig),
_ => Err(DeviceMessageParseError::UnknownContext),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum RxOrigin {
Ota = 0,
LocalLoopback = 1,
}
impl RxOrigin {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Ota,
1 => Self::LocalLoopback,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct RxPayload {
pub rssi_tenths_dbm: i16,
pub snr_tenths_db: i16,
pub freq_err_hz: i32,
pub timestamp_us: u64,
pub crc_valid: bool,
pub packets_dropped: u16,
pub origin: RxOrigin,
pub data: HVec<u8, MAX_OTA_PAYLOAD>,
}
impl RxPayload {
pub const METADATA_SIZE: usize = 20;
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
let total = Self::METADATA_SIZE + self.data.len();
if buf.len() < total {
return Err(DeviceMessageEncodeError::BufferTooSmall);
}
if self.data.len() > MAX_OTA_PAYLOAD {
return Err(DeviceMessageEncodeError::PayloadTooLarge);
}
buf[0..2].copy_from_slice(&self.rssi_tenths_dbm.to_le_bytes());
buf[2..4].copy_from_slice(&self.snr_tenths_db.to_le_bytes());
buf[4..8].copy_from_slice(&self.freq_err_hz.to_le_bytes());
buf[8..16].copy_from_slice(&self.timestamp_us.to_le_bytes());
buf[16] = u8::from(self.crc_valid);
buf[17..19].copy_from_slice(&self.packets_dropped.to_le_bytes());
buf[19] = self.origin.as_u8();
buf[20..total].copy_from_slice(&self.data);
Ok(total)
}
pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
if buf.len() < Self::METADATA_SIZE {
return Err(DeviceMessageParseError::TooShort);
}
let n = buf.len() - Self::METADATA_SIZE;
if n > MAX_OTA_PAYLOAD {
return Err(DeviceMessageParseError::WrongLength);
}
let rssi_tenths_dbm = i16::from_le_bytes([buf[0], buf[1]]);
let snr_tenths_db = i16::from_le_bytes([buf[2], buf[3]]);
let freq_err_hz = i32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
let timestamp_us = u64::from_le_bytes([
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
]);
let crc_valid = match buf[16] {
0 => false,
1 => true,
_ => return Err(DeviceMessageParseError::InvalidField),
};
let packets_dropped = u16::from_le_bytes([buf[17], buf[18]]);
let origin = RxOrigin::from_u8(buf[19]).ok_or(DeviceMessageParseError::InvalidField)?;
let mut data = HVec::new();
data.extend_from_slice(&buf[Self::METADATA_SIZE..Self::METADATA_SIZE + n])
.map_err(|_| DeviceMessageParseError::WrongLength)?;
Ok(Self {
rssi_tenths_dbm,
snr_tenths_db,
freq_err_hz,
timestamp_us,
crc_valid,
packets_dropped,
origin,
data,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum TxResult {
Transmitted = 0,
ChannelBusy = 1,
Cancelled = 2,
}
impl TxResult {
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => Self::Transmitted,
1 => Self::ChannelBusy,
2 => Self::Cancelled,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TxDonePayload {
pub result: TxResult,
pub airtime_us: u32,
}
impl TxDonePayload {
pub const WIRE_SIZE: usize = 5;
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
if buf.len() < Self::WIRE_SIZE {
return Err(DeviceMessageEncodeError::BufferTooSmall);
}
buf[0] = self.result.as_u8();
buf[1..5].copy_from_slice(&self.airtime_us.to_le_bytes());
Ok(Self::WIRE_SIZE)
}
pub fn decode(buf: &[u8]) -> Result<Self, DeviceMessageParseError> {
if buf.len() != Self::WIRE_SIZE {
return Err(DeviceMessageParseError::WrongLength);
}
let result = TxResult::from_u8(buf[0]).ok_or(DeviceMessageParseError::InvalidField)?;
let airtime_us = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
Ok(Self { result, airtime_us })
}
}
pub fn encode_err_payload(
code: ErrorCode,
buf: &mut [u8],
) -> Result<usize, DeviceMessageEncodeError> {
if buf.len() < 2 {
return Err(DeviceMessageEncodeError::BufferTooSmall);
}
buf[0..2].copy_from_slice(&code.as_u16().to_le_bytes());
Ok(2)
}
pub fn decode_err_payload(buf: &[u8]) -> Result<ErrorCode, DeviceMessageParseError> {
if buf.len() != 2 {
return Err(DeviceMessageParseError::WrongLength);
}
Ok(ErrorCode::from_u16(u16::from_le_bytes([buf[0], buf[1]])))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DeviceMessage {
Ok(OkPayload),
Err(ErrorCode),
Rx(RxPayload),
TxDone(TxDonePayload),
}
impl DeviceMessage {
pub const fn type_id(&self) -> u8 {
match self {
Self::Ok(_) => TYPE_OK,
Self::Err(_) => TYPE_ERR,
Self::Rx(_) => TYPE_RX,
Self::TxDone(_) => TYPE_TX_DONE,
}
}
pub fn encode_payload(&self, buf: &mut [u8]) -> Result<usize, DeviceMessageEncodeError> {
match self {
Self::Ok(ok) => ok.encode(buf),
Self::Err(code) => encode_err_payload(*code, buf),
Self::Rx(rx) => rx.encode(buf),
Self::TxDone(td) => td.encode(buf),
}
}
pub fn parse(
type_id: u8,
payload: &[u8],
originating_cmd_type: Option<u8>,
) -> Result<Self, DeviceMessageParseError> {
match type_id {
TYPE_OK => {
let cmd = originating_cmd_type.ok_or(DeviceMessageParseError::MissingContext)?;
Ok(Self::Ok(OkPayload::parse_for(cmd, payload)?))
}
TYPE_ERR => Ok(Self::Err(decode_err_payload(payload)?)),
TYPE_RX => Ok(Self::Rx(RxPayload::decode(payload)?)),
TYPE_TX_DONE => Ok(Self::TxDone(TxDonePayload::decode(payload)?)),
_ => Err(DeviceMessageParseError::UnknownType),
}
}
}
impl From<ModulationParseError> for DeviceMessageParseError {
fn from(e: ModulationParseError) -> Self {
match e {
ModulationParseError::WrongLength { .. } | ModulationParseError::TooShort => {
Self::WrongLength
}
ModulationParseError::InvalidField => Self::InvalidField,
ModulationParseError::UnknownModulation => Self::UnknownContext,
}
}
}
impl From<InfoParseError> for DeviceMessageParseError {
fn from(e: InfoParseError) -> Self {
match e {
InfoParseError::TooShort | InfoParseError::BufferTooSmall => Self::WrongLength,
InfoParseError::InvalidField => Self::InvalidField,
}
}
}
#[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_OK, 0x80);
assert_eq!(TYPE_ERR, 0x81);
assert_eq!(TYPE_RX, 0xC0);
assert_eq!(TYPE_TX_DONE, 0xC1);
}
#[test]
fn ok_empty_roundtrip_for_ping() {
let ok = OkPayload::Empty;
let mut buf = [0u8; 4];
let n = ok.encode(&mut buf).unwrap();
assert_eq!(n, 0);
assert_eq!(
OkPayload::parse_for(commands::TYPE_PING, &buf[..n]).unwrap(),
ok
);
}
#[test]
fn ok_empty_rejects_nonempty_for_tx() {
assert!(matches!(
OkPayload::parse_for(commands::TYPE_TX, &[0]),
Err(DeviceMessageParseError::WrongLength)
));
}
#[test]
fn ok_set_config_roundtrip() {
let r = SetConfigResult {
result: SetConfigResultCode::Applied,
owner: Owner::Mine,
current: Modulation::LoRa(sample_lora()),
};
let mut buf = [0u8; 64];
let n = r.encode(&mut buf).unwrap();
assert_eq!(n, 2 + 1 + 15);
assert_eq!(SetConfigResult::decode(&buf[..n]).unwrap(), r);
}
#[test]
fn ok_set_config_spec_bytes_c23() {
let r = SetConfigResult {
result: SetConfigResultCode::Applied,
owner: Owner::Mine,
current: Modulation::LoRa(sample_lora()),
};
let mut buf = [0u8; 64];
let n = r.encode(&mut buf).unwrap();
let expected: [u8; 18] = [
0x00, 0x01, 0x01, 0xA0, 0x27, 0xBE, 0x33, 0x07, 0x07, 0x00, 0x08, 0x00, 0x24, 0x14, 0x0E, 0x00, 0x01,
0x00,
];
assert_eq!(&buf[..n], &expected);
}
#[test]
fn err_payload_roundtrip() {
let mut buf = [0u8; 2];
let n = encode_err_payload(ErrorCode::ENotConfigured, &mut buf).unwrap();
assert_eq!(n, 2);
assert_eq!(&buf, &[0x03, 0x00]);
let decoded = decode_err_payload(&buf).unwrap();
assert_eq!(decoded, ErrorCode::ENotConfigured);
}
#[test]
fn err_payload_preserves_unknown_codes() {
let mut buf = [0u8; 2];
encode_err_payload(ErrorCode::Unknown(0xABCD), &mut buf).unwrap();
let decoded = decode_err_payload(&buf).unwrap();
assert_eq!(decoded.as_u16(), 0xABCD);
}
#[test]
fn rx_payload_roundtrip_with_data() {
let mut data = HVec::new();
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
let rx = RxPayload {
rssi_tenths_dbm: -735,
snr_tenths_db: 95,
freq_err_hz: -125,
timestamp_us: 42_000_000,
crc_valid: true,
packets_dropped: 0,
origin: RxOrigin::Ota,
data,
};
let mut buf = [0u8; 64];
let n = rx.encode(&mut buf).unwrap();
assert_eq!(n, 20 + 4);
let decoded = RxPayload::decode(&buf[..n]).unwrap();
assert_eq!(decoded, rx);
}
#[test]
fn rx_payload_spec_bytes_c26() {
let mut data = HVec::new();
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
let rx = RxPayload {
rssi_tenths_dbm: -735,
snr_tenths_db: 95,
freq_err_hz: -125,
timestamp_us: 42_000_000,
crc_valid: true,
packets_dropped: 0,
origin: RxOrigin::Ota,
data,
};
let mut buf = [0u8; 64];
let n = rx.encode(&mut buf).unwrap();
let expected: [u8; 24] = [
0x21, 0xFD, 0x5F, 0x00, 0x83, 0xFF, 0xFF, 0xFF, 0x80, 0xDE, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
];
assert_eq!(&buf[..n], &expected);
}
#[test]
fn rx_payload_rejects_invalid_crc_byte() {
let mut buf = [0u8; 20];
buf[16] = 2; assert!(RxPayload::decode(&buf).is_err());
}
#[test]
fn tx_done_roundtrip() {
let td = TxDonePayload {
result: TxResult::Transmitted,
airtime_us: 30_976,
};
let mut buf = [0u8; 8];
let n = td.encode(&mut buf).unwrap();
assert_eq!(n, 5);
assert_eq!(TxDonePayload::decode(&buf[..n]).unwrap(), td);
}
#[test]
fn tx_done_spec_bytes_c24() {
let td = TxDonePayload {
result: TxResult::Transmitted,
airtime_us: 30_976,
};
let mut buf = [0u8; 5];
td.encode(&mut buf).unwrap();
assert_eq!(&buf, &[0x00, 0x00, 0x79, 0x00, 0x00]);
}
#[test]
fn device_message_parse_requires_ok_context() {
let mut buf = [0u8; 4];
let n = OkPayload::Empty.encode(&mut buf).unwrap();
assert!(matches!(
DeviceMessage::parse(TYPE_OK, &buf[..n], None),
Err(DeviceMessageParseError::MissingContext)
));
assert!(matches!(
DeviceMessage::parse(TYPE_OK, &buf[..n], Some(commands::TYPE_PING)).unwrap(),
DeviceMessage::Ok(OkPayload::Empty)
));
}
#[test]
fn device_message_unknown_type_rejects() {
assert!(matches!(
DeviceMessage::parse(0x55, &[], None),
Err(DeviceMessageParseError::UnknownType)
));
}
}