rustedbytes_nmea/message/
gll.rs

1//! GLL (Geographic Position - Latitude/Longitude) message implementation
2//!
3//! The GLL message provides geographic position information including latitude,
4//! longitude, UTC time, and data validity status. It's a simpler alternative to
5//! GGA for applications that only need position and time.
6//!
7//! ## Message Format
8//!
9//! ```text
10//! $GPGLL,llll.ll,a,yyyyy.yy,a,hhmmss.ss,A*hh
11//! ```
12//!
13//! ## Fields
14//!
15//! | Index | Field | Type | Mandatory | Description |
16//! |-------|-------|------|-----------|-------------|
17//! | 0 | Sentence ID | String | Yes | Message type (GPGLL, GNGLL, etc.) |
18//! | 1 | Latitude | f64 | Yes | ddmm.mmmmm format |
19//! | 2 | N/S Indicator | char | Yes | N = North, S = South |
20//! | 3 | Longitude | f64 | Yes | dddmm.mmmmm format |
21//! | 4 | E/W Indicator | char | Yes | E = East, W = West |
22//! | 5 | UTC Time | String | Yes | hhmmss.ss format |
23//! | 6 | Status | char | Yes | A = Valid, V = Invalid |
24//!
25//! ## Status Values
26//!
27//! - **A** (Active): Data is valid
28//! - **V** (Void): Data is invalid or receiver has no fix
29//!
30//! ## Example
31//!
32//! ```text
33//! $GPGLL,4916.45,N,12311.12,W,225444,A*1D
34//! ```
35//!
36//! This represents:
37//! - Position: 49°16.45'N, 123°11.12'W
38//! - Time: 22:54:44 UTC
39//! - Status: Active (valid data)
40
41use crate::message::ParsedSentence;
42use crate::types::{MessageType, TalkerId};
43
44/// GLL - Geographic Position parameters
45#[derive(Debug, Clone)]
46pub struct GllData {
47    pub talker_id: TalkerId,
48    pub latitude: f64,
49    pub lat_direction: char,
50    pub longitude: f64,
51    pub lon_direction: char,
52    time_data: [u8; 16],
53    time_len: u8,
54    pub status: char,
55}
56
57impl GllData {
58    /// Get time as string slice
59    pub fn time(&self) -> &str {
60        core::str::from_utf8(&self.time_data[..self.time_len as usize]).unwrap_or("")
61    }
62}
63
64impl ParsedSentence {
65    /// Extract GLL message parameters
66    ///
67    /// Parses the GLL (Geographic Position) message and returns a structured
68    /// `GllData` object containing all parsed fields.
69    ///
70    /// # Returns
71    ///
72    /// - `Some(GllData)` if the message is a valid GLL message with all mandatory fields
73    /// - `None` if:
74    ///   - The message is not a GLL message
75    ///   - Any mandatory field is missing or invalid
76    ///
77    /// # Mandatory Fields
78    ///
79    /// All fields in the GLL message are mandatory:
80    /// - Latitude (field 1)
81    /// - Latitude direction (field 2)
82    /// - Longitude (field 3)
83    /// - Longitude direction (field 4)
84    /// - Time (field 5)
85    /// - Status (field 6)
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use rustedbytes_nmea::{NmeaParser, MessageType};
91    ///
92    /// let parser = NmeaParser::new();
93    /// let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
94    ///
95    /// let result = parser.parse_bytes(sentence);
96    /// if let Ok((Some(msg), _consumed)) = result {
97    ///     if let Some(gll) = msg.as_gll() {
98    ///         assert_eq!(gll.latitude, 4916.45);
99    ///         assert_eq!(gll.status, 'A');
100    ///     }
101    /// }
102    /// ```
103    pub fn as_gll(&self) -> Option<GllData> {
104        if self.message_type != MessageType::GLL {
105            return None;
106        }
107
108        // Validate mandatory fields
109        let latitude: f64 = self.parse_field(1)?;
110        let lat_direction = self.parse_field_char(2)?;
111        let longitude: f64 = self.parse_field(3)?;
112        let lon_direction = self.parse_field_char(4)?;
113        let time_str = self.get_field_str(5)?;
114        let status = self.parse_field_char(6)?;
115
116        // Copy time to fixed array
117        let mut time_data = [0u8; 16];
118        let time_bytes = time_str.as_bytes();
119        let time_len = time_bytes.len().min(16) as u8;
120        time_data[..time_len as usize].copy_from_slice(&time_bytes[..time_len as usize]);
121
122        Some(GllData {
123            talker_id: self.talker_id,
124            latitude,
125            lat_direction,
126            longitude,
127            lon_direction,
128            time_data,
129            time_len,
130            status,
131        })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use crate::NmeaParser;
138
139    #[test]
140    fn test_gll_complete_message() {
141        let parser = NmeaParser::new();
142        let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
143
144        let result = parser.parse_sentence_complete(sentence);
145
146        assert!(result.is_some());
147        let msg = result.unwrap();
148        let gll = msg.as_gll();
149        assert!(gll.is_some());
150
151        let gll_data = gll.unwrap();
152        assert_eq!(gll_data.latitude, 4916.45);
153        assert_eq!(gll_data.lat_direction, 'N');
154        assert_eq!(gll_data.longitude, 12311.12);
155        assert_eq!(gll_data.lon_direction, 'W');
156        assert_eq!(gll_data.time(), "225444");
157        assert_eq!(gll_data.status, 'A');
158    }
159
160    #[test]
161    fn test_gll_void_status() {
162        let parser = NmeaParser::new();
163        let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,V*1D\r\n";
164
165        let result = parser.parse_sentence_complete(sentence);
166
167        assert!(result.is_some());
168        let msg = result.unwrap();
169        let gll = msg.as_gll();
170        assert!(gll.is_some());
171
172        let gll_data = gll.unwrap();
173        assert_eq!(gll_data.status, 'V');
174    }
175
176    #[test]
177    fn test_gll_south_east() {
178        let parser = NmeaParser::new();
179        let sentence = b"$GPGLL,3723.2475,S,14507.3647,E,225444,A*1D\r\n";
180
181        let result = parser.parse_sentence_complete(sentence);
182
183        assert!(result.is_some());
184        let msg = result.unwrap();
185        let gll = msg.as_gll();
186        assert!(gll.is_some());
187
188        let gll_data = gll.unwrap();
189        assert_eq!(gll_data.lat_direction, 'S');
190        assert_eq!(gll_data.lon_direction, 'E');
191    }
192
193    #[test]
194    fn test_gll_missing_latitude() {
195        let parser = NmeaParser::new();
196        let sentence = b"$GPGLL,,N,12311.12,W,225444,A*1D\r\n";
197
198        let result = parser.parse_sentence_complete(sentence);
199
200        // Should return None because a mandatory field is missing
201        assert!(result.is_none());
202    }
203
204    #[test]
205    fn test_gll_missing_longitude() {
206        let parser = NmeaParser::new();
207        let sentence = b"$GPGLL,4916.45,N,,W,225444,A*1D\r\n";
208
209        let result = parser.parse_sentence_complete(sentence);
210
211        // Should return None because a mandatory field is missing
212        assert!(result.is_none());
213    }
214
215    #[test]
216    fn test_gll_missing_time() {
217        let parser = NmeaParser::new();
218        let sentence = b"$GPGLL,4916.45,N,12311.12,W,,A*1D\r\n";
219
220        let result = parser.parse_sentence_complete(sentence);
221
222        // Should return None because a mandatory field is missing
223        assert!(result.is_none());
224    }
225
226    #[test]
227    fn test_gll_missing_status() {
228        let parser = NmeaParser::new();
229        let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,*1D\r\n";
230
231        let result = parser.parse_sentence_complete(sentence);
232
233        // Should return None because a mandatory field is missing
234        assert!(result.is_none());
235    }
236
237    #[test]
238    fn test_gll_invalid_latitude() {
239        let parser = NmeaParser::new();
240        let sentence = b"$GPGLL,INVALID,N,12311.12,W,225444,A*1D\r\n";
241
242        let result = parser.parse_sentence_complete(sentence);
243
244        // Should return None because a mandatory field is missing
245        assert!(result.is_none());
246    }
247
248    #[test]
249    fn test_gll_numeric_precision() {
250        let parser = NmeaParser::new();
251        let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
252
253        let result = parser.parse_sentence_complete(sentence);
254
255        assert!(result.is_some());
256        let msg = result.unwrap();
257        let gll = msg.as_gll();
258        assert!(gll.is_some());
259
260        let gll_data = gll.unwrap();
261        assert!((gll_data.latitude - 4916.45).abs() < 0.01);
262        assert!((gll_data.longitude - 12311.12).abs() < 0.01);
263    }
264
265    #[test]
266    fn test_gll_high_precision_coordinates() {
267        let parser = NmeaParser::new();
268        let sentence = b"$GPGLL,4916.453789,N,12311.125678,W,225444,A*1D\r\n";
269
270        let result = parser.parse_sentence_complete(sentence);
271
272        assert!(result.is_some());
273        let msg = result.unwrap();
274        let gll = msg.as_gll();
275        assert!(gll.is_some());
276
277        let gll_data = gll.unwrap();
278        assert!((gll_data.latitude - 4916.453789).abs() < 0.000001);
279        assert!((gll_data.longitude - 12311.125678).abs() < 0.000001);
280    }
281
282    #[test]
283    fn test_gll_different_talker_id() {
284        let parser = NmeaParser::new();
285        // GNGLL is multi-GNSS
286        let sentence = b"$GNGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
287
288        let result = parser.parse_sentence_complete(sentence);
289
290        assert!(result.is_some());
291        let msg = result.unwrap();
292        let gll = msg.as_gll();
293        assert!(gll.is_some());
294
295        let gll_data = gll.unwrap();
296        assert_eq!(gll_data.talker_id, crate::types::TalkerId::GN);
297    }
298
299    #[test]
300    fn test_gll_all_constellation_types() {
301        let parser = NmeaParser::new();
302
303        // Test GPS
304        let gp_sentence = b"$GPGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
305        let gp_result = parser.parse_sentence_complete(gp_sentence);
306        assert!(gp_result.is_some());
307        let gp_msg = gp_result.unwrap();
308        let gp_gll = gp_msg.as_gll().unwrap();
309        assert_eq!(gp_gll.talker_id, crate::types::TalkerId::GP);
310
311        // Test BeiDou (GB)
312        let gb_sentence = b"$GBGLL,4916.45,N,12311.12,W,225444,A*1D\r\n";
313        let gb_result = parser.parse_sentence_complete(gb_sentence);
314        assert!(gb_result.is_some());
315        let gb_msg = gb_result.unwrap();
316        let gb_gll = gb_msg.as_gll().unwrap();
317        assert_eq!(gb_gll.talker_id, crate::types::TalkerId::GB);
318    }
319
320    #[test]
321    fn test_gll_time_with_decimals() {
322        let parser = NmeaParser::new();
323        let sentence = b"$GPGLL,4916.45,N,12311.12,W,225444.50,A*1D\r\n";
324
325        let result = parser.parse_sentence_complete(sentence);
326
327        assert!(result.is_some());
328        let msg = result.unwrap();
329        let gll = msg.as_gll();
330        assert!(gll.is_some());
331
332        let gll_data = gll.unwrap();
333        assert_eq!(gll_data.time(), "225444.50");
334    }
335}