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,
40 pub update_interval: u8,
42 pub multiple_message: bool,
44 pub iod_ssr: u8,
46 pub provider_id: u16,
48 pub solution_id: u8,
50 pub satellite_reference_datum: Option<bool>,
52 pub dispersive_bias_consistency: Option<bool>,
54 pub mw_consistency: Option<bool>,
56 pub satellite_count: u8,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct SsrOrbitRecord {
63 pub satellite_id: u8,
65 pub iode: u32,
67 pub delta_radial: i32,
69 pub delta_along: i32,
71 pub delta_cross: i32,
73 pub dot_delta_radial: i32,
75 pub dot_delta_along: i32,
77 pub dot_delta_cross: i32,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct SsrClockRecord {
84 pub satellite_id: u8,
86 pub c0: i32,
88 pub c1: i32,
90 pub c2: i32,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct SsrCodeBiasRecord {
97 pub satellite_id: u8,
99 pub biases: Vec<(u8, i16)>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct SsrPhaseBiasSignal {
106 pub signal_id: u8,
108 pub integer_indicator: u8,
110 pub wide_lane_integer_indicator: u8,
112 pub discontinuity_counter: u8,
114 pub bias: i32,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct SsrPhaseBiasRecord {
121 pub satellite_id: u8,
123 pub yaw_angle: u16,
125 pub yaw_rate: i8,
127 pub biases: Vec<SsrPhaseBiasSignal>,
129}
130
131#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct SsrMessage {
134 pub message_number: u16,
136 pub system: GnssSystem,
138 pub kind: SsrKind,
140 pub header: SsrHeader,
142 pub orbit: Vec<SsrOrbitRecord>,
144 pub clock: Vec<SsrClockRecord>,
146 pub code_bias: Vec<SsrCodeBiasRecord>,
148 pub phase_bias: Vec<SsrPhaseBiasRecord>,
150 pub ura: Vec<(u8, u8)>,
152 pub padding_bits: Vec<bool>,
154}
155
156pub(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
190pub(crate) fn is_supported_ssr(message_number: u16) -> bool {
192 ssr_kind(message_number).is_some()
193}
194
195impl SsrMessage {
196 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 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 }
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}