nmeasis 26.4.1

A memory-safe NMEA 0183 parser with a C FFI
Documentation
use crate::encoder::NmeaEncode;
use crate::macros::{write_byte, write_str};
use crate::message::NmeaMessageError;
use crate::number::NmeaNumber;
use crate::parser::NmeaParse;

/// AAM - Waypoint Arrival Alarm
///
/// This sentence is generated by some units to indicate the status of arrival
/// (entering the arrival circle, or passing the perpendicular of the course line)
/// at the destination waypoint.
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Aam<'a> {
    /// 1. Status, BOOLEAN, A = Arrival circle entered, V = not passed
    pub arrival_circle_entered: &'a str,
    /// 2. Status, BOOLEAN, A = perpendicular passed at waypoint, V = not passed
    pub perpendicular_passed: &'a str,
    /// 3. Arrival circle radius
    pub radius: &'a str,
    // 4. Radius Units, must always be "N"/Nautical Miles so it is omitted.
    pub radius_units: &'a str,
    /// 5. Waypoint ID
    pub waypoint_id: &'a str,
}

impl<'a> NmeaParse<'a> for Aam<'a> {
    fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
        let mut f = fields.splitn(5, ',');
        Ok(Self {
            arrival_circle_entered: f.next().ok_or(NmeaMessageError::MissingField)?,
            perpendicular_passed: f.next().ok_or(NmeaMessageError::MissingField)?,
            radius: f.next().ok_or(NmeaMessageError::MissingField)?,
            radius_units: f.next().ok_or(NmeaMessageError::MissingField)?,
            waypoint_id: f.next().unwrap_or(""),
        })
    }
}

impl NmeaEncode for Aam<'_> {
    fn encoded_len(&self) -> usize {
        self.arrival_circle_entered.len()
            + self.perpendicular_passed.len()
            + self.radius.len()
            + self.radius_units.len()
            + self.waypoint_id.len()
            + 4
    }

    fn encode(&self, buf: &mut [u8]) -> usize {
        let mut pos = 0;

        write_str!(buf, pos, self.arrival_circle_entered);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.perpendicular_passed);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.radius);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.radius_units);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.waypoint_id);

        pos
    }
}

impl Aam<'_> {
    // TODO: Validate trait or something that STRICTLY! validates?

    #[must_use]
    pub fn arrival_circle_entered(&self) -> bool {
        self.arrival_circle_entered == "A"
    }

    #[must_use]
    pub fn perpendicular_passed(&self) -> bool {
        self.perpendicular_passed == "A"
    }

    #[must_use]
    pub fn arrived(&self) -> bool {
        self.arrival_circle_entered() || self.perpendicular_passed()
    }

    #[must_use]
    pub fn radius(&self) -> Option<NmeaNumber> {
        NmeaNumber::parse(self.radius)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn both_conditions_met() {
        let aam = Aam::parse("A,A,0.10,N,WPTNME").unwrap();
        assert!(aam.arrival_circle_entered());
        assert!(aam.perpendicular_passed());
        assert!(aam.arrived());
    }

    #[test]
    fn only_circle_entered() {
        let aam = Aam::parse("A,V,0.10,N,WPTNME").unwrap();
        assert!(aam.arrival_circle_entered());
        assert!(!aam.perpendicular_passed());
        assert!(aam.arrived());
    }

    #[test]
    fn only_perpendicular_passed() {
        let aam = Aam::parse("V,A,0.10,N,WPTNME").unwrap();
        assert!(!aam.arrival_circle_entered());
        assert!(aam.perpendicular_passed());
        assert!(aam.arrived());
    }

    #[test]
    fn neither_condition_met() {
        let aam = Aam::parse("V,V,0.50,N,HOME").unwrap();
        assert!(!aam.arrival_circle_entered());
        assert!(!aam.perpendicular_passed());
        assert!(!aam.arrived());
    }

    #[test]
    fn waypoint_id_parsed() {
        let aam = Aam::parse("A,A,0.10,N,WPTNME").unwrap();
        assert_eq!(aam.waypoint_id, "WPTNME");
    }

    #[test]
    fn empty_waypoint_id() {
        let aam = Aam::parse("A,V,0.10,N,").unwrap();
        assert_eq!(aam.waypoint_id, "");
    }

    #[test]
    fn missing_waypoint_id_field() {
        let aam = Aam::parse("A,V,0.10,N").unwrap();
        assert_eq!(aam.waypoint_id, "");
    }

    #[test]
    fn missing_required_field_errors() {
        assert!(Aam::parse("A,A,0.10").is_err());
        assert!(Aam::parse("A").is_err());
        assert!(Aam::parse("").is_err());
    }

    #[test]
    fn nonstandard_status_value() {
        let aam = Aam::parse("D,D,0.10,N,WPT").unwrap();
        assert!(!aam.arrival_circle_entered());
        assert!(!aam.perpendicular_passed());
    }

    #[test]
    fn radius_parsed() {
        let aam = Aam::parse("A,A,0.10,N,WPT").unwrap();
        assert_eq!(aam.radius(), Some(NmeaNumber::new(10, 2)));
    }

    #[test]
    fn encodes_fields() {
        let aam = Aam::parse("A,A,0.10,N,WPTNME").unwrap();
        let mut buf = [0u8; 64];
        let len = aam.encode(&mut buf);
        assert_eq!(&buf[..len], b"A,A,0.10,N,WPTNME");
    }

    #[test]
    fn encode_round_trip() {
        let input = "A,A,0.10,N,WPTNME";
        let aam = Aam::parse(input).unwrap();
        let mut buf = [0u8; 64];
        let len = aam.encode(&mut buf);
        let encoded = core::str::from_utf8(&buf[..len]).unwrap();
        let aam2 = Aam::parse(encoded).unwrap();
        assert_eq!(aam.arrival_circle_entered, aam2.arrival_circle_entered);
        assert_eq!(aam.perpendicular_passed, aam2.perpendicular_passed);
        assert_eq!(aam.radius, aam2.radius);
        assert_eq!(aam.radius_units, aam2.radius_units);
        assert_eq!(aam.waypoint_id, aam2.waypoint_id);
    }

    #[test]
    fn encoded_len_matches_actual() {
        let aam = Aam::parse("A,A,0.10,N,WPTNME").unwrap();
        let mut buf = [0u8; 64];
        let len = aam.encode(&mut buf);
        assert_eq!(len, aam.encoded_len());
    }
}