Skip to main content

kinavis_nmea0183/
lib.rs

1//! NMEA 0183 sentences to and from the KINAVIS types.
2//!
3//! Anti-corruption layer between receiver sentences (`$GPRMC,...*hh`) and the
4//! domain:
5//!
6//! 1. **Framing**: length, printable characters, start character and a
7//!    mandatory checksum. Any failure is an [`NmeaError`].
8//! 2. **Decoding** into typed records — [`Rmc`], [`Gga`], [`Gll`], [`Vtg`] —
9//!    whose fields are kernel types ([`Position`], [`Speed`], [`TrueCourse`]);
10//!    out-of-domain values (latitude 95°) and implausible ones (see
11//!    [Plausibility bounds](#plausibility-bounds)) are rejected here. AIS arrives as
12//!    [`Vdm`] with the payload still armoured; decoding it is the AIS crate's
13//!    job.
14//! 3. **Translation** into a [`GnssFix`] via `TryFrom<Rmc>` or [`Gga::fix_on`].
15//!
16//! Every step works on the input in place: no copies, no allocation, no panics.
17//! The crate builds for bare-metal targets without an allocator; CI checks the
18//! strict-profile build for panic paths.
19//!
20//! [`encode`] and `Display` write a record back with its checksum, for
21//! generating sentences and for round-trip tests.
22//!
23//! ```rust
24//! use kinavis_nmea0183::{parse, Sentence};
25//! use kinavis_kernel::GnssFix;
26//!
27//! let line = b"$GPRMC,225444.00,A,4916.4500,N,12311.1200,W,3.0,272.5,110926,5.0,W,D*30\r\n";
28//! let Sentence::Rmc(rmc) = parse(line)? else { panic!("not an RMC") };
29//!
30//! let fix = GnssFix::try_from(rmc)?;
31//! assert_eq!(format!("{}", fix.taken_at()), "2026-09-11T22:54:44.000 UTC");
32//! assert_eq!(format!("{:.2}", fix.position()), "49°16.45'N 123°11.12'W");
33//! assert_eq!(fix.speed_over_ground().map(|s| s.knots()), Some(3.0));
34//! assert!(fix.quality().fix_type().is_position_fix());
35//!
36//! // And back out again, checksum recomputed.
37//! assert_eq!(format!("{rmc}"), "$GPRMC,225444.00,A,4916.4500,N,12311.1200,W,3.0,272.5,110926,5.0,W,D*30");
38//! # Ok::<(), Box<dyn std::error::Error>>(())
39//! ```
40//!
41//! [`Position`]: kinavis_kernel::Position
42//! [`Speed`]: kinavis_kernel::Speed
43//! [`TrueCourse`]: kinavis_kernel::TrueCourse
44//! [`GnssFix`]: kinavis_kernel::GnssFix
45//!
46//! # Plausibility bounds
47//!
48//! A value outside these inclusive bounds is a corrupt field, not a
49//! measurement, and fails with [`NmeaError::Value`]:
50//!
51//! | Field | Bounds |
52//! |---|---|
53//! | Speed over ground | 0 to 1000 kn; 0 to 1852 km/h |
54//! | Altitude (GGA) | −10 000 m to 100 000 m |
55//! | Geoid separation (GGA) | −1000 m to 1000 m |
56//! | Dilution of precision | 0 to 100; 0 reads as not available |
57//! | Magnetic variation (RMC) | 180° either way; 999 and above read as not available |
58//! | Differential age (GGA) | 0 s to 9999 s |
59//!
60//! [`parse`] accepts sentences up to [`MAX_ACCEPTED_BYTES`], past the
61//! standard's [`MAX_SENTENCE_BYTES`]: receivers write longer ones.
62//! [`encode`] refuses a sentence longer than [`MAX_SENTENCE_BYTES`] with
63//! [`NmeaError::TooLong`]. RMC, GLL and VTG always fit; a VDM fits with at
64//! most 62 payload characters; a GGA can exceed the limit only with several
65//! fields near their bounds at once.
66//!
67//! # Not supported
68//!
69//! Other sentences (satellites in view, DOP breakdowns, proprietary) return
70//! [`Sentence::Unsupported`] with their address and a verified checksum, for
71//! counting or logging.
72//!
73//! # Feature flags
74//!
75//! - `std` *(default)* — standard library maths in the kernel.
76//! - `libm` — for `no_std` targets: `--no-default-features --features libm`.
77
78#![cfg_attr(not(feature = "std"), no_std)]
79
80mod encode;
81mod error;
82mod field;
83mod frame;
84mod sentence;
85
86pub use encode::encode;
87pub use error::{NmeaError, TranslationError};
88pub use frame::{MAX_ACCEPTED_BYTES, MAX_SENTENCE_BYTES};
89pub use sentence::{
90    Address, Channel, Date, Gga, Gll, Mode, Payload, Rmc, Sentence, Status, Talker, TimeOfDay, Vdm,
91    Vtg, MAX_FRAGMENTS, MAX_PAYLOAD_CHARS,
92};
93
94/// Parses one sentence.
95///
96/// Input: one line from `$` to the checksum, with or without `CR LF`.
97///
98/// # Errors
99///
100/// [`NmeaError`] for framing, checksum, unparseable fields or out-of-domain
101/// values. A well-formed sentence of an unsupported type is not an error: it
102/// returns [`Sentence::Unsupported`].
103pub fn parse(sentence: &[u8]) -> Result<Sentence, NmeaError> {
104    let frame = frame::Frame::parse(sentence)?;
105    Sentence::decode(frame)
106}