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