1use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5use crate::types::{PvtError, PvtMode};
6
7use super::position::BaseVectorGeodBlock;
8use super::block_ids;
9use super::dnu::{f32_or_none, f64_or_none, U16_DNU};
10use super::SbfBlockParse;
11
12#[cfg(test)]
13use super::dnu::{F32_DNU, F64_DNU};
14
15#[derive(Debug, Clone)]
27pub struct ReceiverTimeBlock {
28 tow_ms: u32,
29 wnc: u16,
30 pub utc_year: i16,
32 pub utc_month: u8,
34 pub utc_day: u8,
36 pub utc_hour: u8,
38 pub utc_minute: u8,
40 pub utc_second: u8,
42 pub delta_ls: i8,
44 pub sync_level: u8,
46}
47
48impl ReceiverTimeBlock {
49 pub fn tow_seconds(&self) -> f64 {
50 self.tow_ms as f64 * 0.001
51 }
52 pub fn tow_ms(&self) -> u32 {
53 self.tow_ms
54 }
55 pub fn wnc(&self) -> u16 {
56 self.wnc
57 }
58
59 pub fn utc_string(&self) -> String {
61 format!(
62 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
63 self.utc_year,
64 self.utc_month,
65 self.utc_day,
66 self.utc_hour,
67 self.utc_minute,
68 self.utc_second
69 )
70 }
71
72 pub fn is_valid(&self) -> bool {
74 self.utc_year >= 2000
75 && self.utc_month >= 1
76 && self.utc_month <= 12
77 && self.utc_day >= 1
78 && self.utc_day <= 31
79 && self.utc_hour <= 23
80 && self.utc_minute <= 59
81 && self.utc_second <= 60
82 }
83
84 pub fn sync_level_desc(&self) -> &'static str {
86 match self.sync_level {
87 0 => "Not synchronized",
88 1 => "Approximate time",
89 2 => "Coarse time",
90 3 => "Fine time (PVT)",
91 4 => "Fine time (PPS)",
92 _ => "Unknown",
93 }
94 }
95
96 pub fn is_synchronized(&self) -> bool {
98 self.sync_level >= 3
99 }
100}
101
102impl SbfBlockParse for ReceiverTimeBlock {
103 const BLOCK_ID: u16 = block_ids::RECEIVER_TIME;
104
105 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
106 if data.len() < 20 {
107 return Err(SbfError::ParseError("ReceiverTime too short".into()));
108 }
109
110 let utc_year_raw = data[12] as i8;
121 let utc_year = if utc_year_raw == i8::MIN {
122 i8::MIN as i16
123 } else {
124 2000 + utc_year_raw as i16
125 };
126 let utc_month = data[13];
127 let utc_day = data[14];
128 let utc_hour = data[15];
129 let utc_minute = data[16];
130 let utc_second = data[17];
131 let delta_ls = data[18] as i8;
132 let sync_level = data[19];
133
134 Ok(Self {
135 tow_ms: header.tow_ms,
136 wnc: header.wnc,
137 utc_year,
138 utc_month,
139 utc_day,
140 utc_hour,
141 utc_minute,
142 utc_second,
143 delta_ls,
144 sync_level,
145 })
146 }
147}
148
149#[derive(Debug, Clone)]
157pub struct PpsOffsetBlock {
158 tow_ms: u32,
159 wnc: u16,
160 pub sync_age: u8,
162 pub timescale: u8,
164 offset_ns: f32,
166}
167
168impl PpsOffsetBlock {
169 pub fn tow_seconds(&self) -> f64 {
170 self.tow_ms as f64 * 0.001
171 }
172 pub fn tow_ms(&self) -> u32 {
173 self.tow_ms
174 }
175 pub fn wnc(&self) -> u16 {
176 self.wnc
177 }
178
179 pub fn offset_ns(&self) -> Option<f32> {
181 f32_or_none(self.offset_ns)
182 }
183
184 pub fn offset_seconds(&self) -> Option<f64> {
186 self.offset_ns().map(|value| value as f64 * 1e-9)
187 }
188}
189
190impl SbfBlockParse for PpsOffsetBlock {
191 const BLOCK_ID: u16 = block_ids::PPS_OFFSET;
192
193 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
194 if data.len() < 18 {
195 return Err(SbfError::ParseError("xPPSOffset too short".into()));
196 }
197
198 let sync_age = data[12];
199 let timescale = data[13];
200 let offset_ns = f32::from_le_bytes(data[14..18].try_into().unwrap());
201
202 Ok(Self {
203 tow_ms: header.tow_ms,
204 wnc: header.wnc,
205 sync_age,
206 timescale,
207 offset_ns,
208 })
209 }
210}
211
212#[derive(Debug, Clone)]
220pub struct ExtEventBlock {
221 tow_ms: u32,
222 wnc: u16,
223 pub source: u8,
225 pub polarity: u8,
227 offset_s: f32,
229 rx_clk_bias_s: f64,
231 pub pvt_age: u16,
233}
234
235impl ExtEventBlock {
236 pub fn tow_seconds(&self) -> f64 {
237 self.tow_ms as f64 * 0.001
238 }
239 pub fn tow_ms(&self) -> u32 {
240 self.tow_ms
241 }
242 pub fn wnc(&self) -> u16 {
243 self.wnc
244 }
245
246 pub fn offset_seconds(&self) -> Option<f64> {
248 f32_or_none(self.offset_s).map(|value| value as f64)
249 }
250
251 pub fn offset_ns(&self) -> Option<f32> {
253 self.offset_seconds().map(|value| (value * 1e9) as f32)
254 }
255
256 pub fn rx_clk_bias_seconds(&self) -> Option<f64> {
258 f64_or_none(self.rx_clk_bias_s)
259 }
260
261 pub fn rx_clk_bias_ms(&self) -> Option<f64> {
265 self.rx_clk_bias_seconds().map(|value| value * 1e3)
266 }
267}
268
269impl SbfBlockParse for ExtEventBlock {
270 const BLOCK_ID: u16 = block_ids::EXT_EVENT;
271
272 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
273 if data.len() < 28 {
274 return Err(SbfError::ParseError("ExtEvent too short".into()));
275 }
276
277 let source = data[12];
278 let polarity = data[13];
279 let offset_s = f32::from_le_bytes(data[14..18].try_into().unwrap());
280 let rx_clk_bias_s = f64::from_le_bytes(data[18..26].try_into().unwrap());
281 let pvt_age = u16::from_le_bytes([data[26], data[27]]);
282
283 Ok(Self {
284 tow_ms: header.tow_ms,
285 wnc: header.wnc,
286 source,
287 polarity,
288 offset_s,
289 rx_clk_bias_s,
290 pvt_age,
291 })
292 }
293}
294
295#[derive(Debug, Clone)]
303pub struct ExtEventPvtCartesianBlock {
304 tow_ms: u32,
305 wnc: u16,
306 mode: u8,
307 error: u8,
308 x_m: f64,
309 y_m: f64,
310 z_m: f64,
311 undulation_m: f32,
312 vx_mps: f32,
313 vy_mps: f32,
314 vz_mps: f32,
315 cog_deg: f32,
316 rx_clk_bias_ms: f64,
317 rx_clk_drift_ppm: f32,
318 pub time_system: u8,
319 pub datum: u8,
320 nr_sv: u8,
321 pub wa_corr_info: u8,
322 pub reference_id: u16,
323 mean_corr_age_raw: u16,
324 pub signal_info: u32,
325 pub alert_flag: u8,
326 pub nr_bases: u8,
327}
328
329impl ExtEventPvtCartesianBlock {
330 pub fn tow_seconds(&self) -> f64 {
331 self.tow_ms as f64 * 0.001
332 }
333 pub fn tow_ms(&self) -> u32 {
334 self.tow_ms
335 }
336 pub fn wnc(&self) -> u16 {
337 self.wnc
338 }
339
340 pub fn mode(&self) -> PvtMode {
341 PvtMode::from_mode_byte(self.mode)
342 }
343 pub fn mode_raw(&self) -> u8 {
344 self.mode
345 }
346 pub fn error(&self) -> PvtError {
347 PvtError::from_error_byte(self.error)
348 }
349 pub fn error_raw(&self) -> u8 {
350 self.error
351 }
352 pub fn has_fix(&self) -> bool {
353 self.mode().has_fix() && self.error().is_ok()
354 }
355
356 pub fn x_m(&self) -> Option<f64> {
358 f64_or_none(self.x_m)
359 }
360 pub fn y_m(&self) -> Option<f64> {
361 f64_or_none(self.y_m)
362 }
363 pub fn z_m(&self) -> Option<f64> {
364 f64_or_none(self.z_m)
365 }
366 pub fn undulation_m(&self) -> Option<f32> {
367 f32_or_none(self.undulation_m)
368 }
369
370 pub fn vx_mps(&self) -> Option<f32> {
372 f32_or_none(self.vx_mps)
373 }
374 pub fn vy_mps(&self) -> Option<f32> {
375 f32_or_none(self.vy_mps)
376 }
377 pub fn vz_mps(&self) -> Option<f32> {
378 f32_or_none(self.vz_mps)
379 }
380 pub fn course_over_ground_deg(&self) -> Option<f32> {
381 f32_or_none(self.cog_deg)
382 }
383
384 pub fn clock_bias_ms(&self) -> Option<f64> {
386 f64_or_none(self.rx_clk_bias_ms)
387 }
388 pub fn clock_drift_ppm(&self) -> Option<f32> {
389 f32_or_none(self.rx_clk_drift_ppm)
390 }
391
392 pub fn mean_corr_age_seconds(&self) -> Option<f32> {
394 if self.mean_corr_age_raw == U16_DNU {
395 None
396 } else {
397 Some(self.mean_corr_age_raw as f32 * 0.01)
398 }
399 }
400 pub fn mean_corr_age_raw(&self) -> u16 {
401 self.mean_corr_age_raw
402 }
403
404 pub fn num_satellites(&self) -> u8 {
405 self.nr_sv
406 }
407}
408
409impl SbfBlockParse for ExtEventPvtCartesianBlock {
410 const BLOCK_ID: u16 = block_ids::EXT_EVENT_PVT_CARTESIAN;
411
412 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
413 if data.len() < 84 {
414 return Err(SbfError::ParseError(
415 "ExtEventPVTCartesian too short".into(),
416 ));
417 }
418
419 let mode = data[12];
420 let error = data[13];
421 let x_m = f64::from_le_bytes(data[14..22].try_into().unwrap());
422 let y_m = f64::from_le_bytes(data[22..30].try_into().unwrap());
423 let z_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
424 let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
425 let vx_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
426 let vy_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
427 let vz_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
428 let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
429 let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
430 let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
431 let time_system = data[70];
432 let datum = data[71];
433 let nr_sv = data[72];
434 let wa_corr_info = data[73];
435 let reference_id = u16::from_le_bytes([data[74], data[75]]);
436 let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
437 let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
438 let alert_flag = data[82];
439 let nr_bases = data[83];
440
441 Ok(Self {
442 tow_ms: header.tow_ms,
443 wnc: header.wnc,
444 mode,
445 error,
446 x_m,
447 y_m,
448 z_m,
449 undulation_m,
450 vx_mps,
451 vy_mps,
452 vz_mps,
453 cog_deg,
454 rx_clk_bias_ms,
455 rx_clk_drift_ppm,
456 time_system,
457 datum,
458 nr_sv,
459 wa_corr_info,
460 reference_id,
461 mean_corr_age_raw,
462 signal_info,
463 alert_flag,
464 nr_bases,
465 })
466 }
467}
468
469#[derive(Debug, Clone)]
473pub struct ExtEventPvtGeodeticBlock {
474 tow_ms: u32,
475 wnc: u16,
476 mode: u8,
477 error: u8,
478 latitude_rad: f64,
479 longitude_rad: f64,
480 height_m: f64,
481 undulation_m: f32,
482 vn_mps: f32,
483 ve_mps: f32,
484 vu_mps: f32,
485 cog_deg: f32,
486 rx_clk_bias_ms: f64,
487 rx_clk_drift_ppm: f32,
488 pub time_system: u8,
489 pub datum: u8,
490 nr_sv: u8,
491 pub wa_corr_info: u8,
492 pub reference_id: u16,
493 mean_corr_age_raw: u16,
494 pub signal_info: u32,
495 pub alert_flag: u8,
496 pub nr_bases: u8,
497}
498
499impl ExtEventPvtGeodeticBlock {
500 pub fn tow_seconds(&self) -> f64 {
501 self.tow_ms as f64 * 0.001
502 }
503 pub fn tow_ms(&self) -> u32 {
504 self.tow_ms
505 }
506 pub fn wnc(&self) -> u16 {
507 self.wnc
508 }
509
510 pub fn mode(&self) -> PvtMode {
511 PvtMode::from_mode_byte(self.mode)
512 }
513 pub fn mode_raw(&self) -> u8 {
514 self.mode
515 }
516 pub fn error(&self) -> PvtError {
517 PvtError::from_error_byte(self.error)
518 }
519 pub fn error_raw(&self) -> u8 {
520 self.error
521 }
522 pub fn has_fix(&self) -> bool {
523 self.mode().has_fix() && self.error().is_ok()
524 }
525
526 pub fn latitude_deg(&self) -> Option<f64> {
528 f64_or_none(self.latitude_rad).map(|value| value.to_degrees())
529 }
530 pub fn longitude_deg(&self) -> Option<f64> {
531 f64_or_none(self.longitude_rad).map(|value| value.to_degrees())
532 }
533 pub fn latitude_rad(&self) -> f64 {
534 self.latitude_rad
535 }
536 pub fn longitude_rad(&self) -> f64 {
537 self.longitude_rad
538 }
539 pub fn height_m(&self) -> Option<f64> {
540 f64_or_none(self.height_m)
541 }
542 pub fn undulation_m(&self) -> Option<f32> {
543 f32_or_none(self.undulation_m)
544 }
545
546 pub fn velocity_north_mps(&self) -> Option<f32> {
548 f32_or_none(self.vn_mps)
549 }
550 pub fn velocity_east_mps(&self) -> Option<f32> {
551 f32_or_none(self.ve_mps)
552 }
553 pub fn velocity_up_mps(&self) -> Option<f32> {
554 f32_or_none(self.vu_mps)
555 }
556 pub fn course_over_ground_deg(&self) -> Option<f32> {
557 f32_or_none(self.cog_deg)
558 }
559
560 pub fn clock_bias_ms(&self) -> Option<f64> {
562 f64_or_none(self.rx_clk_bias_ms)
563 }
564 pub fn clock_drift_ppm(&self) -> Option<f32> {
565 f32_or_none(self.rx_clk_drift_ppm)
566 }
567
568 pub fn mean_corr_age_seconds(&self) -> Option<f32> {
570 if self.mean_corr_age_raw == U16_DNU {
571 None
572 } else {
573 Some(self.mean_corr_age_raw as f32 * 0.01)
574 }
575 }
576 pub fn mean_corr_age_raw(&self) -> u16 {
577 self.mean_corr_age_raw
578 }
579
580 pub fn num_satellites(&self) -> u8 {
581 self.nr_sv
582 }
583}
584
585impl SbfBlockParse for ExtEventPvtGeodeticBlock {
586 const BLOCK_ID: u16 = block_ids::EXT_EVENT_PVT_GEODETIC;
587
588 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
589 if data.len() < 84 {
590 return Err(SbfError::ParseError("ExtEventPVTGeodetic too short".into()));
591 }
592
593 let mode = data[12];
594 let error = data[13];
595 let latitude_rad = f64::from_le_bytes(data[14..22].try_into().unwrap());
596 let longitude_rad = f64::from_le_bytes(data[22..30].try_into().unwrap());
597 let height_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
598 let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
599 let vn_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
600 let ve_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
601 let vu_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
602 let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
603 let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
604 let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
605 let time_system = data[70];
606 let datum = data[71];
607 let nr_sv = data[72];
608 let wa_corr_info = data[73];
609 let reference_id = u16::from_le_bytes([data[74], data[75]]);
610 let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
611 let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
612 let alert_flag = data[82];
613 let nr_bases = data[83];
614
615 Ok(Self {
616 tow_ms: header.tow_ms,
617 wnc: header.wnc,
618 mode,
619 error,
620 latitude_rad,
621 longitude_rad,
622 height_m,
623 undulation_m,
624 vn_mps,
625 ve_mps,
626 vu_mps,
627 cog_deg,
628 rx_clk_bias_ms,
629 rx_clk_drift_ppm,
630 time_system,
631 datum,
632 nr_sv,
633 wa_corr_info,
634 reference_id,
635 mean_corr_age_raw,
636 signal_info,
637 alert_flag,
638 nr_bases,
639 })
640 }
641}
642
643#[derive(Debug, Clone)]
649pub struct ExtEventBaseVectGeodBlock(pub BaseVectorGeodBlock);
650
651impl std::ops::Deref for ExtEventBaseVectGeodBlock {
652 type Target = BaseVectorGeodBlock;
653
654 fn deref(&self) -> &Self::Target {
655 &self.0
656 }
657}
658
659impl SbfBlockParse for ExtEventBaseVectGeodBlock {
660 const BLOCK_ID: u16 = block_ids::EXT_EVENT_BASE_VECT_GEOD;
661
662 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
663 Ok(Self(BaseVectorGeodBlock::parse(header, data)?))
664 }
665}
666
667#[derive(Debug, Clone)]
669pub struct ExtEventAttEulerBlock {
670 tow_ms: u32,
671 wnc: u16,
672 nr_sv: u8,
673 error: u8,
674 mode: u16,
675 heading_deg: f32,
676 pitch_deg: f32,
677 roll_deg: f32,
678 pitch_rate_dps: f32,
679 roll_rate_dps: f32,
680 heading_rate_dps: f32,
681}
682
683impl ExtEventAttEulerBlock {
684 pub fn tow_seconds(&self) -> f64 {
685 self.tow_ms as f64 * 0.001
686 }
687 pub fn tow_ms(&self) -> u32 {
688 self.tow_ms
689 }
690 pub fn wnc(&self) -> u16 {
691 self.wnc
692 }
693 pub fn num_satellites(&self) -> u8 {
694 self.nr_sv
695 }
696 pub fn error_raw(&self) -> u8 {
697 self.error
698 }
699 pub fn mode_raw(&self) -> u16 {
700 self.mode
701 }
702 pub fn heading_deg(&self) -> Option<f32> {
703 f32_or_none(self.heading_deg)
704 }
705 pub fn pitch_deg(&self) -> Option<f32> {
706 f32_or_none(self.pitch_deg)
707 }
708 pub fn roll_deg(&self) -> Option<f32> {
709 f32_or_none(self.roll_deg)
710 }
711 pub fn pitch_rate_dps(&self) -> Option<f32> {
712 f32_or_none(self.pitch_rate_dps)
713 }
714 pub fn roll_rate_dps(&self) -> Option<f32> {
715 f32_or_none(self.roll_rate_dps)
716 }
717 pub fn heading_rate_dps(&self) -> Option<f32> {
718 f32_or_none(self.heading_rate_dps)
719 }
720}
721
722impl SbfBlockParse for ExtEventAttEulerBlock {
723 const BLOCK_ID: u16 = block_ids::EXT_EVENT_ATT_EULER;
724
725 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
726 if data.len() < 42 {
727 return Err(SbfError::ParseError("ExtEventAttEuler too short".into()));
728 }
729 Ok(Self {
730 tow_ms: header.tow_ms,
731 wnc: header.wnc,
732 nr_sv: data[12],
733 error: data[13],
734 mode: u16::from_le_bytes(data[14..16].try_into().unwrap()),
735 heading_deg: f32::from_le_bytes(data[18..22].try_into().unwrap()),
736 pitch_deg: f32::from_le_bytes(data[22..26].try_into().unwrap()),
737 roll_deg: f32::from_le_bytes(data[26..30].try_into().unwrap()),
738 pitch_rate_dps: f32::from_le_bytes(data[30..34].try_into().unwrap()),
739 roll_rate_dps: f32::from_le_bytes(data[34..38].try_into().unwrap()),
740 heading_rate_dps: f32::from_le_bytes(data[38..42].try_into().unwrap()),
741 })
742 }
743}
744
745#[derive(Debug, Clone)]
753pub struct EndOfPvtBlock {
754 tow_ms: u32,
755 wnc: u16,
756}
757
758impl EndOfPvtBlock {
759 pub fn tow_seconds(&self) -> f64 {
760 self.tow_ms as f64 * 0.001
761 }
762 pub fn tow_ms(&self) -> u32 {
763 self.tow_ms
764 }
765 pub fn wnc(&self) -> u16 {
766 self.wnc
767 }
768}
769
770impl SbfBlockParse for EndOfPvtBlock {
771 const BLOCK_ID: u16 = block_ids::END_OF_PVT;
772
773 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
774 if data.len() < 12 {
776 return Err(SbfError::ParseError("EndOfPVT too short".into()));
777 }
778
779 Ok(Self {
780 tow_ms: header.tow_ms,
781 wnc: header.wnc,
782 })
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::header::SbfHeader;
790
791 fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
792 SbfHeader {
793 crc: 0,
794 block_id,
795 block_rev: 0,
796 length: (data_len + 2) as u16,
797 tow_ms,
798 wnc,
799 }
800 }
801
802 #[test]
803 fn test_receiver_time_string() {
804 let time = ReceiverTimeBlock {
805 tow_ms: 0,
806 wnc: 0,
807 utc_year: 2024,
808 utc_month: 3,
809 utc_day: 15,
810 utc_hour: 12,
811 utc_minute: 30,
812 utc_second: 45,
813 delta_ls: 18,
814 sync_level: 4,
815 };
816
817 assert_eq!(time.utc_string(), "2024-03-15T12:30:45Z");
818 assert!(time.is_valid());
819 assert!(time.is_synchronized());
820 }
821
822 #[test]
823 fn test_receiver_time_validation() {
824 let invalid_time = ReceiverTimeBlock {
825 tow_ms: 0,
826 wnc: 0,
827 utc_year: 1999, utc_month: 13, utc_day: 1,
830 utc_hour: 0,
831 utc_minute: 0,
832 utc_second: 0,
833 delta_ls: 0,
834 sync_level: 0,
835 };
836
837 assert!(!invalid_time.is_valid());
838 assert!(!invalid_time.is_synchronized());
839 }
840
841 #[test]
842 fn test_sync_levels() {
843 let time = ReceiverTimeBlock {
844 tow_ms: 0,
845 wnc: 0,
846 utc_year: 2024,
847 utc_month: 1,
848 utc_day: 1,
849 utc_hour: 0,
850 utc_minute: 0,
851 utc_second: 0,
852 delta_ls: 18,
853 sync_level: 0,
854 };
855 assert_eq!(time.sync_level_desc(), "Not synchronized");
856
857 let time_sync = ReceiverTimeBlock {
858 sync_level: 4,
859 ..time
860 };
861 assert_eq!(time_sync.sync_level_desc(), "Fine time (PPS)");
862 }
863
864 #[test]
865 fn test_pps_offset_accessors() {
866 let block = PpsOffsetBlock {
867 tow_ms: 1500,
868 wnc: 2100,
869 sync_age: 3,
870 timescale: 1,
871 offset_ns: F32_DNU,
872 };
873
874 assert!((block.tow_seconds() - 1.5).abs() < 1e-6);
875 assert!(block.offset_ns().is_none());
876 assert!(block.offset_seconds().is_none());
877 }
878
879 #[test]
880 fn test_pps_offset_parse() {
881 let mut data = vec![0u8; 18];
882 data[12] = 2;
883 data[13] = 1;
884 data[14..18].copy_from_slice(&2500.0_f32.to_le_bytes());
885
886 let header = header_for(block_ids::PPS_OFFSET, data.len(), 5000, 2200);
887 let block = PpsOffsetBlock::parse(&header, &data).unwrap();
888
889 assert_eq!(block.sync_age, 2);
890 assert_eq!(block.timescale, 1);
891 assert!((block.offset_ns().unwrap() - 2500.0).abs() < 1e-6);
892 }
893
894 #[test]
895 fn test_ext_event_accessors() {
896 let block = ExtEventBlock {
897 tow_ms: 2500,
898 wnc: 2300,
899 source: 1,
900 polarity: 0,
901 offset_s: F32_DNU,
902 rx_clk_bias_s: F64_DNU,
903 pvt_age: 15,
904 };
905
906 assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
907 assert!(block.offset_seconds().is_none());
908 assert!(block.offset_ns().is_none());
909 assert!(block.rx_clk_bias_seconds().is_none());
910 assert!(block.rx_clk_bias_ms().is_none());
911 }
912
913 #[test]
914 fn test_ext_event_parse() {
915 let mut data = vec![0u8; 28];
916 data[12] = 2;
917 data[13] = 1;
918 data[14..18].copy_from_slice(&0.125_f32.to_le_bytes());
919 data[18..26].copy_from_slice(&(-0.25_f64).to_le_bytes());
920 data[26..28].copy_from_slice(&20_u16.to_le_bytes());
921
922 let header = header_for(block_ids::EXT_EVENT, data.len(), 6000, 2400);
923 let block = ExtEventBlock::parse(&header, &data).unwrap();
924
925 assert_eq!(block.source, 2);
926 assert_eq!(block.polarity, 1);
927 assert_eq!(block.pvt_age, 20);
928 assert!((block.offset_seconds().unwrap() - 0.125).abs() < 1e-9);
929 assert!((block.offset_ns().unwrap() - 125000000.0).abs() < 1.0);
930 assert!((block.rx_clk_bias_seconds().unwrap() + 0.25).abs() < 1e-12);
931 assert!((block.rx_clk_bias_ms().unwrap() + 250.0).abs() < 1e-9);
932 }
933
934 #[test]
935 fn test_ext_event_pvt_cartesian_scaled() {
936 let block = ExtEventPvtCartesianBlock {
937 tow_ms: 1000,
938 wnc: 2000,
939 mode: 4,
940 error: 0,
941 x_m: 1.0,
942 y_m: 2.0,
943 z_m: 3.0,
944 undulation_m: 4.0,
945 vx_mps: 0.1,
946 vy_mps: 0.2,
947 vz_mps: 0.3,
948 cog_deg: 45.0,
949 rx_clk_bias_ms: 0.0,
950 rx_clk_drift_ppm: 0.0,
951 time_system: 1,
952 datum: 0,
953 nr_sv: 8,
954 wa_corr_info: 0,
955 reference_id: 12,
956 mean_corr_age_raw: 250,
957 signal_info: 0,
958 alert_flag: 0,
959 nr_bases: 0,
960 };
961
962 assert!((block.mean_corr_age_seconds().unwrap() - 2.5).abs() < 1e-6);
963 }
964
965 #[test]
966 fn test_ext_event_pvt_cartesian_dnu() {
967 let block = ExtEventPvtCartesianBlock {
968 tow_ms: 0,
969 wnc: 0,
970 mode: 0,
971 error: 0,
972 x_m: F64_DNU,
973 y_m: 0.0,
974 z_m: 0.0,
975 undulation_m: F32_DNU,
976 vx_mps: 0.0,
977 vy_mps: 0.0,
978 vz_mps: 0.0,
979 cog_deg: 0.0,
980 rx_clk_bias_ms: 0.0,
981 rx_clk_drift_ppm: 0.0,
982 time_system: 0,
983 datum: 0,
984 nr_sv: 0,
985 wa_corr_info: 0,
986 reference_id: 0,
987 mean_corr_age_raw: U16_DNU,
988 signal_info: 0,
989 alert_flag: 0,
990 nr_bases: 0,
991 };
992
993 assert!(block.x_m().is_none());
994 assert!(block.undulation_m().is_none());
995 assert!(block.mean_corr_age_seconds().is_none());
996 }
997
998 #[test]
999 fn test_ext_event_pvt_cartesian_parse() {
1000 let mut data = vec![0u8; 84];
1001 data[12] = 3;
1002 data[13] = 1;
1003 data[14..22].copy_from_slice(&1.5_f64.to_le_bytes());
1004 data[22..30].copy_from_slice(&2.5_f64.to_le_bytes());
1005 data[30..38].copy_from_slice(&3.5_f64.to_le_bytes());
1006 data[38..42].copy_from_slice(&(-1.25_f32).to_le_bytes());
1007 data[42..46].copy_from_slice(&0.1_f32.to_le_bytes());
1008 data[46..50].copy_from_slice(&0.2_f32.to_le_bytes());
1009 data[50..54].copy_from_slice(&0.3_f32.to_le_bytes());
1010 data[54..58].copy_from_slice(&90.0_f32.to_le_bytes());
1011 data[58..66].copy_from_slice(&(-0.25_f64).to_le_bytes());
1012 data[66..70].copy_from_slice(&0.5_f32.to_le_bytes());
1013 data[70] = 2;
1014 data[71] = 1;
1015 data[72] = 7;
1016 data[73] = 3;
1017 data[74..76].copy_from_slice(&123_u16.to_le_bytes());
1018 data[76..78].copy_from_slice(&200_u16.to_le_bytes());
1019 data[78..82].copy_from_slice(&0xAABBCCDD_u32.to_le_bytes());
1020 data[82] = 1;
1021 data[83] = 2;
1022
1023 let header = header_for(block_ids::EXT_EVENT_PVT_CARTESIAN, data.len(), 9000, 2200);
1024 let block = ExtEventPvtCartesianBlock::parse(&header, &data).unwrap();
1025
1026 assert_eq!(block.mode_raw(), 3);
1027 assert_eq!(block.error_raw(), 1);
1028 assert_eq!(block.reference_id, 123);
1029 assert_eq!(block.num_satellites(), 7);
1030 assert!((block.x_m().unwrap() - 1.5).abs() < 1e-6);
1031 assert!((block.mean_corr_age_seconds().unwrap() - 2.0).abs() < 1e-6);
1032 }
1033
1034 #[test]
1035 fn test_ext_event_pvt_geodetic_scaled() {
1036 let block = ExtEventPvtGeodeticBlock {
1037 tow_ms: 0,
1038 wnc: 0,
1039 mode: 4,
1040 error: 0,
1041 latitude_rad: 1.0,
1042 longitude_rad: -0.5,
1043 height_m: 10.0,
1044 undulation_m: 1.0,
1045 vn_mps: 0.0,
1046 ve_mps: 0.0,
1047 vu_mps: 0.0,
1048 cog_deg: 0.0,
1049 rx_clk_bias_ms: 0.0,
1050 rx_clk_drift_ppm: 0.0,
1051 time_system: 0,
1052 datum: 0,
1053 nr_sv: 0,
1054 wa_corr_info: 0,
1055 reference_id: 0,
1056 mean_corr_age_raw: 150,
1057 signal_info: 0,
1058 alert_flag: 0,
1059 nr_bases: 0,
1060 };
1061
1062 assert!((block.latitude_deg().unwrap() - 57.2958).abs() < 1e-3);
1063 assert!((block.longitude_deg().unwrap() + 28.6479).abs() < 1e-3);
1064 assert!((block.mean_corr_age_seconds().unwrap() - 1.5).abs() < 1e-6);
1065 }
1066
1067 #[test]
1068 fn test_ext_event_pvt_geodetic_dnu() {
1069 let block = ExtEventPvtGeodeticBlock {
1070 tow_ms: 0,
1071 wnc: 0,
1072 mode: 0,
1073 error: 0,
1074 latitude_rad: F64_DNU,
1075 longitude_rad: 0.0,
1076 height_m: F64_DNU,
1077 undulation_m: F32_DNU,
1078 vn_mps: 0.0,
1079 ve_mps: 0.0,
1080 vu_mps: 0.0,
1081 cog_deg: 0.0,
1082 rx_clk_bias_ms: 0.0,
1083 rx_clk_drift_ppm: 0.0,
1084 time_system: 0,
1085 datum: 0,
1086 nr_sv: 0,
1087 wa_corr_info: 0,
1088 reference_id: 0,
1089 mean_corr_age_raw: U16_DNU,
1090 signal_info: 0,
1091 alert_flag: 0,
1092 nr_bases: 0,
1093 };
1094
1095 assert!(block.latitude_deg().is_none());
1096 assert!(block.height_m().is_none());
1097 assert!(block.undulation_m().is_none());
1098 assert!(block.mean_corr_age_seconds().is_none());
1099 }
1100
1101 #[test]
1102 fn test_ext_event_pvt_geodetic_parse() {
1103 let mut data = vec![0u8; 84];
1104 data[12] = 2;
1105 data[13] = 0;
1106 data[14..22].copy_from_slice(&0.5_f64.to_le_bytes());
1107 data[22..30].copy_from_slice(&1.0_f64.to_le_bytes());
1108 data[30..38].copy_from_slice(&50.0_f64.to_le_bytes());
1109 data[38..42].copy_from_slice(&2.5_f32.to_le_bytes());
1110 data[42..46].copy_from_slice(&(-0.1_f32).to_le_bytes());
1111 data[46..50].copy_from_slice(&0.2_f32.to_le_bytes());
1112 data[50..54].copy_from_slice(&0.3_f32.to_le_bytes());
1113 data[54..58].copy_from_slice(&120.0_f32.to_le_bytes());
1114 data[58..66].copy_from_slice(&1.25_f64.to_le_bytes());
1115 data[66..70].copy_from_slice(&(-0.75_f32).to_le_bytes());
1116 data[70] = 1;
1117 data[71] = 2;
1118 data[72] = 9;
1119 data[73] = 4;
1120 data[74..76].copy_from_slice(&321_u16.to_le_bytes());
1121 data[76..78].copy_from_slice(&100_u16.to_le_bytes());
1122 data[78..82].copy_from_slice(&0x01020304_u32.to_le_bytes());
1123 data[82] = 0;
1124 data[83] = 1;
1125
1126 let header = header_for(block_ids::EXT_EVENT_PVT_GEODETIC, data.len(), 9100, 2300);
1127 let block = ExtEventPvtGeodeticBlock::parse(&header, &data).unwrap();
1128
1129 assert_eq!(block.mode_raw(), 2);
1130 assert_eq!(block.reference_id, 321);
1131 assert_eq!(block.num_satellites(), 9);
1132 assert!((block.latitude_deg().unwrap() - 28.6479).abs() < 1e-3);
1133 assert!((block.height_m().unwrap() - 50.0).abs() < 1e-6);
1134 }
1135
1136 #[test]
1137 fn test_ext_event_base_vect_geod_parse() {
1138 let mut data = vec![0u8; 14 + 52];
1139 data[12] = 1;
1140 data[13] = 52;
1141 data[14] = 7;
1142 data[15] = 0;
1143 data[16] = 4;
1144 data[17] = 0;
1145 data[18..26].copy_from_slice(&1.5f64.to_le_bytes());
1146 data[26..34].copy_from_slice(&2.5f64.to_le_bytes());
1147 data[34..42].copy_from_slice(&3.5f64.to_le_bytes());
1148 data[54..56].copy_from_slice(&18000u16.to_le_bytes());
1149 data[56..58].copy_from_slice(&2500i16.to_le_bytes());
1150 data[58..60].copy_from_slice(&42u16.to_le_bytes());
1151 data[60..62].copy_from_slice(&300u16.to_le_bytes());
1152 data[62..66].copy_from_slice(&0x11223344u32.to_le_bytes());
1153
1154 let header = header_for(block_ids::EXT_EVENT_BASE_VECT_GEOD, data.len(), 12000, 2400);
1155 let block = ExtEventBaseVectGeodBlock::parse(&header, &data).unwrap();
1156 assert_eq!(block.num_vectors(), 1);
1157 assert_eq!(block.vectors[0].reference_id, 42);
1158 assert!((block.vectors[0].de_m().unwrap() - 1.5).abs() < 1e-6);
1159 assert!((block.vectors[0].azimuth_deg().unwrap() - 180.0).abs() < 1e-6);
1160 assert!((block.vectors[0].corr_age_seconds().unwrap() - 3.0).abs() < 1e-6);
1161 }
1162
1163 #[test]
1164 fn test_ext_event_att_euler_parse() {
1165 let mut data = vec![0u8; 42];
1166 data[12] = 8;
1167 data[13] = 1;
1168 data[14..16].copy_from_slice(&4u16.to_le_bytes());
1169 data[18..22].copy_from_slice(&45.0f32.to_le_bytes());
1170 data[22..26].copy_from_slice(&(-2.5f32).to_le_bytes());
1171 data[26..30].copy_from_slice(&1.25f32.to_le_bytes());
1172 data[30..34].copy_from_slice(&0.5f32.to_le_bytes());
1173 data[34..38].copy_from_slice(&0.25f32.to_le_bytes());
1174 data[38..42].copy_from_slice(&(-0.75f32).to_le_bytes());
1175
1176 let header = header_for(block_ids::EXT_EVENT_ATT_EULER, data.len(), 13000, 2500);
1177 let block = ExtEventAttEulerBlock::parse(&header, &data).unwrap();
1178 assert_eq!(block.num_satellites(), 8);
1179 assert_eq!(block.error_raw(), 1);
1180 assert_eq!(block.mode_raw(), 4);
1181 assert_eq!(block.heading_deg(), Some(45.0));
1182 assert_eq!(block.pitch_deg(), Some(-2.5));
1183 assert_eq!(block.roll_deg(), Some(1.25));
1184 assert_eq!(block.heading_rate_dps(), Some(-0.75));
1185 }
1186}