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