Skip to content
Snippets Groups Projects
whisker.rs 17.9 KiB
Newer Older
Stephen D's avatar
Stephen D committed
use core::str::FromStr;

Stephen D's avatar
wip
Stephen D committed
use arrayvec::{ArrayString, ArrayVec};
Stephen D's avatar
Stephen D committed
use half::f16;
Stephen D's avatar
wip
Stephen D committed

pub enum Whisker {
    Identification(Identification),
    Timestamp(Timestamp),
    Gps(Gps),
    Comment(Comment),
    Route(Route),
    Destination(Destination),
}

#[derive(Debug, PartialEq, Eq)]
pub struct Identification {
    pub callsign: ArrayString<254>,
    pub ssid: u8,
}

impl Identification {
    pub fn new(call: &str, ssid: u8) -> Option<Self> {
        let callsign = ArrayString::from(call).ok()?;

        Some(Self { callsign, ssid })
    }

    /// Returns none if there is not enough space in the buffer
    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        // safe to unwrap because we know the length is <= 254
        let len: u8 = self.callsign.as_bytes().len().try_into().unwrap();
        *buf.get_mut(0)? = len + 1;

        let mut len = 1;
        for (i, b) in self.callsign.as_bytes().iter().enumerate() {
            *buf.get_mut(i + 1)? = *b;
            len += 1
        }

        *buf.get_mut(len)? = self.ssid;
        len += 1;

        Some(&buf[0..len])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len = (*data.first()?).into();

        let callsign = core::str::from_utf8(data.get(1..len)?).ok()?;
        let callsign = ArrayString::from(callsign).ok()?;
        let ssid = *data.get(len)?;

        Some(Self { callsign, ssid })
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct Timestamp(u64);

impl Timestamp {
    /// Returns none if the given unix timestamp doesn't fit in 5 bytes
    pub fn new(unix_time: u64) -> Option<Self> {
        // must fit in 5 bytes
        if unix_time > (1 << (5 * 8)) - 1 {
            return None;
        }

        Some(Self(unix_time))
    }

    pub fn unix_time(&self) -> u64 {
        self.0
    }

    /// Returns none if there is not enough space in the buffer
    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        if buf.len() < 6 {
            return None;
        }

        buf[0] = 5;
        buf[1..6].copy_from_slice(&self.0.to_le_bytes()[0..5]);

        Some(&buf[0..6])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        if data.len() < 6 {
            return None;
        }
        if data[0] != 5 {
            return None;
        }

        let mut extended_data = [0; 8];
        extended_data[0..5].copy_from_slice(&data[1..6]);

        Some(Self(u64::from_le_bytes(extended_data)))
    }
}

Stephen D's avatar
Stephen D committed
#[derive(Debug, PartialEq)]
Stephen D's avatar
wip
Stephen D committed
pub struct Gps {
    latitude: i32,
    longitude: i32,
Stephen D's avatar
Stephen D committed
    pub altitude: f16,
    pub max_error: u8,
Stephen D's avatar
wip
Stephen D committed
    heading: u8,
Stephen D's avatar
Stephen D committed
    pub speed: f16,
Stephen D's avatar
wip
Stephen D committed
}

impl Gps {
Stephen D's avatar
Stephen D committed
    /// Constructs a new GPS whisker.
    ///
    /// latitude: degrees
    /// longitude: degrees
    /// altitude: meters
    /// max_error: maximum error distance from specified lat/lon/alt, meters
    /// heading: direction relative to north, degrees
    /// speed: meters per second
    pub fn new(
        latitude: f64,
        longitude: f64,
        altitude: f16,
        max_error: u8,
        heading: f64,
        speed: f16,
    ) -> Self {
        let latitude = latitude.clamp(-89.999, 89.999);
        let latitude = (latitude * ((1u32 << 31) as f64) / 90.0) as i32;

        let longitude = longitude.clamp(-179.999, 179.999);
        let longitude = (longitude * ((1u32 << 31) as f64) / 180.0) as i32;

Stephen D's avatar
Stephen D committed
        let heading = heading.clamp(0.0, 359.999);
Stephen D's avatar
Stephen D committed
        let heading = (heading * 128.0 / 360.0) as u8;

        Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        }
    }

    pub fn latitude(&self) -> f64 {
        (self.latitude as f64) / (1u32 << 31) as f64 * 90.0
    }

    pub fn longitude(&self) -> f64 {
        (self.longitude as f64) / (1u32 << 31) as f64 * 180.0
    }

    pub fn heading(&self) -> f64 {
        self.heading as f64 / 128.0 * 360.0
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let buf = buf.get_mut(0..15)?;
        buf[0] = 14;
        buf[1..5].copy_from_slice(&self.latitude.to_le_bytes());
        buf[5..9].copy_from_slice(&self.longitude.to_le_bytes());
        buf[9..11].copy_from_slice(&self.altitude.to_le_bytes());
        buf[11] = self.max_error;
        buf[12] = self.heading;
        buf[13..15].copy_from_slice(&self.speed.to_le_bytes());

        Some(buf)
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let data = data.get(0..15)?;
        if data[0] != 14 {
            return None;
        }
        let latitude = i32::from_le_bytes(data[1..5].try_into().unwrap());
        let longitude = i32::from_le_bytes(data[5..9].try_into().unwrap());
        let altitude = f16::from_le_bytes(data[9..11].try_into().unwrap());
        let max_error = data[11];
        let heading = data[12];
        let speed = f16::from_le_bytes(data[13..15].try_into().unwrap());

        Some(Self {
            latitude,
            longitude,
            altitude,
            max_error,
            heading,
            speed,
        })
    }
Stephen D's avatar
wip
Stephen D committed
}

pub struct Comment(ArrayVec<u8, 255>);

Stephen D's avatar
Stephen D committed
impl Comment {
    pub fn new(data: &[u8]) -> Option<Self> {
        let mut av = ArrayVec::new();
        av.try_extend_from_slice(data).ok()?;

        Some(Self(av))
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let len = self.0.len();

        // length must be <= 255, so this is safe
        *buf.get_mut(0)? = len.try_into().unwrap();
        buf.get_mut(1..(len + 1))?.copy_from_slice(&self.0);

        Some(&buf[0..(len + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let content = data.get(1..(len + 1))?;

        Self::new(content)
    }
}
Stephen D's avatar
Stephen D committed

Stephen D's avatar
Stephen D committed
#[derive(Debug, PartialEq, Eq)]
Stephen D's avatar
wip
Stephen D committed
pub struct Route {
Stephen D's avatar
Stephen D committed
    pub max_hops: u8,
Stephen D's avatar
Stephen D committed
    path: ArrayVec<u8, 254>,
Stephen D's avatar
Stephen D committed
    has_future: bool,
}

impl Route {
    pub fn new(max_hops: u8) -> Self {
        let path = ArrayVec::new();

        Self {
            max_hops,
            path,
            has_future: false,
        }
    }

    /// Returns `None` if there isn't enough space for the callsign
    /// Returns `None` if attempting to pushed is_used=true after an existing is_used=false
    /// is_future=false when this callsign has digipeated
    /// is_future=true when this callsign is expected to be part of the route, but isn't yet
    /// see the specs for more information
    pub fn push_callsign(&mut self, callsign: &str, ssid: u8, is_future: bool) -> Option<()> {
        let len = callsign.as_bytes().len() + 2;
        let free_space = self.path.capacity() - self.path.len();

        if len > free_space {
            return None;
        }

        self.has_future = self.has_future || is_future;
        if self.has_future && !is_future {
            return None;
        }

        // safe to unwrap since we already did a length check
        self.path
            .try_extend_from_slice(callsign.as_bytes())
            .unwrap();
        self.path.push(if is_future { 0xFD } else { 0xFF });
        self.path.push(ssid);

        Some(())
    }

    /// Returns `None` if there isn't enough space
    pub fn push_internet(&mut self) -> Option<()> {
        let free_space = self.path.capacity() - self.path.len();

        if free_space < 1 {
            return None;
        }

        self.path.push(0xFE);

        Some(())
    }

    pub fn iter(&'_ self) -> RouteIter<'_> {
        RouteIter::new(self)
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let packet_len = self.path.len() + 1;
        let buf = buf.get_mut(0..(packet_len + 1))?;
        buf[0] = packet_len.try_into().unwrap();
        buf[1] = self.max_hops;
        buf[2..(packet_len + 1)].copy_from_slice(&self.path);

        Some(buf)
    }

Stephen D's avatar
Stephen D committed
    // TODO reject packets that have a 0xFF anywhere after an FD
Stephen D's avatar
Stephen D committed
    pub fn decode(data: &[u8]) -> Option<Self> {
        let len: usize = (*data.first()?).into();
        let data = data.get(1..(len + 1))?;

        let max_hops = data[0];

        let mut path = ArrayVec::new();
        path.try_extend_from_slice(&data[1..]).unwrap();

        let has_future = data[1..].iter().any(|x| *x == 0xFD);

        Some(Self {
            max_hops,
            path,
            has_future,
        })
    }
}

#[derive(Debug, Eq, PartialEq)]
pub enum RouteNode<'a> {
    Internet,
    Identity(&'a str, u8, bool),
}

#[derive(Debug)]
pub struct RouteIter<'a> {
    route: &'a Route,
    i: usize,
}

impl<'a> RouteIter<'a> {
    fn new(route: &'a Route) -> Self {
        Self { route, i: 0 }
    }
}

impl<'a> Iterator for RouteIter<'a> {
    type Item = RouteNode<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i == self.route.path.len() {
            return None;
        }

        let i_start = self.i;
        self.i += 1;

        if self.route.path[i_start] == 0xFE {
            return Some(RouteNode::Internet);
        }

        while self.route.path[self.i] != 0xFD && self.route.path[self.i] != 0xFF {
            self.i += 1;
        }

        let callsign = core::str::from_utf8(&self.route.path[i_start..self.i]).unwrap();
        let is_future = self.route.path[self.i] == 0xFD;
        self.i += 1;
        let ssid = self.route.path[self.i];
        self.i += 1;

        Some(RouteNode::Identity(callsign, ssid, is_future))
    }
Stephen D's avatar
wip
Stephen D committed
}

Stephen D's avatar
Stephen D committed
#[derive(Debug, PartialEq, Eq)]
Stephen D's avatar
wip
Stephen D committed
pub struct Destination {
    ack: u8,
Stephen D's avatar
Stephen D committed
    callsign: ArrayString<253>,
Stephen D's avatar
wip
Stephen D committed
    ssid: u8,
}

Stephen D's avatar
Stephen D committed
impl Destination {
    /// Returns none if the ack number is > 127
    /// Returns none if the dest callsign is too long
    /// Returns none is is_ack is true and ack_num is 0
    pub fn new(is_ack: bool, ack_num: u8, dest_callsign: &str, dest_ssid: u8) -> Option<Self> {
        if ack_num > 127 {
            return None;
        }
        if is_ack && ack_num == 0 {
            return None;
        }
        let ack = if is_ack { (1 << 7) | ack_num } else { ack_num };
        let callsign = ArrayString::from_str(dest_callsign).ok()?;
        let ssid = dest_ssid;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }

    pub fn is_ack(&self) -> bool {
        self.ack & (1 << 7) > 0
    }

    pub fn ack_num(&self) -> u8 {
        self.ack
    }

    pub fn callsign(&self) -> &str {
        self.callsign.as_str()
    }

    pub fn ssid(&self) -> u8 {
        self.ssid
    }

    pub fn encode<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> {
        let n = self.callsign.len() + 2;
        *buf.get_mut(0)? = n.try_into().unwrap();
        *buf.get_mut(1)? = self.ack;
        buf.get_mut(2..n)?.copy_from_slice(self.callsign.as_bytes());
        *buf.get_mut(n)? = self.ssid;

        Some(&buf[0..(n + 1)])
    }

    pub fn decode(data: &[u8]) -> Option<Self> {
        let n = data.first()?;
        let call_length: usize = n.checked_sub(2)?.into();

        let ack = *data.get(1)?;
        if ack == 0b10000000 {
            // is_ack is true and # is 0
            return None;
        }

        let callsign = core::str::from_utf8(data.get(2..(call_length + 2))?).ok()?;
        let callsign = ArrayString::from_str(callsign).ok()?;

        let ssid = *data.get(call_length + 2)?;

        Some(Self {
            ack,
            callsign,
            ssid,
        })
    }
}

Stephen D's avatar
wip
Stephen D committed
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ident_e2e() {
        let call = "VE9ABCDEFGZZ4839-???";
        let ssid = 17;

        let ident = Identification::new(call, ssid).unwrap();
        let mut buf = [0; 256];

        let encoded = ident.encode(&mut buf).unwrap();
        let decoded = Identification::decode(encoded).unwrap();
        assert_eq!(ident, decoded);
        assert_eq!(call, &decoded.callsign);
        assert_eq!(ssid, decoded.ssid);
    }

    #[test]
    fn new_timestamp() {
        let t = 34894;
        assert_eq!(t, Timestamp::new(t).unwrap().unix_time());

        let t = 1 << (5 * 8);
        assert_eq!(None, Timestamp::new(t));
    }

    #[test]
    fn timestamp_e2e() {
        let t = 34894;
        let timestamp = Timestamp::new(t).unwrap();

        let mut buf = [0; 256];
        let encoded = timestamp.encode(&mut buf).unwrap();
        let decoded = Timestamp::decode(encoded).unwrap();
        assert_eq!(timestamp, decoded);
        assert_eq!(t, timestamp.unix_time());
    }
Stephen D's avatar
Stephen D committed

    #[test]
    fn new_gps() {
        let gps = Gps::new(
            90.0,
            -180.0,
            f16::from_f32(45.02),
            24,
            123.45,
            f16::from_f32(12.3),
        );

        assert_eq!(89.99899999704212, gps.latitude());
        assert_eq!(-179.9989999551326, gps.longitude());
        assert_eq!(120.9375, gps.heading());
    }

Stephen D's avatar
Stephen D committed
    #[test]
    fn gps_max_heading() {
        let gps = Gps::new(
            4.0,
            23.45,
            f16::from_f32(45.02),
            24,
            360.0,
            f16::from_f32(12.3),
        );

        assert_eq!(357.1875, gps.heading());
    }

Stephen D's avatar
Stephen D committed
    #[test]
    fn gps_e2e() {
        let gps = Gps::new(
            90.0,
            -180.0,
            f16::from_f32(45.02),
            24,
            123.45,
            f16::from_f32(12.3),
        );

        let mut buf = [0; 256];
        let encoded = gps.encode(&mut buf).unwrap();
        let decoded = Gps::decode(encoded).unwrap();
        assert_eq!(gps, decoded);
    }
Stephen D's avatar
Stephen D committed

    #[test]
    fn comment_e2e() {
        let data = b"Hello world! This is an example comment";
        let comment = Comment::new(data).unwrap();
        let mut buf = [0; 256];
        let encoded = comment.encode(&mut buf).unwrap();
        let decoded = Comment::decode(encoded).unwrap();
        let decoded2 = Comment::decode(&buf).unwrap();
        assert_eq!(data[..], decoded.0[..]);
        assert_eq!(data[..], decoded2.0[..]);
    }
Stephen D's avatar
Stephen D committed

    #[test]
    fn route_push_and_iter() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_callsign("VE9AAAAA", 94, true).unwrap();
        assert!(route.push_callsign("VE9AAAAA", 94, false).is_none());

        // too long
        assert!(route
            .push_callsign(
                "lsdfjslkdfjlksdjflksfjsdklfjsdklfjsdklfjsdklfjsklfsef;jklsdfjkl;sdf;klsdf;klsjdfJSDJFSKL:DFJDSL:KFskldfj;slkdfjsdkl;fjdskl;fjsdfl;kjsdfl;ksdjfkl;ssdfl;kjsdfl;ksdjf;sdklsd;lfkjsdlfk;jsdl;fkjsd;klfjsd;fljsf;oidfjgwper0tujdfgndfjkl;gjnergjol;kehfgo;dijge;oghdfkl;gjdfkl;gjdeior;lgjedr;ioghjdorighndeklo;grjiop[",
                20,
                true,
            ).is_none());

        route
            .push_callsign("This is the last callsign", 0, true)
            .unwrap();
        route.push_internet().unwrap();

        let mut iter = route.iter();
        assert_eq!(
            RouteNode::Identity("VE2XYZ", 23, false),
            iter.next().unwrap()
        );
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(
            RouteNode::Identity("VE9AAAAA", 94, true),
            iter.next().unwrap()
        );
        assert_eq!(
            RouteNode::Identity("This is the last callsign", 0, true),
            iter.next().unwrap()
        );
        assert_eq!(RouteNode::Internet, iter.next().unwrap());
        assert_eq!(None, iter.next());

        assert_eq!(34, route.max_hops);
    }

    #[test]
    fn route_e2e() {
        let mut route = Route::new(34);
        route.push_callsign("VE2XYZ", 23, false).unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_internet().unwrap();
        route.push_callsign("VE9AAAAA", 94, true).unwrap();
        route
            .push_callsign("This is the last callsign", 0, true)
            .unwrap();
        route.push_internet().unwrap();

        let mut buf = [0; 256];
        let encoded = route.encode(&mut buf).unwrap();
        let decoded = Route::decode(encoded).unwrap();
        assert_eq!(route, decoded);
    }

    // verify examples in the standard doc
    #[test]
    fn route_doc_examples() {
        let mut ex1 = Route::new(4);
        ex1.push_callsign("VE1ABC", 0, false);
        ex1.push_callsign("VE2DEF", 234, false);
        ex1.push_callsign("VE3XYZ", 14, false);

        let mut buf = [0; 256];
        let encoded = ex1.encode(&mut buf).unwrap();
        assert_eq!(
            &[
                0x19, 0x04, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0x56, 0x45, 0x32, 0x44,
                0x45, 0x46, 0xFF, 0xEA, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A, 0xFF, 0x0E
            ],
            &encoded
        );

        let mut ex2 = Route::new(3);
        ex2.push_callsign("VE1ABC", 0, false);
        ex2.push_internet();
        ex2.push_callsign("VE2DEF", 234, false);
        ex2.push_internet();
        ex2.push_callsign("VE3XYZ", 14, false);

        let encoded = ex2.encode(&mut buf).unwrap();
        assert_eq!(
            &[
                0x1B, 0x03, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0xFE, 0x56, 0x45, 0x32,
                0x44, 0x45, 0x46, 0xFF, 0xEA, 0xFE, 0x56, 0x45, 0x33, 0x58, 0x59, 0x5A, 0xFF, 0x0E
            ],
            &encoded
        );

        let mut ex3 = Route::new(0);
        ex3.push_callsign("VE1ABC", 0, false);
        ex3.push_internet();
        ex3.push_internet();

        let encoded = ex3.encode(&mut buf).unwrap();
        assert_eq!(
            &[0x0B, 0x00, 0x56, 0x45, 0x31, 0x41, 0x42, 0x43, 0xFF, 0x00, 0xFE, 0xFE],
            &encoded
        );
    }
Stephen D's avatar
Stephen D committed

    #[test]
    fn dest_e2e() {
        assert_eq!(None, Destination::new(true, 0, "dest", 36));
        assert_eq!(None, Destination::new(false, 128, "abc", 0));
        assert!(Destination::new(false, 0, "anc", 8).is_some());

        let dest = Destination::new(true, 64, "mrow", 31).unwrap();
        let mut buf = [0; 256];
        let encoded = dest.encode(&mut buf).unwrap();
        let decoded = Destination::decode(encoded).unwrap();
        assert_eq!(dest, decoded);
    }
Stephen D's avatar
wip
Stephen D committed
}