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