sidereon-core 0.11.1

The complete Sidereon engine: numerical astrodynamics propagation core plus the GNSS domain layer (SP3, broadcast ephemeris, multi-GNSS positioning, RTK/PPP, ionosphere/troposphere, DOP) behind a default-on gnss feature
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! RTCM 3 differential-GNSS stream decoding and encoding.
//!
//! RTCM 10403.x ("RTCM Standard for Differential GNSS Services, Version 3") is
//! the dominant wire format for real-time GNSS correction and observation
//! streams: base-station observations, reference coordinates, antenna metadata,
//! and broadcast ephemerides flow from a caster to a rover as a sequence of
//! framed binary messages. This module is a sans-I/O codec for that stream,
//! built to the same shape as the crate's RINEX / SP3 / IONEX parsers:
//!
//! 1. a forgiving byte-level frame layer ([`framing`]) that syncs on the `0xD3`
//!    preamble, reads the 10-bit length, and verifies the 24-bit CRC-24Q;
//! 2. a format-agnostic canonical IR ([`Message`] and its typed variants) that
//!    stores each field as its raw transmitted integer; and
//! 3. an encoder that turns the IR back into bytes, so a decode followed by an
//!    encode round-trips byte-for-byte.
//!
//! ## Message coverage
//!
//! Decoded and encoded:
//!
//! | Message            | Numbers                                  | IR type |
//! |--------------------|------------------------------------------|---------|
//! | MSM4 observations  | 1074 / 1084 / 1094 / 1104 / 1114 / 1124 / 1134 | [`MsmMessage`] |
//! | MSM7 observations  | 1077 / 1087 / 1097 / 1107 / 1117 / 1127 / 1137 | [`MsmMessage`] |
//! | Station coordinates| 1005 / 1006                              | [`StationCoordinates`] |
//! | Antenna / receiver | 1007 / 1008 / 1033                       | [`AntennaDescriptor`] |
//! | GPS ephemeris      | 1019                                     | [`GpsEphemeris`] |
//! | GLONASS ephemeris  | 1020                                     | [`GlonassEphemeris`] |
//!
//! Any other message number is preserved losslessly as [`Message::Unsupported`]
//! (its raw body is kept so the frame still round-trips). Deferred message types
//! include the other MSM variants (MSM1/2/3/5/6), the legacy L1/L1-L2
//! observation messages (1001-1004, 1009-1012), the network-RTK and SSR
//! correction families, and the Galileo / BeiDou / QZSS ephemerides
//! (1042-1046). They decode as `Unsupported` rather than erroring.
//!
//! ## Quick start
//!
//! ```
//! use sidereon_core::rtcm::{self, Message, StationCoordinates};
//!
//! // Build a 1006 reference-coordinate message and frame it.
//! let station = StationCoordinates {
//!     message_number: 1006,
//!     reference_station_id: 2003,
//!     itrf_realization_year: 0,
//!     gps_indicator: true,
//!     glonass_indicator: true,
//!     galileo_indicator: false,
//!     reference_station_indicator: false,
//!     ecef_x: 11_446_021_400,
//!     single_receiver_oscillator: false,
//!     reserved: false,
//!     ecef_y: -7_415_136_500,
//!     quarter_cycle_indicator: 0,
//!     ecef_z: 12_602_528_900,
//!     antenna_height: Some(15_000),
//! };
//! // A constructed message encodes either directly on the typed value or
//! // through the [`Message`] wrapper; both produce the same body bytes.
//! let body = station.encode();
//! assert_eq!(body, Message::StationCoordinates(station).encode());
//! let frame = rtcm::encode_frame(&body).unwrap();
//!
//! // Decode it back out of the framed stream.
//! let decoded = rtcm::decode_messages(&frame);
//! assert_eq!(decoded.len(), 1);
//! match &decoded[0] {
//!     Message::StationCoordinates(s) => assert_eq!(s.reference_station_id, 2003),
//!     _ => panic!("expected station coordinates"),
//! }
//! ```

mod antenna;
pub(crate) mod bits;
pub(crate) mod crc;
mod ephemeris;
mod framing;
mod lli;
mod msm;
mod ssr;
mod station;

#[cfg(test)]
mod tests;

use crate::error::Result;

use bits::BitReader;

pub use antenna::AntennaDescriptor;
pub use ephemeris::{GlonassEphemeris, GpsEphemeris};
pub use framing::{
    decode_frame, encode_frame, DecodedFrame, FrameScanner, FRAME_OVERHEAD, MAX_BODY_LEN, PREAMBLE,
};
pub use lli::{
    derive_lli, minimum_lock_time_ms, msm_epoch_dt_ms, msm_signal_rinex_code, CellLli,
    LockTimeTracker, PreviousLock, LLI_HALF_CYCLE, LLI_LOSS_OF_LOCK,
};
pub use msm::{MsmHeader, MsmKind, MsmMessage, MsmSatellite, MsmSignal};
pub use ssr::{
    SsrClockRecord, SsrCodeBiasRecord, SsrHeader, SsrKind, SsrMessage, SsrOrbitRecord,
    SsrPhaseBiasRecord, SsrPhaseBiasSignal,
};
pub use station::StationCoordinates;

/// A message whose number is recognized but whose body this codec does not
/// decode. The raw body is preserved so the frame still round-trips.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnsupportedMessage {
    /// The RTCM message number (read from the first 12 bits of the body).
    pub message_number: u16,
    /// The undecoded message body.
    pub body: Vec<u8>,
}

/// A decoded RTCM byte stream plus diagnostics for skipped frames.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RtcmStream {
    /// Every message decoded from a CRC-valid frame, in stream order.
    pub messages: Vec<Message>,
    /// Forgiving stream diagnostics for skipped bytes and skipped frames.
    pub diagnostics: StreamDiagnostics,
}

/// Diagnostics collected while scanning an RTCM byte stream.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StreamDiagnostics {
    /// Bytes skipped while resynchronizing on the next valid frame.
    pub resync_bytes: usize,
    /// CRC-valid frames whose body could not be decoded into the message IR.
    pub skipped_frames: Vec<FrameSkip>,
}

/// One CRC-valid frame that could not be decoded.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrameSkip {
    /// Byte offset of the frame preamble in the scanned buffer.
    pub offset: usize,
    /// RTCM message number when the body was long enough to carry one.
    pub message_number: Option<u16>,
    /// Why the body did not decode.
    pub reason: FrameSkipReason,
}

/// Typed reason for a skipped CRC-valid frame.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FrameSkipReason {
    /// The body ended before all required fields of its recognized type.
    Truncated,
    /// The body is internally inconsistent for its recognized type.
    Malformed(String),
}

pub(crate) type DecodeResult<T> = std::result::Result<T, DecodeError>;

#[derive(Debug)]
pub(crate) enum DecodeError {
    OutOfInput(bits::OutOfInput),
    Error(crate::error::Error),
}

impl From<bits::OutOfInput> for DecodeError {
    fn from(error: bits::OutOfInput) -> Self {
        Self::OutOfInput(error)
    }
}

impl From<crate::error::Error> for DecodeError {
    fn from(error: crate::error::Error) -> Self {
        Self::Error(error)
    }
}

impl From<DecodeError> for crate::error::Error {
    fn from(error: DecodeError) -> Self {
        match error {
            DecodeError::OutOfInput(error) => error.into(),
            DecodeError::Error(error) => error,
        }
    }
}

#[derive(Debug)]
struct DecodeFailure {
    kind: FrameSkipReason,
}

/// The canonical, format-agnostic RTCM 3 message IR.
///
/// Each variant stores raw transmitted field integers (see the per-type docs),
/// and [`Message::encode`] is the exact inverse of [`Message::decode`].
///
/// The variant set is the codec's full supported coverage; any other message
/// number decodes to [`Message::Unsupported`], so the enum is exhaustive and a
/// caller can both build any variant from scratch and match every case.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
    /// An MSM4 or MSM7 multi-signal observation message.
    Msm(MsmMessage),
    /// A 1005 / 1006 station antenna reference point.
    StationCoordinates(StationCoordinates),
    /// A 1007 / 1008 / 1033 antenna or receiver descriptor.
    AntennaDescriptor(AntennaDescriptor),
    /// A 1019 GPS broadcast ephemeris.
    GpsEphemeris(GpsEphemeris),
    /// A 1020 GLONASS broadcast ephemeris.
    GlonassEphemeris(GlonassEphemeris),
    /// A supported RTCM SSR correction message.
    Ssr(SsrMessage),
    /// A recognized-but-undecoded message, preserved verbatim.
    Unsupported(UnsupportedMessage),
}

/// Read the 12-bit RTCM message number from the start of a message body.
///
/// Returns [`Error::Parse`] if the body is shorter than 12 bits.
pub fn message_number(body: &[u8]) -> Result<u16> {
    message_number_classified(body).map_err(Into::into)
}

fn message_number_classified(body: &[u8]) -> DecodeResult<u16> {
    let mut r = BitReader::new(body);
    Ok(r.u(12)? as u16)
}

impl Message {
    /// Decode a single RTCM 3 message body (the bytes between a frame's length
    /// word and its CRC).
    ///
    /// Never errors on an unknown message number: an unrecognized type decodes
    /// to [`Message::Unsupported`]. Errors only on a truncated body of a
    /// recognized type.
    pub fn decode(body: &[u8]) -> Result<Self> {
        Self::decode_inner(body).map_err(Into::into)
    }

    fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
        let number = message_number_classified(body)?;
        let message = match number {
            1005 | 1006 => Message::StationCoordinates(StationCoordinates::decode_inner(body)?),
            1007 | 1008 | 1033 => {
                Message::AntennaDescriptor(AntennaDescriptor::decode_inner(body)?)
            }
            1019 => Message::GpsEphemeris(GpsEphemeris::decode_inner(body)?),
            1020 => Message::GlonassEphemeris(GlonassEphemeris::decode_inner(body)?),
            n if msm::is_supported_msm(n) => Message::Msm(MsmMessage::decode_inner(body)?),
            n if ssr::is_supported_ssr(n) => Message::Ssr(SsrMessage::decode_inner(body)?),
            _ => Message::Unsupported(UnsupportedMessage {
                message_number: number,
                body: body.to_vec(),
            }),
        };
        Ok(message)
    }

    fn decode_classified(body: &[u8]) -> std::result::Result<Self, DecodeFailure> {
        Self::decode_inner(body).map_err(|error| DecodeFailure {
            kind: match error {
                DecodeError::OutOfInput(_) => FrameSkipReason::Truncated,
                DecodeError::Error(crate::error::Error::Parse(message)) => {
                    FrameSkipReason::Malformed(message)
                }
                DecodeError::Error(other) => FrameSkipReason::Malformed(other.to_string()),
            },
        })
    }

    /// Encode this message back into a body (without the transport frame).
    pub fn encode(&self) -> Vec<u8> {
        match self {
            Message::Msm(m) => m.encode(),
            Message::StationCoordinates(s) => s.encode(),
            Message::AntennaDescriptor(a) => a.encode(),
            Message::GpsEphemeris(e) => e.encode(),
            Message::GlonassEphemeris(e) => e.encode(),
            Message::Ssr(s) => s.encode(),
            Message::Unsupported(u) => u.body.clone(),
        }
    }

    /// The RTCM message number this IR encodes to.
    pub fn message_number(&self) -> u16 {
        match self {
            Message::Msm(m) => m.message_number,
            Message::StationCoordinates(s) => s.message_number,
            Message::AntennaDescriptor(a) => a.message_number,
            Message::GpsEphemeris(_) => 1019,
            Message::GlonassEphemeris(_) => 1020,
            Message::Ssr(s) => s.message_number,
            Message::Unsupported(u) => u.message_number,
        }
    }

    /// Decode this message and wrap it in a fresh RTCM transport frame.
    ///
    /// Returns [`Error::InvalidInput`] if the encoded body exceeds the frame
    /// length limit.
    pub fn to_frame(&self) -> Result<Vec<u8>> {
        encode_frame(&self.encode())
    }
}

/// Decode every CRC-valid frame in a byte buffer into the message IR.
///
/// Frames whose CRC fails, or whose body cannot be decoded, are skipped; the
/// scan resynchronizes on the next preamble. This is the forgiving stream entry
/// point for a noisy serial feed.
pub fn decode_messages(bytes: &[u8]) -> Vec<Message> {
    decode_stream(bytes).messages
}

/// Decode every CRC-valid frame while recording forgiving stream diagnostics.
///
/// Unknown message numbers decode to [`Message::Unsupported`] values and are
/// not diagnostics. CRC-valid frames for recognized message types whose body
/// cannot be decoded are skipped and recorded in [`RtcmStream::diagnostics`].
pub fn decode_stream(bytes: &[u8]) -> RtcmStream {
    let mut stream = RtcmStream {
        messages: Vec::new(),
        diagnostics: StreamDiagnostics::default(),
    };
    let mut pos = 0usize;

    while pos < bytes.len() {
        let Some(rel) = bytes[pos..].iter().position(|&b| b == PREAMBLE) else {
            stream.diagnostics.resync_bytes += bytes.len() - pos;
            break;
        };
        stream.diagnostics.resync_bytes += rel;
        pos += rel;

        if bytes.len() - pos < FRAME_OVERHEAD {
            stream.diagnostics.resync_bytes += 1;
            pos += 1;
            continue;
        }

        let body_len = ((usize::from(bytes[pos + 1] & 0x03)) << 8) | usize::from(bytes[pos + 2]);
        let frame_len = 3 + body_len + 3;
        if bytes.len() - pos < frame_len {
            stream.diagnostics.resync_bytes += 1;
            pos += 1;
            continue;
        }

        match decode_frame(&bytes[pos..pos + frame_len]) {
            Ok(frame) => {
                match Message::decode_classified(frame.body) {
                    Ok(message) => stream.messages.push(message),
                    Err(failure) => stream.diagnostics.skipped_frames.push(FrameSkip {
                        offset: pos,
                        message_number: message_number(frame.body).ok(),
                        reason: failure.kind,
                    }),
                }
                pos += frame.frame_len;
            }
            Err(_) => {
                stream.diagnostics.resync_bytes += 1;
                pos += 1;
            }
        }
    }

    stream
}

/// Owns an RTCM carry buffer for chunked stream decoding.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SsrStreamAssembler {
    buf: Vec<u8>,
}

impl SsrStreamAssembler {
    /// Build an empty assembler.
    pub fn new() -> Self {
        Self { buf: Vec::new() }
    }

    /// Append bytes and drain every complete CRC-valid frame.
    pub fn push(&mut self, chunk: &[u8]) -> Vec<Result<Message>> {
        self.buf.extend_from_slice(chunk);
        let mut out = Vec::new();
        let mut pos = 0usize;

        while pos < self.buf.len() {
            let Some(rel) = self.buf[pos..].iter().position(|&b| b == PREAMBLE) else {
                pos = self.buf.len();
                break;
            };
            pos += rel;
            if self.buf.len() - pos < FRAME_OVERHEAD {
                break;
            }

            let body_len =
                ((usize::from(self.buf[pos + 1] & 0x03)) << 8) | usize::from(self.buf[pos + 2]);
            let frame_len = 3 + body_len + 3;
            if self.buf.len() - pos < frame_len {
                break;
            }

            match decode_frame(&self.buf[pos..pos + frame_len]) {
                Ok(frame) => {
                    out.push(Message::decode(frame.body));
                    pos += frame.frame_len;
                }
                Err(_) => {
                    pos += 1;
                }
            }
        }

        if pos > 0 {
            self.buf.drain(..pos);
        }
        out
    }

    /// Number of bytes retained for the next chunk.
    pub fn retained_len(&self) -> usize {
        self.buf.len()
    }
}