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