1use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5use crate::types::{PvtError, PvtMode};
6
7use super::block_ids;
8use super::dnu::{f32_or_none, f64_or_none, u8_or_none, U16_DNU};
9use super::position::BaseVectorGeodBlock;
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 {
409 u8_or_none(self.nr_sv).unwrap_or(0)
410 }
411 pub fn num_satellites_opt(&self) -> Option<u8> {
413 u8_or_none(self.nr_sv)
414 }
415 pub fn num_satellites_raw(&self) -> u8 {
417 self.nr_sv
418 }
419}
420
421impl SbfBlockParse for ExtEventPvtCartesianBlock {
422 const BLOCK_ID: u16 = block_ids::EXT_EVENT_PVT_CARTESIAN;
423
424 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
425 if data.len() < 84 {
426 return Err(SbfError::ParseError(
427 "ExtEventPVTCartesian too short".into(),
428 ));
429 }
430
431 let mode = data[12];
432 let error = data[13];
433 let x_m = f64::from_le_bytes(data[14..22].try_into().unwrap());
434 let y_m = f64::from_le_bytes(data[22..30].try_into().unwrap());
435 let z_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
436 let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
437 let vx_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
438 let vy_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
439 let vz_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
440 let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
441 let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
442 let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
443 let time_system = data[70];
444 let datum = data[71];
445 let nr_sv = data[72];
446 let wa_corr_info = data[73];
447 let reference_id = u16::from_le_bytes([data[74], data[75]]);
448 let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
449 let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
450 let alert_flag = data[82];
451 let nr_bases = data[83];
452
453 Ok(Self {
454 tow_ms: header.tow_ms,
455 wnc: header.wnc,
456 mode,
457 error,
458 x_m,
459 y_m,
460 z_m,
461 undulation_m,
462 vx_mps,
463 vy_mps,
464 vz_mps,
465 cog_deg,
466 rx_clk_bias_ms,
467 rx_clk_drift_ppm,
468 time_system,
469 datum,
470 nr_sv,
471 wa_corr_info,
472 reference_id,
473 mean_corr_age_raw,
474 signal_info,
475 alert_flag,
476 nr_bases,
477 })
478 }
479}
480
481#[derive(Debug, Clone)]
485pub struct ExtEventPvtGeodeticBlock {
486 tow_ms: u32,
487 wnc: u16,
488 mode: u8,
489 error: u8,
490 latitude_rad: f64,
491 longitude_rad: f64,
492 height_m: f64,
493 undulation_m: f32,
494 vn_mps: f32,
495 ve_mps: f32,
496 vu_mps: f32,
497 cog_deg: f32,
498 rx_clk_bias_ms: f64,
499 rx_clk_drift_ppm: f32,
500 pub time_system: u8,
501 pub datum: u8,
502 nr_sv: u8,
503 pub wa_corr_info: u8,
504 pub reference_id: u16,
505 mean_corr_age_raw: u16,
506 pub signal_info: u32,
507 pub alert_flag: u8,
508 pub nr_bases: u8,
509}
510
511impl ExtEventPvtGeodeticBlock {
512 pub fn tow_seconds(&self) -> f64 {
513 self.tow_ms as f64 * 0.001
514 }
515 pub fn tow_ms(&self) -> u32 {
516 self.tow_ms
517 }
518 pub fn wnc(&self) -> u16 {
519 self.wnc
520 }
521
522 pub fn mode(&self) -> PvtMode {
523 PvtMode::from_mode_byte(self.mode)
524 }
525 pub fn mode_raw(&self) -> u8 {
526 self.mode
527 }
528 pub fn error(&self) -> PvtError {
529 PvtError::from_error_byte(self.error)
530 }
531 pub fn error_raw(&self) -> u8 {
532 self.error
533 }
534 pub fn has_fix(&self) -> bool {
535 self.mode().has_fix() && self.error().is_ok()
536 }
537
538 pub fn latitude_deg(&self) -> Option<f64> {
540 f64_or_none(self.latitude_rad).map(|value| value.to_degrees())
541 }
542 pub fn longitude_deg(&self) -> Option<f64> {
543 f64_or_none(self.longitude_rad).map(|value| value.to_degrees())
544 }
545 pub fn latitude_rad(&self) -> f64 {
546 self.latitude_rad
547 }
548 pub fn longitude_rad(&self) -> f64 {
549 self.longitude_rad
550 }
551 pub fn height_m(&self) -> Option<f64> {
552 f64_or_none(self.height_m)
553 }
554 pub fn undulation_m(&self) -> Option<f32> {
555 f32_or_none(self.undulation_m)
556 }
557
558 pub fn velocity_north_mps(&self) -> Option<f32> {
560 f32_or_none(self.vn_mps)
561 }
562 pub fn velocity_east_mps(&self) -> Option<f32> {
563 f32_or_none(self.ve_mps)
564 }
565 pub fn velocity_up_mps(&self) -> Option<f32> {
566 f32_or_none(self.vu_mps)
567 }
568 pub fn course_over_ground_deg(&self) -> Option<f32> {
569 f32_or_none(self.cog_deg)
570 }
571
572 pub fn clock_bias_ms(&self) -> Option<f64> {
574 f64_or_none(self.rx_clk_bias_ms)
575 }
576 pub fn clock_drift_ppm(&self) -> Option<f32> {
577 f32_or_none(self.rx_clk_drift_ppm)
578 }
579
580 pub fn mean_corr_age_seconds(&self) -> Option<f32> {
582 if self.mean_corr_age_raw == U16_DNU {
583 None
584 } else {
585 Some(self.mean_corr_age_raw as f32 * 0.01)
586 }
587 }
588 pub fn mean_corr_age_raw(&self) -> u16 {
589 self.mean_corr_age_raw
590 }
591
592 pub fn num_satellites(&self) -> u8 {
597 u8_or_none(self.nr_sv).unwrap_or(0)
598 }
599 pub fn num_satellites_opt(&self) -> Option<u8> {
601 u8_or_none(self.nr_sv)
602 }
603 pub fn num_satellites_raw(&self) -> u8 {
605 self.nr_sv
606 }
607}
608
609impl SbfBlockParse for ExtEventPvtGeodeticBlock {
610 const BLOCK_ID: u16 = block_ids::EXT_EVENT_PVT_GEODETIC;
611
612 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
613 if data.len() < 84 {
614 return Err(SbfError::ParseError("ExtEventPVTGeodetic too short".into()));
615 }
616
617 let mode = data[12];
618 let error = data[13];
619 let latitude_rad = f64::from_le_bytes(data[14..22].try_into().unwrap());
620 let longitude_rad = f64::from_le_bytes(data[22..30].try_into().unwrap());
621 let height_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
622 let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
623 let vn_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
624 let ve_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
625 let vu_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
626 let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
627 let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
628 let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
629 let time_system = data[70];
630 let datum = data[71];
631 let nr_sv = data[72];
632 let wa_corr_info = data[73];
633 let reference_id = u16::from_le_bytes([data[74], data[75]]);
634 let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
635 let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
636 let alert_flag = data[82];
637 let nr_bases = data[83];
638
639 Ok(Self {
640 tow_ms: header.tow_ms,
641 wnc: header.wnc,
642 mode,
643 error,
644 latitude_rad,
645 longitude_rad,
646 height_m,
647 undulation_m,
648 vn_mps,
649 ve_mps,
650 vu_mps,
651 cog_deg,
652 rx_clk_bias_ms,
653 rx_clk_drift_ppm,
654 time_system,
655 datum,
656 nr_sv,
657 wa_corr_info,
658 reference_id,
659 mean_corr_age_raw,
660 signal_info,
661 alert_flag,
662 nr_bases,
663 })
664 }
665}
666
667#[derive(Debug, Clone)]
673pub struct ExtEventBaseVectGeodBlock(pub BaseVectorGeodBlock);
674
675impl std::ops::Deref for ExtEventBaseVectGeodBlock {
676 type Target = BaseVectorGeodBlock;
677
678 fn deref(&self) -> &Self::Target {
679 &self.0
680 }
681}
682
683impl SbfBlockParse for ExtEventBaseVectGeodBlock {
684 const BLOCK_ID: u16 = block_ids::EXT_EVENT_BASE_VECT_GEOD;
685
686 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
687 Ok(Self(BaseVectorGeodBlock::parse(header, data)?))
688 }
689}
690
691#[derive(Debug, Clone)]
693pub struct ExtEventAttEulerBlock {
694 tow_ms: u32,
695 wnc: u16,
696 nr_sv: u8,
697 error: u8,
698 mode: u16,
699 heading_deg: f32,
700 pitch_deg: f32,
701 roll_deg: f32,
702 pitch_rate_dps: f32,
703 roll_rate_dps: f32,
704 heading_rate_dps: f32,
705}
706
707impl ExtEventAttEulerBlock {
708 pub fn tow_seconds(&self) -> f64 {
709 self.tow_ms as f64 * 0.001
710 }
711 pub fn tow_ms(&self) -> u32 {
712 self.tow_ms
713 }
714 pub fn wnc(&self) -> u16 {
715 self.wnc
716 }
717 pub fn num_satellites(&self) -> u8 {
722 u8_or_none(self.nr_sv).unwrap_or(0)
723 }
724 pub fn num_satellites_opt(&self) -> Option<u8> {
726 u8_or_none(self.nr_sv)
727 }
728 pub fn num_satellites_raw(&self) -> u8 {
730 self.nr_sv
731 }
732 pub fn error_raw(&self) -> u8 {
733 self.error
734 }
735 pub fn mode_raw(&self) -> u16 {
736 self.mode
737 }
738 pub fn heading_deg(&self) -> Option<f32> {
739 f32_or_none(self.heading_deg)
740 }
741 pub fn pitch_deg(&self) -> Option<f32> {
742 f32_or_none(self.pitch_deg)
743 }
744 pub fn roll_deg(&self) -> Option<f32> {
745 f32_or_none(self.roll_deg)
746 }
747 pub fn pitch_rate_dps(&self) -> Option<f32> {
748 f32_or_none(self.pitch_rate_dps)
749 }
750 pub fn roll_rate_dps(&self) -> Option<f32> {
751 f32_or_none(self.roll_rate_dps)
752 }
753 pub fn heading_rate_dps(&self) -> Option<f32> {
754 f32_or_none(self.heading_rate_dps)
755 }
756}
757
758impl SbfBlockParse for ExtEventAttEulerBlock {
759 const BLOCK_ID: u16 = block_ids::EXT_EVENT_ATT_EULER;
760
761 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
762 if data.len() < 42 {
763 return Err(SbfError::ParseError("ExtEventAttEuler too short".into()));
764 }
765 Ok(Self {
766 tow_ms: header.tow_ms,
767 wnc: header.wnc,
768 nr_sv: data[12],
769 error: data[13],
770 mode: u16::from_le_bytes(data[14..16].try_into().unwrap()),
771 heading_deg: f32::from_le_bytes(data[18..22].try_into().unwrap()),
772 pitch_deg: f32::from_le_bytes(data[22..26].try_into().unwrap()),
773 roll_deg: f32::from_le_bytes(data[26..30].try_into().unwrap()),
774 pitch_rate_dps: f32::from_le_bytes(data[30..34].try_into().unwrap()),
775 roll_rate_dps: f32::from_le_bytes(data[34..38].try_into().unwrap()),
776 heading_rate_dps: f32::from_le_bytes(data[38..42].try_into().unwrap()),
777 })
778 }
779}
780
781#[derive(Debug, Clone)]
789pub struct EndOfPvtBlock {
790 tow_ms: u32,
791 wnc: u16,
792}
793
794impl EndOfPvtBlock {
795 pub fn tow_seconds(&self) -> f64 {
796 self.tow_ms as f64 * 0.001
797 }
798 pub fn tow_ms(&self) -> u32 {
799 self.tow_ms
800 }
801 pub fn wnc(&self) -> u16 {
802 self.wnc
803 }
804}
805
806impl SbfBlockParse for EndOfPvtBlock {
807 const BLOCK_ID: u16 = block_ids::END_OF_PVT;
808
809 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
810 if data.len() < 12 {
812 return Err(SbfError::ParseError("EndOfPVT too short".into()));
813 }
814
815 Ok(Self {
816 tow_ms: header.tow_ms,
817 wnc: header.wnc,
818 })
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::header::SbfHeader;
826
827 fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
828 SbfHeader {
829 crc: 0,
830 block_id,
831 block_rev: 0,
832 length: (data_len + 2) as u16,
833 tow_ms,
834 wnc,
835 }
836 }
837
838 #[test]
839 fn test_receiver_time_string() {
840 let time = ReceiverTimeBlock {
841 tow_ms: 0,
842 wnc: 0,
843 utc_year: 2024,
844 utc_month: 3,
845 utc_day: 15,
846 utc_hour: 12,
847 utc_minute: 30,
848 utc_second: 45,
849 delta_ls: 18,
850 sync_level: 4,
851 };
852
853 assert_eq!(time.utc_string(), "2024-03-15T12:30:45Z");
854 assert!(time.is_valid());
855 assert!(time.is_synchronized());
856 }
857
858 #[test]
859 fn test_receiver_time_validation() {
860 let invalid_time = ReceiverTimeBlock {
861 tow_ms: 0,
862 wnc: 0,
863 utc_year: 1999, utc_month: 13, utc_day: 1,
866 utc_hour: 0,
867 utc_minute: 0,
868 utc_second: 0,
869 delta_ls: 0,
870 sync_level: 0,
871 };
872
873 assert!(!invalid_time.is_valid());
874 assert!(!invalid_time.is_synchronized());
875 }
876
877 #[test]
878 fn test_sync_levels() {
879 let time = ReceiverTimeBlock {
880 tow_ms: 0,
881 wnc: 0,
882 utc_year: 2024,
883 utc_month: 1,
884 utc_day: 1,
885 utc_hour: 0,
886 utc_minute: 0,
887 utc_second: 0,
888 delta_ls: 18,
889 sync_level: 0,
890 };
891 assert_eq!(time.sync_level_desc(), "Not synchronized");
892
893 let time_sync = ReceiverTimeBlock {
894 sync_level: 4,
895 ..time
896 };
897 assert_eq!(time_sync.sync_level_desc(), "Fine time (PPS)");
898 }
899
900 #[test]
901 fn test_pps_offset_accessors() {
902 let block = PpsOffsetBlock {
903 tow_ms: 1500,
904 wnc: 2100,
905 sync_age: 3,
906 timescale: 1,
907 offset_ns: F32_DNU,
908 };
909
910 assert!((block.tow_seconds() - 1.5).abs() < 1e-6);
911 assert!(block.offset_ns().is_none());
912 assert!(block.offset_seconds().is_none());
913 }
914
915 #[test]
916 fn test_pps_offset_parse() {
917 let mut data = vec![0u8; 18];
918 data[12] = 2;
919 data[13] = 1;
920 data[14..18].copy_from_slice(&2500.0_f32.to_le_bytes());
921
922 let header = header_for(block_ids::PPS_OFFSET, data.len(), 5000, 2200);
923 let block = PpsOffsetBlock::parse(&header, &data).unwrap();
924
925 assert_eq!(block.sync_age, 2);
926 assert_eq!(block.timescale, 1);
927 assert!((block.offset_ns().unwrap() - 2500.0).abs() < 1e-6);
928 }
929
930 #[test]
931 fn test_ext_event_accessors() {
932 let block = ExtEventBlock {
933 tow_ms: 2500,
934 wnc: 2300,
935 source: 1,
936 polarity: 0,
937 offset_s: F32_DNU,
938 rx_clk_bias_s: F64_DNU,
939 pvt_age: 15,
940 };
941
942 assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
943 assert!(block.offset_seconds().is_none());
944 assert!(block.offset_ns().is_none());
945 assert!(block.rx_clk_bias_seconds().is_none());
946 assert!(block.rx_clk_bias_ms().is_none());
947 }
948
949 #[test]
950 fn test_ext_event_parse() {
951 let mut data = vec![0u8; 28];
952 data[12] = 2;
953 data[13] = 1;
954 data[14..18].copy_from_slice(&0.125_f32.to_le_bytes());
955 data[18..26].copy_from_slice(&(-0.25_f64).to_le_bytes());
956 data[26..28].copy_from_slice(&20_u16.to_le_bytes());
957
958 let header = header_for(block_ids::EXT_EVENT, data.len(), 6000, 2400);
959 let block = ExtEventBlock::parse(&header, &data).unwrap();
960
961 assert_eq!(block.source, 2);
962 assert_eq!(block.polarity, 1);
963 assert_eq!(block.pvt_age, 20);
964 assert!((block.offset_seconds().unwrap() - 0.125).abs() < 1e-9);
965 assert!((block.offset_ns().unwrap() - 125000000.0).abs() < 1.0);
966 assert!((block.rx_clk_bias_seconds().unwrap() + 0.25).abs() < 1e-12);
967 assert!((block.rx_clk_bias_ms().unwrap() + 250.0).abs() < 1e-9);
968 }
969
970 #[test]
971 fn test_ext_event_pvt_cartesian_scaled() {
972 let block = ExtEventPvtCartesianBlock {
973 tow_ms: 1000,
974 wnc: 2000,
975 mode: 4,
976 error: 0,
977 x_m: 1.0,
978 y_m: 2.0,
979 z_m: 3.0,
980 undulation_m: 4.0,
981 vx_mps: 0.1,
982 vy_mps: 0.2,
983 vz_mps: 0.3,
984 cog_deg: 45.0,
985 rx_clk_bias_ms: 0.0,
986 rx_clk_drift_ppm: 0.0,
987 time_system: 1,
988 datum: 0,
989 nr_sv: 8,
990 wa_corr_info: 0,
991 reference_id: 12,
992 mean_corr_age_raw: 250,
993 signal_info: 0,
994 alert_flag: 0,
995 nr_bases: 0,
996 };
997
998 assert!((block.mean_corr_age_seconds().unwrap() - 2.5).abs() < 1e-6);
999 }
1000
1001 #[test]
1002 fn test_ext_event_pvt_cartesian_dnu() {
1003 let block = ExtEventPvtCartesianBlock {
1004 tow_ms: 0,
1005 wnc: 0,
1006 mode: 0,
1007 error: 0,
1008 x_m: F64_DNU,
1009 y_m: 0.0,
1010 z_m: 0.0,
1011 undulation_m: F32_DNU,
1012 vx_mps: 0.0,
1013 vy_mps: 0.0,
1014 vz_mps: 0.0,
1015 cog_deg: 0.0,
1016 rx_clk_bias_ms: 0.0,
1017 rx_clk_drift_ppm: 0.0,
1018 time_system: 0,
1019 datum: 0,
1020 nr_sv: 255,
1021 wa_corr_info: 0,
1022 reference_id: 0,
1023 mean_corr_age_raw: U16_DNU,
1024 signal_info: 0,
1025 alert_flag: 0,
1026 nr_bases: 0,
1027 };
1028
1029 assert!(block.x_m().is_none());
1030 assert!(block.undulation_m().is_none());
1031 assert_eq!(block.num_satellites_raw(), 255);
1032 assert_eq!(block.num_satellites_opt(), None);
1033 assert_eq!(block.num_satellites(), 0);
1034 assert!(block.mean_corr_age_seconds().is_none());
1035 }
1036
1037 #[test]
1038 fn test_ext_event_pvt_cartesian_parse() {
1039 let mut data = vec![0u8; 84];
1040 data[12] = 3;
1041 data[13] = 1;
1042 data[14..22].copy_from_slice(&1.5_f64.to_le_bytes());
1043 data[22..30].copy_from_slice(&2.5_f64.to_le_bytes());
1044 data[30..38].copy_from_slice(&3.5_f64.to_le_bytes());
1045 data[38..42].copy_from_slice(&(-1.25_f32).to_le_bytes());
1046 data[42..46].copy_from_slice(&0.1_f32.to_le_bytes());
1047 data[46..50].copy_from_slice(&0.2_f32.to_le_bytes());
1048 data[50..54].copy_from_slice(&0.3_f32.to_le_bytes());
1049 data[54..58].copy_from_slice(&90.0_f32.to_le_bytes());
1050 data[58..66].copy_from_slice(&(-0.25_f64).to_le_bytes());
1051 data[66..70].copy_from_slice(&0.5_f32.to_le_bytes());
1052 data[70] = 2;
1053 data[71] = 1;
1054 data[72] = 7;
1055 data[73] = 3;
1056 data[74..76].copy_from_slice(&123_u16.to_le_bytes());
1057 data[76..78].copy_from_slice(&200_u16.to_le_bytes());
1058 data[78..82].copy_from_slice(&0xAABBCCDD_u32.to_le_bytes());
1059 data[82] = 1;
1060 data[83] = 2;
1061
1062 let header = header_for(block_ids::EXT_EVENT_PVT_CARTESIAN, data.len(), 9000, 2200);
1063 let block = ExtEventPvtCartesianBlock::parse(&header, &data).unwrap();
1064
1065 assert_eq!(block.mode_raw(), 3);
1066 assert_eq!(block.error_raw(), 1);
1067 assert_eq!(block.reference_id, 123);
1068 assert_eq!(block.num_satellites(), 7);
1069 assert_eq!(block.num_satellites_opt(), Some(7));
1070 assert_eq!(block.num_satellites_raw(), 7);
1071 assert!((block.x_m().unwrap() - 1.5).abs() < 1e-6);
1072 assert!((block.mean_corr_age_seconds().unwrap() - 2.0).abs() < 1e-6);
1073 }
1074
1075 #[test]
1076 fn test_ext_event_pvt_geodetic_scaled() {
1077 let block = ExtEventPvtGeodeticBlock {
1078 tow_ms: 0,
1079 wnc: 0,
1080 mode: 4,
1081 error: 0,
1082 latitude_rad: 1.0,
1083 longitude_rad: -0.5,
1084 height_m: 10.0,
1085 undulation_m: 1.0,
1086 vn_mps: 0.0,
1087 ve_mps: 0.0,
1088 vu_mps: 0.0,
1089 cog_deg: 0.0,
1090 rx_clk_bias_ms: 0.0,
1091 rx_clk_drift_ppm: 0.0,
1092 time_system: 0,
1093 datum: 0,
1094 nr_sv: 0,
1095 wa_corr_info: 0,
1096 reference_id: 0,
1097 mean_corr_age_raw: 150,
1098 signal_info: 0,
1099 alert_flag: 0,
1100 nr_bases: 0,
1101 };
1102
1103 assert!((block.latitude_deg().unwrap() - 57.2958).abs() < 1e-3);
1104 assert!((block.longitude_deg().unwrap() + 28.6479).abs() < 1e-3);
1105 assert!((block.mean_corr_age_seconds().unwrap() - 1.5).abs() < 1e-6);
1106 }
1107
1108 #[test]
1109 fn test_ext_event_pvt_geodetic_dnu() {
1110 let block = ExtEventPvtGeodeticBlock {
1111 tow_ms: 0,
1112 wnc: 0,
1113 mode: 0,
1114 error: 0,
1115 latitude_rad: F64_DNU,
1116 longitude_rad: 0.0,
1117 height_m: F64_DNU,
1118 undulation_m: F32_DNU,
1119 vn_mps: 0.0,
1120 ve_mps: 0.0,
1121 vu_mps: 0.0,
1122 cog_deg: 0.0,
1123 rx_clk_bias_ms: 0.0,
1124 rx_clk_drift_ppm: 0.0,
1125 time_system: 0,
1126 datum: 0,
1127 nr_sv: 255,
1128 wa_corr_info: 0,
1129 reference_id: 0,
1130 mean_corr_age_raw: U16_DNU,
1131 signal_info: 0,
1132 alert_flag: 0,
1133 nr_bases: 0,
1134 };
1135
1136 assert!(block.latitude_deg().is_none());
1137 assert!(block.height_m().is_none());
1138 assert!(block.undulation_m().is_none());
1139 assert_eq!(block.num_satellites_raw(), 255);
1140 assert_eq!(block.num_satellites_opt(), None);
1141 assert_eq!(block.num_satellites(), 0);
1142 assert!(block.mean_corr_age_seconds().is_none());
1143 }
1144
1145 #[test]
1146 fn test_ext_event_pvt_geodetic_parse() {
1147 let mut data = vec![0u8; 84];
1148 data[12] = 2;
1149 data[13] = 0;
1150 data[14..22].copy_from_slice(&0.5_f64.to_le_bytes());
1151 data[22..30].copy_from_slice(&1.0_f64.to_le_bytes());
1152 data[30..38].copy_from_slice(&50.0_f64.to_le_bytes());
1153 data[38..42].copy_from_slice(&2.5_f32.to_le_bytes());
1154 data[42..46].copy_from_slice(&(-0.1_f32).to_le_bytes());
1155 data[46..50].copy_from_slice(&0.2_f32.to_le_bytes());
1156 data[50..54].copy_from_slice(&0.3_f32.to_le_bytes());
1157 data[54..58].copy_from_slice(&120.0_f32.to_le_bytes());
1158 data[58..66].copy_from_slice(&1.25_f64.to_le_bytes());
1159 data[66..70].copy_from_slice(&(-0.75_f32).to_le_bytes());
1160 data[70] = 1;
1161 data[71] = 2;
1162 data[72] = 9;
1163 data[73] = 4;
1164 data[74..76].copy_from_slice(&321_u16.to_le_bytes());
1165 data[76..78].copy_from_slice(&100_u16.to_le_bytes());
1166 data[78..82].copy_from_slice(&0x01020304_u32.to_le_bytes());
1167 data[82] = 0;
1168 data[83] = 1;
1169
1170 let header = header_for(block_ids::EXT_EVENT_PVT_GEODETIC, data.len(), 9100, 2300);
1171 let block = ExtEventPvtGeodeticBlock::parse(&header, &data).unwrap();
1172
1173 assert_eq!(block.mode_raw(), 2);
1174 assert_eq!(block.reference_id, 321);
1175 assert_eq!(block.num_satellites(), 9);
1176 assert_eq!(block.num_satellites_opt(), Some(9));
1177 assert_eq!(block.num_satellites_raw(), 9);
1178 assert!((block.latitude_deg().unwrap() - 28.6479).abs() < 1e-3);
1179 assert!((block.height_m().unwrap() - 50.0).abs() < 1e-6);
1180 }
1181
1182 #[test]
1183 fn test_ext_event_base_vect_geod_parse() {
1184 let mut data = vec![0u8; 14 + 52];
1185 data[12] = 1;
1186 data[13] = 52;
1187 data[14] = 7;
1188 data[15] = 0;
1189 data[16] = 4;
1190 data[17] = 0;
1191 data[18..26].copy_from_slice(&1.5f64.to_le_bytes());
1192 data[26..34].copy_from_slice(&2.5f64.to_le_bytes());
1193 data[34..42].copy_from_slice(&3.5f64.to_le_bytes());
1194 data[54..56].copy_from_slice(&18000u16.to_le_bytes());
1195 data[56..58].copy_from_slice(&2500i16.to_le_bytes());
1196 data[58..60].copy_from_slice(&42u16.to_le_bytes());
1197 data[60..62].copy_from_slice(&300u16.to_le_bytes());
1198 data[62..66].copy_from_slice(&0x11223344u32.to_le_bytes());
1199
1200 let header = header_for(block_ids::EXT_EVENT_BASE_VECT_GEOD, data.len(), 12000, 2400);
1201 let block = ExtEventBaseVectGeodBlock::parse(&header, &data).unwrap();
1202 assert_eq!(block.num_vectors(), 1);
1203 assert_eq!(block.vectors[0].reference_id, 42);
1204 assert!((block.vectors[0].de_m().unwrap() - 1.5).abs() < 1e-6);
1205 assert!((block.vectors[0].azimuth_deg().unwrap() - 180.0).abs() < 1e-6);
1206 assert!((block.vectors[0].corr_age_seconds().unwrap() - 3.0).abs() < 1e-6);
1207 }
1208
1209 #[test]
1210 fn test_ext_event_att_euler_parse() {
1211 let mut data = vec![0u8; 42];
1212 data[12] = 8;
1213 data[13] = 1;
1214 data[14..16].copy_from_slice(&4u16.to_le_bytes());
1215 data[18..22].copy_from_slice(&45.0f32.to_le_bytes());
1216 data[22..26].copy_from_slice(&(-2.5f32).to_le_bytes());
1217 data[26..30].copy_from_slice(&1.25f32.to_le_bytes());
1218 data[30..34].copy_from_slice(&0.5f32.to_le_bytes());
1219 data[34..38].copy_from_slice(&0.25f32.to_le_bytes());
1220 data[38..42].copy_from_slice(&(-0.75f32).to_le_bytes());
1221
1222 let header = header_for(block_ids::EXT_EVENT_ATT_EULER, data.len(), 13000, 2500);
1223 let block = ExtEventAttEulerBlock::parse(&header, &data).unwrap();
1224 assert_eq!(block.num_satellites(), 8);
1225 assert_eq!(block.num_satellites_opt(), Some(8));
1226 assert_eq!(block.num_satellites_raw(), 8);
1227 assert_eq!(block.error_raw(), 1);
1228 assert_eq!(block.mode_raw(), 4);
1229 assert_eq!(block.heading_deg(), Some(45.0));
1230 assert_eq!(block.pitch_deg(), Some(-2.5));
1231 assert_eq!(block.roll_deg(), Some(1.25));
1232 assert_eq!(block.heading_rate_dps(), Some(-0.75));
1233 }
1234
1235 #[test]
1236 fn test_ext_event_att_euler_nr_sv_dnu() {
1237 let mut data = vec![0u8; 42];
1238 data[12] = 255;
1239 data[18..22].copy_from_slice(&F32_DNU.to_le_bytes());
1240
1241 let header = header_for(block_ids::EXT_EVENT_ATT_EULER, data.len(), 13000, 2500);
1242 let block = ExtEventAttEulerBlock::parse(&header, &data).unwrap();
1243 assert_eq!(block.num_satellites_raw(), 255);
1244 assert_eq!(block.num_satellites_opt(), None);
1245 assert_eq!(block.num_satellites(), 0);
1246 assert_eq!(block.heading_deg(), None);
1247 }
1248}