rustedbytes_nmea/
parser.rs

1//! NMEA sentence parser implementation
2
3use crate::message::{Field, ParsedSentence, MAX_FIELDS};
4use crate::types::{MessageType, NmeaMessage, ParseError, TalkerId};
5
6/// Main NMEA parser structure (now stateless)
7pub struct NmeaParser {}
8
9impl NmeaParser {
10    /// Create a new NMEA parser instance
11    pub fn new() -> Self {
12        NmeaParser {}
13    }
14
15    /// Parse multiple bytes and return a parsed message if found, along with bytes consumed
16    ///
17    /// Returns:
18    /// - Ok((Some(message), bytes_consumed)) - Successfully parsed a complete message
19    /// - Ok((None, bytes_consumed)) - Partial message, need more data (bytes_consumed will be 0 if no $ found)
20    /// - Err((ParseError, bytes_consumed)) - Found complete message but it's invalid
21    ///
22    /// The parser handles spurious characters before the '$' start marker by consuming them.
23    pub fn parse_bytes(
24        &self,
25        data: &[u8],
26    ) -> Result<(Option<NmeaMessage>, usize), (ParseError, usize)> {
27        // Find the start of a message
28        let start_pos = data.iter().position(|&b| b == b'$');
29
30        if start_pos.is_none() {
31            // No message start found, consume all spurious data
32            return Ok((None, data.len()));
33        }
34
35        let start_pos = start_pos.unwrap();
36
37        // Find the end of the message (either \n or \r)
38        let end_pos = data[start_pos..]
39            .iter()
40            .position(|&b| b == b'\n' || b == b'\r');
41
42        if end_pos.is_none() {
43            // Partial message - consume spurious data before $, but not the partial message
44            return Ok((None, start_pos));
45        }
46
47        let end_pos = start_pos + end_pos.unwrap();
48        let sentence = &data[start_pos..end_pos];
49
50        // Parse the complete sentence
51        match self.parse_sentence(sentence) {
52            Some(msg) => {
53                // Successfully parsed - consume up to and including the line ending
54                // Need to skip any additional \r or \n characters
55                let mut consumed = end_pos + 1;
56                while consumed < data.len() && (data[consumed] == b'\r' || data[consumed] == b'\n')
57                {
58                    consumed += 1;
59                }
60                Ok((Some(msg), consumed))
61            }
62            None => {
63                // Complete message but invalid (missing mandatory fields)
64                // Consume the invalid message
65                let mut consumed = end_pos + 1;
66                while consumed < data.len() && (data[consumed] == b'\r' || data[consumed] == b'\n')
67                {
68                    consumed += 1;
69                }
70                Err((ParseError::InvalidMessage, consumed))
71            }
72        }
73    }
74
75    /// Parse a complete NMEA sentence from a buffer
76    fn parse_sentence(&self, buffer: &[u8]) -> Option<NmeaMessage> {
77        if buffer.len() < 7 || buffer[0] != b'$' {
78            return None;
79        }
80
81        // Find sentence end (before checksum marker '*')
82        let sentence_end = buffer
83            .iter()
84            .position(|&b| b == b'*')
85            .unwrap_or(buffer.len());
86
87        if sentence_end < 7 {
88            return None;
89        }
90
91        // Extract talker ID and message type
92        let (talker_id, message_type) = self.identify_message(&buffer[1..6]);
93        if message_type == MessageType::Unknown {
94            return None;
95        }
96
97        // Parse fields
98        let mut fields = [None; MAX_FIELDS];
99        let mut field_count = 0;
100        let mut field_start = 1; // Skip '$'
101
102        for i in 1..sentence_end {
103            if buffer[i] == b',' || i == sentence_end - 1 {
104                let field_end = if buffer[i] == b',' { i } else { i + 1 };
105
106                if field_count < MAX_FIELDS {
107                    let field_bytes = &buffer[field_start..field_end];
108                    if !field_bytes.is_empty() {
109                        fields[field_count] = Some(Field::from_bytes(field_bytes));
110                    }
111                    field_count += 1;
112                }
113                field_start = i + 1;
114            }
115        }
116
117        let parsed = ParsedSentence {
118            message_type,
119            talker_id,
120            fields,
121            field_count,
122        };
123
124        // Convert parsed sentence to typed message
125        match message_type {
126            MessageType::GGA => parsed.as_gga().map(NmeaMessage::GGA),
127            MessageType::RMC => parsed.as_rmc().map(NmeaMessage::RMC),
128            MessageType::GSA => parsed.as_gsa().map(NmeaMessage::GSA),
129            MessageType::GSV => parsed.as_gsv().map(NmeaMessage::GSV),
130            MessageType::GLL => parsed.as_gll().map(NmeaMessage::GLL),
131            MessageType::VTG => parsed.as_vtg().map(NmeaMessage::VTG),
132            MessageType::GNS => parsed.as_gns().map(NmeaMessage::GNS),
133            MessageType::Unknown => None,
134        }
135    }
136
137    /// Identify the talker ID and message type from the sentence header
138    fn identify_message(&self, header_bytes: &[u8]) -> (TalkerId, MessageType) {
139        if header_bytes.len() < 5 {
140            return (TalkerId::Unknown, MessageType::Unknown);
141        }
142
143        let talker_id = match &header_bytes[0..2] {
144            b"GP" => TalkerId::GP,
145            b"GL" => TalkerId::GL,
146            b"GA" => TalkerId::GA,
147            b"GB" => TalkerId::GB,
148            b"GN" => TalkerId::GN,
149            b"BD" => TalkerId::BD,
150            b"QZ" => TalkerId::QZ,
151            _ => TalkerId::Unknown,
152        };
153
154        let message_type = match &header_bytes[2..5] {
155            b"GGA" => MessageType::GGA,
156            b"RMC" => MessageType::RMC,
157            b"GSA" => MessageType::GSA,
158            b"GSV" => MessageType::GSV,
159            b"GLL" => MessageType::GLL,
160            b"VTG" => MessageType::VTG,
161            b"GNS" => MessageType::GNS,
162            _ => MessageType::Unknown,
163        };
164
165        (talker_id, message_type)
166    }
167}
168
169impl Default for NmeaParser {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[cfg(test)]
176impl NmeaParser {
177    /// Parse a complete sentence with line ending for testing purposes
178    /// This is a helper function for migrating old tests
179    pub(crate) fn parse_sentence_complete(&self, sentence: &[u8]) -> Option<NmeaMessage> {
180        match self.parse_bytes(sentence) {
181            Ok((msg, _consumed)) => msg,
182            Err(_) => None,
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    // Tests based on references/nmea_valid.txt
192    #[test]
193    fn test_valid_gga_from_reference() {
194        let parser = NmeaParser::new();
195        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
196
197        let result = parser.parse_bytes(sentence);
198        assert!(result.is_ok());
199        let (msg, consumed) = result.unwrap();
200        assert!(msg.is_some());
201        assert_eq!(consumed, sentence.len());
202
203        let msg = msg.unwrap();
204        assert_eq!(msg.message_type(), MessageType::GGA);
205
206        let gga = msg.as_gga().expect("Should parse as GGA");
207        assert_eq!(gga.time(), "123519");
208        assert_eq!(gga.latitude, 4807.038);
209        assert_eq!(gga.lat_direction, 'N');
210        assert_eq!(gga.longitude, 1131.000);
211        assert_eq!(gga.lon_direction, 'E');
212    }
213
214    #[test]
215    fn test_valid_rmc_from_reference() {
216        let parser = NmeaParser::new();
217        let sentence = b"$GPRMC,235947,A,5540.123,N,01231.456,E,000.0,360.0,130694,011.3,E*62\r\n";
218
219        let result = parser.parse_bytes(sentence);
220        assert!(result.is_ok());
221        let (msg, consumed) = result.unwrap();
222        assert!(msg.is_some());
223        assert_eq!(consumed, sentence.len());
224
225        let msg = msg.unwrap();
226        assert_eq!(msg.message_type(), MessageType::RMC);
227
228        let rmc = msg.as_rmc().expect("Should parse as RMC");
229        assert_eq!(rmc.time(), "235947");
230        assert_eq!(rmc.status, 'A');
231        assert_eq!(rmc.latitude, 5540.123);
232        assert_eq!(rmc.lat_direction, 'N');
233    }
234
235    #[test]
236    fn test_valid_gsa_from_reference() {
237        let parser = NmeaParser::new();
238        let sentence = b"$GPGSA,A,3,04,05,09,12,24,25,29,31,,,,,1.8,1.0,1.5*33\r\n";
239
240        let result = parser.parse_bytes(sentence);
241        assert!(result.is_ok());
242        let (msg, consumed) = result.unwrap();
243        assert!(msg.is_some());
244        assert_eq!(consumed, sentence.len());
245
246        let msg = msg.unwrap();
247        assert_eq!(msg.message_type(), MessageType::GSA);
248
249        let gsa = msg.as_gsa().expect("Should parse as GSA");
250        assert_eq!(gsa.mode, 'A');
251        assert_eq!(gsa.fix_type, 3);
252    }
253
254    #[test]
255    fn test_valid_gsv_from_reference() {
256        let parser = NmeaParser::new();
257        let sentence = b"$GPGSV,3,1,12,02,17,315,44,04,77,268,47,05,55,147,45,07,32,195,42*70\r\n";
258
259        let result = parser.parse_bytes(sentence);
260        assert!(result.is_ok());
261        let (msg, consumed) = result.unwrap();
262        assert!(msg.is_some());
263        assert_eq!(consumed, sentence.len());
264
265        let msg = msg.unwrap();
266        assert_eq!(msg.message_type(), MessageType::GSV);
267
268        let gsv = msg.as_gsv().expect("Should parse as GSV");
269        assert_eq!(gsv.num_messages, 3);
270        assert_eq!(gsv.message_num, 1);
271        assert_eq!(gsv.satellites_in_view, 12);
272    }
273
274    // Tests based on references/nmea_edge_cases.txt
275    #[test]
276    fn test_edge_case_gga_empty_fields() {
277        let parser = NmeaParser::new();
278        // GGA with all empty fields (should fail - mandatory fields missing)
279        let sentence = b"$GPGGA,123519,,,,,,0,00,99.99,,,,,,*48\r\n";
280
281        let result = parser.parse_bytes(sentence);
282        assert!(result.is_err());
283        let (err, _consumed) = result.unwrap_err();
284        assert_eq!(err, ParseError::InvalidMessage);
285    }
286
287    #[test]
288    fn test_edge_case_rmc_zero_coordinates() {
289        let parser = NmeaParser::new();
290        // RMC with zero coordinates (valid but unusual)
291        let sentence = b"$GPRMC,000000,A,0000.000,N,00000.000,E,000.0,000.0,000000,000.0,W*7C\r\n";
292
293        let result = parser.parse_bytes(sentence);
294        assert!(result.is_ok());
295        let (msg, consumed) = result.unwrap();
296        assert!(msg.is_some());
297        assert_eq!(consumed, sentence.len());
298
299        let msg = msg.unwrap();
300        assert_eq!(msg.message_type(), MessageType::RMC);
301
302        let rmc = msg.as_rmc().expect("Should parse as RMC");
303        assert_eq!(rmc.latitude, 0.0);
304        assert_eq!(rmc.longitude, 0.0);
305    }
306
307    #[test]
308    fn test_edge_case_ais_message_with_exclamation() {
309        let parser = NmeaParser::new();
310        // AIS message starts with '!' instead of '$' - should not parse
311        let sentence = b"!AIVDM,1,1,,B,15N:;R0P00PD;88MD5MTDwwP0<0L,0*5C\r\n";
312
313        let result = parser.parse_bytes(sentence);
314        // Should consume spurious data and return None
315        assert!(result.is_ok());
316        let (msg, consumed) = result.unwrap();
317        assert!(msg.is_none());
318        assert_eq!(consumed, sentence.len());
319    }
320
321    #[test]
322    fn test_edge_case_concatenated_messages() {
323        let parser = NmeaParser::new();
324        // Two messages concatenated - parser will parse up to the first checksum
325        // and treat the rest as part of the same line
326        let data = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47$GPRMC,235947,A,5540.123,N,01231.456,E,000.0,360.0,130694,011.3,E*62\r\n";
327
328        let result = parser.parse_bytes(data);
329        // The parser finds the first * and parses up to that point as the first message
330        // This successfully parses the GGA message
331        assert!(result.is_ok());
332        let (msg, consumed) = result.unwrap();
333        assert!(msg.is_some());
334        let msg = msg.unwrap();
335        assert_eq!(msg.message_type(), MessageType::GGA);
336        assert_eq!(consumed, data.len());
337    }
338
339    #[test]
340    fn test_edge_case_unsupported_message_types() {
341        let parser = NmeaParser::new();
342
343        // GPTXT - text message (not supported)
344        let txt_sentence = b"$GPTXT,01,01,02,Software Version 7.03.00 (12345)*6E\r\n";
345        let result = parser.parse_bytes(txt_sentence);
346        assert!(result.is_err());
347        let (err, _consumed) = result.unwrap_err();
348        assert_eq!(err, ParseError::InvalidMessage);
349
350        // GPXTE - cross-track error (not supported)
351        let xte_sentence = b"$GPXTE,A,A,0.67,L,N*6F\r\n";
352        let result2 = parser.parse_bytes(xte_sentence);
353        assert!(result2.is_err());
354        let (err2, _consumed2) = result2.unwrap_err();
355        assert_eq!(err2, ParseError::InvalidMessage);
356    }
357
358    // Tests based on references/nmea_invalid.txt
359    #[test]
360    fn test_invalid_wrong_checksum() {
361        let parser = NmeaParser::new();
362        // Valid GGA structure but wrong checksum (*00 instead of *47)
363        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*00\r\n";
364
365        // Parser doesn't validate checksum, so this will parse successfully
366        let result = parser.parse_bytes(sentence);
367        assert!(result.is_ok());
368        let (msg, consumed) = result.unwrap();
369        assert!(msg.is_some());
370        assert_eq!(consumed, sentence.len());
371    }
372
373    #[test]
374    fn test_invalid_rmc_void_status() {
375        let parser = NmeaParser::new();
376        // RMC with status 'V' (void/invalid) - still valid structure
377        let sentence = b"$GPRMC,235947,V,5540.123,N,01231.456,E,000.0,360.0,130694,011.3,E*00\r\n";
378
379        let result = parser.parse_bytes(sentence);
380        assert!(result.is_ok());
381        let (msg, consumed) = result.unwrap();
382        assert!(msg.is_some());
383        assert_eq!(consumed, sentence.len());
384
385        let msg = msg.unwrap();
386        let rmc = msg.as_rmc().expect("Should parse as RMC");
387        assert_eq!(rmc.status, 'V'); // Status V means data is invalid but structure is valid
388    }
389
390    #[test]
391    fn test_invalid_missing_checksum() {
392        let parser = NmeaParser::new();
393        // GSA without checksum marker
394        let sentence = b"$GPGSA,A,3,04,05,09,12,24,25,29,31,,,,,1.8,1.0,1.5\r\n";
395
396        let result = parser.parse_bytes(sentence);
397        // Should still parse (checksum is optional in parsing)
398        assert!(result.is_ok());
399        let (msg, consumed) = result.unwrap();
400        assert!(msg.is_some());
401        assert_eq!(consumed, sentence.len());
402    }
403
404    #[test]
405    fn test_invalid_missing_dollar_sign() {
406        let parser = NmeaParser::new();
407        // GSV without starting '$'
408        let sentence = b"GPGSV,3,1,12,02,17,315,44,04,77,268,47,05,55,147,45,07,32,195,42*70\r\n";
409
410        let result = parser.parse_bytes(sentence);
411        // Should consume as spurious data and return None
412        assert!(result.is_ok());
413        let (msg, consumed) = result.unwrap();
414        assert!(msg.is_none());
415        assert_eq!(consumed, sentence.len());
416    }
417
418    #[test]
419    fn test_multiple_valid_messages_in_sequence() {
420        let parser = NmeaParser::new();
421
422        // Parse first message
423        let gga = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
424        let result1 = parser.parse_bytes(gga);
425        assert!(result1.is_ok());
426        let (msg1, consumed1) = result1.unwrap();
427        assert!(msg1.is_some());
428        assert_eq!(consumed1, gga.len());
429
430        // Parse second message
431        let rmc = b"$GPRMC,235947,A,5540.123,N,01231.456,E,000.0,360.0,130694,011.3,E*62\r\n";
432        let result2 = parser.parse_bytes(rmc);
433        assert!(result2.is_ok());
434        let (msg2, consumed2) = result2.unwrap();
435        assert!(msg2.is_some());
436        assert_eq!(consumed2, rmc.len());
437
438        // Parse third message
439        let gsa = b"$GPGSA,A,3,04,05,09,12,24,25,29,31,,,,,1.8,1.0,1.5*33\r\n";
440        let result3 = parser.parse_bytes(gsa);
441        assert!(result3.is_ok());
442        let (msg3, consumed3) = result3.unwrap();
443        assert!(msg3.is_some());
444        assert_eq!(consumed3, gsa.len());
445    }
446}