Skip to main content

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//! | BeiDou ephemeris   | 1042                                     | [`BeidouEphemeris`] |
30//! | QZSS ephemeris     | 1044                                     | [`QzssEphemeris`] |
31//! | Galileo ephemeris  | 1045 / 1046                              | [`GalileoFnavEphemeris`] / [`GalileoInavEphemeris`] |
32//!
33//! Any other message number is preserved losslessly as [`Message::Unsupported`]
34//! (its raw body is kept so the frame still round-trips). Deferred message types
35//! include the other MSM variants (MSM1/2/3/5/6), the legacy L1/L1-L2
36//! observation messages (1001-1004, 1009-1012), the network-RTK and SSR
37//! correction families. They decode as `Unsupported` rather than erroring.
38//!
39//! ## Quick start
40//!
41//! ```
42//! use sidereon_core::rtcm::{self, Message, StationCoordinates};
43//!
44//! // Build a 1006 reference-coordinate message and frame it.
45//! let station = StationCoordinates {
46//!     message_number: 1006,
47//!     reference_station_id: 2003,
48//!     itrf_realization_year: 0,
49//!     gps_indicator: true,
50//!     glonass_indicator: true,
51//!     galileo_indicator: false,
52//!     reference_station_indicator: false,
53//!     ecef_x: 11_446_021_400,
54//!     single_receiver_oscillator: false,
55//!     reserved: false,
56//!     ecef_y: -7_415_136_500,
57//!     quarter_cycle_indicator: 0,
58//!     ecef_z: 12_602_528_900,
59//!     antenna_height: Some(15_000),
60//! };
61//! // A constructed message encodes either directly on the typed value or
62//! // through the [`Message`] wrapper; both produce the same body bytes.
63//! let body = station.encode();
64//! assert_eq!(body, Message::StationCoordinates(station).encode());
65//! let frame = rtcm::encode_frame(&body).unwrap();
66//!
67//! // Decode it back out of the framed stream.
68//! let decoded = rtcm::decode_messages(&frame);
69//! assert_eq!(decoded.len(), 1);
70//! match &decoded[0] {
71//!     Message::StationCoordinates(s) => assert_eq!(s.reference_station_id, 2003),
72//!     _ => panic!("expected station coordinates"),
73//! }
74//! ```
75
76mod antenna;
77pub(crate) mod bits;
78pub(crate) mod crc;
79mod ephemeris;
80mod framing;
81mod lli;
82mod msm;
83mod ssr;
84mod station;
85
86#[cfg(test)]
87mod tests;
88
89use crate::error::Result;
90
91use bits::BitReader;
92
93pub use antenna::AntennaDescriptor;
94pub use ephemeris::{
95    BeidouEphemeris, GalileoFnavEphemeris, GalileoInavEphemeris, GlonassEphemeris, GpsEphemeris,
96    QzssEphemeris,
97};
98pub use framing::{
99    decode_frame, encode_frame, DecodedFrame, FrameScanner, FRAME_OVERHEAD, MAX_BODY_LEN, PREAMBLE,
100};
101pub use lli::{
102    derive_lli, minimum_lock_time_ms, msm_epoch_dt_ms, msm_signal_rinex_code, CellLli,
103    LockTimeTracker, PreviousLock, LLI_HALF_CYCLE, LLI_LOSS_OF_LOCK,
104};
105pub use msm::{MsmHeader, MsmKind, MsmMessage, MsmSatellite, MsmSignal};
106pub use ssr::{
107    SsrClockRecord, SsrCodeBiasRecord, SsrHeader, SsrKind, SsrMessage, SsrOrbitRecord,
108    SsrPhaseBiasRecord, SsrPhaseBiasSignal,
109};
110pub use station::StationCoordinates;
111
112/// A message whose number is recognized but whose body this codec does not
113/// decode. The raw body is preserved so the frame still round-trips.
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct UnsupportedMessage {
116    /// The RTCM message number (read from the first 12 bits of the body).
117    pub message_number: u16,
118    /// The undecoded message body.
119    pub body: Vec<u8>,
120}
121
122/// A decoded RTCM byte stream plus diagnostics for skipped frames.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct RtcmStream {
125    /// Every message decoded from a CRC-valid frame, in stream order.
126    pub messages: Vec<Message>,
127    /// Forgiving stream diagnostics for skipped bytes and skipped frames.
128    pub diagnostics: StreamDiagnostics,
129}
130
131/// Diagnostics collected while scanning an RTCM byte stream.
132#[derive(Clone, Debug, Default, PartialEq, Eq)]
133pub struct StreamDiagnostics {
134    /// Bytes skipped while resynchronizing on the next valid frame.
135    pub resync_bytes: usize,
136    /// CRC-valid frames whose body could not be decoded into the message IR.
137    pub skipped_frames: Vec<FrameSkip>,
138}
139
140/// One CRC-valid frame that could not be decoded.
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct FrameSkip {
143    /// Byte offset of the frame preamble in the scanned buffer.
144    pub offset: usize,
145    /// RTCM message number when the body was long enough to carry one.
146    pub message_number: Option<u16>,
147    /// Why the body did not decode.
148    pub reason: FrameSkipReason,
149}
150
151/// Typed reason for a skipped CRC-valid frame.
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub enum FrameSkipReason {
154    /// The body ended before all required fields of its recognized type.
155    Truncated,
156    /// The body is internally inconsistent for its recognized type.
157    Malformed(String),
158}
159
160pub(crate) type DecodeResult<T> = std::result::Result<T, DecodeError>;
161
162#[derive(Debug)]
163pub(crate) enum DecodeError {
164    OutOfInput(bits::OutOfInput),
165    Error(crate::error::Error),
166}
167
168impl From<bits::OutOfInput> for DecodeError {
169    fn from(error: bits::OutOfInput) -> Self {
170        Self::OutOfInput(error)
171    }
172}
173
174impl From<crate::error::Error> for DecodeError {
175    fn from(error: crate::error::Error) -> Self {
176        Self::Error(error)
177    }
178}
179
180impl From<DecodeError> for crate::error::Error {
181    fn from(error: DecodeError) -> Self {
182        match error {
183            DecodeError::OutOfInput(error) => error.into(),
184            DecodeError::Error(error) => error,
185        }
186    }
187}
188
189#[derive(Debug)]
190struct DecodeFailure {
191    kind: FrameSkipReason,
192}
193
194/// The canonical, format-agnostic RTCM 3 message IR.
195///
196/// Each variant stores raw transmitted field integers (see the per-type docs),
197/// and [`Message::encode`] is the exact inverse of [`Message::decode`].
198///
199/// The variant set is the codec's full supported coverage; any other message
200/// number decodes to [`Message::Unsupported`], so the enum is exhaustive and a
201/// caller can both build any variant from scratch and match every case.
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub enum Message {
204    /// An MSM4 or MSM7 multi-signal observation message.
205    Msm(MsmMessage),
206    /// A 1005 / 1006 station antenna reference point.
207    StationCoordinates(StationCoordinates),
208    /// A 1007 / 1008 / 1033 antenna or receiver descriptor.
209    AntennaDescriptor(AntennaDescriptor),
210    /// A 1019 GPS broadcast ephemeris.
211    GpsEphemeris(GpsEphemeris),
212    /// A 1020 GLONASS broadcast ephemeris.
213    GlonassEphemeris(GlonassEphemeris),
214    /// A 1042 BeiDou broadcast ephemeris.
215    BeidouEphemeris(BeidouEphemeris),
216    /// A 1044 QZSS broadcast ephemeris.
217    QzssEphemeris(QzssEphemeris),
218    /// A 1045 Galileo F/NAV broadcast ephemeris.
219    GalileoFnavEphemeris(GalileoFnavEphemeris),
220    /// A 1046 Galileo I/NAV broadcast ephemeris.
221    GalileoInavEphemeris(GalileoInavEphemeris),
222    /// A supported RTCM SSR correction message.
223    Ssr(SsrMessage),
224    /// A recognized-but-undecoded message, preserved verbatim.
225    Unsupported(UnsupportedMessage),
226}
227
228/// Read the 12-bit RTCM message number from the start of a message body.
229///
230/// Returns [`Error::Parse`] if the body is shorter than 12 bits.
231pub fn message_number(body: &[u8]) -> Result<u16> {
232    message_number_classified(body).map_err(Into::into)
233}
234
235fn message_number_classified(body: &[u8]) -> DecodeResult<u16> {
236    let mut r = BitReader::new(body);
237    Ok(r.u(12)? as u16)
238}
239
240impl Message {
241    /// Decode a single RTCM 3 message body (the bytes between a frame's length
242    /// word and its CRC).
243    ///
244    /// Never errors on an unknown message number: an unrecognized type decodes
245    /// to [`Message::Unsupported`]. Errors only on a truncated body of a
246    /// recognized type.
247    pub fn decode(body: &[u8]) -> Result<Self> {
248        Self::decode_inner(body).map_err(Into::into)
249    }
250
251    fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
252        let number = message_number_classified(body)?;
253        let message = match number {
254            1005 | 1006 => Message::StationCoordinates(StationCoordinates::decode_inner(body)?),
255            1007 | 1008 | 1033 => {
256                Message::AntennaDescriptor(AntennaDescriptor::decode_inner(body)?)
257            }
258            1019 => Message::GpsEphemeris(GpsEphemeris::decode_inner(body)?),
259            1020 => Message::GlonassEphemeris(GlonassEphemeris::decode_inner(body)?),
260            1042 => Message::BeidouEphemeris(BeidouEphemeris::decode_inner(body)?),
261            1044 => Message::QzssEphemeris(QzssEphemeris::decode_inner(body)?),
262            1045 => Message::GalileoFnavEphemeris(GalileoFnavEphemeris::decode_inner(body)?),
263            1046 => Message::GalileoInavEphemeris(GalileoInavEphemeris::decode_inner(body)?),
264            n if msm::is_supported_msm(n) => Message::Msm(MsmMessage::decode_inner(body)?),
265            n if ssr::is_supported_ssr(n) => Message::Ssr(SsrMessage::decode_inner(body)?),
266            _ => Message::Unsupported(UnsupportedMessage {
267                message_number: number,
268                body: body.to_vec(),
269            }),
270        };
271        Ok(message)
272    }
273
274    fn decode_classified(body: &[u8]) -> std::result::Result<Self, DecodeFailure> {
275        Self::decode_inner(body).map_err(|error| DecodeFailure {
276            kind: match error {
277                DecodeError::OutOfInput(_) => FrameSkipReason::Truncated,
278                DecodeError::Error(crate::error::Error::Parse(message)) => {
279                    FrameSkipReason::Malformed(message)
280                }
281                DecodeError::Error(other) => FrameSkipReason::Malformed(other.to_string()),
282            },
283        })
284    }
285
286    /// Encode this message back into a body (without the transport frame).
287    pub fn encode(&self) -> Vec<u8> {
288        match self {
289            Message::Msm(m) => m.encode(),
290            Message::StationCoordinates(s) => s.encode(),
291            Message::AntennaDescriptor(a) => a.encode(),
292            Message::GpsEphemeris(e) => e.encode(),
293            Message::GlonassEphemeris(e) => e.encode(),
294            Message::BeidouEphemeris(e) => e.encode(),
295            Message::QzssEphemeris(e) => e.encode(),
296            Message::GalileoFnavEphemeris(e) => e.encode(),
297            Message::GalileoInavEphemeris(e) => e.encode(),
298            Message::Ssr(s) => s.encode(),
299            Message::Unsupported(u) => u.body.clone(),
300        }
301    }
302
303    /// The RTCM message number this IR encodes to.
304    pub fn message_number(&self) -> u16 {
305        match self {
306            Message::Msm(m) => m.message_number,
307            Message::StationCoordinates(s) => s.message_number,
308            Message::AntennaDescriptor(a) => a.message_number,
309            Message::GpsEphemeris(_) => 1019,
310            Message::GlonassEphemeris(_) => 1020,
311            Message::BeidouEphemeris(_) => 1042,
312            Message::QzssEphemeris(_) => 1044,
313            Message::GalileoFnavEphemeris(_) => 1045,
314            Message::GalileoInavEphemeris(_) => 1046,
315            Message::Ssr(s) => s.message_number,
316            Message::Unsupported(u) => u.message_number,
317        }
318    }
319
320    /// Decode this message and wrap it in a fresh RTCM transport frame.
321    ///
322    /// Returns [`Error::InvalidInput`] if the encoded body exceeds the frame
323    /// length limit.
324    pub fn to_frame(&self) -> Result<Vec<u8>> {
325        encode_frame(&self.encode())
326    }
327}
328
329/// Decode every CRC-valid frame in a byte buffer into the message IR.
330///
331/// Frames whose CRC fails, or whose body cannot be decoded, are skipped; the
332/// scan resynchronizes on the next preamble. This is the forgiving stream entry
333/// point for a noisy serial feed.
334pub fn decode_messages(bytes: &[u8]) -> Vec<Message> {
335    decode_stream(bytes).messages
336}
337
338/// Decode every CRC-valid frame while recording forgiving stream diagnostics.
339///
340/// Unknown message numbers decode to [`Message::Unsupported`] values and are
341/// not diagnostics. CRC-valid frames for recognized message types whose body
342/// cannot be decoded are skipped and recorded in [`RtcmStream::diagnostics`].
343pub fn decode_stream(bytes: &[u8]) -> RtcmStream {
344    let mut stream = RtcmStream {
345        messages: Vec::new(),
346        diagnostics: StreamDiagnostics::default(),
347    };
348    let mut pos = 0usize;
349
350    while pos < bytes.len() {
351        let Some(rel) = bytes[pos..].iter().position(|&b| b == PREAMBLE) else {
352            stream.diagnostics.resync_bytes += bytes.len() - pos;
353            break;
354        };
355        stream.diagnostics.resync_bytes += rel;
356        pos += rel;
357
358        if bytes.len() - pos < FRAME_OVERHEAD {
359            stream.diagnostics.resync_bytes += 1;
360            pos += 1;
361            continue;
362        }
363
364        let body_len = ((usize::from(bytes[pos + 1] & 0x03)) << 8) | usize::from(bytes[pos + 2]);
365        let frame_len = 3 + body_len + 3;
366        if bytes.len() - pos < frame_len {
367            stream.diagnostics.resync_bytes += 1;
368            pos += 1;
369            continue;
370        }
371
372        match decode_frame(&bytes[pos..pos + frame_len]) {
373            Ok(frame) => {
374                match Message::decode_classified(frame.body) {
375                    Ok(message) => stream.messages.push(message),
376                    Err(failure) => stream.diagnostics.skipped_frames.push(FrameSkip {
377                        offset: pos,
378                        message_number: message_number(frame.body).ok(),
379                        reason: failure.kind,
380                    }),
381                }
382                pos += frame.frame_len;
383            }
384            Err(_) => {
385                stream.diagnostics.resync_bytes += 1;
386                pos += 1;
387            }
388        }
389    }
390
391    stream
392}
393
394/// Owns an RTCM carry buffer for chunked stream decoding.
395#[derive(Clone, Debug, Default, PartialEq, Eq)]
396pub struct SsrStreamAssembler {
397    buf: Vec<u8>,
398}
399
400impl SsrStreamAssembler {
401    /// Build an empty assembler.
402    pub fn new() -> Self {
403        Self { buf: Vec::new() }
404    }
405
406    /// Append bytes and drain every complete CRC-valid frame.
407    pub fn push(&mut self, chunk: &[u8]) -> Vec<Result<Message>> {
408        self.buf.extend_from_slice(chunk);
409        let mut out = Vec::new();
410        let mut pos = 0usize;
411
412        while pos < self.buf.len() {
413            let Some(rel) = self.buf[pos..].iter().position(|&b| b == PREAMBLE) else {
414                pos = self.buf.len();
415                break;
416            };
417            pos += rel;
418            if self.buf.len() - pos < FRAME_OVERHEAD {
419                break;
420            }
421
422            let body_len =
423                ((usize::from(self.buf[pos + 1] & 0x03)) << 8) | usize::from(self.buf[pos + 2]);
424            let frame_len = 3 + body_len + 3;
425            if self.buf.len() - pos < frame_len {
426                break;
427            }
428
429            match decode_frame(&self.buf[pos..pos + frame_len]) {
430                Ok(frame) => {
431                    out.push(Message::decode(frame.body));
432                    pos += frame.frame_len;
433                }
434                Err(_) => {
435                    pos += 1;
436                }
437            }
438        }
439
440        if pos > 0 {
441            self.buf.drain(..pos);
442        }
443        out
444    }
445
446    /// Number of bytes retained for the next chunk.
447    pub fn retained_len(&self) -> usize {
448        self.buf.len()
449    }
450}