1use chrono::NaiveDate;
13use polars::prelude::*;
14
15use super::table::{Builder, Cell, Kind};
16
17pub const MAX_LINE: usize = 1024;
20pub const BATCH_ROWS: usize = 65_536;
22const MAX_TYPES: usize = 64;
24const KNOTS: f64 = 1852.0 / 3600.0;
26const DAY_MS: i64 = 86_400_000;
27const HALF_DAY_MS: i64 = DAY_MS / 2;
28pub(crate) const BACK_MS: i64 = 5_000;
31const UNDATED_MS: i64 = 3_600_000;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Table {
39 Fixes,
40 Gga,
41 Rmc,
42 Vtg,
43 Gsa,
44 Gsv,
45 Gll,
46 Zda,
47 Sentences,
48}
49
50impl Table {
51 pub const ALL: [Table; 9] = [
52 Table::Fixes,
53 Table::Gga,
54 Table::Rmc,
55 Table::Vtg,
56 Table::Gsa,
57 Table::Gsv,
58 Table::Gll,
59 Table::Zda,
60 Table::Sentences,
61 ];
62
63 pub fn name(self) -> &'static str {
65 match self {
66 Table::Fixes => "fixes",
67 Table::Gga => "GGA",
68 Table::Rmc => "RMC",
69 Table::Vtg => "VTG",
70 Table::Gsa => "GSA",
71 Table::Gsv => "GSV",
72 Table::Gll => "GLL",
73 Table::Zda => "ZDA",
74 Table::Sentences => "sentences",
75 }
76 }
77
78 pub fn from_name(name: &str) -> Option<Self> {
80 Self::ALL
81 .into_iter()
82 .find(|t| t.name().eq_ignore_ascii_case(name.trim()))
83 }
84
85 fn sentence(self) -> Option<&'static str> {
87 match self {
88 Table::Fixes | Table::Sentences => None,
89 other => Some(other.name()),
90 }
91 }
92
93 pub fn columns(self) -> Vec<(&'static str, Kind)> {
95 use Kind::*;
96 match self {
97 Table::Fixes => vec![
98 ("time", Time),
99 ("lat", F64),
100 ("lon", F64),
101 ("alt", F64),
102 ("speed", F64),
103 ("course", F64),
104 ("sats", U32),
105 ("hdop", F64),
106 ("fix", Str),
107 ("gap", F64),
108 ("checksum_ok", Bool),
109 ],
110 Table::Gga => vec![
111 ("time", Time),
112 ("talker", Str),
113 ("lat", F64),
114 ("lon", F64),
115 ("quality", U32),
116 ("sats", U32),
117 ("hdop", F64),
118 ("alt", F64),
119 ("geoid_sep", F64),
120 ("dgps_age", F64),
121 ("dgps_station", Str),
122 ("checksum_ok", Bool),
123 ],
124 Table::Rmc => vec![
125 ("time", Time),
126 ("talker", Str),
127 ("status", Str),
128 ("lat", F64),
129 ("lon", F64),
130 ("speed", F64),
131 ("course", F64),
132 ("mag_var", F64),
133 ("mode", Str),
134 ("checksum_ok", Bool),
135 ],
136 Table::Vtg => vec![
137 ("time", Time),
138 ("talker", Str),
139 ("course", F64),
140 ("course_mag", F64),
141 ("speed", F64),
142 ("mode", Str),
143 ("checksum_ok", Bool),
144 ],
145 Table::Gsa => vec![
146 ("time", Time),
147 ("talker", Str),
148 ("mode", Str),
149 ("fix_type", U32),
150 ("sats", ListU32),
151 ("pdop", F64),
152 ("hdop", F64),
153 ("vdop", F64),
154 ("system", U32),
155 ("checksum_ok", Bool),
156 ],
157 Table::Gsv => vec![
158 ("time", Time),
159 ("talker", Str),
160 ("messages", U32),
161 ("message", U32),
162 ("in_view", U32),
163 ("prn", U32),
164 ("elevation", F64),
165 ("azimuth", F64),
166 ("snr", F64),
167 ("signal", Str),
168 ("checksum_ok", Bool),
169 ],
170 Table::Gll => vec![
171 ("time", Time),
172 ("talker", Str),
173 ("lat", F64),
174 ("lon", F64),
175 ("status", Str),
176 ("mode", Str),
177 ("checksum_ok", Bool),
178 ],
179 Table::Zda => vec![
180 ("time", Time),
181 ("talker", Str),
182 ("zone_hours", I32),
183 ("zone_minutes", I32),
184 ("checksum_ok", Bool),
185 ],
186 Table::Sentences => vec![
187 ("time", Time),
188 ("line", U64),
189 ("talker", Str),
190 ("type", Str),
191 ("fields", Str),
192 ("checksum_ok", Bool),
193 ],
194 }
195 }
196
197 pub fn schema(self) -> Schema {
199 self.columns()
200 .into_iter()
201 .map(|(name, kind)| Field::new(name.into(), kind.dtype()))
202 .collect()
203 }
204}
205
206#[derive(Debug)]
208struct Sentence<'a> {
209 talker: &'a str,
211 kind: &'a str,
213 body: &'a str,
215 fields: Vec<&'a str>,
216 checksum_ok: Option<bool>,
218}
219
220impl Sentence<'_> {
221 fn field(&self, i: usize) -> &str {
222 self.fields.get(i).copied().unwrap_or("")
223 }
224
225 fn type_name(&self) -> String {
227 if self.talker == "P" {
228 format!("P{}", self.kind)
229 } else {
230 self.kind.to_string()
231 }
232 }
233}
234
235fn parse_sentence(line: &[u8]) -> Option<Sentence<'_>> {
238 let start = line.iter().position(|&b| b == b'$' || b == b'!')?;
239 let text = std::str::from_utf8(&line[start + 1..]).ok()?;
240 let text = text.trim_end();
241 let (data, checksum_ok) = match text.rfind('*') {
242 Some(star) => {
243 let given = &text[star + 1..];
244 let data = &text[..star];
245 let ok = given.len() == 2
246 && u8::from_str_radix(given, 16)
247 .is_ok_and(|sum| data.bytes().fold(0u8, |acc, b| acc ^ b) == sum);
248 (data, Some(ok))
249 }
250 None => (text, None),
251 };
252 let (address, body) = data.split_once(',').unwrap_or((data, ""));
253 let valid = (3..=10).contains(&address.len())
254 && address.as_bytes()[0].is_ascii_uppercase()
255 && address
256 .bytes()
257 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
258 if !valid {
259 return None;
260 }
261 let (talker, kind) = if let Some(rest) = address.strip_prefix('P') {
262 ("P", rest)
263 } else {
264 address.split_at(2)
265 };
266 Some(Sentence {
267 talker,
268 kind,
269 body,
270 fields: if data.contains(',') {
271 body.split(',').collect()
272 } else {
273 Vec::new()
274 },
275 checksum_ok,
276 })
277}
278
279const KNOWN: [&str; 14] = [
281 "GGA", "RMC", "VTG", "GSA", "GSV", "GLL", "ZDA", "GNS", "GST", "GBS", "TXT", "HDT", "DTM",
282 "GRS",
283];
284
285pub fn looks_like(head: &[u8]) -> bool {
291 let head = head.strip_prefix(b"\xef\xbb\xbf").unwrap_or(head);
292 let mut lines = head
293 .split(|&b| b == b'\n')
294 .map(|l| l.trim_ascii())
295 .filter(|l| !l.is_empty());
296 let sentence = |line: &[u8]| {
297 line.first() == Some(&b'$')
298 && parse_sentence(line).is_some_and(|s| {
299 s.checksum_ok == Some(true) || (s.talker.len() == 2 && KNOWN.contains(&s.kind))
300 })
301 };
302 let tail = |line: &[u8]| {
303 line.len() >= 3
304 && line[line.len() - 3] == b'*'
305 && line[line.len() - 2..].iter().all(u8::is_ascii_hexdigit)
306 };
307 match lines.next() {
308 Some(first) if sentence(first) => true,
309 Some(first) if first.first() != Some(&b'$') && tail(first) => {
310 lines.next().is_some_and(sentence)
311 }
312 _ => false,
313 }
314}
315
316fn number(s: &str) -> Option<f64> {
317 let s = s.trim();
318 if s.is_empty()
320 || !s
321 .bytes()
322 .all(|b| b.is_ascii_digit() || b"+-.eE".contains(&b))
323 {
324 return None;
325 }
326 s.parse::<f64>().ok().filter(|v| v.is_finite())
327}
328
329fn unsigned(s: &str) -> Option<u32> {
330 s.trim().parse().ok()
331}
332
333fn text(s: &str) -> Option<String> {
334 let s = s.trim();
335 (!s.is_empty()).then(|| s.to_string())
336}
337
338fn degrees(value: &str, hemisphere: &str, limit: f64) -> Option<f64> {
341 let v = number(value).filter(|v| *v >= 0.0)?;
342 let whole = (v / 100.0).floor();
343 let minutes = v - whole * 100.0;
344 if minutes >= 60.0 {
345 return None;
346 }
347 let deg = whole + minutes / 60.0;
348 if deg > limit {
349 return None;
350 }
351 match hemisphere.trim() {
352 "N" | "E" => Some(deg),
353 "S" | "W" => Some(-deg),
354 _ => None,
355 }
356}
357
358fn latitude(value: &str, hemisphere: &str) -> Option<f64> {
359 degrees(value, hemisphere, 90.0).filter(|_| matches!(hemisphere.trim(), "N" | "S"))
360}
361
362fn longitude(value: &str, hemisphere: &str) -> Option<f64> {
363 degrees(value, hemisphere, 180.0).filter(|_| matches!(hemisphere.trim(), "E" | "W"))
364}
365
366fn time_of_day(s: &str) -> Option<u32> {
368 let s = s.trim();
369 let b = s.as_bytes();
370 if b.len() < 6 || !b[..6].iter().all(u8::is_ascii_digit) {
371 return None;
372 }
373 let two = |i: usize| u32::from(b[i] - b'0') * 10 + u32::from(b[i + 1] - b'0');
374 let (h, m, sec) = (two(0), two(2), two(4));
375 if h > 23 || m > 59 || sec > 60 {
376 return None;
377 }
378 let mut ms = 0;
379 if b.len() > 6 {
380 let frac = s[6..].strip_prefix('.')?;
381 if !frac.bytes().all(|c| c.is_ascii_digit()) {
382 return None;
383 }
384 for (i, c) in frac.bytes().take(3).enumerate() {
386 ms += u32::from(c - b'0') * [100, 10, 1][i];
387 }
388 }
389 Some(((h * 60 + m) * 60 + sec) * 1000 + ms)
390}
391
392fn days(year: i32, month: u32, day: u32) -> Option<i64> {
394 let date = NaiveDate::from_ymd_opt(year, month, day)?;
395 Some(
396 date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1)?)
397 .num_days(),
398 )
399}
400
401fn rmc_date(s: &str) -> Option<i64> {
403 let s = s.trim();
404 if s.len() != 6 || !s.bytes().all(|b| b.is_ascii_digit()) {
405 return None;
406 }
407 let n = |i: usize| s[i..i + 2].parse::<u32>().ok();
408 let yy = n(4)? as i32;
409 days(if yy >= 80 { 1900 + yy } else { 2000 + yy }, n(2)?, n(0)?)
410}
411
412#[derive(Debug, Default)]
416struct Clock {
417 anchor: Option<(i64, u32)>,
419}
420
421impl Clock {
422 fn near(anchor: (i64, u32), tod: u32) -> i64 {
424 let (day, last) = (anchor.0, i64::from(anchor.1));
425 let tod = i64::from(tod);
426 if tod + HALF_DAY_MS < last {
427 day + 1
428 } else if tod > last + HALF_DAY_MS {
429 day - 1
430 } else {
431 day
432 }
433 }
434
435 fn date(&mut self, day: i64, tod: u32) {
436 self.anchor = Some((day, tod));
437 }
438
439 fn resolve(&mut self, tod: u32) -> Option<i64> {
441 let anchor = self.anchor?;
442 let day = Self::near(anchor, tod);
443 if day > anchor.0 || (day == anchor.0 && tod > anchor.1) {
444 self.anchor = Some((day, tod));
445 }
446 Some(day * DAY_MS + i64::from(tod))
447 }
448}
449
450#[derive(Debug, Default)]
452struct Fix {
453 tod: Option<u32>,
454 has: u8,
456 lat: Option<f64>,
457 lon: Option<f64>,
458 alt: Option<f64>,
459 speed: Option<f64>,
460 course: Option<f64>,
461 sats: Option<u32>,
462 hdop: Option<f64>,
463 fix: Option<&'static str>,
464 checksum_ok: Option<bool>,
465}
466
467fn gga_fix(quality: Option<u32>) -> Option<&'static str> {
468 Some(match quality? {
469 0 => "none",
470 1 => "gps",
471 2 => "dgps",
472 3 => "pps",
473 4 => "rtk",
474 5 => "rtk float",
475 6 => "estimated",
476 7 => "manual",
477 8 => "simulated",
478 _ => return None,
479 })
480}
481
482fn rmc_fix(status: &str, mode: &str) -> Option<&'static str> {
484 if status.trim() == "V" {
485 return Some("none");
486 }
487 Some(match mode.trim() {
488 "A" => "gps",
489 "D" => "dgps",
490 "E" => "estimated",
491 "F" => "rtk float",
492 "R" => "rtk",
493 "M" => "manual",
494 "S" => "simulated",
495 "N" => "none",
496 "" if status.trim() == "A" => "gps",
497 _ => return None,
498 })
499}
500
501fn vtg_values(s: &Sentence) -> (Option<f64>, Option<f64>, Option<f64>, Option<String>) {
504 let speed = |knots: &str, kmh: &str| {
505 number(knots)
506 .map(|v| v * KNOTS)
507 .or_else(|| number(kmh).map(|v| v / 3.6))
508 };
509 if s.field(1).trim() == "T" || s.fields.len() >= 8 {
510 (
511 number(s.field(0)),
512 number(s.field(2)),
513 speed(s.field(4), s.field(6)),
514 text(s.field(8)),
515 )
516 } else {
517 (
518 number(s.field(0)),
519 number(s.field(1)),
520 speed(s.field(2), s.field(3)),
521 None,
522 )
523 }
524}
525
526fn fix_bit(kind: &str) -> u8 {
527 match kind {
528 "GGA" => 1,
529 "RMC" => 2,
530 "VTG" => 4,
531 "GLL" => 8,
532 _ => 0,
533 }
534}
535
536fn and_checksum(a: Option<bool>, b: Option<bool>) -> Option<bool> {
537 match (a, b) {
538 (None, x) | (x, None) => x,
539 (Some(x), Some(y)) => Some(x && y),
540 }
541}
542
543fn gap_ms(last: (u32, Option<i64>), now: (u32, Option<i64>)) -> Option<i64> {
548 let ms = match (last.1, now.1) {
549 (Some(last), Some(now)) => return Some(now - last).filter(|ms| *ms >= -BACK_MS),
550 _ => i64::from(now.0) - i64::from(last.0),
551 };
552 let ms = if ms < -BACK_MS { ms + DAY_MS } else { ms };
553 (-BACK_MS..=UNDATED_MS).contains(&ms).then_some(ms)
554}
555
556#[derive(Debug, Default, Clone, PartialEq, Eq)]
558pub struct Stats {
559 pub lines: u64,
560 pub sentences: u64,
561 pub skipped: u64,
563 pub bad_checksums: u64,
564 pub types: Vec<(String, u64)>,
566 pub other_types: u64,
568 pub rows: u64,
570 pub dated: bool,
572}
573
574impl Stats {
575 fn count(&mut self, name: String) {
576 if let Some((_, n)) = self.types.iter_mut().find(|(t, _)| *t == name) {
577 *n += 1;
578 } else if self.types.len() < MAX_TYPES {
579 self.types.push((name, 1));
580 } else {
581 self.other_types += 1;
582 }
583 }
584
585 pub fn absorb(&mut self, other: &Stats) {
587 self.lines += other.lines;
588 self.sentences += other.sentences;
589 self.skipped += other.skipped;
590 self.bad_checksums += other.bad_checksums;
591 self.other_types += other.other_types;
592 self.rows += other.rows;
593 self.dated &= other.dated;
594 for (name, n) in &other.types {
595 if let Some((_, have)) = self.types.iter_mut().find(|(t, _)| t == name) {
596 *have += n;
597 } else if self.types.len() < MAX_TYPES {
598 self.types.push((name.clone(), *n));
599 } else {
600 self.other_types += n;
601 }
602 }
603 }
604
605 pub fn of(&self, kind: &str) -> u64 {
607 self.types
608 .iter()
609 .find(|(t, _)| t == kind)
610 .map_or(0, |(_, n)| *n)
611 }
612}
613
614pub struct NmeaReader {
616 table: Table,
617 rows: Builder,
618 line: Vec<u8>,
619 overlong: bool,
621 clock: Clock,
622 undated: Vec<(usize, u32)>,
624 epoch: Option<u32>,
626 last_fix: Option<(u32, Option<i64>)>,
629 fix: Option<Fix>,
630 stats: Stats,
631}
632
633impl NmeaReader {
634 pub fn new(table: Table) -> Self {
635 Self {
636 table,
637 rows: Builder::new(&table.columns()),
638 line: Vec::new(),
639 overlong: false,
640 clock: Clock::default(),
641 undated: Vec::new(),
642 epoch: None,
643 last_fix: None,
644 fix: None,
645 stats: Stats::default(),
646 }
647 }
648
649 pub fn stats(&self) -> &Stats {
650 &self.stats
651 }
652
653 pub fn push(&mut self, mut bytes: &[u8]) {
656 while !bytes.is_empty() {
657 match bytes.iter().position(|&b| b == b'\n') {
658 Some(end) => {
659 self.extend_line(&bytes[..end]);
660 self.end_line();
661 bytes = &bytes[end + 1..];
662 }
663 None => {
664 self.extend_line(bytes);
665 break;
666 }
667 }
668 }
669 }
670
671 fn extend_line(&mut self, piece: &[u8]) {
672 if self.overlong {
673 return;
674 }
675 if self.line.len() + piece.len() > MAX_LINE {
676 self.overlong = true;
677 self.line.clear();
678 } else {
679 self.line.extend_from_slice(piece);
680 }
681 }
682
683 fn end_line(&mut self) {
684 let line = std::mem::take(&mut self.line);
685 self.stats.lines += 1;
686 if std::mem::take(&mut self.overlong) {
687 self.stats.skipped += 1;
688 } else if !line.trim_ascii().is_empty() {
689 match parse_sentence(&line) {
690 Some(sentence) => self.sentence(&sentence),
691 None => self.stats.skipped += 1,
692 }
693 }
694 self.line = line;
696 self.line.clear();
697 }
698
699 pub fn take_batch(&mut self) -> PolarsResult<Option<DataFrame>> {
702 if self.rows.len() < BATCH_ROWS {
703 return Ok(None);
704 }
705 self.take().map(Some)
706 }
707
708 fn take(&mut self) -> PolarsResult<DataFrame> {
709 self.undated.clear();
710 self.rows.take()
711 }
712
713 pub fn finish(&mut self) -> PolarsResult<DataFrame> {
715 if !self.line.is_empty() || self.overlong {
716 self.end_line();
717 }
718 self.flush_fix();
719 self.take()
720 }
721
722 fn row_time(&mut self, tod: Option<u32>) -> Cell {
724 let Some(tod) = tod.or(self.epoch) else {
725 return Cell::Time(None);
726 };
727 match self.clock.resolve(tod) {
728 Some(ms) => Cell::Time(Some(ms)),
729 None => {
730 self.undated.push((self.rows.len(), tod));
732 Cell::Time(None)
733 }
734 }
735 }
736
737 fn dated(&mut self, day: i64, tod: u32) {
739 let first = self.clock.anchor.is_none();
740 self.clock.date(day, tod);
741 self.stats.dated = true;
742 if first {
743 let anchor = (day, tod);
744 for (row, tod) in std::mem::take(&mut self.undated) {
745 let ms = Clock::near(anchor, tod) * DAY_MS + i64::from(tod);
746 self.rows.set_time(0, row, ms);
747 }
748 }
749 }
750
751 fn emit(&mut self, row: Vec<Cell>) {
752 self.rows.push(row);
753 self.stats.rows += 1;
754 }
755
756 fn sentence(&mut self, s: &Sentence) {
757 self.stats.sentences += 1;
758 if s.checksum_ok == Some(false) {
759 self.stats.bad_checksums += 1;
760 }
761 self.stats.count(s.type_name());
762 let standard = s.talker != "P";
763 let kind = if standard { s.kind } else { "" };
764 let tod = match kind {
765 "GGA" | "RMC" | "ZDA" => time_of_day(s.field(0)),
766 "GLL" => time_of_day(s.field(4)),
767 _ => None,
768 };
769 let bit = fix_bit(kind);
772 if self.table == Table::Fixes && bit != 0 {
773 let starts_new = self.fix.as_ref().is_some_and(|fix| {
774 fix.has & bit != 0 || matches!((fix.tod, tod), (Some(a), Some(b)) if a != b)
775 });
776 if starts_new {
777 self.flush_fix();
778 }
779 }
780 match kind {
781 "RMC" => {
782 if let (Some(day), Some(tod)) = (rmc_date(s.field(8)), tod) {
783 self.dated(day, tod);
784 }
785 }
786 "ZDA" => {
787 let n = |i: usize| s.field(i).trim().parse::<u32>().ok();
788 if let (Some(d), Some(m), Some(y), Some(tod)) = (n(1), n(2), n(3), tod)
789 && let Some(day) = i32::try_from(y).ok().and_then(|y| days(y, m, d))
790 {
791 self.dated(day, tod);
792 }
793 }
794 _ => {}
795 }
796 if tod.is_some() {
797 self.epoch = tod;
798 }
799 match self.table {
800 Table::Fixes => {
801 if bit != 0 {
802 self.merge(s, kind, bit, tod);
803 }
804 }
805 Table::Sentences => {
806 let time = self.row_time(tod);
807 let row = vec![
808 time,
809 Cell::U64(Some(self.stats.lines)),
810 Cell::Str(Some(s.talker.to_string())),
811 Cell::Str(Some(s.kind.to_string())),
812 Cell::Str(Some(s.body.to_string())),
813 Cell::Bool(s.checksum_ok),
814 ];
815 self.emit(row);
816 }
817 table if table.sentence() == Some(kind) => self.sentence_rows(s, kind, tod),
818 _ => {}
819 }
820 }
821
822 fn merge(&mut self, s: &Sentence, kind: &str, bit: u8, tod: Option<u32>) {
824 let fix = self.fix.get_or_insert_with(Fix::default);
825 fix.has |= bit;
826 fix.tod = fix.tod.or(tod);
827 fix.checksum_ok = and_checksum(fix.checksum_ok, s.checksum_ok);
828 let (lat, lon) = match kind {
829 "GGA" => (
830 latitude(s.field(1), s.field(2)),
831 longitude(s.field(3), s.field(4)),
832 ),
833 "RMC" => (
834 latitude(s.field(2), s.field(3)),
835 longitude(s.field(4), s.field(5)),
836 ),
837 "GLL" => (
838 latitude(s.field(0), s.field(1)),
839 longitude(s.field(2), s.field(3)),
840 ),
841 _ => (None, None),
842 };
843 fix.lat = fix.lat.or(lat);
844 fix.lon = fix.lon.or(lon);
845 match kind {
846 "GGA" => {
847 let quality = unsigned(s.field(5));
848 fix.fix = gga_fix(quality).or(fix.fix);
850 fix.sats = unsigned(s.field(6));
851 fix.hdop = number(s.field(7));
852 fix.alt = number(s.field(8));
853 }
854 "RMC" => {
855 fix.speed = fix.speed.or(number(s.field(6)).map(|v| v * KNOTS));
856 fix.course = fix.course.or(number(s.field(7)));
857 if fix.has & 1 == 0 {
858 fix.fix = rmc_fix(s.field(1), s.field(11)).or(fix.fix);
859 }
860 }
861 "VTG" => {
862 let (course, _, speed, _) = vtg_values(s);
863 fix.speed = fix.speed.or(speed);
864 fix.course = fix.course.or(course);
865 }
866 _ => {}
867 }
868 }
869
870 fn flush_fix(&mut self) {
871 let Some(fix) = self.fix.take() else {
872 return;
873 };
874 let time = match fix.tod {
875 Some(tod) => self.row_time(Some(tod)),
876 None => Cell::Time(None),
877 };
878 let dated = match time {
879 Cell::Time(ms) => ms,
880 _ => None,
881 };
882 let gap = fix.tod.and_then(|tod| {
883 let last = self.last_fix.replace((tod, dated))?;
884 gap_ms(last, (tod, dated)).map(|ms| ms as f64 / 1000.0)
885 });
886 self.emit(vec![
887 time,
888 Cell::F64(fix.lat),
889 Cell::F64(fix.lon),
890 Cell::F64(fix.alt),
891 Cell::F64(fix.speed),
892 Cell::F64(fix.course),
893 Cell::U32(fix.sats),
894 Cell::F64(fix.hdop),
895 Cell::Str(fix.fix.map(str::to_string)),
896 Cell::F64(gap),
897 Cell::Bool(fix.checksum_ok),
898 ]);
899 }
900
901 fn sentence_rows(&mut self, s: &Sentence, kind: &str, tod: Option<u32>) {
904 let (start, waiting) = (self.rows.len(), self.undated.len());
905 let time = self.row_time(tod);
906 let talker = Cell::Str(Some(s.talker.to_string()));
907 let ok = Cell::Bool(s.checksum_ok);
908 let f = |i: usize| s.field(i);
909 match kind {
910 "GGA" => self.emit(vec![
911 time,
912 talker,
913 Cell::F64(latitude(f(1), f(2))),
914 Cell::F64(longitude(f(3), f(4))),
915 Cell::U32(unsigned(f(5))),
916 Cell::U32(unsigned(f(6))),
917 Cell::F64(number(f(7))),
918 Cell::F64(number(f(8))),
919 Cell::F64(number(f(10))),
920 Cell::F64(number(f(12))),
921 Cell::Str(text(f(13))),
922 ok,
923 ]),
924 "RMC" => {
925 let mag_var = number(f(9)).map(|v| if f(10).trim() == "W" { -v } else { v });
926 self.emit(vec![
927 time,
928 talker,
929 Cell::Str(text(f(1))),
930 Cell::F64(latitude(f(2), f(3))),
931 Cell::F64(longitude(f(4), f(5))),
932 Cell::F64(number(f(6)).map(|v| v * KNOTS)),
933 Cell::F64(number(f(7))),
934 Cell::F64(mag_var),
935 Cell::Str(text(f(11))),
936 ok,
937 ]);
938 }
939 "VTG" => {
940 let (course, course_mag, speed, mode) = vtg_values(s);
941 self.emit(vec![
942 time,
943 talker,
944 Cell::F64(course),
945 Cell::F64(course_mag),
946 Cell::F64(speed),
947 Cell::Str(mode),
948 ok,
949 ]);
950 }
951 "GSA" => {
952 let sats: Vec<u32> = (2..14).filter_map(|i| unsigned(f(i))).collect();
953 self.emit(vec![
954 time,
955 talker,
956 Cell::Str(text(f(0))),
957 Cell::U32(unsigned(f(1))),
958 Cell::ListU32(Some(sats)),
959 Cell::F64(number(f(14))),
960 Cell::F64(number(f(15))),
961 Cell::F64(number(f(16))),
962 Cell::U32(unsigned(f(17))),
963 ok,
964 ]);
965 }
966 "GSV" => {
967 let blocks = s.fields.len().saturating_sub(3);
969 let signal = (blocks % 4 == 1)
970 .then(|| text(f(s.fields.len() - 1)))
971 .flatten();
972 for block in 0..blocks / 4 {
973 let at = 3 + block * 4;
974 let Some(prn) = unsigned(f(at)) else {
975 continue;
976 };
977 self.emit(vec![
978 time.clone(),
979 talker.clone(),
980 Cell::U32(unsigned(f(0))),
981 Cell::U32(unsigned(f(1))),
982 Cell::U32(unsigned(f(2))),
983 Cell::U32(Some(prn)),
984 Cell::F64(number(f(at + 1))),
985 Cell::F64(number(f(at + 2))),
986 Cell::F64(number(f(at + 3))),
987 Cell::Str(signal.clone()),
988 ok.clone(),
989 ]);
990 }
991 }
992 "GLL" => self.emit(vec![
993 time,
994 talker,
995 Cell::F64(latitude(f(0), f(1))),
996 Cell::F64(longitude(f(2), f(3))),
997 Cell::Str(text(f(5))),
998 Cell::Str(text(f(6))),
999 ok,
1000 ]),
1001 "ZDA" => {
1002 let zone = |i: usize| f(i).trim().parse::<i32>().ok();
1003 self.emit(vec![
1004 time,
1005 talker,
1006 Cell::I32(zone(4)),
1007 Cell::I32(zone(5)),
1008 ok,
1009 ]);
1010 }
1011 _ => {}
1012 }
1013 if let Some(&(_, tod)) = self.undated.get(waiting) {
1016 self.undated.truncate(waiting);
1017 for row in start..self.rows.len() {
1018 self.undated.push((row, tod));
1019 }
1020 }
1021 }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026 use super::*;
1027
1028 fn read(table: Table, text: &str) -> (DataFrame, Stats) {
1029 let mut reader = NmeaReader::new(table);
1030 for piece in text.as_bytes().chunks(7) {
1032 reader.push(piece);
1033 }
1034 let df = reader.finish().unwrap();
1035 (df, reader.stats().clone())
1036 }
1037
1038 fn with_checksum(body: &str) -> String {
1039 let sum = body.bytes().fold(0u8, |a, b| a ^ b);
1040 format!("${body}*{sum:02X}")
1041 }
1042
1043 fn f64s(df: &DataFrame, name: &str) -> Vec<Option<f64>> {
1044 df.column(name).unwrap().f64().unwrap().iter().collect()
1045 }
1046
1047 fn times(df: &DataFrame) -> Vec<Option<i64>> {
1048 df.column("time")
1049 .unwrap()
1050 .datetime()
1051 .unwrap()
1052 .physical()
1053 .iter()
1054 .collect()
1055 }
1056
1057 fn ms(date: &str, tod: &str) -> i64 {
1058 let day = rmc_date(date).unwrap();
1059 day * DAY_MS + i64::from(time_of_day(tod).unwrap())
1060 }
1061
1062 #[test]
1063 fn coordinates_are_decimal_degrees() {
1064 let lat = latitude("4807.038", "N").unwrap();
1065 assert!((lat - (48.0 + 7.038 / 60.0)).abs() < 1e-12);
1066 assert!(longitude("01131.000", "W").unwrap() < 0.0);
1067 assert_eq!(latitude("4867.000", "N"), None, "60 minutes or more");
1068 assert_eq!(latitude("9100.000", "N"), None);
1069 assert_eq!(latitude("4807.038", "E"), None, "a longitude's hemisphere");
1070 assert_eq!(latitude("", "N"), None);
1071 assert_eq!(latitude("inf", "N"), None);
1072 assert_eq!(time_of_day("123519.5"), Some(45_319_500));
1073 assert_eq!(time_of_day("246000"), None);
1074 }
1075
1076 #[test]
1077 fn checksums_and_addresses() {
1078 let good = with_checksum("GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,");
1079 let s = parse_sentence(good.as_bytes()).unwrap();
1080 assert_eq!((s.talker, s.kind, s.checksum_ok), ("GP", "GGA", Some(true)));
1081 let bad = good.replace("545.4", "545.5");
1082 assert_eq!(
1083 parse_sentence(bad.as_bytes()).unwrap().checksum_ok,
1084 Some(false)
1085 );
1086 let none = parse_sentence(b"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K").unwrap();
1087 assert_eq!(none.checksum_ok, None);
1088 let vendor = parse_sentence(b"$PUBX,00,1*00").unwrap();
1089 assert_eq!((vendor.talker, vendor.kind), ("P", "UBX"));
1090 assert_eq!(vendor.type_name(), "PUBX");
1091 let prefixed = parse_sentence(b"12:00:01 $GNRMC,,V,,,,,,,,,,N*4D").unwrap();
1092 assert_eq!(prefixed.kind, "RMC");
1093 assert!(parse_sentence(b"$gpgga,1").is_none());
1094 assert!(parse_sentence(b"hello").is_none());
1095 assert!(looks_like(b"\n$GPGGA,123519,48"));
1096 assert!(!looks_like(b"time,lat\n$GPGGA,1"));
1097 let vendor = with_checksum("PUBX,00,1");
1098 assert!(looks_like(vendor.as_bytes()), "a vendor's, by its checksum");
1099 let wrong = vendor.replace("PUBX,00", "PUBX,01");
1100 assert!(
1101 !looks_like(wrong.as_bytes()),
1102 "a vendor's, its checksum wrong"
1103 );
1104 assert!(!looks_like(b"$USD,$EUR\n1,2\n"), "a CSV header");
1105 assert!(!looks_like(b"$AMOUNT,QTY\n1,2\n"));
1106 assert!(
1107 looks_like(b"5.4,M,46.9,M,,*47\r\n$GNRMC,120000,A,4807.038,N"),
1108 "a capture that starts mid-sentence"
1109 );
1110 assert!(!looks_like(b"a,b*47\n$USD,$EUR\n"));
1111 }
1112
1113 #[test]
1116 fn a_gap_is_empty_where_time_cannot_say() {
1117 let h = 3_600_000;
1118 let day = DAY_MS;
1119 assert_eq!(gap_ms((0, Some(day)), (0, Some(2 * day))), Some(day));
1121 assert_eq!(
1122 gap_ms((1000, Some(1000)), (0, Some(0))),
1123 Some(-1000),
1124 "settling"
1125 );
1126 assert_eq!(
1127 gap_ms((0, Some(10 * h)), (0, Some(6 * h))),
1128 None,
1129 "back an hour"
1130 );
1131 let last_second = (DAY_MS - 1000) as u32;
1133 assert_eq!(gap_ms((last_second, None), (1000, None)), Some(2000));
1134 assert_eq!(gap_ms((0, None), (60_000, None)), Some(60_000));
1135 assert_eq!(
1136 gap_ms((0, None), (2 * h as u32, None)),
1137 None,
1138 "days may hide"
1139 );
1140 assert_eq!(
1141 gap_ms((5 * h as u32, None), (h as u32, None)),
1142 None,
1143 "back hours"
1144 );
1145 }
1146
1147 #[test]
1148 fn an_epoch_s_sentences_merge_into_one_fix() {
1149 let log = [
1150 with_checksum("GPRMC,235959.00,A,4807.038,N,01131.000,E,10.0,84.4,311223,003.1,W,A"),
1151 with_checksum("GPGGA,235959.00,4807.038,N,01131.000,E,4,12,0.8,545.4,M,46.9,M,,"),
1152 with_checksum("GPVTG,84.4,T,,M,10.0,N,18.5,K,A"),
1153 "not a sentence".to_string(),
1154 with_checksum("GPGGA,000000.00,4807.040,N,01131.000,E,1,07,1.0,546.0,M,46.9,M,,"),
1156 "$GPRMC,000000.00,A,4807.040,N,01131.000,E,9.0,84.0,010124,,,A*00".to_string(),
1157 ]
1158 .join("\r\n");
1159 let (df, stats) = read(Table::Fixes, &log);
1160 assert_eq!(df.height(), 2, "{df}");
1161 assert_eq!(
1162 times(&df),
1163 [Some(ms("311223", "235959")), Some(ms("010124", "000000"))]
1164 );
1165 let speed = f64s(&df, "speed");
1166 assert!(
1167 (speed[0].unwrap() - 10.0 * KNOTS).abs() < 1e-12,
1168 "knots to m/s"
1169 );
1170 assert_eq!(f64s(&df, "alt"), [Some(545.4), Some(546.0)]);
1171 assert_eq!(f64s(&df, "gap"), [None, Some(1.0)], "across midnight");
1172 let fix: Vec<_> = df.column("fix").unwrap().str().unwrap().iter().collect();
1173 assert_eq!(fix, [Some("rtk"), Some("gps")], "GGA's quality wins");
1174 let ok: Vec<_> = df
1175 .column("checksum_ok")
1176 .unwrap()
1177 .bool()
1178 .unwrap()
1179 .iter()
1180 .collect();
1181 assert_eq!(
1182 ok,
1183 [Some(true), Some(false)],
1184 "the second RMC's checksum is wrong"
1185 );
1186 assert_eq!(
1187 (stats.skipped, stats.bad_checksums, stats.sentences),
1188 (1, 1, 5)
1189 );
1190 assert_eq!(stats.of("GGA"), 2);
1191 assert!(stats.dated);
1192 }
1193
1194 #[test]
1195 fn rows_before_the_first_date_are_dated_by_it() {
1196 let log = [
1197 with_checksum("GPGGA,235958,4807.038,N,01131.000,E,1,08,0.9,545.4,M,,M,,"),
1198 with_checksum("GPGGA,000001,4807.038,N,01131.000,E,1,08,0.9,545.4,M,,M,,"),
1199 with_checksum("GPRMC,000001,A,4807.038,N,01131.000,E,1.0,0.0,010124,,,A"),
1200 ]
1201 .join("\n");
1202 let (df, _) = read(Table::Fixes, &log);
1203 assert_eq!(
1204 times(&df),
1205 [Some(ms("311223", "235958")), Some(ms("010124", "000001"))],
1206 "the day before, across midnight"
1207 );
1208 let (df, stats) = read(Table::Gga, log.lines().next().unwrap());
1210 assert_eq!(times(&df), [None]);
1211 assert!(!stats.dated);
1212 }
1213
1214 #[test]
1215 fn each_sentence_type_is_a_table() {
1216 let log = [
1217 with_checksum("GPRMC,120000,A,4807.038,N,01131.000,E,1.0,0.0,010124,,,A"),
1218 with_checksum("GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45"),
1219 with_checksum("GPGSV,2,2,08,15,,,,16,10,100,,,,,,,,,,1"),
1220 with_checksum("GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1"),
1221 with_checksum("GPVTG,054.7,034.4,005.5,010.2"),
1222 with_checksum("PGRME,15.0,M,45.0,M,25.0,M"),
1223 ]
1224 .join("\n");
1225 let (gsv, _) = read(Table::Gsv, &log);
1226 assert_eq!(gsv.height(), 6, "a row per satellite listed: {gsv}");
1227 let snr = f64s(&gsv, "snr");
1228 assert_eq!(snr[..4], [Some(46.0), Some(41.0), Some(39.0), Some(45.0)]);
1229 let signal: Vec<_> = gsv
1230 .column("signal")
1231 .unwrap()
1232 .str()
1233 .unwrap()
1234 .iter()
1235 .collect();
1236 assert_eq!(signal[4..], [Some("1"), Some("1")]);
1237 assert!(
1238 times(&gsv)
1239 .iter()
1240 .all(|t| *t == Some(ms("010124", "120000")))
1241 );
1242 let (gsa, _) = read(Table::Gsa, &log);
1243 let sats = gsa
1244 .column("sats")
1245 .unwrap()
1246 .list()
1247 .unwrap()
1248 .get_as_series(0)
1249 .unwrap();
1250 assert_eq!(
1251 sats.u32().unwrap().into_no_null_iter().collect::<Vec<_>>(),
1252 [4, 5, 9, 12, 24]
1253 );
1254 let (vtg, _) = read(Table::Vtg, &log);
1255 assert_eq!(
1256 f64s(&vtg, "course_mag"),
1257 [Some(34.4)],
1258 "the bare pre-2.3 form"
1259 );
1260 let (all, stats) = read(Table::Sentences, &log);
1261 assert_eq!(all.height(), 6);
1262 assert_eq!(stats.of("PGRME"), 1);
1263 assert_eq!(Table::from_name("gsv"), Some(Table::Gsv));
1264 for table in Table::ALL {
1265 let (df, _) = read(table, &log);
1266 assert_eq!(df.schema().as_ref(), &table.schema(), "{}", table.name());
1267 }
1268 }
1269
1270 #[test]
1271 fn an_overlong_line_is_skipped_and_costs_one_line() {
1272 let mut reader = NmeaReader::new(Table::Sentences);
1273 let long = vec![b'x'; MAX_LINE * 4];
1274 reader.push(&long);
1275 reader.push(&long);
1276 assert!(reader.line.len() <= MAX_LINE);
1277 reader.push(b"\n$GPGGA,1*00\n");
1278 let df = reader.finish().unwrap();
1279 assert_eq!(df.height(), 1);
1280 assert_eq!(reader.stats().skipped, 1);
1281 }
1282}