1use crate::DateTime;
18use crate::nmea::{Assembler, Gsv, Sentence};
19
20const MAX_CONSTELLATIONS: usize = 4;
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum FixQuality {
30 #[default]
32 None,
33 TwoD,
35 ThreeD,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub struct Fix {
46 pub quality: FixQuality,
48 pub latitude_e7: Option<i32>,
50 pub longitude_e7: Option<i32>,
52 pub altitude_m: Option<i32>,
55 pub hdop_centi: Option<u16>,
57 pub sats_used: u8,
59 pub sats_in_view: Option<u8>,
62 pub time: Option<DateTime>,
68 pub time_from_fix: bool,
75}
76
77impl Fix {
78 pub const fn has_position(&self) -> bool {
80 self.latitude_e7.is_some() && self.longitude_e7.is_some()
81 }
82}
83
84#[derive(Clone, Copy, Default)]
86struct Constellation {
87 talker: [u8; 2],
88 in_view: u8,
89}
90
91pub struct Driver {
93 assembler: Assembler,
94 cycle: Cycle,
96}
97
98#[derive(Clone, Copy, Default)]
100struct Cycle {
101 gga_quality: u8,
102 gga_sats_used: u8,
103 gga_altitude_m: Option<i32>,
104 gga_latitude: Option<i32>,
105 gga_longitude: Option<i32>,
106 gga_hdop_centi: Option<u16>,
107 gsa_fix_mode: u8,
108 hdop_centi: Option<u16>,
109 constellations: [Constellation; MAX_CONSTELLATIONS],
110 constellation_count: usize,
111 saw_gsv: bool,
112}
113
114impl Cycle {
115 fn in_view(&self) -> Option<u8> {
117 self.saw_gsv.then(|| {
118 self.constellations[..self.constellation_count]
119 .iter()
120 .fold(0u8, |sum, entry| sum.saturating_add(entry.in_view))
121 })
122 }
123
124 fn absorb_gsv(&mut self, gsv: Gsv) {
128 if gsv.message != 1 {
129 return;
130 }
131 self.saw_gsv = true;
132 if let Some(entry) = self.constellations[..self.constellation_count]
133 .iter_mut()
134 .find(|entry| entry.talker == gsv.talker)
135 {
136 entry.in_view = gsv.in_view;
137 return;
138 }
139 if self.constellation_count < MAX_CONSTELLATIONS {
140 self.constellations[self.constellation_count] = Constellation {
141 talker: gsv.talker,
142 in_view: gsv.in_view,
143 };
144 self.constellation_count += 1;
145 }
146 }
147}
148
149impl Default for Driver {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl Driver {
156 pub const fn new() -> Self {
158 Self {
159 assembler: Assembler::new(),
160 cycle: Cycle {
161 gga_quality: 0,
162 gga_sats_used: 0,
163 gga_altitude_m: None,
164 gga_latitude: None,
165 gga_longitude: None,
166 gga_hdop_centi: None,
167 gsa_fix_mode: 0,
168 hdop_centi: None,
169 constellations: [Constellation {
170 talker: [0; 2],
171 in_view: 0,
172 }; MAX_CONSTELLATIONS],
173 constellation_count: 0,
174 saw_gsv: false,
175 },
176 }
177 }
178
179 pub fn reset(&mut self) {
185 self.assembler.reset();
186 self.cycle = Cycle::default();
187 }
188
189 pub fn push(&mut self, byte: u8) -> Option<Fix> {
191 let sentence = self.assembler.push(byte)?;
192 match sentence {
193 Sentence::Gga(gga) => {
194 self.cycle.gga_quality = gga.quality;
195 self.cycle.gga_sats_used = gga.sats_used;
196 self.cycle.gga_altitude_m = gga.altitude_m;
197 self.cycle.gga_latitude = gga.latitude;
198 self.cycle.gga_longitude = gga.longitude;
199 self.cycle.gga_hdop_centi = gga.hdop_centi;
200 None
201 }
202 Sentence::Gsa(gsa) => {
203 self.cycle.gsa_fix_mode = gsa.fix_mode;
204 self.cycle.hdop_centi = gsa.hdop_centi;
208 None
209 }
210 Sentence::Gsv(gsv) => {
211 self.cycle.absorb_gsv(gsv);
212 None
213 }
214 Sentence::Rmc(rmc) => {
215 let cycle = core::mem::take(&mut self.cycle);
216 let latitude = rmc.latitude.or(cycle.gga_latitude);
221 let longitude = rmc.longitude.or(cycle.gga_longitude);
222 let has_position = latitude.is_some() && longitude.is_some();
223
224 let quality = match (has_position, cycle.gsa_fix_mode, cycle.gga_quality) {
236 (false, _, _) => FixQuality::None,
237 (true, 3, _) => FixQuality::ThreeD,
238 (true, 2, _) => FixQuality::TwoD,
239 (true, _, 0) => FixQuality::None,
240 (true, _, _) if cycle.gga_altitude_m.is_some() => FixQuality::ThreeD,
241 (true, _, _) => FixQuality::TwoD,
242 };
243
244 Some(Fix {
245 quality,
246 latitude_e7: has_position.then_some(latitude).flatten(),
251 longitude_e7: has_position.then_some(longitude).flatten(),
252 altitude_m: match quality {
258 FixQuality::ThreeD => cycle.gga_altitude_m,
259 _ => None,
260 },
261 hdop_centi: cycle.hdop_centi.or(cycle.gga_hdop_centi),
265 sats_used: cycle.gga_sats_used,
266 sats_in_view: cycle.in_view(),
267 time: rmc.time,
268 time_from_fix: rmc.valid,
269 })
270 }
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use core::fmt::Write as _;
279
280 fn feed(driver: &mut Driver, body: &str) -> Option<Fix> {
282 let checksum = body.bytes().fold(0u8, |sum, byte| sum ^ byte);
283 let mut line = heapless::String::<{ crate::nmea::MAX_SENTENCE }>::new();
284 line.push('$').unwrap();
285 line.push_str(body).unwrap();
286 write!(line, "*{checksum:02X}\r\n").unwrap();
287
288 let mut out = None;
289 for byte in line.bytes() {
290 if let Some(fix) = driver.push(byte) {
291 out = Some(fix);
292 }
293 }
294 out
295 }
296
297 fn three_d_cycle(driver: &mut Driver) -> Fix {
299 assert!(
300 feed(
301 driver,
302 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"
303 )
304 .is_none(),
305 "GGA ended a cycle"
306 );
307 assert!(
308 feed(driver, "GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1").is_none(),
309 "GSA ended a cycle"
310 );
311 assert!(
312 feed(driver, "GPGSV,3,1,11,03,03,111,00,04,15,270,00").is_none(),
313 "GSV ended a cycle"
314 );
315 feed(
316 driver,
317 "GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230326,003.1,W",
318 )
319 .expect("RMC did not end the cycle")
320 }
321
322 #[test]
323 fn a_cycle_assembles_into_one_fix() {
324 let mut driver = Driver::new();
325 let fix = three_d_cycle(&mut driver);
326 assert_eq!(fix.quality, FixQuality::ThreeD);
327 assert_eq!(fix.latitude_e7, Some(481_173_000));
328 assert_eq!(fix.longitude_e7, Some(115_166_666));
329 assert_eq!(fix.altitude_m, Some(545));
330 assert_eq!(fix.hdop_centi, Some(130));
331 assert_eq!(fix.sats_used, 8);
332 assert_eq!(fix.sats_in_view, Some(11));
333 assert!(fix.time_from_fix);
334 assert_eq!(fix.time.map(|at| at.hour), Some(12));
335 assert!(fix.has_position());
336 }
337
338 #[test]
341 fn a_searching_receiver_produces_a_fix_with_nothing_in_it() {
342 let mut driver = Driver::new();
343 feed(&mut driver, "GPGGA,,,,,,0,00,,,M,,M,,");
344 feed(&mut driver, "GPGSA,A,1,,,,,,,,,,,,,,,");
345 let fix = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
346 assert_eq!(fix.quality, FixQuality::None);
347 assert_eq!(fix.latitude_e7, None);
348 assert_eq!(fix.altitude_m, None);
349 assert_eq!(fix.sats_used, 0);
350 assert_eq!(fix.time, None);
351 assert!(!fix.time_from_fix);
352 assert!(!fix.has_position());
353 }
354
355 #[test]
358 fn time_without_a_fix_is_reported_and_marked_as_such() {
359 let mut driver = Driver::new();
360 let fix = feed(&mut driver, "GPRMC,081836.00,V,,,,,,,130926,,").expect("no fix emitted");
361 assert_eq!(
362 fix.time.map(|at| (at.year, at.month, at.day)),
363 Some((2026, 9, 13))
364 );
365 assert!(
366 !fix.time_from_fix,
367 "a time from a void fix must not claim satellite discipline"
368 );
369 assert!(!fix.has_position());
370 assert_eq!(fix.quality, FixQuality::None);
371 }
372
373 #[test]
378 fn a_two_dimensional_solution_drops_the_altitude() {
379 let mut driver = Driver::new();
380 feed(
381 &mut driver,
382 "GPGGA,123519,4807.038,N,01131.000,E,1,05,2.4,545.4,M,46.9,M,,",
383 );
384 feed(&mut driver, "GPGSA,A,2,04,05,,,,,,,,,,,4.1,2.4,3.1");
385 let fix = feed(
386 &mut driver,
387 "GPRMC,123519,A,4807.038,N,01131.000,E,,,230326,,",
388 )
389 .expect("no fix emitted");
390 assert_eq!(fix.quality, FixQuality::TwoD);
391 assert!(fix.has_position());
392 assert_eq!(fix.altitude_m, None);
393 }
394
395 #[test]
398 fn a_quality_indicator_without_a_position_is_not_a_fix() {
399 let mut driver = Driver::new();
400 feed(&mut driver, "GPGGA,123519,,,,,1,08,0.9,545.4,M,46.9,M,,");
401 feed(&mut driver, "GPGSA,A,3,04,05,,,,,,,,,,,2.5,1.3,2.1");
402 let fix = feed(&mut driver, "GPRMC,123519,A,,,,,,,230326,,").expect("no fix emitted");
403 assert_eq!(fix.quality, FixQuality::None);
404 assert_eq!(fix.altitude_m, None);
405 }
406
407 #[test]
410 fn gga_alone_establishes_a_fix() {
411 let mut driver = Driver::new();
412 feed(
413 &mut driver,
414 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,",
415 );
416 let fix = feed(
417 &mut driver,
418 "GPRMC,123519,A,4807.038,N,01131.000,E,,,230326,,",
419 )
420 .expect("no fix emitted");
421 assert_eq!(fix.quality, FixQuality::ThreeD);
422 assert_eq!(fix.altitude_m, Some(545));
423 assert_eq!(fix.hdop_centi, Some(90));
425 }
426
427 #[test]
430 fn gga_without_an_altitude_is_two_dimensional() {
431 let mut driver = Driver::new();
432 feed(
433 &mut driver,
434 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,,M,46.9,M,,",
435 );
436 let fix = feed(
437 &mut driver,
438 "GPRMC,123519,A,4807.038,N,01131.000,E,,,230326,,",
439 )
440 .expect("no fix emitted");
441 assert_eq!(fix.quality, FixQuality::TwoD);
442 assert_eq!(fix.altitude_m, None);
443 }
444
445 #[test]
449 fn gsa_outranks_a_lingering_gga_altitude() {
450 let mut driver = Driver::new();
451 feed(
452 &mut driver,
453 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,",
454 );
455 feed(&mut driver, "GPGSA,A,2,04,05,,,,,,,,,,,2.5,1.3,2.1");
456 let fix = feed(
457 &mut driver,
458 "GPRMC,123519,A,4807.038,N,01131.000,E,,,230326,,",
459 )
460 .expect("no fix emitted");
461 assert_eq!(fix.quality, FixQuality::TwoD);
462 assert_eq!(fix.altitude_m, None);
463 assert_eq!(fix.hdop_centi, Some(130));
465 }
466
467 #[test]
473 fn an_ag3335_emitting_only_gga_and_rmc_still_reports_fully() {
474 let mut driver = Driver::new();
475 feed(
476 &mut driver,
477 "GNGGA,081519.000,4208.0416,N,12237.0543,W,1,10,1.04,698.3,M,-22.4,M,,",
478 );
479 let fix = feed(
480 &mut driver,
481 "GNRMC,081519.000,A,4208.0416,N,12237.0543,W,0.04,0.00,050826,,,A,V",
482 )
483 .expect("no fix emitted");
484
485 assert_eq!(fix.quality, FixQuality::ThreeD);
486 assert_eq!(fix.altitude_m, Some(698));
487 assert_eq!(fix.hdop_centi, Some(104));
488 assert_eq!(fix.sats_used, 10);
489 assert_eq!(fix.sats_in_view, None);
491 assert!(fix.time_from_fix);
492 assert!(fix.has_position());
493 }
494
495 #[test]
499 fn satellites_in_view_sum_across_constellations_once_each() {
500 let mut driver = Driver::new();
501 feed(&mut driver, "GPGSV,3,1,11,03,03,111,00");
502 feed(&mut driver, "GPGSV,3,2,11,09,23,313,00");
503 feed(&mut driver, "GPGSV,3,3,11,24,58,065,00");
504 feed(&mut driver, "GLGSV,2,1,07,65,12,034,00");
505 feed(&mut driver, "GLGSV,2,2,07,66,45,120,00");
506 let fix = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
507 assert_eq!(fix.sats_in_view, Some(18), "11 GPS + 7 GLONASS");
508
509 let mut driver = Driver::new();
512 feed(&mut driver, "GPGSV,1,1,05,03,03,111,00");
513 feed(&mut driver, "GPGSV,1,1,05,03,03,111,00");
514 let fix = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
515 assert_eq!(fix.sats_in_view, Some(5));
516 }
517
518 #[test]
521 fn no_gsv_means_no_answer_rather_than_zero() {
522 let mut driver = Driver::new();
523 let fix = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
524 assert_eq!(fix.sats_in_view, None);
525 }
526
527 #[test]
530 fn a_cycle_does_not_inherit_the_previous_one() {
531 let mut driver = Driver::new();
532 let first = three_d_cycle(&mut driver);
533 assert_eq!(first.altitude_m, Some(545));
534
535 let second = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
537 assert_eq!(second.quality, FixQuality::None);
538 assert_eq!(second.altitude_m, None);
539 assert_eq!(second.hdop_centi, None);
540 assert_eq!(second.sats_used, 0);
541 assert_eq!(second.sats_in_view, None);
542 }
543
544 #[test]
547 fn resetting_discards_a_partial_cycle() {
548 let mut driver = Driver::new();
549 feed(
550 &mut driver,
551 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,",
552 );
553 driver.reset();
554 let fix = feed(&mut driver, "GPRMC,,V,,,,,,,,,,N").expect("no fix emitted");
555 assert_eq!(fix.latitude_e7, None);
556 assert_eq!(fix.altitude_m, None);
557 assert_eq!(fix.sats_used, 0);
558 }
559
560 #[test]
563 fn a_corrupt_sentence_costs_only_itself() {
564 let mut driver = Driver::new();
565 for byte in b"\x00\xff$GPGGA,tor" {
566 driver.push(*byte);
567 }
568 feed(
569 &mut driver,
570 "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,",
571 );
572 feed(&mut driver, "GPGSA,A,3,04,05,,,,,,,,,,,2.5,1.3,2.1");
573 let fix = feed(
574 &mut driver,
575 "GPRMC,123519,A,4807.038,N,01131.000,E,,,230326,,",
576 )
577 .expect("no fix emitted");
578 assert_eq!(fix.quality, FixQuality::ThreeD);
579 assert_eq!(fix.altitude_m, Some(545));
580 }
581}