diff --git a/src/packet.rs b/src/packet.rs index 12288e7bb9c680ce6be2258126f1fb2038070491..8f134ede4dcd67ec02fecd307ba7fb0c2eb822b8 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -7,8 +7,8 @@ use crate::{ error::{CommentError, DecodeError, DigipeatError, EncodeError, PacketRouteAppendError}, interleaver, ldpc, utf8, whisker::{ - Arbitrary, Comment, Destination, Gps, Identification, Route, RouteNode, Timestamp, - ValidatedWhiskerIter, Whisker, WhiskerIter, COMMENT_TYPE, + Arbitrary, Comment, Destination, Gps, Identification, NodeInfo, Route, RouteNode, + Timestamp, ValidatedWhiskerIter, Whisker, WhiskerIter, COMMENT_TYPE, }, whitener, }; @@ -18,15 +18,15 @@ const X25: Crc<u16> = Crc::<u16>::new(&CRC_16_IBM_SDLC); macro_rules! uniq_whisker { ($t:meta) => { ::paste::paste! { - pub fn [<$t:lower>](&self) -> Option<$t> { + pub fn [<$t:snake:lower>](&self) -> Option<$t> { self.iter().find_map(|w| match w { Whisker::$t(x) => Some(x), _ => None, }) } - pub fn [<add_ $t:lower>](&mut self, w: $t) -> Result<(), EncodeError> { - if self.[<$t:lower>]().is_some() { + pub fn [<add_ $t:snake:lower>](&mut self, w: $t) -> Result<(), EncodeError> { + if self.[<$t:snake:lower>]().is_some() { return Err(EncodeError::DuplicateData); } @@ -35,7 +35,7 @@ macro_rules! uniq_whisker { // safe to unwrap since we know we have enough space let out = w.encode(&mut buf).unwrap(); - data.try_push(crate::whisker::[<$t:upper _TYPE>]).ok().ok_or(EncodeError::CatsOverflow)?; + data.try_push(crate::whisker::[<$t:snake:upper _TYPE>]).ok().ok_or(EncodeError::CatsOverflow)?; data.try_extend_from_slice(out).ok().ok_or(EncodeError::CatsOverflow)?; Ok(()) @@ -44,8 +44,8 @@ macro_rules! uniq_whisker { Ok(()) } - pub fn [<clear_ $t:lower>](&mut self) { - self.clear_by_type(crate::whisker::[<$t:upper _TYPE>], false); + pub fn [<clear_ $t:snake:lower>](&mut self) { + self.clear_by_type(crate::whisker::[<$t:snake:upper _TYPE>], false); } } }; @@ -201,6 +201,7 @@ impl<'a, const N: usize> Packet<'a, N> { uniq_whisker!(Timestamp); uniq_whisker!(Gps); uniq_whisker!(Route); + uniq_whisker!(NodeInfo); poly_whisker!(Destination); poly_whisker!(Arbitrary); @@ -392,6 +393,8 @@ fn try_lock<'a, const N: usize, T, E, F: Fn(&mut Buffer<'a, N>) -> Result<T, E>> mod tests { use arrayvec::ArrayString; + use crate::whisker::NodeInfoBuilder; + use super::*; #[test] @@ -517,6 +520,80 @@ mod tests { assert_eq!(comment, packet2.comment(&mut buf).unwrap()); } + #[test] + fn node_info_e2e() { + let mut buf = [0; 4096]; + let mut packet = Packet::new(&mut buf); + packet + .add_node_info( + NodeInfoBuilder::default() + .hardware_id(0xBEEF) + .software_id(0xBC) + .uptime(2304) + .antenna_height(5) + .antenna_gain(3.0) + .tx_power(30.0) + .voltage(12.6) + .xcvr_temperature(-15) + .battery_charge(65.0) + .build(), + ) + .unwrap(); + + let mut buf2 = [0; 4096]; + let mut encoded = Buffer::new_empty(&mut buf2); + packet.fully_encode(&mut encoded).unwrap(); + + let mut buf3 = [0; 4096]; + let packet2 = Packet::fully_decode(&encoded[2..], &mut buf3).unwrap(); + + let node_info = packet2.node_info().unwrap(); + assert_eq!(0xBEEF, node_info.hardware_id().unwrap()); + assert_eq!(0xBC, node_info.software_id().unwrap()); + assert_eq!(2304, node_info.uptime().unwrap()); + assert_eq!(5, node_info.antenna_height().unwrap()); + assert_eq!(3.0, node_info.antenna_gain().unwrap()); + assert_eq!(30.0, node_info.tx_power().unwrap()); + assert_eq!(12.6, node_info.voltage().unwrap()); + assert_eq!(-15, node_info.xcvr_temperature().unwrap()); + assert_eq!(64.70588235294117, node_info.battery_charge().unwrap()); + } + + #[test] + fn node_info_e2e_some_unpopulated() { + let mut buf = [0; 4096]; + let mut packet = Packet::new(&mut buf); + packet + .add_node_info( + NodeInfoBuilder::default() + .software_id(0xBC) + .uptime(2304) + .antenna_gain(3.0) + .voltage(12.6) + .xcvr_temperature(-15) + .build(), + ) + .unwrap(); + + let mut buf2 = [0; 4096]; + let mut encoded = Buffer::new_empty(&mut buf2); + packet.fully_encode(&mut encoded).unwrap(); + + let mut buf3 = [0; 4096]; + let packet2 = Packet::fully_decode(&encoded[2..], &mut buf3).unwrap(); + + let node_info = packet2.node_info().unwrap(); + assert_eq!(None, node_info.hardware_id()); + assert_eq!(0xBC, node_info.software_id().unwrap()); + assert_eq!(2304, node_info.uptime().unwrap()); + assert_eq!(None, node_info.antenna_height()); + assert_eq!(3.0, node_info.antenna_gain().unwrap()); + assert_eq!(None, node_info.tx_power()); + assert_eq!(12.6, node_info.voltage().unwrap()); + assert_eq!(-15, node_info.xcvr_temperature().unwrap()); + assert_eq!(None, node_info.battery_charge()); + } + #[test] fn fully_decode_fuzz_tests() { let data = [ diff --git a/src/whisker/mod.rs b/src/whisker/mod.rs index b32ea3a475be13247fedd713896166226dd50839..48142250ae789ea6af28f0404cbd2a2d3b4414d0 100644 --- a/src/whisker/mod.rs +++ b/src/whisker/mod.rs @@ -3,6 +3,7 @@ mod comment; mod destination; mod gps; mod identification; +mod node_info; mod route; mod timestamp; mod unknown; @@ -15,6 +16,7 @@ pub use self::{ destination::Destination, gps::Gps, identification::Identification, + node_info::{NodeInfo, NodeInfoBuilder}, route::{Route, RouteIdentity, RouteIter, RouteNode}, timestamp::Timestamp, unknown::Unknown, @@ -27,6 +29,7 @@ pub(crate) const COMMENT_TYPE: u8 = 0x03; pub(crate) const ROUTE_TYPE: u8 = 0x04; pub(crate) const DESTINATION_TYPE: u8 = 0x05; pub(crate) const ARBITRARY_TYPE: u8 = 0x06; +pub(crate) const NODE_INFO_TYPE: u8 = 0x09; #[derive(Debug)] pub enum Whisker { @@ -37,6 +40,7 @@ pub enum Whisker { Route(Route), Destination(Destination), Arbitrary(Arbitrary), + NodeInfo(NodeInfo), Unknown(Unknown), } @@ -137,6 +141,9 @@ impl<'a> WhiskerIter<'a> { ARBITRARY_TYPE => Whisker::Arbitrary( Arbitrary::decode(data).ok_or(DecodeError::MalformedWhisker { position })?, ), + NODE_INFO_TYPE => Whisker::NodeInfo( + NodeInfo::decode(data).ok_or(DecodeError::MalformedWhisker { position })?, + ), // safe to unwrap because we know len has to be 255 or less, since it's a u8 whisker_type => { diff --git a/src/whisker/node_info.rs b/src/whisker/node_info.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac4d689b4201f0eb16f136913916bde9101750c5 --- /dev/null +++ b/src/whisker/node_info.rs @@ -0,0 +1,321 @@ +#[derive(Copy, Clone, Debug)] +pub struct NodeInfo { + hardware_id: Option<u16>, + software_id: Option<u8>, + uptime: Option<u32>, + antenna_height: Option<u8>, + antenna_gain: Option<u8>, + tx_power: Option<u8>, + voltage: Option<u8>, + xcvr_temperature: Option<i8>, + battery_charge: Option<u8>, +} + +impl NodeInfo { + pub fn builder() -> NodeInfoBuilder { + NodeInfoBuilder::default() + } + + pub fn hardware_id(&self) -> Option<u16> { + self.hardware_id + } + + pub fn software_id(&self) -> Option<u8> { + self.software_id + } + + /// Seconds + pub fn uptime(&self) -> Option<u32> { + self.uptime + } + + /// Meters + pub fn antenna_height(&self) -> Option<u8> { + self.antenna_height + } + + /// dBd + pub fn antenna_gain(&self) -> Option<f64> { + self.antenna_gain.map(|g| g as f64 / 4.0) + } + + /// dBm + pub fn tx_power(&self) -> Option<f64> { + self.tx_power.map(|p| p as f64 / 4.0) + } + + /// Volts + pub fn voltage(&self) -> Option<f64> { + self.voltage.map(|v| v as f64 / 10.0) + } + + /// Degrees C + pub fn xcvr_temperature(&self) -> Option<i8> { + self.xcvr_temperature + } + + /// Percent + pub fn battery_charge(&self) -> Option<f64> { + self.battery_charge.map(|b| b as f64 / 2.55) + } + + pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> { + let mut bitmask: u32 = 0; + + let mut i = 4; + + // TODO these could be macros + if let Some(x) = self.hardware_id { + bitmask |= 1; + + buf.get_mut(i..(i + 2))?.copy_from_slice(&x.to_le_bytes()); + i += 2; + } + + if let Some(x) = self.software_id { + bitmask |= 2; + + *buf.get_mut(i)? = x; + i += 1; + } + + if let Some(x) = self.uptime { + bitmask |= 4; + + buf.get_mut(i..(i + 4))?.copy_from_slice(&x.to_le_bytes()); + i += 4; + } + + if let Some(x) = self.antenna_height { + bitmask |= 8; + + *buf.get_mut(i)? = x; + i += 1; + } + + if let Some(x) = self.antenna_gain { + bitmask |= 16; + + *buf.get_mut(i)? = x; + i += 1; + } + + if let Some(x) = self.tx_power { + bitmask |= 32; + + *buf.get_mut(i)? = x; + i += 1; + } + + if let Some(x) = self.voltage { + bitmask |= 64; + + *buf.get_mut(i)? = x; + i += 1; + } + + if let Some(x) = self.xcvr_temperature { + bitmask |= 128; + + *buf.get_mut(i)? = x.to_le_bytes()[0]; + i += 1; + } + + if let Some(x) = self.battery_charge { + bitmask |= 256; + + *buf.get_mut(i)? = x; + i += 1; + } + + buf[0] = (i - 1).try_into().ok()?; + buf[1..4].copy_from_slice(&bitmask.to_be_bytes()[1..]); + + Some(&buf[..i]) + } + + pub fn decode(data: &[u8]) -> Option<Self> { + let bitmask = u32::from_be_bytes([0, *data.get(1)?, *data.get(2)?, *data.get(3)?]); + + let mut builder = NodeInfoBuilder::default(); + let mut i = 4; + + if bitmask & 1 > 0 { + builder = builder.hardware_id(u16::from_le_bytes([*data.get(i)?, *data.get(i + 1)?])); + i += 2; + } + + if bitmask & 2 > 0 { + builder = builder.software_id(*data.get(i)?); + i += 1; + } + + if bitmask & 4 > 0 { + builder = builder.uptime(u32::from_le_bytes([ + *data.get(i)?, + *data.get(i + 1)?, + *data.get(i + 2)?, + *data.get(i + 3)?, + ])); + + i += 4; + } + + if bitmask & 8 > 0 { + builder = builder.antenna_height(*data.get(i)?); + i += 1; + } + + if bitmask & 16 > 0 { + builder.antenna_gain = Some(*data.get(i)?); + i += 1; + } + + if bitmask & 32 > 0 { + builder.tx_power = Some(*data.get(i)?); + i += 1; + } + + if bitmask & 64 > 0 { + builder.voltage = Some(*data.get(i)?); + i += 1; + } + + if bitmask & 128 > 0 { + builder = builder.xcvr_temperature(i8::from_le_bytes([*data.get(i)?])); + i += 1; + } + + if bitmask & 256 > 0 { + builder.battery_charge = Some(*data.get(i)?); + } + + Some(builder.build()) + } +} + +#[derive(Default, Copy, Clone)] +pub struct NodeInfoBuilder { + hardware_id: Option<u16>, + software_id: Option<u8>, + uptime: Option<u32>, + antenna_height: Option<u8>, + antenna_gain: Option<u8>, + tx_power: Option<u8>, + voltage: Option<u8>, + xcvr_temperature: Option<i8>, + battery_charge: Option<u8>, +} + +impl NodeInfoBuilder { + pub fn build(self) -> NodeInfo { + let NodeInfoBuilder { + hardware_id, + software_id, + uptime, + antenna_height, + antenna_gain, + tx_power, + voltage, + xcvr_temperature, + battery_charge, + } = self; + + NodeInfo { + hardware_id, + software_id, + uptime, + antenna_height, + antenna_gain, + tx_power, + voltage, + xcvr_temperature, + battery_charge, + } + } + + pub fn hardware_id(mut self, val: u16) -> Self { + self.hardware_id = Some(val); + + self + } + + pub fn software_id(mut self, val: u8) -> Self { + self.software_id = Some(val); + + self + } + + /// Seconds + pub fn uptime(mut self, val: u32) -> Self { + self.uptime = Some(val); + + self + } + + /// Meters + pub fn antenna_height(mut self, val: u8) -> Self { + self.antenna_height = Some(val); + + self + } + + /// dBd + pub fn antenna_gain(mut self, val: f64) -> Self { + self.antenna_gain = Some((val * 4.0).min(255.0) as u8); + + self + } + + /// dBm + pub fn tx_power(mut self, val: f64) -> Self { + self.tx_power = Some((val * 4.0).min(255.0) as u8); + + self + } + + /// Volts + pub fn voltage(mut self, val: f64) -> Self { + self.voltage = Some((val * 10.0).min(255.0) as u8); + + self + } + + /// Degrees C + pub fn xcvr_temperature(mut self, val: i8) -> Self { + self.xcvr_temperature = Some(val); + + self + } + + /// Percent + pub fn battery_charge(mut self, val: f64) -> Self { + self.battery_charge = Some((val * 2.55).min(255.0) as u8); + + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // verify examples in the standard doc + #[test] + fn node_info_doc_examples() { + let node_info = NodeInfoBuilder::default() + .hardware_id(7408) + .uptime(98) + .tx_power(30.0) + .voltage(12.8) + .build(); + + let mut buf = [0; 255]; + let encoded = node_info.encode(&mut buf).unwrap(); + + assert_eq!( + [0x0B, 0x00, 0x00, 0x65, 0xF0, 0x1C, 0x62, 0x00, 0x00, 0x00, 0x78, 0x80], + encoded + ); + } +}