rustedbytes_nmea/message/
gga.rs

1//! GGA (Global Positioning System Fix Data) message implementation
2//!
3//! The GGA message provides essential GPS fix data including time, position,
4//! fix quality, number of satellites, horizontal dilution of precision (HDOP),
5//! altitude, and geoid separation.
6//!
7//! ## Message Format
8//!
9//! ```text
10//! $GPGGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh
11//! ```
12//!
13//! ## Fields
14//!
15//! | Index | Field | Type | Mandatory | Description |
16//! |-------|-------|------|-----------|-------------|
17//! | 0 | Sentence ID | String | Yes | Message type (GPGGA, GNGGA, etc.) |
18//! | 1 | UTC Time | String | Yes | hhmmss.ss format |
19//! | 2 | Latitude | f64 | Yes | ddmm.mmmmm format |
20//! | 3 | N/S Indicator | char | Yes | N = North, S = South |
21//! | 4 | Longitude | f64 | Yes | dddmm.mmmmm format |
22//! | 5 | E/W Indicator | char | Yes | E = East, W = West |
23//! | 6 | Fix Quality | u8 | Yes | 0=Invalid, 1=GPS, 2=DGPS, etc. |
24//! | 7 | Satellites | u8 | No | Number of satellites in use |
25//! | 8 | HDOP | f32 | No | Horizontal dilution of precision |
26//! | 9 | Altitude | f32 | No | Altitude above mean sea level |
27//! | 10 | Altitude Units | char | No | M = meters |
28//! | 11 | Geoid Sep | f32 | No | Geoid separation |
29//! | 12 | Geoid Units | char | No | M = meters |
30//! | 13 | Age of Diff | f32 | No | Age of differential corrections |
31//! | 14 | Diff Station ID | String | No | Differential station ID |
32//!
33//! ## Example
34//!
35//! ```text
36//! $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
37//! ```
38//!
39//! This represents:
40//! - Time: 12:35:19 UTC
41//! - Position: 48°07.038'N, 11°31.000'E
42//! - Fix quality: GPS fix
43//! - 8 satellites in use
44//! - HDOP: 0.9
45//! - Altitude: 545.4 meters above MSL
46//! - Geoid separation: 46.9 meters
47
48use crate::message::ParsedSentence;
49use crate::types::{MessageType, TalkerId};
50
51/// GGA - Global Positioning System Fix Data parameters
52#[derive(Debug, Clone)]
53pub struct GgaData {
54    pub talker_id: TalkerId,
55    time_data: [u8; 16],
56    time_len: u8,
57    pub latitude: f64,
58    pub lat_direction: char,
59    pub longitude: f64,
60    pub lon_direction: char,
61    pub fix_quality: u8,
62    pub num_satellites: Option<u8>,
63    pub hdop: Option<f32>,
64    pub altitude: Option<f32>,
65    pub altitude_units: Option<char>,
66    pub geoid_separation: Option<f32>,
67    pub geoid_units: Option<char>,
68    pub age_of_diff: Option<f32>,
69    diff_station_id_data: [u8; 8],
70    diff_station_id_len: u8,
71}
72
73impl GgaData {
74    /// Get time as string slice
75    pub fn time(&self) -> &str {
76        core::str::from_utf8(&self.time_data[..self.time_len as usize]).unwrap_or("")
77    }
78
79    /// Get differential station ID as string slice (if present)
80    pub fn diff_station_id(&self) -> Option<&str> {
81        if self.diff_station_id_len > 0 {
82            core::str::from_utf8(&self.diff_station_id_data[..self.diff_station_id_len as usize])
83                .ok()
84        } else {
85            None
86        }
87    }
88}
89
90impl ParsedSentence {
91    /// Extract GGA message parameters
92    ///
93    /// Parses the GGA (Global Positioning System Fix Data) message and returns
94    /// a structured `GgaData` object containing all parsed fields.
95    ///
96    /// # Returns
97    ///
98    /// - `Some(GgaData)` if the message is a valid GGA message with all mandatory fields
99    /// - `None` if:
100    ///   - The message is not a GGA message
101    ///   - Any mandatory field is missing or invalid
102    ///
103    /// # Mandatory Fields
104    ///
105    /// - Time (field 1)
106    /// - Latitude (field 2)
107    /// - Latitude direction (field 3)
108    /// - Longitude (field 4)
109    /// - Longitude direction (field 5)
110    /// - Fix quality (field 6)
111    ///
112    /// # Optional Fields
113    ///
114    /// All other fields are optional and will be `None` if not present or invalid.
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use rustedbytes_nmea::{NmeaParser, MessageType};
120    ///
121    /// let parser = NmeaParser::new();
122    /// let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
123    ///
124    /// let result = parser.parse_bytes(sentence);
125    /// if let Ok((Some(msg), _consumed)) = result {
126    ///     if let Some(gga) = msg.as_gga() {
127    ///         assert_eq!(gga.time(), "123519");
128    ///         assert_eq!(gga.latitude, 4807.038);
129    ///         assert_eq!(gga.fix_quality, 1);
130    ///     }
131    /// }
132    /// ```
133    pub fn as_gga(&self) -> Option<GgaData> {
134        if self.message_type != MessageType::GGA {
135            return None;
136        }
137
138        // Validate mandatory fields
139        let time_str = self.get_field_str(1)?;
140        let latitude: f64 = self.parse_field(2)?;
141        let lat_direction = self.parse_field_char(3)?;
142        let longitude: f64 = self.parse_field(4)?;
143        let lon_direction = self.parse_field_char(5)?;
144        let fix_quality: u8 = self.parse_field(6)?;
145
146        // Copy time string to fixed array
147        let mut time_data = [0u8; 16];
148        let time_bytes = time_str.as_bytes();
149        let time_len = time_bytes.len().min(16) as u8;
150        time_data[..time_len as usize].copy_from_slice(&time_bytes[..time_len as usize]);
151
152        // Copy diff station ID if present
153        let mut diff_station_id_data = [0u8; 8];
154        let diff_station_id_len = if let Some(id_str) = self.get_field_str(14) {
155            let id_bytes = id_str.as_bytes();
156            let len = id_bytes.len().min(8) as u8;
157            diff_station_id_data[..len as usize].copy_from_slice(&id_bytes[..len as usize]);
158            len
159        } else {
160            0
161        };
162
163        Some(GgaData {
164            talker_id: self.talker_id,
165            time_data,
166            time_len,
167            latitude,
168            lat_direction,
169            longitude,
170            lon_direction,
171            fix_quality,
172            num_satellites: self.parse_field(7),
173            hdop: self.parse_field(8),
174            altitude: self.parse_field(9),
175            altitude_units: self.parse_field_char(10),
176            geoid_separation: self.parse_field(11),
177            geoid_units: self.parse_field_char(12),
178            age_of_diff: self.parse_field(13),
179            diff_station_id_data,
180            diff_station_id_len,
181        })
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::NmeaParser;
188
189    #[test]
190    fn test_gga_complete_message() {
191        let parser = NmeaParser::new();
192        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
193
194        let result = parser.parse_sentence_complete(sentence);
195
196        assert!(result.is_some());
197        let msg = result.unwrap();
198        let gga = msg.as_gga();
199        assert!(gga.is_some());
200
201        let gga_data = gga.unwrap();
202        assert_eq!(gga_data.time(), "123519");
203        assert_eq!(gga_data.latitude, 4807.038);
204        assert_eq!(gga_data.lat_direction, 'N');
205        assert_eq!(gga_data.longitude, 1131.000);
206        assert_eq!(gga_data.lon_direction, 'E');
207        assert_eq!(gga_data.fix_quality, 1);
208        assert_eq!(gga_data.num_satellites, Some(8));
209        assert_eq!(gga_data.hdop, Some(0.9));
210        assert_eq!(gga_data.altitude, Some(545.4));
211        assert_eq!(gga_data.altitude_units, Some('M'));
212        assert_eq!(gga_data.geoid_separation, Some(46.9));
213        assert_eq!(gga_data.geoid_units, Some('M'));
214        assert_eq!(gga_data.age_of_diff, None);
215        assert_eq!(gga_data.diff_station_id(), None);
216    }
217
218    #[test]
219    fn test_gga_with_empty_optional_fields() {
220        let parser = NmeaParser::new();
221        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,,,,,M,,M,,*47\r\n";
222
223        let result = parser.parse_sentence_complete(sentence);
224
225        assert!(result.is_some());
226        let msg = result.unwrap();
227        let gga = msg.as_gga();
228        assert!(gga.is_some());
229
230        let gga_data = gga.unwrap();
231        assert_eq!(gga_data.time(), "123519");
232        assert_eq!(gga_data.latitude, 4807.038);
233        assert_eq!(gga_data.fix_quality, 1);
234        assert_eq!(gga_data.num_satellites, None);
235        assert_eq!(gga_data.hdop, None);
236        assert_eq!(gga_data.altitude, None);
237    }
238
239    #[test]
240    fn test_gga_missing_time() {
241        let parser = NmeaParser::new();
242        let sentence = b"$GPGGA,,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
243
244        let result = parser.parse_sentence_complete(sentence);
245
246        // Should return None because time is mandatory
247        assert!(result.is_none());
248    }
249
250    #[test]
251    fn test_gga_missing_latitude() {
252        let parser = NmeaParser::new();
253        let sentence = b"$GPGGA,123519,,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
254
255        let result = parser.parse_sentence_complete(sentence);
256
257        // Should return None because a mandatory field is missing
258        assert!(result.is_none());
259    }
260
261    #[test]
262    fn test_gga_missing_longitude() {
263        let parser = NmeaParser::new();
264        let sentence = b"$GPGGA,123519,4807.038,N,,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
265
266        let result = parser.parse_sentence_complete(sentence);
267
268        // Should return None because a mandatory field is missing
269        assert!(result.is_none());
270    }
271
272    #[test]
273    fn test_gga_missing_fix_quality() {
274        let parser = NmeaParser::new();
275        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,,08,0.9,545.4,M,46.9,M,,*47\r\n";
276
277        let result = parser.parse_sentence_complete(sentence);
278
279        // Should return None because a mandatory field is missing
280        assert!(result.is_none());
281    }
282
283    #[test]
284    fn test_gga_invalid_latitude_format() {
285        let parser = NmeaParser::new();
286        let sentence = b"$GPGGA,123519,INVALID,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
287
288        let result = parser.parse_sentence_complete(sentence);
289
290        // Should return None because a mandatory field is missing
291        assert!(result.is_none());
292    }
293
294    #[test]
295    fn test_gga_with_differential_data() {
296        let parser = NmeaParser::new();
297        let sentence =
298            b"$GPGGA,123519,4807.038,N,01131.000,E,2,08,0.9,545.4,M,46.9,M,3.2,0120*47\r\n";
299
300        let result = parser.parse_sentence_complete(sentence);
301
302        assert!(result.is_some());
303        let msg = result.unwrap();
304        let gga = msg.as_gga();
305        assert!(gga.is_some());
306
307        let gga_data = gga.unwrap();
308        assert_eq!(gga_data.fix_quality, 2);
309        assert_eq!(gga_data.age_of_diff, Some(3.2));
310        assert_eq!(gga_data.diff_station_id(), Some("0120"));
311    }
312
313    #[test]
314    fn test_gga_numeric_precision() {
315        let parser = NmeaParser::new();
316        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
317
318        let result = parser.parse_sentence_complete(sentence);
319
320        assert!(result.is_some());
321        let msg = result.unwrap();
322        let gga = msg.as_gga();
323        assert!(gga.is_some());
324
325        let gga_data = gga.unwrap();
326        assert!((gga_data.latitude - 4807.038).abs() < 0.001);
327        assert!((gga_data.longitude - 1131.000).abs() < 0.001);
328
329        if let Some(hdop) = gga_data.hdop {
330            assert!((hdop - 0.9).abs() < 0.01);
331        }
332
333        if let Some(alt) = gga_data.altitude {
334            assert!((alt - 545.4).abs() < 0.1);
335        }
336    }
337
338    #[test]
339    fn test_gga_different_talker_id() {
340        let parser = NmeaParser::new();
341        // GNGGA is multi-GNSS (GPS + GLONASS + others)
342        let sentence = b"$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
343
344        let result = parser.parse_sentence_complete(sentence);
345
346        assert!(result.is_some());
347        let msg = result.unwrap();
348        let gga = msg.as_gga();
349        assert!(gga.is_some());
350
351        let gga_data = gga.unwrap();
352        assert_eq!(gga_data.talker_id, crate::types::TalkerId::GN);
353    }
354
355    #[test]
356    fn test_gga_gps_talker_id() {
357        let parser = NmeaParser::new();
358        let sentence = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
359
360        let result = parser.parse_sentence_complete(sentence);
361
362        assert!(result.is_some());
363        let msg = result.unwrap();
364        let gga = msg.as_gga();
365        assert!(gga.is_some());
366
367        let gga_data = gga.unwrap();
368        assert_eq!(gga_data.talker_id, crate::types::TalkerId::GP);
369    }
370
371    #[test]
372    fn test_gga_glonass_talker_id() {
373        let parser = NmeaParser::new();
374        let sentence = b"$GLGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
375
376        let result = parser.parse_sentence_complete(sentence);
377
378        assert!(result.is_some());
379        let msg = result.unwrap();
380        let gga = msg.as_gga();
381        assert!(gga.is_some());
382
383        let gga_data = gga.unwrap();
384        assert_eq!(gga_data.talker_id, crate::types::TalkerId::GL);
385    }
386
387    #[test]
388    fn test_gga_galileo_talker_id() {
389        let parser = NmeaParser::new();
390        let sentence = b"$GAGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
391
392        let result = parser.parse_sentence_complete(sentence);
393
394        assert!(result.is_some());
395        let msg = result.unwrap();
396        let gga = msg.as_gga();
397        assert!(gga.is_some());
398
399        let gga_data = gga.unwrap();
400        assert_eq!(gga_data.talker_id, crate::types::TalkerId::GA);
401    }
402
403    #[test]
404    fn test_gga_beidou_talker_id() {
405        let parser = NmeaParser::new();
406        let sentence = b"$GBGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
407
408        let result = parser.parse_sentence_complete(sentence);
409
410        assert!(result.is_some());
411        let msg = result.unwrap();
412        let gga = msg.as_gga();
413        assert!(gga.is_some());
414
415        let gga_data = gga.unwrap();
416        assert_eq!(gga_data.talker_id, crate::types::TalkerId::GB);
417    }
418}