rustedbytes_nmea/message/
gsv.rs

1//! GSV (GPS Satellites in View) message implementation
2//!
3//! The GSV message provides detailed information about satellites in view,
4//! including their PRN (Pseudo-Random Noise) number, elevation, azimuth,
5//! and signal-to-noise ratio (SNR). This message can span multiple sentences
6//! if more than 4 satellites are visible.
7//!
8//! ## Message Format
9//!
10//! ```text
11//! $GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75
12//! ```
13//!
14//! ## Fields
15//!
16//! | Index | Field | Type | Mandatory | Description |
17//! |-------|-------|------|-----------|-------------|
18//! | 0 | Sentence ID | String | Yes | Message type (GPGSV, GNGSV, etc.) |
19//! | 1 | Num Messages | u8 | Yes | Total number of GSV messages |
20//! | 2 | Message Num | u8 | Yes | Current message number (1-based) |
21//! | 3 | Satellites | u8 | Yes | Total satellites in view |
22//! | 4-7 | Sat 1 Info | - | No | PRN, elevation, azimuth, SNR |
23//! | 8-11 | Sat 2 Info | - | No | PRN, elevation, azimuth, SNR |
24//! | 12-15 | Sat 3 Info | - | No | PRN, elevation, azimuth, SNR |
25//! | 16-19 | Sat 4 Info | - | No | PRN, elevation, azimuth, SNR |
26//!
27//! ## Satellite Information
28//!
29//! Each satellite includes 4 fields:
30//! - **PRN**: Satellite PRN (Pseudo-Random Noise) number (u8)
31//! - **Elevation**: Elevation in degrees, 0-90° (u16)
32//! - **Azimuth**: Azimuth in degrees, 0-359° (u16)
33//! - **SNR**: Signal-to-noise ratio in dB, 0-99 (u8)
34//!
35//! ## Multi-Sentence Messages
36//!
37//! If more than 4 satellites are visible, multiple GSV messages are sent.
38//! Each message contains up to 4 satellites. The num_messages field indicates
39//! how many total messages to expect, and message_num indicates which message
40//! this is in the sequence.
41//!
42//! ## Example
43//!
44//! ```text
45//! $GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75
46//! ```
47//!
48//! This represents:
49//! - 2 total GSV messages
50//! - This is message 1 of 2
51//! - 8 satellites in view
52//! - Satellite 1: PRN=01, elevation=40°, azimuth=83°, SNR=46dB
53//! - Satellite 2: PRN=02, elevation=17°, azimuth=308°, SNR=41dB
54//! - Satellite 3: PRN=12, elevation=7°, azimuth=344°, SNR=39dB
55//! - Satellite 4: PRN=14, elevation=22°, azimuth=228°, SNR=45dB
56
57use crate::message::ParsedSentence;
58use crate::types::{MessageType, TalkerId};
59
60/// GSV - GPS Satellites in view parameters
61#[derive(Debug, Clone)]
62pub struct GsvData {
63    pub talker_id: TalkerId,
64    pub num_messages: u8,
65    pub message_num: u8,
66    pub satellites_in_view: u8,
67    pub satellite_info: [Option<SatelliteInfo>; 4],
68}
69
70/// Information about a single satellite
71#[derive(Debug, Clone)]
72pub struct SatelliteInfo {
73    pub prn: Option<u8>,
74    pub elevation: Option<u16>,
75    pub azimuth: Option<u16>,
76    pub snr: Option<u8>,
77}
78
79impl ParsedSentence {
80    /// Extract GSV message parameters
81    ///
82    /// Parses the GSV (GPS Satellites in View) message and returns a structured
83    /// `GsvData` object containing all parsed fields.
84    ///
85    /// # Returns
86    ///
87    /// - `Some(GsvData)` if the message is a valid GSV message with all mandatory fields
88    /// - `None` if:
89    ///   - The message is not a GSV message
90    ///   - Any mandatory field is missing or invalid
91    ///
92    /// # Mandatory Fields
93    ///
94    /// - Number of messages (field 1)
95    /// - Message number (field 2)
96    /// - Total satellites in view (field 3)
97    ///
98    /// # Optional Fields
99    ///
100    /// - Up to 4 satellite information blocks (fields 4-19)
101    /// - Each satellite block contains: PRN, elevation, azimuth, SNR
102    /// - Individual fields within a satellite block are also optional
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// use rustedbytes_nmea::{NmeaParser, MessageType};
108    ///
109    /// let parser = NmeaParser::new();
110    /// let sentence = b"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
111    ///
112    /// let result = parser.parse_bytes(sentence);
113    /// if let Ok((Some(msg), _consumed)) = result {
114    ///     if let Some(gsv) = msg.as_gsv() {
115    ///         assert_eq!(gsv.num_messages, 2);
116    ///         assert_eq!(gsv.satellites_in_view, 8);
117    ///         assert!(gsv.satellite_info[0].is_some());
118    ///     }
119    /// }
120    /// ```
121    pub fn as_gsv(&self) -> Option<GsvData> {
122        if self.message_type != MessageType::GSV {
123            return None;
124        }
125
126        // Validate mandatory fields
127        let num_messages: u8 = self.parse_field(1)?;
128        let message_num: u8 = self.parse_field(2)?;
129        let satellites_in_view: u8 = self.parse_field(3)?;
130
131        let sat1 = if self.get_field_str(4).is_some() {
132            Some(SatelliteInfo {
133                prn: self.parse_field(4),
134                elevation: self.parse_field(5),
135                azimuth: self.parse_field(6),
136                snr: self.parse_field(7),
137            })
138        } else {
139            None
140        };
141
142        let sat2 = if self.get_field_str(8).is_some() {
143            Some(SatelliteInfo {
144                prn: self.parse_field(8),
145                elevation: self.parse_field(9),
146                azimuth: self.parse_field(10),
147                snr: self.parse_field(11),
148            })
149        } else {
150            None
151        };
152
153        let sat3 = if self.get_field_str(12).is_some() {
154            Some(SatelliteInfo {
155                prn: self.parse_field(12),
156                elevation: self.parse_field(13),
157                azimuth: self.parse_field(14),
158                snr: self.parse_field(15),
159            })
160        } else {
161            None
162        };
163
164        let sat4 = if self.get_field_str(16).is_some() {
165            Some(SatelliteInfo {
166                prn: self.parse_field(16),
167                elevation: self.parse_field(17),
168                azimuth: self.parse_field(18),
169                snr: self.parse_field(19),
170            })
171        } else {
172            None
173        };
174
175        Some(GsvData {
176            talker_id: self.talker_id,
177            num_messages,
178            message_num,
179            satellites_in_view,
180            satellite_info: [sat1, sat2, sat3, sat4],
181        })
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::NmeaParser;
188
189    #[test]
190    fn test_gsv_complete_message() {
191        let parser = NmeaParser::new();
192        let sentence = b"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
193
194        let result = parser.parse_sentence_complete(sentence);
195
196        assert!(result.is_some());
197        let msg = result.unwrap();
198        let gsv = msg.as_gsv();
199        assert!(gsv.is_some());
200
201        let gsv_data = gsv.unwrap();
202        assert_eq!(gsv_data.num_messages, 2);
203        assert_eq!(gsv_data.message_num, 1);
204        assert_eq!(gsv_data.satellites_in_view, 8);
205
206        // Check first satellite
207        assert!(gsv_data.satellite_info[0].is_some());
208        let sat1 = gsv_data.satellite_info[0].as_ref().unwrap();
209        assert_eq!(sat1.prn, Some(1));
210        assert_eq!(sat1.elevation, Some(40));
211        assert_eq!(sat1.azimuth, Some(83));
212        assert_eq!(sat1.snr, Some(46));
213
214        // Check second satellite
215        assert!(gsv_data.satellite_info[1].is_some());
216        let sat2 = gsv_data.satellite_info[1].as_ref().unwrap();
217        assert_eq!(sat2.prn, Some(2));
218        assert_eq!(sat2.elevation, Some(17));
219        assert_eq!(sat2.azimuth, Some(308));
220        assert_eq!(sat2.snr, Some(41));
221    }
222
223    #[test]
224    fn test_gsv_partial_satellites() {
225        let parser = NmeaParser::new();
226        let sentence = b"$GPGSV,1,1,02,01,40,083,46,02,17,308,*75\r\n";
227
228        let result = parser.parse_sentence_complete(sentence);
229
230        assert!(result.is_some());
231        let msg = result.unwrap();
232        let gsv = msg.as_gsv();
233        assert!(gsv.is_some());
234
235        let gsv_data = gsv.unwrap();
236        assert_eq!(gsv_data.satellites_in_view, 2);
237
238        // First satellite should be complete
239        assert!(gsv_data.satellite_info[0].is_some());
240        let sat1 = gsv_data.satellite_info[0].as_ref().unwrap();
241        assert_eq!(sat1.prn, Some(1));
242
243        // Second satellite should have missing SNR
244        assert!(gsv_data.satellite_info[1].is_some());
245        let sat2 = gsv_data.satellite_info[1].as_ref().unwrap();
246        assert_eq!(sat2.prn, Some(2));
247        assert_eq!(sat2.snr, None);
248
249        // Third and fourth should be None
250        assert!(gsv_data.satellite_info[2].is_none());
251        assert!(gsv_data.satellite_info[3].is_none());
252    }
253
254    #[test]
255    fn test_gsv_single_satellite() {
256        let parser = NmeaParser::new();
257        let sentence = b"$GPGSV,1,1,01,01,40,083,46*75\r\n";
258
259        let result = parser.parse_sentence_complete(sentence);
260
261        assert!(result.is_some());
262        let msg = result.unwrap();
263        let gsv = msg.as_gsv();
264        assert!(gsv.is_some());
265
266        let gsv_data = gsv.unwrap();
267        assert_eq!(gsv_data.satellites_in_view, 1);
268        assert!(gsv_data.satellite_info[0].is_some());
269        assert!(gsv_data.satellite_info[1].is_none());
270    }
271
272    #[test]
273    fn test_gsv_missing_num_messages() {
274        let parser = NmeaParser::new();
275        let sentence = b"$GPGSV,,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\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_gsv_missing_message_num() {
285        let parser = NmeaParser::new();
286        let sentence = b"$GPGSV,2,,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\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_gsv_missing_satellites_in_view() {
296        let parser = NmeaParser::new();
297        let sentence = b"$GPGSV,2,1,,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
298
299        let result = parser.parse_sentence_complete(sentence);
300
301        // Should return None because a mandatory field is missing
302        assert!(result.is_none());
303    }
304
305    #[test]
306    fn test_gsv_satellite_partial_info() {
307        let parser = NmeaParser::new();
308        // Satellite with PRN but missing other fields
309        let sentence = b"$GPGSV,1,1,01,01,,,*75\r\n";
310
311        let result = parser.parse_sentence_complete(sentence);
312
313        assert!(result.is_some());
314        let msg = result.unwrap();
315        let gsv = msg.as_gsv();
316        assert!(gsv.is_some());
317
318        let gsv_data = gsv.unwrap();
319        assert!(gsv_data.satellite_info[0].is_some());
320        let sat = gsv_data.satellite_info[0].as_ref().unwrap();
321        assert_eq!(sat.prn, Some(1));
322        assert_eq!(sat.elevation, None);
323        assert_eq!(sat.azimuth, None);
324        assert_eq!(sat.snr, None);
325    }
326
327    #[test]
328    fn test_gsv_zero_elevation_azimuth() {
329        let parser = NmeaParser::new();
330        let sentence = b"$GPGSV,1,1,01,01,0,0,46*75\r\n";
331
332        let result = parser.parse_sentence_complete(sentence);
333
334        assert!(result.is_some());
335        let msg = result.unwrap();
336        let gsv = msg.as_gsv();
337        assert!(gsv.is_some());
338
339        let gsv_data = gsv.unwrap();
340        let sat = gsv_data.satellite_info[0].as_ref().unwrap();
341        assert_eq!(sat.elevation, Some(0));
342        assert_eq!(sat.azimuth, Some(0));
343    }
344
345    #[test]
346    fn test_gsv_max_elevation() {
347        let parser = NmeaParser::new();
348        let sentence = b"$GPGSV,1,1,01,01,90,180,46*75\r\n";
349
350        let result = parser.parse_sentence_complete(sentence);
351
352        assert!(result.is_some());
353        let msg = result.unwrap();
354        let gsv = msg.as_gsv();
355        assert!(gsv.is_some());
356
357        let gsv_data = gsv.unwrap();
358        let sat = gsv_data.satellite_info[0].as_ref().unwrap();
359        assert_eq!(sat.elevation, Some(90));
360    }
361
362    #[test]
363    fn test_gsv_max_azimuth() {
364        let parser = NmeaParser::new();
365        let sentence = b"$GPGSV,1,1,01,01,45,359,46*75\r\n";
366
367        let result = parser.parse_sentence_complete(sentence);
368
369        assert!(result.is_some());
370        let msg = result.unwrap();
371        let gsv = msg.as_gsv();
372        assert!(gsv.is_some());
373
374        let gsv_data = gsv.unwrap();
375        let sat = gsv_data.satellite_info[0].as_ref().unwrap();
376        assert_eq!(sat.azimuth, Some(359));
377    }
378
379    #[test]
380    fn test_gsv_different_talker_id() {
381        let parser = NmeaParser::new();
382        // GNGSV is multi-GNSS
383        let sentence = b"$GNGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
384
385        let result = parser.parse_sentence_complete(sentence);
386
387        assert!(result.is_some());
388        let msg = result.unwrap();
389        let gsv = msg.as_gsv();
390        assert!(gsv.is_some());
391
392        let gsv_data = gsv.unwrap();
393        assert_eq!(gsv_data.talker_id, crate::types::TalkerId::GN);
394    }
395
396    #[test]
397    fn test_gsv_satellites_from_different_constellations() {
398        let parser = NmeaParser::new();
399
400        // Test GPS satellites
401        let gp_sentence =
402            b"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
403        let gp_result = parser.parse_sentence_complete(gp_sentence);
404        assert!(gp_result.is_some());
405        let gp_msg = gp_result.unwrap();
406        let gp_gsv = gp_msg.as_gsv().unwrap();
407        assert_eq!(gp_gsv.talker_id, crate::types::TalkerId::GP);
408        assert_eq!(gp_gsv.satellites_in_view, 8);
409
410        // Test GLONASS satellites
411        let gl_sentence =
412            b"$GLGSV,2,1,08,65,40,083,46,66,17,308,41,75,07,344,39,76,22,228,45*75\r\n";
413        let gl_result = parser.parse_sentence_complete(gl_sentence);
414        assert!(gl_result.is_some());
415        let gl_msg = gl_result.unwrap();
416        let gl_gsv = gl_msg.as_gsv().unwrap();
417        assert_eq!(gl_gsv.talker_id, crate::types::TalkerId::GL);
418
419        // Test Galileo satellites
420        let ga_sentence =
421            b"$GAGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
422        let ga_result = parser.parse_sentence_complete(ga_sentence);
423        assert!(ga_result.is_some());
424        let ga_msg = ga_result.unwrap();
425        let ga_gsv = ga_msg.as_gsv().unwrap();
426        assert_eq!(ga_gsv.talker_id, crate::types::TalkerId::GA);
427    }
428
429    #[test]
430    fn test_gsv_multiple_message_sequence() {
431        let parser = NmeaParser::new();
432
433        // First message of sequence
434        let sentence1 = b"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n";
435        let result1 = parser.parse_sentence_complete(sentence1);
436        assert!(result1.is_some());
437        let msg1 = result1.unwrap();
438        let gsv1_data = msg1.as_gsv().unwrap();
439        assert_eq!(gsv1_data.message_num, 1);
440        assert_eq!(gsv1_data.num_messages, 2);
441
442        // Second message of sequence
443        let sentence2 = b"$GPGSV,2,2,08,20,35,073,44,21,25,210,42,25,15,120,40,32,10,045,38*75\r\n";
444        let result2 = parser.parse_sentence_complete(sentence2);
445        assert!(result2.is_some());
446        let msg2 = result2.unwrap();
447        let gsv2_data = msg2.as_gsv().unwrap();
448        assert_eq!(gsv2_data.message_num, 2);
449        assert_eq!(gsv2_data.num_messages, 2);
450    }
451}