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