1use super::scheduler::Scheduler;
20use crate::io::ConfigRepr;
21use crate::io::{ConfigError, duration_from_str, duration_to_str, epoch_from_str, epoch_to_str};
22use der::{Decode, Encode, Reader};
23use hifitime::TimeUnits;
24use hifitime::{Duration, Epoch, TimeScale};
25use serde::Deserialize;
26use serde::Serialize;
27use std::fmt;
28use std::fmt::Debug;
29use std::str::FromStr;
30use typed_builder::TypedBuilder;
31
32#[cfg(feature = "python")]
33use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes, types::PyType};
34
35#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, TypedBuilder)]
43#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
44#[builder(doc)]
45pub struct TrkConfig {
46 #[serde(default)]
48 #[builder(default, setter(strip_option))]
49 pub scheduler: Option<Scheduler>,
50 #[serde(
51 serialize_with = "duration_to_str",
52 deserialize_with = "duration_from_str"
53 )]
54 #[builder(default = 1.minutes())]
56 pub sampling: Duration,
57 #[builder(default, setter(strip_option))]
59 pub strands: Option<Vec<Strand>>,
60}
61
62impl<'a> Decode<'a> for TrkConfig {
63 fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
64 let scheduler = if decoder.decode::<bool>()? {
65 Some(decoder.decode()?)
66 } else {
67 None
68 };
69 let sampling_ns = decoder.decode::<i128>()?;
70 let strands = if decoder.decode::<bool>()? {
71 Some(decoder.decode()?)
72 } else {
73 None
74 };
75
76 Ok(Self {
77 scheduler,
78 sampling: Duration::from_total_nanoseconds(sampling_ns),
79 strands,
80 })
81 }
82}
83
84impl Encode for TrkConfig {
85 fn encoded_len(&self) -> der::Result<der::Length> {
86 let mut len = self.scheduler.is_some().encoded_len()?;
87 if let Some(sched) = &self.scheduler {
88 len = (len + sched.encoded_len()?)?;
89 }
90 len = (len + self.sampling.total_nanoseconds().encoded_len()?)?;
91 len = (len + self.strands.is_some().encoded_len()?)?;
92 if let Some(strands) = &self.strands {
93 len = (len + strands.encoded_len()?)?;
94 }
95 Ok(len)
96 }
97
98 fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
99 if let Some(sched) = &self.scheduler {
100 true.encode(encoder)?;
101 sched.encode(encoder)?;
102 } else {
103 false.encode(encoder)?;
104 }
105 self.sampling.total_nanoseconds().encode(encoder)?;
106 if let Some(strands) = &self.strands {
107 true.encode(encoder)?;
108 strands.encode(encoder)?;
109 } else {
110 false.encode(encoder)?;
111 }
112 Ok(())
113 }
114}
115
116#[cfg(feature = "python")]
117#[cfg_attr(feature = "python", pymethods)]
118impl TrkConfig {
119 #[new]
120 #[pyo3(signature = (scheduler=None, sampling=1.minutes(), strands=None))]
121 fn py_new(
122 scheduler: Option<Scheduler>,
123 sampling: Duration,
124 strands: Option<Vec<Strand>>,
125 ) -> Self {
126 Self {
127 scheduler,
128 sampling,
129 strands,
130 }
131 }
132
133 fn __repr__(&self) -> String {
134 format!("{self:?}")
135 }
136
137 fn __str__(&self) -> String {
138 format!("{self:?}")
139 }
140
141 #[classmethod]
146 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
147 match Self::from_der(data) {
148 Ok(obj) => Ok(obj),
149 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
150 }
151 }
152
153 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
157 let mut buf = Vec::new();
158 match self.encode_to_vec(&mut buf) {
159 Ok(_) => Ok(PyBytes::new(py, &buf)),
160 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
161 }
162 }
163}
164
165impl ConfigRepr for TrkConfig {}
166
167impl FromStr for TrkConfig {
168 type Err = ConfigError;
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 serde_yml::from_str(s).map_err(|source| ConfigError::ParseError { source })
172 }
173}
174
175impl TrkConfig {
176 pub fn from_sample_rate(sampling: Duration) -> Self {
179 Self {
180 sampling,
181 scheduler: Some(Scheduler::builder().sample_alignment(sampling).build()),
182 ..Default::default()
183 }
184 }
185
186 pub(crate) fn sanity_check(&self) -> Result<(), ConfigError> {
188 if self.strands.is_some() && self.scheduler.is_some() {
189 return Err(ConfigError::InvalidConfig {
190 msg:
191 "Both tracking strands and a scheduler are configured, must be one or the other"
192 .to_string(),
193 });
194 } else if let Some(strands) = &self.strands {
195 if strands.is_empty() && self.scheduler.is_none() {
196 return Err(ConfigError::InvalidConfig {
197 msg: "Provided tracking strands is empty and no scheduler is defined"
198 .to_string(),
199 });
200 }
201 for (ii, strand) in strands.iter().enumerate() {
202 if strand.duration() < self.sampling {
203 return Err(ConfigError::InvalidConfig {
204 msg: format!(
205 "Strand #{ii} lasts {} which is shorter than sampling time of {}",
206 strand.duration(),
207 self.sampling
208 ),
209 });
210 }
211 if strand.duration().is_negative() {
212 return Err(ConfigError::InvalidConfig {
213 msg: format!("Strand #{ii} is anti-chronological"),
214 });
215 }
216 }
217 } else if self.strands.is_none() && self.scheduler.is_none() {
218 return Err(ConfigError::InvalidConfig {
219 msg: "Neither tracking strands not a scheduler is provided".to_string(),
220 });
221 }
222
223 Ok(())
224 }
225}
226
227impl fmt::Display for TrkConfig {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 write!(f, "Sampling rate: {}", self.sampling)?;
230
231 match (&self.scheduler, &self.strands) {
232 (Some(sched), None) => {
233 write!(f, " | Mode: Auto-scheduler active ({:?})", sched)
234 }
235 (None, Some(strands)) => {
236 write!(f, " | Mode: Executing {} explicit strand(s)", strands.len())
237 }
238 (Some(sched), Some(strands)) => write!(
239 f,
240 " | CONFIG ERROR: Conflicting state (Scheduler {:?} AND {} strands)",
241 sched,
242 strands.len()
243 ),
244 (None, None) => write!(
245 f,
246 " | CONFIG ERROR: Invalid state (Neither scheduler nor strands defined)"
247 ),
248 }
249 }
250}
251
252impl Default for TrkConfig {
253 fn default() -> Self {
255 Self {
256 scheduler: Some(Scheduler::builder().build()),
258 sampling: 1.minutes(),
259 strands: None,
260 }
261 }
262}
263
264#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
269#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
270pub struct Strand {
271 #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
272 pub start: Epoch,
273 #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
274 pub end: Epoch,
275}
276
277impl fmt::Display for Strand {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(
280 f,
281 "[{}, {}] (Δt: {})",
282 self.start,
283 self.end,
284 self.duration()
285 )
286 }
287}
288
289impl<'a> Decode<'a> for Strand {
290 fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
291 let start_ns = decoder.decode::<i128>()?;
292 let start_ts_u8 = decoder.decode::<u8>()?;
293 let start_ts = TimeScale::from(start_ts_u8);
294
295 let end_ns = decoder.decode::<i128>()?;
296 let end_ts_u8 = decoder.decode::<u8>()?;
297 let end_ts = TimeScale::from(end_ts_u8);
298
299 Ok(Self {
300 start: Epoch::from_duration(Duration::from_total_nanoseconds(start_ns), start_ts),
301 end: Epoch::from_duration(Duration::from_total_nanoseconds(end_ns), end_ts),
302 })
303 }
304}
305
306impl Encode for Strand {
307 fn encoded_len(&self) -> der::Result<der::Length> {
308 let ts_len = 1u8.encoded_len()?;
309 let start_len = (self.start.duration.total_nanoseconds().encoded_len()? + ts_len)?;
310 let end_len = (self.end.duration.total_nanoseconds().encoded_len()? + ts_len)?;
311 start_len + end_len
312 }
313
314 fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
315 self.start.duration.total_nanoseconds().encode(encoder)?;
316 (self.start.time_scale as u8).encode(encoder)?;
317
318 self.end.duration.total_nanoseconds().encode(encoder)?;
319 (self.end.time_scale as u8).encode(encoder)
320 }
321}
322
323impl Strand {
324 pub fn new(start: Epoch, end: Epoch) -> Self {
325 Self { start, end }
326 }
327
328 pub fn contains(&self, epoch: Epoch) -> bool {
330 (self.start..=self.end).contains(&epoch)
331 }
332
333 pub fn duration(&self) -> Duration {
335 self.end - self.start
336 }
337}
338
339#[cfg(feature = "python")]
340#[cfg_attr(feature = "python", pymethods)]
341impl Strand {
342 #[new]
343 fn py_new(start: Epoch, end: Epoch) -> Self {
344 Self::new(start, end)
345 }
346
347 fn __repr__(&self) -> String {
348 format!("{self:?}")
349 }
350
351 fn __str__(&self) -> String {
352 format!("{self:?}")
353 }
354
355 #[classmethod]
360 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
361 match Self::from_der(data) {
362 Ok(obj) => Ok(obj),
363 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
364 }
365 }
366
367 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
371 let mut buf = Vec::new();
372 match self.encode_to_vec(&mut buf) {
373 Ok(_) => Ok(PyBytes::new(py, &buf)),
374 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
375 }
376 }
377}
378
379#[cfg(test)]
380mod trkconfig_ut {
381 use crate::io::ConfigRepr;
382 use crate::od::simulator::{Cadence, Handoff, Scheduler, Strand, TrkConfig};
383 use der::{Decode, Encode};
384 use hifitime::{Epoch, TimeUnits};
385
386 #[test]
387 fn sanity_checks() {
388 let mut cfg = TrkConfig::default();
389 assert!(cfg.sanity_check().is_ok(), "default config should be sane");
390
391 cfg.scheduler = None;
392 assert!(
393 cfg.sanity_check().is_err(),
394 "no scheduler should mark this insane"
395 );
396
397 cfg.strands = Some(Vec::new());
398 assert!(
399 cfg.sanity_check().is_err(),
400 "no scheduler and empty strands should mark this insane"
401 );
402
403 let start = Epoch::now().unwrap();
404 let end = start + 10.seconds();
405 cfg.strands = Some(vec![Strand { start, end }]);
406 assert!(
407 cfg.sanity_check().is_err(),
408 "strand of too short of a duration should mark this insane"
409 );
410
411 let end = start + cfg.sampling;
412 cfg.strands = Some(vec![Strand { start, end }]);
413 assert!(
414 cfg.sanity_check().is_ok(),
415 "strand allowing for a single measurement should be OK"
416 );
417
418 cfg.strands = Some(vec![Strand {
420 start: end,
421 end: start,
422 }]);
423 assert!(
424 cfg.sanity_check().is_err(),
425 "anti chronological strand should be insane"
426 );
427 }
428
429 #[test]
430 fn serde_trkconfig() {
431 use serde_yml;
432
433 let cfg = TrkConfig::default();
435 let serialized = serde_yml::to_string(&cfg).unwrap();
436 println!("{serialized}");
437 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
438 assert_eq!(deserd, cfg);
439 assert_eq!(
440 cfg.scheduler.unwrap(),
441 Scheduler::builder().min_samples(10).build()
442 );
443 assert!(cfg.strands.is_none());
444
445 let cfg = TrkConfig {
447 scheduler: Some(Scheduler {
448 cadence: Cadence::Intermittent {
449 on: 23.1.hours(),
450 off: 0.9.hours(),
451 },
452 handoff: Handoff::Eager,
453 min_samples: 10,
454 ..Default::default()
455 }),
456 sampling: 45.2.seconds(),
457 ..Default::default()
458 };
459 let serialized = serde_yml::to_string(&cfg).unwrap();
460 println!("{serialized}");
461 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
462 assert_eq!(deserd, cfg);
463 }
464
465 #[test]
466 fn deserialize_from_file() {
467 use std::collections::BTreeMap;
468 use std::env;
469 use std::path::PathBuf;
470
471 let trkconfg_yaml: PathBuf = [
473 env!("CARGO_MANIFEST_DIR"),
474 "../data",
475 "03_tests",
476 "config",
477 "tracking_cfg.yaml",
478 ]
479 .iter()
480 .collect();
481
482 let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
483 dbg!(configs);
484 }
485
486 #[test]
487 fn api_trk_config() {
488 use serde_yml;
489
490 let cfg = TrkConfig::builder()
491 .sampling(15.seconds())
492 .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
493 .build();
494
495 let serialized = serde_yml::to_string(&cfg).unwrap();
496 println!("{serialized}");
497 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
498 assert_eq!(deserd, cfg);
499
500 let cfg = TrkConfig::builder()
501 .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
502 .build();
503
504 assert_eq!(cfg.sampling, 60.seconds());
505 }
506
507 #[test]
508 fn test_handoff_asn1() {
509 let h = Handoff::Greedy;
510 let mut buf = Vec::new();
511 h.encode_to_vec(&mut buf).unwrap();
512 let h2 = Handoff::from_der(&buf).unwrap();
513 assert_eq!(h, h2);
514 }
515
516 #[test]
517 fn test_cadence_asn1() {
518 let c = Cadence::Intermittent {
519 on: 1.0.hours(),
520 off: 0.5.hours(),
521 };
522 let mut buf = Vec::new();
523 c.encode_to_vec(&mut buf).unwrap();
524 let c2 = Cadence::from_der(&buf).unwrap();
525 assert_eq!(c, c2);
526
527 let c = Cadence::Continuous;
528 let mut buf = Vec::new();
529 c.encode_to_vec(&mut buf).unwrap();
530 let c2 = Cadence::from_der(&buf).unwrap();
531 assert_eq!(c, c2);
532 }
533
534 #[test]
535 fn test_scheduler_asn1() {
536 let s = Scheduler::builder()
537 .handoff(Handoff::Overlap)
538 .cadence(Cadence::Intermittent {
539 on: 10.0.minutes(),
540 off: 5.0.minutes(),
541 })
542 .min_samples(5)
543 .sample_alignment(1.0.seconds())
544 .build();
545
546 let mut buf = Vec::new();
547 s.encode_to_vec(&mut buf).unwrap();
548 let s2 = Scheduler::from_der(&buf).unwrap();
549 assert_eq!(s, s2);
550 }
551
552 #[test]
553 fn test_strand_asn1() {
554 let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
555 let s = Strand {
556 start: epoch,
557 end: epoch + 1.0.hours(),
558 };
559
560 let mut buf = Vec::new();
561 s.encode_to_vec(&mut buf).unwrap();
562 let s2 = Strand::from_der(&buf).unwrap();
563
564 assert_eq!(s, s2);
565
566 let epoch_tai = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
568 let s = Strand {
569 start: epoch_tai,
570 end: epoch_tai + 1.0.hours(),
571 };
572
573 let mut buf = Vec::new();
574 s.encode_to_vec(&mut buf).unwrap();
575 let s2 = Strand::from_der(&buf).unwrap();
576
577 assert_eq!(s, s2);
578 }
579
580 #[test]
581 fn test_trkconfig_asn1() {
582 let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
584 let strand = Strand {
585 start: epoch,
586 end: (epoch + 1.0.hours()).to_time_scale(hifitime::TimeScale::TAI),
587 };
588
589 let cfg = TrkConfig::builder()
590 .sampling(10.0.seconds())
591 .strands(vec![strand])
592 .build();
593
594 let mut buf = Vec::new();
595 cfg.encode_to_vec(&mut buf).unwrap();
596 let cfg2 = TrkConfig::from_der(&buf).unwrap();
597
598 assert_eq!(cfg, cfg2);
599 }
600}