marlin_nmea_0183/lib.rs
1//! # marlin-nmea-0183
2//!
3//! Sans-I/O typed decoders for NMEA 0183 sentences. Built on
4//! [`marlin-nmea-envelope`](marlin_nmea_envelope).
5//!
6//! The envelope crate produces [`RawSentence`] values — verified framing,
7//! checksum-checked, fields split as byte slices. This crate turns those
8//! into typed Rust structs:
9//!
10//! ```text
11//! bytes → marlin_nmea_envelope → RawSentence → marlin_nmea_0183 → Nmea0183Message
12//! ```
13//!
14//! # Supported sentence types
15//!
16//! - **`$__GGA`** — [`GgaData`] — position + fix quality + satellites
17//! - **`$__HDT`** — [`HdtData`] — true heading
18//! - **`$__VTG`** — [`VtgData`] — course & speed over ground
19//! - **`$PSXN`** — [`PsxnData`] — Kongsberg-family proprietary motion; slot
20//! meanings are install-configurable via [`PsxnLayout`] / [`DecodeOptions`]
21//! - **`$PRDID`** — [`PrdidData`] — proprietary attitude with multiple
22//! vendor dialects; default refuses to guess (emits
23//! [`PrdidData::Raw`]). Select a dialect via
24//! [`DecodeOptions::with_prdid_dialect`].
25//!
26//! The talker ID is preserved as metadata on each typed struct rather
27//! than dispatched on; `$GPGGA`, `$INGGA`, `$GNGGA` all decode to
28//! [`GgaData`] with different `talker` values.
29//!
30//! # Quickstart
31//!
32//! ```
33//! use marlin_nmea_envelope::{OneShot, SentenceSource};
34//! use marlin_nmea_0183::{decode, Nmea0183Message};
35//!
36//! // Classic NMEA 0183 GGA example (checksum 0x47 = XOR of body bytes).
37//! let mut parser = OneShot::new();
38//! parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47");
39//!
40//! let raw = parser.next_sentence().unwrap().unwrap();
41//! match decode(&raw).unwrap() {
42//! Nmea0183Message::Gga(gga) => {
43//! assert_eq!(gga.talker, Some(*b"GP"));
44//! assert_eq!(gga.satellites_used, Some(8));
45//! }
46//! _ => panic!("expected GGA"),
47//! }
48//! ```
49//!
50//! # Extension
51//!
52//! Each sentence type has a **public** per-sentence decoder
53//! ([`decode_gga`], [`decode_hdt`], …). Downstream crates that need
54//! proprietary sentences this crate doesn't decode can build their own
55//! enum and delegate to these decoders. See the crate
56//! [README](https://docs.rs/marlin-nmea-0183/latest/marlin_nmea_0183)
57//! for a full example.
58//!
59//! # Policy
60//!
61//! Unknown sentence types are returned as
62//! [`Nmea0183Message::Unknown`] with the raw sentence preserved. They
63//! are **not** silently dropped and **not** returned as errors — the
64//! caller decides what to do with them.
65
66#![doc(html_root_url = "https://docs.rs/marlin-nmea-0183/0.1.0")]
67#![no_std]
68
69extern crate alloc;
70
71mod error;
72mod message;
73mod parser;
74mod sentences;
75mod util;
76
77#[cfg(test)]
78pub(crate) mod testing;
79
80pub use error::DecodeError;
81pub use message::Nmea0183Message;
82pub use parser::{Nmea0183Error, Nmea0183Parser, Parser};
83pub use sentences::{
84 decode_gga, decode_gll, decode_hdt, decode_prdid, decode_prdid_pitch_roll_heading,
85 decode_prdid_roll_pitch_heading, decode_psxn, decode_rmc, decode_vtg, DataStatus, GgaData,
86 GgaFixQuality, GllData, HdtData, PrdidData, PrdidDialect, PrdidPitchRollHeading,
87 PrdidRollPitchHeading, PsxnData, PsxnLayout, PsxnLayoutParseError, PsxnSlot, RmcData,
88 RmcNavStatus, UtcDate, UtcTime, VtgData, VtgMode,
89};
90
91// Re-export the envelope's `RawSentence` for convenience — most callers
92// of this crate will need it to pattern-match `Nmea0183Message::Unknown`.
93pub use marlin_nmea_envelope::RawSentence;
94
95use marlin_nmea_envelope::RawSentence as Raw;
96
97/// Decode a [`RawSentence`] into a typed [`Nmea0183Message`] using the
98/// default [`DecodeOptions`].
99///
100/// Dispatch is on [`sentence_type`](marlin_nmea_envelope::RawSentence::sentence_type)
101/// alone — the talker ID is preserved on each typed struct but not used
102/// for routing. `$GPGGA`, `$INGGA`, `$GNGGA` all land in
103/// [`Nmea0183Message::Gga`] with distinct `talker` values.
104///
105/// Equivalent to `decode_with(raw, &DecodeOptions::default())`. Use
106/// [`decode_with`] when you need to configure ambiguous decodings such
107/// as the PSXN data-slot layout.
108///
109/// Sentence types this crate doesn't recognize return
110/// [`Nmea0183Message::Unknown`] wrapping a clone of the input, so the
111/// caller can decode them with their own logic or discard them.
112///
113/// # Errors
114///
115/// Returns [`DecodeError`] when a sentence of a **recognized** type has
116/// malformed fields (wrong field count, invalid number, invalid
117/// coordinate, etc.). Unknown types are **not** errors — see above.
118pub fn decode<'a>(raw: &Raw<'a>) -> Result<Nmea0183Message<'a>, DecodeError> {
119 decode_with(raw, &DecodeOptions::default())
120}
121
122/// Decode a [`RawSentence`] into a typed [`Nmea0183Message`] using the
123/// supplied [`DecodeOptions`].
124///
125/// The options carry per-sentence knobs for decodings that can't be
126/// inferred from the bytes alone — currently the [`PsxnLayout`].
127///
128/// # Errors
129///
130/// Propagates the same [`DecodeError`] variants as each per-sentence
131/// decoder. Unknown sentence types are **not** errors — they return
132/// [`Nmea0183Message::Unknown`] wrapping the input.
133pub fn decode_with<'a>(
134 raw: &Raw<'a>,
135 options: &DecodeOptions,
136) -> Result<Nmea0183Message<'a>, DecodeError> {
137 match raw.sentence_type {
138 "GGA" => Ok(Nmea0183Message::Gga(decode_gga(raw)?)),
139 "GLL" => Ok(Nmea0183Message::Gll(decode_gll(raw)?)),
140 "HDT" => Ok(Nmea0183Message::Hdt(decode_hdt(raw)?)),
141 "RMC" => Ok(Nmea0183Message::Rmc(decode_rmc(raw)?)),
142 "VTG" => Ok(Nmea0183Message::Vtg(decode_vtg(raw)?)),
143 "PSXN" => Ok(Nmea0183Message::Psxn(decode_psxn(
144 raw,
145 &options.psxn_layout,
146 )?)),
147 "PRDID" => Ok(Nmea0183Message::Prdid(decode_prdid(
148 raw,
149 options.prdid_dialect,
150 )?)),
151 _ => Ok(Nmea0183Message::Unknown(raw.clone())),
152 }
153}
154
155/// Runtime configuration for ambiguous sentence decodings.
156///
157/// Several NMEA sentence types cannot be interpreted correctly from
158/// bytes alone — their schema depends on vendor or install-time
159/// configuration. `DecodeOptions` carries the knobs needed to resolve
160/// that ambiguity.
161///
162/// Construct with [`Default`] and chain builder methods:
163///
164/// ```
165/// use marlin_nmea_0183::{DecodeOptions, PsxnLayout};
166///
167/// let opts = DecodeOptions::default()
168/// .with_psxn_layout("rphx1".parse::<PsxnLayout>().unwrap());
169/// ```
170///
171/// `#[non_exhaustive]` so future knobs (PRDID dialect, checksum
172/// policy, etc.) can land in minor versions without a breaking change.
173#[derive(Debug, Clone, Default)]
174#[non_exhaustive]
175pub struct DecodeOptions {
176 /// Layout used when decoding `$PSXN` sentences. See [`PsxnLayout`]
177 /// for the full configuration surface.
178 pub psxn_layout: PsxnLayout,
179 /// Dialect used when decoding `$PRDID` sentences. Default
180 /// [`PrdidDialect::Unknown`] refuses to guess and emits
181 /// [`PrdidData::Raw`].
182 pub prdid_dialect: PrdidDialect,
183}
184
185impl DecodeOptions {
186 /// Set the PSXN layout.
187 #[must_use]
188 pub fn with_psxn_layout(mut self, layout: PsxnLayout) -> Self {
189 self.psxn_layout = layout;
190 self
191 }
192
193 /// Set the PRDID dialect.
194 #[must_use]
195 pub fn with_prdid_dialect(mut self, dialect: PrdidDialect) -> Self {
196 self.prdid_dialect = dialect;
197 self
198 }
199}