sidereon_core/rtcm/mod.rs
1//! RTCM 3 differential-GNSS stream decoding and encoding.
2//!
3//! RTCM 10403.x ("RTCM Standard for Differential GNSS Services, Version 3") is
4//! the dominant wire format for real-time GNSS correction and observation
5//! streams: base-station observations, reference coordinates, antenna metadata,
6//! and broadcast ephemerides flow from a caster to a rover as a sequence of
7//! framed binary messages. This module is a sans-I/O codec for that stream,
8//! built to the same shape as the crate's RINEX / SP3 / IONEX parsers:
9//!
10//! 1. a forgiving byte-level frame layer ([`framing`]) that syncs on the `0xD3`
11//! preamble, reads the 10-bit length, and verifies the 24-bit CRC-24Q;
12//! 2. a format-agnostic canonical IR ([`Message`] and its typed variants) that
13//! stores each field as its raw transmitted integer; and
14//! 3. an encoder that turns the IR back into bytes, so a decode followed by an
15//! encode round-trips byte-for-byte.
16//!
17//! ## Message coverage
18//!
19//! Decoded and encoded:
20//!
21//! | Message | Numbers | IR type |
22//! |--------------------|------------------------------------------|---------|
23//! | MSM4 observations | 1074 / 1084 / 1094 / 1104 / 1114 / 1124 / 1134 | [`MsmMessage`] |
24//! | MSM7 observations | 1077 / 1087 / 1097 / 1107 / 1117 / 1127 / 1137 | [`MsmMessage`] |
25//! | Station coordinates| 1005 / 1006 | [`StationCoordinates`] |
26//! | Antenna / receiver | 1007 / 1008 / 1033 | [`AntennaDescriptor`] |
27//! | GPS ephemeris | 1019 | [`GpsEphemeris`] |
28//! | GLONASS ephemeris | 1020 | [`GlonassEphemeris`] |
29//!
30//! Any other message number is preserved losslessly as [`Message::Unsupported`]
31//! (its raw body is kept so the frame still round-trips). Deferred message types
32//! include the other MSM variants (MSM1/2/3/5/6), the legacy L1/L1-L2
33//! observation messages (1001-1004, 1009-1012), the network-RTK and SSR
34//! correction families, and the Galileo / BeiDou / QZSS ephemerides
35//! (1042-1046). They decode as `Unsupported` rather than erroring.
36//!
37//! ## Quick start
38//!
39//! ```
40//! use sidereon_core::rtcm::{self, Message, StationCoordinates};
41//!
42//! // Build a 1006 reference-coordinate message and frame it.
43//! let station = StationCoordinates {
44//! message_number: 1006,
45//! reference_station_id: 2003,
46//! itrf_realization_year: 0,
47//! gps_indicator: true,
48//! glonass_indicator: true,
49//! galileo_indicator: false,
50//! reference_station_indicator: false,
51//! ecef_x: 11_446_021_400,
52//! single_receiver_oscillator: false,
53//! reserved: false,
54//! ecef_y: -7_415_136_500,
55//! quarter_cycle_indicator: 0,
56//! ecef_z: 12_602_528_900,
57//! antenna_height: Some(15_000),
58//! };
59//! // A constructed message encodes either directly on the typed value or
60//! // through the [`Message`] wrapper; both produce the same body bytes.
61//! let body = station.encode();
62//! assert_eq!(body, Message::StationCoordinates(station).encode());
63//! let frame = rtcm::encode_frame(&body).unwrap();
64//!
65//! // Decode it back out of the framed stream.
66//! let decoded = rtcm::decode_messages(&frame);
67//! assert_eq!(decoded.len(), 1);
68//! match &decoded[0] {
69//! Message::StationCoordinates(s) => assert_eq!(s.reference_station_id, 2003),
70//! _ => panic!("expected station coordinates"),
71//! }
72//! ```
73
74mod antenna;
75mod bits;
76mod crc;
77mod ephemeris;
78mod framing;
79mod msm;
80mod station;
81
82#[cfg(test)]
83mod tests;
84
85use crate::error::Result;
86
87use bits::BitReader;
88
89pub use antenna::AntennaDescriptor;
90pub use ephemeris::{GlonassEphemeris, GpsEphemeris};
91pub use framing::{
92 decode_frame, encode_frame, DecodedFrame, FrameScanner, FRAME_OVERHEAD, MAX_BODY_LEN, PREAMBLE,
93};
94pub use msm::{MsmHeader, MsmKind, MsmMessage, MsmSatellite, MsmSignal};
95pub use station::StationCoordinates;
96
97/// A message whose number is recognized but whose body this codec does not
98/// decode. The raw body is preserved so the frame still round-trips.
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct UnsupportedMessage {
101 /// The RTCM message number (read from the first 12 bits of the body).
102 pub message_number: u16,
103 /// The undecoded message body.
104 pub body: Vec<u8>,
105}
106
107/// The canonical, format-agnostic RTCM 3 message IR.
108///
109/// Each variant stores raw transmitted field integers (see the per-type docs),
110/// and [`Message::encode`] is the exact inverse of [`Message::decode`].
111///
112/// The variant set is the codec's full supported coverage; any other message
113/// number decodes to [`Message::Unsupported`], so the enum is exhaustive and a
114/// caller can both build any variant from scratch and match every case.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub enum Message {
117 /// An MSM4 or MSM7 multi-signal observation message.
118 Msm(MsmMessage),
119 /// A 1005 / 1006 station antenna reference point.
120 StationCoordinates(StationCoordinates),
121 /// A 1007 / 1008 / 1033 antenna or receiver descriptor.
122 AntennaDescriptor(AntennaDescriptor),
123 /// A 1019 GPS broadcast ephemeris.
124 GpsEphemeris(GpsEphemeris),
125 /// A 1020 GLONASS broadcast ephemeris.
126 GlonassEphemeris(GlonassEphemeris),
127 /// A recognized-but-undecoded message, preserved verbatim.
128 Unsupported(UnsupportedMessage),
129}
130
131/// Read the 12-bit RTCM message number from the start of a message body.
132///
133/// Returns [`Error::Parse`] if the body is shorter than 12 bits.
134pub fn message_number(body: &[u8]) -> Result<u16> {
135 let mut r = BitReader::new(body);
136 Ok(r.u(12)? as u16)
137}
138
139impl Message {
140 /// Decode a single RTCM 3 message body (the bytes between a frame's length
141 /// word and its CRC).
142 ///
143 /// Never errors on an unknown message number: an unrecognized type decodes
144 /// to [`Message::Unsupported`]. Errors only on a truncated body of a
145 /// recognized type.
146 pub fn decode(body: &[u8]) -> Result<Self> {
147 let number = message_number(body)?;
148 let message = match number {
149 1005 | 1006 => Message::StationCoordinates(StationCoordinates::decode(body)?),
150 1007 | 1008 | 1033 => Message::AntennaDescriptor(AntennaDescriptor::decode(body)?),
151 1019 => Message::GpsEphemeris(GpsEphemeris::decode(body)?),
152 1020 => Message::GlonassEphemeris(GlonassEphemeris::decode(body)?),
153 n if msm::is_supported_msm(n) => Message::Msm(MsmMessage::decode(body)?),
154 _ => Message::Unsupported(UnsupportedMessage {
155 message_number: number,
156 body: body.to_vec(),
157 }),
158 };
159 Ok(message)
160 }
161
162 /// Encode this message back into a body (without the transport frame).
163 pub fn encode(&self) -> Vec<u8> {
164 match self {
165 Message::Msm(m) => m.encode(),
166 Message::StationCoordinates(s) => s.encode(),
167 Message::AntennaDescriptor(a) => a.encode(),
168 Message::GpsEphemeris(e) => e.encode(),
169 Message::GlonassEphemeris(e) => e.encode(),
170 Message::Unsupported(u) => u.body.clone(),
171 }
172 }
173
174 /// The RTCM message number this IR encodes to.
175 pub fn message_number(&self) -> u16 {
176 match self {
177 Message::Msm(m) => m.message_number,
178 Message::StationCoordinates(s) => s.message_number,
179 Message::AntennaDescriptor(a) => a.message_number,
180 Message::GpsEphemeris(_) => 1019,
181 Message::GlonassEphemeris(_) => 1020,
182 Message::Unsupported(u) => u.message_number,
183 }
184 }
185
186 /// Decode this message and wrap it in a fresh RTCM transport frame.
187 ///
188 /// Returns [`Error::InvalidInput`] if the encoded body exceeds the frame
189 /// length limit.
190 pub fn to_frame(&self) -> Result<Vec<u8>> {
191 encode_frame(&self.encode())
192 }
193}
194
195/// Decode every CRC-valid frame in a byte buffer into the message IR.
196///
197/// Frames whose CRC fails, or whose body cannot be decoded, are skipped; the
198/// scan resynchronizes on the next preamble. This is the forgiving stream entry
199/// point for a noisy serial feed.
200pub fn decode_messages(bytes: &[u8]) -> Vec<Message> {
201 FrameScanner::new(bytes)
202 .filter_map(|frame| Message::decode(frame.body).ok())
203 .collect()
204}