rustedbytes_nmea/message/
vtg.rs

1//! VTG (Track Made Good and Ground Speed) message implementation
2//!
3//! The VTG message provides velocity and track information including:
4//! - True track angle (relative to true north)
5//! - Magnetic track angle (relative to magnetic north)
6//! - Ground speed in knots
7//! - Ground speed in kilometers per hour
8//!
9//! ## Message Format
10//!
11//! ```text
12//! $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48
13//! ```
14//!
15//! ## Fields
16//!
17//! | Index | Field | Type | Mandatory | Description |
18//! |-------|-------|------|-----------|-------------|
19//! | 0 | Sentence ID | String | Yes | Message type (GPVTG, GNVTG, etc.) |
20//! | 1 | Track True | f32 | No | Track angle in degrees (true north) |
21//! | 2 | True Indicator | char | No | T = True |
22//! | 3 | Track Magnetic | f32 | No | Track angle in degrees (magnetic north) |
23//! | 4 | Magnetic Indicator | char | No | M = Magnetic |
24//! | 5 | Speed Knots | f32 | No | Ground speed in knots |
25//! | 6 | Knots Indicator | char | No | N = Knots |
26//! | 7 | Speed KPH | f32 | No | Ground speed in kilometers per hour |
27//! | 8 | KPH Indicator | char | No | K = Kilometers per hour |
28//!
29//! ## Note on Optional Fields
30//!
31//! All fields in VTG are technically optional. The message may contain empty
32//! fields if the receiver doesn't have a fix or if certain data is not available.
33//!
34//! ## Track Angles
35//!
36//! - **True Track**: Angle relative to true north (0-359.9°)
37//! - **Magnetic Track**: Angle relative to magnetic north (0-359.9°)
38//! - The difference between true and magnetic track is the magnetic variation
39//!
40//! ## Speed Conversion
41//!
42//! - 1 knot = 1.852 km/h
43//! - Speed values are ground speed, not airspeed
44//!
45//! ## Example
46//!
47//! ```text
48//! $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48
49//! ```
50//!
51//! This represents:
52//! - True track: 54.7° (relative to true north)
53//! - Magnetic track: 34.4° (relative to magnetic north)
54//! - Speed: 5.5 knots = 10.2 km/h
55//! - Magnetic variation: ~20° East (54.7 - 34.4)
56
57use crate::message::ParsedSentence;
58use crate::types::{MessageType, TalkerId};
59
60/// VTG - Track Made Good and Ground Speed parameters
61#[derive(Debug, Clone)]
62pub struct VtgData {
63    pub talker_id: TalkerId,
64    pub track_true: Option<f32>,
65    pub track_true_indicator: Option<char>,
66    pub track_magnetic: Option<f32>,
67    pub track_magnetic_indicator: Option<char>,
68    pub speed_knots: Option<f32>,
69    pub speed_knots_indicator: Option<char>,
70    pub speed_kph: Option<f32>,
71    pub speed_kph_indicator: Option<char>,
72}
73
74impl ParsedSentence {
75    /// Extract VTG message parameters
76    ///
77    /// Parses the VTG (Track Made Good and Ground Speed) message and returns
78    /// a structured `VtgData` object containing all parsed fields.
79    ///
80    /// # Returns
81    ///
82    /// - `Some(VtgData)` if the message is a valid VTG message
83    /// - `None` if the message is not a VTG message
84    ///
85    /// # Mandatory Fields
86    ///
87    /// The VTG message has no strictly mandatory fields. All fields are optional
88    /// and will be `None` if not present or invalid.
89    ///
90    /// # Optional Fields
91    ///
92    /// All fields (1-8) are optional:
93    /// - Track true and indicator
94    /// - Track magnetic and indicator
95    /// - Speed in knots and indicator
96    /// - Speed in km/h and indicator
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// use rustedbytes_nmea::{NmeaParser, MessageType};
102    ///
103    /// let parser = NmeaParser::new();
104    /// let sentence = b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
105    ///
106    /// let result = parser.parse_bytes(sentence);
107    /// if let Ok((Some(msg), _consumed)) = result {
108    ///     if let Some(vtg) = msg.as_vtg() {
109    ///         assert_eq!(vtg.track_true, Some(54.7));
110    ///         assert_eq!(vtg.speed_knots, Some(5.5));
111    ///     }
112    /// }
113    /// ```
114    pub fn as_vtg(&self) -> Option<VtgData> {
115        if self.message_type != MessageType::VTG {
116            return None;
117        }
118
119        Some(VtgData {
120            talker_id: self.talker_id,
121            track_true: self.parse_field(1),
122            track_true_indicator: self.parse_field_char(2),
123            track_magnetic: self.parse_field(3),
124            track_magnetic_indicator: self.parse_field_char(4),
125            speed_knots: self.parse_field(5),
126            speed_knots_indicator: self.parse_field_char(6),
127            speed_kph: self.parse_field(7),
128            speed_kph_indicator: self.parse_field_char(8),
129        })
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::NmeaParser;
136
137    #[test]
138    fn test_vtg_complete_message() {
139        let parser = NmeaParser::new();
140        let sentence = b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
141
142        let result = parser.parse_sentence_complete(sentence);
143
144        assert!(result.is_some());
145        let msg = result.unwrap();
146        let vtg = msg.as_vtg();
147        assert!(vtg.is_some());
148
149        let vtg_data = vtg.unwrap();
150        assert_eq!(vtg_data.track_true, Some(54.7));
151        assert_eq!(vtg_data.track_true_indicator, Some('T'));
152        assert_eq!(vtg_data.track_magnetic, Some(34.4));
153        assert_eq!(vtg_data.track_magnetic_indicator, Some('M'));
154        assert_eq!(vtg_data.speed_knots, Some(5.5));
155        assert_eq!(vtg_data.speed_knots_indicator, Some('N'));
156        assert_eq!(vtg_data.speed_kph, Some(10.2));
157        assert_eq!(vtg_data.speed_kph_indicator, Some('K'));
158    }
159
160    #[test]
161    fn test_vtg_with_empty_fields() {
162        let parser = NmeaParser::new();
163        let sentence = b"$GPVTG,,T,,M,,N,,K*48\r\n";
164
165        let result = parser.parse_sentence_complete(sentence);
166
167        assert!(result.is_some());
168        let msg = result.unwrap();
169        let vtg = msg.as_vtg();
170        assert!(vtg.is_some());
171
172        let vtg_data = vtg.unwrap();
173        assert_eq!(vtg_data.track_true, None);
174        assert_eq!(vtg_data.track_magnetic, None);
175        assert_eq!(vtg_data.speed_knots, None);
176        assert_eq!(vtg_data.speed_kph, None);
177    }
178
179    #[test]
180    fn test_vtg_zero_speed() {
181        let parser = NmeaParser::new();
182        let sentence = b"$GPVTG,0.0,T,0.0,M,0.0,N,0.0,K*48\r\n";
183
184        let result = parser.parse_sentence_complete(sentence);
185
186        assert!(result.is_some());
187        let msg = result.unwrap();
188        let vtg = msg.as_vtg();
189        assert!(vtg.is_some());
190
191        let vtg_data = vtg.unwrap();
192        assert_eq!(vtg_data.speed_knots, Some(0.0));
193        assert_eq!(vtg_data.speed_kph, Some(0.0));
194    }
195
196    #[test]
197    fn test_vtg_high_speed() {
198        let parser = NmeaParser::new();
199        let sentence = b"$GPVTG,270.5,T,250.3,M,125.8,N,233.0,K*48\r\n";
200
201        let result = parser.parse_sentence_complete(sentence);
202
203        assert!(result.is_some());
204        let msg = result.unwrap();
205        let vtg = msg.as_vtg();
206        assert!(vtg.is_some());
207
208        let vtg_data = vtg.unwrap();
209        assert!((vtg_data.speed_knots.unwrap() - 125.8).abs() < 0.1);
210        assert!((vtg_data.speed_kph.unwrap() - 233.0).abs() < 0.1);
211    }
212
213    #[test]
214    fn test_vtg_only_true_track() {
215        let parser = NmeaParser::new();
216        let sentence = b"$GPVTG,054.7,T,,M,,N,,K*48\r\n";
217
218        let result = parser.parse_sentence_complete(sentence);
219
220        assert!(result.is_some());
221        let msg = result.unwrap();
222        let vtg = msg.as_vtg();
223        assert!(vtg.is_some());
224
225        let vtg_data = vtg.unwrap();
226        assert_eq!(vtg_data.track_true, Some(54.7));
227        assert_eq!(vtg_data.track_magnetic, None);
228    }
229
230    #[test]
231    fn test_vtg_only_knots() {
232        let parser = NmeaParser::new();
233        let sentence = b"$GPVTG,,T,,M,5.5,N,,K*48\r\n";
234
235        let result = parser.parse_sentence_complete(sentence);
236
237        assert!(result.is_some());
238        let msg = result.unwrap();
239        let vtg = msg.as_vtg();
240        assert!(vtg.is_some());
241
242        let vtg_data = vtg.unwrap();
243        assert_eq!(vtg_data.speed_knots, Some(5.5));
244        assert_eq!(vtg_data.speed_kph, None);
245    }
246
247    #[test]
248    fn test_vtg_only_kph() {
249        let parser = NmeaParser::new();
250        let sentence = b"$GPVTG,,T,,M,,N,10.2,K*48\r\n";
251
252        let result = parser.parse_sentence_complete(sentence);
253
254        assert!(result.is_some());
255        let msg = result.unwrap();
256        let vtg = msg.as_vtg();
257        assert!(vtg.is_some());
258
259        let vtg_data = vtg.unwrap();
260        assert_eq!(vtg_data.speed_knots, None);
261        assert_eq!(vtg_data.speed_kph, Some(10.2));
262    }
263
264    #[test]
265    fn test_vtg_track_angle_ranges() {
266        let parser = NmeaParser::new();
267
268        // Test 0 degrees
269        let sentence = b"$GPVTG,0.0,T,0.0,M,5.5,N,10.2,K*48\r\n";
270        let result = parser.parse_sentence_complete(sentence);
271        assert!(result.is_some());
272        let msg = result.unwrap();
273        let vtg = msg.as_vtg().unwrap();
274        assert_eq!(vtg.track_true, Some(0.0));
275
276        // Test 359.9 degrees
277        let sentence = b"$GPVTG,359.9,T,359.9,M,5.5,N,10.2,K*48\r\n";
278        let result = parser.parse_sentence_complete(sentence);
279        assert!(result.is_some());
280        let msg = result.unwrap();
281        let vtg = msg.as_vtg().unwrap();
282        assert!((vtg.track_true.unwrap() - 359.9).abs() < 0.1);
283    }
284
285    #[test]
286    fn test_vtg_speed_conversion_accuracy() {
287        let parser = NmeaParser::new();
288        // 1 knot = 1.852 km/h
289        // 5.5 knots = 10.186 km/h (rounded to 10.2)
290        let sentence = b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
291
292        let result = parser.parse_sentence_complete(sentence);
293
294        assert!(result.is_some());
295        let msg = result.unwrap();
296        let vtg = msg.as_vtg();
297        assert!(vtg.is_some());
298
299        let vtg_data = vtg.unwrap();
300        let knots = vtg_data.speed_knots.unwrap();
301        let kph = vtg_data.speed_kph.unwrap();
302
303        // Verify the conversion is approximately correct
304        let expected_kph = knots * 1.852;
305        assert!((kph - expected_kph).abs() < 0.2);
306    }
307
308    #[test]
309    fn test_vtg_numeric_precision() {
310        let parser = NmeaParser::new();
311        let sentence = b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
312
313        let result = parser.parse_sentence_complete(sentence);
314
315        assert!(result.is_some());
316        let msg = result.unwrap();
317        let vtg = msg.as_vtg();
318        assert!(vtg.is_some());
319
320        let vtg_data = vtg.unwrap();
321        assert!((vtg_data.track_true.unwrap() - 54.7).abs() < 0.1);
322        assert!((vtg_data.track_magnetic.unwrap() - 34.4).abs() < 0.1);
323        assert!((vtg_data.speed_knots.unwrap() - 5.5).abs() < 0.1);
324        assert!((vtg_data.speed_kph.unwrap() - 10.2).abs() < 0.1);
325    }
326
327    #[test]
328    fn test_vtg_different_talker_id() {
329        let parser = NmeaParser::new();
330        // GNVTG is multi-GNSS
331        let sentence = b"$GNVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
332
333        let result = parser.parse_sentence_complete(sentence);
334
335        assert!(result.is_some());
336        let msg = result.unwrap();
337        let vtg = msg.as_vtg();
338        assert!(vtg.is_some());
339
340        let vtg_data = vtg.unwrap();
341        assert_eq!(vtg_data.talker_id, crate::types::TalkerId::GN);
342    }
343
344    #[test]
345    fn test_vtg_mixed_constellation_data() {
346        let parser = NmeaParser::new();
347
348        // Test GPS
349        let gp_sentence = b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\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_vtg = gp_msg.as_vtg().unwrap();
354        assert_eq!(gp_vtg.talker_id, crate::types::TalkerId::GP);
355        assert_eq!(gp_vtg.track_true, Some(54.7));
356
357        // Test GLONASS
358        let gl_sentence = b"$GLVTG,154.7,T,134.4,M,015.5,N,028.7,K*48\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_vtg = gl_msg.as_vtg().unwrap();
363        assert_eq!(gl_vtg.talker_id, crate::types::TalkerId::GL);
364        assert_eq!(gl_vtg.track_true, Some(154.7));
365    }
366
367    #[test]
368    fn test_vtg_easterly_heading() {
369        let parser = NmeaParser::new();
370        let sentence = b"$GPVTG,090.0,T,085.0,M,10.0,N,18.5,K*48\r\n";
371
372        let result = parser.parse_sentence_complete(sentence);
373
374        assert!(result.is_some());
375        let msg = result.unwrap();
376        let vtg = msg.as_vtg();
377        assert!(vtg.is_some());
378
379        let vtg_data = vtg.unwrap();
380        assert_eq!(vtg_data.track_true, Some(90.0));
381    }
382
383    #[test]
384    fn test_vtg_westerly_heading() {
385        let parser = NmeaParser::new();
386        let sentence = b"$GPVTG,270.0,T,265.0,M,10.0,N,18.5,K*48\r\n";
387
388        let result = parser.parse_sentence_complete(sentence);
389
390        assert!(result.is_some());
391        let msg = result.unwrap();
392        let vtg = msg.as_vtg();
393        assert!(vtg.is_some());
394
395        let vtg_data = vtg.unwrap();
396        assert_eq!(vtg_data.track_true, Some(270.0));
397    }
398}