From 663f23b07670982e99d740e07763d504ea531b3e Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Wed, 8 Jan 2025 11:35:30 +0000 Subject: [PATCH] lrwn: Implement v1 applayer multicast setup structs. --- lrwn/src/applayer/mod.rs | 1 + lrwn/src/applayer/multicastsetup/mod.rs | 1 + lrwn/src/applayer/multicastsetup/v1.rs | 1283 +++++++++++++++++++++++ 3 files changed, 1285 insertions(+) create mode 100644 lrwn/src/applayer/multicastsetup/mod.rs create mode 100644 lrwn/src/applayer/multicastsetup/v1.rs diff --git a/lrwn/src/applayer/mod.rs b/lrwn/src/applayer/mod.rs index ee002523..83872ec1 100644 --- a/lrwn/src/applayer/mod.rs +++ b/lrwn/src/applayer/mod.rs @@ -1,6 +1,7 @@ use anyhow::Result; pub mod clocksync; +pub mod multicastsetup; pub trait PayloadCodec { fn decode(b: &[u8]) -> Result; diff --git a/lrwn/src/applayer/multicastsetup/mod.rs b/lrwn/src/applayer/multicastsetup/mod.rs new file mode 100644 index 00000000..a3a6d96c --- /dev/null +++ b/lrwn/src/applayer/multicastsetup/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/lrwn/src/applayer/multicastsetup/v1.rs b/lrwn/src/applayer/multicastsetup/v1.rs new file mode 100644 index 00000000..a3742230 --- /dev/null +++ b/lrwn/src/applayer/multicastsetup/v1.rs @@ -0,0 +1,1283 @@ +use anyhow::Result; + +use crate::applayer::PayloadCodec; +use crate::DevAddr; + +pub enum Cid { + PackageVersionReq, + PackageVersionAns, + McGroupStatusReq, + McGroupStatusAns, + McGroupSetupReq, + McGroupSetupAns, + McGroupDeleteReq, + McGroupDeleteAns, + McClassCSessionReq, + McClassCSessionAns, + McClassBSessionReq, + McClassBSessionAns, +} + +impl Cid { + pub fn from_u8(uplink: bool, value: u8) -> Result { + Ok(match uplink { + true => match value { + 0x00 => Cid::PackageVersionAns, + 0x01 => Cid::McGroupStatusAns, + 0x02 => Cid::McGroupSetupAns, + 0x03 => Cid::McGroupDeleteAns, + 0x04 => Cid::McClassCSessionAns, + 0x05 => Cid::McClassBSessionAns, + _ => return Err(anyhow!("Invalid CID: {}", value)), + }, + false => match value { + 0x00 => Cid::PackageVersionReq, + 0x01 => Cid::McGroupStatusReq, + 0x02 => Cid::McGroupSetupReq, + 0x03 => Cid::McGroupDeleteReq, + 0x04 => Cid::McClassCSessionReq, + 0x05 => Cid::McClassBSessionReq, + _ => return Err(anyhow!("Invalid CID: {}", value)), + }, + }) + } + + pub fn to_u8(&self) -> u8 { + match self { + Cid::PackageVersionReq | Cid::PackageVersionAns => 0x00, + Cid::McGroupStatusReq | Cid::McGroupStatusAns => 0x01, + Cid::McGroupSetupReq | Cid::McGroupSetupAns => 0x02, + Cid::McGroupDeleteReq | Cid::McGroupDeleteAns => 0x03, + Cid::McClassCSessionReq | Cid::McClassCSessionAns => 0x04, + Cid::McClassBSessionReq | Cid::McClassBSessionAns => 0x05, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum Payload { + PackageVersionReq, + PackageVersionAns(PackageVersionAnsPayload), + McGroupStatusReq(McGroupStatusReqPayload), + McGroupStatusAns(McGroupStatusAnsPayload), + McGroupSetupReq(McGroupSetupReqPayload), + McGroupSetupAns(McGroupSetupAnsPayload), + McGroupDeleteReq(McGroupDeleteReqPayload), + McGroupDeleteAns(McGroupDeleteAnsPayload), + McClassCSessionReq(McClassCSessionReqPayload), + McClassCSessionAns(McClassCSessionAnsPayload), + McClassBSessionReq(McClassBSessionReqPayload), + McClassBSessionAns(McClassBSessionAnsPayload), +} + +impl Payload { + pub fn cid(&self) -> Cid { + match self { + Self::PackageVersionReq => Cid::PackageVersionReq, + Self::PackageVersionAns(_) => Cid::PackageVersionAns, + Self::McGroupStatusReq(_) => Cid::McGroupStatusReq, + Self::McGroupStatusAns(_) => Cid::McGroupStatusAns, + Self::McGroupSetupReq(_) => Cid::McGroupSetupReq, + Self::McGroupSetupAns(_) => Cid::McGroupSetupAns, + Self::McGroupDeleteReq(_) => Cid::McGroupDeleteReq, + Self::McGroupDeleteAns(_) => Cid::McGroupDeleteAns, + Self::McClassCSessionReq(_) => Cid::McClassCSessionReq, + Self::McClassCSessionAns(_) => Cid::McClassCSessionAns, + Self::McClassBSessionReq(_) => Cid::McClassBSessionReq, + Self::McClassBSessionAns(_) => Cid::McClassBSessionAns, + } + } + + pub fn from_slice(uplink: bool, b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("At least one byte is expected")); + } + + let cid = Cid::from_u8(uplink, b[0])?; + + Ok(match cid { + Cid::PackageVersionReq => Payload::PackageVersionReq, + Cid::PackageVersionAns => { + Payload::PackageVersionAns(PackageVersionAnsPayload::decode(&b[1..])?) + } + Cid::McGroupStatusReq => { + Payload::McGroupStatusReq(McGroupStatusReqPayload::decode(&b[1..])?) + } + Cid::McGroupStatusAns => { + Payload::McGroupStatusAns(McGroupStatusAnsPayload::decode(&b[1..])?) + } + Cid::McGroupSetupReq => { + Payload::McGroupSetupReq(McGroupSetupReqPayload::decode(&b[1..])?) + } + Cid::McGroupSetupAns => { + Payload::McGroupSetupAns(McGroupSetupAnsPayload::decode(&b[1..])?) + } + Cid::McGroupDeleteReq => { + Payload::McGroupDeleteReq(McGroupDeleteReqPayload::decode(&b[1..])?) + } + Cid::McGroupDeleteAns => { + Payload::McGroupDeleteAns(McGroupDeleteAnsPayload::decode(&b[1..])?) + } + Cid::McClassCSessionReq => { + Payload::McClassCSessionReq(McClassCSessionReqPayload::decode(&b[1..])?) + } + Cid::McClassCSessionAns => { + Payload::McClassCSessionAns(McClassCSessionAnsPayload::decode(&b[1..])?) + } + Cid::McClassBSessionReq => { + Payload::McClassBSessionReq(McClassBSessionReqPayload::decode(&b[1..])?) + } + Cid::McClassBSessionAns => { + Payload::McClassBSessionAns(McClassBSessionAnsPayload::decode(&b[1..])?) + } + }) + } + + pub fn to_vec(&self) -> Result> { + let mut out = vec![self.cid().to_u8()]; + + match self { + Self::PackageVersionReq => {} + Self::PackageVersionAns(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupStatusReq(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupStatusAns(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupSetupReq(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupSetupAns(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupDeleteReq(pl) => out.extend_from_slice(&pl.encode()?), + Self::McGroupDeleteAns(pl) => out.extend_from_slice(&pl.encode()?), + Self::McClassCSessionReq(pl) => out.extend_from_slice(&pl.encode()?), + Self::McClassCSessionAns(pl) => out.extend_from_slice(&pl.encode()?), + Self::McClassBSessionReq(pl) => out.extend_from_slice(&pl.encode()?), + Self::McClassBSessionAns(pl) => out.extend_from_slice(&pl.encode()?), + }; + + Ok(out) + } +} + +#[derive(Debug, PartialEq)] +pub struct PackageVersionAnsPayload { + pub package_identifier: u8, + pub package_version: u8, +} + +impl PayloadCodec for PackageVersionAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 2 { + return Err(anyhow!("Expected 2 bytes")); + } + + Ok(PackageVersionAnsPayload { + package_identifier: b[0], + package_version: b[1], + }) + } + fn encode(&self) -> Result> { + Ok(vec![self.package_identifier, self.package_version]) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupStatusReqPayload { + pub cmd_mask: McGroupStatusReqPayloadCmdMask, +} + +impl PayloadCodec for McGroupStatusReqPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 1 { + return Err(anyhow!("Expected 1 byte")); + } + + let mut out = McGroupStatusReqPayload { + cmd_mask: McGroupStatusReqPayloadCmdMask { + req_group_mask: [false; 4], + }, + }; + + for (i, v) in out.cmd_mask.req_group_mask.iter_mut().enumerate() { + *v = b[0] & (1 << i) != 0; + } + + Ok(out) + } + + fn encode(&self) -> Result> { + let mut out = vec![0; 1]; + for (i, v) in self.cmd_mask.req_group_mask.iter().enumerate() { + if *v { + out[0] |= 1 << i; + } + } + Ok(out) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupStatusReqPayloadCmdMask { + pub req_group_mask: [bool; 4], +} + +#[derive(Debug, PartialEq)] +pub struct McGroupStatusAnsPayload { + pub status: McGroupStatusAnsPayloadStatus, + pub items: Vec, +} + +impl PayloadCodec for McGroupStatusAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("At least 1 byte is expected")); + } + + if (b.len() - 1) % 5 != 0 { + return Err(anyhow!("Expected a multiple of 5 bytes + 1")); + } + + Ok(McGroupStatusAnsPayload { + status: McGroupStatusAnsPayloadStatus { + ans_group_mask: { + let mut mask = [false; 4]; + for (i, v) in mask.iter_mut().enumerate() { + *v = b[0] & (1 << i) != 0; + } + mask + }, + nb_total_groups: (b[0] >> 4) & 0x07, + }, + items: b[1..] + .chunks(5) + .map(|b| { + let mut mc_addr = [0; 4]; + mc_addr.copy_from_slice(&b[1..5]); + McGroupStatusAnsPayloadItem { + mc_group_id: b[0], + mc_addr: DevAddr::from_le_bytes(mc_addr), + } + }) + .collect(), + }) + } + + fn encode(&self) -> Result> { + let mut b = Vec::with_capacity(1 + (self.items.len() * 5)); + + b.push(self.status.nb_total_groups << 4); + for (i, v) in self.status.ans_group_mask.iter().enumerate() { + if *v { + b[0] |= 1 << i; + } + } + + for v in &self.items { + b.push(v.mc_group_id); + b.extend_from_slice(&v.mc_addr.to_le_bytes()); + } + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupStatusAnsPayloadStatus { + pub ans_group_mask: [bool; 4], + pub nb_total_groups: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McGroupStatusAnsPayloadItem { + pub mc_group_id: u8, + pub mc_addr: DevAddr, +} + +#[derive(Debug, PartialEq)] +pub struct McGroupSetupReqPayload { + pub mc_group_id_header: McGroupSetupReqPayloadMcGroupIdHeader, + pub mc_addr: DevAddr, + pub mc_key_encrypted: [u8; 16], + pub min_mc_f_count: u32, + pub max_mc_f_count: u32, +} + +impl PayloadCodec for McGroupSetupReqPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 29 { + return Err(anyhow!("Expected 29 bytes")); + } + + Ok(McGroupSetupReqPayload { + mc_group_id_header: McGroupSetupReqPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + }, + mc_addr: { + let mut mc_addr = [0; 4]; + mc_addr.copy_from_slice(&b[1..5]); + DevAddr::from_le_bytes(mc_addr) + }, + mc_key_encrypted: { + let mut mc_key_encrypted = [0; 16]; + mc_key_encrypted.copy_from_slice(&b[5..21]); + mc_key_encrypted + }, + min_mc_f_count: { + let mut f_count = [0; 4]; + f_count.copy_from_slice(&b[21..25]); + u32::from_le_bytes(f_count) + }, + max_mc_f_count: { + let mut f_count = [0; 4]; + f_count.copy_from_slice(&b[25..29]); + u32::from_le_bytes(f_count) + }, + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + let mut b = Vec::with_capacity(29); + b.push(self.mc_group_id_header.mc_group_id); + b.extend_from_slice(&self.mc_addr.to_le_bytes()); + b.extend_from_slice(&self.mc_key_encrypted); + b.extend_from_slice(&self.min_mc_f_count.to_le_bytes()); + b.extend_from_slice(&self.max_mc_f_count.to_le_bytes()); + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupSetupReqPayloadMcGroupIdHeader { + pub mc_group_id: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McGroupSetupAnsPayload { + pub mc_group_id_header: McGroupSetupAnsPayloadMcGroupIdHeader, +} + +impl PayloadCodec for McGroupSetupAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 1 { + return Err(anyhow!("Expected 1 byte")); + } + + Ok(McGroupSetupAnsPayload { + mc_group_id_header: McGroupSetupAnsPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + id_error: b[0] & 0x04 != 0, + }, + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + let mut b = vec![self.mc_group_id_header.mc_group_id]; + if self.mc_group_id_header.id_error { + b[0] |= 0x04; + } + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupSetupAnsPayloadMcGroupIdHeader { + pub mc_group_id: u8, + pub id_error: bool, +} + +#[derive(Debug, PartialEq)] +pub struct McGroupDeleteReqPayload { + pub mc_group_id_header: McGroupDeleteReqPayloadMcGroupIdHeader, +} + +impl PayloadCodec for McGroupDeleteReqPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 1 { + return Err(anyhow!("Expected 1 byte")); + } + + Ok(McGroupDeleteReqPayload { + mc_group_id_header: McGroupDeleteReqPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + }, + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + Ok(vec![self.mc_group_id_header.mc_group_id]) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupDeleteReqPayloadMcGroupIdHeader { + pub mc_group_id: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McGroupDeleteAnsPayload { + pub mc_group_id_header: McGroupDeleteAnsPayloadMcGroupIdHeader, +} + +impl PayloadCodec for McGroupDeleteAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 1 { + return Err(anyhow!("Expected 1 byte")); + } + + Ok(McGroupDeleteAnsPayload { + mc_group_id_header: McGroupDeleteAnsPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + mc_group_undefined: b[0] & 0x04 != 0, + }, + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + let mut b = vec![self.mc_group_id_header.mc_group_id]; + if self.mc_group_id_header.mc_group_undefined { + b[0] |= 0x04; + } + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McGroupDeleteAnsPayloadMcGroupIdHeader { + pub mc_group_id: u8, + pub mc_group_undefined: bool, +} + +#[derive(Debug, PartialEq)] +pub struct McClassCSessionReqPayload { + pub mc_group_id_header: McClassCSessionReqPayloadMcGroupIdHeader, + pub session_time: u32, + pub session_time_out: McClassCSessionReqPayloadSessionTimeOut, + pub dl_frequ: u32, + pub dr: u8, +} + +impl PayloadCodec for McClassCSessionReqPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 10 { + return Err(anyhow!("Expected 10 bytes")); + } + + Ok(McClassCSessionReqPayload { + mc_group_id_header: McClassCSessionReqPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + }, + session_time: { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&b[1..5]); + u32::from_le_bytes(bytes) + }, + session_time_out: McClassCSessionReqPayloadSessionTimeOut { + time_out: b[5] & 0x0f, + }, + dl_frequ: { + let mut bytes = [0; 4]; + bytes[0..3].copy_from_slice(&b[6..9]); + u32::from_le_bytes(bytes) * 100 + }, + dr: b[9], + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + if self.session_time_out.time_out > 15 { + return Err(anyhow!("Max time_out value is 15")); + } + + if self.dl_frequ % 100 != 0 { + return Err(anyhow!("dl_frequ must be a multiple of 100")); + } + + let mut b = Vec::with_capacity(10); + b.push(self.mc_group_id_header.mc_group_id); + b.extend_from_slice(&self.session_time.to_le_bytes()); + b.push(self.session_time_out.time_out); + b.extend_from_slice(&(self.dl_frequ / 100).to_le_bytes()[0..3]); + b.push(self.dr); + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McClassCSessionReqPayloadMcGroupIdHeader { + pub mc_group_id: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McClassCSessionReqPayloadSessionTimeOut { + pub time_out: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McClassCSessionAnsPayload { + pub status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId, + pub time_to_start: Option, +} + +impl PayloadCodec for McClassCSessionAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("At least 1 byte expected")); + } + + Ok(McClassCSessionAnsPayload { + status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId { + mc_group_id: b[0] & 0x03, + dr_error: b[0] & 0x04 != 0, + freq_error: b[0] & 0x08 != 0, + mc_group_undefined: b[0] & 0x10 != 0, + }, + time_to_start: if b[0] & 0x1c != 0 { + None + } else { + if b.len() != 4 { + return Err(anyhow!("Expected 4 bytes")); + } + + let mut bytes = [0; 4]; + bytes[0..3].copy_from_slice(&b[1..4]); + Some(u32::from_le_bytes(bytes)) + }, + }) + } + + fn encode(&self) -> Result> { + if self.status_and_mc_group_id.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + if let Some(v) = self.time_to_start { + if v >= (1 << 24) { + return Err(anyhow!("Max time_to_start is 1^24 - 1")); + } + } + + let mut b = Vec::with_capacity(4); + b.push(self.status_and_mc_group_id.mc_group_id); + if self.status_and_mc_group_id.dr_error { + b[0] |= 0x04; + } + if self.status_and_mc_group_id.freq_error { + b[0] |= 0x08; + } + if self.status_and_mc_group_id.mc_group_undefined { + b[0] |= 0x10; + } + + if let Some(v) = self.time_to_start { + b.extend_from_slice(&v.to_le_bytes()[0..3]); + } + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McClassCSessionAnsPayloadStatusAnsMcGroupId { + pub mc_group_id: u8, + pub dr_error: bool, + pub freq_error: bool, + pub mc_group_undefined: bool, +} + +#[derive(Debug, PartialEq)] +pub struct McClassBSessionReqPayload { + pub mc_group_id_header: McClassBSessionReqPayloadMcGroupIdHeader, + pub session_time: u32, + pub time_out_periodicity: McClassBSessionReqPayloadTimeOutPeriodicity, + pub dl_frequ: u32, + pub dr: u8, +} + +impl PayloadCodec for McClassBSessionReqPayload { + fn decode(b: &[u8]) -> Result { + if b.len() != 10 { + return Err(anyhow!("Expected 10 bytes")); + } + + Ok(McClassBSessionReqPayload { + mc_group_id_header: McClassBSessionReqPayloadMcGroupIdHeader { + mc_group_id: b[0] & 0x03, + }, + session_time: { + let mut bytes = [0; 4]; + bytes.copy_from_slice(&b[1..5]); + u32::from_le_bytes(bytes) + }, + time_out_periodicity: McClassBSessionReqPayloadTimeOutPeriodicity { + time_out: b[5] & 0x0f, + periodicity: (b[5] >> 4) & 0x07, + }, + dl_frequ: { + let mut bytes = [0; 4]; + bytes[0..3].copy_from_slice(&b[6..9]); + u32::from_le_bytes(bytes) * 100 + }, + dr: b[9], + }) + } + + fn encode(&self) -> Result> { + if self.mc_group_id_header.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + if self.time_out_periodicity.time_out > 15 { + return Err(anyhow!("Max time_out value is 15")); + } + + if self.time_out_periodicity.periodicity > 7 { + return Err(anyhow!("Max periodicity value is 7")); + } + + let mut b = Vec::with_capacity(10); + b.push(self.mc_group_id_header.mc_group_id); + b.extend_from_slice(&self.session_time.to_le_bytes()); + b.push((self.time_out_periodicity.periodicity << 4) | self.time_out_periodicity.time_out); + b.extend_from_slice(&(self.dl_frequ / 100).to_le_bytes()[0..3]); + b.push(self.dr); + + Ok(b) + } +} + +#[derive(Debug, PartialEq)] +pub struct McClassBSessionReqPayloadMcGroupIdHeader { + pub mc_group_id: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McClassBSessionReqPayloadTimeOutPeriodicity { + pub time_out: u8, + pub periodicity: u8, +} + +#[derive(Debug, PartialEq)] +pub struct McClassBSessionAnsPayload { + pub status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId, + pub time_to_start: Option, +} + +#[derive(Debug, PartialEq)] +pub struct McClassBSessionAnsPayloadStatusAndMcGroupId { + pub mc_group_id: u8, + pub dr_error: bool, + pub freq_error: bool, + pub mc_group_undefined: bool, +} + +impl PayloadCodec for McClassBSessionAnsPayload { + fn decode(b: &[u8]) -> Result { + if b.is_empty() { + return Err(anyhow!("At least 1 byte expected")); + } + + Ok(McClassBSessionAnsPayload { + status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId { + mc_group_id: b[0] & 0x03, + dr_error: b[0] & 0x04 != 0, + freq_error: b[0] & 0x08 != 0, + mc_group_undefined: b[0] & 0x10 != 0, + }, + time_to_start: if b[0] & 0x1c != 0 { + None + } else { + if b.len() != 4 { + return Err(anyhow!("Expected 4 bytes")); + } + + let mut bytes = [0; 4]; + bytes[0..3].copy_from_slice(&b[1..4]); + Some(u32::from_le_bytes(bytes)) + }, + }) + } + + fn encode(&self) -> Result> { + if self.status_and_mc_group_id.mc_group_id > 3 { + return Err(anyhow!("Max mc_group_id value is 3")); + } + + if let Some(v) = self.time_to_start { + if v >= (1 << 24) { + return Err(anyhow!("Max time_to_start is 1^24 - 1")); + } + } + + let mut b = Vec::with_capacity(4); + b.push(self.status_and_mc_group_id.mc_group_id); + + if self.status_and_mc_group_id.dr_error { + b[0] |= 0x04; + } + if self.status_and_mc_group_id.freq_error { + b[0] |= 0x08; + } + if self.status_and_mc_group_id.mc_group_undefined { + b[0] |= 0x10; + } + + if let Some(v) = self.time_to_start { + b.extend_from_slice(&v.to_le_bytes()[0..3]); + } + + Ok(b) + } +} + +#[cfg(test)] +mod test { + use super::*; + + struct CommandTest { + name: String, + uplink: bool, + command: Payload, + bytes: Vec, + expected_error: Option, + } + + #[test] + fn test_package_version_req() { + let encode_tests = [CommandTest { + name: "encode PackageVersionReq".into(), + uplink: false, + command: Payload::PackageVersionReq, + bytes: vec![0x00], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode PackageVersionReq".into(), + uplink: false, + command: Payload::PackageVersionReq, + bytes: vec![0x00], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_package_version_ans() { + let encode_tests = [CommandTest { + name: "encode PackageVersionAns".into(), + uplink: true, + command: Payload::PackageVersionAns(PackageVersionAnsPayload { + package_identifier: 1, + package_version: 1, + }), + bytes: vec![0x00, 0x01, 0x01], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode PackageVersionAns".into(), + uplink: true, + command: Payload::PackageVersionAns(PackageVersionAnsPayload { + package_identifier: 1, + package_version: 1, + }), + bytes: vec![0x00, 0x01, 0x01], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_status_req() { + let encode_tests = [CommandTest { + name: "encode McGroupStatusReq".into(), + uplink: false, + command: Payload::McGroupStatusReq(McGroupStatusReqPayload { + cmd_mask: McGroupStatusReqPayloadCmdMask { + req_group_mask: [true, true, false, false], + }, + }), + bytes: vec![0x01, 0x03], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupStatusReq".into(), + uplink: false, + command: Payload::McGroupStatusReq(McGroupStatusReqPayload { + cmd_mask: McGroupStatusReqPayloadCmdMask { + req_group_mask: [true, true, false, false], + }, + }), + bytes: vec![0x01, 0x03], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_status_ans() { + let encode_tests = [CommandTest { + name: "encode McGroupStatusAns".into(), + uplink: true, + command: Payload::McGroupStatusAns(McGroupStatusAnsPayload { + status: McGroupStatusAnsPayloadStatus { + ans_group_mask: [true, true, false, false], + nb_total_groups: 2, + }, + items: vec![ + McGroupStatusAnsPayloadItem { + mc_group_id: 0, + mc_addr: DevAddr::from_be_bytes([1, 2, 3, 4]), + }, + McGroupStatusAnsPayloadItem { + mc_group_id: 1, + mc_addr: DevAddr::from_be_bytes([2, 2, 3, 4]), + }, + ], + }), + bytes: vec![ + 0x01, 0x23, 0x00, 0x04, 0x03, 0x02, 0x01, 0x01, 0x04, 0x03, 0x02, 0x02, + ], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupStatusAns".into(), + uplink: true, + command: Payload::McGroupStatusAns(McGroupStatusAnsPayload { + status: McGroupStatusAnsPayloadStatus { + ans_group_mask: [true, true, false, false], + nb_total_groups: 2, + }, + items: vec![ + McGroupStatusAnsPayloadItem { + mc_group_id: 0, + mc_addr: DevAddr::from_be_bytes([1, 2, 3, 4]), + }, + McGroupStatusAnsPayloadItem { + mc_group_id: 1, + mc_addr: DevAddr::from_be_bytes([2, 2, 3, 4]), + }, + ], + }), + bytes: vec![ + 0x01, 0x23, 0x00, 0x04, 0x03, 0x02, 0x01, 0x01, 0x04, 0x03, 0x02, 0x02, + ], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_setup_req() { + let encode_tests = [CommandTest { + name: "encode McGroupSetupReq".into(), + uplink: false, + command: Payload::McGroupSetupReq(McGroupSetupReqPayload { + mc_group_id_header: McGroupSetupReqPayloadMcGroupIdHeader { mc_group_id: 2 }, + mc_addr: DevAddr::from_be_bytes([1, 2, 3, 4]), + mc_key_encrypted: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8], + min_mc_f_count: 1024, + max_mc_f_count: 2048, + }), + bytes: vec![ + 0x02, 0x02, 0x04, 0x03, 0x02, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, + ], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupSetupReq".into(), + uplink: false, + command: Payload::McGroupSetupReq(McGroupSetupReqPayload { + mc_group_id_header: McGroupSetupReqPayloadMcGroupIdHeader { mc_group_id: 2 }, + mc_addr: DevAddr::from_be_bytes([1, 2, 3, 4]), + mc_key_encrypted: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8], + min_mc_f_count: 1024, + max_mc_f_count: 2048, + }), + bytes: vec![ + 0x02, 0x02, 0x04, 0x03, 0x02, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, + ], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_setup_ans() { + let encode_tests = [CommandTest { + name: "encode McGroupSetupAns".into(), + uplink: true, + command: Payload::McGroupSetupAns(McGroupSetupAnsPayload { + mc_group_id_header: McGroupSetupAnsPayloadMcGroupIdHeader { + mc_group_id: 2, + id_error: true, + }, + }), + bytes: vec![0x02, 0x06], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupSetupAns".into(), + uplink: true, + command: Payload::McGroupSetupAns(McGroupSetupAnsPayload { + mc_group_id_header: McGroupSetupAnsPayloadMcGroupIdHeader { + mc_group_id: 2, + id_error: true, + }, + }), + bytes: vec![0x02, 0x06], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_delete_req() { + let encode_tests = [CommandTest { + name: "encode McGroupDeleteReq".into(), + uplink: false, + command: Payload::McGroupDeleteReq(McGroupDeleteReqPayload { + mc_group_id_header: McGroupDeleteReqPayloadMcGroupIdHeader { mc_group_id: 3 }, + }), + bytes: vec![0x03, 0x03], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupDeleteReq".into(), + uplink: false, + command: Payload::McGroupDeleteReq(McGroupDeleteReqPayload { + mc_group_id_header: McGroupDeleteReqPayloadMcGroupIdHeader { mc_group_id: 3 }, + }), + bytes: vec![0x03, 0x03], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_group_delete_ans() { + let encode_tests = [CommandTest { + name: "encode McGroupDeleteAns".into(), + uplink: true, + command: Payload::McGroupDeleteAns(McGroupDeleteAnsPayload { + mc_group_id_header: McGroupDeleteAnsPayloadMcGroupIdHeader { + mc_group_id: 3, + mc_group_undefined: true, + }, + }), + bytes: vec![0x03, 0x07], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McGroupDeleteAns".into(), + uplink: true, + command: Payload::McGroupDeleteAns(McGroupDeleteAnsPayload { + mc_group_id_header: McGroupDeleteAnsPayloadMcGroupIdHeader { + mc_group_id: 3, + mc_group_undefined: true, + }, + }), + bytes: vec![0x03, 0x07], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_class_c_session_req() { + let encode_tests = [CommandTest { + name: "encode McClassCSessionReq".into(), + uplink: false, + command: Payload::McClassCSessionReq(McClassCSessionReqPayload { + mc_group_id_header: McClassCSessionReqPayloadMcGroupIdHeader { mc_group_id: 2 }, + session_time: 1024, + session_time_out: McClassCSessionReqPayloadSessionTimeOut { time_out: 15 }, + dl_frequ: 868100000, + dr: 5, + }), + bytes: vec![ + 0x04, 0x02, 0x00, 0x04, 0x00, 0x00, 0x0f, 0x28, 0x76, 0x84, 0x05, + ], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McClassCSessionReq".into(), + uplink: false, + command: Payload::McClassCSessionReq(McClassCSessionReqPayload { + mc_group_id_header: McClassCSessionReqPayloadMcGroupIdHeader { mc_group_id: 2 }, + session_time: 1024, + session_time_out: McClassCSessionReqPayloadSessionTimeOut { time_out: 15 }, + dl_frequ: 868100000, + dr: 5, + }), + bytes: vec![ + 0x04, 0x02, 0x00, 0x04, 0x00, 0x00, 0x0f, 0x28, 0x76, 0x84, 0x05, + ], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_class_c_session_ans() { + let encode_tests = [ + CommandTest { + name: "encode McClassCSessionAns no error".into(), + uplink: true, + command: Payload::McClassCSessionAns(McClassCSessionAnsPayload { + status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId { + mc_group_id: 2, + dr_error: false, + freq_error: false, + mc_group_undefined: false, + }, + time_to_start: Some(1024), + }), + bytes: vec![0x04, 0x02, 0x00, 0x04, 0x00], + expected_error: None, + }, + CommandTest { + name: "encode McClassCSessionAns with error".into(), + uplink: true, + command: Payload::McClassCSessionAns(McClassCSessionAnsPayload { + status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId { + mc_group_id: 2, + dr_error: true, + freq_error: true, + mc_group_undefined: true, + }, + time_to_start: None, + }), + bytes: vec![0x04, 0x1e], + expected_error: None, + }, + ]; + + let decode_tests = [ + CommandTest { + name: "decode McClassCSessionAns no error".into(), + uplink: true, + command: Payload::McClassCSessionAns(McClassCSessionAnsPayload { + status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId { + mc_group_id: 2, + dr_error: false, + freq_error: false, + mc_group_undefined: false, + }, + time_to_start: Some(1024), + }), + bytes: vec![0x04, 0x02, 0x00, 0x04, 0x00], + expected_error: None, + }, + CommandTest { + name: "decode McClassCSessionAns with error".into(), + uplink: true, + command: Payload::McClassCSessionAns(McClassCSessionAnsPayload { + status_and_mc_group_id: McClassCSessionAnsPayloadStatusAnsMcGroupId { + mc_group_id: 2, + dr_error: true, + freq_error: true, + mc_group_undefined: true, + }, + time_to_start: None, + }), + bytes: vec![0x04, 0x1e], + expected_error: None, + }, + ]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_class_b_session_req() { + let encode_tests = [CommandTest { + name: "encode McClassBSessionReq".into(), + uplink: false, + command: Payload::McClassBSessionReq(McClassBSessionReqPayload { + mc_group_id_header: McClassBSessionReqPayloadMcGroupIdHeader { mc_group_id: 3 }, + session_time: 1024, + time_out_periodicity: McClassBSessionReqPayloadTimeOutPeriodicity { + time_out: 15, + periodicity: 4, + }, + dl_frequ: 868100000, + dr: 5, + }), + bytes: vec![ + 0x05, 0x03, 0x00, 0x04, 0x00, 0x00, 0x4f, 0x28, 0x76, 0x84, 0x05, + ], + expected_error: None, + }]; + + let decode_tests = [CommandTest { + name: "decode McClassBSessionReq".into(), + uplink: false, + command: Payload::McClassBSessionReq(McClassBSessionReqPayload { + mc_group_id_header: McClassBSessionReqPayloadMcGroupIdHeader { mc_group_id: 3 }, + session_time: 1024, + time_out_periodicity: McClassBSessionReqPayloadTimeOutPeriodicity { + time_out: 15, + periodicity: 4, + }, + dl_frequ: 868100000, + dr: 5, + }), + bytes: vec![ + 0x05, 0x03, 0x00, 0x04, 0x00, 0x00, 0x4f, 0x28, 0x76, 0x84, 0x05, + ], + expected_error: None, + }]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + #[test] + fn test_mc_class_b_session_ans() { + let encode_tests = [ + CommandTest { + name: "encode McClassBSessionAns no errors".into(), + uplink: true, + command: Payload::McClassBSessionAns(McClassBSessionAnsPayload { + status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId { + mc_group_id: 3, + dr_error: false, + freq_error: false, + mc_group_undefined: false, + }, + time_to_start: Some(1024), + }), + bytes: vec![0x05, 0x03, 0x00, 0x04, 0x00], + expected_error: None, + }, + CommandTest { + name: "encode McClassBSessionAns with errors".into(), + uplink: true, + command: Payload::McClassBSessionAns(McClassBSessionAnsPayload { + status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId { + mc_group_id: 3, + dr_error: true, + freq_error: true, + mc_group_undefined: true, + }, + time_to_start: None, + }), + bytes: vec![0x05, 0x1f], + expected_error: None, + }, + ]; + + let decode_tests = [ + CommandTest { + name: "decode McClassBSessionAns no errors".into(), + uplink: true, + command: Payload::McClassBSessionAns(McClassBSessionAnsPayload { + status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId { + mc_group_id: 3, + dr_error: false, + freq_error: false, + mc_group_undefined: false, + }, + time_to_start: Some(1024), + }), + bytes: vec![0x05, 0x03, 0x00, 0x04, 0x00], + expected_error: None, + }, + CommandTest { + name: "decode McClassBSessionAns with errors".into(), + uplink: true, + command: Payload::McClassBSessionAns(McClassBSessionAnsPayload { + status_and_mc_group_id: McClassBSessionAnsPayloadStatusAndMcGroupId { + mc_group_id: 3, + dr_error: true, + freq_error: true, + mc_group_undefined: true, + }, + time_to_start: None, + }), + bytes: vec![0x05, 0x1f], + expected_error: None, + }, + ]; + + run_tests_encode(&encode_tests); + run_tests_decode(&decode_tests); + } + + fn run_tests_encode(tests: &[CommandTest]) { + for tst in tests { + println!("> {}", tst.name); + let resp = tst.command.to_vec(); + if let Some(e) = &tst.expected_error { + assert!(resp.is_err()); + assert_eq!(e, &resp.err().unwrap().to_string()); + } else { + assert_eq!(tst.bytes, resp.unwrap()); + } + } + } + + fn run_tests_decode(tests: &[CommandTest]) { + for tst in tests { + println!("> {}", tst.name); + let resp = Payload::from_slice(tst.uplink, &tst.bytes); + if let Some(e) = &tst.expected_error { + assert!(resp.is_err()); + assert_eq!(e, &resp.err().unwrap().to_string()); + } else { + assert_eq!(tst.command, resp.unwrap()); + } + } + } +}