use crate::error::{Error, Result};
use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::len::{need_in, need_out};
use broadcast_common::{Parse, Serialize};
pub const TABLE_ID: u8 = 0x4D;
pub const PID: mpeg_ts::pid::Pid = mpeg_ts::pid::Pid::new(0x001B);
const HEADER_LEN: usize = 9;
const SECTION_LENGTH_PREFIX: usize = 3;
const SAT_BODY_STACK_BUF: usize = 4096;
const MAX_SECTION_LENGTH: usize = 0x0FFF;
const SATELLITE_TABLE_ID_SHIFT: u8 = 2;
const TABLE_COUNT_HI_MASK: u8 = 0x03;
use super::CRC_LEN;
fn pad_to_byte(bits: usize) -> usize {
(8 - (bits % 8)) % 8
}
struct BitReader<'a> {
inner: broadcast_common::bits::BitReader<'a>,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
inner: broadcast_common::bits::BitReader::new(data),
}
}
fn remaining_bits(&self) -> usize {
self.inner.bits_remaining()
}
fn bits_consumed(&self) -> usize {
self.inner.bits_read()
}
fn overrun(&self, bits: usize) -> Error {
Error::BufferTooShort(broadcast_common::error::BufferTooShort {
need: self.inner.bits_read() + bits,
have: self.inner.total_bits(),
what: "SatSection bit reader overrun",
})
}
fn read_u(&mut self, bits: u8) -> Result<u64> {
self.inner
.read_bits(u32::from(bits))
.map_err(|_| self.overrun(bits as usize))
}
fn read_i(&mut self, bits: u8) -> Result<i64> {
let raw = self.read_u(bits)?;
let bits = bits as usize;
if raw & (1u64 << (bits - 1)) != 0 {
Ok((raw as i64) | (!0i64 << bits))
} else {
Ok(raw as i64)
}
}
fn seek_forward(&mut self, target_bits: usize) {
let target = target_bits.min(self.inner.total_bits());
let _ = self.inner.skip_bits(target - self.inner.bits_read());
}
fn skip(&mut self, bits: u8) -> Result<()> {
self.inner
.skip_bits(bits as usize)
.map_err(|_| self.overrun(bits as usize))
}
}
struct BitWriter<'a> {
buf: &'a mut [u8],
bit_pos: usize,
}
impl<'a> BitWriter<'a> {
fn new(buf: &'a mut [u8]) -> Self {
Self { buf, bit_pos: 0 }
}
fn bits_written(&self) -> usize {
self.bit_pos
}
fn write_u(&mut self, bits: u8, val: u64) -> Result<()> {
broadcast_common::len::fit_bits(val, u32::from(bits), "SatSection field")?;
let bits = bits as usize;
if self.bit_pos + bits > self.buf.len() * 8 {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: self.bit_pos + bits,
have: self.buf.len() * 8,
what: "SatSection bit writer overrun",
},
));
}
for i in 0..bits {
let byte_idx = (self.bit_pos + i) / 8;
let bit_idx = 7 - ((self.bit_pos + i) % 8);
let bit_val = ((val >> (bits - 1 - i)) & 1) as u8;
self.buf[byte_idx] |= bit_val << bit_idx;
}
self.bit_pos += bits;
Ok(())
}
fn write_i(&mut self, bits: u8, val: i64) -> Result<()> {
let max = (1i64 << (bits - 1)) - 1;
let min = -(1i64 << (bits - 1));
if val < min || val > max {
return Err(broadcast_common::len::FieldOverflow {
field: "SatSection signed field",
value: val.unsigned_abs(),
max: max as u64,
}
.into());
}
self.write_u(bits, val as u64 & ((1u64 << bits) - 1))
}
fn write_zero(&mut self, bits: u8) -> Result<()> {
if self.bit_pos + bits as usize > self.buf.len() * 8 {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: self.bit_pos + bits as usize,
have: self.buf.len() * 8,
what: "SatSection bit writer overrun",
},
));
}
self.bit_pos += bits as usize;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
#[non_exhaustive]
pub enum SatTableId {
PositionV2 = 0,
CellFragment = 1,
TimeAssociation = 2,
BeamhoppingTimePlan = 3,
PositionV3 = 4,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum PositionSystem {
Orbital {
orbital_position: u16,
west_east_flag: bool,
reserved_zero_future_use: u8,
},
Sgp4 {
epoch_year: u8,
day_of_the_year: u16,
day_fraction: u32,
mean_motion_first_derivative: u32,
mean_motion_second_derivative: u32,
drag_term: u32,
inclination: u32,
right_ascension: u32,
eccentricity: u32,
argument_of_perigree: u32,
mean_anomaly: u32,
mean_motion: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PositionV2Satellite {
pub satellite_id: u32,
pub position: PositionSystem,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PositionV2Body {
pub satellites: Vec<PositionV2Satellite>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CellCentre {
pub centre_latitude: i32,
pub centre_longitude: i32,
pub max_distance: u32,
pub reserved_zero_future_use_4: u8,
pub reserved_zero_future_use_5: u8,
pub reserved_zero_future_use_6: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct NewDeliverySystem {
pub new_delivery_system_id: u32,
pub time_of_application_base: u64,
pub time_of_application_ext: u16,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ObsolescentDeliverySystem {
pub obsolescent_delivery_system_id: u32,
pub time_of_obsolescence_base: u64,
pub time_of_obsolescence_ext: u16,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CellFragment {
pub cell_fragment_id: u32,
pub first_occurrence: bool,
pub last_occurrence: bool,
pub centre: Option<CellCentre>,
pub delivery_system_ids: Vec<u32>,
pub new_delivery_systems: Vec<NewDeliverySystem>,
pub obsolescent_delivery_systems: Vec<ObsolescentDeliverySystem>,
pub reserved_zero_future_use_4: u8,
pub reserved_zero_future_use_new: u8,
pub reserved_zero_future_use_obsolescent: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CellFragmentBody {
pub fragments: Vec<CellFragment>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum AssociationType {
UtcWithoutLeap,
UtcWithLeap,
Reserved(u8),
}
impl AssociationType {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v & 0x0F {
0 => Self::UtcWithoutLeap,
1 => Self::UtcWithLeap,
v => Self::Reserved(v),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::UtcWithoutLeap => 0,
Self::UtcWithLeap => 1,
Self::Reserved(v) => v,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::UtcWithoutLeap => "UTC without leap second",
Self::UtcWithLeap => "UTC with leap second",
Self::Reserved(_) => "Reserved",
}
}
}
broadcast_common::impl_spec_display!(AssociationType, Reserved);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LeapInfo {
pub leap59: bool,
pub leap61: bool,
pub pastleap59: bool,
pub pastleap61: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TimeAssociationBody {
pub association_type: AssociationType,
pub leap_info: Option<LeapInfo>,
pub ncr_base: u64,
pub ncr_ext: u16,
pub association_timestamp_seconds: u64,
pub association_timestamp_nanoseconds: u32,
pub reserved_zero_future_use_4: u8,
pub ncr_reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum TimePlanMode {
DwellOnTime,
Bitmap,
GridRevisitSleep,
Reserved(u8),
}
impl TimePlanMode {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v & 0x03 {
0 => Self::DwellOnTime,
1 => Self::Bitmap,
2 => Self::GridRevisitSleep,
v => Self::Reserved(v),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::DwellOnTime => 0,
Self::Bitmap => 1,
Self::GridRevisitSleep => 2,
Self::Reserved(v) => v,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::DwellOnTime => "dwell/on-time",
Self::Bitmap => "bitmap",
Self::GridRevisitSleep => "grid/revisit/sleep",
Self::Reserved(_) => "reserved",
}
}
}
broadcast_common::impl_spec_display!(TimePlanMode, Reserved);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum BeamhoppingMode {
Mode0 {
dwell_duration_base: u64,
dwell_duration_ext: u16,
on_time_base: u64,
on_time_ext: u16,
dwell_duration_reserved_zero_future_use: u8,
on_time_reserved_zero_future_use: u8,
},
Mode1 {
bit_map_size: u16,
current_slot: u16,
slot_transmission_on: Vec<bool>,
reserved_zero_future_use_size: u8,
reserved_zero_future_use_slot: u8,
},
Mode2 {
grid_size_base: u64,
grid_size_ext: u16,
revisit_duration_base: u64,
revisit_duration_ext: u16,
sleep_time_base: u64,
sleep_time_ext: u16,
sleep_duration_base: u64,
sleep_duration_ext: u16,
grid_size_reserved_zero_future_use: u8,
revisit_duration_reserved_zero_future_use: u8,
sleep_time_reserved_zero_future_use: u8,
sleep_duration_reserved_zero_future_use: u8,
},
Reserved(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BeamhoppingPlan {
pub beamhopping_time_plan_id: u32,
pub time_plan_mode: TimePlanMode,
pub time_of_application_base: u64,
pub time_of_application_ext: u16,
pub cycle_duration_base: u64,
pub cycle_duration_ext: u16,
pub mode: BeamhoppingMode,
pub reserved_zero_future_use_4: u8,
pub reserved_zero_future_use_mode: u8,
pub time_of_application_reserved_zero_future_use: u8,
pub cycle_duration_reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BeamhoppingTimePlanBody {
pub plans: Vec<BeamhoppingPlan>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct UsableTime {
pub year: u8,
pub day: u16,
pub day_fraction: u32,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PositionV3Metadata {
pub total_start_time_year: u8,
pub total_start_time_day: u16,
pub total_start_time_day_fraction: u32,
pub total_stop_time_year: u8,
pub total_stop_time_day: u16,
pub total_stop_time_day_fraction: u32,
pub interpolation_flag: bool,
pub interpolation_type: InterpolationType,
pub interpolation_degree: u8,
pub usable_start_time: Option<UsableTime>,
pub usable_stop_time: Option<UsableTime>,
pub total_start_time_reserved_zero_future_use: u8,
pub total_stop_time_reserved_zero_future_use: u8,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum InterpolationType {
Reserved0,
Linear,
Lagrange,
Reserved3,
Hermite,
ReservedOther(u8),
}
impl InterpolationType {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v & 0x07 {
0 => Self::Reserved0,
1 => Self::Linear,
2 => Self::Lagrange,
3 => Self::Reserved3,
4 => Self::Hermite,
v => Self::ReservedOther(v),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Reserved0 => 0,
Self::Linear => 1,
Self::Lagrange => 2,
Self::Reserved3 => 3,
Self::Hermite => 4,
Self::ReservedOther(v) => v,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Reserved0 => "Reserved",
Self::Linear => "Linear",
Self::Lagrange => "Lagrange",
Self::Reserved3 => "Reserved",
Self::Hermite => "Hermite",
Self::ReservedOther(_) => "Reserved",
}
}
}
broadcast_common::impl_spec_display!(InterpolationType, ReservedOther);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct EphemerisAccel {
pub ephemeris_x_ddot: u32,
pub ephemeris_y_ddot: u32,
pub ephemeris_z_ddot: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct EphemerisData {
pub epoch_year: u8,
pub epoch_day: u16,
pub epoch_day_fraction: u32,
pub ephemeris_x: u32,
pub ephemeris_y: u32,
pub ephemeris_z: u32,
pub ephemeris_x_dot: u32,
pub ephemeris_y_dot: u32,
pub ephemeris_z_dot: u32,
pub acceleration: Option<EphemerisAccel>,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CovarianceData {
pub covariance_epoch_year: u8,
pub covariance_epoch_day: u16,
pub covariance_epoch_day_fraction: u32,
pub covariance_elements: [u32; 21],
pub covariance_epoch_reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PositionV3Satellite {
pub satellite_id: u32,
pub usable_start_time_flag: bool,
pub usable_stop_time_flag: bool,
pub ephemeris_accel_flag: bool,
pub covariance_flag: bool,
pub metadata: Option<PositionV3Metadata>,
pub ephemeris_data: Vec<EphemerisData>,
pub covariance: Option<CovarianceData>,
pub reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PositionV3Body {
pub oem_version_major: u8,
pub oem_version_minor: u8,
pub creation_date_year: u8,
pub creation_date_day: u16,
pub creation_date_day_fraction: u32,
pub satellites: Vec<PositionV3Satellite>,
pub creation_date_reserved_zero_future_use: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum SatBody {
PositionV2(PositionV2Body),
CellFragment(CellFragmentBody),
TimeAssociation(TimeAssociationBody),
BeamhoppingTimePlan(BeamhoppingTimePlanBody),
PositionV3(PositionV3Body),
Raw(Vec<u8>),
}
fn sat_body_serialized_len(body: &SatBody) -> usize {
match body {
SatBody::Raw(v) => v.len(),
_ => {
let mut stack = [0u8; SAT_BODY_STACK_BUF];
let mut writer = BitWriter::new(&mut stack);
if sat_body_write(body, &mut writer).is_ok() {
return writer.bits_written().div_ceil(8);
}
let mut cap = SAT_BODY_STACK_BUF * 2;
loop {
let mut tmp = vec![0u8; cap];
let mut writer = BitWriter::new(&mut tmp);
if sat_body_write(body, &mut writer).is_ok() {
break writer.bits_written().div_ceil(8);
}
if cap >= 1 << 20 {
break cap; }
cap *= 2;
}
}
}
}
fn sat_body_write(body: &SatBody, w: &mut BitWriter) -> Result<()> {
match body {
SatBody::PositionV2(b) => sat_write_position_v2(b, w)?,
SatBody::CellFragment(b) => sat_write_cell_fragment(b, w)?,
SatBody::TimeAssociation(b) => sat_write_time_association(b, w)?,
SatBody::BeamhoppingTimePlan(b) => sat_write_beamhopping_time_plan(b, w)?,
SatBody::PositionV3(b) => sat_write_position_v3(b, w)?,
SatBody::Raw(_) => {}
}
Ok(())
}
fn sat_write_position_v2(b: &PositionV2Body, w: &mut BitWriter<'_>) -> Result<()> {
for sat in &b.satellites {
w.write_u(24, sat.satellite_id as u64)?;
w.write_u(7, u64::from(sat.reserved_zero_future_use))?;
match &sat.position {
PositionSystem::Orbital {
orbital_position,
west_east_flag,
reserved_zero_future_use,
} => {
w.write_u(1, 0)?;
w.write_u(16, *orbital_position as u64)?;
w.write_u(1, *west_east_flag as u64)?;
w.write_u(7, u64::from(*reserved_zero_future_use))?;
}
PositionSystem::Sgp4 {
epoch_year,
day_of_the_year,
day_fraction,
mean_motion_first_derivative,
mean_motion_second_derivative,
drag_term,
inclination,
right_ascension,
eccentricity,
argument_of_perigree,
mean_anomaly,
mean_motion,
} => {
w.write_u(1, 1)?;
w.write_u(8, *epoch_year as u64)?;
w.write_u(16, *day_of_the_year as u64)?;
w.write_u(32, *day_fraction as u64)?;
w.write_u(32, *mean_motion_first_derivative as u64)?;
w.write_u(32, *mean_motion_second_derivative as u64)?;
w.write_u(32, *drag_term as u64)?;
w.write_u(32, *inclination as u64)?;
w.write_u(32, *right_ascension as u64)?;
w.write_u(32, *eccentricity as u64)?;
w.write_u(32, *argument_of_perigree as u64)?;
w.write_u(32, *mean_anomaly as u64)?;
w.write_u(32, *mean_motion as u64)?;
}
}
}
Ok(())
}
fn sat_write_cell_fragment(b: &CellFragmentBody, w: &mut BitWriter<'_>) -> Result<()> {
for frag in &b.fragments {
w.write_u(32, frag.cell_fragment_id as u64)?;
w.write_u(1, frag.first_occurrence as u64)?;
w.write_u(1, frag.last_occurrence as u64)?;
if frag.first_occurrence {
let Some(ref c) = frag.centre else {
return Err(Error::ValueOutOfRange {
field: "centre",
reason: "required when first_occurrence is set",
});
};
w.write_u(4, u64::from(c.reserved_zero_future_use_4))?;
w.write_i(18, c.centre_latitude as i64)?;
w.write_u(5, u64::from(c.reserved_zero_future_use_5))?;
w.write_i(19, c.centre_longitude as i64)?;
w.write_u(24, c.max_distance as u64)?;
w.write_u(6, u64::from(c.reserved_zero_future_use_6))?;
} else {
w.write_u(4, u64::from(frag.reserved_zero_future_use_4))?;
}
w.write_u(10, frag.delivery_system_ids.len() as u64)?;
for id in &frag.delivery_system_ids {
w.write_u(32, *id as u64)?;
}
w.write_u(6, u64::from(frag.reserved_zero_future_use_new))?;
w.write_u(10, frag.new_delivery_systems.len() as u64)?;
for nds in &frag.new_delivery_systems {
w.write_u(32, nds.new_delivery_system_id as u64)?;
w.write_u(33, nds.time_of_application_base)?;
w.write_u(6, u64::from(nds.reserved_zero_future_use))?;
w.write_u(9, nds.time_of_application_ext as u64)?;
}
w.write_u(6, u64::from(frag.reserved_zero_future_use_obsolescent))?;
w.write_u(10, frag.obsolescent_delivery_systems.len() as u64)?;
for ods in &frag.obsolescent_delivery_systems {
w.write_u(32, ods.obsolescent_delivery_system_id as u64)?;
w.write_u(33, ods.time_of_obsolescence_base)?;
w.write_u(6, u64::from(ods.reserved_zero_future_use))?;
w.write_u(9, ods.time_of_obsolescence_ext as u64)?;
}
}
Ok(())
}
fn sat_write_time_association(b: &TimeAssociationBody, w: &mut BitWriter<'_>) -> Result<()> {
w.write_u(4, b.association_type.to_u8() as u64)?;
if b.association_type.to_u8() == 1 {
if let Some(ref li) = b.leap_info {
w.write_u(1, li.leap59 as u64)?;
w.write_u(1, li.leap61 as u64)?;
w.write_u(1, li.pastleap59 as u64)?;
w.write_u(1, li.pastleap61 as u64)?;
} else {
w.write_u(4, u64::from(b.reserved_zero_future_use_4))?;
}
} else {
w.write_u(4, u64::from(b.reserved_zero_future_use_4))?;
}
w.write_u(33, b.ncr_base)?;
w.write_u(6, u64::from(b.ncr_reserved_zero_future_use))?;
w.write_u(9, b.ncr_ext as u64)?;
w.write_u(64, b.association_timestamp_seconds)?;
w.write_u(32, b.association_timestamp_nanoseconds as u64)?;
Ok(())
}
fn sat_write_beamhopping_time_plan(
b: &BeamhoppingTimePlanBody,
w: &mut BitWriter<'_>,
) -> Result<()> {
for plan in &b.plans {
w.write_u(32, plan.beamhopping_time_plan_id as u64)?;
w.write_u(4, u64::from(plan.reserved_zero_future_use_4))?;
let mode_bits = match &plan.mode {
BeamhoppingMode::Mode0 { .. } => 33 + 6 + 9 + 33 + 6 + 9,
BeamhoppingMode::Mode1 { bit_map_size, .. } => {
let bm = *bit_map_size as usize;
let raw = 1 + 15 + 1 + 15 + bm;
raw + pad_to_byte(raw)
}
BeamhoppingMode::Mode2 { .. } => 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9,
BeamhoppingMode::Reserved(v) => v.len() * 8,
};
let total_bits_after_length = 8 + 48 + 48 + mode_bits;
let plan_length_bytes = total_bits_after_length / 8;
w.write_u(12, plan_length_bytes as u64)?;
w.write_u(6, u64::from(plan.reserved_zero_future_use_mode))?;
w.write_u(2, plan.time_plan_mode.to_u8() as u64)?;
w.write_u(33, plan.time_of_application_base)?;
w.write_u(
6,
u64::from(plan.time_of_application_reserved_zero_future_use),
)?;
w.write_u(9, plan.time_of_application_ext as u64)?;
w.write_u(33, plan.cycle_duration_base)?;
w.write_u(6, u64::from(plan.cycle_duration_reserved_zero_future_use))?;
w.write_u(9, plan.cycle_duration_ext as u64)?;
match &plan.mode {
BeamhoppingMode::Mode0 {
dwell_duration_base,
dwell_duration_ext,
on_time_base,
on_time_ext,
dwell_duration_reserved_zero_future_use,
on_time_reserved_zero_future_use,
} => {
w.write_u(33, *dwell_duration_base)?;
w.write_u(6, u64::from(*dwell_duration_reserved_zero_future_use))?;
w.write_u(9, *dwell_duration_ext as u64)?;
w.write_u(33, *on_time_base)?;
w.write_u(6, u64::from(*on_time_reserved_zero_future_use))?;
w.write_u(9, *on_time_ext as u64)?;
}
BeamhoppingMode::Mode1 {
bit_map_size,
current_slot,
slot_transmission_on,
reserved_zero_future_use_size,
reserved_zero_future_use_slot,
} => {
w.write_u(1, u64::from(*reserved_zero_future_use_size))?;
w.write_u(15, *bit_map_size as u64)?;
w.write_u(1, u64::from(*reserved_zero_future_use_slot))?;
w.write_u(15, *current_slot as u64)?;
for &on in slot_transmission_on {
w.write_u(1, on as u64)?;
}
let total = 1 + 15 + 1 + 15 + *bit_map_size as usize;
for _ in 0..pad_to_byte(total) {
w.write_zero(1)?;
}
}
BeamhoppingMode::Mode2 {
grid_size_base,
grid_size_ext,
revisit_duration_base,
revisit_duration_ext,
sleep_time_base,
sleep_time_ext,
sleep_duration_base,
sleep_duration_ext,
grid_size_reserved_zero_future_use,
revisit_duration_reserved_zero_future_use,
sleep_time_reserved_zero_future_use,
sleep_duration_reserved_zero_future_use,
} => {
w.write_u(33, *grid_size_base)?;
w.write_u(6, u64::from(*grid_size_reserved_zero_future_use))?;
w.write_u(9, *grid_size_ext as u64)?;
w.write_u(33, *revisit_duration_base)?;
w.write_u(6, u64::from(*revisit_duration_reserved_zero_future_use))?;
w.write_u(9, *revisit_duration_ext as u64)?;
w.write_u(33, *sleep_time_base)?;
w.write_u(6, u64::from(*sleep_time_reserved_zero_future_use))?;
w.write_u(9, *sleep_time_ext as u64)?;
w.write_u(33, *sleep_duration_base)?;
w.write_u(6, u64::from(*sleep_duration_reserved_zero_future_use))?;
w.write_u(9, *sleep_duration_ext as u64)?;
}
BeamhoppingMode::Reserved(v) => {
for &b in v {
w.write_u(8, b as u64)?;
}
}
}
}
Ok(())
}
fn sat_write_position_v3(b: &PositionV3Body, w: &mut BitWriter<'_>) -> Result<()> {
w.write_u(4, b.oem_version_major as u64)?;
w.write_u(4, b.oem_version_minor as u64)?;
w.write_u(8, b.creation_date_year as u64)?;
w.write_u(7, u64::from(b.creation_date_reserved_zero_future_use))?;
w.write_u(9, b.creation_date_day as u64)?;
w.write_u(32, b.creation_date_day_fraction as u64)?;
for sat in &b.satellites {
w.write_u(24, sat.satellite_id as u64)?;
w.write_u(3, u64::from(sat.reserved_zero_future_use))?;
w.write_u(1, u8::from(sat.metadata.is_some()) as u64)?;
w.write_u(1, sat.usable_start_time_flag as u64)?;
w.write_u(1, sat.usable_stop_time_flag as u64)?;
w.write_u(1, sat.ephemeris_accel_flag as u64)?;
w.write_u(1, sat.covariance_flag as u64)?;
if let Some(ref md) = sat.metadata {
w.write_u(8, md.total_start_time_year as u64)?;
w.write_u(7, u64::from(md.total_start_time_reserved_zero_future_use))?;
w.write_u(9, md.total_start_time_day as u64)?;
w.write_u(32, md.total_start_time_day_fraction as u64)?;
w.write_u(8, md.total_stop_time_year as u64)?;
w.write_u(7, u64::from(md.total_stop_time_reserved_zero_future_use))?;
w.write_u(9, md.total_stop_time_day as u64)?;
w.write_u(32, md.total_stop_time_day_fraction as u64)?;
w.write_u(1, u64::from(md.reserved_zero_future_use))?;
w.write_u(1, md.interpolation_flag as u64)?;
w.write_u(3, md.interpolation_type.to_u8() as u64)?;
w.write_u(3, md.interpolation_degree as u64)?;
if sat.usable_start_time_flag {
if let Some(ref ut) = md.usable_start_time {
w.write_u(8, ut.year as u64)?;
w.write_u(7, u64::from(ut.reserved_zero_future_use))?;
w.write_u(9, ut.day as u64)?;
w.write_u(32, ut.day_fraction as u64)?;
} else {
w.write_zero(8)?;
w.write_zero(7)?;
w.write_zero(9)?;
w.write_zero(32)?;
}
}
if sat.usable_stop_time_flag {
if let Some(ref ut) = md.usable_stop_time {
w.write_u(8, ut.year as u64)?;
w.write_u(7, u64::from(ut.reserved_zero_future_use))?;
w.write_u(9, ut.day as u64)?;
w.write_u(32, ut.day_fraction as u64)?;
} else {
w.write_zero(8)?;
w.write_zero(7)?;
w.write_zero(9)?;
w.write_zero(32)?;
}
}
}
w.write_u(16, sat.ephemeris_data.len() as u64)?;
for ed in &sat.ephemeris_data {
w.write_u(8, ed.epoch_year as u64)?;
w.write_u(7, u64::from(ed.reserved_zero_future_use))?;
w.write_u(9, ed.epoch_day as u64)?;
w.write_u(32, ed.epoch_day_fraction as u64)?;
w.write_u(32, ed.ephemeris_x as u64)?;
w.write_u(32, ed.ephemeris_y as u64)?;
w.write_u(32, ed.ephemeris_z as u64)?;
w.write_u(32, ed.ephemeris_x_dot as u64)?;
w.write_u(32, ed.ephemeris_y_dot as u64)?;
w.write_u(32, ed.ephemeris_z_dot as u64)?;
if sat.ephemeris_accel_flag {
if let Some(ref acc) = ed.acceleration {
w.write_u(32, acc.ephemeris_x_ddot as u64)?;
w.write_u(32, acc.ephemeris_y_ddot as u64)?;
w.write_u(32, acc.ephemeris_z_ddot as u64)?;
} else {
w.write_zero(32)?;
w.write_zero(32)?;
w.write_zero(32)?;
}
}
}
if sat.covariance_flag {
if let Some(ref cov) = sat.covariance {
w.write_u(8, cov.covariance_epoch_year as u64)?;
w.write_u(7, u64::from(cov.covariance_epoch_reserved_zero_future_use))?;
w.write_u(9, cov.covariance_epoch_day as u64)?;
w.write_u(32, cov.covariance_epoch_day_fraction as u64)?;
for elem in &cov.covariance_elements {
w.write_u(32, *elem as u64)?;
}
} else {
w.write_zero(8)?;
w.write_zero(7)?;
w.write_zero(9)?;
w.write_zero(32)?;
for _ in 0..21 {
w.write_zero(32)?;
}
}
}
}
Ok(())
}
fn sat_body_parse(sat_table_id: u8, data: &[u8]) -> Result<SatBody> {
let body = sat_body_parse_entries(sat_table_id, data)?;
let used = sat_body_serialized_len(&body);
if used != data.len() {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: data.len().saturating_sub(used),
have: 0,
what: "SatSection trailing body bytes after the last whole entry",
},
));
}
Ok(body)
}
fn sat_body_parse_entries(sat_table_id: u8, data: &[u8]) -> Result<SatBody> {
if data.is_empty() && sat_table_id <= 4 {
return Ok(match sat_table_id {
0 => SatBody::PositionV2(PositionV2Body {
satellites: Vec::new(),
}),
1 => SatBody::CellFragment(CellFragmentBody {
fragments: Vec::new(),
}),
3 => SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody { plans: Vec::new() }),
_ => {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: 1,
have: 0,
what: "SatSection body (non-loop type requires data)",
},
));
}
});
}
let mut r = BitReader::new(data);
match sat_table_id {
0 => sat_parse_position_v2(&mut r),
1 => sat_parse_cell_fragment(&mut r),
2 => sat_parse_time_association(&mut r),
3 => sat_parse_beamhopping_time_plan(&mut r, data),
4 => sat_parse_position_v3(&mut r),
_ => Ok(SatBody::Raw(data.to_vec())),
}
}
fn sat_parse_position_v2(r: &mut BitReader<'_>) -> Result<SatBody> {
let mut satellites = Vec::new();
while r.remaining_bits() > 24 + 7 {
let satellite_id = r.read_u(24)? as u32;
let sat_reserved = r.read_u(7)? as u8;
let position_system = r.read_u(1)?;
let position = if position_system == 0 {
const ORBITAL_BITS: usize = 16 + 1 + 7;
if r.remaining_bits() < ORBITAL_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: ORBITAL_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV2 Orbital fields",
},
));
}
let orbital_position = r.read_u(16)? as u16;
let west_east_flag = r.read_u(1)? != 0;
let orbital_reserved = r.read_u(7)? as u8;
PositionSystem::Orbital {
orbital_position,
west_east_flag,
reserved_zero_future_use: orbital_reserved,
}
} else {
const SGP4_BITS: usize = 8 + 16 + 32 * 10;
if r.remaining_bits() < SGP4_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: SGP4_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV2 SGP4 fields",
},
));
}
let epoch_year = r.read_u(8)? as u8;
let day_of_the_year = r.read_u(16)? as u16;
let day_fraction = r.read_u(32)? as u32;
let mean_motion_first_derivative = r.read_u(32)? as u32;
let mean_motion_second_derivative = r.read_u(32)? as u32;
let drag_term = r.read_u(32)? as u32;
let inclination = r.read_u(32)? as u32;
let right_ascension = r.read_u(32)? as u32;
let eccentricity = r.read_u(32)? as u32;
let argument_of_perigree = r.read_u(32)? as u32;
let mean_anomaly = r.read_u(32)? as u32;
let mean_motion = r.read_u(32)? as u32;
PositionSystem::Sgp4 {
epoch_year,
day_of_the_year,
day_fraction,
mean_motion_first_derivative,
mean_motion_second_derivative,
drag_term,
inclination,
right_ascension,
eccentricity,
argument_of_perigree,
mean_anomaly,
mean_motion,
}
};
satellites.push(PositionV2Satellite {
satellite_id,
reserved_zero_future_use: sat_reserved,
position,
});
}
Ok(SatBody::PositionV2(PositionV2Body { satellites }))
}
fn sat_parse_cell_fragment(r: &mut BitReader<'_>) -> Result<SatBody> {
let mut fragments = Vec::new();
while r.remaining_bits() >= 32 + 2 {
let cell_fragment_id = r.read_u(32)? as u32;
let first_occurrence = r.read_u(1)? != 0;
let last_occurrence = r.read_u(1)? != 0;
let mut frag_r4 = 0u8;
let centre = if first_occurrence {
const CENTER_BITS: usize = 4 + 18 + 5 + 19 + 24 + 6;
if r.remaining_bits() < CENTER_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: CENTER_BITS,
have: r.remaining_bits(),
what: "SatSection CellFragment centre",
},
));
}
let center_r4 = r.read_u(4)? as u8;
let centre_latitude = r.read_i(18)? as i32;
let center_r5 = r.read_u(5)? as u8;
let centre_longitude = r.read_i(19)? as i32;
let max_distance = r.read_u(24)? as u32;
let center_r6 = r.read_u(6)? as u8;
Some(CellCentre {
centre_latitude,
centre_longitude,
max_distance,
reserved_zero_future_use_4: center_r4,
reserved_zero_future_use_5: center_r5,
reserved_zero_future_use_6: center_r6,
})
} else {
frag_r4 = r.read_u(4)? as u8;
None
};
let dsid_count = r.read_u(10)? as usize;
if r.remaining_bits() < dsid_count * 32 {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: dsid_count * 32,
have: r.remaining_bits(),
what: "SatSection CellFragment delivery_system_ids",
},
));
}
let mut delivery_system_ids = Vec::with_capacity(dsid_count.min(r.remaining_bits() / 32));
for _ in 0..dsid_count {
delivery_system_ids.push(r.read_u(32)? as u32);
}
let frag_new_r = r.read_u(6)? as u8;
let nds_count = r.read_u(10)? as usize;
const NDS_ENTRY_BITS: usize = 32 + 33 + 6 + 9;
if r.remaining_bits() < nds_count * NDS_ENTRY_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: nds_count * NDS_ENTRY_BITS,
have: r.remaining_bits(),
what: "SatSection CellFragment new_delivery_systems",
},
));
}
let mut new_delivery_systems =
Vec::with_capacity(nds_count.min(r.remaining_bits() / NDS_ENTRY_BITS));
for _ in 0..nds_count {
let new_delivery_system_id = r.read_u(32)? as u32;
let time_of_application_base = r.read_u(33)?;
let nds_r = r.read_u(6)? as u8;
let time_of_application_ext = r.read_u(9)? as u16;
new_delivery_systems.push(NewDeliverySystem {
new_delivery_system_id,
time_of_application_base,
time_of_application_ext,
reserved_zero_future_use: nds_r,
});
}
let frag_obs_r = r.read_u(6)? as u8;
let ods_count = r.read_u(10)? as usize;
if r.remaining_bits() < ods_count * NDS_ENTRY_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: ods_count * NDS_ENTRY_BITS,
have: r.remaining_bits(),
what: "SatSection CellFragment obsolescent_delivery_systems",
},
));
}
let mut obsolescent_delivery_systems =
Vec::with_capacity(ods_count.min(r.remaining_bits() / NDS_ENTRY_BITS));
for _ in 0..ods_count {
let obsolescent_delivery_system_id = r.read_u(32)? as u32;
let time_of_obsolescence_base = r.read_u(33)?;
let ods_r = r.read_u(6)? as u8;
let time_of_obsolescence_ext = r.read_u(9)? as u16;
obsolescent_delivery_systems.push(ObsolescentDeliverySystem {
obsolescent_delivery_system_id,
time_of_obsolescence_base,
time_of_obsolescence_ext,
reserved_zero_future_use: ods_r,
});
}
fragments.push(CellFragment {
cell_fragment_id,
first_occurrence,
last_occurrence,
centre,
delivery_system_ids,
new_delivery_systems,
obsolescent_delivery_systems,
reserved_zero_future_use_4: frag_r4,
reserved_zero_future_use_new: frag_new_r,
reserved_zero_future_use_obsolescent: frag_obs_r,
});
}
Ok(SatBody::CellFragment(CellFragmentBody { fragments }))
}
fn sat_parse_time_association(r: &mut BitReader<'_>) -> Result<SatBody> {
const TIME_ASSOC_MIN_BITS: usize = 4 + 4 + 33 + 6 + 9 + 64 + 32;
if r.remaining_bits() < TIME_ASSOC_MIN_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: TIME_ASSOC_MIN_BITS,
have: r.remaining_bits(),
what: "SatSection TimeAssociation body",
},
));
}
let association_type = AssociationType::from_u8(r.read_u(4)? as u8);
let mut assoc_r4 = 0u8;
let leap_info = if association_type.to_u8() == 1 {
Some(LeapInfo {
leap59: r.read_u(1)? != 0,
leap61: r.read_u(1)? != 0,
pastleap59: r.read_u(1)? != 0,
pastleap61: r.read_u(1)? != 0,
})
} else {
assoc_r4 = r.read_u(4)? as u8;
None
};
let ncr_base = r.read_u(33)?;
let ncr_r = r.read_u(6)? as u8;
let ncr_ext = r.read_u(9)? as u16;
let association_timestamp_seconds = r.read_u(64)?;
let association_timestamp_nanoseconds = r.read_u(32)? as u32;
Ok(SatBody::TimeAssociation(TimeAssociationBody {
association_type,
leap_info,
ncr_base,
ncr_ext,
association_timestamp_seconds,
association_timestamp_nanoseconds,
reserved_zero_future_use_4: assoc_r4,
ncr_reserved_zero_future_use: ncr_r,
}))
}
fn sat_parse_beamhopping_time_plan(r: &mut BitReader<'_>, data: &[u8]) -> Result<SatBody> {
let mut plans = Vec::new();
while r.remaining_bits() >= 32 + 4 + 12 {
let beamhopping_time_plan_id = r.read_u(32)? as u32;
let plan_r4 = r.read_u(4)? as u8;
let plan_length = r.read_u(12)? as usize;
let plan_end_bits = r.bits_consumed() + plan_length * 8;
let plan_mode_r = r.read_u(6)? as u8;
let time_plan_mode = TimePlanMode::from_u8(r.read_u(2)? as u8);
let time_of_application_base = r.read_u(33)?;
let toa_r = r.read_u(6)? as u8;
let time_of_application_ext = r.read_u(9)? as u16;
let cycle_duration_base = r.read_u(33)?;
let cycle_r = r.read_u(6)? as u8;
let cycle_duration_ext = r.read_u(9)? as u16;
let mode = match time_plan_mode {
TimePlanMode::DwellOnTime => {
const MODE0_BITS: usize = 33 + 6 + 9 + 33 + 6 + 9;
if r.remaining_bits() < MODE0_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: MODE0_BITS,
have: r.remaining_bits(),
what: "SatSection Beamhopping Mode0",
},
));
}
let dwell_duration_base = r.read_u(33)?;
let dwell_r = r.read_u(6)? as u8;
let dwell_duration_ext = r.read_u(9)? as u16;
let on_time_base = r.read_u(33)?;
let on_time_r = r.read_u(6)? as u8;
let on_time_ext = r.read_u(9)? as u16;
BeamhoppingMode::Mode0 {
dwell_duration_base,
dwell_duration_ext,
on_time_base,
on_time_ext,
dwell_duration_reserved_zero_future_use: dwell_r,
on_time_reserved_zero_future_use: on_time_r,
}
}
TimePlanMode::Bitmap => {
const MODE1_HEADER_BITS: usize = 1 + 15 + 1 + 15;
if r.remaining_bits() < MODE1_HEADER_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: MODE1_HEADER_BITS,
have: r.remaining_bits(),
what: "SatSection Beamhopping Mode1 header",
},
));
}
let m1_size_r = r.read_u(1)? as u8;
let bit_map_size = r.read_u(15)? as u16;
let m1_slot_r = r.read_u(1)? as u8;
let current_slot = r.read_u(15)? as u16;
if r.remaining_bits() < bit_map_size as usize {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: bit_map_size as usize,
have: r.remaining_bits(),
what: "SatSection Beamhopping Mode1 bitmap",
},
));
}
let mut slot_transmission_on =
Vec::with_capacity((bit_map_size as usize).min(r.remaining_bits()));
for _ in 0..bit_map_size {
slot_transmission_on.push(r.read_u(1)? != 0);
}
let total = 1 + 15 + 1 + 15 + bit_map_size as usize;
r.skip(pad_to_byte(total) as u8)?;
BeamhoppingMode::Mode1 {
bit_map_size,
current_slot,
slot_transmission_on,
reserved_zero_future_use_size: m1_size_r,
reserved_zero_future_use_slot: m1_slot_r,
}
}
TimePlanMode::GridRevisitSleep => {
const MODE2_BITS: usize = 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9;
if r.remaining_bits() < MODE2_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: MODE2_BITS,
have: r.remaining_bits(),
what: "SatSection Beamhopping Mode2",
},
));
}
let grid_size_base = r.read_u(33)?;
let grid_r = r.read_u(6)? as u8;
let grid_size_ext = r.read_u(9)? as u16;
let revisit_duration_base = r.read_u(33)?;
let revisit_r = r.read_u(6)? as u8;
let revisit_duration_ext = r.read_u(9)? as u16;
let sleep_time_base = r.read_u(33)?;
let sleep_time_r = r.read_u(6)? as u8;
let sleep_time_ext = r.read_u(9)? as u16;
let sleep_duration_base = r.read_u(33)?;
let sleep_dur_r = r.read_u(6)? as u8;
let sleep_duration_ext = r.read_u(9)? as u16;
BeamhoppingMode::Mode2 {
grid_size_base,
grid_size_ext,
revisit_duration_base,
revisit_duration_ext,
sleep_time_base,
sleep_time_ext,
sleep_duration_base,
sleep_duration_ext,
grid_size_reserved_zero_future_use: grid_r,
revisit_duration_reserved_zero_future_use: revisit_r,
sleep_time_reserved_zero_future_use: sleep_time_r,
sleep_duration_reserved_zero_future_use: sleep_dur_r,
}
}
_ => {
let start_byte = r.bits_consumed().div_ceil(8);
let end_byte = plan_end_bits / 8;
let raw = if start_byte < end_byte && end_byte <= data.len() {
data[start_byte..end_byte].to_vec()
} else {
Vec::new()
};
BeamhoppingMode::Reserved(raw)
}
};
if r.bits_consumed() > plan_end_bits {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: r.bits_consumed() - plan_end_bits,
have: 0,
what: "SatSection Beamhopping plan_length shorter than plan fields",
},
));
}
r.seek_forward(plan_end_bits);
plans.push(BeamhoppingPlan {
beamhopping_time_plan_id,
time_plan_mode,
time_of_application_base,
time_of_application_ext,
cycle_duration_base,
cycle_duration_ext,
mode,
reserved_zero_future_use_4: plan_r4,
reserved_zero_future_use_mode: plan_mode_r,
time_of_application_reserved_zero_future_use: toa_r,
cycle_duration_reserved_zero_future_use: cycle_r,
});
}
Ok(SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans,
}))
}
fn sat_parse_position_v3(r: &mut BitReader<'_>) -> Result<SatBody> {
const POS_V3_HEADER_BITS: usize = 4 + 4 + 8 + 7 + 9 + 32;
if r.remaining_bits() < POS_V3_HEADER_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: POS_V3_HEADER_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV3 body header",
},
));
}
let oem_version_major = r.read_u(4)? as u8;
let oem_version_minor = r.read_u(4)? as u8;
let creation_date_year = r.read_u(8)? as u8;
let creation_r = r.read_u(7)? as u8;
let creation_date_day = r.read_u(9)? as u16;
let creation_date_day_fraction = r.read_u(32)? as u32;
let mut satellites = Vec::new();
while r.remaining_bits() >= 24 + 3 + 5 {
let satellite_id = r.read_u(24)? as u32;
let v3sat_r = r.read_u(3)? as u8;
let metadata_flag = r.read_u(1)? != 0;
let usable_start_time_flag = r.read_u(1)? != 0;
let usable_stop_time_flag = r.read_u(1)? != 0;
let ephemeris_accel_flag = r.read_u(1)? != 0;
let covariance_flag = r.read_u(1)? != 0;
let metadata = if metadata_flag {
const METADATA_FIXED_BITS: usize = 8 + 7 + 9 + 32 + 8 + 7 + 9 + 32 + 1 + 1 + 3 + 3;
if r.remaining_bits() < METADATA_FIXED_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: METADATA_FIXED_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV3 metadata",
},
));
}
let total_start_time_year = r.read_u(8)? as u8;
let start_r = r.read_u(7)? as u8;
let total_start_time_day = r.read_u(9)? as u16;
let total_start_time_day_fraction = r.read_u(32)? as u32;
let total_stop_time_year = r.read_u(8)? as u8;
let stop_r = r.read_u(7)? as u8;
let total_stop_time_day = r.read_u(9)? as u16;
let total_stop_time_day_fraction = r.read_u(32)? as u32;
let interp_r = r.read_u(1)? as u8;
let interpolation_flag = r.read_u(1)? != 0;
let interpolation_type = InterpolationType::from_u8(r.read_u(3)? as u8);
let interpolation_degree = r.read_u(3)? as u8;
let usable_start_time = if usable_start_time_flag {
const USABLE_TIME_BITS: usize = 8 + 7 + 9 + 32;
if r.remaining_bits() < USABLE_TIME_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: USABLE_TIME_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV3 usable_start_time",
},
));
}
let year = r.read_u(8)? as u8;
let ustart_r = r.read_u(7)? as u8;
let day = r.read_u(9)? as u16;
let day_fraction = r.read_u(32)? as u32;
Some(UsableTime {
year,
day,
day_fraction,
reserved_zero_future_use: ustart_r,
})
} else {
None
};
let usable_stop_time = if usable_stop_time_flag {
const USABLE_TIME_BITS: usize = 8 + 7 + 9 + 32;
if r.remaining_bits() < USABLE_TIME_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: USABLE_TIME_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV3 usable_stop_time",
},
));
}
let year = r.read_u(8)? as u8;
let ustop_r = r.read_u(7)? as u8;
let day = r.read_u(9)? as u16;
let day_fraction = r.read_u(32)? as u32;
Some(UsableTime {
year,
day,
day_fraction,
reserved_zero_future_use: ustop_r,
})
} else {
None
};
Some(PositionV3Metadata {
total_start_time_year,
total_start_time_day,
total_start_time_day_fraction,
total_stop_time_year,
total_stop_time_day,
total_stop_time_day_fraction,
interpolation_flag,
interpolation_type,
interpolation_degree,
usable_start_time,
usable_stop_time,
total_start_time_reserved_zero_future_use: start_r,
total_stop_time_reserved_zero_future_use: stop_r,
reserved_zero_future_use: interp_r,
})
} else {
None
};
let ephemeris_data_count = r.read_u(16)? as u16;
let entry_bits: usize =
8 + 7 + 9 + 32 + 32 * 6 + if ephemeris_accel_flag { 32 * 3 } else { 0 };
let mut ephemeris_data = Vec::with_capacity(
(ephemeris_data_count as usize)
.min(r.remaining_bits().saturating_sub(entry_bits) / entry_bits + 1),
);
for _ in 0..ephemeris_data_count {
if r.remaining_bits() < entry_bits {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: entry_bits,
have: r.remaining_bits(),
what: "SatSection PositionV3 ephemeris_data entry",
},
));
}
let epoch_year = r.read_u(8)? as u8;
let eph_r = r.read_u(7)? as u8;
let epoch_day = r.read_u(9)? as u16;
let epoch_day_fraction = r.read_u(32)? as u32;
let ephemeris_x = r.read_u(32)? as u32;
let ephemeris_y = r.read_u(32)? as u32;
let ephemeris_z = r.read_u(32)? as u32;
let ephemeris_x_dot = r.read_u(32)? as u32;
let ephemeris_y_dot = r.read_u(32)? as u32;
let ephemeris_z_dot = r.read_u(32)? as u32;
let acceleration = if ephemeris_accel_flag {
Some(EphemerisAccel {
ephemeris_x_ddot: r.read_u(32)? as u32,
ephemeris_y_ddot: r.read_u(32)? as u32,
ephemeris_z_ddot: r.read_u(32)? as u32,
})
} else {
None
};
ephemeris_data.push(EphemerisData {
epoch_year,
epoch_day,
epoch_day_fraction,
ephemeris_x,
ephemeris_y,
ephemeris_z,
ephemeris_x_dot,
ephemeris_y_dot,
ephemeris_z_dot,
acceleration,
reserved_zero_future_use: eph_r,
});
}
let covariance = if covariance_flag {
const COV_HEADER_BITS: usize = 8 + 7 + 9 + 32;
const COV_ELEMENTS_BITS: usize = 21 * 32;
const COV_BITS: usize = COV_HEADER_BITS + COV_ELEMENTS_BITS;
if r.remaining_bits() < COV_BITS {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: COV_BITS,
have: r.remaining_bits(),
what: "SatSection PositionV3 covariance",
},
));
}
let covariance_epoch_year = r.read_u(8)? as u8;
let cov_r = r.read_u(7)? as u8;
let covariance_epoch_day = r.read_u(9)? as u16;
let covariance_epoch_day_fraction = r.read_u(32)? as u32;
let mut covariance_elements = [0u32; 21];
for elem in &mut covariance_elements {
*elem = r.read_u(32)? as u32;
}
Some(CovarianceData {
covariance_epoch_year,
covariance_epoch_day,
covariance_epoch_day_fraction,
covariance_elements,
covariance_epoch_reserved_zero_future_use: cov_r,
})
} else {
None
};
satellites.push(PositionV3Satellite {
satellite_id,
usable_start_time_flag,
usable_stop_time_flag,
ephemeris_accel_flag,
covariance_flag,
metadata,
ephemeris_data,
covariance,
reserved_zero_future_use: v3sat_r,
});
}
Ok(SatBody::PositionV3(PositionV3Body {
oem_version_major,
oem_version_minor,
creation_date_year,
creation_date_day,
creation_date_day_fraction,
satellites,
creation_date_reserved_zero_future_use: creation_r,
}))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct SatSection {
pub satellite_table_id: u8,
pub section_syntax_indicator: bool,
pub private_indicator: bool,
pub table_count: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub body: SatBody,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_b1: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_b5: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_zero_future_use: u8,
}
impl SatSection {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
satellite_table_id: u8,
private_indicator: bool,
table_count: u16,
version_number: u8,
current_next_indicator: bool,
section_number: u8,
last_section_number: u8,
body: SatBody,
) -> Self {
Self {
satellite_table_id,
section_syntax_indicator: true,
private_indicator,
table_count,
version_number,
current_next_indicator,
section_number,
last_section_number,
body,
reserved_b1: super::RESERVED_2,
reserved_b5: super::RESERVED_2,
reserved_zero_future_use: 0,
}
}
#[must_use]
pub fn kind(&self) -> Option<SatTableId> {
SatTableId::try_from(self.satellite_table_id).ok()
}
}
impl<'a> Parse<'a> for SatSection {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let min_len = HEADER_LEN + CRC_LEN;
need_in(bytes, min_len, "SatSection")?;
if bytes[0] != TABLE_ID {
return Err(Error::UnexpectedTableId {
table_id: bytes[0],
what: "SatSection",
expected: &[TABLE_ID],
});
}
let section_length = super::section_length_of(bytes);
let total = super::check_section_length(
bytes.len(),
SECTION_LENGTH_PREFIX,
section_length,
HEADER_LEN + CRC_LEN,
)?;
let satellite_table_id = bytes[3] >> SATELLITE_TABLE_ID_SHIFT;
let private_indicator = (bytes[1] & super::SECTION_B1_PRIVATE_INDICATOR) != 0;
let table_count = (((bytes[3] & TABLE_COUNT_HI_MASK) as u16) << 8) | bytes[4] as u16;
let version_number = super::version_number_of(bytes[5]);
let current_next_indicator = super::current_next_of(bytes[5]);
let section_number = bytes[6];
let last_section_number = bytes[7];
let body_data = &bytes[HEADER_LEN..total - CRC_LEN];
let body = sat_body_parse(satellite_table_id, body_data)?;
Ok(SatSection {
satellite_table_id,
section_syntax_indicator: bytes[1] & super::SECTION_B1_SSI != 0,
private_indicator,
table_count,
version_number,
current_next_indicator,
section_number,
last_section_number,
body,
reserved_b1: super::b1_reserved_of(bytes[1]),
reserved_b5: super::b5_reserved_of(bytes[5]),
reserved_zero_future_use: bytes[8],
})
}
}
impl Serialize for SatSection {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
HEADER_LEN + sat_body_serialized_len(&self.body) + CRC_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
need_out(buf, len)?;
let section_length_usize = len - SECTION_LENGTH_PREFIX;
if section_length_usize > MAX_SECTION_LENGTH {
return Err(Error::SectionLengthOverflow {
declared: section_length_usize,
available: MAX_SECTION_LENGTH,
});
}
let section_length = section_length_usize as u16;
if let SatBody::PositionV3(ref v3) = self.body {
for sat in &v3.satellites {
if sat.ephemeris_data.len() > u16::MAX as usize {
return Err(Error::SectionLengthOverflow {
declared: sat.ephemeris_data.len(),
available: u16::MAX as usize,
});
}
}
}
buf[0] = TABLE_ID;
buf[1] = super::b1_of(
self.section_syntax_indicator,
self.private_indicator,
self.reserved_b1,
)? | ((section_length >> 8) as u8 & super::SECTION_LENGTH_HI_MASK);
buf[2] = (section_length & 0xFF) as u8;
broadcast_common::len::fit_mask(
self.table_count,
(u16::from(TABLE_COUNT_HI_MASK) << 8) | 0xFF,
"table_count",
)?;
buf[3] =
(broadcast_common::len::fit_mask(self.satellite_table_id, 0x3F, "satellite_table_id")?
<< SATELLITE_TABLE_ID_SHIFT)
| ((self.table_count >> 8) as u8 & TABLE_COUNT_HI_MASK);
buf[4] = (self.table_count & 0xFF) as u8;
buf[5] = super::b5_of(
self.reserved_b5,
self.version_number,
self.current_next_indicator,
)?;
buf[6] = self.section_number;
buf[7] = self.last_section_number;
buf[8] = self.reserved_zero_future_use;
let body_start = HEADER_LEN;
match &self.body {
SatBody::Raw(v) => {
buf[body_start..body_start + v.len()].copy_from_slice(v);
}
_ => {
let body_byte_len = sat_body_serialized_len(&self.body);
for b in &mut buf[body_start..body_start + body_byte_len] {
*b = 0;
}
let mut writer = BitWriter::new(&mut buf[body_start..body_start + body_byte_len]);
sat_body_write(&self.body, &mut writer)?;
}
}
let body_end = HEADER_LEN + sat_body_serialized_len(&self.body);
super::write_crc(buf, body_end);
Ok(len)
}
}
impl crate::traits::TableDef<'_> for SatSection {
const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
const NAME: &'static str = "SATELLITE_ACCESS";
}
#[cfg(test)]
mod tests {
use super::*;
fn build_sat(stid: u8, table_count: u16, body: &SatBody) -> Vec<u8> {
let sat = SatSection::new(stid, true, table_count, 5, true, 0, 0, body.clone());
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
buf
}
fn build_sat_private_indicator_false(stid: u8, body: &SatBody) -> Vec<u8> {
let sat = SatSection::new(stid, false, 0, 5, true, 0, 0, body.clone());
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
buf
}
#[test]
fn parse_raw_body() {
let body_data = [0xAA, 0xBB, 0xCC, 0xDD];
let bytes = build_sat(7, 0, &SatBody::Raw(body_data.to_vec()));
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 7);
assert_eq!(sat.kind(), None);
assert_eq!(sat.body, SatBody::Raw(body_data.to_vec()));
}
#[test]
fn private_indicator_false_round_trip() {
let body = SatBody::TimeAssociation(TimeAssociationBody {
association_type: AssociationType::UtcWithoutLeap,
leap_info: None,
ncr_base: 0,
ncr_ext: 0,
association_timestamp_seconds: 0,
association_timestamp_nanoseconds: 0,
reserved_zero_future_use_4: 0,
ncr_reserved_zero_future_use: 0,
});
let bytes = build_sat_private_indicator_false(2, &body);
let sat = SatSection::parse(&bytes).unwrap();
assert!(!sat.private_indicator);
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(
bytes, buf2,
"byte-exact round-trip with private_indicator=false"
);
}
#[test]
fn parse_position_v3_discriminant() {
let body = SatBody::PositionV3(PositionV3Body {
oem_version_major: 1,
oem_version_minor: 0,
creation_date_year: 25,
creation_date_day: 100,
creation_date_day_fraction: 0,
satellites: Vec::new(),
creation_date_reserved_zero_future_use: 0,
});
let bytes = build_sat(4, 0x1A3, &body);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 4);
assert_eq!(sat.kind(), Some(SatTableId::PositionV3));
assert_eq!(sat.table_count, 0x1A3);
}
#[test]
fn time_association_round_trip() {
let body = SatBody::TimeAssociation(TimeAssociationBody {
association_type: AssociationType::UtcWithLeap,
leap_info: Some(LeapInfo {
leap59: true,
leap61: false,
pastleap59: false,
pastleap61: true,
}),
ncr_base: 0x0000_AAAA_AAAA_u64,
ncr_ext: 0x1AA,
association_timestamp_seconds: 0x12345678_9ABCDEF0,
association_timestamp_nanoseconds: 0xDEADBEEF,
reserved_zero_future_use_4: 0,
ncr_reserved_zero_future_use: 0,
});
let bytes = build_sat(2, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::TimeAssociation(ta) => {
assert_eq!(ta.association_type, AssociationType::UtcWithLeap);
let li = ta.leap_info.as_ref().unwrap();
assert!(li.leap59);
assert!(!li.leap61);
assert!(!li.pastleap59);
assert!(li.pastleap61);
assert_eq!(ta.ncr_base, 0x0000_AAAA_AAAA);
assert_eq!(ta.ncr_ext, 0x1AA);
assert_eq!(ta.association_timestamp_seconds, 0x12345678_9ABCDEF0);
assert_eq!(ta.association_timestamp_nanoseconds, 0xDEADBEEF);
}
other => panic!("expected TimeAssociation, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact re-serialize");
}
#[test]
fn position_v2_orbital_round_trip() {
let body = SatBody::PositionV2(PositionV2Body {
satellites: vec![PositionV2Satellite {
satellite_id: 0x123456,
position: PositionSystem::Orbital {
orbital_position: 0x1234,
west_east_flag: true,
reserved_zero_future_use: 0,
},
reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(0, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::PositionV2(pv2) => {
assert_eq!(pv2.satellites.len(), 1);
assert_eq!(pv2.satellites[0].satellite_id, 0x123456);
match &pv2.satellites[0].position {
PositionSystem::Orbital {
orbital_position,
west_east_flag,
..
} => {
assert_eq!(*orbital_position, 0x1234);
assert!(*west_east_flag);
}
other => panic!("expected Orbital, got {other:?}"),
}
}
other => panic!("expected PositionV2, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact re-serialize");
}
#[test]
fn beamhopping_mode0_round_trip() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 0xDEADBEEF,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0x0000_AAAA_AAAA,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_5555_5555,
cycle_duration_ext: 0x080,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0x0000_1111_1111,
dwell_duration_ext: 0x111,
on_time_base: 0x0000_2222_2222,
on_time_ext: 0x022,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 1);
assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0xDEADBEEF);
assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
match &bhp.plans[0].mode {
BeamhoppingMode::Mode0 {
dwell_duration_base,
..
} => {
assert_eq!(*dwell_duration_base, 0x0000_1111_1111);
}
other => panic!("expected Mode0, got {other:?}"),
}
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact re-serialize");
}
#[test]
fn beamhopping_plan_length_shorter_than_fields_is_rejected() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 0xDEAD_BEEF,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0x0000_AAAA_AAAA,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_5555_5555,
cycle_duration_ext: 0x080,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0x0000_1111_1111,
dwell_duration_ext: 0x111,
on_time_base: 0x0000_2222_2222,
on_time_ext: 0x022,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
});
let mut bytes = build_sat(3, 0, &body);
let marker = [0xDEu8, 0xAD, 0xBE, 0xEF];
let id_pos = bytes
.windows(4)
.position(|w| w == marker)
.expect("beamhopping_time_plan_id marker found");
let plan_length_pos = id_pos + 4;
bytes[plan_length_pos] &= 0xF0;
bytes[plan_length_pos + 1] = 0x00;
let err = SatSection::parse(&bytes).unwrap_err();
assert!(
matches!(err, Error::BufferTooShort(broadcast_common::error::BufferTooShort { what, .. }) if what.contains("plan_length")),
"expected a plan_length error, got {err:?}"
);
}
#[test]
fn reserved_discriminant_has_no_kind() {
let bytes = build_sat(7, 0, &SatBody::Raw(Vec::new()));
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 7);
assert_eq!(sat.kind(), None);
}
#[test]
fn parse_rejects_wrong_tag() {
let mut bytes = build_sat(0, 0, &SatBody::Raw(vec![1, 2, 3]));
bytes[0] = 0x40;
assert!(matches!(
SatSection::parse(&bytes).unwrap_err(),
Error::UnexpectedTableId { table_id: 0x40, .. }
));
}
#[test]
fn rejects_short_buffer() {
assert!(matches!(
SatSection::parse(&[0x4D, 0xF0]).unwrap_err(),
Error::BufferTooShort(broadcast_common::error::BufferTooShort {
what: "SatSection",
..
})
));
}
#[test]
fn serialize_round_trip_raw() {
let body_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let sat = SatSection::new(
10,
true,
0x2FF,
5,
true,
0,
0,
SatBody::Raw(body_data.clone()),
);
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
let re = SatSection::parse(&buf).unwrap();
assert_eq!(re.body, SatBody::Raw(body_data));
assert_eq!(re.table_count, 0x2FF);
}
#[test]
fn parse_handwritten_sat_raw() {
let mut bytes: Vec<u8> = vec![
0x4D, 0xF0, 0x0E, 0x1C, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD,
];
let crc = broadcast_common::crc32_mpeg2::compute(&bytes);
bytes.extend_from_slice(&crc.to_be_bytes());
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 7);
assert_eq!(sat.table_count, 0);
assert_eq!(sat.version_number, 5);
assert!(sat.current_next_indicator);
match sat.body {
SatBody::Raw(v) => assert_eq!(v, &[0xAA, 0xBB, 0xCC, 0xDD]),
other => panic!("expected Raw, got {other:?}"),
}
}
#[test]
fn beamhopping_multi_plan_round_trip() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![
BeamhoppingPlan {
beamhopping_time_plan_id: 0x11111111,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0x0000_AAAA_AAAA,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_5555_5555,
cycle_duration_ext: 0x080,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0x0000_1111_1111,
dwell_duration_ext: 0x111,
on_time_base: 0x0000_2222_2222,
on_time_ext: 0x022,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
},
BeamhoppingPlan {
beamhopping_time_plan_id: 0x22222222,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0x0000_BBBB_BBBB,
time_of_application_ext: 0x000,
cycle_duration_base: 0x0000_6666_6666,
cycle_duration_ext: 0x090,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0x0000_3333_3333,
dwell_duration_ext: 0x133,
on_time_base: 0x0000_4444_4444,
on_time_ext: 0x044,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
},
],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 2);
assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0x11111111);
assert_eq!(bhp.plans[1].beamhopping_time_plan_id, 0x22222222);
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact multi-plan round-trip");
}
#[test]
fn position_v3_not_used_fields_round_trip_verbatim() {
let sat_with = |flags: bool, md: Option<PositionV3Metadata>| PositionV3Satellite {
satellite_id: 0x00_1234,
usable_start_time_flag: flags,
usable_stop_time_flag: flags,
ephemeris_accel_flag: false,
covariance_flag: false,
metadata: md,
ephemeris_data: Vec::new(),
covariance: None,
reserved_zero_future_use: 0,
};
let md = PositionV3Metadata {
total_start_time_year: 26,
total_start_time_day: 1,
total_start_time_day_fraction: 0,
total_stop_time_year: 27,
total_stop_time_day: 2,
total_stop_time_day_fraction: 0,
interpolation_flag: false,
interpolation_type: InterpolationType::ReservedOther(6),
interpolation_degree: 5,
usable_start_time: None,
usable_stop_time: None,
total_start_time_reserved_zero_future_use: 0,
total_stop_time_reserved_zero_future_use: 0,
reserved_zero_future_use: 0,
};
let body = SatBody::PositionV3(PositionV3Body {
oem_version_major: 1,
oem_version_minor: 0,
creation_date_year: 26,
creation_date_day: 1,
creation_date_day_fraction: 0,
satellites: vec![sat_with(true, None), sat_with(false, Some(md))],
creation_date_reserved_zero_future_use: 0,
});
let bytes = build_sat(4, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
let SatBody::PositionV3(v3) = &sat.body else {
panic!("expected PositionV3");
};
assert!(v3.satellites[0].usable_start_time_flag && v3.satellites[0].usable_stop_time_flag);
assert!(v3.satellites[0].metadata.is_none());
let md = v3.satellites[1].metadata.as_ref().unwrap();
assert!(!md.interpolation_flag);
assert_eq!(md.interpolation_type, InterpolationType::ReservedOther(6));
assert_eq!(md.interpolation_degree, 5);
let mut out = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut out).unwrap();
assert_eq!(out, bytes);
}
#[test]
fn position_v3_one_sat_with_metadata_round_trip() {
let body = SatBody::PositionV3(PositionV3Body {
oem_version_major: 2,
oem_version_minor: 1,
creation_date_year: 26,
creation_date_day: 42,
creation_date_day_fraction: 0,
satellites: vec![PositionV3Satellite {
satellite_id: 0xABCDEF,
usable_start_time_flag: true,
usable_stop_time_flag: false,
ephemeris_accel_flag: false,
covariance_flag: false,
metadata: Some(PositionV3Metadata {
total_start_time_year: 26,
total_start_time_day: 1,
total_start_time_day_fraction: 0,
total_stop_time_year: 27,
total_stop_time_day: 100,
total_stop_time_day_fraction: 0,
interpolation_flag: true,
interpolation_type: InterpolationType::Linear,
interpolation_degree: 2,
usable_start_time: Some(UsableTime {
year: 26,
day: 10,
day_fraction: 0,
reserved_zero_future_use: 0,
}),
usable_stop_time: None,
total_start_time_reserved_zero_future_use: 0,
total_stop_time_reserved_zero_future_use: 0,
reserved_zero_future_use: 0,
}),
ephemeris_data: Vec::new(),
covariance: None,
reserved_zero_future_use: 0,
}],
creation_date_reserved_zero_future_use: 0,
});
let bytes = build_sat(4, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::PositionV3(v3) => {
assert_eq!(v3.satellites.len(), 1);
assert_eq!(v3.satellites[0].satellite_id, 0xABCDEF);
let md = v3.satellites[0].metadata.as_ref().unwrap();
assert!(md.interpolation_flag);
assert_eq!(md.interpolation_type, InterpolationType::Linear);
assert_eq!(md.interpolation_degree, 2);
assert!(md.usable_start_time.is_some());
}
other => panic!("expected PositionV3, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact PositionV3 round-trip");
}
#[test]
fn cell_fragment_round_trip() {
let body = SatBody::CellFragment(CellFragmentBody {
fragments: vec![CellFragment {
cell_fragment_id: 0x11223344,
first_occurrence: true,
last_occurrence: false,
centre: Some(CellCentre {
centre_latitude: 1000,
centre_longitude: -2000,
max_distance: 500000,
reserved_zero_future_use_4: 0,
reserved_zero_future_use_5: 0,
reserved_zero_future_use_6: 0,
}),
delivery_system_ids: vec![0x55667788],
new_delivery_systems: vec![NewDeliverySystem {
new_delivery_system_id: 0xAABBCCDD,
time_of_application_base: 0x0000_1234_5678,
time_of_application_ext: 0x100,
reserved_zero_future_use: 0,
}],
obsolescent_delivery_systems: vec![ObsolescentDeliverySystem {
obsolescent_delivery_system_id: 0xEEFF0011,
time_of_obsolescence_base: 0x0000_9ABC_DEF0,
time_of_obsolescence_ext: 0x1FF,
reserved_zero_future_use: 0,
}],
reserved_zero_future_use_4: 0,
reserved_zero_future_use_new: 0,
reserved_zero_future_use_obsolescent: 0,
}],
});
let bytes = build_sat(1, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::CellFragment(cf) => {
assert_eq!(cf.fragments.len(), 1);
assert_eq!(cf.fragments[0].cell_fragment_id, 0x11223344);
assert!(cf.fragments[0].first_occurrence);
assert!(cf.fragments[0].centre.is_some());
assert_eq!(cf.fragments[0].delivery_system_ids.len(), 1);
assert_eq!(cf.fragments[0].new_delivery_systems.len(), 1);
assert_eq!(cf.fragments[0].obsolescent_delivery_systems.len(), 1);
}
other => panic!("expected CellFragment, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact CellFragment round-trip");
}
#[test]
fn beamhopping_mode1_round_trip() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 0x12345678,
time_plan_mode: TimePlanMode::Bitmap,
time_of_application_base: 0x0000_AAAA_AAAA,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_5555_5555,
cycle_duration_ext: 0x080,
mode: BeamhoppingMode::Mode1 {
bit_map_size: 8,
current_slot: 3,
slot_transmission_on: vec![true, false, true, true, false, false, true, false],
reserved_zero_future_use_size: 0,
reserved_zero_future_use_slot: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 1);
assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::Bitmap);
match &bhp.plans[0].mode {
BeamhoppingMode::Mode1 {
bit_map_size,
current_slot,
slot_transmission_on,
..
} => {
assert_eq!(*bit_map_size, 8);
assert_eq!(*current_slot, 3);
assert_eq!(
slot_transmission_on,
&[true, false, true, true, false, false, true, false]
);
}
other => panic!("expected Mode1, got {other:?}"),
}
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact Mode1 round-trip");
}
#[test]
fn beamhopping_mode2_round_trip() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 0x87654321,
time_plan_mode: TimePlanMode::GridRevisitSleep,
time_of_application_base: 0x0000_BBBB_BBBB,
time_of_application_ext: 0x000,
cycle_duration_base: 0x0000_6666_6666,
cycle_duration_ext: 0x090,
mode: BeamhoppingMode::Mode2 {
grid_size_base: 0x0000_1111_1111,
grid_size_ext: 0x111,
revisit_duration_base: 0x0000_2222_2222,
revisit_duration_ext: 0x022,
sleep_time_base: 0x0000_3333_3333,
sleep_time_ext: 0x133,
sleep_duration_base: 0x0000_4444_4444,
sleep_duration_ext: 0x044,
grid_size_reserved_zero_future_use: 0,
revisit_duration_reserved_zero_future_use: 0,
sleep_time_reserved_zero_future_use: 0,
sleep_duration_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 1);
assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::GridRevisitSleep);
match &bhp.plans[0].mode {
BeamhoppingMode::Mode2 { grid_size_base, .. } => {
assert_eq!(*grid_size_base, 0x0000_1111_1111);
}
other => panic!("expected Mode2, got {other:?}"),
}
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact Mode2 round-trip");
}
#[test]
fn beamhopping_reserved_mode_round_trip() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![
BeamhoppingPlan {
beamhopping_time_plan_id: 0x11111111,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0x0000_AAAA_AAAA,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_5555_5555,
cycle_duration_ext: 0x080,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0x0000_1111_1111,
dwell_duration_ext: 0x111,
on_time_base: 0x0000_2222_2222,
on_time_ext: 0x022,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
},
BeamhoppingPlan {
beamhopping_time_plan_id: 0x22222222,
time_plan_mode: TimePlanMode::Reserved(3),
time_of_application_base: 0x0000_CCCC_CCCC,
time_of_application_ext: 0x100,
cycle_duration_base: 0x0000_DDDD_DDDD,
cycle_duration_ext: 0x000,
mode: BeamhoppingMode::Reserved(vec![0xAA, 0xBB, 0xCC]),
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
},
],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 2);
assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
assert_eq!(bhp.plans[1].time_plan_mode, TimePlanMode::Reserved(3));
match &bhp.plans[1].mode {
BeamhoppingMode::Reserved(v) => {
assert_eq!(v, &[0xAA, 0xBB, 0xCC]);
}
other => panic!("expected Reserved, got {other:?}"),
}
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2, "byte-exact Reserved mode round-trip");
}
#[test]
fn cell_fragment_truncated_dsid_count() {
let body = SatBody::CellFragment(CellFragmentBody {
fragments: vec![CellFragment {
cell_fragment_id: 1,
first_occurrence: false,
last_occurrence: false,
centre: None,
delivery_system_ids: vec![0x11111111],
new_delivery_systems: Vec::new(),
obsolescent_delivery_systems: Vec::new(),
reserved_zero_future_use_4: 0,
reserved_zero_future_use_new: 0,
reserved_zero_future_use_obsolescent: 0,
}],
});
let bytes = build_sat(1, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
let mut buf2 = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf2).unwrap();
assert_eq!(bytes, buf2);
let corrupt_sat = SatSection::new(
1,
true,
0,
5,
true,
0,
0,
SatBody::CellFragment(CellFragmentBody {
fragments: vec![CellFragment {
cell_fragment_id: 1,
first_occurrence: false,
last_occurrence: false,
centre: None,
delivery_system_ids: vec![0x11111111; 50],
new_delivery_systems: Vec::new(),
obsolescent_delivery_systems: Vec::new(),
reserved_zero_future_use_4: 0,
reserved_zero_future_use_new: 0,
reserved_zero_future_use_obsolescent: 0,
}],
}),
);
let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
let section_length = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
corrupt_buf[1] = 0x80 | 0x40 | 0x30 | ((section_length >> 8) as u8 & 0x0F);
corrupt_buf[2] = (section_length & 0xFF) as u8;
let crc_end = corrupt_buf.len();
let crc = broadcast_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
let original_len = corrupt_buf.len();
corrupt_buf.truncate(original_len - 100);
let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
corrupt_buf[2] = (sl & 0xFF) as u8;
let crc_end = corrupt_buf.len();
let crc2 = broadcast_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc2.to_be_bytes());
assert!(matches!(
&(SatSection::parse(&corrupt_buf)),
Err(crate::error::Error::BufferTooShort(..))
));
}
#[test]
fn beamhopping_mode1_truncated_bit_map_size() {
let corrupt_sat = SatSection::new(
3,
true,
0,
5,
true,
0,
0,
SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 1,
time_plan_mode: TimePlanMode::Bitmap,
time_of_application_base: 0,
time_of_application_ext: 0,
cycle_duration_base: 0,
cycle_duration_ext: 0,
mode: BeamhoppingMode::Mode1 {
bit_map_size: 200,
current_slot: 0,
slot_transmission_on: vec![true; 200],
reserved_zero_future_use_size: 0,
reserved_zero_future_use_slot: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
}),
);
let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
let original_len = corrupt_buf.len();
let truncate_at = HEADER_LEN + 20;
assert!(
truncate_at + CRC_LEN < original_len,
"fixture must be large enough to truncate meaningfully"
);
{
corrupt_buf.truncate(truncate_at + CRC_LEN);
let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
corrupt_buf[2] = (sl & 0xFF) as u8;
let crc_end = corrupt_buf.len();
let crc = broadcast_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
assert!(matches!(
&(SatSection::parse(&corrupt_buf)),
Err(crate::error::Error::BufferTooShort(..))
));
}
}
#[test]
fn position_v3_truncated_ephemeris_data_count() {
let corrupt_sat = SatSection::new(
4,
true,
0,
5,
true,
0,
0,
SatBody::PositionV3(PositionV3Body {
oem_version_major: 1,
oem_version_minor: 0,
creation_date_year: 25,
creation_date_day: 1,
creation_date_day_fraction: 0,
satellites: vec![PositionV3Satellite {
satellite_id: 1,
usable_start_time_flag: false,
usable_stop_time_flag: false,
ephemeris_accel_flag: false,
covariance_flag: false,
metadata: None,
ephemeris_data: vec![
EphemerisData {
epoch_year: 25,
epoch_day: 1,
epoch_day_fraction: 0,
ephemeris_x: 0,
ephemeris_y: 0,
ephemeris_z: 0,
ephemeris_x_dot: 0,
ephemeris_y_dot: 0,
ephemeris_z_dot: 0,
acceleration: None,
reserved_zero_future_use: 0,
};
5
],
covariance: None,
reserved_zero_future_use: 0,
}],
creation_date_reserved_zero_future_use: 0,
}),
);
let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
let original_len = corrupt_buf.len();
let truncate_at = HEADER_LEN + 30;
assert!(
truncate_at + CRC_LEN < original_len,
"fixture must be large enough to truncate meaningfully"
);
{
corrupt_buf.truncate(truncate_at + CRC_LEN);
let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
corrupt_buf[2] = (sl & 0xFF) as u8;
let crc_end = corrupt_buf.len();
let crc = broadcast_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
assert!(matches!(
&(SatSection::parse(&corrupt_buf)),
Err(crate::error::Error::BufferTooShort(..))
));
}
}
#[test]
fn hand_byte_time_association() {
let body = SatBody::TimeAssociation(TimeAssociationBody {
association_type: AssociationType::UtcWithoutLeap,
leap_info: None,
ncr_base: 0x0000_AAAA_AAAA_u64,
ncr_ext: 0x1AA,
association_timestamp_seconds: 0,
association_timestamp_nanoseconds: 0,
reserved_zero_future_use_4: 0,
ncr_reserved_zero_future_use: 0,
});
let bytes = build_sat(2, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 2);
match &sat.body {
SatBody::TimeAssociation(ta) => {
assert_eq!(ta.association_type, AssociationType::UtcWithoutLeap);
assert_eq!(ta.ncr_base, 0x0000_AAAA_AAAA);
assert_eq!(ta.ncr_ext, 0x1AA);
}
other => panic!("expected TimeAssociation, got {other:?}"),
}
assert_eq!((bytes[1] >> 6) & 1, 1);
assert_eq!((bytes[3] >> 2) & 0x3F, 2);
}
#[test]
fn hand_byte_position_v2_orbital() {
let body = SatBody::PositionV2(PositionV2Body {
satellites: vec![PositionV2Satellite {
satellite_id: 0x010203,
position: PositionSystem::Orbital {
orbital_position: 0x1920,
west_east_flag: true,
reserved_zero_future_use: 0,
},
reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(0, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::PositionV2(pv2) => {
assert_eq!(pv2.satellites[0].satellite_id, 0x010203);
match &pv2.satellites[0].position {
PositionSystem::Orbital {
orbital_position, ..
} => {
assert_eq!(*orbital_position, 0x1920);
}
other => panic!("expected Orbital, got {other:?}"),
}
}
other => panic!("expected PositionV2, got {other:?}"),
}
assert_eq!((bytes[3] >> 2) & 0x3F, 0);
}
#[test]
fn hand_byte_cell_fragment() {
let body = SatBody::CellFragment(CellFragmentBody {
fragments: vec![CellFragment {
cell_fragment_id: 0xAABBCCDD,
first_occurrence: false,
last_occurrence: true,
centre: None,
delivery_system_ids: Vec::new(),
new_delivery_systems: Vec::new(),
obsolescent_delivery_systems: Vec::new(),
reserved_zero_future_use_4: 0,
reserved_zero_future_use_new: 0,
reserved_zero_future_use_obsolescent: 0,
}],
});
let bytes = build_sat(1, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::CellFragment(cf) => {
assert_eq!(cf.fragments[0].cell_fragment_id, 0xAABBCCDD);
assert!(cf.fragments[0].last_occurrence);
}
other => panic!("expected CellFragment, got {other:?}"),
}
assert_eq!((bytes[3] >> 2) & 0x3F, 1);
}
#[test]
fn hand_byte_beamhopping_mode0() {
let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
plans: vec![BeamhoppingPlan {
beamhopping_time_plan_id: 0xDEADBEEF,
time_plan_mode: TimePlanMode::DwellOnTime,
time_of_application_base: 0,
time_of_application_ext: 0,
cycle_duration_base: 0,
cycle_duration_ext: 0,
mode: BeamhoppingMode::Mode0 {
dwell_duration_base: 0,
dwell_duration_ext: 0,
on_time_base: 0,
on_time_ext: 0,
dwell_duration_reserved_zero_future_use: 0,
on_time_reserved_zero_future_use: 0,
},
reserved_zero_future_use_4: 0,
reserved_zero_future_use_mode: 0,
time_of_application_reserved_zero_future_use: 0,
cycle_duration_reserved_zero_future_use: 0,
}],
});
let bytes = build_sat(3, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0xDEADBEEF);
assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
assert_eq!((bytes[3] >> 2) & 0x3F, 3);
}
#[test]
fn hand_byte_position_v3() {
let body = SatBody::PositionV3(PositionV3Body {
oem_version_major: 1,
oem_version_minor: 2,
creation_date_year: 26,
creation_date_day: 42,
creation_date_day_fraction: 0,
satellites: Vec::new(),
creation_date_reserved_zero_future_use: 0,
});
let bytes = build_sat(4, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
match &sat.body {
SatBody::PositionV3(v3) => {
assert_eq!(v3.oem_version_major, 1);
assert_eq!(v3.oem_version_minor, 2);
}
other => panic!("expected PositionV3, got {other:?}"),
}
assert_eq!((bytes[3] >> 2) & 0x3F, 4);
}
fn crc_section(bytes: &[u8]) -> Vec<u8> {
let mut v = bytes.to_vec();
let crc = broadcast_common::crc32_mpeg2::compute(&v);
v.extend_from_slice(&crc.to_be_bytes());
v
}
#[test]
fn hand_built_time_association_anchor() {
let bytes = crc_section(&[
0x4D, 0xF0, 0x1D, 0x08, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 2);
match &sat.body {
SatBody::TimeAssociation(ta) => {
assert_eq!(ta.association_type, AssociationType::UtcWithoutLeap);
assert_eq!(ta.ncr_base, 1);
assert_eq!(ta.ncr_ext, 0);
assert_eq!(ta.association_timestamp_seconds, 0);
assert_eq!(ta.association_timestamp_nanoseconds, 0);
}
other => panic!("expected TimeAssociation, got {other:?}"),
}
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "byte-identical re-serialize");
}
#[test]
fn hand_built_position_v2_orbital_anchor() {
let bytes = crc_section(&[
0x4D, 0xF0, 0x11, 0x00, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x12,
0x34, 0x80,
]);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 0);
match &sat.body {
SatBody::PositionV2(pv2) => {
assert_eq!(pv2.satellites.len(), 1);
let s = &pv2.satellites[0];
assert_eq!(s.satellite_id, 0x010203);
match &s.position {
PositionSystem::Orbital {
orbital_position,
west_east_flag,
..
} => {
assert_eq!(*orbital_position, 0x1234);
assert!(*west_east_flag);
}
other => panic!("expected Orbital, got {other:?}"),
}
}
other => panic!("expected PositionV2, got {other:?}"),
}
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "byte-identical re-serialize");
}
#[test]
fn hand_built_cell_fragment_anchor() {
let bytes = crc_section(&[
0x4D, 0xF0, 0x14, 0x04, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00,
]);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 1);
match &sat.body {
SatBody::CellFragment(cf) => {
assert_eq!(cf.fragments.len(), 1);
let f = &cf.fragments[0];
assert_eq!(f.cell_fragment_id, 0xAABBCCDD);
assert!(!f.first_occurrence);
assert!(f.last_occurrence);
assert!(f.centre.is_none());
assert!(f.delivery_system_ids.is_empty());
assert!(f.new_delivery_systems.is_empty());
assert!(f.obsolescent_delivery_systems.is_empty());
}
other => panic!("expected CellFragment, got {other:?}"),
}
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "byte-identical re-serialize");
}
#[test]
fn hand_built_beamhopping_mode0_anchor() {
let bytes = crc_section(&[
0x4D, 0xF0, 0x29, 0x0C, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 3);
match &sat.body {
SatBody::BeamhoppingTimePlan(bhp) => {
assert_eq!(bhp.plans.len(), 1);
let p = &bhp.plans[0];
assert_eq!(p.beamhopping_time_plan_id, 0xDEADBEEF);
assert_eq!(p.time_plan_mode, TimePlanMode::DwellOnTime);
assert_eq!(p.time_of_application_base, 0);
assert_eq!(p.cycle_duration_base, 0);
match &p.mode {
BeamhoppingMode::Mode0 {
dwell_duration_base,
dwell_duration_ext,
on_time_base,
on_time_ext,
..
} => {
assert_eq!(*dwell_duration_base, 0);
assert_eq!(*dwell_duration_ext, 0);
assert_eq!(*on_time_base, 0);
assert_eq!(*on_time_ext, 0);
}
other => panic!("expected Mode0, got {other:?}"),
}
}
other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
}
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "byte-identical re-serialize");
}
#[test]
fn hand_built_position_v3_anchor() {
let bytes = crc_section(&[
0x4D, 0xF0, 0x12, 0x10, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x12, 0x1A, 0x00, 0x2A, 0x00,
0x00, 0x00, 0x00,
]);
let sat = SatSection::parse(&bytes).unwrap();
assert_eq!(sat.satellite_table_id, 4);
match &sat.body {
SatBody::PositionV3(v3) => {
assert_eq!(v3.oem_version_major, 1);
assert_eq!(v3.oem_version_minor, 2);
assert_eq!(v3.creation_date_year, 26);
assert_eq!(v3.creation_date_day, 42);
assert_eq!(v3.creation_date_day_fraction, 0);
assert!(v3.satellites.is_empty());
}
other => panic!("expected PositionV3, got {other:?}"),
}
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "byte-identical re-serialize");
}
#[test]
fn parse_rejects_truncated_time_association_body() {
let body = SatBody::TimeAssociation(TimeAssociationBody {
association_type: AssociationType::UtcWithoutLeap,
leap_info: None,
ncr_base: 0,
ncr_ext: 0,
association_timestamp_seconds: 0,
association_timestamp_nanoseconds: 0,
reserved_zero_future_use_4: 0,
ncr_reserved_zero_future_use: 0,
});
let bytes = build_sat(2, 0, &body);
let sat = SatSection::parse(&bytes).unwrap();
let mut buf = vec![0u8; sat.serialized_len()];
sat.serialize_into(&mut buf).unwrap();
buf.truncate(HEADER_LEN + 4 + CRC_LEN);
let sl = (buf.len() - SECTION_LENGTH_PREFIX) as u16;
buf[1] = (buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
buf[2] = (sl & 0xFF) as u8;
let crc_end = buf.len();
let crc = broadcast_common::crc32_mpeg2::compute(&buf[..crc_end - CRC_LEN]);
buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
assert!(matches!(
&(SatSection::parse(&buf)),
Err(crate::error::Error::BufferTooShort(..))
));
}
#[test]
fn sat_table_id_wire_to_name() {
let stid = SatTableId::try_from(0u8).unwrap();
assert_eq!(stid, SatTableId::PositionV2);
let stid = SatTableId::try_from(2u8).unwrap();
assert_eq!(stid, SatTableId::TimeAssociation);
let stid = SatTableId::try_from(4u8).unwrap();
assert_eq!(stid, SatTableId::PositionV3);
}
#[test]
fn association_type_wire_to_name() {
assert_eq!(
AssociationType::from_u8(0).name(),
"UTC without leap second"
);
assert_eq!(AssociationType::from_u8(1).name(), "UTC with leap second");
}
#[test]
fn interpolation_type_wire_to_name() {
assert_eq!(InterpolationType::from_u8(1).name(), "Linear");
assert_eq!(InterpolationType::from_u8(2).name(), "Lagrange");
assert_eq!(InterpolationType::from_u8(4).name(), "Hermite");
assert_eq!(InterpolationType::from_u8(0).name(), "Reserved");
}
#[test]
fn time_plan_mode_full_range_round_trip() {
for v in 0u8..=0x03 {
let tpm = TimePlanMode::from_u8(v);
assert_eq!(tpm.to_u8(), v, "TimePlanMode round-trip failed for {v}");
}
}
#[test]
fn time_plan_mode_known_values() {
assert_eq!(TimePlanMode::from_u8(0), TimePlanMode::DwellOnTime);
assert_eq!(TimePlanMode::from_u8(1), TimePlanMode::Bitmap);
assert_eq!(TimePlanMode::from_u8(2), TimePlanMode::GridRevisitSleep);
assert_eq!(TimePlanMode::from_u8(3), TimePlanMode::Reserved(3));
assert_eq!(TimePlanMode::DwellOnTime.name(), "dwell/on-time");
assert_eq!(TimePlanMode::Bitmap.name(), "bitmap");
assert_eq!(TimePlanMode::GridRevisitSleep.name(), "grid/revisit/sleep");
assert_eq!(TimePlanMode::Reserved(3).name(), "reserved");
}
}