use crate::descriptors::mjd_time::{MJD_BCD_LEN, MjdBcdTime};
use crate::descriptors::{descriptor_body, write_descriptor_header};
use crate::error::{Error, Result};
use broadcast_common::{Parse, Serialize};
pub const TAG: u8 = 0x01;
const HEADER_LEN: usize = 2;
const FIXED_LEN: usize = 2 * MJD_BCD_LEN + 4;
const FINAL_AVAILABILITY: u8 = 0x80;
const PERIODICITY_FLAG: u8 = 0x40;
const UNIT_MASK: u8 = 0x03;
const PERIOD_UNIT_SHIFT: u8 = 4;
const DURATION_UNIT_SHIFT: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum TimeUnit {
Second,
Minute,
Hour,
Day,
}
impl TimeUnit {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v & UNIT_MASK {
0 => Self::Second,
1 => Self::Minute,
2 => Self::Hour,
_ => Self::Day,
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Second => 0,
Self::Minute => 1,
Self::Hour => 2,
Self::Day => 3,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Second => "second",
Self::Minute => "minute",
Self::Hour => "hour",
Self::Day => "day",
}
}
}
broadcast_common::impl_spec_display!(TimeUnit);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct SchedulingDescriptor<'a> {
pub start_date_time: MjdBcdTime,
pub end_date_time: MjdBcdTime,
pub final_availability: bool,
pub periodicity_flag: bool,
pub period_unit: TimeUnit,
pub duration_unit: TimeUnit,
pub estimated_cycle_time_unit: TimeUnit,
pub period: u8,
pub duration: u8,
pub estimated_cycle_time: u8,
pub private_data: &'a [u8],
}
impl<'a> Parse<'a> for SchedulingDescriptor<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = descriptor_body(
bytes,
TAG,
"SchedulingDescriptor",
"unexpected tag for scheduling_descriptor",
)?;
if body.len() < FIXED_LEN {
return Err(Error::InvalidDescriptor {
tag: TAG,
reason: "scheduling_descriptor body shorter than 14 bytes",
});
}
let flags = body[2 * MJD_BCD_LEN];
let units = body[2 * MJD_BCD_LEN + 1];
Ok(Self {
start_date_time: MjdBcdTime::read(body, 0, "scheduling_descriptor start_date_time")?,
end_date_time: MjdBcdTime::read(
body,
MJD_BCD_LEN,
"scheduling_descriptor end_date_time",
)?,
final_availability: flags & FINAL_AVAILABILITY != 0,
periodicity_flag: flags & PERIODICITY_FLAG != 0,
period_unit: TimeUnit::from_u8(flags >> PERIOD_UNIT_SHIFT),
duration_unit: TimeUnit::from_u8(flags >> DURATION_UNIT_SHIFT),
estimated_cycle_time_unit: TimeUnit::from_u8(flags),
period: units,
duration: body[2 * MJD_BCD_LEN + 2],
estimated_cycle_time: body[2 * MJD_BCD_LEN + 3],
private_data: &body[FIXED_LEN..],
})
}
}
impl Serialize for SchedulingDescriptor<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
HEADER_LEN + FIXED_LEN + self.private_data.len()
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
if buf.len() < len {
return Err(Error::OutputBufferTooSmall {
need: len,
have: buf.len(),
});
}
write_descriptor_header(buf, TAG, len - HEADER_LEN)?;
let b = &mut buf[HEADER_LEN..];
self.start_date_time.write(b, 0);
self.end_date_time.write(b, MJD_BCD_LEN);
let period_unit =
broadcast_common::len::fit_mask(self.period_unit.to_u8(), UNIT_MASK, "period_unit")?;
let duration_unit = broadcast_common::len::fit_mask(
self.duration_unit.to_u8(),
UNIT_MASK,
"duration_unit",
)?;
let cycle_unit = broadcast_common::len::fit_mask(
self.estimated_cycle_time_unit.to_u8(),
UNIT_MASK,
"estimated_cycle_time_unit",
)?;
let mut flags = (period_unit << PERIOD_UNIT_SHIFT)
| (duration_unit << DURATION_UNIT_SHIFT)
| cycle_unit;
if self.final_availability {
flags |= FINAL_AVAILABILITY;
}
if self.periodicity_flag {
flags |= PERIODICITY_FLAG;
}
b[2 * MJD_BCD_LEN] = flags;
b[2 * MJD_BCD_LEN + 1] = self.period;
b[2 * MJD_BCD_LEN + 2] = self.duration;
b[2 * MJD_BCD_LEN + 3] = self.estimated_cycle_time;
b[FIXED_LEN..FIXED_LEN + self.private_data.len()].copy_from_slice(self.private_data);
Ok(len)
}
}
impl<'a> crate::traits::DescriptorDef<'a> for SchedulingDescriptor<'a> {
const TAG: u8 = TAG;
const NAME: &'static str = "SCHEDULING";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_fields_and_round_trip() {
let bytes = [
TAG,
15,
0xC0,
0x79,
0x12,
0x45,
0x00,
0xC0,
0x7A,
0x01,
0x02,
0x03,
0b1110_0111,
6,
30,
4,
0xEE,
];
let d = SchedulingDescriptor::parse(&bytes).unwrap();
assert_eq!(
d.start_date_time.parts(),
(0xC079, Some(12), Some(45), Some(0))
);
assert_eq!(d.end_date_time.parts(), (0xC07A, Some(1), Some(2), Some(3)));
assert!(d.final_availability && d.periodicity_flag);
assert_eq!(d.period_unit, TimeUnit::Hour);
assert_eq!(d.duration_unit, TimeUnit::Minute);
assert_eq!(d.estimated_cycle_time_unit, TimeUnit::Day);
assert_eq!((d.period, d.duration, d.estimated_cycle_time), (6, 30, 4));
assert_eq!(d.private_data, &[0xEE]);
let mut out = vec![0u8; d.serialized_len()];
d.serialize_into(&mut out).unwrap();
assert_eq!(out, bytes);
}
#[test]
fn every_flag_byte_round_trips() {
for f in 0..=u8::MAX {
let bytes = [TAG, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, f, 1, 2, 3];
let d = SchedulingDescriptor::parse(&bytes).unwrap();
let mut out = vec![0u8; d.serialized_len()];
d.serialize_into(&mut out).unwrap();
assert_eq!(out, bytes, "flags {f:#04x}");
}
}
#[test]
fn rejects_short_body_and_wrong_tag() {
assert!(
SchedulingDescriptor::parse(&[TAG, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).is_err()
);
assert!(SchedulingDescriptor::parse(&[0x02, 0]).is_err());
assert_eq!(TimeUnit::Day.to_string(), "day");
}
}