Skip to main content

sbf_tools/blocks/
status.rs

1//! Status and miscellaneous blocks
2
3use 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
14// ============================================================================
15// Constants
16// ============================================================================
17
18fn 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// ============================================================================
39// ReceiverStatus Block
40// ============================================================================
41
42/// AGC (Automatic Gain Control) data for each frontend
43#[derive(Debug, Clone)]
44pub struct AgcData {
45    /// Frontend identifier
46    pub frontend_id: u8,
47    /// AGC gain in dB
48    pub gain_db: i8,
49    /// Sample variance
50    pub sample_var: u8,
51    /// Blanking statistics
52    pub blanking_stat: u8,
53}
54
55/// ReceiverStatus block
56///
57/// Receiver status and health information.
58#[derive(Debug, Clone)]
59pub struct ReceiverStatusBlock {
60    tow_ms: u32,
61    wnc: u16,
62    /// CPU load percentage
63    pub cpu_load: u8,
64    /// External error code
65    pub ext_error: u8,
66    /// Receiver uptime in seconds
67    pub uptime_s: u32,
68    /// Receiver state bitfield
69    pub rx_state: u32,
70    /// Receiver error bitfield
71    pub rx_error: u32,
72    /// Command cyclic counter (Rev 1+)
73    cmd_count: Option<u8>,
74    /// Raw temperature byte with +100 deg C offset (Rev 1+)
75    temperature_raw: Option<u8>,
76    /// AGC data per frontend
77    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    /// Check if receiver has any errors
92    pub fn has_errors(&self) -> bool {
93        self.rx_error != 0 || self.ext_error != 0
94    }
95
96    /// Command cyclic counter, when present.
97    pub fn cmd_count(&self) -> Option<u8> {
98        self.cmd_count
99    }
100
101    /// Raw temperature byte, when present.
102    pub fn temperature_raw(&self) -> Option<u8> {
103        self.temperature_raw
104    }
105
106    /// Receiver temperature in deg C, when present and valid.
107    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    /// Get uptime as Duration-like struct (hours, minutes, seconds)
118    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        // Offsets:
136        // 12: CPULoad
137        // 13: ExtError
138        // 14-17: UpTime (u32)
139        // 18-21: RxState (u32)
140        // 22-25: RxError (u32)
141        // 26: N (number of AGC sub-blocks)
142        // 27: SBLength
143
144        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        // Rev 1+ layout (documented in mosaic-X5 FW v4.15.1):
155        // 26: N, 27: SBLength, 28: CmdCount, 29: Temperature, 30+: AGCState sub-blocks
156        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            // Legacy fallback for pre-Rev1 payloads that include N/SBLength
187            // directly followed by AGC sub-blocks.
188            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// ============================================================================
232// ChannelStatus Block
233// ============================================================================
234
235/// Channel state information for a tracking channel
236#[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/// Satellite tracking information
245#[derive(Debug, Clone)]
246pub struct ChannelSatInfo {
247    /// Satellite ID
248    pub sat_id: SatelliteId,
249    /// Frequency number (for GLONASS, raw field value)
250    pub freq_nr: u8,
251    /// Azimuth in degrees (raw value, 0..359)
252    azimuth_raw: u16,
253    /// Rise/set indicator
254    pub rise_set: u8,
255    /// Elevation in degrees (raw)
256    elevation_raw: i8,
257    /// Health status
258    pub health_status: u16,
259    /// Channel states
260    pub states: Vec<ChannelState>,
261}
262
263impl ChannelSatInfo {
264    /// Get azimuth in degrees
265    pub fn azimuth_deg(&self) -> f64 {
266        self.azimuth_raw as f64
267    }
268
269    /// Get elevation in degrees
270    pub fn elevation_deg(&self) -> f64 {
271        self.elevation_raw as f64
272    }
273
274    /// Check if satellite is rising
275    pub fn is_rising(&self) -> bool {
276        self.rise_set == 1
277    }
278
279    /// Check if satellite is setting
280    pub fn is_setting(&self) -> bool {
281        self.rise_set == 0
282    }
283
284    /// Check if rise/set state is unknown
285    pub fn is_rise_set_unknown(&self) -> bool {
286        self.rise_set == 3
287    }
288}
289
290/// ChannelStatus block (Block ID 4013)
291///
292/// Detailed tracking status per channel.
293#[derive(Debug, Clone)]
294pub struct ChannelStatusBlock {
295    tow_ms: u32,
296    wnc: u16,
297    /// Satellite tracking info
298    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    /// Get number of tracked satellites
313    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        // Offsets:
327        // 12: N1 (number of satellites)
328        // 13: SB1Length
329        // 14: SB2Length
330        // 15-17: Reserved
331
332        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            // Sub-block 1 structure:
351            // 0: SVID
352            // 1: FreqNr
353            // 2-3: SVIDFull
354            // 4-5: Azimuth_RiseSet (packed)
355            // 6-7: HealthStatus
356            // 8: Elevation
357            // 9: N2 (number of channel states)
358            // 10: RxChannel
359            // 11: Reserved
360
361            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            // Parse channel states (sub-block 2).
375            // Byte 1 in each sub-block is reserved and must be skipped.
376            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// ============================================================================
424// SatVisibility Block
425// ============================================================================
426
427/// Satellite visibility information
428#[derive(Debug, Clone)]
429pub struct SatVisibilityInfo {
430    /// Satellite ID
431    pub sat_id: SatelliteId,
432    /// Frequency number (for GLONASS, raw field value)
433    pub freq_nr: u8,
434    /// Azimuth (raw, multiply by 0.01 for degrees)
435    azimuth_raw: u16,
436    /// Elevation (raw, multiply by 0.01 for degrees)
437    elevation_raw: i16,
438    /// Rise/set indicator (0=setting, 1=rising)
439    pub rise_set: u8,
440    /// Satellite info flags
441    pub satellite_info: u8,
442}
443
444impl SatVisibilityInfo {
445    /// Get azimuth in degrees
446    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    /// Get raw azimuth
455    pub fn azimuth_raw(&self) -> u16 {
456        self.azimuth_raw
457    }
458
459    /// Get elevation in degrees
460    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    /// Get raw elevation
469    pub fn elevation_raw(&self) -> i16 {
470        self.elevation_raw
471    }
472
473    /// Check if satellite is rising
474    pub fn is_rising(&self) -> bool {
475        self.rise_set == 1
476    }
477
478    /// Check if rise/set state is unknown
479    pub fn is_rise_set_unknown(&self) -> bool {
480        self.rise_set == 255
481    }
482
483    /// Check if satellite is above horizon
484    pub fn is_above_horizon(&self) -> bool {
485        self.elevation_deg().map(|e| e > 0.0).unwrap_or(false)
486    }
487}
488
489/// SatVisibility block (Block ID 4012)
490///
491/// Satellite visibility (azimuth, elevation) information.
492#[derive(Debug, Clone)]
493pub struct SatVisibilityBlock {
494    tow_ms: u32,
495    wnc: u16,
496    /// Satellite visibility data
497    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    /// Get number of visible satellites
512    pub fn num_satellites(&self) -> usize {
513        self.satellites.len()
514    }
515
516    /// Get satellites above a minimum elevation
517    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    /// Get visibility info for a specific satellite
529    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        // Offsets:
543        // 12: N (number of satellites)
544        // 13: SBLength (sub-block length)
545
546        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            // Sub-block structure:
564            // 0: SVID
565            // 1: FreqNr
566            // 2-3: Azimuth (u16 * 0.01)
567            // 4-5: Elevation (i16 * 0.01)
568            // 6: RiseSet
569            // 7: SatelliteInfo
570
571            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// ============================================================================
601// QualityInd Block
602// ============================================================================
603
604/// QualityInd block (Block ID 4082)
605///
606/// Quality indicator values (raw indicator list).
607#[derive(Debug, Clone)]
608pub struct QualityIndBlock {
609    tow_ms: u32,
610    wnc: u16,
611    /// Raw quality indicators (u16 values)
612    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        // Offsets:
640        // 12: N (number of indicators)
641        // 13: Reserved
642        // 14+: N * u16 indicators
643        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// ============================================================================
672// InputLink Block
673// ============================================================================
674
675/// Input link statistics entry
676#[derive(Debug, Clone)]
677pub struct InputLinkStats {
678    /// Connection descriptor
679    pub connection_descriptor: u8,
680    /// Link type
681    pub link_type: u8,
682    /// Age of last message (raw, seconds)
683    age_last_message_raw: u16,
684    /// Bytes received
685    pub bytes_received: u32,
686    /// Bytes accepted
687    pub bytes_accepted: u32,
688    /// Messages received
689    pub messages_received: u32,
690    /// Messages accepted
691    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/// InputLink block (Block ID 4090)
705#[derive(Debug, Clone)]
706pub struct InputLinkBlock {
707    tow_ms: u32,
708    wnc: u16,
709    /// Input link statistics
710    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// ============================================================================
786// OutputLink Block
787// ============================================================================
788
789/// Output type entry
790#[derive(Debug, Clone)]
791pub struct OutputType {
792    /// Output type
793    pub output_type: u8,
794    /// Percentage of output bandwidth
795    pub percentage: u8,
796}
797
798/// Output link statistics entry
799#[derive(Debug, Clone)]
800pub struct OutputLinkStats {
801    /// Connection descriptor
802    pub connection_descriptor: u8,
803    /// Allowed rate (raw, kbyte/s)
804    allowed_rate_raw: u16,
805    /// Bytes produced
806    pub bytes_produced: u32,
807    /// Bytes sent
808    pub bytes_sent: u32,
809    /// Number of clients
810    pub nr_clients: u8,
811    /// Output types
812    pub output_types: Vec<OutputType>,
813}
814
815impl OutputLinkStats {
816    /// Allowed rate in kbyte/s (as provided by the block).
817    pub fn allowed_rate_kbytes_per_s(&self) -> u16 {
818        self.allowed_rate_raw
819    }
820
821    /// Allowed rate in bytes/s (decimal kilobytes).
822    pub fn allowed_rate_bytes_per_s(&self) -> u32 {
823        self.allowed_rate_raw as u32 * 1000
824    }
825
826    /// Legacy accessor kept for compatibility.
827    ///
828    /// The name is historical; it returns the raw `AllowedRate` value (kbyte/s),
829    /// wrapped in `Some` because this field has no documented DNU sentinel.
830    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/// OutputLink block (Block ID 4091)
840#[derive(Debug, Clone)]
841pub struct OutputLinkBlock {
842    tow_ms: u32,
843    wnc: u16,
844    /// Output link statistics
845    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        // Bytes 15..17 are reserved.
884        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// ============================================================================
936// IPStatus Block
937// ============================================================================
938
939/// IPStatus block (Block ID 4058)
940///
941/// Ethernet IP address, gateway, MAC address, and netmask status.
942#[derive(Debug, Clone)]
943pub struct IpStatusBlock {
944    tow_ms: u32,
945    wnc: u16,
946    /// MAC address (6 bytes)
947    pub mac_address: [u8; 6],
948    /// IP address (16 bytes, supports IPv4/IPv6)
949    pub ip_address: [u8; 16],
950    /// Gateway address (16 bytes)
951    pub gateway: [u8; 16],
952    /// Network mask prefix length (e.g. 24 for /24)
953    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    /// Format MAC address as colon-separated hex (e.g. "AA:BB:CC:DD:EE:FF")
968    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    /// Format IPv4 address if first 12 bytes are zero (IPv4-mapped)
977    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            // IPv6 or other - format as hex groups
985            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    /// Format gateway address (same logic as IP)
994    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; // 12 + 6 + 16 + 16 + 1
1015        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// ============================================================================
1039// LBandTrackerStatus Block
1040// ============================================================================
1041
1042/// One L-band tracker entry from `LBandTrackerStatus`.
1043#[derive(Debug, Clone)]
1044pub struct LBandTrackerData {
1045    /// Nominal beam frequency (Hz)
1046    pub frequency_hz: u32,
1047    /// Beam baudrate (baud)
1048    pub baudrate: u16,
1049    /// Beam service ID
1050    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    /// Current operation mode
1056    pub mode: u8,
1057    /// Current tracker status (0=Idle, 1=Search, 2=FrameSearch, 3=Locked)
1058    pub status: u8,
1059    /// Satellite ID (Rev 2+)
1060    pub svid: Option<u8>,
1061    /// Lock time in seconds (Rev 1+)
1062    pub lock_time_s: Option<u16>,
1063    /// Source tracker module (Rev 3+)
1064    pub source: Option<u8>,
1065}
1066
1067impl LBandTrackerData {
1068    /// Demodulator frequency offset in Hz.
1069    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    /// C/N0 in dB-Hz (raw * 0.01).
1078    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    /// Average power in dB (raw * 0.01).
1091    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    /// L-band AGC gain in dB.
1104    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/// LBandTrackerStatus block (Block ID 4201)
1118///
1119/// General L-band tracker status and per-tracker demodulator information.
1120#[derive(Debug, Clone)]
1121pub struct LBandTrackerStatusBlock {
1122    tow_ms: u32,
1123    wnc: u16,
1124    /// Number of `TrackData` sub-blocks
1125    pub n: u8,
1126    /// Length of one `TrackData` sub-block
1127    pub sb_length: u8,
1128    /// Parsed tracker entries
1129    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
1237// ============================================================================
1238// ReceiverSetup Block
1239// ============================================================================
1240
1241fn field_text(bytes: &[u8]) -> String {
1242    String::from_utf8_lossy(trim_trailing_nuls(bytes)).into_owned()
1243}
1244
1245/// ReceiverSetup block (Block ID 5902)
1246///
1247/// Receiver installation metadata and RINEX header information.
1248#[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// ============================================================================
1501// BBSamples Block
1502// ============================================================================
1503
1504/// One complex baseband sample from `BBSamples`.
1505#[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    /// In-phase component (I), stored in the upper byte.
1516    pub fn i(&self) -> i8 {
1517        (self.raw >> 8) as u8 as i8
1518    }
1519
1520    /// Quadrature component (Q), stored in the lower byte.
1521    pub fn q(&self) -> i8 {
1522        self.raw as u8 as i8
1523    }
1524}
1525
1526/// BBSamples block (Block ID 4040)
1527///
1528/// Complex baseband samples for RF monitoring and spectral analysis.
1529#[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// ============================================================================
1636// ASCIIIn Block
1637// ============================================================================
1638
1639/// ASCIIIn block (Block ID 4075)
1640///
1641/// Raw ASCII line received on a configured receiver input port.
1642#[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// ============================================================================
1725// Commands Block
1726// ============================================================================
1727
1728/// Commands block (Block ID 4015)
1729///
1730/// Logs command data entered by the user.
1731#[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    /// Best-effort UTF-8 text view of command data.
1753    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// ============================================================================
1777// NTRIP Client / Server status (4053 / 4122)
1778// ============================================================================
1779
1780/// One NTRIP connection slot (`NTRIPClientConnection` in SBF; server block uses the same layout).
1781#[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/// NTRIP client connection status (Block ID 4053).
1824#[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/// NTRIP server connection status (Block ID 4122).
1861///
1862/// Uses the same binary layout as the client status block (common Septentrio pairing). If a
1863/// firmware revision differs, compare with the SBF reference for your receiver.
1864#[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// ============================================================================
1901// RFStatus (4092)
1902// ============================================================================
1903
1904/// One RF mitigation band entry (`RFBand` in SBF).
1905#[derive(Debug, Clone)]
1906pub struct RfBandEntry {
1907    pub frequency_hz: u32,
1908    pub bandwidth: u16,
1909    pub info: u8,
1910}
1911
1912/// RF interference mitigation status (Block ID 4092).
1913#[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// ============================================================================
1981// Comment Block
1982// ============================================================================
1983
1984/// Comment block (Block ID 5936)
1985///
1986/// User comment string entered with `setObserverComment`.
1987#[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    /// Best-effort UTF-8 text view of the comment bytes.
2013    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// ============================================================================
2044// RTCMDatum / LBandBeams / DynDNSStatus / DiskStatus / P2PPStatus
2045// ============================================================================
2046
2047/// RTCM datum information from the correction provider (4049).
2048#[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/// One `BeamInfo` entry from `LBandBeams`.
2102#[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/// L-band beam list (4204).
2124#[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/// DynDNS status (4105).
2190#[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/// One `DiskData` entry from `DiskStatus`.
2251#[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/// Disk usage and status (4059).
2273#[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/// One `P2PPSession` entry from `P2PPStatus`.
2348#[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/// P2PP session status (4238).
2357#[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/// Cosmos service status (4243).
2421#[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// ============================================================================
2456// RxMessage / EncapsulatedOutput / GIS
2457// ============================================================================
2458
2459/// Receiver activity log entry (4103).
2460#[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/// Encapsulated non-SBF output message (4097).
2516#[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/// GIS action entry (4106).
2567#[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/// One `DatabaseStatus` entry from `GISStatus`.
2627#[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/// GIS database status (4107).
2637#[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,  // 180.00 degrees
2725            elevation_raw: 4500, // 45.00 degrees
2726            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, // 1 hour, 1 minute, 1 second
2826            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; // CPULoad
2843        data[13] = 2; // ExtError
2844        data[14..18].copy_from_slice(&120_u32.to_le_bytes()); // UpTime
2845        data[18..22].copy_from_slice(&0x0001_0002_u32.to_le_bytes()); // RxState
2846        data[22..26].copy_from_slice(&0x0000_0200_u32.to_le_bytes()); // RxError
2847        data[26] = 1; // N
2848        data[27] = 4; // SBLength
2849        data[28] = 11; // CmdCount
2850        data[29] = 123; // Temperature raw => 23 degC
2851        data[30] = 9; // FrontEndID
2852        data[31] = (-12_i8) as u8; // Gain
2853        data[32] = 100; // SampleVar
2854        data[33] = 3; // BlankingStat
2855
2856        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; // N
2916        data[13] = 20; // SBLength
2917
2918        let offset = 14;
2919        data[offset] = 3; // CD
2920        data[offset + 1] = 4; // Type
2921        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; // N
2943        data[13] = 0; // Reserved
2944
2945        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); // includes sync bytes
2963        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()); // CRC (unused)
2968        data[4..6].copy_from_slice(&block_ids::QUALITY_IND.to_le_bytes()); // ID/Rev
2969        data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); // Length
2970        data[8..12].copy_from_slice(&1000_u32.to_le_bytes()); // TOW
2971        data[12..14].copy_from_slice(&2000_u16.to_le_bytes()); // WNc
2972        data[14] = n as u8;
2973        data[15] = 0; // Reserved
2974
2975        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; // N1
3044        data[13] = 13; // SB1Length
3045        data[14] = 2; // SB2Length
3046
3047        // 15..17 reserved
3048        let offset = 18;
3049        data[offset] = 1; // CD
3050        data[offset + 1] = 1; // N2
3051        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; // NrClients
3055
3056        let type_offset = offset + 13;
3057        data[type_offset] = 7; // Output type
3058        data[type_offset + 1] = 80; // Percentage
3059
3060        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        // 2 sync + 12 header + 3 base fields + 3 reserved + 12 sat info + 8 state info = 40
3076        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()); // CRC (unused)
3081        data[4..6].copy_from_slice(&block_ids::TRACKING_STATUS.to_le_bytes()); // ID/Rev
3082        data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); // Length
3083        data[8..12].copy_from_slice(&12345_u32.to_le_bytes()); // TOW
3084        data[12..14].copy_from_slice(&2045_u16.to_le_bytes()); // WNc
3085
3086        // Block payload starts at absolute offset 14 (offset 12 in parse() data slice)
3087        data[14] = 1; // N1
3088        data[15] = 12; // SB1Length
3089        data[16] = 8; // SB2Length
3090        data[17] = 0; // Reserved
3091        data[18] = 0; // Reserved
3092        data[19] = 0; // Reserved
3093
3094        let sb1 = 20;
3095        data[sb1] = 5; // SVID
3096        data[sb1 + 1] = 0; // FreqNr
3097        data[sb1 + 2..sb1 + 4].copy_from_slice(&0_u16.to_le_bytes()); // SVIDFull
3098        let az_rise_set = (1_u16 << 14) | 180_u16; // rise=1, azimuth=180 deg
3099        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()); // HealthStatus
3101        data[sb1 + 8] = 45; // Elevation
3102        data[sb1 + 9] = 1; // N2
3103        data[sb1 + 10] = 3; // RxChannel
3104        data[sb1 + 11] = 0; // Reserved
3105
3106        let sb2 = sb1 + 12;
3107        data[sb2] = 0; // Antenna
3108        data[sb2 + 1] = 0; // Reserved
3109        data[sb2 + 2..sb2 + 4].copy_from_slice(&3_u16.to_le_bytes()); // TrackingStatus
3110        data[sb2 + 4..sb2 + 6].copy_from_slice(&2_u16.to_le_bytes()); // PVTStatus
3111        data[sb2 + 6..sb2 + 8].copy_from_slice(&0x1234_u16.to_le_bytes()); // PVTInfo
3112
3113        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; // N
3158        data[13] = sb_length as u8; // SBLength
3159
3160        let offset = 14;
3161        data[offset..offset + 4].copy_from_slice(&1_545_000_000_u32.to_le_bytes()); // Frequency
3162        data[offset + 4..offset + 6].copy_from_slice(&1200_u16.to_le_bytes()); // Baudrate
3163        data[offset + 6..offset + 8].copy_from_slice(&42_u16.to_le_bytes()); // ServiceID
3164        data[offset + 8..offset + 12].copy_from_slice(&12.5_f32.to_le_bytes()); // FreqOffset
3165        data[offset + 12..offset + 14].copy_from_slice(&4550_u16.to_le_bytes()); // CN0 (45.50 dB-Hz)
3166        data[offset + 14..offset + 16].copy_from_slice(&(-123_i16).to_le_bytes()); // AvgPower
3167        data[offset + 16] = (-7_i8) as u8; // AGCGain
3168        data[offset + 17] = 0; // Mode
3169        data[offset + 18] = 3; // Status (Locked)
3170        data[offset + 19] = 110; // SVID (Rev2+)
3171        data[offset + 20..offset + 22].copy_from_slice(&360_u16.to_le_bytes()); // LockTime (Rev1+)
3172        data[offset + 22] = 2; // Source (Rev3+)
3173
3174        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()); // CommentLn
3371        data[14..19].copy_from_slice(b"hello");
3372        data[19] = 0; // Padding
3373
3374        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()); // CommentLn exceeds payload
3386
3387        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}