1use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5use crate::types::SatelliteId;
6
7use super::block_ids;
8use super::dnu::{f32_or_none, f64_or_none, u16_or_none, I16_DNU, I8_DNU, U16_DNU};
9use super::SbfBlockParse;
10
11#[cfg(test)]
12use super::dnu::{F32_DNU, F64_DNU};
13
14fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] {
19 let end = bytes
20 .iter()
21 .rposition(|&byte| byte != 0)
22 .map(|idx| idx + 1)
23 .unwrap_or(0);
24 &bytes[..end]
25}
26
27fn format_ip_bytes(bytes: &[u8; 16]) -> String {
28 if bytes[0..12].iter().all(|&b| b == 0) {
29 format!("{}.{}.{}.{}", bytes[12], bytes[13], bytes[14], bytes[15])
30 } else {
31 bytes
32 .chunks(2)
33 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
34 .collect::<Vec<_>>()
35 .join(":")
36 }
37}
38
39#[derive(Debug, Clone)]
45pub struct AgcData {
46 pub frontend_id: u8,
48 pub gain_db: i8,
50 pub sample_var: u8,
52 pub blanking_stat: u8,
54}
55
56#[derive(Debug, Clone)]
60pub struct ReceiverStatusBlock {
61 tow_ms: u32,
62 wnc: u16,
63 pub cpu_load: u8,
65 pub ext_error: u8,
67 pub uptime_s: u32,
69 pub rx_state: u32,
71 pub rx_error: u32,
73 cmd_count: Option<u8>,
75 temperature_raw: Option<u8>,
77 pub agc_data: Vec<AgcData>,
79}
80
81impl ReceiverStatusBlock {
82 pub fn tow_seconds(&self) -> f64 {
83 self.tow_ms as f64 * 0.001
84 }
85 pub fn tow_ms(&self) -> u32 {
86 self.tow_ms
87 }
88 pub fn wnc(&self) -> u16 {
89 self.wnc
90 }
91
92 pub fn has_errors(&self) -> bool {
94 self.rx_error != 0 || self.ext_error != 0
95 }
96
97 pub fn cmd_count(&self) -> Option<u8> {
99 self.cmd_count
100 }
101
102 pub fn temperature_raw(&self) -> Option<u8> {
104 self.temperature_raw
105 }
106
107 pub fn temperature_celsius(&self) -> Option<i16> {
109 self.temperature_raw.and_then(|raw| {
110 if raw == 0 {
111 None
112 } else {
113 Some(raw as i16 - 100)
114 }
115 })
116 }
117
118 pub fn uptime_hms(&self) -> (u32, u8, u8) {
120 let hours = self.uptime_s / 3600;
121 let minutes = ((self.uptime_s % 3600) / 60) as u8;
122 let seconds = (self.uptime_s % 60) as u8;
123 (hours, minutes, seconds)
124 }
125}
126
127impl SbfBlockParse for ReceiverStatusBlock {
128 const BLOCK_ID: u16 = block_ids::RECEIVER_STATUS;
129
130 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
131 let min_len = if header.block_rev >= 1 { 30 } else { 26 };
132 if data.len() < min_len {
133 return Err(SbfError::ParseError("ReceiverStatus too short".into()));
134 }
135
136 let cpu_load = data[12];
146 let ext_error = data[13];
147 let uptime_s = u32::from_le_bytes(data[14..18].try_into().unwrap());
148 let rx_state = u32::from_le_bytes(data[18..22].try_into().unwrap());
149 let rx_error = u32::from_le_bytes(data[22..26].try_into().unwrap());
150
151 let mut cmd_count = None;
152 let mut temperature_raw = None;
153 let mut agc_data = Vec::new();
154
155 if header.block_rev >= 1 {
158 let n = data[26] as usize;
159 let sb_length = data[27] as usize;
160 cmd_count = Some(data[28]);
161 temperature_raw = Some(data[29]);
162
163 if n > 0 && sb_length < 4 {
164 return Err(SbfError::ParseError(
165 "ReceiverStatus SBLength too small".into(),
166 ));
167 }
168
169 if sb_length >= 4 {
170 let mut offset = 30;
171 for _ in 0..n {
172 if offset + sb_length > data.len() {
173 break;
174 }
175
176 agc_data.push(AgcData {
177 frontend_id: data[offset],
178 gain_db: data[offset + 1] as i8,
179 sample_var: data[offset + 2],
180 blanking_stat: data[offset + 3],
181 });
182
183 offset += sb_length;
184 }
185 }
186 } else if data.len() >= 28 {
187 let n = data[26] as usize;
190 let sb_length = data[27] as usize;
191
192 if n > 0 && sb_length < 4 {
193 return Err(SbfError::ParseError(
194 "ReceiverStatus SBLength too small".into(),
195 ));
196 }
197
198 if sb_length >= 4 {
199 let mut offset = 28;
200 for _ in 0..n {
201 if offset + sb_length > data.len() {
202 break;
203 }
204
205 agc_data.push(AgcData {
206 frontend_id: data[offset],
207 gain_db: data[offset + 1] as i8,
208 sample_var: data[offset + 2],
209 blanking_stat: data[offset + 3],
210 });
211
212 offset += sb_length;
213 }
214 }
215 }
216
217 Ok(Self {
218 tow_ms: header.tow_ms,
219 wnc: header.wnc,
220 cpu_load,
221 ext_error,
222 uptime_s,
223 rx_state,
224 rx_error,
225 cmd_count,
226 temperature_raw,
227 agc_data,
228 })
229 }
230}
231
232#[derive(Debug, Clone)]
238pub struct ChannelState {
239 pub antenna: u8,
240 pub tracking_status: u16,
241 pub pvt_status: u16,
242 pub pvt_info: u16,
243}
244
245#[derive(Debug, Clone)]
247pub struct ChannelSatInfo {
248 pub sat_id: SatelliteId,
250 pub freq_nr: u8,
252 azimuth_raw: u16,
254 pub rise_set: u8,
256 elevation_raw: i8,
258 pub health_status: u16,
260 pub states: Vec<ChannelState>,
262}
263
264impl ChannelSatInfo {
265 pub fn azimuth_deg(&self) -> f64 {
270 self.azimuth_deg_opt().unwrap_or(0.0)
271 }
272
273 pub fn azimuth_deg_opt(&self) -> Option<f64> {
275 if self.azimuth_raw == 511 {
276 None
277 } else {
278 Some(self.azimuth_raw as f64)
279 }
280 }
281
282 pub fn azimuth_raw(&self) -> u16 {
284 self.azimuth_raw
285 }
286
287 pub fn elevation_deg(&self) -> f64 {
292 self.elevation_deg_opt().unwrap_or(0.0)
293 }
294
295 pub fn elevation_deg_opt(&self) -> Option<f64> {
297 if self.elevation_raw == I8_DNU {
298 None
299 } else {
300 Some(self.elevation_raw as f64)
301 }
302 }
303
304 pub fn elevation_raw(&self) -> i8 {
306 self.elevation_raw
307 }
308
309 pub fn is_rising(&self) -> bool {
311 self.rise_set == 1
312 }
313
314 pub fn is_setting(&self) -> bool {
316 self.rise_set == 0
317 }
318
319 pub fn is_rise_set_unknown(&self) -> bool {
321 self.rise_set == 3
322 }
323}
324
325#[derive(Debug, Clone)]
329pub struct ChannelStatusBlock {
330 tow_ms: u32,
331 wnc: u16,
332 pub satellites: Vec<ChannelSatInfo>,
334}
335
336impl ChannelStatusBlock {
337 pub fn tow_seconds(&self) -> f64 {
338 self.tow_ms as f64 * 0.001
339 }
340 pub fn tow_ms(&self) -> u32 {
341 self.tow_ms
342 }
343 pub fn wnc(&self) -> u16 {
344 self.wnc
345 }
346
347 pub fn num_satellites(&self) -> usize {
349 self.satellites.len()
350 }
351}
352
353impl SbfBlockParse for ChannelStatusBlock {
354 const BLOCK_ID: u16 = block_ids::CHANNEL_STATUS;
355
356 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
357 if data.len() < 18 {
358 return Err(SbfError::ParseError("ChannelStatus too short".into()));
359 }
360
361 let n1 = data[12] as usize;
368 let sb1_length = data[13] as usize;
369 let sb2_length = data[14] as usize;
370
371 if sb1_length < 12 {
372 return Err(SbfError::ParseError(
373 "ChannelStatus SB1Length too small".into(),
374 ));
375 }
376
377 let mut satellites = Vec::new();
378 let mut offset = 18;
379
380 for _ in 0..n1 {
381 if offset + sb1_length > data.len() {
382 break;
383 }
384
385 let svid = data[offset];
397 let freq_nr = data[offset + 1];
398 let svid_full = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
399 let az_rise_set = u16::from_le_bytes([data[offset + 4], data[offset + 5]]);
400 let health_status = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
401 let elevation_raw = data[offset + 8] as i8;
402 let n2 = data[offset + 9] as usize;
403
404 let azimuth_raw = az_rise_set & 0x01FF;
405 let rise_set = ((az_rise_set >> 14) & 0x03) as u8;
406
407 offset += sb1_length;
408
409 let mut states = Vec::new();
412 for _ in 0..n2 {
413 if offset + sb2_length > data.len() {
414 break;
415 }
416
417 if sb2_length >= 8 {
418 states.push(ChannelState {
419 antenna: data[offset],
420 tracking_status: u16::from_le_bytes([data[offset + 2], data[offset + 3]]),
421 pvt_status: u16::from_le_bytes([data[offset + 4], data[offset + 5]]),
422 pvt_info: u16::from_le_bytes([data[offset + 6], data[offset + 7]]),
423 });
424 }
425
426 offset += sb2_length;
427 }
428
429 let sat_id = if svid != 0 {
430 SatelliteId::from_svid(svid)
431 } else if svid_full <= u8::MAX as u16 {
432 SatelliteId::from_svid(svid_full as u8)
433 } else {
434 None
435 };
436
437 if let Some(sat_id) = sat_id {
438 satellites.push(ChannelSatInfo {
439 sat_id,
440 freq_nr,
441 azimuth_raw,
442 rise_set,
443 elevation_raw,
444 health_status,
445 states,
446 });
447 }
448 }
449
450 Ok(Self {
451 tow_ms: header.tow_ms,
452 wnc: header.wnc,
453 satellites,
454 })
455 }
456}
457
458#[derive(Debug, Clone)]
464pub struct SatVisibilityInfo {
465 pub sat_id: SatelliteId,
467 pub freq_nr: u8,
469 azimuth_raw: u16,
471 elevation_raw: i16,
473 pub rise_set: u8,
475 pub satellite_info: u8,
477}
478
479impl SatVisibilityInfo {
480 pub fn azimuth_deg(&self) -> Option<f64> {
482 if self.azimuth_raw == 65535 {
483 None
484 } else {
485 Some(self.azimuth_raw as f64 * 0.01)
486 }
487 }
488
489 pub fn azimuth_raw(&self) -> u16 {
491 self.azimuth_raw
492 }
493
494 pub fn elevation_deg(&self) -> Option<f64> {
496 if self.elevation_raw == -32768 {
497 None
498 } else {
499 Some(self.elevation_raw as f64 * 0.01)
500 }
501 }
502
503 pub fn elevation_raw(&self) -> i16 {
505 self.elevation_raw
506 }
507
508 pub fn is_rising(&self) -> bool {
510 self.rise_set == 1
511 }
512
513 pub fn is_rise_set_unknown(&self) -> bool {
515 self.rise_set == 255
516 }
517
518 pub fn is_above_horizon(&self) -> bool {
520 self.elevation_deg().map(|e| e > 0.0).unwrap_or(false)
521 }
522}
523
524#[derive(Debug, Clone)]
528pub struct SatVisibilityBlock {
529 tow_ms: u32,
530 wnc: u16,
531 pub satellites: Vec<SatVisibilityInfo>,
533}
534
535impl SatVisibilityBlock {
536 pub fn tow_seconds(&self) -> f64 {
537 self.tow_ms as f64 * 0.001
538 }
539 pub fn tow_ms(&self) -> u32 {
540 self.tow_ms
541 }
542 pub fn wnc(&self) -> u16 {
543 self.wnc
544 }
545
546 pub fn num_satellites(&self) -> usize {
548 self.satellites.len()
549 }
550
551 pub fn above_elevation(&self, min_elevation_deg: f64) -> Vec<&SatVisibilityInfo> {
553 self.satellites
554 .iter()
555 .filter(|s| {
556 s.elevation_deg()
557 .map(|e| e >= min_elevation_deg)
558 .unwrap_or(false)
559 })
560 .collect()
561 }
562
563 pub fn get_satellite(&self, sat_id: &SatelliteId) -> Option<&SatVisibilityInfo> {
565 self.satellites.iter().find(|s| &s.sat_id == sat_id)
566 }
567}
568
569impl SbfBlockParse for SatVisibilityBlock {
570 const BLOCK_ID: u16 = block_ids::SAT_VISIBILITY;
571
572 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
573 if data.len() < 14 {
574 return Err(SbfError::ParseError("SatVisibility too short".into()));
575 }
576
577 let n = data[12] as usize;
582 let sb_length = data[13] as usize;
583
584 if sb_length < 8 {
585 return Err(SbfError::ParseError(
586 "SatVisibility SBLength too small".into(),
587 ));
588 }
589
590 let mut satellites = Vec::new();
591 let mut offset = 14;
592
593 for _ in 0..n {
594 if offset + sb_length > data.len() {
595 break;
596 }
597
598 let svid = data[offset];
607 let freq_nr = data[offset + 1];
608 let azimuth_raw = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
609 let elevation_raw = i16::from_le_bytes([data[offset + 4], data[offset + 5]]);
610 let rise_set = data[offset + 6];
611 let satellite_info = data[offset + 7];
612
613 if let Some(sat_id) = SatelliteId::from_svid(svid) {
614 satellites.push(SatVisibilityInfo {
615 sat_id,
616 freq_nr,
617 azimuth_raw,
618 elevation_raw,
619 rise_set,
620 satellite_info,
621 });
622 }
623
624 offset += sb_length;
625 }
626
627 Ok(Self {
628 tow_ms: header.tow_ms,
629 wnc: header.wnc,
630 satellites,
631 })
632 }
633}
634
635#[derive(Debug, Clone)]
643pub struct QualityIndBlock {
644 tow_ms: u32,
645 wnc: u16,
646 pub indicators: Vec<u16>,
648}
649
650impl QualityIndBlock {
651 pub fn tow_seconds(&self) -> f64 {
652 self.tow_ms as f64 * 0.001
653 }
654 pub fn tow_ms(&self) -> u32 {
655 self.tow_ms
656 }
657 pub fn wnc(&self) -> u16 {
658 self.wnc
659 }
660
661 pub fn num_indicators(&self) -> usize {
662 self.indicators.len()
663 }
664}
665
666impl SbfBlockParse for QualityIndBlock {
667 const BLOCK_ID: u16 = block_ids::QUALITY_IND;
668
669 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
670 if data.len() < 14 {
671 return Err(SbfError::ParseError("QualityInd too short".into()));
672 }
673
674 let n = data[12] as usize;
679 if n > 40 {
680 return Err(SbfError::ParseError(
681 "QualityInd too many indicators".into(),
682 ));
683 }
684
685 let required_len = 14 + (n * 2);
686 if data.len() < required_len {
687 return Err(SbfError::ParseError("QualityInd too short".into()));
688 }
689
690 let mut indicators = Vec::with_capacity(n);
691 let mut offset = 14;
692 for _ in 0..n {
693 let value = u16::from_le_bytes([data[offset], data[offset + 1]]);
694 indicators.push(value);
695 offset += 2;
696 }
697
698 Ok(Self {
699 tow_ms: header.tow_ms,
700 wnc: header.wnc,
701 indicators,
702 })
703 }
704}
705
706#[derive(Debug, Clone)]
712pub struct InputLinkStats {
713 pub connection_descriptor: u8,
715 pub link_type: u8,
717 age_last_message_raw: u16,
719 pub bytes_received: u32,
721 pub bytes_accepted: u32,
723 pub messages_received: u32,
725 pub messages_accepted: u32,
727}
728
729impl InputLinkStats {
730 pub fn age_last_message_s(&self) -> Option<u16> {
731 u16_or_none(self.age_last_message_raw)
732 }
733
734 pub fn age_last_message_raw(&self) -> u16 {
735 self.age_last_message_raw
736 }
737}
738
739#[derive(Debug, Clone)]
741pub struct InputLinkBlock {
742 tow_ms: u32,
743 wnc: u16,
744 pub inputs: Vec<InputLinkStats>,
746}
747
748impl InputLinkBlock {
749 pub fn tow_seconds(&self) -> f64 {
750 self.tow_ms as f64 * 0.001
751 }
752 pub fn tow_ms(&self) -> u32 {
753 self.tow_ms
754 }
755 pub fn wnc(&self) -> u16 {
756 self.wnc
757 }
758
759 pub fn num_links(&self) -> usize {
760 self.inputs.len()
761 }
762}
763
764impl SbfBlockParse for InputLinkBlock {
765 const BLOCK_ID: u16 = block_ids::INPUT_LINK;
766
767 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
768 if data.len() < 14 {
769 return Err(SbfError::ParseError("InputLink too short".into()));
770 }
771
772 let n = data[12] as usize;
773 let sb_length = data[13] as usize;
774
775 if sb_length < 20 {
776 return Err(SbfError::ParseError("InputLink SBLength too small".into()));
777 }
778
779 let mut inputs = Vec::new();
780 let mut offset = 14;
781
782 for _ in 0..n {
783 if offset + sb_length > data.len() {
784 break;
785 }
786
787 let connection_descriptor = data[offset];
788 let link_type = data[offset + 1];
789 let age_last_message_raw = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
790 let bytes_received =
791 u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
792 let bytes_accepted =
793 u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
794 let messages_received =
795 u32::from_le_bytes(data[offset + 12..offset + 16].try_into().unwrap());
796 let messages_accepted =
797 u32::from_le_bytes(data[offset + 16..offset + 20].try_into().unwrap());
798
799 inputs.push(InputLinkStats {
800 connection_descriptor,
801 link_type,
802 age_last_message_raw,
803 bytes_received,
804 bytes_accepted,
805 messages_received,
806 messages_accepted,
807 });
808
809 offset += sb_length;
810 }
811
812 Ok(Self {
813 tow_ms: header.tow_ms,
814 wnc: header.wnc,
815 inputs,
816 })
817 }
818}
819
820#[derive(Debug, Clone)]
826pub struct OutputType {
827 pub output_type: u8,
829 pub percentage: u8,
831}
832
833#[derive(Debug, Clone)]
835pub struct OutputLinkStats {
836 pub connection_descriptor: u8,
838 allowed_rate_raw: u16,
840 pub bytes_produced: u32,
842 pub bytes_sent: u32,
844 pub nr_clients: u8,
846 pub output_types: Vec<OutputType>,
848}
849
850impl OutputLinkStats {
851 pub fn allowed_rate_kbytes_per_s(&self) -> u16 {
853 self.allowed_rate_raw
854 }
855
856 pub fn allowed_rate_bytes_per_s(&self) -> u32 {
858 self.allowed_rate_raw as u32 * 1000
859 }
860
861 pub fn allowed_rate_bps(&self) -> Option<u16> {
866 Some(self.allowed_rate_raw)
867 }
868
869 pub fn allowed_rate_raw(&self) -> u16 {
870 self.allowed_rate_raw
871 }
872}
873
874#[derive(Debug, Clone)]
876pub struct OutputLinkBlock {
877 tow_ms: u32,
878 wnc: u16,
879 pub outputs: Vec<OutputLinkStats>,
881}
882
883impl OutputLinkBlock {
884 pub fn tow_seconds(&self) -> f64 {
885 self.tow_ms as f64 * 0.001
886 }
887 pub fn tow_ms(&self) -> u32 {
888 self.tow_ms
889 }
890 pub fn wnc(&self) -> u16 {
891 self.wnc
892 }
893
894 pub fn num_links(&self) -> usize {
895 self.outputs.len()
896 }
897}
898
899impl SbfBlockParse for OutputLinkBlock {
900 const BLOCK_ID: u16 = block_ids::OUTPUT_LINK;
901
902 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
903 if data.len() < 18 {
904 return Err(SbfError::ParseError("OutputLink too short".into()));
905 }
906
907 let n1 = data[12] as usize;
908 let sb1_length = data[13] as usize;
909 let sb2_length = data[14] as usize;
910
911 if sb1_length < 13 {
912 return Err(SbfError::ParseError(
913 "OutputLink SB1Length too small".into(),
914 ));
915 }
916
917 let mut outputs = Vec::new();
918 let mut offset = 18;
920
921 for _ in 0..n1 {
922 if offset + sb1_length > data.len() {
923 break;
924 }
925
926 let connection_descriptor = data[offset];
927 let n2 = data[offset + 1] as usize;
928 let allowed_rate_raw = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
929 let bytes_produced =
930 u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
931 let bytes_sent = u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
932 let nr_clients = data[offset + 12];
933
934 offset += sb1_length;
935
936 let mut output_types = Vec::new();
937 if sb2_length >= 2 {
938 for _ in 0..n2 {
939 if offset + sb2_length > data.len() {
940 break;
941 }
942
943 output_types.push(OutputType {
944 output_type: data[offset],
945 percentage: data[offset + 1],
946 });
947
948 offset += sb2_length;
949 }
950 }
951
952 outputs.push(OutputLinkStats {
953 connection_descriptor,
954 allowed_rate_raw,
955 bytes_produced,
956 bytes_sent,
957 nr_clients,
958 output_types,
959 });
960 }
961
962 Ok(Self {
963 tow_ms: header.tow_ms,
964 wnc: header.wnc,
965 outputs,
966 })
967 }
968}
969
970#[derive(Debug, Clone)]
978pub struct IpStatusBlock {
979 tow_ms: u32,
980 wnc: u16,
981 pub mac_address: [u8; 6],
983 pub ip_address: [u8; 16],
985 pub gateway: [u8; 16],
987 pub netmask_prefix: u8,
989}
990
991impl IpStatusBlock {
992 pub fn tow_seconds(&self) -> f64 {
993 self.tow_ms as f64 * 0.001
994 }
995 pub fn tow_ms(&self) -> u32 {
996 self.tow_ms
997 }
998 pub fn wnc(&self) -> u16 {
999 self.wnc
1000 }
1001
1002 pub fn mac_address_string(&self) -> String {
1004 self.mac_address
1005 .iter()
1006 .map(|b| format!("{:02X}", b))
1007 .collect::<Vec<_>>()
1008 .join(":")
1009 }
1010
1011 pub fn ip_address_string(&self) -> String {
1013 if self.ip_address[0..12].iter().all(|&b| b == 0) {
1014 format!(
1015 "{}.{}.{}.{}",
1016 self.ip_address[12], self.ip_address[13], self.ip_address[14], self.ip_address[15]
1017 )
1018 } else {
1019 self.ip_address
1021 .chunks(2)
1022 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
1023 .collect::<Vec<_>>()
1024 .join(":")
1025 }
1026 }
1027
1028 pub fn gateway_string(&self) -> String {
1030 if self.gateway[0..12].iter().all(|&b| b == 0) {
1031 format!(
1032 "{}.{}.{}.{}",
1033 self.gateway[12], self.gateway[13], self.gateway[14], self.gateway[15]
1034 )
1035 } else {
1036 self.gateway
1037 .chunks(2)
1038 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
1039 .collect::<Vec<_>>()
1040 .join(":")
1041 }
1042 }
1043}
1044
1045impl SbfBlockParse for IpStatusBlock {
1046 const BLOCK_ID: u16 = block_ids::IP_STATUS;
1047
1048 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1049 const MIN_LEN: usize = 51; if data.len() < MIN_LEN {
1051 return Err(SbfError::ParseError("IPStatus too short".into()));
1052 }
1053
1054 let mut mac_address = [0u8; 6];
1055 mac_address.copy_from_slice(&data[12..18]);
1056 let mut ip_address = [0u8; 16];
1057 ip_address.copy_from_slice(&data[18..34]);
1058 let mut gateway = [0u8; 16];
1059 gateway.copy_from_slice(&data[34..50]);
1060 let netmask_prefix = data[50];
1061
1062 Ok(Self {
1063 tow_ms: header.tow_ms,
1064 wnc: header.wnc,
1065 mac_address,
1066 ip_address,
1067 gateway,
1068 netmask_prefix,
1069 })
1070 }
1071}
1072
1073#[derive(Debug, Clone)]
1079pub struct LBandTrackerData {
1080 pub frequency_hz: u32,
1082 pub baudrate: u16,
1084 pub service_id: u16,
1086 freq_offset_hz_raw: f32,
1087 cn0_raw: u16,
1088 avg_power_raw: i16,
1089 agc_gain_db_raw: i8,
1090 pub mode: u8,
1092 pub status: u8,
1094 pub svid: Option<u8>,
1096 pub lock_time_s: Option<u16>,
1098 pub source: Option<u8>,
1100}
1101
1102impl LBandTrackerData {
1103 pub fn freq_offset_hz(&self) -> Option<f32> {
1105 f32_or_none(self.freq_offset_hz_raw)
1106 }
1107
1108 pub fn freq_offset_hz_raw(&self) -> f32 {
1109 self.freq_offset_hz_raw
1110 }
1111
1112 pub fn cn0_dbhz(&self) -> Option<f32> {
1114 if self.cn0_raw == 0 {
1115 None
1116 } else {
1117 Some(self.cn0_raw as f32 * 0.01)
1118 }
1119 }
1120
1121 pub fn cn0_raw(&self) -> u16 {
1122 self.cn0_raw
1123 }
1124
1125 pub fn avg_power_db(&self) -> Option<f32> {
1127 if self.avg_power_raw == I16_DNU {
1128 None
1129 } else {
1130 Some(self.avg_power_raw as f32 * 0.01)
1131 }
1132 }
1133
1134 pub fn avg_power_raw(&self) -> i16 {
1135 self.avg_power_raw
1136 }
1137
1138 pub fn agc_gain_db(&self) -> Option<i8> {
1140 if self.agc_gain_db_raw == I8_DNU {
1141 None
1142 } else {
1143 Some(self.agc_gain_db_raw)
1144 }
1145 }
1146
1147 pub fn agc_gain_db_raw(&self) -> i8 {
1148 self.agc_gain_db_raw
1149 }
1150}
1151
1152#[derive(Debug, Clone)]
1156pub struct LBandTrackerStatusBlock {
1157 tow_ms: u32,
1158 wnc: u16,
1159 pub n: u8,
1161 pub sb_length: u8,
1163 pub trackers: Vec<LBandTrackerData>,
1165}
1166
1167impl LBandTrackerStatusBlock {
1168 pub fn tow_seconds(&self) -> f64 {
1169 self.tow_ms as f64 * 0.001
1170 }
1171 pub fn tow_ms(&self) -> u32 {
1172 self.tow_ms
1173 }
1174 pub fn wnc(&self) -> u16 {
1175 self.wnc
1176 }
1177 pub fn num_trackers(&self) -> usize {
1178 self.trackers.len()
1179 }
1180}
1181
1182impl SbfBlockParse for LBandTrackerStatusBlock {
1183 const BLOCK_ID: u16 = block_ids::LBAND_TRACKER_STATUS;
1184
1185 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1186 let block_len = header.length as usize;
1187 let data_len = block_len.saturating_sub(2);
1188 if data_len < 14 || data.len() < data_len {
1189 return Err(SbfError::ParseError("LBandTrackerStatus too short".into()));
1190 }
1191
1192 let n = data[12] as usize;
1193 let sb_length = data[13] as usize;
1194 let required_sb_len = 19
1195 + if header.block_rev >= 2 { 1 } else { 0 }
1196 + if header.block_rev >= 1 { 2 } else { 0 }
1197 + if header.block_rev >= 3 { 1 } else { 0 };
1198 if n > 0 && sb_length < required_sb_len {
1199 return Err(SbfError::ParseError(
1200 "LBandTrackerStatus SBLength too small".into(),
1201 ));
1202 }
1203
1204 let mut trackers = Vec::with_capacity(n);
1205 let mut offset = 14usize;
1206
1207 for _ in 0..n {
1208 if offset + sb_length > data_len {
1209 break;
1210 }
1211
1212 let entry = &data[offset..offset + sb_length];
1213 let frequency_hz = u32::from_le_bytes(entry[0..4].try_into().unwrap());
1214 let baudrate = u16::from_le_bytes(entry[4..6].try_into().unwrap());
1215 let service_id = u16::from_le_bytes(entry[6..8].try_into().unwrap());
1216 let freq_offset_hz_raw = f32::from_le_bytes(entry[8..12].try_into().unwrap());
1217 let cn0_raw = u16::from_le_bytes(entry[12..14].try_into().unwrap());
1218 let avg_power_raw = i16::from_le_bytes(entry[14..16].try_into().unwrap());
1219 let agc_gain_db_raw = entry[16] as i8;
1220 let mode = entry[17];
1221 let status = entry[18];
1222
1223 let mut cursor = 19usize;
1224 let svid = if header.block_rev >= 2 {
1225 let value = entry[cursor];
1226 cursor += 1;
1227 Some(value)
1228 } else {
1229 None
1230 };
1231 let lock_time_s = if header.block_rev >= 1 {
1232 let value = u16::from_le_bytes(entry[cursor..cursor + 2].try_into().unwrap());
1233 cursor += 2;
1234 Some(value)
1235 } else {
1236 None
1237 };
1238 let source = if header.block_rev >= 3 {
1239 Some(entry[cursor])
1240 } else {
1241 None
1242 };
1243
1244 trackers.push(LBandTrackerData {
1245 frequency_hz,
1246 baudrate,
1247 service_id,
1248 freq_offset_hz_raw,
1249 cn0_raw,
1250 avg_power_raw,
1251 agc_gain_db_raw,
1252 mode,
1253 status,
1254 svid,
1255 lock_time_s,
1256 source,
1257 });
1258
1259 offset += sb_length;
1260 }
1261
1262 Ok(Self {
1263 tow_ms: header.tow_ms,
1264 wnc: header.wnc,
1265 n: n as u8,
1266 sb_length: sb_length as u8,
1267 trackers,
1268 })
1269 }
1270}
1271
1272fn field_text(bytes: &[u8]) -> String {
1277 String::from_utf8_lossy(trim_trailing_nuls(bytes)).into_owned()
1278}
1279
1280#[derive(Debug, Clone)]
1284pub struct ReceiverSetupBlock {
1285 tow_ms: u32,
1286 wnc: u16,
1287 marker_name: Vec<u8>,
1288 marker_number: Vec<u8>,
1289 observer: Vec<u8>,
1290 agency: Vec<u8>,
1291 rx_serial_number: Vec<u8>,
1292 rx_name: Vec<u8>,
1293 rx_version: Vec<u8>,
1294 ant_serial_nbr: Vec<u8>,
1295 ant_type: Vec<u8>,
1296 delta_h_m: f32,
1297 delta_e_m: f32,
1298 delta_n_m: f32,
1299 marker_type: Option<Vec<u8>>,
1300 gnss_fw_version: Option<Vec<u8>>,
1301 product_name: Option<Vec<u8>>,
1302 latitude_rad: Option<f64>,
1303 longitude_rad: Option<f64>,
1304 height_m: Option<f32>,
1305 station_code: Option<Vec<u8>>,
1306 monument_idx: Option<u8>,
1307 receiver_idx: Option<u8>,
1308 country_code: Option<Vec<u8>>,
1309}
1310
1311impl ReceiverSetupBlock {
1312 pub fn tow_seconds(&self) -> f64 {
1313 self.tow_ms as f64 * 0.001
1314 }
1315 pub fn tow_ms(&self) -> u32 {
1316 self.tow_ms
1317 }
1318 pub fn wnc(&self) -> u16 {
1319 self.wnc
1320 }
1321
1322 pub fn marker_name(&self) -> &[u8] {
1323 &self.marker_name
1324 }
1325 pub fn marker_name_lossy(&self) -> String {
1326 field_text(&self.marker_name)
1327 }
1328
1329 pub fn marker_number(&self) -> &[u8] {
1330 &self.marker_number
1331 }
1332 pub fn marker_number_lossy(&self) -> String {
1333 field_text(&self.marker_number)
1334 }
1335
1336 pub fn observer(&self) -> &[u8] {
1337 &self.observer
1338 }
1339 pub fn observer_lossy(&self) -> String {
1340 field_text(&self.observer)
1341 }
1342
1343 pub fn agency(&self) -> &[u8] {
1344 &self.agency
1345 }
1346 pub fn agency_lossy(&self) -> String {
1347 field_text(&self.agency)
1348 }
1349
1350 pub fn rx_serial_number(&self) -> &[u8] {
1351 &self.rx_serial_number
1352 }
1353 pub fn rx_serial_number_lossy(&self) -> String {
1354 field_text(&self.rx_serial_number)
1355 }
1356
1357 pub fn rx_name(&self) -> &[u8] {
1358 &self.rx_name
1359 }
1360 pub fn rx_name_lossy(&self) -> String {
1361 field_text(&self.rx_name)
1362 }
1363
1364 pub fn rx_version(&self) -> &[u8] {
1365 &self.rx_version
1366 }
1367 pub fn rx_version_lossy(&self) -> String {
1368 field_text(&self.rx_version)
1369 }
1370
1371 pub fn ant_serial_number(&self) -> &[u8] {
1372 &self.ant_serial_nbr
1373 }
1374 pub fn ant_serial_number_lossy(&self) -> String {
1375 field_text(&self.ant_serial_nbr)
1376 }
1377
1378 pub fn ant_type(&self) -> &[u8] {
1379 &self.ant_type
1380 }
1381 pub fn ant_type_lossy(&self) -> String {
1382 field_text(&self.ant_type)
1383 }
1384
1385 pub fn delta_h_m(&self) -> f32 {
1386 self.delta_h_m
1387 }
1388 pub fn delta_e_m(&self) -> f32 {
1389 self.delta_e_m
1390 }
1391 pub fn delta_n_m(&self) -> f32 {
1392 self.delta_n_m
1393 }
1394
1395 pub fn marker_type_lossy(&self) -> Option<String> {
1396 self.marker_type.as_deref().map(field_text)
1397 }
1398 pub fn gnss_fw_version_lossy(&self) -> Option<String> {
1399 self.gnss_fw_version.as_deref().map(field_text)
1400 }
1401 pub fn product_name_lossy(&self) -> Option<String> {
1402 self.product_name.as_deref().map(field_text)
1403 }
1404
1405 pub fn latitude_rad(&self) -> Option<f64> {
1406 self.latitude_rad.and_then(f64_or_none)
1407 }
1408 pub fn longitude_rad(&self) -> Option<f64> {
1409 self.longitude_rad.and_then(f64_or_none)
1410 }
1411 pub fn latitude_deg(&self) -> Option<f64> {
1412 self.latitude_rad().map(f64::to_degrees)
1413 }
1414 pub fn longitude_deg(&self) -> Option<f64> {
1415 self.longitude_rad().map(f64::to_degrees)
1416 }
1417 pub fn height_m(&self) -> Option<f32> {
1418 self.height_m.and_then(f32_or_none)
1419 }
1420
1421 pub fn station_code_lossy(&self) -> Option<String> {
1422 self.station_code.as_deref().map(field_text)
1423 }
1424 pub fn monument_idx(&self) -> Option<u8> {
1425 self.monument_idx
1426 }
1427 pub fn receiver_idx(&self) -> Option<u8> {
1428 self.receiver_idx
1429 }
1430 pub fn country_code_lossy(&self) -> Option<String> {
1431 self.country_code.as_deref().map(field_text)
1432 }
1433}
1434
1435impl SbfBlockParse for ReceiverSetupBlock {
1436 const BLOCK_ID: u16 = block_ids::RECEIVER_SETUP;
1437
1438 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1439 const REV0_LEN: usize = 266;
1440 const REV1_LEN: usize = 286;
1441 const REV2_LEN: usize = 326;
1442 const REV3_LEN: usize = 386;
1443 const REV4_LEN: usize = 422;
1444
1445 let block_len = header.length as usize;
1446 let data_len = block_len.saturating_sub(2);
1447 if data.len() < data_len {
1448 return Err(SbfError::ParseError("ReceiverSetup too short".into()));
1449 }
1450
1451 let required_len = match header.block_rev {
1452 0 => REV0_LEN,
1453 1 => REV1_LEN,
1454 2 => REV2_LEN,
1455 3 => REV3_LEN,
1456 _ => REV4_LEN,
1457 };
1458 if data_len < required_len {
1459 return Err(SbfError::ParseError("ReceiverSetup too short".into()));
1460 }
1461
1462 let marker_name = data[14..74].to_vec();
1463 let marker_number = data[74..94].to_vec();
1464 let observer = data[94..114].to_vec();
1465 let agency = data[114..154].to_vec();
1466 let rx_serial_number = data[154..174].to_vec();
1467 let rx_name = data[174..194].to_vec();
1468 let rx_version = data[194..214].to_vec();
1469 let ant_serial_nbr = data[214..234].to_vec();
1470 let ant_type = data[234..254].to_vec();
1471 let delta_h_m = f32::from_le_bytes(data[254..258].try_into().unwrap());
1472 let delta_e_m = f32::from_le_bytes(data[258..262].try_into().unwrap());
1473 let delta_n_m = f32::from_le_bytes(data[262..266].try_into().unwrap());
1474
1475 let marker_type = if header.block_rev >= 1 {
1476 Some(data[266..286].to_vec())
1477 } else {
1478 None
1479 };
1480 let gnss_fw_version = if header.block_rev >= 2 {
1481 Some(data[286..326].to_vec())
1482 } else {
1483 None
1484 };
1485 let (product_name, latitude_rad, longitude_rad, height_m) = if header.block_rev >= 3 {
1486 (
1487 Some(data[326..366].to_vec()),
1488 Some(f64::from_le_bytes(data[366..374].try_into().unwrap())),
1489 Some(f64::from_le_bytes(data[374..382].try_into().unwrap())),
1490 Some(f32::from_le_bytes(data[382..386].try_into().unwrap())),
1491 )
1492 } else {
1493 (None, None, None, None)
1494 };
1495 let (station_code, monument_idx, receiver_idx, country_code) = if header.block_rev >= 4 {
1496 (
1497 Some(data[386..396].to_vec()),
1498 Some(data[396]),
1499 Some(data[397]),
1500 Some(data[398..401].to_vec()),
1501 )
1502 } else {
1503 (None, None, None, None)
1504 };
1505
1506 Ok(Self {
1507 tow_ms: header.tow_ms,
1508 wnc: header.wnc,
1509 marker_name,
1510 marker_number,
1511 observer,
1512 agency,
1513 rx_serial_number,
1514 rx_name,
1515 rx_version,
1516 ant_serial_nbr,
1517 ant_type,
1518 delta_h_m,
1519 delta_e_m,
1520 delta_n_m,
1521 marker_type,
1522 gnss_fw_version,
1523 product_name,
1524 latitude_rad,
1525 longitude_rad,
1526 height_m,
1527 station_code,
1528 monument_idx,
1529 receiver_idx,
1530 country_code,
1531 })
1532 }
1533}
1534
1535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1541pub struct BBSample {
1542 raw: u16,
1543}
1544
1545impl BBSample {
1546 pub fn raw(&self) -> u16 {
1547 self.raw
1548 }
1549
1550 pub fn i(&self) -> i8 {
1552 (self.raw >> 8) as u8 as i8
1553 }
1554
1555 pub fn q(&self) -> i8 {
1557 self.raw as u8 as i8
1558 }
1559}
1560
1561#[derive(Debug, Clone)]
1565pub struct BBSamplesBlock {
1566 tow_ms: u32,
1567 wnc: u16,
1568 n: u16,
1569 info: u8,
1570 sample_freq_hz: u32,
1571 lo_freq_hz: u32,
1572 samples: Vec<BBSample>,
1573 tow_delta_s: f32,
1574}
1575
1576impl BBSamplesBlock {
1577 pub fn tow_seconds(&self) -> f64 {
1578 self.tow_ms as f64 * 0.001
1579 }
1580 pub fn tow_ms(&self) -> u32 {
1581 self.tow_ms
1582 }
1583 pub fn wnc(&self) -> u16 {
1584 self.wnc
1585 }
1586
1587 pub fn num_samples(&self) -> u16 {
1588 self.n
1589 }
1590 pub fn info_raw(&self) -> u8 {
1591 self.info
1592 }
1593 pub fn antenna_id(&self) -> u8 {
1594 self.info & 0x07
1595 }
1596 pub fn sample_freq_hz(&self) -> u32 {
1597 self.sample_freq_hz
1598 }
1599 pub fn lo_freq_hz(&self) -> u32 {
1600 self.lo_freq_hz
1601 }
1602 pub fn samples(&self) -> &[BBSample] {
1603 &self.samples
1604 }
1605 pub fn sample_iq(&self, index: usize) -> Option<(i8, i8)> {
1606 self.samples
1607 .get(index)
1608 .map(|sample| (sample.i(), sample.q()))
1609 }
1610 pub fn tow_delta_seconds(&self) -> Option<f32> {
1611 f32_or_none(self.tow_delta_s)
1612 }
1613 pub fn first_sample_time_seconds(&self) -> Option<f64> {
1614 self.tow_delta_seconds()
1615 .map(|tow_delta_s| self.tow_seconds() + tow_delta_s as f64)
1616 }
1617}
1618
1619impl SbfBlockParse for BBSamplesBlock {
1620 const BLOCK_ID: u16 = block_ids::BB_SAMPLES;
1621
1622 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1623 let block_len = header.length as usize;
1624 let data_len = block_len.saturating_sub(2);
1625 if data_len < 30 || data.len() < data_len {
1626 return Err(SbfError::ParseError("BBSamples too short".into()));
1627 }
1628
1629 let n = u16::from_le_bytes(data[12..14].try_into().unwrap()) as usize;
1630 let info = data[14];
1631 let sample_freq_hz = u32::from_le_bytes(data[18..22].try_into().unwrap());
1632 let lo_freq_hz = u32::from_le_bytes(data[22..26].try_into().unwrap());
1633
1634 let samples_len = n
1635 .checked_mul(2)
1636 .ok_or_else(|| SbfError::ParseError("BBSamples sample count overflow".into()))?;
1637 let samples_end = 26usize
1638 .checked_add(samples_len)
1639 .ok_or_else(|| SbfError::ParseError("BBSamples sample data overflow".into()))?;
1640 let tow_delta_end = samples_end
1641 .checked_add(4)
1642 .ok_or_else(|| SbfError::ParseError("BBSamples TOWDelta overflow".into()))?;
1643 if tow_delta_end > data_len {
1644 return Err(SbfError::ParseError(
1645 "BBSamples sample data exceeds block length".into(),
1646 ));
1647 }
1648
1649 let mut samples = Vec::with_capacity(n);
1650 for chunk in data[26..samples_end].chunks_exact(2) {
1651 samples.push(BBSample {
1652 raw: u16::from_le_bytes(chunk.try_into().unwrap()),
1653 });
1654 }
1655 let tow_delta_s = f32::from_le_bytes(data[samples_end..tow_delta_end].try_into().unwrap());
1656
1657 Ok(Self {
1658 tow_ms: header.tow_ms,
1659 wnc: header.wnc,
1660 n: n as u16,
1661 info,
1662 sample_freq_hz,
1663 lo_freq_hz,
1664 samples,
1665 tow_delta_s,
1666 })
1667 }
1668}
1669
1670#[derive(Debug, Clone)]
1678pub struct ASCIIInBlock {
1679 tow_ms: u32,
1680 wnc: u16,
1681 cd: u8,
1682 string_len: u16,
1683 sensor_model: Vec<u8>,
1684 sensor_type: Vec<u8>,
1685 ascii_string: Vec<u8>,
1686}
1687
1688impl ASCIIInBlock {
1689 pub fn tow_seconds(&self) -> f64 {
1690 self.tow_ms as f64 * 0.001
1691 }
1692 pub fn tow_ms(&self) -> u32 {
1693 self.tow_ms
1694 }
1695 pub fn wnc(&self) -> u16 {
1696 self.wnc
1697 }
1698
1699 pub fn connection_descriptor(&self) -> u8 {
1700 self.cd
1701 }
1702 pub fn string_len(&self) -> u16 {
1703 self.string_len
1704 }
1705 pub fn sensor_model(&self) -> &[u8] {
1706 &self.sensor_model
1707 }
1708 pub fn sensor_model_lossy(&self) -> String {
1709 field_text(&self.sensor_model)
1710 }
1711 pub fn sensor_type(&self) -> &[u8] {
1712 &self.sensor_type
1713 }
1714 pub fn sensor_type_lossy(&self) -> String {
1715 field_text(&self.sensor_type)
1716 }
1717 pub fn ascii_string(&self) -> &[u8] {
1718 &self.ascii_string
1719 }
1720 pub fn ascii_text_lossy(&self) -> String {
1721 String::from_utf8_lossy(&self.ascii_string).into_owned()
1722 }
1723}
1724
1725impl SbfBlockParse for ASCIIInBlock {
1726 const BLOCK_ID: u16 = block_ids::ASCII_IN;
1727
1728 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1729 let block_len = header.length as usize;
1730 let data_len = block_len.saturating_sub(2);
1731 if data_len < 78 || data.len() < data_len {
1732 return Err(SbfError::ParseError("ASCIIIn too short".into()));
1733 }
1734
1735 let cd = data[12];
1736 let string_len = u16::from_le_bytes(data[16..18].try_into().unwrap()) as usize;
1737 let sensor_model = data[18..38].to_vec();
1738 let sensor_type = data[38..58].to_vec();
1739 let string_start = 78usize;
1740 let string_end = string_start
1741 .checked_add(string_len)
1742 .ok_or_else(|| SbfError::ParseError("ASCIIIn string length overflow".into()))?;
1743 if string_end > data_len {
1744 return Err(SbfError::ParseError("ASCIIIn string exceeds block".into()));
1745 }
1746
1747 Ok(Self {
1748 tow_ms: header.tow_ms,
1749 wnc: header.wnc,
1750 cd,
1751 string_len: string_len as u16,
1752 sensor_model,
1753 sensor_type,
1754 ascii_string: data[string_start..string_end].to_vec(),
1755 })
1756 }
1757}
1758
1759#[derive(Debug, Clone)]
1767pub struct CommandsBlock {
1768 tow_ms: u32,
1769 wnc: u16,
1770 cmd_data: Vec<u8>,
1771}
1772
1773impl CommandsBlock {
1774 pub fn tow_seconds(&self) -> f64 {
1775 self.tow_ms as f64 * 0.001
1776 }
1777 pub fn tow_ms(&self) -> u32 {
1778 self.tow_ms
1779 }
1780 pub fn wnc(&self) -> u16 {
1781 self.wnc
1782 }
1783 pub fn cmd_data(&self) -> &[u8] {
1784 &self.cmd_data
1785 }
1786
1787 pub fn cmd_text_lossy(&self) -> String {
1789 String::from_utf8_lossy(trim_trailing_nuls(&self.cmd_data)).into_owned()
1790 }
1791}
1792
1793impl SbfBlockParse for CommandsBlock {
1794 const BLOCK_ID: u16 = block_ids::COMMANDS;
1795
1796 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1797 let block_len = header.length as usize;
1798 let data_len = block_len.saturating_sub(2);
1799 if data_len < 14 || data.len() < data_len {
1800 return Err(SbfError::ParseError("Commands too short".into()));
1801 }
1802
1803 Ok(Self {
1804 tow_ms: header.tow_ms,
1805 wnc: header.wnc,
1806 cmd_data: data[14..data_len].to_vec(),
1807 })
1808 }
1809}
1810
1811#[derive(Debug, Clone)]
1817pub struct NtripConnectionSlot {
1818 pub cd_index: u8,
1819 pub status: u8,
1820 pub error_code: u8,
1821}
1822
1823fn parse_ntrip_connection_status(data: &[u8]) -> SbfResult<(u8, u8, Vec<NtripConnectionSlot>)> {
1824 const MIN_HEADER: usize = 14;
1825 const MIN_SB_LENGTH: usize = 4;
1826 if data.len() < MIN_HEADER {
1827 return Err(SbfError::ParseError("NTRIP status too short".into()));
1828 }
1829 let n = data[12];
1830 let sb_length = data[13];
1831 let sb_length_usize = sb_length as usize;
1832 if sb_length_usize < MIN_SB_LENGTH {
1833 return Err(SbfError::ParseError(
1834 "NTRIP status SBLength too small".into(),
1835 ));
1836 }
1837
1838 let required_len = MIN_HEADER + n as usize * sb_length_usize;
1839 if required_len > data.len() {
1840 return Err(SbfError::ParseError(
1841 "NTRIP status sub-blocks exceed block length".into(),
1842 ));
1843 }
1844
1845 let mut connections = Vec::with_capacity(n as usize);
1846 let mut off = MIN_HEADER;
1847 for _ in 0..n as usize {
1848 connections.push(NtripConnectionSlot {
1849 cd_index: data[off],
1850 status: data[off + 1],
1851 error_code: data[off + 2],
1852 });
1853 off += sb_length_usize;
1854 }
1855 Ok((n, sb_length, connections))
1856}
1857
1858#[derive(Debug, Clone)]
1860pub struct NtripClientStatusBlock {
1861 tow_ms: u32,
1862 wnc: u16,
1863 pub n: u8,
1864 pub sb_length: u8,
1865 pub connections: Vec<NtripConnectionSlot>,
1866}
1867
1868impl NtripClientStatusBlock {
1869 pub fn tow_ms(&self) -> u32 {
1870 self.tow_ms
1871 }
1872 pub fn wnc(&self) -> u16 {
1873 self.wnc
1874 }
1875 pub fn tow_seconds(&self) -> f64 {
1876 self.tow_ms as f64 * 0.001
1877 }
1878}
1879
1880impl SbfBlockParse for NtripClientStatusBlock {
1881 const BLOCK_ID: u16 = block_ids::NTRIP_CLIENT_STATUS;
1882
1883 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1884 let (n, sb_length, connections) = parse_ntrip_connection_status(data)?;
1885 Ok(Self {
1886 tow_ms: header.tow_ms,
1887 wnc: header.wnc,
1888 n,
1889 sb_length,
1890 connections,
1891 })
1892 }
1893}
1894
1895#[derive(Debug, Clone)]
1900pub struct NtripServerStatusBlock {
1901 tow_ms: u32,
1902 wnc: u16,
1903 pub n: u8,
1904 pub sb_length: u8,
1905 pub connections: Vec<NtripConnectionSlot>,
1906}
1907
1908impl NtripServerStatusBlock {
1909 pub fn tow_ms(&self) -> u32 {
1910 self.tow_ms
1911 }
1912 pub fn wnc(&self) -> u16 {
1913 self.wnc
1914 }
1915 pub fn tow_seconds(&self) -> f64 {
1916 self.tow_ms as f64 * 0.001
1917 }
1918}
1919
1920impl SbfBlockParse for NtripServerStatusBlock {
1921 const BLOCK_ID: u16 = block_ids::NTRIP_SERVER_STATUS;
1922
1923 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1924 let (n, sb_length, connections) = parse_ntrip_connection_status(data)?;
1925 Ok(Self {
1926 tow_ms: header.tow_ms,
1927 wnc: header.wnc,
1928 n,
1929 sb_length,
1930 connections,
1931 })
1932 }
1933}
1934
1935#[derive(Debug, Clone)]
1941pub struct RfBandEntry {
1942 pub frequency_hz: u32,
1943 pub bandwidth: u16,
1944 pub info: u8,
1945}
1946
1947#[derive(Debug, Clone)]
1949pub struct RfStatusBlock {
1950 tow_ms: u32,
1951 wnc: u16,
1952 pub n: u8,
1953 pub sb_length: u8,
1954 pub reserved: u32,
1955 pub bands: Vec<RfBandEntry>,
1956}
1957
1958impl RfStatusBlock {
1959 pub fn tow_ms(&self) -> u32 {
1960 self.tow_ms
1961 }
1962 pub fn wnc(&self) -> u16 {
1963 self.wnc
1964 }
1965 pub fn tow_seconds(&self) -> f64 {
1966 self.tow_ms as f64 * 0.001
1967 }
1968}
1969
1970impl SbfBlockParse for RfStatusBlock {
1971 const BLOCK_ID: u16 = block_ids::RF_STATUS;
1972
1973 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1974 const MIN_HEADER: usize = 18;
1975 const MIN_SB_LENGTH: usize = 8;
1976 if data.len() < MIN_HEADER {
1977 return Err(SbfError::ParseError("RFStatus too short".into()));
1978 }
1979 let n = data[12];
1980 let sb_length = data[13];
1981 let sb_length_usize = sb_length as usize;
1982 if sb_length_usize < MIN_SB_LENGTH {
1983 return Err(SbfError::ParseError("RFStatus SBLength too small".into()));
1984 }
1985
1986 let required_len = MIN_HEADER + n as usize * sb_length_usize;
1987 if required_len > data.len() {
1988 return Err(SbfError::ParseError(
1989 "RFStatus sub-blocks exceed block length".into(),
1990 ));
1991 }
1992
1993 let reserved = u32::from_le_bytes(data[14..18].try_into().unwrap());
1994 let mut bands = Vec::with_capacity(n as usize);
1995 let mut off = MIN_HEADER;
1996 for _ in 0..n as usize {
1997 bands.push(RfBandEntry {
1998 frequency_hz: u32::from_le_bytes(data[off..off + 4].try_into().unwrap()),
1999 bandwidth: u16::from_le_bytes(data[off + 4..off + 6].try_into().unwrap()),
2000 info: data[off + 6],
2001 });
2002 off += sb_length_usize;
2003 }
2004 Ok(Self {
2005 tow_ms: header.tow_ms,
2006 wnc: header.wnc,
2007 n,
2008 sb_length,
2009 reserved,
2010 bands,
2011 })
2012 }
2013}
2014
2015#[derive(Debug, Clone)]
2023pub struct CommentBlock {
2024 tow_ms: u32,
2025 wnc: u16,
2026 comment_len: u16,
2027 comment_data: Vec<u8>,
2028}
2029
2030impl CommentBlock {
2031 pub fn tow_seconds(&self) -> f64 {
2032 self.tow_ms as f64 * 0.001
2033 }
2034 pub fn tow_ms(&self) -> u32 {
2035 self.tow_ms
2036 }
2037 pub fn wnc(&self) -> u16 {
2038 self.wnc
2039 }
2040 pub fn comment_len(&self) -> u16 {
2041 self.comment_len
2042 }
2043 pub fn comment_data(&self) -> &[u8] {
2044 &self.comment_data
2045 }
2046
2047 pub fn comment_text_lossy(&self) -> String {
2049 String::from_utf8_lossy(&self.comment_data).into_owned()
2050 }
2051}
2052
2053impl SbfBlockParse for CommentBlock {
2054 const BLOCK_ID: u16 = block_ids::COMMENT;
2055
2056 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2057 let block_len = header.length as usize;
2058 let data_len = block_len.saturating_sub(2);
2059 if data_len < 14 || data.len() < data_len {
2060 return Err(SbfError::ParseError("Comment too short".into()));
2061 }
2062
2063 let comment_len = u16::from_le_bytes([data[12], data[13]]) as usize;
2064 let comment_end = 14 + comment_len;
2065 if comment_end > data_len {
2066 return Err(SbfError::ParseError("Comment length exceeds block".into()));
2067 }
2068
2069 Ok(Self {
2070 tow_ms: header.tow_ms,
2071 wnc: header.wnc,
2072 comment_len: comment_len as u16,
2073 comment_data: data[14..comment_end].to_vec(),
2074 })
2075 }
2076}
2077
2078#[derive(Debug, Clone)]
2084pub struct RtcmDatumBlock {
2085 tow_ms: u32,
2086 wnc: u16,
2087 source_crs: [u8; 32],
2088 target_crs: [u8; 32],
2089 pub datum: u8,
2090 pub height_type: u8,
2091 pub quality_ind: u8,
2092}
2093
2094impl RtcmDatumBlock {
2095 pub fn tow_ms(&self) -> u32 {
2096 self.tow_ms
2097 }
2098 pub fn wnc(&self) -> u16 {
2099 self.wnc
2100 }
2101 pub fn tow_seconds(&self) -> f64 {
2102 self.tow_ms as f64 * 0.001
2103 }
2104 pub fn source_crs_lossy(&self) -> String {
2105 String::from_utf8_lossy(trim_trailing_nuls(&self.source_crs)).into_owned()
2106 }
2107 pub fn target_crs_lossy(&self) -> String {
2108 String::from_utf8_lossy(trim_trailing_nuls(&self.target_crs)).into_owned()
2109 }
2110}
2111
2112impl SbfBlockParse for RtcmDatumBlock {
2113 const BLOCK_ID: u16 = block_ids::RTCM_DATUM;
2114
2115 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2116 const MIN: usize = 79;
2117 if data.len() < MIN {
2118 return Err(SbfError::ParseError("RTCMDatum too short".into()));
2119 }
2120 let mut source_crs = [0u8; 32];
2121 source_crs.copy_from_slice(&data[12..44]);
2122 let mut target_crs = [0u8; 32];
2123 target_crs.copy_from_slice(&data[44..76]);
2124 Ok(Self {
2125 tow_ms: header.tow_ms,
2126 wnc: header.wnc,
2127 source_crs,
2128 target_crs,
2129 datum: data[76],
2130 height_type: data[77],
2131 quality_ind: data[78],
2132 })
2133 }
2134}
2135
2136#[derive(Debug, Clone)]
2138pub struct LBandBeamInfo {
2139 pub svid: u8,
2140 sat_name: [u8; 9],
2141 sat_longitude_raw: i16,
2142 pub beam_freq_hz: u32,
2143}
2144
2145impl LBandBeamInfo {
2146 pub fn sat_name_lossy(&self) -> String {
2147 String::from_utf8_lossy(trim_trailing_nuls(&self.sat_name)).into_owned()
2148 }
2149 pub fn sat_longitude_deg(&self) -> Option<f64> {
2150 if self.sat_longitude_raw == I16_DNU {
2151 None
2152 } else {
2153 Some(self.sat_longitude_raw as f64 * 0.01)
2154 }
2155 }
2156}
2157
2158#[derive(Debug, Clone)]
2160pub struct LBandBeamsBlock {
2161 tow_ms: u32,
2162 wnc: u16,
2163 pub n: u8,
2164 pub sb_length: u8,
2165 pub beams: Vec<LBandBeamInfo>,
2166}
2167
2168impl LBandBeamsBlock {
2169 pub fn tow_ms(&self) -> u32 {
2170 self.tow_ms
2171 }
2172 pub fn wnc(&self) -> u16 {
2173 self.wnc
2174 }
2175 pub fn tow_seconds(&self) -> f64 {
2176 self.tow_ms as f64 * 0.001
2177 }
2178}
2179
2180impl SbfBlockParse for LBandBeamsBlock {
2181 const BLOCK_ID: u16 = block_ids::LBAND_BEAMS;
2182
2183 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2184 const MIN_HEADER: usize = 14;
2185 const MIN_SB: usize = 16;
2186 if data.len() < MIN_HEADER {
2187 return Err(SbfError::ParseError("LBandBeams too short".into()));
2188 }
2189 let n = data[12];
2190 let sb_length = data[13];
2191 let sb_length_usize = sb_length as usize;
2192 if sb_length_usize < MIN_SB {
2193 return Err(SbfError::ParseError("LBandBeams SBLength too small".into()));
2194 }
2195 let required_len = MIN_HEADER + n as usize * sb_length_usize;
2196 if required_len > data.len() {
2197 return Err(SbfError::ParseError(
2198 "LBandBeams sub-blocks exceed block length".into(),
2199 ));
2200 }
2201 let mut beams = Vec::with_capacity(n as usize);
2202 let mut off = MIN_HEADER;
2203 for _ in 0..n as usize {
2204 let mut sat_name = [0u8; 9];
2205 sat_name.copy_from_slice(&data[off + 1..off + 10]);
2206 beams.push(LBandBeamInfo {
2207 svid: data[off],
2208 sat_name,
2209 sat_longitude_raw: i16::from_le_bytes(data[off + 10..off + 12].try_into().unwrap()),
2210 beam_freq_hz: u32::from_le_bytes(data[off + 12..off + 16].try_into().unwrap()),
2211 });
2212 off += sb_length_usize;
2213 }
2214 Ok(Self {
2215 tow_ms: header.tow_ms,
2216 wnc: header.wnc,
2217 n,
2218 sb_length,
2219 beams,
2220 })
2221 }
2222}
2223
2224#[derive(Debug, Clone)]
2226pub struct DynDnsStatusBlock {
2227 tow_ms: u32,
2228 wnc: u16,
2229 pub status: u8,
2230 pub error_code: u8,
2231 pub ip_address: [u8; 16],
2232 pub ipv6_address: Option<[u8; 16]>,
2233}
2234
2235impl DynDnsStatusBlock {
2236 pub fn tow_ms(&self) -> u32 {
2237 self.tow_ms
2238 }
2239 pub fn wnc(&self) -> u16 {
2240 self.wnc
2241 }
2242 pub fn tow_seconds(&self) -> f64 {
2243 self.tow_ms as f64 * 0.001
2244 }
2245 pub fn ip_address_string(&self) -> String {
2246 format_ip_bytes(&self.ip_address)
2247 }
2248 pub fn ipv6_address_string(&self) -> Option<String> {
2249 self.ipv6_address.as_ref().map(format_ip_bytes)
2250 }
2251}
2252
2253impl SbfBlockParse for DynDnsStatusBlock {
2254 const BLOCK_ID: u16 = block_ids::DYN_DNS_STATUS;
2255
2256 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2257 const MIN_V0: usize = 14;
2258 const MIN_V1: usize = 30;
2259 const MIN_V2: usize = 46;
2260 if data.len() < MIN_V0 {
2261 return Err(SbfError::ParseError("DynDNSStatus too short".into()));
2262 }
2263 let mut ip_address = [0u8; 16];
2264 if data.len() >= MIN_V1 {
2265 ip_address.copy_from_slice(&data[14..30]);
2266 }
2267 let ipv6_address = if header.block_rev >= 2 && data.len() >= MIN_V2 {
2268 let mut addr = [0u8; 16];
2269 addr.copy_from_slice(&data[30..46]);
2270 Some(addr)
2271 } else {
2272 None
2273 };
2274 Ok(Self {
2275 tow_ms: header.tow_ms,
2276 wnc: header.wnc,
2277 status: data[12],
2278 error_code: data[13],
2279 ip_address,
2280 ipv6_address,
2281 })
2282 }
2283}
2284
2285#[derive(Debug, Clone)]
2287pub struct DiskData {
2288 pub disk_id: u8,
2289 pub status: u8,
2290 pub disk_usage_msb: u16,
2291 pub disk_usage_lsb: u32,
2292 pub disk_size_mb: u32,
2293 pub create_delete_count: u8,
2294 pub error: Option<u8>,
2295}
2296
2297impl DiskData {
2298 pub fn disk_usage_bytes(&self) -> Option<u64> {
2299 if self.disk_usage_msb == U16_DNU && self.disk_usage_lsb == u32::MAX {
2300 None
2301 } else {
2302 Some(((self.disk_usage_msb as u64) << 32) | self.disk_usage_lsb as u64)
2303 }
2304 }
2305}
2306
2307#[derive(Debug, Clone)]
2309pub struct DiskStatusBlock {
2310 tow_ms: u32,
2311 wnc: u16,
2312 pub n: u8,
2313 pub sb_length: u8,
2314 pub reserved: [u8; 4],
2315 pub disks: Vec<DiskData>,
2316}
2317
2318impl DiskStatusBlock {
2319 pub fn tow_ms(&self) -> u32 {
2320 self.tow_ms
2321 }
2322 pub fn wnc(&self) -> u16 {
2323 self.wnc
2324 }
2325 pub fn tow_seconds(&self) -> f64 {
2326 self.tow_ms as f64 * 0.001
2327 }
2328}
2329
2330impl SbfBlockParse for DiskStatusBlock {
2331 const BLOCK_ID: u16 = block_ids::DISK_STATUS;
2332
2333 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2334 const MIN_HEADER: usize = 18;
2335 const MIN_SB: usize = 13;
2336 if data.len() < MIN_HEADER {
2337 return Err(SbfError::ParseError("DiskStatus too short".into()));
2338 }
2339 let n = data[12];
2340 let sb_length = data[13];
2341 let sb_length_usize = sb_length as usize;
2342 if sb_length_usize < MIN_SB {
2343 return Err(SbfError::ParseError("DiskStatus SBLength too small".into()));
2344 }
2345 let required_len = MIN_HEADER + n as usize * sb_length_usize;
2346 if required_len > data.len() {
2347 return Err(SbfError::ParseError(
2348 "DiskStatus sub-blocks exceed block length".into(),
2349 ));
2350 }
2351 let mut reserved = [0u8; 4];
2352 reserved.copy_from_slice(&data[14..18]);
2353 let mut disks = Vec::with_capacity(n as usize);
2354 let mut off = MIN_HEADER;
2355 for _ in 0..n as usize {
2356 disks.push(DiskData {
2357 disk_id: data[off],
2358 status: data[off + 1],
2359 disk_usage_msb: u16::from_le_bytes(data[off + 2..off + 4].try_into().unwrap()),
2360 disk_usage_lsb: u32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap()),
2361 disk_size_mb: u32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap()),
2362 create_delete_count: data[off + 12],
2363 error: if header.block_rev >= 1 && sb_length_usize >= 14 {
2364 Some(data[off + 13])
2365 } else {
2366 None
2367 },
2368 });
2369 off += sb_length_usize;
2370 }
2371 Ok(Self {
2372 tow_ms: header.tow_ms,
2373 wnc: header.wnc,
2374 n,
2375 sb_length,
2376 reserved,
2377 disks,
2378 })
2379 }
2380}
2381
2382#[derive(Debug, Clone)]
2384pub struct P2ppSession {
2385 pub session_id: u8,
2386 pub port: u8,
2387 pub status: u8,
2388 pub error_code: u8,
2389}
2390
2391#[derive(Debug, Clone)]
2393pub struct P2ppStatusBlock {
2394 tow_ms: u32,
2395 wnc: u16,
2396 pub n: u8,
2397 pub sb_length: u8,
2398 pub sessions: Vec<P2ppSession>,
2399}
2400
2401impl P2ppStatusBlock {
2402 pub fn tow_ms(&self) -> u32 {
2403 self.tow_ms
2404 }
2405 pub fn wnc(&self) -> u16 {
2406 self.wnc
2407 }
2408 pub fn tow_seconds(&self) -> f64 {
2409 self.tow_ms as f64 * 0.001
2410 }
2411}
2412
2413impl SbfBlockParse for P2ppStatusBlock {
2414 const BLOCK_ID: u16 = block_ids::P2PP_STATUS;
2415
2416 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2417 const MIN_HEADER: usize = 14;
2418 const MIN_SB: usize = 4;
2419 if data.len() < MIN_HEADER {
2420 return Err(SbfError::ParseError("P2PPStatus too short".into()));
2421 }
2422 let n = data[12];
2423 let sb_length = data[13];
2424 let sb_length_usize = sb_length as usize;
2425 if sb_length_usize < MIN_SB {
2426 return Err(SbfError::ParseError("P2PPStatus SBLength too small".into()));
2427 }
2428 let required_len = MIN_HEADER + n as usize * sb_length_usize;
2429 if required_len > data.len() {
2430 return Err(SbfError::ParseError(
2431 "P2PPStatus sub-blocks exceed block length".into(),
2432 ));
2433 }
2434 let mut sessions = Vec::with_capacity(n as usize);
2435 let mut off = MIN_HEADER;
2436 for _ in 0..n as usize {
2437 sessions.push(P2ppSession {
2438 session_id: data[off],
2439 port: data[off + 1],
2440 status: data[off + 2],
2441 error_code: data[off + 3],
2442 });
2443 off += sb_length_usize;
2444 }
2445 Ok(Self {
2446 tow_ms: header.tow_ms,
2447 wnc: header.wnc,
2448 n,
2449 sb_length,
2450 sessions,
2451 })
2452 }
2453}
2454
2455#[derive(Debug, Clone)]
2457pub struct CosmosStatusBlock {
2458 tow_ms: u32,
2459 wnc: u16,
2460 pub status: u8,
2461}
2462
2463impl CosmosStatusBlock {
2464 pub fn tow_ms(&self) -> u32 {
2465 self.tow_ms
2466 }
2467 pub fn wnc(&self) -> u16 {
2468 self.wnc
2469 }
2470 pub fn tow_seconds(&self) -> f64 {
2471 self.tow_ms as f64 * 0.001
2472 }
2473}
2474
2475impl SbfBlockParse for CosmosStatusBlock {
2476 const BLOCK_ID: u16 = block_ids::COSMOS_STATUS;
2477
2478 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2479 if data.len() < 13 {
2480 return Err(SbfError::ParseError("CosmosStatus too short".into()));
2481 }
2482 Ok(Self {
2483 tow_ms: header.tow_ms,
2484 wnc: header.wnc,
2485 status: data[12],
2486 })
2487 }
2488}
2489
2490#[derive(Debug, Clone)]
2496pub struct RxMessageBlock {
2497 tow_ms: u32,
2498 wnc: u16,
2499 pub message_type: u8,
2500 pub severity: u8,
2501 pub message_id: u32,
2502 pub string_len: u16,
2503 message: Vec<u8>,
2504}
2505
2506impl RxMessageBlock {
2507 pub fn tow_ms(&self) -> u32 {
2508 self.tow_ms
2509 }
2510 pub fn wnc(&self) -> u16 {
2511 self.wnc
2512 }
2513 pub fn tow_seconds(&self) -> f64 {
2514 self.tow_ms as f64 * 0.001
2515 }
2516 pub fn message(&self) -> &[u8] {
2517 &self.message
2518 }
2519 pub fn message_text_lossy(&self) -> String {
2520 String::from_utf8_lossy(trim_trailing_nuls(&self.message)).into_owned()
2521 }
2522}
2523
2524impl SbfBlockParse for RxMessageBlock {
2525 const BLOCK_ID: u16 = block_ids::RX_MESSAGE;
2526
2527 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2528 let block_len = header.length as usize;
2529 let data_len = block_len.saturating_sub(2);
2530 if data_len < 22 || data.len() < data_len {
2531 return Err(SbfError::ParseError("RxMessage too short".into()));
2532 }
2533 let string_len = u16::from_le_bytes(data[18..20].try_into().unwrap()) as usize;
2534 let end = 22 + string_len;
2535 if end > data_len {
2536 return Err(SbfError::ParseError(
2537 "RxMessage length exceeds block".into(),
2538 ));
2539 }
2540 Ok(Self {
2541 tow_ms: header.tow_ms,
2542 wnc: header.wnc,
2543 message_type: data[12],
2544 severity: data[13],
2545 message_id: u32::from_le_bytes(data[14..18].try_into().unwrap()),
2546 string_len: string_len as u16,
2547 message: data[22..end].to_vec(),
2548 })
2549 }
2550}
2551
2552#[derive(Debug, Clone)]
2554pub struct EncapsulatedOutputBlock {
2555 tow_ms: u32,
2556 wnc: u16,
2557 pub mode: u8,
2558 pub reserved_id: u16,
2559 payload: Vec<u8>,
2560}
2561
2562impl EncapsulatedOutputBlock {
2563 pub fn tow_ms(&self) -> u32 {
2564 self.tow_ms
2565 }
2566 pub fn wnc(&self) -> u16 {
2567 self.wnc
2568 }
2569 pub fn tow_seconds(&self) -> f64 {
2570 self.tow_ms as f64 * 0.001
2571 }
2572 pub fn payload(&self) -> &[u8] {
2573 &self.payload
2574 }
2575}
2576
2577impl SbfBlockParse for EncapsulatedOutputBlock {
2578 const BLOCK_ID: u16 = block_ids::ENCAPSULATED_OUTPUT;
2579
2580 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2581 let block_len = header.length as usize;
2582 let data_len = block_len.saturating_sub(2);
2583 if data_len < 18 || data.len() < data_len {
2584 return Err(SbfError::ParseError("EncapsulatedOutput too short".into()));
2585 }
2586 let payload_len = u16::from_le_bytes(data[14..16].try_into().unwrap()) as usize;
2587 let end = 18 + payload_len;
2588 if end > data_len {
2589 return Err(SbfError::ParseError(
2590 "EncapsulatedOutput payload exceeds block".into(),
2591 ));
2592 }
2593 Ok(Self {
2594 tow_ms: header.tow_ms,
2595 wnc: header.wnc,
2596 mode: data[12],
2597 reserved_id: u16::from_le_bytes(data[16..18].try_into().unwrap()),
2598 payload: data[18..end].to_vec(),
2599 })
2600 }
2601}
2602
2603#[derive(Debug, Clone)]
2605pub struct GisActionBlock {
2606 tow_ms: u32,
2607 wnc: u16,
2608 pub comment_len: u16,
2609 pub item_id_msb: u32,
2610 pub item_id_lsb: u32,
2611 pub action: u8,
2612 pub trigger: u8,
2613 pub database: u8,
2614 comment: Vec<u8>,
2615}
2616
2617impl GisActionBlock {
2618 pub fn tow_ms(&self) -> u32 {
2619 self.tow_ms
2620 }
2621 pub fn wnc(&self) -> u16 {
2622 self.wnc
2623 }
2624 pub fn tow_seconds(&self) -> f64 {
2625 self.tow_ms as f64 * 0.001
2626 }
2627 pub fn comment(&self) -> &[u8] {
2628 &self.comment
2629 }
2630 pub fn comment_text_lossy(&self) -> String {
2631 String::from_utf8_lossy(trim_trailing_nuls(&self.comment)).into_owned()
2632 }
2633}
2634
2635impl SbfBlockParse for GisActionBlock {
2636 const BLOCK_ID: u16 = block_ids::GIS_ACTION;
2637
2638 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2639 let block_len = header.length as usize;
2640 let data_len = block_len.saturating_sub(2);
2641 if data_len < 26 || data.len() < data_len {
2642 return Err(SbfError::ParseError("GISAction too short".into()));
2643 }
2644 let comment_len = u16::from_le_bytes(data[12..14].try_into().unwrap()) as usize;
2645 let end = 26 + comment_len;
2646 if end > data_len {
2647 return Err(SbfError::ParseError(
2648 "GISAction comment exceeds block".into(),
2649 ));
2650 }
2651 Ok(Self {
2652 tow_ms: header.tow_ms,
2653 wnc: header.wnc,
2654 comment_len: comment_len as u16,
2655 item_id_msb: u32::from_le_bytes(data[14..18].try_into().unwrap()),
2656 item_id_lsb: u32::from_le_bytes(data[18..22].try_into().unwrap()),
2657 action: data[22],
2658 trigger: data[23],
2659 database: data[24],
2660 comment: data[26..end].to_vec(),
2661 })
2662 }
2663}
2664
2665#[derive(Debug, Clone)]
2667pub struct GisDatabaseStatus {
2668 pub database: u8,
2669 pub online_status: u8,
2670 pub error: u8,
2671 pub nr_items: u32,
2672 pub nr_not_sync: u32,
2673}
2674
2675#[derive(Debug, Clone)]
2677pub struct GisStatusBlock {
2678 tow_ms: u32,
2679 wnc: u16,
2680 pub n: u8,
2681 pub sb_length: u8,
2682 pub databases: Vec<GisDatabaseStatus>,
2683}
2684
2685impl GisStatusBlock {
2686 pub fn tow_ms(&self) -> u32 {
2687 self.tow_ms
2688 }
2689 pub fn wnc(&self) -> u16 {
2690 self.wnc
2691 }
2692 pub fn tow_seconds(&self) -> f64 {
2693 self.tow_ms as f64 * 0.001
2694 }
2695}
2696
2697impl SbfBlockParse for GisStatusBlock {
2698 const BLOCK_ID: u16 = block_ids::GIS_STATUS;
2699
2700 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2701 const MIN_HEADER: usize = 14;
2702 const MIN_SB: usize = 12;
2703 if data.len() < MIN_HEADER {
2704 return Err(SbfError::ParseError("GISStatus too short".into()));
2705 }
2706 let n = data[12];
2707 let sb_length = data[13];
2708 let sb_length_usize = sb_length as usize;
2709 if sb_length_usize < MIN_SB {
2710 return Err(SbfError::ParseError("GISStatus SBLength too small".into()));
2711 }
2712 let required_len = MIN_HEADER + n as usize * sb_length_usize;
2713 if required_len > data.len() {
2714 return Err(SbfError::ParseError(
2715 "GISStatus sub-blocks exceed block length".into(),
2716 ));
2717 }
2718 let mut databases = Vec::with_capacity(n as usize);
2719 let mut off = MIN_HEADER;
2720 for _ in 0..n as usize {
2721 databases.push(GisDatabaseStatus {
2722 database: data[off],
2723 online_status: data[off + 1],
2724 error: data[off + 2],
2725 nr_items: u32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap()),
2726 nr_not_sync: u32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap()),
2727 });
2728 off += sb_length_usize;
2729 }
2730 Ok(Self {
2731 tow_ms: header.tow_ms,
2732 wnc: header.wnc,
2733 n,
2734 sb_length,
2735 databases,
2736 })
2737 }
2738}
2739
2740#[cfg(test)]
2741mod tests {
2742 use super::*;
2743 use crate::blocks::SbfBlock;
2744 use crate::header::{SbfHeader, SBF_SYNC};
2745 use crate::types::Constellation;
2746
2747 fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
2748 SbfHeader {
2749 crc: 0,
2750 block_id,
2751 block_rev: 0,
2752 length: (data_len + 2) as u16,
2753 tow_ms,
2754 wnc,
2755 }
2756 }
2757
2758 #[test]
2759 fn test_sat_visibility_elevation() {
2760 let info = SatVisibilityInfo {
2761 sat_id: SatelliteId::new(Constellation::GPS, 1),
2762 freq_nr: 0,
2763 azimuth_raw: 18000, elevation_raw: 4500, rise_set: 1,
2766 satellite_info: 0,
2767 };
2768
2769 assert!((info.azimuth_deg().unwrap() - 180.0).abs() < 0.01);
2770 assert!((info.elevation_deg().unwrap() - 45.0).abs() < 0.01);
2771 assert!(info.is_rising());
2772 assert!(info.is_above_horizon());
2773 }
2774
2775 #[test]
2776 fn test_channel_sat_info_dnu_handling() {
2777 let info = ChannelSatInfo {
2778 sat_id: SatelliteId::new(Constellation::GPS, 1),
2779 freq_nr: 0,
2780 azimuth_raw: 511,
2781 rise_set: 3,
2782 elevation_raw: I8_DNU,
2783 health_status: 0,
2784 states: Vec::new(),
2785 };
2786
2787 assert_eq!(info.azimuth_raw(), 511);
2788 assert_eq!(info.azimuth_deg_opt(), None);
2789 assert_eq!(info.azimuth_deg(), 0.0);
2790 assert_eq!(info.elevation_raw(), I8_DNU);
2791 assert_eq!(info.elevation_deg_opt(), None);
2792 assert_eq!(info.elevation_deg(), 0.0);
2793 assert!(info.is_rise_set_unknown());
2794 }
2795
2796 #[test]
2797 fn ntrip_client_status_parse_min() {
2798 let mut data = vec![0u8; 22];
2799 data[12] = 2;
2800 data[13] = 4;
2801 data[14] = 10;
2802 data[15] = 20;
2803 data[16] = 30;
2804 data[18] = 11;
2805 data[19] = 21;
2806 data[20] = 31;
2807 let header = header_for(block_ids::NTRIP_CLIENT_STATUS, data.len(), 1000, 100);
2808 let b = NtripClientStatusBlock::parse(&header, &data).unwrap();
2809 assert_eq!(b.n, 2);
2810 assert_eq!(b.sb_length, 4);
2811 assert_eq!(b.connections.len(), 2);
2812 assert_eq!(b.connections[0].cd_index, 10);
2813 assert_eq!(b.connections[0].status, 20);
2814 assert_eq!(b.connections[0].error_code, 30);
2815 assert_eq!(b.connections[1].cd_index, 11);
2816 assert_eq!(b.connections[1].status, 21);
2817 assert_eq!(b.connections[1].error_code, 31);
2818 }
2819
2820 #[test]
2821 fn ntrip_server_status_respects_n() {
2822 let mut data = vec![0u8; 18];
2823 data[12] = 1;
2824 data[13] = 4;
2825 data[14] = 9;
2826 data[15] = 8;
2827 data[16] = 7;
2828 let header = header_for(block_ids::NTRIP_SERVER_STATUS, data.len(), 1500, 101);
2829 let b = NtripServerStatusBlock::parse(&header, &data).unwrap();
2830 assert_eq!(b.n, 1);
2831 assert_eq!(b.connections.len(), 1);
2832 assert_eq!(b.connections[0].cd_index, 9);
2833 assert_eq!(b.connections[0].status, 8);
2834 assert_eq!(b.connections[0].error_code, 7);
2835 }
2836
2837 #[test]
2838 fn rf_status_parse_min() {
2839 let mut data = vec![0u8; 34];
2840 data[12] = 2;
2841 data[13] = 8;
2842 data[14..18].copy_from_slice(&0x01020304u32.to_le_bytes());
2843 data[18..22].copy_from_slice(&1_575_420_000u32.to_le_bytes());
2844 data[22..24].copy_from_slice(&2000u16.to_le_bytes());
2845 data[24] = 42;
2846 data[26..30].copy_from_slice(&1_227_600_000u32.to_le_bytes());
2847 data[30..32].copy_from_slice(&1000u16.to_le_bytes());
2848 data[32] = 24;
2849 let header = header_for(block_ids::RF_STATUS, data.len(), 500, 200);
2850 let b = RfStatusBlock::parse(&header, &data).unwrap();
2851 assert_eq!(b.n, 2);
2852 assert_eq!(b.sb_length, 8);
2853 assert_eq!(b.reserved, 0x0102_0304);
2854 assert_eq!(b.bands.len(), 2);
2855 assert_eq!(b.bands[0].frequency_hz, 1_575_420_000);
2856 assert_eq!(b.bands[0].bandwidth, 2000);
2857 assert_eq!(b.bands[0].info, 42);
2858 assert_eq!(b.bands[1].frequency_hz, 1_227_600_000);
2859 assert_eq!(b.bands[1].bandwidth, 1000);
2860 assert_eq!(b.bands[1].info, 24);
2861 }
2862
2863 #[test]
2864 fn test_sat_visibility_invalid() {
2865 let info = SatVisibilityInfo {
2866 sat_id: SatelliteId::new(Constellation::GPS, 1),
2867 freq_nr: 0,
2868 azimuth_raw: 65535,
2869 elevation_raw: -32768,
2870 rise_set: 0,
2871 satellite_info: 0,
2872 };
2873
2874 assert!(info.azimuth_deg().is_none());
2875 assert!(info.elevation_deg().is_none());
2876 }
2877
2878 #[test]
2879 fn test_receiver_status_uptime() {
2880 let status = ReceiverStatusBlock {
2881 tow_ms: 0,
2882 wnc: 0,
2883 cpu_load: 50,
2884 ext_error: 0,
2885 uptime_s: 3661, rx_state: 0,
2887 rx_error: 0,
2888 cmd_count: None,
2889 temperature_raw: None,
2890 agc_data: vec![],
2891 };
2892
2893 let (h, m, s) = status.uptime_hms();
2894 assert_eq!(h, 1);
2895 assert_eq!(m, 1);
2896 assert_eq!(s, 1);
2897 }
2898
2899 #[test]
2900 fn test_receiver_status_parse_rev1_with_agc() {
2901 let mut data = vec![0u8; 30 + 4];
2902 data[12] = 75; data[13] = 2; data[14..18].copy_from_slice(&120_u32.to_le_bytes()); data[18..22].copy_from_slice(&0x0001_0002_u32.to_le_bytes()); data[22..26].copy_from_slice(&0x0000_0200_u32.to_le_bytes()); data[26] = 1; data[27] = 4; data[28] = 11; data[29] = 123; data[30] = 9; data[31] = (-12_i8) as u8; data[32] = 100; data[33] = 3; let header = SbfHeader {
2917 crc: 0,
2918 block_id: block_ids::RECEIVER_STATUS,
2919 block_rev: 1,
2920 length: (data.len() + 2) as u16,
2921 tow_ms: 4321,
2922 wnc: 2045,
2923 };
2924
2925 let block = ReceiverStatusBlock::parse(&header, &data).unwrap();
2926 assert_eq!(block.cpu_load, 75);
2927 assert_eq!(block.cmd_count(), Some(11));
2928 assert_eq!(block.temperature_raw(), Some(123));
2929 assert_eq!(block.temperature_celsius(), Some(23));
2930 assert_eq!(block.agc_data.len(), 1);
2931 assert_eq!(block.agc_data[0].frontend_id, 9);
2932 assert_eq!(block.agc_data[0].gain_db, -12);
2933 }
2934
2935 #[test]
2936 fn test_receiver_status_parse_rev1_min_length_enforced() {
2937 let data = vec![0u8; 26];
2938 let header = SbfHeader {
2939 crc: 0,
2940 block_id: block_ids::RECEIVER_STATUS,
2941 block_rev: 1,
2942 length: (data.len() + 2) as u16,
2943 tow_ms: 0,
2944 wnc: 0,
2945 };
2946
2947 let err = ReceiverStatusBlock::parse(&header, &data).unwrap_err();
2948 assert!(matches!(err, SbfError::ParseError(_)));
2949 }
2950
2951 #[test]
2952 fn test_input_link_accessors() {
2953 let stats = InputLinkStats {
2954 connection_descriptor: 1,
2955 link_type: 2,
2956 age_last_message_raw: U16_DNU,
2957 bytes_received: 100,
2958 bytes_accepted: 90,
2959 messages_received: 10,
2960 messages_accepted: 9,
2961 };
2962 let block = InputLinkBlock {
2963 tow_ms: 2000,
2964 wnc: 3000,
2965 inputs: vec![stats],
2966 };
2967
2968 assert!((block.tow_seconds() - 2.0).abs() < 1e-6);
2969 assert!(block.inputs[0].age_last_message_s().is_none());
2970 }
2971
2972 #[test]
2973 fn test_input_link_parse() {
2974 let mut data = vec![0u8; 14 + 20];
2975 data[12] = 1; data[13] = 20; let offset = 14;
2979 data[offset] = 3; data[offset + 1] = 4; data[offset + 2..offset + 4].copy_from_slice(&120_u16.to_le_bytes());
2982 data[offset + 4..offset + 8].copy_from_slice(&1000_u32.to_le_bytes());
2983 data[offset + 8..offset + 12].copy_from_slice(&900_u32.to_le_bytes());
2984 data[offset + 12..offset + 16].copy_from_slice(&10_u32.to_le_bytes());
2985 data[offset + 16..offset + 20].copy_from_slice(&9_u32.to_le_bytes());
2986
2987 let header = header_for(block_ids::INPUT_LINK, data.len(), 123456, 2222);
2988 let block = InputLinkBlock::parse(&header, &data).unwrap();
2989
2990 assert_eq!(block.num_links(), 1);
2991 let entry = &block.inputs[0];
2992 assert_eq!(entry.connection_descriptor, 3);
2993 assert_eq!(entry.link_type, 4);
2994 assert_eq!(entry.age_last_message_raw(), 120);
2995 assert_eq!(entry.bytes_received, 1000);
2996 assert_eq!(entry.messages_accepted, 9);
2997 }
2998
2999 #[test]
3000 fn test_quality_ind_parse() {
3001 let mut data = vec![0u8; 14 + 6];
3002 data[12] = 3; data[13] = 0; data[14..16].copy_from_slice(&0x1234_u16.to_le_bytes());
3006 data[16..18].copy_from_slice(&0x5678_u16.to_le_bytes());
3007 data[18..20].copy_from_slice(&0x9abc_u16.to_le_bytes());
3008
3009 let header = header_for(block_ids::QUALITY_IND, data.len(), 1000, 2000);
3010 let block = QualityIndBlock::parse(&header, &data).unwrap();
3011
3012 assert_eq!(block.num_indicators(), 3);
3013 assert_eq!(block.indicators[0], 0x1234);
3014 assert_eq!(block.indicators[1], 0x5678);
3015 assert_eq!(block.indicators[2], 0x9abc);
3016 }
3017
3018 #[test]
3019 fn test_quality_ind_sbf_block_parse() {
3020 let indicators = [0x1111_u16, 0x2222_u16];
3021 let n = indicators.len();
3022 let total_len = 16 + (n * 2); assert_eq!(total_len % 4, 0);
3024
3025 let mut data = vec![0u8; total_len];
3026 data[0..2].copy_from_slice(&SBF_SYNC);
3027 data[2..4].copy_from_slice(&0_u16.to_le_bytes()); data[4..6].copy_from_slice(&block_ids::QUALITY_IND.to_le_bytes()); data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); data[8..12].copy_from_slice(&1000_u32.to_le_bytes()); data[12..14].copy_from_slice(&2000_u16.to_le_bytes()); data[14] = n as u8;
3033 data[15] = 0; let mut offset = 16;
3036 for value in indicators {
3037 data[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
3038 offset += 2;
3039 }
3040
3041 let (block, used) = SbfBlock::parse(&data).unwrap();
3042 assert_eq!(used, total_len);
3043 assert_eq!(block.block_id(), block_ids::QUALITY_IND);
3044 match block {
3045 SbfBlock::QualityInd(quality) => {
3046 assert_eq!(quality.wnc(), 2000);
3047 assert_eq!(quality.tow_ms(), 1000);
3048 assert_eq!(quality.indicators, indicators);
3049 }
3050 _ => panic!("Expected QualityInd block"),
3051 }
3052 }
3053
3054 #[test]
3055 fn test_output_link_accessors() {
3056 let stats = OutputLinkStats {
3057 connection_descriptor: 2,
3058 allowed_rate_raw: 500,
3059 bytes_produced: 2000,
3060 bytes_sent: 1900,
3061 nr_clients: 1,
3062 output_types: vec![OutputType {
3063 output_type: 10,
3064 percentage: 50,
3065 }],
3066 };
3067 let block = OutputLinkBlock {
3068 tow_ms: 3000,
3069 wnc: 4000,
3070 outputs: vec![stats],
3071 };
3072
3073 assert!((block.tow_seconds() - 3.0).abs() < 1e-6);
3074 assert_eq!(block.outputs[0].allowed_rate_kbytes_per_s(), 500);
3075 assert_eq!(block.outputs[0].allowed_rate_bytes_per_s(), 500_000);
3076 assert_eq!(block.outputs[0].allowed_rate_bps(), Some(500));
3077 }
3078
3079 #[test]
3080 fn test_ip_status_parse() {
3081 let mut data = vec![0u8; 51];
3082 data[6..10].copy_from_slice(&5000u32.to_le_bytes());
3083 data[10..12].copy_from_slice(&2400u16.to_le_bytes());
3084 data[12..18].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
3085 data[18..34].copy_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 100]);
3086 data[34..50].copy_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]);
3087 data[50] = 24;
3088
3089 let header = header_for(block_ids::IP_STATUS, 51, 5000, 2400);
3090 let block = IpStatusBlock::parse(&header, &data).unwrap();
3091 assert_eq!(block.tow_seconds(), 5.0);
3092 assert_eq!(block.wnc(), 2400);
3093 assert_eq!(block.mac_address, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
3094 assert_eq!(block.mac_address_string(), "AA:BB:CC:DD:EE:FF");
3095 assert_eq!(block.ip_address_string(), "192.168.1.100");
3096 assert_eq!(block.gateway_string(), "192.168.1.1");
3097 assert_eq!(block.netmask_prefix, 24);
3098 }
3099
3100 #[test]
3101 fn test_output_link_parse() {
3102 let mut data = vec![0u8; 18 + 13 + 2];
3103 data[12] = 1; data[13] = 13; data[14] = 2; let offset = 18;
3109 data[offset] = 1; data[offset + 1] = 1; data[offset + 2..offset + 4].copy_from_slice(&500_u16.to_le_bytes());
3112 data[offset + 4..offset + 8].copy_from_slice(&2000_u32.to_le_bytes());
3113 data[offset + 8..offset + 12].copy_from_slice(&1800_u32.to_le_bytes());
3114 data[offset + 12] = 2; let type_offset = offset + 13;
3117 data[type_offset] = 7; data[type_offset + 1] = 80; let header = header_for(block_ids::OUTPUT_LINK, data.len(), 654321, 3333);
3121 let block = OutputLinkBlock::parse(&header, &data).unwrap();
3122
3123 assert_eq!(block.num_links(), 1);
3124 let entry = &block.outputs[0];
3125 assert_eq!(entry.connection_descriptor, 1);
3126 assert_eq!(entry.allowed_rate_raw(), 500);
3127 assert_eq!(entry.nr_clients, 2);
3128 assert_eq!(entry.output_types.len(), 1);
3129 assert_eq!(entry.output_types[0].output_type, 7);
3130 assert_eq!(entry.output_types[0].percentage, 80);
3131 }
3132
3133 #[test]
3134 fn test_tracking_status_sbf_block_parse() {
3135 let total_len = 40usize;
3137 let mut data = vec![0u8; total_len];
3138
3139 data[0..2].copy_from_slice(&SBF_SYNC);
3140 data[2..4].copy_from_slice(&0_u16.to_le_bytes()); data[4..6].copy_from_slice(&block_ids::TRACKING_STATUS.to_le_bytes()); data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); data[8..12].copy_from_slice(&12345_u32.to_le_bytes()); data[12..14].copy_from_slice(&2045_u16.to_le_bytes()); data[14] = 1; data[15] = 12; data[16] = 8; data[17] = 0; data[18] = 0; data[19] = 0; let sb1 = 20;
3155 data[sb1] = 5; data[sb1 + 1] = 0; data[sb1 + 2..sb1 + 4].copy_from_slice(&0_u16.to_le_bytes()); let az_rise_set = (1_u16 << 14) | 180_u16; data[sb1 + 4..sb1 + 6].copy_from_slice(&az_rise_set.to_le_bytes());
3160 data[sb1 + 6..sb1 + 8].copy_from_slice(&0_u16.to_le_bytes()); data[sb1 + 8] = 45; data[sb1 + 9] = 1; data[sb1 + 10] = 3; data[sb1 + 11] = 0; let sb2 = sb1 + 12;
3167 data[sb2] = 0; data[sb2 + 1] = 0; data[sb2 + 2..sb2 + 4].copy_from_slice(&3_u16.to_le_bytes()); data[sb2 + 4..sb2 + 6].copy_from_slice(&2_u16.to_le_bytes()); data[sb2 + 6..sb2 + 8].copy_from_slice(&0x1234_u16.to_le_bytes()); let (block, used) = SbfBlock::parse(&data).unwrap();
3174 assert_eq!(used, total_len);
3175 assert_eq!(block.block_id(), block_ids::TRACKING_STATUS);
3176 match block {
3177 SbfBlock::TrackingStatus(tracking) => {
3178 assert_eq!(tracking.tow_ms(), 12345);
3179 assert_eq!(tracking.wnc(), 2045);
3180 assert_eq!(tracking.num_satellites(), 1);
3181 assert_eq!(tracking.satellites[0].states.len(), 1);
3182 assert_eq!(tracking.satellites[0].states[0].tracking_status, 3);
3183 assert_eq!(tracking.satellites[0].states[0].pvt_status, 2);
3184 assert_eq!(tracking.satellites[0].states[0].pvt_info, 0x1234);
3185 }
3186 _ => panic!("Expected TrackingStatus block"),
3187 }
3188 }
3189
3190 #[test]
3191 fn test_lband_tracker_data_accessors() {
3192 let entry = LBandTrackerData {
3193 frequency_hz: 1_545_000_000,
3194 baudrate: 1200,
3195 service_id: 7,
3196 freq_offset_hz_raw: F32_DNU,
3197 cn0_raw: 0,
3198 avg_power_raw: I16_DNU,
3199 agc_gain_db_raw: I8_DNU,
3200 mode: 0,
3201 status: 1,
3202 svid: None,
3203 lock_time_s: None,
3204 source: None,
3205 };
3206
3207 assert!(entry.freq_offset_hz().is_none());
3208 assert!(entry.cn0_dbhz().is_none());
3209 assert!(entry.avg_power_db().is_none());
3210 assert!(entry.agc_gain_db().is_none());
3211 }
3212
3213 #[test]
3214 fn test_lband_tracker_status_parse_rev3() {
3215 let sb_length = 24usize;
3216 let mut data = vec![0u8; 14 + sb_length];
3217 data[12] = 1; data[13] = sb_length as u8; let offset = 14;
3221 data[offset..offset + 4].copy_from_slice(&1_545_000_000_u32.to_le_bytes()); data[offset + 4..offset + 6].copy_from_slice(&1200_u16.to_le_bytes()); data[offset + 6..offset + 8].copy_from_slice(&42_u16.to_le_bytes()); data[offset + 8..offset + 12].copy_from_slice(&12.5_f32.to_le_bytes()); data[offset + 12..offset + 14].copy_from_slice(&4550_u16.to_le_bytes()); data[offset + 14..offset + 16].copy_from_slice(&(-123_i16).to_le_bytes()); data[offset + 16] = (-7_i8) as u8; data[offset + 17] = 0; data[offset + 18] = 3; data[offset + 19] = 110; data[offset + 20..offset + 22].copy_from_slice(&360_u16.to_le_bytes()); data[offset + 22] = 2; let header = SbfHeader {
3235 crc: 0,
3236 block_id: block_ids::LBAND_TRACKER_STATUS,
3237 block_rev: 3,
3238 length: (data.len() + 2) as u16,
3239 tow_ms: 7777,
3240 wnc: 2099,
3241 };
3242
3243 let block = LBandTrackerStatusBlock::parse(&header, &data).unwrap();
3244 assert_eq!(block.n, 1);
3245 assert_eq!(block.sb_length, sb_length as u8);
3246 assert_eq!(block.num_trackers(), 1);
3247
3248 let entry = &block.trackers[0];
3249 assert_eq!(entry.frequency_hz, 1_545_000_000);
3250 assert_eq!(entry.baudrate, 1200);
3251 assert_eq!(entry.service_id, 42);
3252 assert!((entry.freq_offset_hz().unwrap() - 12.5).abs() < 1e-6);
3253 assert!((entry.cn0_dbhz().unwrap() - 45.5).abs() < 1e-6);
3254 assert!((entry.avg_power_db().unwrap() + 1.23).abs() < 1e-6);
3255 assert_eq!(entry.agc_gain_db(), Some(-7));
3256 assert_eq!(entry.svid, Some(110));
3257 assert_eq!(entry.lock_time_s, Some(360));
3258 assert_eq!(entry.source, Some(2));
3259 }
3260
3261 #[test]
3262 fn test_receiver_setup_parse_rev4() {
3263 let mut data = vec![0u8; 422];
3264 data[14..18].copy_from_slice(b"TEST");
3265 data[74..78].copy_from_slice(b"1234");
3266 data[94..102].copy_from_slice(b"Observer");
3267 data[114..120].copy_from_slice(b"Agency");
3268 data[154..160].copy_from_slice(b"RX1234");
3269 data[174..183].copy_from_slice(b"mosaic-X5");
3270 data[194..200].copy_from_slice(b"4.15.1");
3271 data[214..220].copy_from_slice(b"ANT123");
3272 data[234..242].copy_from_slice(b"ANT-TYPE");
3273 data[254..258].copy_from_slice(&1.25_f32.to_le_bytes());
3274 data[258..262].copy_from_slice(&(-0.5_f32).to_le_bytes());
3275 data[262..266].copy_from_slice(&0.75_f32.to_le_bytes());
3276 data[266..274].copy_from_slice(b"GEODETIC");
3277 data[286..293].copy_from_slice(b"GNSS_FW");
3278 data[326..335].copy_from_slice(b"mosaic-X5");
3279 data[366..374].copy_from_slice(&0.5_f64.to_le_bytes());
3280 data[374..382].copy_from_slice(&1.0_f64.to_le_bytes());
3281 data[382..386].copy_from_slice(&123.25_f32.to_le_bytes());
3282 data[386..390].copy_from_slice(b"TST1");
3283 data[396] = 1;
3284 data[397] = 2;
3285 data[398..401].copy_from_slice(b"BEL");
3286
3287 let header = SbfHeader {
3288 crc: 0,
3289 block_id: block_ids::RECEIVER_SETUP,
3290 block_rev: 4,
3291 length: (data.len() + 2) as u16,
3292 tow_ms: 60_000,
3293 wnc: 2300,
3294 };
3295 let block = ReceiverSetupBlock::parse(&header, &data).unwrap();
3296
3297 assert_eq!(block.tow_seconds(), 60.0);
3298 assert_eq!(block.marker_name_lossy(), "TEST");
3299 assert_eq!(block.marker_number_lossy(), "1234");
3300 assert_eq!(block.observer_lossy(), "Observer");
3301 assert_eq!(block.agency_lossy(), "Agency");
3302 assert_eq!(block.rx_serial_number_lossy(), "RX1234");
3303 assert_eq!(block.rx_name_lossy(), "mosaic-X5");
3304 assert_eq!(block.rx_version_lossy(), "4.15.1");
3305 assert_eq!(block.ant_serial_number_lossy(), "ANT123");
3306 assert_eq!(block.ant_type_lossy(), "ANT-TYPE");
3307 assert_eq!(block.delta_h_m(), 1.25);
3308 assert_eq!(block.delta_e_m(), -0.5);
3309 assert_eq!(block.delta_n_m(), 0.75);
3310 assert_eq!(block.marker_type_lossy().as_deref(), Some("GEODETIC"));
3311 assert_eq!(block.gnss_fw_version_lossy().as_deref(), Some("GNSS_FW"));
3312 assert_eq!(block.product_name_lossy().as_deref(), Some("mosaic-X5"));
3313 assert!((block.latitude_deg().unwrap() - 28.64788975654116).abs() < 1e-9);
3314 assert!((block.longitude_deg().unwrap() - 57.29577951308232).abs() < 1e-9);
3315 assert_eq!(block.height_m(), Some(123.25));
3316 assert_eq!(block.station_code_lossy().as_deref(), Some("TST1"));
3317 assert_eq!(block.monument_idx(), Some(1));
3318 assert_eq!(block.receiver_idx(), Some(2));
3319 assert_eq!(block.country_code_lossy().as_deref(), Some("BEL"));
3320 }
3321
3322 #[test]
3323 fn test_receiver_setup_dnu_reference_position() {
3324 let mut data = vec![0u8; 386];
3325 data[366..374].copy_from_slice(&F64_DNU.to_le_bytes());
3326 data[374..382].copy_from_slice(&F64_DNU.to_le_bytes());
3327 data[382..386].copy_from_slice(&F32_DNU.to_le_bytes());
3328
3329 let header = SbfHeader {
3330 crc: 0,
3331 block_id: block_ids::RECEIVER_SETUP,
3332 block_rev: 3,
3333 length: (data.len() + 2) as u16,
3334 tow_ms: 1,
3335 wnc: 2,
3336 };
3337 let block = ReceiverSetupBlock::parse(&header, &data).unwrap();
3338
3339 assert!(block.latitude_deg().is_none());
3340 assert!(block.longitude_deg().is_none());
3341 assert!(block.height_m().is_none());
3342 }
3343
3344 #[test]
3345 fn test_bb_samples_parse() {
3346 let mut data = vec![0u8; 34];
3347 data[12..14].copy_from_slice(&2_u16.to_le_bytes());
3348 data[14] = 2;
3349 data[18..22].copy_from_slice(&40_000_000_u32.to_le_bytes());
3350 data[22..26].copy_from_slice(&1_575_420_000_u32.to_le_bytes());
3351 data[26..28].copy_from_slice(&0xFF02_u16.to_le_bytes());
3352 data[28..30].copy_from_slice(&0x7F80_u16.to_le_bytes());
3353 data[30..34].copy_from_slice(&0.125_f32.to_le_bytes());
3354
3355 let header = header_for(block_ids::BB_SAMPLES, data.len(), 2000, 2100);
3356 let block = BBSamplesBlock::parse(&header, &data).unwrap();
3357
3358 assert_eq!(block.tow_seconds(), 2.0);
3359 assert_eq!(block.num_samples(), 2);
3360 assert_eq!(block.antenna_id(), 2);
3361 assert_eq!(block.sample_freq_hz(), 40_000_000);
3362 assert_eq!(block.lo_freq_hz(), 1_575_420_000);
3363 assert_eq!(block.samples()[0].raw(), 0xFF02);
3364 assert_eq!(block.sample_iq(0), Some((-1, 2)));
3365 assert_eq!(block.sample_iq(1), Some((127, -128)));
3366 assert_eq!(block.tow_delta_seconds(), Some(0.125));
3367 assert_eq!(block.first_sample_time_seconds(), Some(2.125));
3368 }
3369
3370 #[test]
3371 fn test_bb_samples_tow_delta_dnu() {
3372 let mut data = vec![0u8; 30];
3373 data[12..14].copy_from_slice(&0_u16.to_le_bytes());
3374 data[30 - 4..30].copy_from_slice(&F32_DNU.to_le_bytes());
3375
3376 let header = header_for(block_ids::BB_SAMPLES, data.len(), 0, 0);
3377 let block = BBSamplesBlock::parse(&header, &data).unwrap();
3378
3379 assert!(block.tow_delta_seconds().is_none());
3380 assert!(block.first_sample_time_seconds().is_none());
3381 }
3382
3383 #[test]
3384 fn test_ascii_in_parse() {
3385 let mut data = vec![0u8; 86];
3386 data[12] = 33;
3387 data[16..18].copy_from_slice(&5_u16.to_le_bytes());
3388 data[18..22].copy_from_slice(b"MET4");
3389 data[38..40].copy_from_slice(b"WX");
3390 data[78..83].copy_from_slice(b"hello");
3391
3392 let header = header_for(block_ids::ASCII_IN, data.len(), 9000, 2200);
3393 let block = ASCIIInBlock::parse(&header, &data).unwrap();
3394
3395 assert_eq!(block.tow_seconds(), 9.0);
3396 assert_eq!(block.connection_descriptor(), 33);
3397 assert_eq!(block.string_len(), 5);
3398 assert_eq!(block.sensor_model_lossy(), "MET4");
3399 assert_eq!(block.sensor_type_lossy(), "WX");
3400 assert_eq!(block.ascii_string(), b"hello");
3401 assert_eq!(block.ascii_text_lossy(), "hello");
3402 }
3403
3404 #[test]
3405 fn test_ascii_in_rejects_overlong_string() {
3406 let mut data = vec![0u8; 82];
3407 data[16..18].copy_from_slice(&8_u16.to_le_bytes());
3408
3409 let header = header_for(block_ids::ASCII_IN, data.len(), 0, 0);
3410 assert!(ASCIIInBlock::parse(&header, &data).is_err());
3411 }
3412
3413 #[test]
3414 fn test_commands_parse() {
3415 let mut data = vec![0u8; 20];
3416 data[14..20].copy_from_slice(&[b's', b'e', b't', 0, 0, 0]);
3417
3418 let header = header_for(block_ids::COMMANDS, data.len(), 1234, 2040);
3419 let block = CommandsBlock::parse(&header, &data).unwrap();
3420
3421 assert_eq!(block.tow_ms(), 1234);
3422 assert_eq!(block.wnc(), 2040);
3423 assert_eq!(block.cmd_data(), &[b's', b'e', b't', 0, 0, 0]);
3424 assert_eq!(block.cmd_text_lossy(), "set");
3425 }
3426
3427 #[test]
3428 fn test_comment_parse() {
3429 let mut data = vec![0u8; 20];
3430 data[12..14].copy_from_slice(&5_u16.to_le_bytes()); data[14..19].copy_from_slice(b"hello");
3432 data[19] = 0; let header = header_for(block_ids::COMMENT, data.len(), 4321, 2055);
3435 let block = CommentBlock::parse(&header, &data).unwrap();
3436
3437 assert_eq!(block.comment_len(), 5);
3438 assert_eq!(block.comment_data(), b"hello");
3439 assert_eq!(block.comment_text_lossy(), "hello");
3440 }
3441
3442 #[test]
3443 fn test_comment_parse_rejects_too_long_length() {
3444 let mut data = vec![0u8; 16];
3445 data[12..14].copy_from_slice(&8_u16.to_le_bytes()); let header = header_for(block_ids::COMMENT, data.len(), 0, 0);
3448 assert!(CommentBlock::parse(&header, &data).is_err());
3449 }
3450
3451 #[test]
3452 fn test_rtcm_datum_parse() {
3453 let mut data = vec![0u8; 79];
3454 data[12..20].copy_from_slice(b"WGS84\0\0\0");
3455 data[44..53].copy_from_slice(b"ETRS89\0\0\0");
3456 data[76] = 19;
3457 data[77] = 2;
3458 data[78] = 0xA5;
3459 let header = header_for(block_ids::RTCM_DATUM, data.len(), 100, 200);
3460 let block = RtcmDatumBlock::parse(&header, &data).unwrap();
3461 assert_eq!(block.source_crs_lossy(), "WGS84");
3462 assert_eq!(block.target_crs_lossy(), "ETRS89");
3463 assert_eq!(block.datum, 19);
3464 assert_eq!(block.height_type, 2);
3465 assert_eq!(block.quality_ind, 0xA5);
3466 }
3467
3468 #[test]
3469 fn test_lband_beams_parse() {
3470 let mut data = vec![0u8; 14 + 16];
3471 data[12] = 1;
3472 data[13] = 16;
3473 data[14] = 110;
3474 data[15..24].copy_from_slice(b"AORE\0\0\0\0\0");
3475 data[24..26].copy_from_slice(&(-1550i16).to_le_bytes());
3476 data[26..30].copy_from_slice(&1_539_982_500u32.to_le_bytes());
3477 let header = header_for(block_ids::LBAND_BEAMS, data.len(), 200, 300);
3478 let block = LBandBeamsBlock::parse(&header, &data).unwrap();
3479 assert_eq!(block.beams.len(), 1);
3480 assert_eq!(block.beams[0].svid, 110);
3481 assert_eq!(block.beams[0].sat_name_lossy(), "AORE");
3482 assert!((block.beams[0].sat_longitude_deg().unwrap() + 15.5).abs() < 1e-6);
3483 assert_eq!(block.beams[0].beam_freq_hz, 1_539_982_500);
3484 }
3485
3486 #[test]
3487 fn test_dyndns_status_parse_rev2() {
3488 let mut data = vec![0u8; 46];
3489 data[12] = 2;
3490 data[13] = 0;
3491 data[14..30].copy_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 10]);
3492 data[30..46].copy_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
3493 let header = SbfHeader {
3494 crc: 0,
3495 block_id: block_ids::DYN_DNS_STATUS,
3496 block_rev: 2,
3497 length: (data.len() + 2) as u16,
3498 tow_ms: 1,
3499 wnc: 2,
3500 };
3501 let block = DynDnsStatusBlock::parse(&header, &data).unwrap();
3502 assert_eq!(block.status, 2);
3503 assert_eq!(block.ip_address_string(), "192.168.1.10");
3504 assert!(block.ipv6_address.is_some());
3505 }
3506
3507 #[test]
3508 fn test_disk_status_parse_rev1() {
3509 let mut data = vec![0u8; 18 + 16];
3510 data[12] = 1;
3511 data[13] = 16;
3512 data[18] = 1;
3513 data[19] = 0b0010_1111;
3514 data[20..22].copy_from_slice(&1u16.to_le_bytes());
3515 data[22..26].copy_from_slice(&2u32.to_le_bytes());
3516 data[26..30].copy_from_slice(&4096u32.to_le_bytes());
3517 data[30] = 9;
3518 data[31] = 4;
3519 let header = SbfHeader {
3520 crc: 0,
3521 block_id: block_ids::DISK_STATUS,
3522 block_rev: 1,
3523 length: (data.len() + 2) as u16,
3524 tow_ms: 10,
3525 wnc: 20,
3526 };
3527 let block = DiskStatusBlock::parse(&header, &data).unwrap();
3528 assert_eq!(block.disks.len(), 1);
3529 assert_eq!(block.disks[0].disk_id, 1);
3530 assert_eq!(block.disks[0].disk_usage_bytes(), Some((1u64 << 32) | 2));
3531 assert_eq!(block.disks[0].disk_size_mb, 4096);
3532 assert_eq!(block.disks[0].error, Some(4));
3533 }
3534
3535 #[test]
3536 fn test_p2pp_status_parse() {
3537 let mut data = vec![0u8; 14 + 4];
3538 data[12] = 1;
3539 data[13] = 4;
3540 data[14] = 2;
3541 data[15] = 1;
3542 data[16] = 0b0000_0100;
3543 data[17] = 9;
3544 let header = header_for(block_ids::P2PP_STATUS, data.len(), 30, 40);
3545 let block = P2ppStatusBlock::parse(&header, &data).unwrap();
3546 assert_eq!(block.sessions.len(), 1);
3547 assert_eq!(block.sessions[0].session_id, 2);
3548 assert_eq!(block.sessions[0].port, 1);
3549 assert_eq!(block.sessions[0].status, 0b0000_0100);
3550 assert_eq!(block.sessions[0].error_code, 9);
3551 }
3552
3553 #[test]
3554 fn test_rx_message_parse() {
3555 let mut data = vec![0u8; 22 + 6];
3556 data[12] = 4;
3557 data[13] = 2;
3558 data[14..18].copy_from_slice(&77u32.to_le_bytes());
3559 data[18..20].copy_from_slice(&6u16.to_le_bytes());
3560 data[22..28].copy_from_slice(b"boot!\0");
3561 let header = header_for(block_ids::RX_MESSAGE, data.len(), 50, 60);
3562 let block = RxMessageBlock::parse(&header, &data).unwrap();
3563 assert_eq!(block.message_type, 4);
3564 assert_eq!(block.severity, 2);
3565 assert_eq!(block.message_id, 77);
3566 assert_eq!(block.message_text_lossy(), "boot!");
3567 }
3568
3569 #[test]
3570 fn test_encapsulated_output_parse() {
3571 let mut data = vec![0u8; 18 + 5];
3572 data[12] = 4;
3573 data[14..16].copy_from_slice(&5u16.to_le_bytes());
3574 data[16..18].copy_from_slice(&99u16.to_le_bytes());
3575 data[18..23].copy_from_slice(b"$GGA\n");
3576 let header = header_for(block_ids::ENCAPSULATED_OUTPUT, data.len(), 70, 80);
3577 let block = EncapsulatedOutputBlock::parse(&header, &data).unwrap();
3578 assert_eq!(block.mode, 4);
3579 assert_eq!(block.reserved_id, 99);
3580 assert_eq!(block.payload(), b"$GGA\n");
3581 }
3582
3583 #[test]
3584 fn test_gis_and_cosmos_parse() {
3585 let mut action = vec![0u8; 26 + 4];
3586 action[12..14].copy_from_slice(&4u16.to_le_bytes());
3587 action[14..18].copy_from_slice(&1u32.to_le_bytes());
3588 action[18..22].copy_from_slice(&2u32.to_le_bytes());
3589 action[22] = 3;
3590 action[23] = 4;
3591 action[24] = 5;
3592 action[26..30].copy_from_slice(b"note");
3593 let action_header = header_for(block_ids::GIS_ACTION, action.len(), 90, 91);
3594 let action_block = GisActionBlock::parse(&action_header, &action).unwrap();
3595 assert_eq!(action_block.item_id_msb, 1);
3596 assert_eq!(action_block.item_id_lsb, 2);
3597 assert_eq!(action_block.comment_text_lossy(), "note");
3598
3599 let mut gis = vec![0u8; 14 + 12];
3600 gis[12] = 1;
3601 gis[13] = 12;
3602 gis[14] = 7;
3603 gis[15] = 1;
3604 gis[16] = 2;
3605 gis[18..22].copy_from_slice(&123u32.to_le_bytes());
3606 gis[22..26].copy_from_slice(&9u32.to_le_bytes());
3607 let gis_header = header_for(block_ids::GIS_STATUS, gis.len(), 92, 93);
3608 let gis_block = GisStatusBlock::parse(&gis_header, &gis).unwrap();
3609 assert_eq!(gis_block.databases.len(), 1);
3610 assert_eq!(gis_block.databases[0].nr_items, 123);
3611
3612 let mut cosmos = vec![0u8; 13];
3613 cosmos[12] = 6;
3614 let cosmos_header = header_for(block_ids::COSMOS_STATUS, cosmos.len(), 94, 95);
3615 let cosmos_block = CosmosStatusBlock::parse(&cosmos_header, &cosmos).unwrap();
3616 assert_eq!(cosmos_block.status, 6);
3617 }
3618}