Skip to main content

sidereon_core/rtcm/
ssr.rs

1//! RTCM SSR orbit, clock, URA, and high-rate clock messages.
2//!
3//! This module is the wire-level IR for the RTCM SSR Phase A messages. Values
4//! are stored as the raw transmitted integers. Scaling to meters and seconds is
5//! handled by the crate-level `ssr` correction store.
6
7use crate::error::{Error, Result};
8use crate::id::GnssSystem;
9
10use super::bits::{BitReader, BitWriter};
11use super::DecodeResult;
12
13/// The SSR message group derived from the message number.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum SsrKind {
16    /// Orbit corrections.
17    Orbit,
18    /// Clock corrections.
19    Clock,
20    /// Combined orbit and clock corrections.
21    CombinedOrbitClock,
22    /// Code-bias corrections.
23    CodeBias,
24    /// Phase-bias corrections.
25    PhaseBias,
26    /// User range accuracy.
27    Ura,
28    /// High-rate clock correction.
29    HighRateClock,
30    /// VTEC ionosphere correction.
31    Vtec,
32}
33
34/// Common header for RTCM SSR messages.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct SsrHeader {
37    /// SSR epoch time. GPS, Galileo, and BeiDou use seconds of week; GLONASS
38    /// uses its RTCM 17-bit day time field.
39    pub epoch_time_s: u32,
40    /// SSR update interval index.
41    pub update_interval: u8,
42    /// Multiple-message indicator.
43    pub multiple_message: bool,
44    /// IOD SSR.
45    pub iod_ssr: u8,
46    /// SSR provider identifier.
47    pub provider_id: u16,
48    /// SSR solution identifier.
49    pub solution_id: u8,
50    /// Satellite reference datum bit for orbit and combined messages.
51    pub satellite_reference_datum: Option<bool>,
52    /// Phase-bias dispersive-bias consistency flag.
53    pub dispersive_bias_consistency: Option<bool>,
54    /// Phase-bias Melbourne-Wubbena consistency flag.
55    pub mw_consistency: Option<bool>,
56    /// Number of satellite records.
57    pub satellite_count: u8,
58}
59
60/// One satellite orbit-correction record.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct SsrOrbitRecord {
63    /// Constellation-native satellite id.
64    pub satellite_id: u8,
65    /// Referenced broadcast issue, with constellation-specific bit width.
66    pub iode: u32,
67    /// Radial delta, int22, scale 0.1 mm.
68    pub delta_radial: i32,
69    /// Along-track delta, int20, scale 0.4 mm.
70    pub delta_along: i32,
71    /// Cross-track delta, int20, scale 0.4 mm.
72    pub delta_cross: i32,
73    /// Radial delta rate, int21, scale 0.001 mm/s.
74    pub dot_delta_radial: i32,
75    /// Along-track delta rate, int19, scale 0.004 mm/s.
76    pub dot_delta_along: i32,
77    /// Cross-track delta rate, int19, scale 0.004 mm/s.
78    pub dot_delta_cross: i32,
79}
80
81/// One satellite clock-correction record.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct SsrClockRecord {
84    /// Constellation-native satellite id.
85    pub satellite_id: u8,
86    /// C0 clock term, int22, scale 0.1 mm.
87    pub c0: i32,
88    /// C1 clock term, int21, scale 0.001 mm/s.
89    pub c1: i32,
90    /// C2 clock term, int27, scale 0.02 micrometer/s^2.
91    pub c2: i32,
92}
93
94/// One satellite code-bias record.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct SsrCodeBiasRecord {
97    /// Constellation-native satellite id.
98    pub satellite_id: u8,
99    /// Raw signal and tracking-mode id plus raw bias.
100    pub biases: Vec<(u8, i16)>,
101}
102
103/// One signal in a phase-bias record.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct SsrPhaseBiasSignal {
106    /// Raw signal and tracking-mode id.
107    pub signal_id: u8,
108    /// Signal integer indicator.
109    pub integer_indicator: u8,
110    /// Wide-lane integer indicator.
111    pub wide_lane_integer_indicator: u8,
112    /// Discontinuity counter.
113    pub discontinuity_counter: u8,
114    /// Raw phase bias.
115    pub bias: i32,
116}
117
118/// One satellite phase-bias record.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct SsrPhaseBiasRecord {
121    /// Constellation-native satellite id.
122    pub satellite_id: u8,
123    /// Raw yaw angle.
124    pub yaw_angle: u16,
125    /// Raw yaw rate.
126    pub yaw_rate: i8,
127    /// Per-signal phase biases.
128    pub biases: Vec<SsrPhaseBiasSignal>,
129}
130
131/// A decoded RTCM SSR message body.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct SsrMessage {
134    /// RTCM message number.
135    pub message_number: u16,
136    /// Constellation derived from the message number.
137    pub system: GnssSystem,
138    /// SSR message group.
139    pub kind: SsrKind,
140    /// Common SSR header.
141    pub header: SsrHeader,
142    /// Orbit records, present for orbit and combined messages.
143    pub orbit: Vec<SsrOrbitRecord>,
144    /// Clock records, present for clock, combined, and high-rate messages.
145    pub clock: Vec<SsrClockRecord>,
146    /// Code-bias records.
147    pub code_bias: Vec<SsrCodeBiasRecord>,
148    /// Phase-bias records.
149    pub phase_bias: Vec<SsrPhaseBiasRecord>,
150    /// URA records as `(satellite_id, ura_index)`.
151    pub ura: Vec<(u8, u8)>,
152    /// Body bits after the parsed message fields.
153    pub padding_bits: Vec<bool>,
154}
155
156/// Map an RTCM message number to a supported SSR Phase A type.
157pub(crate) fn ssr_kind(message_number: u16) -> Option<(GnssSystem, SsrKind)> {
158    match message_number {
159        1057 => Some((GnssSystem::Gps, SsrKind::Orbit)),
160        1058 => Some((GnssSystem::Gps, SsrKind::Clock)),
161        1059 => Some((GnssSystem::Gps, SsrKind::CodeBias)),
162        1060 => Some((GnssSystem::Gps, SsrKind::CombinedOrbitClock)),
163        1061 => Some((GnssSystem::Gps, SsrKind::Ura)),
164        1062 => Some((GnssSystem::Gps, SsrKind::HighRateClock)),
165        1063 => Some((GnssSystem::Glonass, SsrKind::Orbit)),
166        1064 => Some((GnssSystem::Glonass, SsrKind::Clock)),
167        1065 => Some((GnssSystem::Glonass, SsrKind::CodeBias)),
168        1066 => Some((GnssSystem::Glonass, SsrKind::CombinedOrbitClock)),
169        1067 => Some((GnssSystem::Glonass, SsrKind::Ura)),
170        1068 => Some((GnssSystem::Glonass, SsrKind::HighRateClock)),
171        1265 => Some((GnssSystem::Gps, SsrKind::PhaseBias)),
172        1240 => Some((GnssSystem::Galileo, SsrKind::Orbit)),
173        1241 => Some((GnssSystem::Galileo, SsrKind::Clock)),
174        1242 => Some((GnssSystem::Galileo, SsrKind::CodeBias)),
175        1243 => Some((GnssSystem::Galileo, SsrKind::CombinedOrbitClock)),
176        1244 => Some((GnssSystem::Galileo, SsrKind::Ura)),
177        1245 => Some((GnssSystem::Galileo, SsrKind::HighRateClock)),
178        1267 => Some((GnssSystem::Galileo, SsrKind::PhaseBias)),
179        1258 => Some((GnssSystem::BeiDou, SsrKind::Orbit)),
180        1259 => Some((GnssSystem::BeiDou, SsrKind::Clock)),
181        1260 => Some((GnssSystem::BeiDou, SsrKind::CodeBias)),
182        1261 => Some((GnssSystem::BeiDou, SsrKind::CombinedOrbitClock)),
183        1262 => Some((GnssSystem::BeiDou, SsrKind::Ura)),
184        1263 => Some((GnssSystem::BeiDou, SsrKind::HighRateClock)),
185        1270 => Some((GnssSystem::BeiDou, SsrKind::PhaseBias)),
186        _ => None,
187    }
188}
189
190/// True when this module decodes `message_number`.
191pub(crate) fn is_supported_ssr(message_number: u16) -> bool {
192    ssr_kind(message_number).is_some()
193}
194
195impl SsrMessage {
196    /// Decode one RTCM SSR body, without the transport frame.
197    pub fn decode(body: &[u8]) -> Result<Self> {
198        Self::decode_inner(body).map_err(Into::into)
199    }
200
201    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
202        let mut r = BitReader::new(body);
203        let message_number = r.u(12)? as u16;
204        let (system, kind) = ssr_kind(message_number).ok_or_else(|| {
205            Error::Parse(format!(
206                "message {message_number} is not a supported RTCM SSR Phase A type"
207            ))
208        })?;
209        let header = read_header(&mut r, system, kind)?;
210        let count = usize::from(header.satellite_count);
211        let mut orbit = Vec::new();
212        let mut clock = Vec::new();
213        let mut code_bias = Vec::new();
214        let mut phase_bias = Vec::new();
215        let mut ura = Vec::new();
216
217        match kind {
218            SsrKind::Orbit => {
219                orbit.reserve(count);
220                for _ in 0..count {
221                    orbit.push(read_orbit_record(&mut r, system)?);
222                }
223            }
224            SsrKind::Clock => {
225                clock.reserve(count);
226                for _ in 0..count {
227                    clock.push(read_clock_record(&mut r, system)?);
228                }
229            }
230            SsrKind::CombinedOrbitClock => {
231                orbit.reserve(count);
232                clock.reserve(count);
233                for _ in 0..count {
234                    let rec = read_orbit_record(&mut r, system)?;
235                    let satellite_id = rec.satellite_id;
236                    orbit.push(rec);
237                    clock.push(SsrClockRecord {
238                        satellite_id,
239                        c0: r.i(22)? as i32,
240                        c1: r.i(21)? as i32,
241                        c2: r.i(27)? as i32,
242                    });
243                }
244            }
245            SsrKind::Ura => {
246                ura.reserve(count);
247                for _ in 0..count {
248                    let satellite_id = r.u(satellite_id_bits(system))? as u8;
249                    let index = r.u(6)? as u8;
250                    ura.push((satellite_id, index));
251                }
252            }
253            SsrKind::HighRateClock => {
254                clock.reserve(count);
255                for _ in 0..count {
256                    clock.push(SsrClockRecord {
257                        satellite_id: r.u(satellite_id_bits(system))? as u8,
258                        c0: r.i(22)? as i32,
259                        c1: 0,
260                        c2: 0,
261                    });
262                }
263            }
264            SsrKind::CodeBias => {
265                code_bias.reserve(count);
266                for _ in 0..count {
267                    code_bias.push(read_code_bias_record(&mut r, system)?);
268                }
269            }
270            SsrKind::PhaseBias => {
271                phase_bias.reserve(count);
272                for _ in 0..count {
273                    phase_bias.push(read_phase_bias_record(&mut r, system)?);
274                }
275            }
276            SsrKind::Vtec => {
277                return Err(Error::Parse(format!(
278                    "message {message_number} is not enabled in RTCM SSR Phase A"
279                ))
280                .into());
281            }
282        }
283
284        let mut padding_bits = Vec::with_capacity(r.remaining_bits());
285        while r.remaining_bits() > 0 {
286            padding_bits.push(r.flag()?);
287        }
288
289        Ok(Self {
290            message_number,
291            system,
292            kind,
293            header,
294            orbit,
295            clock,
296            code_bias,
297            phase_bias,
298            ura,
299            padding_bits,
300        })
301    }
302
303    /// Encode this message back into an RTCM body.
304    pub fn encode(&self) -> Vec<u8> {
305        let mut w = BitWriter::new();
306        w.push_u(u64::from(self.message_number), 12);
307        write_header(&mut w, self.system, &self.header, self.kind);
308
309        match self.kind {
310            SsrKind::Orbit => {
311                for rec in &self.orbit {
312                    write_orbit_record(&mut w, self.system, rec);
313                }
314            }
315            SsrKind::Clock => {
316                for rec in &self.clock {
317                    write_clock_record(&mut w, self.system, rec);
318                }
319            }
320            SsrKind::CombinedOrbitClock => {
321                for (orbit, clock) in self.orbit.iter().zip(&self.clock) {
322                    write_orbit_record(&mut w, self.system, orbit);
323                    w.push_i(i64::from(clock.c0), 22);
324                    w.push_i(i64::from(clock.c1), 21);
325                    w.push_i(i64::from(clock.c2), 27);
326                }
327            }
328            SsrKind::Ura => {
329                for &(satellite_id, index) in &self.ura {
330                    w.push_u(u64::from(satellite_id), satellite_id_bits(self.system));
331                    w.push_u(u64::from(index), 6);
332                }
333            }
334            SsrKind::HighRateClock => {
335                for rec in &self.clock {
336                    w.push_u(u64::from(rec.satellite_id), satellite_id_bits(self.system));
337                    w.push_i(i64::from(rec.c0), 22);
338                }
339            }
340            SsrKind::CodeBias => {
341                for rec in &self.code_bias {
342                    write_code_bias_record(&mut w, self.system, rec);
343                }
344            }
345            SsrKind::PhaseBias => {
346                for rec in &self.phase_bias {
347                    write_phase_bias_record(&mut w, self.system, rec);
348                }
349            }
350            SsrKind::Vtec => {}
351        }
352
353        for &bit in &self.padding_bits {
354            w.push_flag(bit);
355        }
356        w.into_bytes()
357    }
358}
359
360fn read_header(
361    r: &mut BitReader<'_>,
362    system: GnssSystem,
363    kind: SsrKind,
364) -> DecodeResult<SsrHeader> {
365    let epoch_time_s = r.u(epoch_time_bits(system))? as u32;
366    let update_interval = r.u(4)? as u8;
367    let multiple_message = r.flag()?;
368    let satellite_reference_datum = if matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock)
369    {
370        Some(r.flag()?)
371    } else {
372        None
373    };
374    let iod_ssr = r.u(4)? as u8;
375    let provider_id = r.u(16)? as u16;
376    let solution_id = r.u(4)? as u8;
377    let dispersive_bias_consistency = if kind == SsrKind::PhaseBias {
378        Some(r.flag()?)
379    } else {
380        None
381    };
382    let mw_consistency = if kind == SsrKind::PhaseBias {
383        Some(r.flag()?)
384    } else {
385        None
386    };
387    let satellite_count = r.u(6)? as u8;
388    Ok(SsrHeader {
389        epoch_time_s,
390        update_interval,
391        multiple_message,
392        iod_ssr,
393        provider_id,
394        solution_id,
395        satellite_reference_datum,
396        dispersive_bias_consistency,
397        mw_consistency,
398        satellite_count,
399    })
400}
401
402fn write_header(w: &mut BitWriter, system: GnssSystem, header: &SsrHeader, kind: SsrKind) {
403    w.push_u(u64::from(header.epoch_time_s), epoch_time_bits(system));
404    w.push_u(u64::from(header.update_interval), 4);
405    w.push_flag(header.multiple_message);
406    if matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock) {
407        w.push_flag(header.satellite_reference_datum.unwrap_or(false));
408    }
409    w.push_u(u64::from(header.iod_ssr), 4);
410    w.push_u(u64::from(header.provider_id), 16);
411    w.push_u(u64::from(header.solution_id), 4);
412    if kind == SsrKind::PhaseBias {
413        w.push_flag(header.dispersive_bias_consistency.unwrap_or(false));
414        w.push_flag(header.mw_consistency.unwrap_or(false));
415    }
416    w.push_u(u64::from(header.satellite_count), 6);
417}
418
419fn read_orbit_record(r: &mut BitReader<'_>, system: GnssSystem) -> DecodeResult<SsrOrbitRecord> {
420    Ok(SsrOrbitRecord {
421        satellite_id: r.u(satellite_id_bits(system))? as u8,
422        iode: r.u(iode_bits(system))? as u32,
423        delta_radial: r.i(22)? as i32,
424        delta_along: r.i(20)? as i32,
425        delta_cross: r.i(20)? as i32,
426        dot_delta_radial: r.i(21)? as i32,
427        dot_delta_along: r.i(19)? as i32,
428        dot_delta_cross: r.i(19)? as i32,
429    })
430}
431
432fn write_orbit_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrOrbitRecord) {
433    w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
434    w.push_u(u64::from(rec.iode), iode_bits(system));
435    w.push_i(i64::from(rec.delta_radial), 22);
436    w.push_i(i64::from(rec.delta_along), 20);
437    w.push_i(i64::from(rec.delta_cross), 20);
438    w.push_i(i64::from(rec.dot_delta_radial), 21);
439    w.push_i(i64::from(rec.dot_delta_along), 19);
440    w.push_i(i64::from(rec.dot_delta_cross), 19);
441}
442
443fn read_clock_record(r: &mut BitReader<'_>, system: GnssSystem) -> DecodeResult<SsrClockRecord> {
444    Ok(SsrClockRecord {
445        satellite_id: r.u(satellite_id_bits(system))? as u8,
446        c0: r.i(22)? as i32,
447        c1: r.i(21)? as i32,
448        c2: r.i(27)? as i32,
449    })
450}
451
452fn write_clock_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrClockRecord) {
453    w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
454    w.push_i(i64::from(rec.c0), 22);
455    w.push_i(i64::from(rec.c1), 21);
456    w.push_i(i64::from(rec.c2), 27);
457}
458
459fn read_code_bias_record(
460    r: &mut BitReader<'_>,
461    system: GnssSystem,
462) -> DecodeResult<SsrCodeBiasRecord> {
463    let satellite_id = r.u(satellite_id_bits(system))? as u8;
464    let count = r.u(5)? as usize;
465    let mut biases = Vec::with_capacity(count);
466    for _ in 0..count {
467        let signal_id = r.u(5)? as u8;
468        let bias = r.i(14)? as i16;
469        biases.push((signal_id, bias));
470    }
471    Ok(SsrCodeBiasRecord {
472        satellite_id,
473        biases,
474    })
475}
476
477fn write_code_bias_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrCodeBiasRecord) {
478    w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
479    w.push_u(rec.biases.len() as u64, 5);
480    for &(signal_id, bias) in &rec.biases {
481        w.push_u(u64::from(signal_id), 5);
482        w.push_i(i64::from(bias), 14);
483    }
484}
485
486fn read_phase_bias_record(
487    r: &mut BitReader<'_>,
488    system: GnssSystem,
489) -> DecodeResult<SsrPhaseBiasRecord> {
490    let satellite_id = r.u(satellite_id_bits(system))? as u8;
491    let count = r.u(5)? as usize;
492    let yaw_angle = r.u(9)? as u16;
493    let yaw_rate = r.i(8)? as i8;
494    let mut biases = Vec::with_capacity(count);
495    for _ in 0..count {
496        biases.push(SsrPhaseBiasSignal {
497            signal_id: r.u(5)? as u8,
498            integer_indicator: r.u(1)? as u8,
499            wide_lane_integer_indicator: r.u(2)? as u8,
500            discontinuity_counter: r.u(4)? as u8,
501            bias: r.i(20)? as i32,
502        });
503    }
504    Ok(SsrPhaseBiasRecord {
505        satellite_id,
506        yaw_angle,
507        yaw_rate,
508        biases,
509    })
510}
511
512fn write_phase_bias_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrPhaseBiasRecord) {
513    w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
514    w.push_u(rec.biases.len() as u64, 5);
515    w.push_u(u64::from(rec.yaw_angle), 9);
516    w.push_i(i64::from(rec.yaw_rate), 8);
517    for bias in &rec.biases {
518        w.push_u(u64::from(bias.signal_id), 5);
519        w.push_u(u64::from(bias.integer_indicator), 1);
520        w.push_u(u64::from(bias.wide_lane_integer_indicator), 2);
521        w.push_u(u64::from(bias.discontinuity_counter), 4);
522        w.push_i(i64::from(bias.bias), 20);
523    }
524}
525
526fn satellite_id_bits(system: GnssSystem) -> usize {
527    match system {
528        GnssSystem::Glonass => 5,
529        _ => 6,
530    }
531}
532
533fn iode_bits(system: GnssSystem) -> usize {
534    match system {
535        GnssSystem::Galileo => 10,
536        GnssSystem::BeiDou => 18,
537        _ => 8,
538    }
539}
540
541fn epoch_time_bits(system: GnssSystem) -> usize {
542    match system {
543        GnssSystem::Glonass => 17,
544        _ => 20,
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use crate::rtcm::{
552        decode_frame, encode_frame, Message, SsrStreamAssembler, UnsupportedMessage,
553    };
554
555    const REAL_SSRA02IGS0_1243_FRAME_HEX: &str = include_str!(concat!(
556        env!("CARGO_MANIFEST_DIR"),
557        "/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1243.hex"
558    ));
559    const REAL_SSRA02IGS0_1060_FRAME_HEX: &str = include_str!(concat!(
560        env!("CARGO_MANIFEST_DIR"),
561        "/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1060.hex"
562    ));
563
564    type RtklibCombinedRecord = (u8, u32, i32, i32, i32, i32, i32, i32, i32, i32, i32);
565
566    const RTKLIB_GALILEO_1243: &[RtklibCombinedRecord] = &[
567        (2, 65, 1010, 274, -80, -46, -28, 7, 1426, 0, 0),
568        (3, 64, -714, 92, -83, 101, -29, 10, 1467, 0, 0),
569        (4, 63, 2270, -273, -570, 62, -10, -10, -1957, 0, 0),
570        (5, 65, 598, -257, -32, 85, -31, 4, -334, 0, 0),
571        (6, 63, 3510, -770, -997, 44, 11, 3, -4312, 0, 0),
572        (7, 61, -523, -420, 424, 8, -30, -14, 2136, 0, 0),
573        (8, 65, -678, -462, 147, 26, -20, 6, 4289, 0, 0),
574        (9, 65, 4049, -350, -709, 53, -25, 32, -2437, 0, 0),
575        (10, 61, 2796, -279, 104, -5, -14, -22, -2916, 0, 0),
576        (11, 63, 5304, -453, 225, -5, -23, -16, 4, 0, 0),
577        (12, 65, -150, 129, -165, -5, -22, 5, 2686, 0, 0),
578        (13, 65, -1364, -594, 186, 34, -39, -7, 1752, 0, 0),
579        (15, 63, 1526, -1182, -594, 48, -15, 23, -129, 0, 0),
580        (16, 63, 1103, 153, -549, -18, -22, 15, -3064, 0, 0),
581        (19, 63, 1957, 1032, 379, -40, 35, 2, -3568, 0, 0),
582        (21, 65, -2238, 369, -208, 12, -38, 3, 3171, 0, 0),
583        (23, 65, 1153, 535, -516, -49, -22, 23, -2598, 0, 0),
584        (25, 65, 98, 733, -726, -25, -15, 0, 581, 0, 0),
585        (26, 64, -822, -146, 190, 23, -9, -20, 2149, 0, 0),
586        (27, 64, 343, -1258, -237, 32, -24, -9, 220, 0, 0),
587        (28, 65, 2459, -256, -275, -53, -16, 8, -1086, 0, 0),
588        (29, 65, 1202, 228, -407, 0, -12, -16, -77, 0, 0),
589        (30, 65, 1485, 157, 415, -53, -12, 0, 566, 0, 0),
590        (31, 65, -563, 616, 1, -30, 4, -10, 151, 0, 0),
591        (33, 65, 630, -60, 258, -87, -5, -6, 1554, 0, 0),
592        (34, 58, -471, -690, -100, 20, -26, -20, 1790, 0, 0),
593        (36, 49, 1519, 292, 670, -54, -15, 16, 694, 0, 0),
594    ];
595    const RTKLIB_GPS_1060: &[RtklibCombinedRecord] = &[
596        (30, 90, 807, 621, -349, 30, -10, -8, 166, 0, 0),
597        (31, 67, -227, -1752, 1423, -43, -7, 3, 4170, 0, 0),
598    ];
599
600    fn header(system: GnssSystem, kind: SsrKind, count: u8) -> SsrHeader {
601        SsrHeader {
602            epoch_time_s: if system == GnssSystem::Glonass {
603                61_632
604            } else {
605                345_600
606            },
607            update_interval: 2,
608            multiple_message: true,
609            iod_ssr: 9,
610            provider_id: 123,
611            solution_id: 4,
612            satellite_reference_datum: matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock)
613                .then_some(false),
614            dispersive_bias_consistency: (kind == SsrKind::PhaseBias).then_some(true),
615            mw_consistency: (kind == SsrKind::PhaseBias).then_some(false),
616            satellite_count: count,
617        }
618    }
619
620    fn orbit_record(system: GnssSystem) -> SsrOrbitRecord {
621        SsrOrbitRecord {
622            satellite_id: 3,
623            iode: match system {
624                GnssSystem::Galileo => 513,
625                GnssSystem::BeiDou => 123_456,
626                _ => 42,
627            },
628            delta_radial: -12_345,
629            delta_along: 23_456,
630            delta_cross: -34_567,
631            dot_delta_radial: 456,
632            dot_delta_along: -567,
633            dot_delta_cross: 678,
634        }
635    }
636
637    fn clock_record() -> SsrClockRecord {
638        SsrClockRecord {
639            satellite_id: 3,
640            c0: -78_901,
641            c1: 89_012,
642            c2: -9_012_345,
643        }
644    }
645
646    fn message(message_number: u16, system: GnssSystem, kind: SsrKind) -> SsrMessage {
647        let mut orbit = Vec::new();
648        let mut clock = Vec::new();
649        let mut ura = Vec::new();
650        let mut phase_bias = Vec::new();
651        match kind {
652            SsrKind::Orbit => orbit.push(orbit_record(system)),
653            SsrKind::Clock => clock.push(clock_record()),
654            SsrKind::CombinedOrbitClock => {
655                orbit.push(orbit_record(system));
656                clock.push(clock_record());
657            }
658            SsrKind::Ura => ura.push((3, 41)),
659            SsrKind::HighRateClock => clock.push(SsrClockRecord {
660                satellite_id: 3,
661                c0: -22_222,
662                c1: 0,
663                c2: 0,
664            }),
665            SsrKind::CodeBias => {
666                // RTCM SSR code bias: 5-bit signal id, int14 bias at 0.01 m.
667            }
668            SsrKind::PhaseBias => phase_bias.push(SsrPhaseBiasRecord {
669                satellite_id: 3,
670                yaw_angle: 127,
671                yaw_rate: -12,
672                biases: vec![
673                    SsrPhaseBiasSignal {
674                        signal_id: 1,
675                        integer_indicator: 1,
676                        wide_lane_integer_indicator: 2,
677                        discontinuity_counter: 3,
678                        bias: -123_456,
679                    },
680                    SsrPhaseBiasSignal {
681                        signal_id: 9,
682                        integer_indicator: 0,
683                        wide_lane_integer_indicator: 1,
684                        discontinuity_counter: 4,
685                        bias: 234_567,
686                    },
687                ],
688            }),
689            SsrKind::Vtec => {}
690        }
691        let code_bias = if kind == SsrKind::CodeBias {
692            vec![SsrCodeBiasRecord {
693                satellite_id: 3,
694                biases: vec![(1, -1234), (9, 2345)],
695            }]
696        } else {
697            Vec::new()
698        };
699        SsrMessage {
700            message_number,
701            system,
702            kind,
703            header: header(system, kind, 1),
704            orbit,
705            clock,
706            code_bias,
707            phase_bias,
708            ura,
709            padding_bits: Vec::new(),
710        }
711    }
712
713    fn hex_bytes(hex: &str) -> Vec<u8> {
714        let compact: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
715        assert_eq!(compact.len() % 2, 0);
716        compact
717            .as_bytes()
718            .chunks_exact(2)
719            .map(|chunk| {
720                let hi = (chunk[0] as char).to_digit(16).unwrap();
721                let lo = (chunk[1] as char).to_digit(16).unwrap();
722                ((hi << 4) | lo) as u8
723            })
724            .collect()
725    }
726
727    #[test]
728    fn phase_a_messages_decode_fields_and_roundtrip() {
729        for (number, system, kind) in [
730            (1057, GnssSystem::Gps, SsrKind::Orbit),
731            (1058, GnssSystem::Gps, SsrKind::Clock),
732            (1059, GnssSystem::Gps, SsrKind::CodeBias),
733            (1060, GnssSystem::Gps, SsrKind::CombinedOrbitClock),
734            (1061, GnssSystem::Gps, SsrKind::Ura),
735            (1062, GnssSystem::Gps, SsrKind::HighRateClock),
736            (1265, GnssSystem::Gps, SsrKind::PhaseBias),
737            (1063, GnssSystem::Glonass, SsrKind::Orbit),
738            (1064, GnssSystem::Glonass, SsrKind::Clock),
739            (1065, GnssSystem::Glonass, SsrKind::CodeBias),
740            (1066, GnssSystem::Glonass, SsrKind::CombinedOrbitClock),
741            (1067, GnssSystem::Glonass, SsrKind::Ura),
742            (1068, GnssSystem::Glonass, SsrKind::HighRateClock),
743            (1240, GnssSystem::Galileo, SsrKind::Orbit),
744            (1241, GnssSystem::Galileo, SsrKind::Clock),
745            (1242, GnssSystem::Galileo, SsrKind::CodeBias),
746            (1243, GnssSystem::Galileo, SsrKind::CombinedOrbitClock),
747            (1244, GnssSystem::Galileo, SsrKind::Ura),
748            (1245, GnssSystem::Galileo, SsrKind::HighRateClock),
749            (1267, GnssSystem::Galileo, SsrKind::PhaseBias),
750            (1258, GnssSystem::BeiDou, SsrKind::Orbit),
751            (1259, GnssSystem::BeiDou, SsrKind::Clock),
752            (1260, GnssSystem::BeiDou, SsrKind::CodeBias),
753            (1261, GnssSystem::BeiDou, SsrKind::CombinedOrbitClock),
754            (1262, GnssSystem::BeiDou, SsrKind::Ura),
755            (1263, GnssSystem::BeiDou, SsrKind::HighRateClock),
756            (1270, GnssSystem::BeiDou, SsrKind::PhaseBias),
757        ] {
758            let expected = message(number, system, kind);
759            let body = expected.encode();
760            let decoded = SsrMessage::decode(&body).unwrap();
761            assert_eq!(
762                decoded.message_number, expected.message_number,
763                "message {number}"
764            );
765            assert_eq!(decoded.system, expected.system, "message {number}");
766            assert_eq!(decoded.kind, expected.kind, "message {number}");
767            assert_eq!(decoded.header, expected.header, "message {number}");
768            assert_eq!(decoded.orbit, expected.orbit, "message {number}");
769            assert_eq!(decoded.clock, expected.clock, "message {number}");
770            assert_eq!(decoded.code_bias, expected.code_bias, "message {number}");
771            assert_eq!(decoded.phase_bias, expected.phase_bias, "message {number}");
772            assert_eq!(decoded.ura, expected.ura, "message {number}");
773            assert_eq!(decoded.encode(), body, "message {number} round trip");
774            assert!(matches!(Message::decode(&body).unwrap(), Message::Ssr(_)));
775        }
776    }
777
778    #[test]
779    fn real_ssr_apc_frames_match_rtklib_decode_oracle_and_roundtrip() {
780        let gal_frame = hex_bytes(REAL_SSRA02IGS0_1243_FRAME_HEX);
781        let gps_frame = hex_bytes(REAL_SSRA02IGS0_1060_FRAME_HEX);
782        let mut stream = gal_frame.clone();
783        stream.extend_from_slice(&gps_frame);
784        let mut assembler = SsrStreamAssembler::new();
785        let decoded = assembler.push(&stream);
786        assert_eq!(decoded.len(), 2);
787
788        let Message::Ssr(gal) = decoded[0].as_ref().unwrap() else {
789            panic!("expected Galileo SSR");
790        };
791        assert_eq!(gal.message_number, 1243);
792        assert_eq!(gal.system, GnssSystem::Galileo);
793        assert_eq!(gal.kind, SsrKind::CombinedOrbitClock);
794        assert_eq!(gal.header.epoch_time_s, 344_970);
795        assert_eq!(gal.header.update_interval, 3);
796        assert!(!gal.header.multiple_message);
797        assert_eq!(gal.header.iod_ssr, 1);
798        assert_eq!(gal.header.provider_id, 0);
799        assert_eq!(gal.header.solution_id, 2);
800        assert_eq!(gal.header.satellite_count, 27);
801        assert_rtklib_combined_records(gal, RTKLIB_GALILEO_1243);
802        assert_eq!(
803            encode_frame(&Message::Ssr(gal.clone()).encode()).unwrap(),
804            gal_frame
805        );
806
807        let Message::Ssr(gps) = decoded[1].as_ref().unwrap() else {
808            panic!("expected GPS SSR");
809        };
810        assert_eq!(gps.message_number, 1060);
811        assert_eq!(gps.system, GnssSystem::Gps);
812        assert_eq!(gps.kind, SsrKind::CombinedOrbitClock);
813        assert_eq!(gps.header.epoch_time_s, 344_970);
814        assert_eq!(gps.header.update_interval, 3);
815        assert!(!gps.header.multiple_message);
816        assert_eq!(gps.header.iod_ssr, 1);
817        assert_eq!(gps.header.provider_id, 0);
818        assert_eq!(gps.header.solution_id, 2);
819        assert_eq!(gps.header.satellite_count, 2);
820        assert_rtklib_combined_records(gps, RTKLIB_GPS_1060);
821        assert_eq!(
822            encode_frame(&Message::Ssr(gps.clone()).encode()).unwrap(),
823            gps_frame
824        );
825        assert_eq!(decode_frame(&gal_frame).unwrap().body, gal.encode());
826        assert_eq!(decode_frame(&gps_frame).unwrap().body, gps.encode());
827        assert_eq!(assembler.retained_len(), 0);
828    }
829
830    fn assert_rtklib_combined_records(message: &SsrMessage, expected: &[RtklibCombinedRecord]) {
831        assert_eq!(message.orbit.len(), expected.len());
832        assert_eq!(message.clock.len(), expected.len());
833        for ((orbit, clock), expected) in message.orbit.iter().zip(&message.clock).zip(expected) {
834            let (
835                satellite_id,
836                iode,
837                delta_radial,
838                delta_along,
839                delta_cross,
840                dot_delta_radial,
841                dot_delta_along,
842                dot_delta_cross,
843                c0,
844                c1,
845                c2,
846            ) = *expected;
847            assert_eq!(orbit.satellite_id, satellite_id);
848            assert_eq!(orbit.iode, iode, "sat {satellite_id}");
849            assert_eq!(orbit.delta_radial, delta_radial, "sat {satellite_id}");
850            assert_eq!(orbit.delta_along, delta_along, "sat {satellite_id}");
851            assert_eq!(orbit.delta_cross, delta_cross, "sat {satellite_id}");
852            assert_eq!(
853                orbit.dot_delta_radial, dot_delta_radial,
854                "sat {satellite_id}"
855            );
856            assert_eq!(orbit.dot_delta_along, dot_delta_along, "sat {satellite_id}");
857            assert_eq!(orbit.dot_delta_cross, dot_delta_cross, "sat {satellite_id}");
858            assert_eq!(clock.satellite_id, satellite_id);
859            assert_eq!(clock.c0, c0, "sat {satellite_id}");
860            assert_eq!(clock.c1, c1, "sat {satellite_id}");
861            assert_eq!(clock.c2, c2, "sat {satellite_id}");
862        }
863    }
864
865    #[test]
866    fn truncated_supported_ssr_is_parse_error() {
867        let body = message(1057, GnssSystem::Gps, SsrKind::Orbit).encode();
868        let err = SsrMessage::decode(&body[..body.len() - 1]).unwrap_err();
869        assert!(matches!(err, Error::Parse(_)));
870    }
871
872    #[test]
873    fn unsupported_ssr_bias_message_stays_unsupported() {
874        let mut w = BitWriter::new();
875        w.push_u(1266, 12);
876        let body = w.into_bytes();
877        let decoded = Message::decode(&body).unwrap();
878        assert_eq!(
879            decoded,
880            Message::Unsupported(UnsupportedMessage {
881                message_number: 1266,
882                body
883            })
884        );
885    }
886
887    #[test]
888    fn stream_assembler_keeps_trailing_partial_frame() {
889        let a = Message::Ssr(message(1057, GnssSystem::Gps, SsrKind::Orbit))
890            .to_frame()
891            .unwrap();
892        let b = Message::Ssr(message(1058, GnssSystem::Gps, SsrKind::Clock))
893            .to_frame()
894            .unwrap();
895        let mut chunk = Vec::new();
896        chunk.extend_from_slice(&[0, 1, 2]);
897        chunk.extend_from_slice(&a);
898        chunk.extend_from_slice(&b[..b.len() - 2]);
899
900        let mut assembler = SsrStreamAssembler::new();
901        let first = assembler.push(&chunk);
902        assert_eq!(first.len(), 1);
903        assert_eq!(first[0].as_ref().unwrap().message_number(), 1057);
904        assert_eq!(assembler.retained_len(), b.len() - 2);
905
906        let second = assembler.push(&b[b.len() - 2..]);
907        assert_eq!(second.len(), 1);
908        assert_eq!(second[0].as_ref().unwrap().message_number(), 1058);
909        assert_eq!(assembler.retained_len(), 0);
910    }
911
912    #[test]
913    fn framed_ssr_roundtrips_through_message_decode() {
914        let message = Message::Ssr(message(
915            1243,
916            GnssSystem::Galileo,
917            SsrKind::CombinedOrbitClock,
918        ));
919        let frame = message.to_frame().unwrap();
920        let mut assembler = SsrStreamAssembler::new();
921        let decoded = assembler.push(&frame);
922        assert_eq!(decoded.len(), 1);
923        assert_eq!(decoded[0].as_ref().unwrap().encode(), message.encode());
924        assert_eq!(encode_frame(&message.encode()).unwrap(), frame);
925    }
926}