sidereon_core/nmea/
mod.rs1mod epoch;
4mod fields;
5mod sentence;
6#[cfg(test)]
7mod tests;
8mod write;
9
10pub use crate::format::{Diagnostics, Parsed, RecordRef, Skip, SkipReason, Warning, WarningKind};
11pub use crate::validate::FieldError;
12pub use epoch::{EpochSnapshot, GsaEntry, GsvGroup, NmeaAccumulator, NmeaChunkOutput};
13pub use fields::{
14 Gga, GgaQuality, Gll, Gsa, GsaFixMode, GsaSelectionMode, Gst, Gsv, GsvSatellite,
15 NmeaCoordinate, NmeaDate, NmeaSatNumber, NmeaSignalId, NmeaTalker, NmeaTime, Rmc, RmcStatus,
16 Vtg, Zda,
17};
18pub use sentence::{NmeaBody, NmeaSentence};
19pub use write::write_gga;
20
21#[derive(Debug, Clone, PartialEq, thiserror::Error)]
22pub enum NmeaError {
23 #[error("not an NMEA sentence: {reason}")]
24 NotFramed { reason: &'static str },
25 #[error("checksum mismatch: computed {computed:02X}, stated {stated:02X}")]
26 ChecksumMismatch { computed: u8, stated: u8 },
27 #[error("unsupported sentence type {address}")]
28 UnsupportedType { address: String },
29 #[error("proprietary sentence {address}")]
30 Proprietary { address: String },
31 #[error("malformed field: {0}")]
32 MalformedField(#[from] FieldError),
33 #[error("invalid input {field}: {reason}")]
34 InvalidInput {
35 field: &'static str,
36 reason: &'static str,
37 },
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct NmeaLog {
42 pub sentences: Vec<NmeaSentence>,
43}
44
45pub fn parse_sentence(line: &str) -> Result<Parsed<NmeaSentence>, NmeaError> {
46 sentence::parse_framed(sentence::frame_sentence(line)?)
47}
48
49pub fn parse_nmea(input: &[u8]) -> Parsed<NmeaLog> {
50 let mut diagnostics = Diagnostics::new();
51 let mut sentences = Vec::new();
52 for (index, line) in input.split(|b| *b == b'\n').enumerate() {
53 let line = line.strip_suffix(b"\r").unwrap_or(line);
54 if line.is_empty() {
55 continue;
56 }
57 let line_number = index + 1;
58 match std::str::from_utf8(line) {
59 Ok(line) => match parse_sentence(line) {
60 Ok(mut parsed) => {
61 set_diagnostic_lines(&mut parsed.diagnostics, line_number);
62 merge_diagnostics(&mut diagnostics, parsed.diagnostics);
63 sentences.push(parsed.value);
64 }
65 Err(error) => push_error_skip_at_line(&mut diagnostics, error, line_number),
66 },
67 Err(_) => push_error_skip_at_line(
68 &mut diagnostics,
69 NmeaError::NotFramed {
70 reason: "non-ASCII byte",
71 },
72 line_number,
73 ),
74 }
75 }
76 Parsed::new(NmeaLog { sentences }, diagnostics)
77}
78
79pub fn parse_nmea_str(text: &str) -> Parsed<NmeaLog> {
80 parse_nmea(text.as_bytes())
81}
82
83pub fn group_epochs(log: &NmeaLog) -> Vec<EpochSnapshot> {
84 let mut accumulator = NmeaAccumulator::new();
85 let mut snapshots = Vec::new();
86 for sentence in &log.sentences {
87 if let Some(snapshot) = accumulator.push(sentence) {
88 snapshots.push(snapshot);
89 }
90 }
91 if let Some(snapshot) = accumulator.finish() {
92 snapshots.push(snapshot);
93 }
94 snapshots
95}
96
97pub(crate) fn merge_diagnostics(target: &mut Diagnostics, mut source: Diagnostics) {
98 target.skips.append(&mut source.skips);
99 target.warnings.append(&mut source.warnings);
100}
101
102fn push_error_skip_at_line(diagnostics: &mut Diagnostics, error: NmeaError, line: usize) {
103 push_error_skip_at(diagnostics, error, RecordRef::at_line(line));
104}
105
106pub(crate) fn push_error_skip_at(diagnostics: &mut Diagnostics, error: NmeaError, at: RecordRef) {
107 let reason = match error {
108 NmeaError::NotFramed {
109 reason: "non-ASCII byte",
110 } => SkipReason::InconsistentRecord("non-ASCII byte"),
111 NmeaError::NotFramed {
112 reason: "sentence over length cap",
113 } => SkipReason::InconsistentRecord("sentence over length cap"),
114 NmeaError::NotFramed {
115 reason: "malformed checksum",
116 } => SkipReason::InconsistentRecord("malformed checksum"),
117 NmeaError::NotFramed { .. } => {
118 SkipReason::UnknownBlock("no NMEA start delimiter".to_string())
119 }
120 NmeaError::ChecksumMismatch { .. } => SkipReason::InconsistentRecord("checksum mismatch"),
121 NmeaError::UnsupportedType { ref address } if address == "encapsulated sentence" => {
122 SkipReason::UnsupportedRecordType("encapsulated sentence")
123 }
124 NmeaError::UnsupportedType { .. } => {
125 SkipReason::UnsupportedRecordType("unsupported sentence type")
126 }
127 NmeaError::Proprietary { .. } => SkipReason::UnsupportedRecordType("proprietary sentence"),
128 NmeaError::MalformedField(error) => SkipReason::MalformedField(error),
129 NmeaError::InvalidInput { .. } => SkipReason::InconsistentRecord("invalid input"),
130 };
131 diagnostics.push_skip(Skip { at, reason });
132}
133
134pub(crate) fn set_diagnostic_lines(diagnostics: &mut Diagnostics, line: usize) {
135 for skip in &mut diagnostics.skips {
136 if skip.at.line.is_none() {
137 skip.at.line = Some(line);
138 }
139 }
140 for warning in &mut diagnostics.warnings {
141 if warning.at.line.is_none() {
142 warning.at.line = Some(line);
143 }
144 }
145}