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 1060 => Some((GnssSystem::Gps, SsrKind::CombinedOrbitClock)),
161 1061 => Some((GnssSystem::Gps, SsrKind::Ura)),
162 1062 => Some((GnssSystem::Gps, SsrKind::HighRateClock)),
163 1240 => Some((GnssSystem::Galileo, SsrKind::Orbit)),
164 1241 => Some((GnssSystem::Galileo, SsrKind::Clock)),
165 1243 => Some((GnssSystem::Galileo, SsrKind::CombinedOrbitClock)),
166 1244 => Some((GnssSystem::Galileo, SsrKind::Ura)),
167 1245 => Some((GnssSystem::Galileo, SsrKind::HighRateClock)),
168 _ => None,
169 }
170}
171
172pub(crate) fn is_supported_ssr(message_number: u16) -> bool {
174 ssr_kind(message_number).is_some()
175}
176
177impl SsrMessage {
178 pub fn decode(body: &[u8]) -> Result<Self> {
180 Self::decode_inner(body).map_err(Into::into)
181 }
182
183 pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
184 let mut r = BitReader::new(body);
185 let message_number = r.u(12)? as u16;
186 let (system, kind) = ssr_kind(message_number).ok_or_else(|| {
187 Error::Parse(format!(
188 "message {message_number} is not a supported RTCM SSR Phase A type"
189 ))
190 })?;
191 let header = read_header(&mut r, kind)?;
192 let count = usize::from(header.satellite_count);
193 let mut orbit = Vec::new();
194 let mut clock = Vec::new();
195 let mut ura = Vec::new();
196
197 match kind {
198 SsrKind::Orbit => {
199 orbit.reserve(count);
200 for _ in 0..count {
201 orbit.push(read_orbit_record(&mut r, system)?);
202 }
203 }
204 SsrKind::Clock => {
205 clock.reserve(count);
206 for _ in 0..count {
207 clock.push(read_clock_record(&mut r)?);
208 }
209 }
210 SsrKind::CombinedOrbitClock => {
211 orbit.reserve(count);
212 clock.reserve(count);
213 for _ in 0..count {
214 let rec = read_orbit_record(&mut r, system)?;
215 let satellite_id = rec.satellite_id;
216 orbit.push(rec);
217 clock.push(SsrClockRecord {
218 satellite_id,
219 c0: r.i(22)? as i32,
220 c1: r.i(21)? as i32,
221 c2: r.i(27)? as i32,
222 });
223 }
224 }
225 SsrKind::Ura => {
226 ura.reserve(count);
227 for _ in 0..count {
228 let satellite_id = r.u(satellite_id_bits(system))? as u8;
229 let index = r.u(6)? as u8;
230 ura.push((satellite_id, index));
231 }
232 }
233 SsrKind::HighRateClock => {
234 clock.reserve(count);
235 for _ in 0..count {
236 clock.push(SsrClockRecord {
237 satellite_id: r.u(satellite_id_bits(system))? as u8,
238 c0: r.i(22)? as i32,
239 c1: 0,
240 c2: 0,
241 });
242 }
243 }
244 SsrKind::CodeBias | SsrKind::PhaseBias | SsrKind::Vtec => {
245 return Err(Error::Parse(format!(
246 "message {message_number} is not enabled in RTCM SSR Phase A"
247 ))
248 .into());
249 }
250 }
251
252 let mut padding_bits = Vec::with_capacity(r.remaining_bits());
253 while r.remaining_bits() > 0 {
254 padding_bits.push(r.flag()?);
255 }
256
257 Ok(Self {
258 message_number,
259 system,
260 kind,
261 header,
262 orbit,
263 clock,
264 code_bias: Vec::new(),
265 phase_bias: Vec::new(),
266 ura,
267 padding_bits,
268 })
269 }
270
271 pub fn encode(&self) -> Vec<u8> {
273 let mut w = BitWriter::new();
274 w.push_u(u64::from(self.message_number), 12);
275 write_header(&mut w, &self.header, self.kind);
276
277 match self.kind {
278 SsrKind::Orbit => {
279 for rec in &self.orbit {
280 write_orbit_record(&mut w, self.system, rec);
281 }
282 }
283 SsrKind::Clock => {
284 for rec in &self.clock {
285 write_clock_record(&mut w, self.system, rec);
286 }
287 }
288 SsrKind::CombinedOrbitClock => {
289 for (orbit, clock) in self.orbit.iter().zip(&self.clock) {
290 write_orbit_record(&mut w, self.system, orbit);
291 w.push_i(i64::from(clock.c0), 22);
292 w.push_i(i64::from(clock.c1), 21);
293 w.push_i(i64::from(clock.c2), 27);
294 }
295 }
296 SsrKind::Ura => {
297 for &(satellite_id, index) in &self.ura {
298 w.push_u(u64::from(satellite_id), satellite_id_bits(self.system));
299 w.push_u(u64::from(index), 6);
300 }
301 }
302 SsrKind::HighRateClock => {
303 for rec in &self.clock {
304 w.push_u(u64::from(rec.satellite_id), satellite_id_bits(self.system));
305 w.push_i(i64::from(rec.c0), 22);
306 }
307 }
308 SsrKind::CodeBias | SsrKind::PhaseBias | SsrKind::Vtec => {}
309 }
310
311 for &bit in &self.padding_bits {
312 w.push_flag(bit);
313 }
314 w.into_bytes()
315 }
316}
317
318fn read_header(r: &mut BitReader<'_>, kind: SsrKind) -> DecodeResult<SsrHeader> {
319 let epoch_time_s = r.u(20)? as u32;
320 let update_interval = r.u(4)? as u8;
321 let multiple_message = r.flag()?;
322 let satellite_reference_datum = if matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock)
323 {
324 Some(r.flag()?)
325 } else {
326 None
327 };
328 let iod_ssr = r.u(4)? as u8;
329 let provider_id = r.u(16)? as u16;
330 let solution_id = r.u(4)? as u8;
331 let satellite_count = r.u(6)? as u8;
332 Ok(SsrHeader {
333 epoch_time_s,
334 update_interval,
335 multiple_message,
336 iod_ssr,
337 provider_id,
338 solution_id,
339 satellite_reference_datum,
340 dispersive_bias_consistency: None,
341 mw_consistency: None,
342 satellite_count,
343 })
344}
345
346fn write_header(w: &mut BitWriter, header: &SsrHeader, kind: SsrKind) {
347 w.push_u(u64::from(header.epoch_time_s), 20);
348 w.push_u(u64::from(header.update_interval), 4);
349 w.push_flag(header.multiple_message);
350 if matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock) {
351 w.push_flag(header.satellite_reference_datum.unwrap_or(false));
352 }
353 w.push_u(u64::from(header.iod_ssr), 4);
354 w.push_u(u64::from(header.provider_id), 16);
355 w.push_u(u64::from(header.solution_id), 4);
356 w.push_u(u64::from(header.satellite_count), 6);
357}
358
359fn read_orbit_record(r: &mut BitReader<'_>, system: GnssSystem) -> DecodeResult<SsrOrbitRecord> {
360 Ok(SsrOrbitRecord {
361 satellite_id: r.u(satellite_id_bits(system))? as u8,
362 iode: r.u(iode_bits(system))? as u32,
363 delta_radial: r.i(22)? as i32,
364 delta_along: r.i(20)? as i32,
365 delta_cross: r.i(20)? as i32,
366 dot_delta_radial: r.i(21)? as i32,
367 dot_delta_along: r.i(19)? as i32,
368 dot_delta_cross: r.i(19)? as i32,
369 })
370}
371
372fn write_orbit_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrOrbitRecord) {
373 w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
374 w.push_u(u64::from(rec.iode), iode_bits(system));
375 w.push_i(i64::from(rec.delta_radial), 22);
376 w.push_i(i64::from(rec.delta_along), 20);
377 w.push_i(i64::from(rec.delta_cross), 20);
378 w.push_i(i64::from(rec.dot_delta_radial), 21);
379 w.push_i(i64::from(rec.dot_delta_along), 19);
380 w.push_i(i64::from(rec.dot_delta_cross), 19);
381}
382
383fn read_clock_record(r: &mut BitReader<'_>) -> DecodeResult<SsrClockRecord> {
384 Ok(SsrClockRecord {
385 satellite_id: r.u(6)? as u8,
386 c0: r.i(22)? as i32,
387 c1: r.i(21)? as i32,
388 c2: r.i(27)? as i32,
389 })
390}
391
392fn write_clock_record(w: &mut BitWriter, system: GnssSystem, rec: &SsrClockRecord) {
393 w.push_u(u64::from(rec.satellite_id), satellite_id_bits(system));
394 w.push_i(i64::from(rec.c0), 22);
395 w.push_i(i64::from(rec.c1), 21);
396 w.push_i(i64::from(rec.c2), 27);
397}
398
399fn satellite_id_bits(system: GnssSystem) -> usize {
400 match system {
401 GnssSystem::Gps | GnssSystem::Galileo => 6,
402 _ => 6,
403 }
404}
405
406fn iode_bits(system: GnssSystem) -> usize {
407 match system {
408 GnssSystem::Galileo => 10,
409 _ => 8,
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::rtcm::{
417 decode_frame, encode_frame, Message, SsrStreamAssembler, UnsupportedMessage,
418 };
419
420 const REAL_SSRA02IGS0_1243_FRAME_HEX: &str = include_str!(concat!(
421 env!("CARGO_MANIFEST_DIR"),
422 "/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1243.hex"
423 ));
424 const REAL_SSRA02IGS0_1060_FRAME_HEX: &str = include_str!(concat!(
425 env!("CARGO_MANIFEST_DIR"),
426 "/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1060.hex"
427 ));
428
429 type RtklibCombinedRecord = (u8, u32, i32, i32, i32, i32, i32, i32, i32, i32, i32);
430
431 const RTKLIB_GALILEO_1243: &[RtklibCombinedRecord] = &[
432 (2, 65, 1010, 274, -80, -46, -28, 7, 1426, 0, 0),
433 (3, 64, -714, 92, -83, 101, -29, 10, 1467, 0, 0),
434 (4, 63, 2270, -273, -570, 62, -10, -10, -1957, 0, 0),
435 (5, 65, 598, -257, -32, 85, -31, 4, -334, 0, 0),
436 (6, 63, 3510, -770, -997, 44, 11, 3, -4312, 0, 0),
437 (7, 61, -523, -420, 424, 8, -30, -14, 2136, 0, 0),
438 (8, 65, -678, -462, 147, 26, -20, 6, 4289, 0, 0),
439 (9, 65, 4049, -350, -709, 53, -25, 32, -2437, 0, 0),
440 (10, 61, 2796, -279, 104, -5, -14, -22, -2916, 0, 0),
441 (11, 63, 5304, -453, 225, -5, -23, -16, 4, 0, 0),
442 (12, 65, -150, 129, -165, -5, -22, 5, 2686, 0, 0),
443 (13, 65, -1364, -594, 186, 34, -39, -7, 1752, 0, 0),
444 (15, 63, 1526, -1182, -594, 48, -15, 23, -129, 0, 0),
445 (16, 63, 1103, 153, -549, -18, -22, 15, -3064, 0, 0),
446 (19, 63, 1957, 1032, 379, -40, 35, 2, -3568, 0, 0),
447 (21, 65, -2238, 369, -208, 12, -38, 3, 3171, 0, 0),
448 (23, 65, 1153, 535, -516, -49, -22, 23, -2598, 0, 0),
449 (25, 65, 98, 733, -726, -25, -15, 0, 581, 0, 0),
450 (26, 64, -822, -146, 190, 23, -9, -20, 2149, 0, 0),
451 (27, 64, 343, -1258, -237, 32, -24, -9, 220, 0, 0),
452 (28, 65, 2459, -256, -275, -53, -16, 8, -1086, 0, 0),
453 (29, 65, 1202, 228, -407, 0, -12, -16, -77, 0, 0),
454 (30, 65, 1485, 157, 415, -53, -12, 0, 566, 0, 0),
455 (31, 65, -563, 616, 1, -30, 4, -10, 151, 0, 0),
456 (33, 65, 630, -60, 258, -87, -5, -6, 1554, 0, 0),
457 (34, 58, -471, -690, -100, 20, -26, -20, 1790, 0, 0),
458 (36, 49, 1519, 292, 670, -54, -15, 16, 694, 0, 0),
459 ];
460 const RTKLIB_GPS_1060: &[RtklibCombinedRecord] = &[
461 (30, 90, 807, 621, -349, 30, -10, -8, 166, 0, 0),
462 (31, 67, -227, -1752, 1423, -43, -7, 3, 4170, 0, 0),
463 ];
464
465 fn header(kind: SsrKind, count: u8) -> SsrHeader {
466 SsrHeader {
467 epoch_time_s: 345_600,
468 update_interval: 2,
469 multiple_message: true,
470 iod_ssr: 9,
471 provider_id: 123,
472 solution_id: 4,
473 satellite_reference_datum: matches!(kind, SsrKind::Orbit | SsrKind::CombinedOrbitClock)
474 .then_some(false),
475 dispersive_bias_consistency: None,
476 mw_consistency: None,
477 satellite_count: count,
478 }
479 }
480
481 fn orbit_record(system: GnssSystem) -> SsrOrbitRecord {
482 SsrOrbitRecord {
483 satellite_id: 3,
484 iode: if system == GnssSystem::Galileo {
485 513
486 } else {
487 42
488 },
489 delta_radial: -12_345,
490 delta_along: 23_456,
491 delta_cross: -34_567,
492 dot_delta_radial: 456,
493 dot_delta_along: -567,
494 dot_delta_cross: 678,
495 }
496 }
497
498 fn clock_record() -> SsrClockRecord {
499 SsrClockRecord {
500 satellite_id: 3,
501 c0: -78_901,
502 c1: 89_012,
503 c2: -9_012_345,
504 }
505 }
506
507 fn message(message_number: u16, system: GnssSystem, kind: SsrKind) -> SsrMessage {
508 let mut orbit = Vec::new();
509 let mut clock = Vec::new();
510 let mut ura = Vec::new();
511 match kind {
512 SsrKind::Orbit => orbit.push(orbit_record(system)),
513 SsrKind::Clock => clock.push(clock_record()),
514 SsrKind::CombinedOrbitClock => {
515 orbit.push(orbit_record(system));
516 clock.push(clock_record());
517 }
518 SsrKind::Ura => ura.push((3, 41)),
519 SsrKind::HighRateClock => clock.push(SsrClockRecord {
520 satellite_id: 3,
521 c0: -22_222,
522 c1: 0,
523 c2: 0,
524 }),
525 SsrKind::CodeBias | SsrKind::PhaseBias | SsrKind::Vtec => {}
526 }
527 SsrMessage {
528 message_number,
529 system,
530 kind,
531 header: header(kind, 1),
532 orbit,
533 clock,
534 code_bias: Vec::new(),
535 phase_bias: Vec::new(),
536 ura,
537 padding_bits: Vec::new(),
538 }
539 }
540
541 fn hex_bytes(hex: &str) -> Vec<u8> {
542 let compact: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
543 assert_eq!(compact.len() % 2, 0);
544 compact
545 .as_bytes()
546 .chunks_exact(2)
547 .map(|chunk| {
548 let hi = (chunk[0] as char).to_digit(16).unwrap();
549 let lo = (chunk[1] as char).to_digit(16).unwrap();
550 ((hi << 4) | lo) as u8
551 })
552 .collect()
553 }
554
555 #[test]
556 fn phase_a_messages_decode_fields_and_roundtrip() {
557 for (number, system, kind) in [
558 (1057, GnssSystem::Gps, SsrKind::Orbit),
559 (1058, GnssSystem::Gps, SsrKind::Clock),
560 (1060, GnssSystem::Gps, SsrKind::CombinedOrbitClock),
561 (1061, GnssSystem::Gps, SsrKind::Ura),
562 (1062, GnssSystem::Gps, SsrKind::HighRateClock),
563 (1240, GnssSystem::Galileo, SsrKind::Orbit),
564 (1241, GnssSystem::Galileo, SsrKind::Clock),
565 (1243, GnssSystem::Galileo, SsrKind::CombinedOrbitClock),
566 (1244, GnssSystem::Galileo, SsrKind::Ura),
567 (1245, GnssSystem::Galileo, SsrKind::HighRateClock),
568 ] {
569 let expected = message(number, system, kind);
570 let body = expected.encode();
571 let decoded = SsrMessage::decode(&body).unwrap();
572 assert_eq!(
573 decoded.message_number, expected.message_number,
574 "message {number}"
575 );
576 assert_eq!(decoded.system, expected.system, "message {number}");
577 assert_eq!(decoded.kind, expected.kind, "message {number}");
578 assert_eq!(decoded.header, expected.header, "message {number}");
579 assert_eq!(decoded.orbit, expected.orbit, "message {number}");
580 assert_eq!(decoded.clock, expected.clock, "message {number}");
581 assert_eq!(decoded.ura, expected.ura, "message {number}");
582 assert_eq!(decoded.encode(), body, "message {number} round trip");
583 assert!(matches!(Message::decode(&body).unwrap(), Message::Ssr(_)));
584 }
585 }
586
587 #[test]
588 fn real_ssr_apc_frames_match_rtklib_decode_oracle_and_roundtrip() {
589 let gal_frame = hex_bytes(REAL_SSRA02IGS0_1243_FRAME_HEX);
590 let gps_frame = hex_bytes(REAL_SSRA02IGS0_1060_FRAME_HEX);
591 let mut stream = gal_frame.clone();
592 stream.extend_from_slice(&gps_frame);
593 let mut assembler = SsrStreamAssembler::new();
594 let decoded = assembler.push(&stream);
595 assert_eq!(decoded.len(), 2);
596
597 let Message::Ssr(gal) = decoded[0].as_ref().unwrap() else {
598 panic!("expected Galileo SSR");
599 };
600 assert_eq!(gal.message_number, 1243);
601 assert_eq!(gal.system, GnssSystem::Galileo);
602 assert_eq!(gal.kind, SsrKind::CombinedOrbitClock);
603 assert_eq!(gal.header.epoch_time_s, 344_970);
604 assert_eq!(gal.header.update_interval, 3);
605 assert!(!gal.header.multiple_message);
606 assert_eq!(gal.header.iod_ssr, 1);
607 assert_eq!(gal.header.provider_id, 0);
608 assert_eq!(gal.header.solution_id, 2);
609 assert_eq!(gal.header.satellite_count, 27);
610 assert_rtklib_combined_records(gal, RTKLIB_GALILEO_1243);
611 assert_eq!(
612 encode_frame(&Message::Ssr(gal.clone()).encode()).unwrap(),
613 gal_frame
614 );
615
616 let Message::Ssr(gps) = decoded[1].as_ref().unwrap() else {
617 panic!("expected GPS SSR");
618 };
619 assert_eq!(gps.message_number, 1060);
620 assert_eq!(gps.system, GnssSystem::Gps);
621 assert_eq!(gps.kind, SsrKind::CombinedOrbitClock);
622 assert_eq!(gps.header.epoch_time_s, 344_970);
623 assert_eq!(gps.header.update_interval, 3);
624 assert!(!gps.header.multiple_message);
625 assert_eq!(gps.header.iod_ssr, 1);
626 assert_eq!(gps.header.provider_id, 0);
627 assert_eq!(gps.header.solution_id, 2);
628 assert_eq!(gps.header.satellite_count, 2);
629 assert_rtklib_combined_records(gps, RTKLIB_GPS_1060);
630 assert_eq!(
631 encode_frame(&Message::Ssr(gps.clone()).encode()).unwrap(),
632 gps_frame
633 );
634 assert_eq!(decode_frame(&gal_frame).unwrap().body, gal.encode());
635 assert_eq!(decode_frame(&gps_frame).unwrap().body, gps.encode());
636 assert_eq!(assembler.retained_len(), 0);
637 }
638
639 fn assert_rtklib_combined_records(message: &SsrMessage, expected: &[RtklibCombinedRecord]) {
640 assert_eq!(message.orbit.len(), expected.len());
641 assert_eq!(message.clock.len(), expected.len());
642 for ((orbit, clock), expected) in message.orbit.iter().zip(&message.clock).zip(expected) {
643 let (
644 satellite_id,
645 iode,
646 delta_radial,
647 delta_along,
648 delta_cross,
649 dot_delta_radial,
650 dot_delta_along,
651 dot_delta_cross,
652 c0,
653 c1,
654 c2,
655 ) = *expected;
656 assert_eq!(orbit.satellite_id, satellite_id);
657 assert_eq!(orbit.iode, iode, "sat {satellite_id}");
658 assert_eq!(orbit.delta_radial, delta_radial, "sat {satellite_id}");
659 assert_eq!(orbit.delta_along, delta_along, "sat {satellite_id}");
660 assert_eq!(orbit.delta_cross, delta_cross, "sat {satellite_id}");
661 assert_eq!(
662 orbit.dot_delta_radial, dot_delta_radial,
663 "sat {satellite_id}"
664 );
665 assert_eq!(orbit.dot_delta_along, dot_delta_along, "sat {satellite_id}");
666 assert_eq!(orbit.dot_delta_cross, dot_delta_cross, "sat {satellite_id}");
667 assert_eq!(clock.satellite_id, satellite_id);
668 assert_eq!(clock.c0, c0, "sat {satellite_id}");
669 assert_eq!(clock.c1, c1, "sat {satellite_id}");
670 assert_eq!(clock.c2, c2, "sat {satellite_id}");
671 }
672 }
673
674 #[test]
675 fn truncated_supported_ssr_is_parse_error() {
676 let body = message(1057, GnssSystem::Gps, SsrKind::Orbit).encode();
677 let err = SsrMessage::decode(&body[..body.len() - 1]).unwrap_err();
678 assert!(matches!(err, Error::Parse(_)));
679 }
680
681 #[test]
682 fn unsupported_ssr_bias_message_stays_unsupported() {
683 let mut w = BitWriter::new();
684 w.push_u(1059, 12);
685 let body = w.into_bytes();
686 let decoded = Message::decode(&body).unwrap();
687 assert_eq!(
688 decoded,
689 Message::Unsupported(UnsupportedMessage {
690 message_number: 1059,
691 body
692 })
693 );
694 }
695
696 #[test]
697 fn stream_assembler_keeps_trailing_partial_frame() {
698 let a = Message::Ssr(message(1057, GnssSystem::Gps, SsrKind::Orbit))
699 .to_frame()
700 .unwrap();
701 let b = Message::Ssr(message(1058, GnssSystem::Gps, SsrKind::Clock))
702 .to_frame()
703 .unwrap();
704 let mut chunk = Vec::new();
705 chunk.extend_from_slice(&[0, 1, 2]);
706 chunk.extend_from_slice(&a);
707 chunk.extend_from_slice(&b[..b.len() - 2]);
708
709 let mut assembler = SsrStreamAssembler::new();
710 let first = assembler.push(&chunk);
711 assert_eq!(first.len(), 1);
712 assert_eq!(first[0].as_ref().unwrap().message_number(), 1057);
713 assert_eq!(assembler.retained_len(), b.len() - 2);
714
715 let second = assembler.push(&b[b.len() - 2..]);
716 assert_eq!(second.len(), 1);
717 assert_eq!(second[0].as_ref().unwrap().message_number(), 1058);
718 assert_eq!(assembler.retained_len(), 0);
719 }
720
721 #[test]
722 fn framed_ssr_roundtrips_through_message_decode() {
723 let message = Message::Ssr(message(
724 1243,
725 GnssSystem::Galileo,
726 SsrKind::CombinedOrbitClock,
727 ));
728 let frame = message.to_frame().unwrap();
729 let mut assembler = SsrStreamAssembler::new();
730 let decoded = assembler.push(&frame);
731 assert_eq!(decoded.len(), 1);
732 assert_eq!(decoded[0].as_ref().unwrap().encode(), message.encode());
733 assert_eq!(encode_frame(&message.encode()).unwrap(), frame);
734 }
735}