use num_enum::TryFromPrimitive;
use dvb_common::{Parse, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
pub enum Bandwidth {
Mhz1_7 = 0,
Mhz5 = 1,
Mhz6 = 2,
Mhz7 = 3,
Mhz8 = 4,
Mhz10 = 5,
}
impl From<Bandwidth> for u8 {
fn from(bw: Bandwidth) -> Self {
bw as u8
}
}
impl From<num_enum::TryFromPrimitiveError<Bandwidth>> for crate::error::Error {
fn from(_: num_enum::TryFromPrimitiveError<Bandwidth>) -> Self {
crate::error::Error::ReservedBitsViolation {
field: "bw",
reason: "Must be 0..=5 per ETSI TS 102 773 §5.2.7 Table 3",
}
}
}
const SUBSEC_DENOM_1_7MHZ: u64 = 131;
const SUBSEC_DENOM_5MHZ: u64 = 40;
const SUBSEC_DENOM_6MHZ: u64 = 48;
const SUBSEC_DENOM_7MHZ: u64 = 56;
const SUBSEC_DENOM_8MHZ: u64 = 64;
const SUBSEC_DENOM_10MHZ: u64 = 80;
impl Bandwidth {
pub fn subseconds_per_second(self) -> u64 {
match self {
Bandwidth::Mhz1_7 => SUBSEC_DENOM_1_7MHZ * 1_000_000,
Bandwidth::Mhz5 => SUBSEC_DENOM_5MHZ * 1_000_000,
Bandwidth::Mhz6 => SUBSEC_DENOM_6MHZ * 1_000_000,
Bandwidth::Mhz7 => SUBSEC_DENOM_7MHZ * 1_000_000,
Bandwidth::Mhz8 => SUBSEC_DENOM_8MHZ * 1_000_000,
Bandwidth::Mhz10 => SUBSEC_DENOM_10MHZ * 1_000_000,
}
}
}
const SECONDS_SINCE_2000_MAX: u64 = 0xFF_FFFF_FFFF;
const SUBSECONDS_MAX: u32 = 0x7FF_FFFF;
const UTCO_MAX: u16 = 0x1FFF;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct T2TimestampPayload {
pub bw: Bandwidth,
pub seconds_since_2000: u64,
pub subseconds: u32,
pub utco: u16,
}
const TIMESTAMP_HEADER_LEN: usize = 11;
impl<'a> Parse<'a> for T2TimestampPayload {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
if bytes.len() < TIMESTAMP_HEADER_LEN {
return Err(crate::Error::BufferTooShort {
need: TIMESTAMP_HEADER_LEN,
have: bytes.len(),
what: "T2TimestampPayload header",
});
}
if bytes[0] & 0xF0 != 0 {
return Err(crate::Error::ReservedBitsViolation {
field: "4-bit RFU",
reason: "Must be zero (ETSI TS 102 773 §5.2.7)",
});
}
let bw = Bandwidth::try_from(bytes[0] & 0x0F)?;
let seconds_since_2000 = (bytes[1] as u64) << 32
| (bytes[2] as u64) << 24
| (bytes[3] as u64) << 16
| (bytes[4] as u64) << 8
| (bytes[5] as u64);
let subseconds = (bytes[6] as u32) << 19
| (bytes[7] as u32) << 11
| (bytes[8] as u32) << 3
| ((bytes[9] >> 5) as u32 & 0x7);
let utco = ((bytes[9] as u16 & 0x1F) << 8) | (bytes[10] as u16);
Ok(T2TimestampPayload {
bw,
seconds_since_2000,
subseconds,
utco,
})
}
}
impl<'a> crate::traits::PayloadDef<'a> for T2TimestampPayload {
const PACKET_TYPE: u8 = 0x20;
const NAME: &'static str = "TIMESTAMP";
}
impl Serialize for T2TimestampPayload {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
TIMESTAMP_HEADER_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
if buf.len() < self.serialized_len() {
return Err(crate::Error::OutputBufferTooSmall {
need: self.serialized_len(),
have: buf.len(),
});
}
if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
return Err(crate::Error::ReservedBitsViolation {
field: "seconds_since_2000",
reason: "Must fit in 40 bits",
});
}
if self.subseconds > 0x7FFFFFF {
return Err(crate::Error::ReservedBitsViolation {
field: "subseconds",
reason: "Must fit in 27 bits",
});
}
if self.utco > 0x1FFF {
return Err(crate::Error::ReservedBitsViolation {
field: "utco",
reason: "Must fit in 13 bits",
});
}
buf[0] = u8::from(self.bw) & 0x0F; buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
buf[10] = (self.utco & 0xFF) as u8;
Ok(self.serialized_len())
}
}
impl T2TimestampPayload {
pub fn is_null(&self) -> bool {
self.seconds_since_2000 == SECONDS_SINCE_2000_MAX
&& self.subseconds == SUBSECONDS_MAX
&& self.utco == UTCO_MAX
}
pub fn is_relative(&self) -> bool {
self.seconds_since_2000 == 0 && !self.is_null()
}
pub fn emission_offset(&self) -> Option<core::time::Duration> {
if self.is_null() {
return None;
}
let sps = self.bw.subseconds_per_second();
let total_nanos: u128 = self.subseconds as u128 * 1_000_000_000u128 / sps as u128;
let secs = self.seconds_since_2000 + (total_nanos / 1_000_000_000) as u64;
let sub_nanos = (total_nanos % 1_000_000_000) as u32;
Some(core::time::Duration::new(secs, sub_nanos))
}
pub fn set_emission_offset(
&mut self,
offset: core::time::Duration,
) -> Result<(), crate::error::Error> {
let secs = offset.as_secs();
if secs > SECONDS_SINCE_2000_MAX {
return Err(crate::error::Error::ReservedBitsViolation {
field: "seconds_since_2000",
reason: "exceeds 40 bits",
});
}
let sps = self.bw.subseconds_per_second();
let subseconds = (offset.subsec_nanos() as u128 * sps as u128 / 1_000_000_000u128) as u32;
if subseconds > SUBSECONDS_MAX {
return Err(crate::error::Error::ReservedBitsViolation {
field: "subseconds",
reason: "exceeds 27 bits",
});
}
self.seconds_since_2000 = secs;
self.subseconds = subseconds;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bandwidth_try_from_valid() {
assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
}
#[test]
fn bandwidth_try_from_rejects_6() {
assert!(Bandwidth::try_from(6).is_err());
}
#[test]
fn exhaustive_byte_sweep() {
let mut matched = 0u16;
for byte in 0u8..=0xFF {
if let Ok(v) = Bandwidth::try_from(byte) {
assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
matched += 1;
}
}
assert_eq!(matched, 6, "expected 6 matched variants");
}
#[test]
fn parse_extracts_all_fields() {
let mut buf = [0u8; 11];
buf[0] = 0x02; buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x01; buf[6] = 0x00;
buf[7] = 0x00;
buf[8] = 0x00;
buf[9] = 0x00; buf[10] = 0x00;
let result = T2TimestampPayload::parse(&buf).unwrap();
assert_eq!(result.bw, Bandwidth::Mhz6);
assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
}
#[test]
fn parse_rejects_nonzero_rfu() {
let buf = [
0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(T2TimestampPayload::parse(&buf).is_err());
}
#[test]
fn parse_rejects_short_buffer() {
assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
}
#[test]
fn serialize_round_trip() {
let orig = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: 0x00_00_01_02_03,
subseconds: 0x0123456,
utco: 0x7FF,
};
let mut buf = [0u8; 11];
orig.serialize_into(&mut buf).unwrap();
let parsed = T2TimestampPayload::parse(&buf).unwrap();
assert_eq!(orig, parsed);
}
#[test]
fn null_timestamp_all_ones() {
let mut buf = [0xFFu8; 11];
buf[0] = 0x0F; buf[0] = 0x0F; buf[0] = 0x00; buf[0] = 0x02; buf[1..11].fill(0xFF);
let result = T2TimestampPayload::parse(&buf);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
#[test]
fn subseconds_per_second_per_table4() {
assert_eq!(
Bandwidth::Mhz1_7.subseconds_per_second(),
131_000_000,
"1.7 MHz: D=131"
);
assert_eq!(
Bandwidth::Mhz5.subseconds_per_second(),
40_000_000,
"5 MHz: D=40"
);
assert_eq!(
Bandwidth::Mhz6.subseconds_per_second(),
48_000_000,
"6 MHz: D=48"
);
assert_eq!(
Bandwidth::Mhz7.subseconds_per_second(),
56_000_000,
"7 MHz: D=56"
);
assert_eq!(
Bandwidth::Mhz8.subseconds_per_second(),
64_000_000,
"8 MHz: D=64"
);
assert_eq!(
Bandwidth::Mhz10.subseconds_per_second(),
80_000_000,
"10 MHz: D=80"
);
}
#[test]
fn emission_offset_known_values() {
let p = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: 100,
subseconds: 32_000_000,
utco: 0,
};
assert_eq!(
p.emission_offset(),
Some(core::time::Duration::new(100, 500_000_000))
);
let p2 = T2TimestampPayload {
bw: Bandwidth::Mhz6,
seconds_since_2000: 200,
subseconds: 12_000_000,
utco: 0,
};
assert_eq!(
p2.emission_offset(),
Some(core::time::Duration::new(200, 250_000_000))
);
}
#[test]
fn set_emission_offset_round_trips() {
let mut p = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: 0,
subseconds: 0,
utco: 0,
};
let dur = core::time::Duration::new(12345, 500_000_000);
p.set_emission_offset(dur).unwrap();
assert_eq!(p.emission_offset(), Some(dur));
}
#[test]
fn null_timestamp_offset_is_none() {
let p = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: SECONDS_SINCE_2000_MAX,
subseconds: SUBSECONDS_MAX,
utco: UTCO_MAX,
};
assert!(p.is_null());
assert_eq!(p.emission_offset(), None);
}
#[test]
fn relative_timestamp_flag() {
let p = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: 0,
subseconds: 1000,
utco: 0,
};
assert!(p.is_relative());
assert!(!p.is_null());
assert!(p.emission_offset().is_some());
}
}