nmea/sentences/
aam.rs

1use crate::parse::TEXT_PARAMETER_MAX_LEN;
2
3use arrayvec::ArrayString;
4use nom::{
5    bytes::complete::is_not,
6    character::complete::{char, one_of},
7    combinator::opt,
8    number::complete::float,
9};
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14use crate::{parse::NmeaSentence, sentences::utils::array_string, Error, SentenceType};
15
16/// AAM - Waypoint Arrival Alarm
17///
18/// <https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm>
19///
20/// ```text
21///        1 2 3   4 5    6
22///        | | |   | |    |
23/// $--AAM,A,A,x.x,N,c--c*hh<CR><LF>
24///
25/// Field Number:
26///   1. Status, BOOLEAN, A = Arrival circle entered, V = not passed
27///   2. Status, BOOLEAN, A = perpendicular passed at waypoint, V = not passed
28///   3. Arrival circle radius
29///   4. Units of radius, nautical miles
30///   5. Waypoint ID
31///   6. Checksum
32///
33/// Example: $GPAAM,A,A,0.10,N,WPTNME*43
34/// WPTNME is the waypoint name.
35/// ```
36#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]
38#[derive(Debug, PartialEq)]
39pub struct AamData {
40    pub arrival_circle_entered: Option<bool>,
41    pub perpendicular_passed: Option<bool>,
42    pub arrival_circle_radius: Option<f32>,
43    pub radius_units: Option<char>,
44    #[cfg_attr(feature = "defmt-03", defmt(Debug2Format))]
45    pub waypoint_id: Option<ArrayString<TEXT_PARAMETER_MAX_LEN>>,
46}
47
48/// Parse AAM message
49pub fn parse_aam(sentence: NmeaSentence) -> Result<AamData, Error> {
50    if sentence.message_id != SentenceType::AAM {
51        Err(Error::WrongSentenceHeader {
52            expected: SentenceType::AAM,
53            found: sentence.message_id,
54        })
55    } else {
56        Ok(do_parse_aam(sentence.data)?)
57    }
58}
59
60fn do_parse_aam(i: &str) -> Result<AamData, Error> {
61    let (i, arrival_circle_entered) = one_of("AV")(i)?;
62    let arrival_circle_entered = match arrival_circle_entered {
63        'A' => Some(true),
64        'V' => Some(false),
65        _ => unreachable!(),
66    };
67    let (i, _) = char(',')(i)?;
68
69    let (i, perpendicular_passed) = one_of("AV")(i)?;
70    let perpendicular_passed = match perpendicular_passed {
71        'A' => Some(true),
72        'V' => Some(false),
73        _ => unreachable!(),
74    };
75    let (i, _) = char(',')(i)?;
76
77    let (i, arrival_circle_radius) = opt(float)(i)?;
78    let (i, _) = char(',')(i)?;
79
80    let (i, radius_units) = opt(char('N'))(i)?;
81    let (i, _) = char(',')(i)?;
82
83    let (_i, waypoint_id) = opt(is_not("*"))(i)?;
84
85    Ok(AamData {
86        arrival_circle_entered,
87        perpendicular_passed,
88        arrival_circle_radius,
89        radius_units,
90        waypoint_id: waypoint_id
91            .map(array_string::<TEXT_PARAMETER_MAX_LEN>)
92            .transpose()?,
93    })
94}
95
96#[cfg(test)]
97mod tests {
98    use approx::assert_relative_eq;
99
100    use super::*;
101    use crate::{parse::parse_nmea_sentence, SentenceType};
102
103    #[test]
104    fn parse_aam_with_nmea_sentence_struct() {
105        let data = parse_aam(NmeaSentence {
106            talker_id: "GP",
107            message_id: SentenceType::AAM,
108            data: "A,V,0.10,N,WPTNME",
109            checksum: 0x0,
110        })
111        .unwrap();
112
113        assert!(data.arrival_circle_entered.unwrap());
114        assert!(!data.perpendicular_passed.unwrap());
115        assert_relative_eq!(data.arrival_circle_radius.unwrap(), 0.10);
116        assert_eq!(data.radius_units.unwrap(), 'N');
117        assert_eq!(&data.waypoint_id.unwrap(), "WPTNME");
118    }
119
120    #[test]
121    #[should_panic]
122    fn parse_aam_with_invalid_arrival_circle_entered_value() {
123        parse_aam(NmeaSentence {
124            talker_id: "GP",
125            message_id: SentenceType::AAM,
126            data: "G,V,0.10,N,WPTNME",
127            checksum: 0x0,
128        })
129        .unwrap();
130    }
131
132    #[test]
133    #[should_panic]
134    fn parse_aam_with_invalid_perpendicular_passed_value() {
135        parse_aam(NmeaSentence {
136            talker_id: "GP",
137            message_id: SentenceType::AAM,
138            data: "V,X,0.10,N,WPTNME",
139            checksum: 0x0,
140        })
141        .unwrap();
142    }
143
144    #[test]
145    #[should_panic]
146    fn parse_aam_with_invalid_radius_units_value() {
147        parse_aam(NmeaSentence {
148            talker_id: "GP",
149            message_id: SentenceType::AAM,
150            data: "V,A,0.10,P,WPTNME",
151            checksum: 0x0,
152        })
153        .unwrap();
154    }
155
156    #[test]
157    fn parse_aam_full_sentence() {
158        let sentence = parse_nmea_sentence("$GPAAM,A,A,0.10,N,WPTNME*32").unwrap();
159        assert_eq!(sentence.checksum, 0x32);
160        assert_eq!(sentence.calc_checksum(), 0x32);
161
162        let data = parse_aam(sentence).unwrap();
163        assert!(data.arrival_circle_entered.unwrap());
164        assert!(data.perpendicular_passed.unwrap());
165        assert_relative_eq!(data.arrival_circle_radius.unwrap(), 0.10);
166        assert_eq!(data.radius_units.unwrap(), 'N');
167        assert_eq!(&data.waypoint_id.unwrap(), "WPTNME");
168    }
169
170    #[test]
171    fn parse_aam_with_wrong_message_id() {
172        let error = parse_aam(NmeaSentence {
173            talker_id: "GP",
174            message_id: SentenceType::ABK,
175            data: "A,V,0.10,N,WPTNME",
176            checksum: 0x43,
177        })
178        .unwrap_err();
179
180        if let Error::WrongSentenceHeader { expected, found } = error {
181            assert_eq!(expected, SentenceType::AAM);
182            assert_eq!(found, SentenceType::ABK);
183        }
184    }
185}