#[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>, altitude: Option<f32>, balloon: bool, ambient_temperature: Option<i8>, ambient_humidity: Option<u8>, ambient_pressure: Option<u16>, } 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 } /// dBi 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) } /// Higher precision altitude than what's available in the GPS whisker. Takes precedent if set. Useful for high altitude balloons. /// Meters pub fn altitude(&self) -> Option<f32> { self.altitude } /// If true, this whisker was emitted by a high altitude balloon payload pub fn is_balloon(&self) -> bool { self.balloon } /// Degrees C pub fn ambient_temperature(&self) -> Option<i8> { self.ambient_temperature } /// Relative humidity, percent pub fn ambient_humidity(&self) -> Option<f64> { self.ambient_humidity.map(|x| x as f64 / 2.55) } /// Decapascal (daPa) pub fn ambient_pressure(&self) -> Option<u16> { self.ambient_pressure } 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; } if let Some(x) = self.altitude { bitmask |= 512; buf.get_mut(i..(i + 4))?.copy_from_slice(&x.to_le_bytes()); i += 4; } if self.balloon { bitmask |= 1024; } if let Some(x) = self.ambient_temperature { bitmask |= 2048; *buf.get_mut(i)? = x.to_le_bytes()[0]; i += 1; } if let Some(x) = self.ambient_humidity { bitmask |= 4096; *buf.get_mut(i)? = x; i += 1; } if let Some(x) = self.ambient_pressure { bitmask |= 8192; buf.get_mut(i..(i + 2))?.copy_from_slice(&x.to_le_bytes()); i += 2; } 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.hardware_id = Some(u16::from_le_bytes([*data.get(i)?, *data.get(i + 1)?])); i += 2; } if bitmask & 2 > 0 { builder.software_id = Some(*data.get(i)?); i += 1; } if bitmask & 4 > 0 { builder.uptime = Some(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.antenna_height = Some(*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.xcvr_temperature = Some(i8::from_le_bytes([*data.get(i)?])); i += 1; } if bitmask & 256 > 0 { builder.battery_charge = Some(*data.get(i)?); i += 1; } if bitmask & 512 > 0 { builder.altitude = Some(f32::from_le_bytes( data.get(i..(i + 4))?.try_into().unwrap(), )); i += 4; } builder.balloon = bitmask & 1024 > 0; if bitmask & 2048 > 0 { builder.ambient_temperature = Some(i8::from_le_bytes([*data.get(i)?])); i += 1; } if bitmask & 4096 > 0 { builder.ambient_humidity = Some(*data.get(i)?); i += 1; } if bitmask & 8192 > 0 { builder.ambient_pressure = Some(u16::from_le_bytes( data.get(i..(i + 2))?.try_into().unwrap(), )); i += 2; } // prevent unused variable warning let _ = 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>, altitude: Option<f32>, balloon: bool, ambient_temperature: Option<i8>, ambient_humidity: Option<u8>, ambient_pressure: Option<u16>, } 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, altitude, balloon, ambient_temperature, ambient_humidity, ambient_pressure, } = self; NodeInfo { hardware_id, software_id, uptime, antenna_height, antenna_gain, tx_power, voltage, xcvr_temperature, battery_charge, altitude, balloon, ambient_temperature, ambient_humidity, ambient_pressure, } } 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 } /// dBi 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 } /// Higher precision altitude than what's available in the GPS whisker. Takes precedent if set. Useful for high altitude balloons. /// Meters pub fn altitude(mut self, val: f32) -> Self { self.altitude = Some(val); self } /// If true, this whisker was emitted by a high altitude balloon payload /// Please only set this on actual balloon payloads. Otherwise it can cause issues for downstream users! pub fn set_balloon(mut self) -> Self { self.balloon = true; self } /// Degrees C pub fn ambient_temperature(mut self, val: i8) -> Self { self.ambient_temperature = Some(val); self } /// Relative humidity, percent pub fn ambient_humidity(mut self, val: f64) -> Self { self.ambient_humidity = Some((val * 2.55).min(255.0) as u8); self } /// Decapascal (daPa) pub fn ambient_pressure(mut self, val: u16) -> Self { self.ambient_pressure = Some(val); 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 ); } }