rustedbytes_nmea/message/
rmc.rs

1//! RMC (Recommended Minimum Navigation Information) message implementation
2//!
3//! The RMC message is one of the most important NMEA sentences as it provides
4//! minimum GPS/GNSS fix data including time, date, position, speed, and course.
5//! It's commonly referred to as the "Recommended Minimum" sentence.
6//!
7//! ## Message Format
8//!
9//! ```text
10//! $GPRMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh
11//! ```
12//!
13//! ## Fields
14//!
15//! | Index | Field | Type | Mandatory | Description |
16//! |-------|-------|------|-----------|-------------|
17//! | 0 | Sentence ID | String | Yes | Message type (GPRMC, GNRMC, etc.) |
18//! | 1 | UTC Time | String | Yes | hhmmss.ss format |
19//! | 2 | Status | char | Yes | A = Valid, V = Invalid |
20//! | 3 | Latitude | f64 | Yes | ddmm.mmmmm format |
21//! | 4 | N/S Indicator | char | Yes | N = North, S = South |
22//! | 5 | Longitude | f64 | Yes | dddmm.mmmmm format |
23//! | 6 | E/W Indicator | char | Yes | E = East, W = West |
24//! | 7 | Speed (knots) | f32 | Yes | Speed over ground in knots |
25//! | 8 | Track Angle | f32 | Yes | Track angle in degrees |
26//! | 9 | Date | String | Yes | ddmmyy format |
27//! | 10 | Mag Variation | f32 | No | Magnetic variation in degrees |
28//! | 11 | Mag Var Dir | char | No | E = East, W = West |
29//!
30//! ## Example
31//!
32//! ```text
33//! $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
34//! ```
35//!
36//! This represents:
37//! - Time: 12:35:19 UTC
38//! - Status: Active (valid fix)
39//! - Position: 48°07.038'N, 11°31.000'E
40//! - Speed: 22.4 knots
41//! - Track angle: 84.4°
42//! - Date: March 23, 1994
43//! - Magnetic variation: 3.1° West
44
45use crate::message::ParsedSentence;
46use crate::types::{MessageType, TalkerId};
47
48/// RMC - Recommended Minimum Navigation Information parameters
49#[derive(Debug, Clone)]
50pub struct RmcData {
51    pub talker_id: TalkerId,
52    time_data: [u8; 16],
53    time_len: u8,
54    pub status: char,
55    pub latitude: f64,
56    pub lat_direction: char,
57    pub longitude: f64,
58    pub lon_direction: char,
59    pub speed_knots: f32,
60    pub track_angle: f32,
61    date_data: [u8; 8],
62    date_len: u8,
63    pub magnetic_variation: Option<f32>,
64    pub mag_var_direction: Option<char>,
65}
66
67impl RmcData {
68    /// Get time as string slice
69    pub fn time(&self) -> &str {
70        core::str::from_utf8(&self.time_data[..self.time_len as usize]).unwrap_or("")
71    }
72
73    /// Get date as string slice
74    pub fn date(&self) -> &str {
75        core::str::from_utf8(&self.date_data[..self.date_len as usize]).unwrap_or("")
76    }
77}
78
79impl ParsedSentence {
80    /// Extract RMC message parameters
81    ///
82    /// Parses the RMC (Recommended Minimum Navigation Information) message and
83    /// returns a structured `RmcData` object containing all parsed fields.
84    ///
85    /// # Returns
86    ///
87    /// - `Some(RmcData)` if the message is a valid RMC message with all mandatory fields
88    /// - `None` if:
89    ///   - The message is not an RMC message
90    ///   - Any mandatory field is missing or invalid
91    ///
92    /// # Mandatory Fields
93    ///
94    /// - Time (field 1)
95    /// - Status (field 2) - 'A' for active, 'V' for void
96    /// - Latitude (field 3)
97    /// - Latitude direction (field 4)
98    /// - Longitude (field 5)
99    /// - Longitude direction (field 6)
100    /// - Speed in knots (field 7)
101    /// - Track angle (field 8)
102    /// - Date (field 9)
103    ///
104    /// # Optional Fields
105    ///
106    /// - Magnetic variation and direction are optional
107    ///
108    /// # Example
109    ///
110    /// ```
111    /// use rustedbytes_nmea::{NmeaParser, MessageType};
112    ///
113    /// let parser = NmeaParser::new();
114    /// let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
115    ///
116    /// let result = parser.parse_bytes(sentence);
117    /// if let Ok((Some(msg), _consumed)) = result {
118    ///     if let Some(rmc) = msg.as_rmc() {
119    ///         assert_eq!(rmc.time(), "123519");
120    ///         assert_eq!(rmc.status, 'A');
121    ///         assert_eq!(rmc.speed_knots, 22.4);
122    ///     }
123    /// }
124    /// ```
125    pub fn as_rmc(&self) -> Option<RmcData> {
126        if self.message_type != MessageType::RMC {
127            return None;
128        }
129
130        // Validate mandatory fields
131        let time_str = self.get_field_str(1)?;
132        let status = self.parse_field_char(2)?;
133        let latitude: f64 = self.parse_field(3)?;
134        let lat_direction = self.parse_field_char(4)?;
135        let longitude: f64 = self.parse_field(5)?;
136        let lon_direction = self.parse_field_char(6)?;
137        let speed_knots: f32 = self.parse_field(7)?;
138        let track_angle: f32 = self.parse_field(8)?;
139        let date_str = self.get_field_str(9)?;
140
141        // Copy time to fixed array
142        let mut time_data = [0u8; 16];
143        let time_bytes = time_str.as_bytes();
144        let time_len = time_bytes.len().min(16) as u8;
145        time_data[..time_len as usize].copy_from_slice(&time_bytes[..time_len as usize]);
146
147        // Copy date to fixed array
148        let mut date_data = [0u8; 8];
149        let date_bytes = date_str.as_bytes();
150        let date_len = date_bytes.len().min(8) as u8;
151        date_data[..date_len as usize].copy_from_slice(&date_bytes[..date_len as usize]);
152
153        Some(RmcData {
154            talker_id: self.talker_id,
155            time_data,
156            time_len,
157            status,
158            latitude,
159            lat_direction,
160            longitude,
161            lon_direction,
162            speed_knots,
163            track_angle,
164            date_data,
165            date_len,
166            magnetic_variation: self.parse_field(10),
167            mag_var_direction: self.parse_field_char(11),
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use crate::NmeaParser;
175
176    #[test]
177    fn test_rmc_complete_message() {
178        let parser = NmeaParser::new();
179        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
180
181        let result = parser.parse_sentence_complete(sentence);
182
183        assert!(result.is_some());
184        let msg = result.unwrap();
185        let rmc = msg.as_rmc();
186        assert!(rmc.is_some());
187
188        let rmc_data = rmc.unwrap();
189        assert_eq!(rmc_data.time(), "123519");
190        assert_eq!(rmc_data.status, 'A');
191        assert_eq!(rmc_data.latitude, 4807.038);
192        assert_eq!(rmc_data.lat_direction, 'N');
193        assert_eq!(rmc_data.longitude, 1131.000);
194        assert_eq!(rmc_data.lon_direction, 'E');
195        assert_eq!(rmc_data.speed_knots, 22.4);
196        assert_eq!(rmc_data.track_angle, 84.4);
197        assert_eq!(rmc_data.date(), "230394");
198        assert_eq!(rmc_data.magnetic_variation, Some(3.1));
199        assert_eq!(rmc_data.mag_var_direction, Some('W'));
200    }
201
202    #[test]
203    fn test_rmc_void_status() {
204        let parser = NmeaParser::new();
205        let sentence = b"$GPRMC,123519,V,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
206
207        let result = parser.parse_sentence_complete(sentence);
208
209        assert!(result.is_some());
210        let msg = result.unwrap();
211        let rmc = msg.as_rmc();
212        assert!(rmc.is_some());
213
214        let rmc_data = rmc.unwrap();
215        assert_eq!(rmc_data.status, 'V');
216    }
217
218    #[test]
219    fn test_rmc_without_magnetic_variation() {
220        let parser = NmeaParser::new();
221        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,,*6A\r\n";
222
223        let result = parser.parse_sentence_complete(sentence);
224
225        assert!(result.is_some());
226        let msg = result.unwrap();
227        let rmc = msg.as_rmc();
228        assert!(rmc.is_some());
229
230        let rmc_data = rmc.unwrap();
231        assert_eq!(rmc_data.magnetic_variation, None);
232        assert_eq!(rmc_data.mag_var_direction, None);
233    }
234
235    #[test]
236    fn test_rmc_missing_time() {
237        let parser = NmeaParser::new();
238        let sentence = b"$GPRMC,,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
239
240        let result = parser.parse_sentence_complete(sentence);
241
242        // Should return None because a mandatory field is missing
243        assert!(result.is_none());
244    }
245
246    #[test]
247    fn test_rmc_missing_status() {
248        let parser = NmeaParser::new();
249        let sentence = b"$GPRMC,123519,,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
250
251        let result = parser.parse_sentence_complete(sentence);
252
253        // Should return None because a mandatory field is missing
254        assert!(result.is_none());
255    }
256
257    #[test]
258    fn test_rmc_missing_date() {
259        let parser = NmeaParser::new();
260        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,,003.1,W*6A\r\n";
261
262        let result = parser.parse_sentence_complete(sentence);
263
264        // Should return None because a mandatory field is missing
265        assert!(result.is_none());
266    }
267
268    #[test]
269    fn test_rmc_missing_speed() {
270        let parser = NmeaParser::new();
271        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,,084.4,230394,003.1,W*6A\r\n";
272
273        let result = parser.parse_sentence_complete(sentence);
274
275        // Should return None because a mandatory field is missing
276        assert!(result.is_none());
277    }
278
279    #[test]
280    fn test_rmc_missing_track_angle() {
281        let parser = NmeaParser::new();
282        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,,230394,003.1,W*6A\r\n";
283
284        let result = parser.parse_sentence_complete(sentence);
285
286        // Should return None because a mandatory field is missing
287        assert!(result.is_none());
288    }
289
290    #[test]
291    fn test_rmc_zero_speed() {
292        let parser = NmeaParser::new();
293        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,0.0,0.0,230394,003.1,W*6A\r\n";
294
295        let result = parser.parse_sentence_complete(sentence);
296
297        assert!(result.is_some());
298        let msg = result.unwrap();
299        let rmc = msg.as_rmc();
300        assert!(rmc.is_some());
301
302        let rmc_data = rmc.unwrap();
303        assert_eq!(rmc_data.speed_knots, 0.0);
304        assert_eq!(rmc_data.track_angle, 0.0);
305    }
306
307    #[test]
308    fn test_rmc_numeric_precision() {
309        let parser = NmeaParser::new();
310        let sentence = b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
311
312        let result = parser.parse_sentence_complete(sentence);
313
314        assert!(result.is_some());
315        let msg = result.unwrap();
316        let rmc = msg.as_rmc();
317        assert!(rmc.is_some());
318
319        let rmc_data = rmc.unwrap();
320        assert!((rmc_data.latitude - 4807.038).abs() < 0.001);
321        assert!((rmc_data.longitude - 1131.000).abs() < 0.001);
322        assert!((rmc_data.speed_knots - 22.4).abs() < 0.1);
323        assert!((rmc_data.track_angle - 84.4).abs() < 0.1);
324    }
325
326    #[test]
327    fn test_rmc_different_talker_id() {
328        let parser = NmeaParser::new();
329        // GNRMC is multi-GNSS
330        let sentence = b"$GNRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
331
332        let result = parser.parse_sentence_complete(sentence);
333
334        assert!(result.is_some());
335        let msg = result.unwrap();
336        let rmc = msg.as_rmc();
337        assert!(rmc.is_some());
338
339        let rmc_data = rmc.unwrap();
340        assert_eq!(rmc_data.talker_id, crate::types::TalkerId::GN);
341    }
342
343    #[test]
344    fn test_rmc_multiple_constellations() {
345        let parser = NmeaParser::new();
346
347        // Test GPS
348        let gp_sentence =
349            b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
350        let gp_result = parser.parse_sentence_complete(gp_sentence);
351        assert!(gp_result.is_some());
352        let gp_msg = gp_result.unwrap();
353        let gp_rmc = gp_msg.as_rmc().unwrap();
354        assert_eq!(gp_rmc.talker_id, crate::types::TalkerId::GP);
355
356        // Test GLONASS
357        let gl_sentence =
358            b"$GLRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
359        let gl_result = parser.parse_sentence_complete(gl_sentence);
360        assert!(gl_result.is_some());
361        let gl_msg = gl_result.unwrap();
362        let gl_rmc = gl_msg.as_rmc().unwrap();
363        assert_eq!(gl_rmc.talker_id, crate::types::TalkerId::GL);
364
365        // Test Galileo
366        let ga_sentence =
367            b"$GARMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n";
368        let ga_result = parser.parse_sentence_complete(ga_sentence);
369        assert!(ga_result.is_some());
370        let ga_msg = ga_result.unwrap();
371        let ga_rmc = ga_msg.as_rmc().unwrap();
372        assert_eq!(ga_rmc.talker_id, crate::types::TalkerId::GA);
373    }
374}