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