rustedbytes_nmea/message/
gsa.rs

1//! GSA (GPS DOP and Active Satellites) message implementation
2//!
3//! The GSA message provides information about the GPS receiver operating mode,
4//! active satellites used for navigation, and dilution of precision (DOP) values.
5//! DOP values indicate the quality of the satellite geometry.
6//!
7//! ## Message Format
8//!
9//! ```text
10//! $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39
11//! ```
12//!
13//! ## Fields
14//!
15//! | Index | Field | Type | Mandatory | Description |
16//! |-------|-------|------|-----------|-------------|
17//! | 0 | Sentence ID | String | Yes | Message type (GPGSA, GNGSA, etc.) |
18//! | 1 | Mode | char | Yes | M = Manual, A = Automatic |
19//! | 2 | Fix Type | u8 | Yes | 1=No fix, 2=2D, 3=3D |
20//! | 3-14 | Satellite IDs | u8 | No | PRNs of satellites used (up to 12) |
21//! | 15 | PDOP | f32 | No | Position dilution of precision |
22//! | 16 | HDOP | f32 | No | Horizontal dilution of precision |
23//! | 17 | VDOP | f32 | No | Vertical dilution of precision |
24//!
25//! ## DOP Values
26//!
27//! DOP (Dilution of Precision) values indicate satellite geometry quality:
28//! - **PDOP**: Position (3D) dilution of precision
29//! - **HDOP**: Horizontal (2D) dilution of precision
30//! - **VDOP**: Vertical dilution of precision
31//!
32//! Lower values indicate better precision:
33//! - < 1: Ideal
34//! - 1-2: Excellent
35//! - 2-5: Good
36//! - 5-10: Moderate
37//! - 10-20: Fair
38//! - > 20: Poor
39//!
40//! ## Example
41//!
42//! ```text
43//! $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39
44//! ```
45//!
46//! This represents:
47//! - Mode: Automatic
48//! - Fix type: 3D fix
49//! - Satellites: PRN 04, 05, 09, 12, 24
50//! - PDOP: 2.5
51//! - HDOP: 1.3
52//! - VDOP: 2.1
53
54use crate::message::ParsedSentence;
55use crate::types::{MessageType, TalkerId};
56
57/// GSA - GPS DOP and active satellites parameters
58#[derive(Debug, Clone)]
59pub struct GsaData {
60    pub talker_id: TalkerId,
61    pub mode: char,
62    pub fix_type: u8,
63    pub satellite_ids: [Option<u8>; 12],
64    pub pdop: Option<f32>,
65    pub hdop: Option<f32>,
66    pub vdop: Option<f32>,
67}
68
69impl ParsedSentence {
70    /// Extract GSA message parameters
71    ///
72    /// Parses the GSA (GPS DOP and Active Satellites) message and returns
73    /// a structured `GsaData` object containing all parsed fields.
74    ///
75    /// # Returns
76    ///
77    /// - `Some(GsaData)` if the message is a valid GSA message with all mandatory fields
78    /// - `None` if:
79    ///   - The message is not a GSA message
80    ///   - Any mandatory field is missing or invalid
81    ///
82    /// # Mandatory Fields
83    ///
84    /// - Mode (field 1) - 'M' for manual, 'A' for automatic
85    /// - Fix type (field 2) - 1 for no fix, 2 for 2D, 3 for 3D
86    ///
87    /// # Optional Fields
88    ///
89    /// - Satellite IDs (fields 3-14) - up to 12 satellite PRNs
90    /// - PDOP, HDOP, VDOP (fields 15-17)
91    ///
92    /// # Example
93    ///
94    /// ```
95    /// use rustedbytes_nmea::{NmeaParser, MessageType};
96    ///
97    /// let parser = NmeaParser::new();
98    /// let sentence = b"$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
99    ///
100    /// let result = parser.parse_bytes(sentence);
101    /// if let Ok((Some(msg), _consumed)) = result {
102    ///     if let Some(gsa) = msg.as_gsa() {
103    ///         assert_eq!(gsa.mode, 'A');
104    ///         assert_eq!(gsa.fix_type, 3);
105    ///         assert_eq!(gsa.satellite_ids[0], Some(4));
106    ///     }
107    /// }
108    /// ```
109    pub fn as_gsa(&self) -> Option<GsaData> {
110        if self.message_type != MessageType::GSA {
111            return None;
112        }
113
114        // Validate mandatory fields
115        let mode = self.parse_field_char(1)?;
116        let fix_type: u8 = self.parse_field(2)?;
117
118        Some(GsaData {
119            talker_id: self.talker_id,
120            mode,
121            fix_type,
122            satellite_ids: [
123                self.parse_field(3),
124                self.parse_field(4),
125                self.parse_field(5),
126                self.parse_field(6),
127                self.parse_field(7),
128                self.parse_field(8),
129                self.parse_field(9),
130                self.parse_field(10),
131                self.parse_field(11),
132                self.parse_field(12),
133                self.parse_field(13),
134                self.parse_field(14),
135            ],
136            pdop: self.parse_field(15),
137            hdop: self.parse_field(16),
138            vdop: self.parse_field(17),
139        })
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use crate::NmeaParser;
146
147    #[test]
148    fn test_gsa_complete_message() {
149        let parser = NmeaParser::new();
150        let sentence = b"$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
151
152        let result = parser.parse_sentence_complete(sentence);
153
154        assert!(result.is_some());
155        let msg = result.unwrap();
156        let gsa = msg.as_gsa();
157        assert!(gsa.is_some());
158
159        let gsa_data = gsa.unwrap();
160        assert_eq!(gsa_data.mode, 'A');
161        assert_eq!(gsa_data.fix_type, 3);
162        assert_eq!(gsa_data.satellite_ids[0], Some(4));
163        assert_eq!(gsa_data.satellite_ids[1], Some(5));
164        assert_eq!(gsa_data.satellite_ids[2], None);
165        assert_eq!(gsa_data.satellite_ids[3], Some(9));
166        assert_eq!(gsa_data.satellite_ids[4], Some(12));
167        assert_eq!(gsa_data.pdop, Some(2.5));
168        assert_eq!(gsa_data.hdop, Some(1.3));
169        assert_eq!(gsa_data.vdop, Some(2.1));
170    }
171
172    #[test]
173    fn test_gsa_manual_mode() {
174        let parser = NmeaParser::new();
175        let sentence = b"$GPGSA,M,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
176
177        let result = parser.parse_sentence_complete(sentence);
178
179        assert!(result.is_some());
180        let msg = result.unwrap();
181        let gsa = msg.as_gsa();
182        assert!(gsa.is_some());
183
184        let gsa_data = gsa.unwrap();
185        assert_eq!(gsa_data.mode, 'M');
186    }
187
188    #[test]
189    fn test_gsa_2d_fix() {
190        let parser = NmeaParser::new();
191        let sentence = b"$GPGSA,A,2,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
192
193        let result = parser.parse_sentence_complete(sentence);
194
195        assert!(result.is_some());
196        let msg = result.unwrap();
197        let gsa = msg.as_gsa();
198        assert!(gsa.is_some());
199
200        let gsa_data = gsa.unwrap();
201        assert_eq!(gsa_data.fix_type, 2);
202    }
203
204    #[test]
205    fn test_gsa_no_fix() {
206        let parser = NmeaParser::new();
207        let sentence = b"$GPGSA,A,1,,,,,,,,,,,,,,,*39\r\n";
208
209        let result = parser.parse_sentence_complete(sentence);
210
211        assert!(result.is_some());
212        let msg = result.unwrap();
213        let gsa = msg.as_gsa();
214        assert!(gsa.is_some());
215
216        let gsa_data = gsa.unwrap();
217        assert_eq!(gsa_data.fix_type, 1);
218        assert_eq!(gsa_data.satellite_ids[0], None);
219    }
220
221    #[test]
222    fn test_gsa_partial_satellites() {
223        let parser = NmeaParser::new();
224        let sentence = b"$GPGSA,A,3,01,,,,,,,,,,,,2.5,1.3,2.1*39\r\n";
225
226        let result = parser.parse_sentence_complete(sentence);
227
228        assert!(result.is_some());
229        let msg = result.unwrap();
230        let gsa = msg.as_gsa();
231        assert!(gsa.is_some());
232
233        let gsa_data = gsa.unwrap();
234        assert_eq!(gsa_data.satellite_ids[0], Some(1));
235        assert_eq!(gsa_data.satellite_ids[1], None);
236        assert_eq!(gsa_data.satellite_ids[11], None);
237    }
238
239    #[test]
240    fn test_gsa_all_satellites() {
241        let parser = NmeaParser::new();
242        let sentence = b"$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,2.5,1.3,2.1*39\r\n";
243
244        let result = parser.parse_sentence_complete(sentence);
245
246        assert!(result.is_some());
247        let msg = result.unwrap();
248        let gsa = msg.as_gsa();
249        assert!(gsa.is_some());
250
251        let gsa_data = gsa.unwrap();
252        for i in 0..12 {
253            assert_eq!(gsa_data.satellite_ids[i], Some((i + 1) as u8));
254        }
255    }
256
257    #[test]
258    fn test_gsa_without_dop() {
259        let parser = NmeaParser::new();
260        let sentence = b"$GPGSA,A,3,04,05,,09,12,,,24,,,,,,,*39\r\n";
261
262        let result = parser.parse_sentence_complete(sentence);
263
264        assert!(result.is_some());
265        let msg = result.unwrap();
266        let gsa = msg.as_gsa();
267        assert!(gsa.is_some());
268
269        let gsa_data = gsa.unwrap();
270        assert_eq!(gsa_data.pdop, None);
271        assert_eq!(gsa_data.hdop, None);
272        assert_eq!(gsa_data.vdop, None);
273    }
274
275    #[test]
276    fn test_gsa_missing_mode() {
277        let parser = NmeaParser::new();
278        let sentence = b"$GPGSA,,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
279
280        let result = parser.parse_sentence_complete(sentence);
281
282        // Should return None because a mandatory field is missing
283        assert!(result.is_none());
284    }
285
286    #[test]
287    fn test_gsa_missing_fix_type() {
288        let parser = NmeaParser::new();
289        let sentence = b"$GPGSA,A,,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
290
291        let result = parser.parse_sentence_complete(sentence);
292
293        // Should return None because a mandatory field is missing
294        assert!(result.is_none());
295    }
296
297    #[test]
298    fn test_gsa_dop_precision() {
299        let parser = NmeaParser::new();
300        let sentence = b"$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
301
302        let result = parser.parse_sentence_complete(sentence);
303
304        assert!(result.is_some());
305        let msg = result.unwrap();
306        let gsa = msg.as_gsa();
307        assert!(gsa.is_some());
308
309        let gsa_data = gsa.unwrap();
310        assert!((gsa_data.pdop.unwrap() - 2.5).abs() < 0.01);
311        assert!((gsa_data.hdop.unwrap() - 1.3).abs() < 0.01);
312        assert!((gsa_data.vdop.unwrap() - 2.1).abs() < 0.01);
313    }
314
315    #[test]
316    fn test_gsa_different_talker_id() {
317        let parser = NmeaParser::new();
318        // GNGSA is multi-GNSS
319        let sentence = b"$GNGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
320
321        let result = parser.parse_sentence_complete(sentence);
322
323        assert!(result.is_some());
324        let msg = result.unwrap();
325        let gsa = msg.as_gsa();
326        assert!(gsa.is_some());
327
328        let gsa_data = gsa.unwrap();
329        assert_eq!(gsa_data.talker_id, crate::types::TalkerId::GN);
330    }
331
332    #[test]
333    fn test_gsa_constellation_tracking() {
334        let parser = NmeaParser::new();
335
336        // Test BeiDou
337        let bd_sentence = b"$BDGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
338        let bd_result = parser.parse_sentence_complete(bd_sentence);
339        assert!(bd_result.is_some());
340        let bd_msg = bd_result.unwrap();
341        let bd_gsa = bd_msg.as_gsa().unwrap();
342        assert_eq!(bd_gsa.talker_id, crate::types::TalkerId::BD);
343
344        // Test QZSS
345        let qz_sentence = b"$QZGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n";
346        let qz_result = parser.parse_sentence_complete(qz_sentence);
347        assert!(qz_result.is_some());
348        let qz_msg = qz_result.unwrap();
349        let qz_gsa = qz_msg.as_gsa().unwrap();
350        assert_eq!(qz_gsa.talker_id, crate::types::TalkerId::QZ);
351    }
352}