Skip to content
Snippets Groups Projects
node_info.rs 10.7 KiB
Newer Older
Stephen D's avatar
Stephen D committed
#[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>,
Stephen D's avatar
Stephen D committed
}

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
    }

Stephen D's avatar
Stephen D committed
    /// dBi
Stephen D's avatar
Stephen D committed
    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
    }

Stephen D's avatar
Stephen D committed
    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;
        }

Stephen D's avatar
Stephen D committed
        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)?]));
Stephen D's avatar
Stephen D committed
            i += 2;
        }

        if bitmask & 2 > 0 {
            builder.software_id = Some(*data.get(i)?);
Stephen D's avatar
Stephen D committed
            i += 1;
        }

        if bitmask & 4 > 0 {
            builder.uptime = Some(u32::from_le_bytes([
Stephen D's avatar
Stephen D committed
                *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)?);
Stephen D's avatar
Stephen D committed
            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)?]));
Stephen D's avatar
Stephen D committed
            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;

Stephen D's avatar
Stephen D committed
        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>,
Stephen D's avatar
Stephen D committed
}

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,
Stephen D's avatar
Stephen D committed
        } = 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,
Stephen D's avatar
Stephen D committed
        }
    }

    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
    }

Stephen D's avatar
Stephen D committed
    /// dBi
Stephen D's avatar
Stephen D committed
    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
    }
Stephen D's avatar
Stephen D committed
}

#[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
        );
    }
}