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)]
39#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
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 fn __repr__(&self) -> String {
130 format!("{self:?}")
131 }
132
133 fn __str__(&self) -> String {
134 format!("{self:?}")
135 }
136
137 #[classmethod]
142 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
143 match Self::from_der(data) {
144 Ok(obj) => Ok(obj),
145 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
146 }
147 }
148
149 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
153 let mut buf = Vec::new();
154 match self.encode_to_vec(&mut buf) {
155 Ok(_) => Ok(PyBytes::new(py, &buf)),
156 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
157 }
158 }
159}
160
161impl ConfigRepr for TrkConfig {}
162
163impl FromStr for TrkConfig {
164 type Err = ConfigError;
165
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 serde_yml::from_str(s).map_err(|source| ConfigError::ParseError { source })
168 }
169}
170
171impl TrkConfig {
172 pub fn from_sample_rate(sampling: Duration) -> Self {
175 Self {
176 sampling,
177 scheduler: Some(Scheduler::builder().sample_alignment(sampling).build()),
178 ..Default::default()
179 }
180 }
181
182 pub(crate) fn sanity_check(&self) -> Result<(), ConfigError> {
184 if self.strands.is_some() && self.scheduler.is_some() {
185 return Err(ConfigError::InvalidConfig {
186 msg:
187 "Both tracking strands and a scheduler are configured, must be one or the other"
188 .to_string(),
189 });
190 } else if let Some(strands) = &self.strands {
191 if strands.is_empty() && self.scheduler.is_none() {
192 return Err(ConfigError::InvalidConfig {
193 msg: "Provided tracking strands is empty and no scheduler is defined"
194 .to_string(),
195 });
196 }
197 for (ii, strand) in strands.iter().enumerate() {
198 if strand.duration() < self.sampling {
199 return Err(ConfigError::InvalidConfig {
200 msg: format!(
201 "Strand #{ii} lasts {} which is shorter than sampling time of {}",
202 strand.duration(),
203 self.sampling
204 ),
205 });
206 }
207 if strand.duration().is_negative() {
208 return Err(ConfigError::InvalidConfig {
209 msg: format!("Strand #{ii} is anti-chronological"),
210 });
211 }
212 }
213 } else if self.strands.is_none() && self.scheduler.is_none() {
214 return Err(ConfigError::InvalidConfig {
215 msg: "Neither tracking strands not a scheduler is provided".to_string(),
216 });
217 }
218
219 Ok(())
220 }
221}
222
223impl fmt::Display for TrkConfig {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 write!(f, "Sampling rate: {}", self.sampling)?;
226
227 match (&self.scheduler, &self.strands) {
228 (Some(sched), None) => {
229 write!(f, " | Mode: Auto-scheduler active ({:?})", sched)
230 }
231 (None, Some(strands)) => {
232 write!(f, " | Mode: Executing {} explicit strand(s)", strands.len())
233 }
234 (Some(sched), Some(strands)) => write!(
235 f,
236 " | CONFIG ERROR: Conflicting state (Scheduler {:?} AND {} strands)",
237 sched,
238 strands.len()
239 ),
240 (None, None) => write!(
241 f,
242 " | CONFIG ERROR: Invalid state (Neither scheduler nor strands defined)"
243 ),
244 }
245 }
246}
247
248impl Default for TrkConfig {
249 fn default() -> Self {
251 Self {
252 scheduler: Some(Scheduler::builder().build()),
254 sampling: 1.minutes(),
255 strands: None,
256 }
257 }
258}
259
260#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
262#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
263pub struct Strand {
264 #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
265 pub start: Epoch,
266 #[serde(serialize_with = "epoch_to_str", deserialize_with = "epoch_from_str")]
267 pub end: Epoch,
268}
269
270impl fmt::Display for Strand {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 write!(
273 f,
274 "[{}, {}] (Δt: {})",
275 self.start,
276 self.end,
277 self.duration()
278 )
279 }
280}
281
282impl<'a> Decode<'a> for Strand {
283 fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
284 let start_ns = decoder.decode::<i128>()?;
285 let start_ts_u8 = decoder.decode::<u8>()?;
286 let start_ts = TimeScale::from(start_ts_u8);
287
288 let end_ns = decoder.decode::<i128>()?;
289 let end_ts_u8 = decoder.decode::<u8>()?;
290 let end_ts = TimeScale::from(end_ts_u8);
291
292 Ok(Self {
293 start: Epoch::from_duration(Duration::from_total_nanoseconds(start_ns), start_ts),
294 end: Epoch::from_duration(Duration::from_total_nanoseconds(end_ns), end_ts),
295 })
296 }
297}
298
299impl Encode for Strand {
300 fn encoded_len(&self) -> der::Result<der::Length> {
301 let ts_len = 1u8.encoded_len()?;
302 let start_len = (self.start.duration.total_nanoseconds().encoded_len()? + ts_len)?;
303 let end_len = (self.end.duration.total_nanoseconds().encoded_len()? + ts_len)?;
304 start_len + end_len
305 }
306
307 fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
308 self.start.duration.total_nanoseconds().encode(encoder)?;
309 (self.start.time_scale as u8).encode(encoder)?;
310
311 self.end.duration.total_nanoseconds().encode(encoder)?;
312 (self.end.time_scale as u8).encode(encoder)
313 }
314}
315
316impl Strand {
317 pub fn new(start: Epoch, end: Epoch) -> Self {
318 Self { start, end }
319 }
320
321 pub fn contains(&self, epoch: Epoch) -> bool {
323 (self.start..=self.end).contains(&epoch)
324 }
325
326 pub fn duration(&self) -> Duration {
328 self.end - self.start
329 }
330}
331
332#[cfg(feature = "python")]
333#[cfg_attr(feature = "python", pymethods)]
334impl Strand {
335 #[new]
336 fn py_new(start: Epoch, end: Epoch) -> Self {
337 Self::new(start, end)
338 }
339
340 fn __repr__(&self) -> String {
341 format!("{self:?}")
342 }
343
344 fn __str__(&self) -> String {
345 format!("{self:?}")
346 }
347
348 #[classmethod]
353 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
354 match Self::from_der(data) {
355 Ok(obj) => Ok(obj),
356 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
357 }
358 }
359
360 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
364 let mut buf = Vec::new();
365 match self.encode_to_vec(&mut buf) {
366 Ok(_) => Ok(PyBytes::new(py, &buf)),
367 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
368 }
369 }
370}
371
372#[cfg(test)]
373mod trkconfig_ut {
374 use crate::io::ConfigRepr;
375 use crate::od::simulator::{Cadence, Handoff, Scheduler, Strand, TrkConfig};
376 use der::{Decode, Encode};
377 use hifitime::{Epoch, TimeUnits};
378
379 #[test]
380 fn sanity_checks() {
381 let mut cfg = TrkConfig::default();
382 assert!(cfg.sanity_check().is_ok(), "default config should be sane");
383
384 cfg.scheduler = None;
385 assert!(
386 cfg.sanity_check().is_err(),
387 "no scheduler should mark this insane"
388 );
389
390 cfg.strands = Some(Vec::new());
391 assert!(
392 cfg.sanity_check().is_err(),
393 "no scheduler and empty strands should mark this insane"
394 );
395
396 let start = Epoch::now().unwrap();
397 let end = start + 10.seconds();
398 cfg.strands = Some(vec![Strand { start, end }]);
399 assert!(
400 cfg.sanity_check().is_err(),
401 "strand of too short of a duration should mark this insane"
402 );
403
404 let end = start + cfg.sampling;
405 cfg.strands = Some(vec![Strand { start, end }]);
406 assert!(
407 cfg.sanity_check().is_ok(),
408 "strand allowing for a single measurement should be OK"
409 );
410
411 cfg.strands = Some(vec![Strand {
413 start: end,
414 end: start,
415 }]);
416 assert!(
417 cfg.sanity_check().is_err(),
418 "anti chronological strand should be insane"
419 );
420 }
421
422 #[test]
423 fn serde_trkconfig() {
424 use serde_yml;
425
426 let cfg = TrkConfig::default();
428 let serialized = serde_yml::to_string(&cfg).unwrap();
429 println!("{serialized}");
430 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
431 assert_eq!(deserd, cfg);
432 assert_eq!(
433 cfg.scheduler.unwrap(),
434 Scheduler::builder().min_samples(10).build()
435 );
436 assert!(cfg.strands.is_none());
437
438 let cfg = TrkConfig {
440 scheduler: Some(Scheduler {
441 cadence: Cadence::Intermittent {
442 on: 23.1.hours(),
443 off: 0.9.hours(),
444 },
445 handoff: Handoff::Eager,
446 min_samples: 10,
447 ..Default::default()
448 }),
449 sampling: 45.2.seconds(),
450 ..Default::default()
451 };
452 let serialized = serde_yml::to_string(&cfg).unwrap();
453 println!("{serialized}");
454 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
455 assert_eq!(deserd, cfg);
456 }
457
458 #[test]
459 fn deserialize_from_file() {
460 use std::collections::BTreeMap;
461 use std::env;
462 use std::path::PathBuf;
463
464 let trkconfg_yaml: PathBuf = [
466 env!("CARGO_MANIFEST_DIR"),
467 "../data",
468 "03_tests",
469 "config",
470 "tracking_cfg.yaml",
471 ]
472 .iter()
473 .collect();
474
475 let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
476 dbg!(configs);
477 }
478
479 #[test]
480 fn api_trk_config() {
481 use serde_yml;
482
483 let cfg = TrkConfig::builder()
484 .sampling(15.seconds())
485 .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
486 .build();
487
488 let serialized = serde_yml::to_string(&cfg).unwrap();
489 println!("{serialized}");
490 let deserd: TrkConfig = serde_yml::from_str(&serialized).unwrap();
491 assert_eq!(deserd, cfg);
492
493 let cfg = TrkConfig::builder()
494 .scheduler(Scheduler::builder().handoff(Handoff::Overlap).build())
495 .build();
496
497 assert_eq!(cfg.sampling, 60.seconds());
498 }
499
500 #[test]
501 fn test_handoff_asn1() {
502 let h = Handoff::Greedy;
503 let mut buf = Vec::new();
504 h.encode_to_vec(&mut buf).unwrap();
505 let h2 = Handoff::from_der(&buf).unwrap();
506 assert_eq!(h, h2);
507 }
508
509 #[test]
510 fn test_cadence_asn1() {
511 let c = Cadence::Intermittent {
512 on: 1.0.hours(),
513 off: 0.5.hours(),
514 };
515 let mut buf = Vec::new();
516 c.encode_to_vec(&mut buf).unwrap();
517 let c2 = Cadence::from_der(&buf).unwrap();
518 assert_eq!(c, c2);
519
520 let c = Cadence::Continuous;
521 let mut buf = Vec::new();
522 c.encode_to_vec(&mut buf).unwrap();
523 let c2 = Cadence::from_der(&buf).unwrap();
524 assert_eq!(c, c2);
525 }
526
527 #[test]
528 fn test_scheduler_asn1() {
529 let s = Scheduler::builder()
530 .handoff(Handoff::Overlap)
531 .cadence(Cadence::Intermittent {
532 on: 10.0.minutes(),
533 off: 5.0.minutes(),
534 })
535 .min_samples(5)
536 .sample_alignment(1.0.seconds())
537 .build();
538
539 let mut buf = Vec::new();
540 s.encode_to_vec(&mut buf).unwrap();
541 let s2 = Scheduler::from_der(&buf).unwrap();
542 assert_eq!(s, s2);
543 }
544
545 #[test]
546 fn test_strand_asn1() {
547 let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
548 let s = Strand {
549 start: epoch,
550 end: epoch + 1.0.hours(),
551 };
552
553 let mut buf = Vec::new();
554 s.encode_to_vec(&mut buf).unwrap();
555 let s2 = Strand::from_der(&buf).unwrap();
556
557 assert_eq!(s, s2);
558
559 let epoch_tai = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
561 let s = Strand {
562 start: epoch_tai,
563 end: epoch_tai + 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
573 #[test]
574 fn test_trkconfig_asn1() {
575 let epoch = Epoch::from_gregorian_utc_at_midnight(2023, 1, 1);
577 let strand = Strand {
578 start: epoch,
579 end: (epoch + 1.0.hours()).to_time_scale(hifitime::TimeScale::TAI),
580 };
581
582 let cfg = TrkConfig::builder()
583 .sampling(10.0.seconds())
584 .strands(vec![strand])
585 .build();
586
587 let mut buf = Vec::new();
588 cfg.encode_to_vec(&mut buf).unwrap();
589 let cfg2 = TrkConfig::from_der(&buf).unwrap();
590
591 assert_eq!(cfg, cfg2);
592 }
593}