use crate::crc::crc16_ccitt;
use crate::error::{SbfError, SbfResult};
pub const SBF_SYNC: [u8; 2] = [0x24, 0x40];
pub const MIN_BLOCK_LENGTH: u16 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SbfHeader {
pub crc: u16,
pub block_id: u16,
pub block_rev: u8,
pub length: u16,
pub tow_ms: u32,
pub wnc: u16,
}
impl SbfHeader {
pub fn parse(data: &[u8]) -> SbfResult<Self> {
if data.len() < 6 {
return Err(SbfError::IncompleteBlock {
needed: 6,
have: data.len(),
});
}
let crc = u16::from_le_bytes([data[0], data[1]]);
let id_rev = u16::from_le_bytes([data[2], data[3]]);
let length = u16::from_le_bytes([data[4], data[5]]);
let block_id = id_rev & 0x1FFF;
let block_rev = ((id_rev >> 13) & 0x07) as u8;
if length < MIN_BLOCK_LENGTH || (length & 0x03) != 0 {
return Err(SbfError::InvalidLength(length));
}
let (tow_ms, wnc) = if data.len() >= 12 {
(
u32::from_le_bytes([data[6], data[7], data[8], data[9]]),
u16::from_le_bytes([data[10], data[11]]),
)
} else {
(0xFFFFFFFF, 0xFFFF)
};
Ok(Self {
crc,
block_id,
block_rev,
length,
tow_ms,
wnc,
})
}
pub fn parse_from_block(block_data: &[u8]) -> SbfResult<Self> {
if block_data.len() < 2 {
return Err(SbfError::IncompleteBlock {
needed: 2,
have: block_data.len(),
});
}
if block_data[0] != SBF_SYNC[0] || block_data[1] != SBF_SYNC[1] {
return Err(SbfError::InvalidSync);
}
Self::parse(&block_data[2..])
}
pub fn validate_crc(&self, block_data: &[u8]) -> SbfResult<()> {
let length = self.length as usize;
if block_data.len() < length {
return Err(SbfError::IncompleteBlock {
needed: length,
have: block_data.len(),
});
}
let calculated_crc = crc16_ccitt(&block_data[4..length]);
if calculated_crc != self.crc {
return Err(SbfError::CrcMismatch {
expected: self.crc,
actual: calculated_crc,
});
}
Ok(())
}
pub fn tow_seconds(&self) -> Option<f64> {
if self.tow_ms == 0xFFFFFFFF {
None
} else {
Some(self.tow_ms as f64 * 0.001)
}
}
pub fn tow_ms_raw(&self) -> Option<u32> {
if self.tow_ms == 0xFFFFFFFF {
None
} else {
Some(self.tow_ms)
}
}
pub fn week_number(&self) -> Option<u16> {
if self.wnc == 0xFFFF {
None
} else {
Some(self.wnc)
}
}
pub fn has_valid_time(&self) -> bool {
self.tow_ms != 0xFFFFFFFF && self.wnc != 0xFFFF
}
pub const fn body_offset() -> usize {
12 }
}
impl std::fmt::Display for SbfHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SbfHeader {{ id: {}, rev: {}, len: {}, tow: {}ms, wnc: {} }}",
self.block_id, self.block_rev, self.length, self.tow_ms, self.wnc
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_parse() {
let data = [
0x00, 0x00, 0xAB, 0x0F, 0x10, 0x00, 0xE8, 0x03, 0x00, 0x00, 0x64, 0x00, ];
let header = SbfHeader::parse(&data).unwrap();
assert_eq!(header.block_id, 4011);
assert_eq!(header.block_rev, 0);
assert_eq!(header.length, 16);
assert_eq!(header.tow_ms, 1000);
assert_eq!(header.wnc, 100);
}
#[test]
fn test_header_id_rev_extraction() {
let data = [
0x00, 0x00, 0xA7, 0x4F, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let header = SbfHeader::parse(&data).unwrap();
assert_eq!(header.block_id, 4007);
assert_eq!(header.block_rev, 2);
}
#[test]
fn test_header_invalid_length() {
let data = [
0x00, 0x00, 0xAB, 0x0F, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = SbfHeader::parse(&data);
assert!(matches!(result, Err(SbfError::InvalidLength(9))));
}
#[test]
fn test_header_too_short() {
let data = [0x00, 0x00, 0x00];
let result = SbfHeader::parse(&data);
assert!(matches!(result, Err(SbfError::IncompleteBlock { .. })));
}
#[test]
fn test_tow_seconds() {
let header = SbfHeader {
crc: 0,
block_id: 4007,
block_rev: 0,
length: 16,
tow_ms: 1500,
wnc: 100,
};
assert_eq!(header.tow_seconds(), Some(1.5));
let header_no_tow = SbfHeader {
tow_ms: 0xFFFFFFFF,
..header
};
assert_eq!(header_no_tow.tow_seconds(), None);
}
#[test]
fn test_parse_from_block_with_sync() {
let block = [
0x24, 0x40, 0x00, 0x00, 0xAB, 0x0F, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let header = SbfHeader::parse_from_block(&block).unwrap();
assert_eq!(header.block_id, 4011);
}
#[test]
fn test_parse_from_block_invalid_sync() {
let block = [
0x00, 0x00, 0x00, 0x00, 0xAB, 0x0F, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = SbfHeader::parse_from_block(&block);
assert!(matches!(result, Err(SbfError::InvalidSync)));
}
}