1use crate::FrameError;
2
3#[derive(Debug, Clone, PartialEq)]
12pub struct NmeaFrame<'a> {
13 pub prefix: char,
15 pub talker: &'a str,
17 pub sentence_type: &'a str,
19 pub fields: Vec<&'a str>,
21 pub tag_block: Option<&'a str>,
23}
24
25pub fn parse_frame(line: &str) -> Result<NmeaFrame<'_>, FrameError> {
43 let line = line.trim();
44 if line.is_empty() {
45 return Err(FrameError::Empty);
46 }
47
48 let (tag_block, line) = strip_tag_block(line)?;
50
51 let prefix = line.chars().next().ok_or(FrameError::Empty)?;
53 if prefix != '$' && prefix != '!' {
54 return Err(FrameError::InvalidPrefix(prefix));
55 }
56
57 let after_prefix = &line[1..];
58
59 let (body, checksum_str) = match after_prefix.rfind('*') {
61 Some(pos) => {
62 let body = &after_prefix[..pos];
63 let cs_str = after_prefix[pos + 1..].trim_end_matches(['\r', '\n']);
64 (body, Some(cs_str))
65 }
66 None => (after_prefix.trim_end_matches(['\r', '\n']), None),
67 };
68
69 if let Some(cs_str) = checksum_str {
71 let expected = u8::from_str_radix(cs_str, 16).map_err(|_| FrameError::MalformedChecksum)?;
72 let computed = body.bytes().fold(0u8, |acc, b| acc ^ b);
73 if expected != computed {
74 return Err(FrameError::BadChecksum { expected, computed });
75 }
76 }
77
78 if body.len() < 5 {
80 return Err(FrameError::TooShort);
81 }
82
83 let addr_end = body.find(',').unwrap_or(body.len());
85 let addr = &body[..addr_end];
86
87 if addr.len() < 3 {
89 return Err(FrameError::TooShort);
90 }
91 let talker = &addr[..addr.len() - 3];
92 let sentence_type = &addr[addr.len() - 3..];
93
94 let fields_str = if addr_end < body.len() {
96 &body[addr_end + 1..]
97 } else {
98 ""
99 };
100
101 let fields: Vec<&str> = if fields_str.is_empty() {
102 Vec::new()
103 } else {
104 fields_str.split(',').collect()
105 };
106
107 Ok(NmeaFrame {
108 prefix,
109 talker,
110 sentence_type,
111 fields,
112 tag_block,
113 })
114}
115
116pub fn encode_frame(prefix: char, talker: &str, sentence_type: &str, fields: &[&str]) -> String {
130 let body = if fields.is_empty() {
131 format!("{talker}{sentence_type}")
132 } else {
133 format!("{talker}{sentence_type},{}", fields.join(","))
134 };
135
136 let checksum = body.bytes().fold(0u8, |acc, b| acc ^ b);
137 format!("{prefix}{body}*{checksum:02X}\r\n")
138}
139
140fn strip_tag_block(line: &str) -> Result<(Option<&str>, &str), FrameError> {
143 if let Some(rest) = line.strip_prefix('\\') {
144 match rest.find('\\') {
145 Some(close) => {
146 let tag = &rest[..close];
147 let remaining = &rest[close + 1..];
148 Ok((Some(tag), remaining))
149 }
150 None => Err(FrameError::MalformedTagBlock),
151 }
152 } else {
153 Ok((None, line))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn ais_multi_fragment_signalk() {
163 let frame1 = parse_frame(
164 "!AIVDM,2,1,0,A,53brRt4000010SG700iE@LE8@Tp4000000000153P615t0Ht0SCkjH4jC1C,0*25",
165 )
166 .expect("AIS fragment 1");
167 assert_eq!(frame1.prefix, '!');
168 assert_eq!(frame1.sentence_type, "VDM");
169 assert_eq!(frame1.fields[1], "1"); }
171
172 #[test]
173 fn apb_fixture_signalk() {
174 let frame =
175 parse_frame("$GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C").expect("valid APB");
176 assert_eq!(frame.sentence_type, "APB");
177 assert_eq!(frame.fields[9], "DEST");
178 }
179
180 #[test]
181 fn dbt_sounder_gpsd() {
182 let frame =
183 parse_frame("$SDDBT,7.7,f,2.3,M,1.3,F*05").expect("valid DBT from GPSD sounder.log");
184 assert_eq!(frame.sentence_type, "DBT");
185 assert_eq!(frame.fields[2], "2.3"); }
187
188 #[test]
189 fn dpt_fixtures_signalk() {
190 let fixtures = [
191 ("$IIDPT,4.1,0.0*45", "4.1", "0.0"),
192 ("$IIDPT,4.1,1.0*44", "4.1", "1.0"),
193 ("$IIDPT,4.1,-1.0*69", "4.1", "-1.0"),
194 ];
195 for (fix, depth, offset) in &fixtures {
196 let frame = parse_frame(fix).unwrap_or_else(|e| panic!("failed to parse {fix}: {e}"));
197 assert_eq!(frame.sentence_type, "DPT");
198 assert_eq!(frame.fields[0], *depth);
199 assert_eq!(frame.fields[1], *offset);
200 }
201 }
202
203 #[test]
204 fn dpt_humminbird_gpsd() {
205 let frame = parse_frame("$INDPT,2.2,0.0*47").expect("valid DPT from GPSD humminbird");
206 assert_eq!(frame.talker, "IN");
207 assert_eq!(frame.sentence_type, "DPT");
208 }
209
210 #[test]
211 fn encode_no_fields() {
212 let result = encode_frame('$', "GP", "RMC", &[]);
213 assert!(result.starts_with("$GPRMC*"));
214 }
215
216 #[test]
217 fn encode_simple_sentence() {
218 let result = encode_frame(
219 '$',
220 "WI",
221 "MWD",
222 &["270.0", "T", "268.5", "M", "12.4", "N", "6.4", "M"],
223 );
224 assert!(result.starts_with("$WIMWD,270.0,T,268.5,M,12.4,N,6.4,M*"));
225 assert!(result.ends_with("\r\n"));
226 let frame = parse_frame(result.trim()).expect("encoded sentence should be parseable");
228 assert_eq!(frame.sentence_type, "MWD");
229 }
230
231 #[test]
232 fn encode_with_empty_fields() {
233 let result = encode_frame(
234 '$',
235 "GP",
236 "APB",
237 &["", "", "", "", "", "", "", "", "", "", "", "", "", ""],
238 );
239 let frame = parse_frame(result.trim()).expect("should re-parse");
240 assert_eq!(frame.sentence_type, "APB");
241 assert!(frame.fields.iter().all(|f| f.is_empty()));
242 }
243
244 #[test]
245 fn error_bad_checksum() {
246 assert!(matches!(
247 parse_frame("$GPRMC,175957.917,A*FF"),
248 Err(FrameError::BadChecksum { .. })
249 ));
250 }
251
252 #[test]
253 fn error_empty_input() {
254 assert_eq!(parse_frame(""), Err(FrameError::Empty));
255 assert_eq!(parse_frame(" "), Err(FrameError::Empty));
256 }
257
258 #[test]
259 fn error_invalid_prefix() {
260 assert!(matches!(
261 parse_frame("GPRMC,175957.917,A*00"),
262 Err(FrameError::InvalidPrefix('G'))
263 ));
264 }
265
266 #[test]
267 fn error_malformed_tag_block() {
268 assert_eq!(
269 parse_frame("\\s:FooBar$GPRMC,175957.917,A*00"),
270 Err(FrameError::MalformedTagBlock)
271 );
272 }
273
274 #[test]
275 fn error_too_short() {
276 assert_eq!(parse_frame("$GP*17"), Err(FrameError::TooShort));
277 }
278
279 #[test]
280 fn hdg_fixtures_signalk() {
281 let frame = parse_frame("$INHDG,180,5,W,10,W*6D").expect("valid HDG");
282 assert_eq!(frame.sentence_type, "HDG");
283 assert_eq!(frame.fields[0], "180");
284 assert_eq!(frame.fields[1], "5");
285 assert_eq!(frame.fields[2], "W");
286 }
287
288 #[test]
289 fn hdt_saab_gpsd() {
290 let frame = parse_frame("$HEHDT,4.0,T*2B").expect("valid HDT from GPSD saab-r4");
291 assert_eq!(frame.talker, "HE");
292 assert_eq!(frame.sentence_type, "HDT");
293 }
294
295 #[test]
296 fn mtw_humminbird_gpsd() {
297 let frame = parse_frame("$INMTW,17.9,C*1B").expect("valid MTW from GPSD humminbird");
298 assert_eq!(frame.sentence_type, "MTW");
299 assert_eq!(frame.fields[0], "17.9");
300 }
301
302 #[test]
303 fn mwd_fixtures_signalk() {
304 let fixtures = [
306 "$IIMWD,,,046.,M,10.1,N,05.2,M*0B",
307 "$IIMWD,046.,T,046.,M,10.1,N,,*17",
308 "$IIMWD,046.,T,,,,,5.2,M*72",
309 ];
310 for fix in &fixtures {
311 let frame = parse_frame(fix).unwrap_or_else(|e| panic!("failed to parse {fix}: {e}"));
312 assert_eq!(frame.sentence_type, "MWD");
313 }
314 }
315
316 #[test]
317 fn parse_ais_sentence() {
318 let frame =
319 parse_frame("!AIVDM,1,1,,A,13u@Dt002s000000000000000000,0*60").expect("valid frame");
320 assert_eq!(frame.prefix, '!');
321 assert_eq!(frame.talker, "AI");
322 assert_eq!(frame.sentence_type, "VDM");
323 assert_eq!(frame.fields[0], "1");
324 }
325
326 #[test]
327 fn parse_depth_sentence() {
328 let frame = parse_frame("$SDDBT,7.7,f,2.3,M,1.3,F*05").expect("valid frame");
329 assert_eq!(frame.talker, "SD");
330 assert_eq!(frame.sentence_type, "DBT");
331 assert_eq!(frame.fields[2], "2.3");
332 }
333
334 #[test]
335 fn parse_empty_fields() {
336 let frame = parse_frame("$GPAPB,,,,,,,,,,,,,,*44").expect("valid frame");
337 assert_eq!(frame.sentence_type, "APB");
338 assert!(frame.fields.iter().all(|f| f.is_empty()));
339 }
340
341 #[test]
342 fn parse_multi_constellation_talker() {
343 let frame =
345 parse_frame("$GNRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*69")
346 .expect("valid frame");
347 assert_eq!(frame.talker, "GN");
348 assert_eq!(frame.sentence_type, "RMC");
349 }
350
351 #[test]
352 fn parse_no_checksum_accepted() {
353 let result = parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A");
354 assert!(result.is_ok());
355 }
356
357 #[test]
358 fn parse_standard_nmea_sentence() {
359 let frame =
360 parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77")
361 .expect("valid frame");
362 assert_eq!(frame.prefix, '$');
363 assert_eq!(frame.talker, "GP");
364 assert_eq!(frame.sentence_type, "RMC");
365 assert_eq!(frame.fields[0], "175957.917");
366 assert_eq!(frame.fields[1], "A");
367 assert_eq!(frame.tag_block, None);
368 }
369
370 #[test]
371 fn parse_wind_sentence() {
372 let frame = parse_frame("$WIMWD,270.0,T,268.5,M,12.4,N,6.4,M*63").expect("valid frame");
373 assert_eq!(frame.talker, "WI");
374 assert_eq!(frame.sentence_type, "MWD");
375 assert_eq!(frame.fields.len(), 8);
376 assert_eq!(frame.fields[0], "270.0");
377 assert_eq!(frame.fields[1], "T");
378 }
379
380 #[test]
381 fn parse_with_tag_block() {
382 let frame = parse_frame("\\s:FooBar,c:1234567890*xx\\$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77").expect("valid frame");
383 assert!(frame.tag_block.is_some());
384 assert_eq!(frame.prefix, '$');
385 assert_eq!(frame.sentence_type, "RMC");
386 }
387
388 #[test]
389 fn rot_saab_gpsd() {
390 let frame = parse_frame("$HEROT,0.0,A*2B").expect("valid ROT from GPSD saab-r4");
391 assert_eq!(frame.sentence_type, "ROT");
392 }
393
394 #[test]
395 fn roundtrip_parse_encode_parse() {
396 let original = "$WIMWD,270.0,T,268.5,M,12.4,N,6.4,M*63";
397 let frame1 = parse_frame(original).expect("parse original");
398 let encoded = encode_frame(
399 frame1.prefix,
400 frame1.talker,
401 frame1.sentence_type,
402 &frame1.fields,
403 );
404 let frame2 = parse_frame(encoded.trim()).expect("parse re-encoded");
405 assert_eq!(frame1.talker, frame2.talker);
406 assert_eq!(frame1.sentence_type, frame2.sentence_type);
407 assert_eq!(frame1.fields, frame2.fields);
408 }
409}