use super::scheduler::Scheduler;
use crate::io::ConfigRepr;
use crate::io::{ConfigError, duration_from_str, duration_to_str, epoch_from_str, epoch_to_str};
use der::{Decode, Encode, Reader};
use hifitime::TimeUnits;
use hifitime::{Duration, Epoch, TimeScale};
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::fmt::Debug;
use std::str::FromStr;
use typed_builder::TypedBuilder;
#[cfg(feature = "python")]
use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes, types::PyType};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TypedBuilder)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
#[builder(doc)]
pub struct TrkConfig {
#[serde(default)]
#[builder(default, setter(strip_option))]
pub scheduler: Option<Scheduler>,
#[serde(
serialize_with = "duration_to_str",
deserialize_with = "duration_from_str"
)]
#[builder(default = 1.minutes())]
pub sampling: Duration,
#[builder(default, setter(strip_option))]
pub strands: Option<Vec<Strand>>,
}
impl<'a> Decode<'a> for TrkConfig {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let scheduler = if decoder.decode::<bool>()? {
Some(decoder.decode()?)
} else {
None
};
let sampling_ns = decoder.decode::<i128>()?;
let strands = if decoder.decode::<bool>()? {
Some(decoder.decode()?)
} else {
None
};
Ok(Self {
scheduler,
sampling: Duration::from_total_nanoseconds(sampling_ns),
strands,
})
}
}
impl Encode for TrkConfig {
fn encoded_len(&self) -> der::Result<der::Length> {
let mut len = self.scheduler.is_some().encoded_len()?;
if let Some(sched) = &self.scheduler {
len = (len + sched.encoded_len()?)?;
}
len = (len + self.sampling.total_nanoseconds().encoded_len()?)?;
len = (len + self.strands.is_some().encoded_len()?)?;
if let Some(strands) = &self.strands {
len = (len + strands.encoded_len()?)?;
}
Ok(len)
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
if let Some(sched) = &self.scheduler {
true.encode(encoder)?;
sched.encode(encoder)?;
} else {
false.encode(encoder)?;
}
self.sampling.total_nanoseconds().encode(encoder)?;
if let Some(strands) = &self.strands {
true.encode(encoder)?;
strands.encode(encoder)?;
} else {
false.encode(encoder)?;
}
Ok(())
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl TrkConfig {
#[new]
#[pyo3(signature = (scheduler=None, sampling=1.minutes(), strands=None))]
fn py_new(
scheduler: Option<Scheduler>,
sampling: Duration,
strands: Option<Vec<Strand>>,
) -> Self {
Self {
scheduler,
sampling,
strands,
}
}
fn __repr__(&self) -> String {
format!("{self:?}")
}
fn __str__(&self) -> String {
format!("{self:?}")
}
#[classmethod]
pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
match Self::from_der(data) {
Ok(obj) => Ok(obj),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
}
}
pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
let mut buf = Vec::new();
match self.encode_to_vec(&mut buf) {
Ok(_) => Ok(PyBytes::new(py, &buf)),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
}
}
}
impl ConfigRepr for TrkConfig {}
impl FromStr for TrkConfig {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yml::from_str(s).map_err(|source| ConfigError::ParseError { source })
}
}
impl TrkConfig {
pub fn from_sample_rate(sampling: Duration) -> Self {
Self {
sampling,
scheduler: Some(Scheduler::builder().sample_alignment(sampling).build()),
..Default::default()
}
}
pub(crate) fn sanity_check(&self) -> Result<(), ConfigError> {
if self.strands.is_some() && self.scheduler.is_some() {
return Err(ConfigError::InvalidConfig {
msg:
"Both tracking strands and a scheduler are configured, must be one or the other"
.to_string(),
});
} else if let Some(strands) = &self.strands {
if strands.is_empty() && self.scheduler.is_none() {
return Err(ConfigError::InvalidConfig {
msg: "Provided tracking strands is empty and no scheduler is defined"
.to_string(),
});
}
for (ii, strand) in strands.iter().enumerate() {
if strand.duration() < self.sampling {
return Err(ConfigError::InvalidConfig {
msg: format!(
"Strand #{ii} lasts {} which is shorter than sampling time of {}",
strand.duration(),
self.sampling
),
});
}
if strand.duration().is_negative() {
return Err(ConfigError::InvalidConfig {
msg: format!("Strand #{ii} is anti-chronological"),
});
}
}
} else if self.strands.is_none() && self.scheduler.is_none() {
return Err(ConfigError::InvalidConfig {
msg: "Neither tracking strands not a scheduler is provided".to_string(),
});
}
Ok(())
}
}
impl fmt::Display for TrkConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Sampling rate: {}", self.sampling)?;
match (&self.scheduler, &self.strands) {
(Some(sched), None) => {
write!(f, " | Mode: Auto-scheduler active ({:?})", sched)
}
(None, Some(strands)) => {
write!(f, " | Mode: Executing {} explicit strand(s)", strands.len())
}
(Some(sched), Some(strands)) => write!(
f,
" | CONFIG ERROR: Conflicting state (Scheduler {:?} AND {} strands)",
sched,
strands.len()
),
(None, None) => write!(
f,
" | CONFIG ERROR: Invalid state (Neither scheduler nor strands defined)"
),
}
}
}
impl Default for TrkConfig {
fn default() -> Self {
Self {
scheduler: Some(Scheduler::builder().build()),
sampling: 1.minutes(),
strands: None,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct Strand {
#[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
pub start: Epoch,
#[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
pub end: Epoch,
}
impl fmt::Display for Strand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}, {}] (Δt: {})",
self.start,
self.end,
self.duration()
)
}
}
impl<'a> Decode<'a> for Strand {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let start_ns = decoder.decode::<i128>()?;
let start_ts_u8 = decoder.decode::<u8>()?;
let start_ts = TimeScale::from(start_ts_u8);
let end_ns = decoder.decode::<i128>()?;
let end_ts_u8 = decoder.decode::<u8>()?;
let end_ts = TimeScale::from(end_ts_u8);
Ok(Self {
start: Epoch::from_duration(Duration::from_total_nanoseconds(start_ns), start_ts),
end: Epoch::from_duration(Duration::from_total_nanoseconds(end_ns), end_ts),
})
}
}
impl Encode for Strand {
fn encoded_len(&self) -> der::Result<der::Length> {
let ts_len = 1u8.encoded_len()?;
let start_len = (self.start.duration.total_nanoseconds().encoded_len()? + ts_len)?;
let end_len = (self.end.duration.total_nanoseconds().encoded_len()? + ts_len)?;
start_len + end_len
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
self.start.duration.total_nanoseconds().encode(encoder)?;
(self.start.time_scale as u8).encode(encoder)?;
self.end.duration.total_nanoseconds().encode(encoder)?;
(self.end.time_scale as u8).encode(encoder)
}
}
impl Strand {
pub fn new(start: Epoch, end: Epoch) -> Self {
Self { start, end }
}
pub fn contains(&self, epoch: Epoch) -> bool {
(self.start..=self.end).contains(&epoch)
}
pub fn duration(&self) -> Duration {
self.end - self.start
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl Strand {
#[new]
fn py_new(start: Epoch, end: Epoch) -> Self {
Self::new(start, end)
}
fn __repr__(&self) -> String {
format!("{self:?}")
}
fn __str__(&self) -> String {
format!("{self:?}")
}
#[classmethod]
pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
match Self::from_der(data) {
Ok(obj) => Ok(obj),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
}
}
pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
let mut buf = Vec::new();
match self.encode_to_vec(&mut buf) {
Ok(_) => Ok(PyBytes::new(py, &buf)),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
}
}
}
#[cfg(test)]
mod trkconfig_ut {
use crate::io::ConfigRepr;
use crate::od::simulator::{Cadence, Handoff, Scheduler, Strand, TrkConfig};
use der::{Decode, Encode};
use hifitime::{Epoch, TimeUnits};
#[test]
fn sanity_checks() {
let mut cfg = TrkConfig::default();
assert!(cfg.sanity_check().is_ok(), "default config should be sane");
cfg.scheduler = None;
assert!(
cfg.sanity_check().is_err(),
"no scheduler should mark this insane"
);
cfg.strands = Some(Vec::new());
assert!(
cfg.sanity_check().is_err(),
"no scheduler and empty strands should mark this insane"
);
let start = Epoch::now().unwrap();
let end = start + 10.seconds();
cfg.strands = Some(vec![Strand { start, end }]);
assert!(
cfg.sanity_check().is_err(),
"strand of too short of a duration should mark this insane"
);
let end = start + cfg.sampling;
cfg.strands = Some(vec![Strand { start, end }]);
assert!(
cfg.sanity_check().is_ok(),
"strand allowing for a single measurement should be OK"
);
cfg.strands = Some(vec![Strand {
start: end,
end: start,
}]);
assert!(
cfg.sanity_check().is_err(),
"anti chronological strand should be insane"
);
}
#[test]
fn serde_trkconfig() {
use serde_yml;
let cfg = TrkConfig::default();
let serialized = serde_yml::to_string(&cfg).unwrap();
println!("{serialized}");
let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
assert_eq!(deserd, cfg);
assert_eq!(
cfg.scheduler.unwrap(),
Scheduler::builder().min_samples(10).build()
);
assert!(cfg.strands.is_none());
let cfg = TrkConfig {
scheduler: Some(Scheduler {
cadence: Cadence::Intermittent {
on: 23.1.hours(),
off: 0.9.hours(),
},
handoff: Handoff::Eager,
min_samples: 10,
..Default::default()
}),
sampling: 45.2.seconds(),
..Default::default()
};
let serialized = serde_yml::to_string(&cfg).unwrap();
println!("{serialized}");
let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
assert_eq!(deserd, cfg);
}
#[test]
fn deserialize_from_file() {
use std::collections::BTreeMap;
use std::env;
use std::path::PathBuf;
let trkconfg_yaml: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"03_tests",
"config",
"tracking_cfg.yaml",
]
.iter()
.collect();
let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
dbg!(configs);
}
#[test]
fn api_trk_config() {
use serde_yml;
let cfg = TrkConfig::builder()
.sampling(15.seconds())
.scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
.build();
let serialized = serde_yml::to_string(&cfg).unwrap();
println!("{serialized}");
let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
assert_eq!(deserd, cfg);
let cfg = TrkConfig::builder()
.scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
.build();
assert_eq!(cfg.sampling, 60.seconds());
}
#[test]
fn test_handoff_asn1() {
let h = Handoff::Greedy;
let mut buf = Vec::new();
h.encode_to_vec(&mut buf).unwrap();
let h2 = Handoff::from_der(&buf).unwrap();
assert_eq!(h, h2);
}
#[test]
fn test_cadence_asn1() {
let c = Cadence::Intermittent {
on: 1.0.hours(),
off: 0.5.hours(),
};
let mut buf = Vec::new();
c.encode_to_vec(&mut buf).unwrap();
let c2 = Cadence::from_der(&buf).unwrap();
assert_eq!(c, c2);
let c = Cadence::Continuous;
let mut buf = Vec::new();
c.encode_to_vec(&mut buf).unwrap();
let c2 = Cadence::from_der(&buf).unwrap();
assert_eq!(c, c2);
}
#[test]
fn test_scheduler_asn1() {
let s = Scheduler::builder()
.handoff(Handoff::Overlap)
.cadence(Cadence::Intermittent {
on: 10.0.minutes(),
off: 5.0.minutes(),
})
.min_samples(5)
.sample_alignment(1.0.seconds())
.build();
let mut buf = Vec::new();
s.encode_to_vec(&mut buf).unwrap();
let s2 = Scheduler::from_der(&buf).unwrap();
assert_eq!(s, s2);
}
#[test]
fn test_strand_asn1() {
let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
let s = Strand {
start: epoch,
end: epoch + 1.0.hours(),
};
let mut buf = Vec::new();
s.encode_to_vec(&mut buf).unwrap();
let s2 = Strand::from_der(&buf).unwrap();
assert_eq!(s, s2);
let epoch_tai = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
let s = Strand {
start: epoch_tai,
end: epoch_tai + 1.0.hours(),
};
let mut buf = Vec::new();
s.encode_to_vec(&mut buf).unwrap();
let s2 = Strand::from_der(&buf).unwrap();
assert_eq!(s, s2);
}
#[test]
fn test_trkconfig_asn1() {
let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
let strand = Strand {
start: epoch,
end: (epoch + 1.0.hours()).to_time_scale(hifitime::TimeScale::TAI),
};
let cfg = TrkConfig::builder()
.sampling(10.0.seconds())
.strands(vec![strand])
.build();
let mut buf = Vec::new();
cfg.encode_to_vec(&mut buf).unwrap();
let cfg2 = TrkConfig::from_der(&buf).unwrap();
assert_eq!(cfg, cfg2);
}
}