Skip to main content

gondola_core/
monitoring.rs

1// This file is part of gaps-online-software and published 
2// under the GPLv3 license
3
4pub mod pa_moni_data;
5pub use pa_moni_data::{
6  PAMoniData,
7  PAMoniDataSeries
8};
9pub mod pb_moni_data;
10pub use pb_moni_data::{
11  PBMoniData,
12  PBMoniDataSeries
13};
14pub mod mtb_moni_data;
15pub use mtb_moni_data::{
16  MtbMoniData,
17  MtbMoniDataSeries
18};
19pub mod ltb_moni_data;
20pub use ltb_moni_data::{
21  LTBMoniData,
22  LTBMoniDataSeries
23};
24pub mod rb_moni_data;
25pub use rb_moni_data::{
26  RBMoniData,
27  RBMoniDataSeries
28};
29pub mod cpu_moni_data;
30pub use cpu_moni_data::{
31  CPUMoniData,
32  CPUMoniDataSeries
33};
34
35pub mod heartbeats;
36pub use heartbeats::{
37  DataSinkHB,
38  DataSinkHBSeries,
39  MasterTriggerHB,
40  MasterTriggerHBSeries,
41  EventBuilderHB,
42  EventBuilderHBSeries,
43};
44
45pub mod run_statistics;
46pub use run_statistics::RunStatistics;
47
48pub mod sip_position;
49pub use sip_position::{
50  SipPosMoniData,
51  SipPosMoniDataSeries
52};
53
54pub mod sip_pressure;
55pub use sip_pressure::{
56  SipPresMoniData,
57  SipPresMoniDataSeries,
58};
59
60pub mod sip_time;
61pub use sip_time::{
62  SipTimeMoniData,
63  SipTimeMoniDataSeries
64};
65
66pub mod gcu_ev_stats;
67pub use gcu_ev_stats::{
68  GcuEvBldStatsMoniData,
69  GcuEvBldStatsMoniDataSeries
70};
71
72pub mod tracker_gps;
73pub use tracker_gps::{
74  TrackerGpsMoniData,
75  TrackerGpsMoniDataSeries
76}; 
77
78pub mod cooling; 
79pub use cooling::{
80  CoolingMoniData,
81  CoolingMoniDataSeries
82};
83
84pub mod wastie;
85pub use wastie::*;
86
87use std::collections::VecDeque;
88use std::collections::HashMap;
89
90#[cfg(feature="pybindings")]
91use crate::prelude::*;
92
93#[cfg(feature="pybindings")]
94use polars::frame::column::Column;
95#[cfg(feature="pybindings")]
96use polars::prelude::NamedFrom; 
97
98/// Monitoring data shall share the same kind 
99/// of interface. 
100pub trait MoniData {
101  /// Monitoring data is always tied to a specific
102  /// board. This might not be its own board, but 
103  /// maybe the RB the data was gathered from
104  /// This is an unique identifier for the 
105  /// monitoring data across the same type. 
106  ///
107  /// MoniData which only belongs to a single board will 
108  /// have board id 0
109  fn get_board_id(&self) -> u8;
110  
111  /// Access the (data) members by name 
112  fn get(&self, varname : &str) -> Option<f32>;
113
114  /// A list of the variables in this MoniData
115  fn keys() -> Vec<&'static str>;
116
117  fn get_timestamp(&self) -> u64 {
118    0
119  }
120
121  fn set_timestamp(&mut self, _ts : u64) {
122  }
123}
124
125// FIXME - this whole system gets too much. We need to reorganize this 
126// -> push it to 0.13. For now, just make the timestamp settable, which 
127// will basically achieve the same thing
128///// A specialized MoniData series, originating in housekeeping 
129///// from the TOF system. During flight, the housekeeping 
130///// data has been wrapped in telemetry packets as an "outer layer" 
131//pub trait TofMoniData 
132//  where Self : MoniData {
133//  
134//  // Read TofMoniData from TelemetryPackets, since technically, 
135//  // they are not TelemetryPackable
136//  fn from_telemetrypacket(packet : TelemetryPacket) -> PyResult<Self> {
137//  } 
138//}
139
140
141/// A MoniSeries is a collection of (primarily) monitoring
142/// data, which comes from multiple senders.
143/// E.g. a MoniSeries could hold RBMoniData from all 
144/// 40 ReadoutBoards.
145pub trait MoniSeries<T>
146  where T : Copy + MoniData {
147
148  fn get_first_ts(&self) -> u64;
149
150  fn get_data(&self) -> &HashMap<u8,VecDeque<T>>;
151
152  fn get_data_mut(&mut self) -> &mut HashMap<u8,VecDeque<T>>;
153 
154  fn get_max_size(&self) -> usize;
155
156  fn set_max_size(&mut self, size : usize);
157
158  fn get_timestamps(&self) -> &Vec<u64>;
159
160  fn add_timestamp(&mut self, ts : u64);
161
162  /// A HashMap of -> rbid, Vec\<var\> 
163  fn get_var(&self, varname : &str) -> HashMap<u8, Vec<f32>> {
164    let mut values = HashMap::<u8, Vec<f32>>::new();
165    for k in self.get_data().keys() {
166      match self.get_var_for_board(varname, k) {
167        None => (),
168        Some(vals) => {
169          values.insert(*k, vals);
170        }
171      }
172      //values.insert(*k, Vec::<f32>::new());
173      //match self.get_data().get(k) {
174      //  None => (),
175      //  Some(vec_moni) => {
176      //    for moni in vec_moni {
177      //      match moni.get(varname) {
178      //        None => (),
179      //        Some(val) => {
180      //          values.get_mut(k).unwrap().push(val);
181      //        }
182      //      }
183      //    }
184      //  }
185      //}
186    }
187    values
188  }
189
190  /// Get a certain variable, but only for a single board
191  fn get_var_for_board(&self, varname : &str, rb_id : &u8) -> Option<Vec<f32>> {
192    let mut values = Vec::<f32>::new();
193    match self.get_data().get(&rb_id) {
194      None => (),
195      Some(vec_moni) => {
196        for moni in vec_moni {
197          match moni.get(varname) {
198            None => {
199              return None;
200            },
201            Some(val) => {
202              values.push(val);
203            }
204          }
205        }
206      }
207    }
208    // FIXME This needs to be returning a reference,
209    // not cloning
210    Some(values)
211  }
212
213  #[cfg(feature = "pybindings")]
214  fn get_dataframe(&self) -> PolarsResult<DataFrame> {
215    let mut series = Vec::<Column>::new();
216    for k in Self::keys() {
217      match self.get_series(k) {
218        None => {
219          error!("Unable to get series for {}", k);
220        }
221        Some(ser) => {
222          //println!("{}", ser);
223          series.push(ser.into());
224        }
225      }
226    }
227    //if self.get_timestamps().len() > 0 {
228    //  let timestamps  = Series::new("timestamps".into(), self.get_timestamps());
229    //  series.push(timestamps.into());
230    //}
231    // each column is now the specific variable but in terms for 
232    // all rbs
233    let df = DataFrame::new(series)?;
234    Ok(df)
235  }
236
237  #[cfg(feature = "pybindings")]
238  /// Get the variable for all boards. This keeps the order of the 
239  /// underlying VecDeque. Values of all boards intermixed.
240  /// To get a more useful version, use the Dataframe instead.
241  ///
242  /// # Arguments
243  ///
244  /// * varname : The name of the attribute of the underlying
245  ///             moni structure
246  fn get_series(&self, varname : &str) -> Option<Series> {
247    let mut data = Vec::<f32>::with_capacity(self.get_data().len());
248    let sorted_keys: Vec<u8> = self.get_data().keys().cloned().collect();
249    for board_id in sorted_keys.iter() {
250      let dqe = self.get_data().get(board_id).unwrap(); //uwrap is fine, bc we checked earlier
251      for moni in dqe {
252        match moni.get(varname) {
253          None => {
254            error!("This type of MoniData does not have a key called {}", varname);
255            return None;
256          }
257          Some(var) => {
258            data.push(var);
259          }
260        }
261      }
262    }
263    let series = Series::new(varname.into(), data);
264    Some(series)
265  }
266
267  /// A list of the variables in this MoniSeries
268  fn keys() -> Vec<&'static str> {
269    T::keys()
270  }
271
272  /// A list of boards in this series
273  fn get_board_ids(&self) -> Vec<u8> {
274    self.get_data().keys().cloned().collect()
275  }
276
277  /// Add another instance of the data container to the series
278  fn add(&mut self, data : T) {
279    let board_id = data.get_board_id();
280    if !self.get_data().contains_key(&board_id) {
281      self.get_data_mut().insert(board_id, VecDeque::<T>::new());
282    } 
283    self.get_data_mut().get_mut(&board_id).unwrap().push_back(data);
284    if self.get_data_mut().get_mut(&board_id).unwrap().len() > self.get_max_size() {
285      error!("The queue is too large, returning the first element! If you need a larger series size, set the max_size field");
286      self.get_data_mut().get_mut(&board_id).unwrap().pop_front();
287    }
288  }
289 
290  fn get_last_moni(&self, board_id : u8) -> Option<T> {
291    let size = self.get_data().get(&board_id)?.len();
292    Some(self.get_data().get(&board_id).unwrap()[size - 1])
293  }
294}
295
296//--------------------------------------------------
297
298#[macro_export]
299macro_rules! moniseries_general {
300  ($name : ident, $class:ty) => {
301     use std::collections::VecDeque;
302     use std::collections::HashMap;
303
304     use crate::monitoring::MoniSeries;
305
306     #[cfg_attr(feature="pybindings",pyclass)]
307     #[derive(Debug, Clone, PartialEq)]
308     pub struct $name {
309       data        : HashMap<u8, VecDeque<$class>>,
310       max_size    : usize,
311       timestamps  : Vec<u64>,
312     }
313     
314     impl $name {
315       pub fn new() -> Self {
316        Self {
317           data       : HashMap::<u8, VecDeque<$class>>::new(),
318           max_size   : 10000,
319           timestamps : Vec::<u64>::new()
320         }
321       }
322     } 
323     
324     impl Default for $name {
325       fn default() -> Self {
326         Self::new()
327       }
328     }
329     
330     impl fmt::Display for $name {
331       fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
332         write!(f, "<{} : {} boards>", stringify!($name), self.data.len())
333       }
334     }
335     
336     impl MoniSeries<$class> for $name {
337   
338       fn get_first_ts(&self) -> u64 {
339         if self.timestamps.len() == 0 {
340           return 0;
341         } else {
342           self.timestamps[0]
343         }
344       }
345
346       fn get_data(&self) -> &HashMap<u8,VecDeque<$class>> {
347         return &self.data;
348       }
349     
350       fn get_data_mut(&mut self) -> &mut HashMap<u8,VecDeque<$class>> {
351         return &mut self.data;
352       }
353      
354       fn get_max_size(&self) -> usize {
355         return self.max_size;
356       }
357       
358       fn set_max_size(&mut self, size : usize) {
359         self.max_size = size;
360       }
361  
362       fn add_timestamp(&mut self, ts : u64) {
363         self.timestamps.push(ts);
364       }
365
366       fn get_timestamps(&self) -> &Vec<u64> {
367         //if self.timestamps.len() == 0 {
368         //  let mut timestamps = Vec::<u64>::new();
369         //  for k in self.
370         //} 
371         return &self.timestamps;
372       }
373     }
374  
375     #[cfg(feature="pybindings")]
376     #[pymethods]
377     impl $name {
378       #[new]
379       fn new_py() -> Self {
380         Self::new() 
381       }
382  
383       #[pyo3(name="keys")]
384       #[staticmethod]
385       fn keys_py() -> Vec<&'static str> {
386         Self::keys()
387       }
388
389       /// The maximum size of the series. If more data 
390       /// are added, data from the front will be removed 
391       #[getter]
392       #[pyo3(name="max_size")]
393       fn get_max_size_py(&self) -> usize {
394         self.get_max_size()
395       }
396
397       #[setter]
398       #[pyo3(name="max_size")] 
399       fn set_max_size_py(&mut self, size : usize) {
400         self.set_max_size(size);
401       }
402       
403       #[getter]
404       #[pyo3(name="first_ts")]
405       pub fn get_first_ts_py(&self) -> u64 {
406         self.get_first_ts()
407       }
408
409       /// If monitoring is retrieved from telemetry, we 
410       /// save the gcu timestamp of the packet, wich 
411       /// herein can be accessed.
412       #[getter] 
413       #[pyo3(name="timestamps")] 
414       fn get_timestamps_py(&self) -> Vec<u64> {
415         warn!("This returns a full copy and is a performance bottleneck!");
416         return self.timestamps.clone();
417       }
418
419       /// Manually add a timestamp. There is a caveat to the 
420       /// timestamps. In general, f32 is not large enough to 
421       /// hold a 48bit timestamp, as the gcutime. 
422       ///
423       /// This needs some overhaul, this will be done in v0.13. 
424       /// For now, we can keep track manually
425       #[pyo3(name="add_timestamp")]
426       fn add_timestamp_py(&mut self, ts : u64) {
427         self.timestamps.push(ts);
428       }
429
430       /// Add a new daughter item to the series
431       #[pyo3(name="add")]
432       fn add_py(&mut self, moni : $class) {
433         self.add(moni);
434       }
435
436       #[getter]
437       #[pyo3(name="boards")]
438       fn get_boards(&self) -> Vec<u16> {
439         // use u16 here, so it apperas as numbers
440         // in python (u8 will be bytes)
441         let mut boards = Vec::<u16>::new();
442         for k in self.data.keys() {
443           boards.push(*k as u16);
444         }
445         boards
446       }
447       
448       /// The sizes of the underlying series for each board 
449       ///
450       /// # Returns:
451       ///   * HashMap<board id, size>  
452       #[getter]
453       #[pyo3(name="sizes")]
454       fn get_sizes(&self) -> HashMap<u16,usize> {
455         // use u16 here, so it apperas as numbers
456         // in python (u8 will be bytes)
457         let mut boards = HashMap::<u16,usize>::new();
458         for k in self.data.keys() {
459           boards.insert(*k as u16, self.data[k].len());
460         }
461         boards
462       }
463
464       #[pyo3(name="get_var_for_board")]
465       fn get_var_for_board_py(&self, varname : &str, rb_id : u8) -> Option<Vec<f32>> {
466         self.get_var_for_board(varname, &rb_id)
467       }
468
469       #[pyo3(name="get_monidata_for_board")]
470       fn get_monidata_for_board(&self, board_id : u8, idx : usize) -> Option<$class> {
471         if !self.data.contains_key(&board_id) {
472           return None;
473         }
474         if self.data.get(&board_id).unwrap().len() < idx + 1 {
475           return None;
476         } else { 
477           return Some(self.data.get(&board_id).unwrap()[idx].clone());
478         }
479       }
480
481       /// Reduces the MoniSeries to a single polars data frame
482       /// The structure itself will not be changed
483       #[pyo3(name="get_dataframe")]
484       fn get_dataframe_py(&self) -> PyResult<PyDataFrame> {
485         match self.get_dataframe() {
486           Ok(df) => {
487             let pydf = PyDataFrame(df);
488             return Ok(pydf);
489           },
490           Err(err) => {
491             return Err(PyValueError::new_err(err.to_string()));
492           }
493         }
494       } 
495     }
496     #[cfg(feature="pybindings")]
497     pythonize_display!($name);
498  }
499}
500
501/// Implements the moniseries trait for a MoniData 
502/// type of class
503// this should basically be moniseries_tof
504#[macro_export]
505macro_rules! moniseries {
506  ($name : ident, $class:ty) => {
507    
508    moniseries_general!($name, $class);
509
510    #[cfg(feature="pybindings")]
511    #[pymethods]
512    impl $name {
513      
514      //fn __len__(&self) -> usize {
515      //  self.items.len()
516      //}
517      
518      /// Add an additional (Caraspace) file to the series 
519      ///
520      /// # Arguments:
521      ///   * filename    : The name of the (caraspace) file to add
522      ///   * from_object : Since this adds caraspace files, it is possible 
523      ///                   to choose from where to get the information.
524      ///                   Either the telemetry packet, or the tofpacket, if 
525      ///                   either is present in the frame. When 
526      ///                   CRFrameObjectType = Unknown, it will figure it out 
527      ///                   automatically, preferring the telemetry since it has
528      ///                   the gcu timestamp
529      #[pyo3(signature = (filename, from_object = CRFrameObjectType::TelemetryPacket))]
530      fn add_crfile(&mut self, filename : String, from_object : CRFrameObjectType) {
531        let reader = CRReader::new(filename).expect("Unable to open file!");
532        // now we have a problem - from which frame should we get it?
533        // if we get it from the dedicated TOF stream it will be much 
534        // faster (if that is available) since it will be it's own 
535        // presence in the frame
536        //let address = &source.clone();
537        //let mut try_from_telly = false;
538        let tp_source     = String::from("TofPacketType.") + stringify!($class);
539        let tp_source_alt = String::from("PacketType.") + stringify!($class);
540        let tel_source    = "TelemetryPacketType.AnyTofHK";
541        for frame in reader {
542          match from_object { 
543            CRFrameObjectType::TofPacket =>  {
544              if frame.has(&tp_source) || frame.has(&tp_source_alt) {
545                if frame.has(&tp_source) {
546                  let moni_res = frame.get::<TofPacket>(&tp_source).unwrap().unpack::<$class>();
547                  match moni_res {
548                    Err(err) => {
549                      println!("Error unpacking! {err}");
550                    }
551                    Ok(moni) => {
552                      self.add(moni);
553                    }
554                  }
555                }
556                if frame.has(&tp_source_alt) {
557                  let moni_res = frame.get::<TofPacket>(&tp_source_alt).unwrap().unpack::<$class>();
558                  match moni_res { 
559                    Err(err) => {
560                      println!("Error unpacking! {err}");
561                    }
562                    Ok(moni) => {
563                      self.add(moni);
564                    }
565                  }
566                }
567              } 
568            }
569            CRFrameObjectType::TelemetryPacket | CRFrameObjectType::Unknown => {
570              if frame.has(tel_source) {
571                let hk_res = frame.get::<TelemetryPacket>(tel_source);
572                match hk_res {
573                  Err(err) => {
574                    println!("Error unpacking! {err}");
575                  }
576                  Ok(hk) => {
577                    let mut pos = 0;
578                    // subtract the 2020/1/1 midnight from the gcutime to make 
579                    // it f32
580                    let gcutime = hk.header.get_gcutime() as u64;
581                    match TofPacket::from_bytestream(&hk.payload, &mut pos) {
582                      Err(err) => {
583                        println!("Error unpackin! {err}");
584                      }
585                      Ok(tp) => {
586                        if tp.packet_type == <$class>::TOF_PACKET_TYPE  {
587                          match tp.unpack::<$class>() {
588                            Err(err) => {
589                              println!("Error unpacking! {err}");
590                            }
591                            Ok(mut moni) => {
592                              self.add_timestamp(gcutime);
593                              moni.set_timestamp(gcutime - self.get_first_ts()); 
594                              self.add(moni);
595                              //self.timestamps.push(gcutime);
596                            }
597                          }
598                        }
599                      }
600                    } 
601                  }
602                }
603              }
604            }
605            _ => () // do nothing for other types
606          }
607        }
608      }
609      
610      /// Add an additional (Telemetry) file to the series 
611      ///
612      /// # Arguments:
613      ///   * filename    : The name of the (telemetry) file to add
614      fn add_telemetryfile(&mut self, filename : String) {
615        let reader = TelemetryPacketReader::new(filename, true, None, None, 0, 0);
616        for pack in reader {
617          if pack.header.packet_type == TelemetryPacketType::AnyTofHK {
618            let mut pos = 0;
619            // subtract the 2020/1/1 midnight from the gcutime to make 
620            // it f32
621            let gcutime = pack.header.get_gcutime() as u64;
622            match TofPacket::from_bytestream(&pack.payload, &mut pos) {
623              Err(err) => {
624                println!("Error unpackin! {err}");
625              }
626              Ok(tp) => {
627                if tp.packet_type == <$class>::TOF_PACKET_TYPE  {
628                  match tp.unpack::<$class>() {
629                    Err(err) => {
630                      println!("Error unpacking! {err}");
631                    }
632                    Ok(mut moni) => {
633                      self.add_timestamp(gcutime);
634                      moni.set_timestamp(gcutime - self.get_first_ts()); 
635                      self.add(moni);
636                      //self.timestamps.push(gcutime);
637                    }
638                  }
639                }
640              }
641            } 
642          }
643        }
644      }
645      
646      /// Add an additional (Tof) file to the series 
647      ///
648      /// # Arguments:
649      ///   * filename    : The name of the (caraspace) file to add
650      ///   * from_object : Since this adds caraspace files, it is possible 
651      ///                   to choose from where to get the information.
652      ///                   Either the telemetry packet, or the tofpacket, if 
653      ///                   either is present in the frame. When 
654      ///                   CRFrameObjectType = Unknown, it will figure it out 
655      ///                   automatically, preferring the telemetry since it has
656      ///                   the gcu timestamp
657      #[pyo3(signature = (filename))]
658      fn add_toffile(&mut self, filename : String) {
659        let reader = TofPacketReader::new(&filename);
660        for tp in reader { 
661          if tp.packet_type == <$class>::TOF_PACKET_TYPE  {
662            match tp.unpack::<$class>() {
663              Err(err) => {
664                println!("Error unpacking! {err}");
665              }
666              Ok(mut moni) => {
667                self.add_timestamp(moni.get_timestamp());
668                moni.set_timestamp(moni.get_timestamp()); 
669                self.add(moni);
670                //self.timestamps.push(gcutime);
671              }
672            }
673          }
674        }
675      }
676
677      /// Generate a polars dataframe with monitoring data from the 
678      /// given TOF file.
679      /// This will load ONLY data of the specific type of the 
680      /// MoniSeries itself
681      ///
682      /// # Arguments:
683      ///   * filename : A single .tof.gaps file with monitoring 
684      ///                information 
685      #[staticmethod]
686      fn from_tof_file(filename : String) -> PyResult<PyDataFrame> {
687        let mut reader = TofPacketReader::new(&filename);
688        let mut series = Self::new();
689        // it would be nice to set the filter here, but I 
690        // don't know how that can be done in the macro
691        reader.filter  = <$class>::TOF_PACKET_TYPE;
692        for tp in reader {
693          if let Ok(moni) =  tp.unpack::<$class>() {
694            series.add(moni);
695          }
696          // other packets will get thrown away 
697        }
698        match series.get_dataframe() {
699          Ok(df) => {
700            let pydf = PyDataFrame(df);
701            return Ok(pydf);
702          },
703          Err(err) => {
704            return Err(PyValueError::new_err(err.to_string()));
705          }
706        }
707      }  
708    }
709  }
710}
711
712// ------------------------------------------------
713
714// baiscally the same macro, but for data coming 
715// from telemetry 
716
717/// Implements the moniseries trait for a MoniData 
718/// type of class
719#[macro_export]
720macro_rules! moniseries_telemetry {
721  ($name : ident, $class:ty) => {
722    
723    moniseries_general!($name, $class);
724
725    #[cfg(feature="pybindings")]
726    #[pymethods]
727    impl $name {
728
729      /// Add an additional (Caraspace) file to the series 
730      ///
731      /// # Arguments:
732      ///   * filename    : The name of the (caraspace) file to add
733      ///   * from_object : Since this adds caraspace files, it is possible 
734      ///                   to choose from where to get the information.
735      ///                   Either the telemetry packet, or the tofpacket, if 
736      ///                   either is present in the frame. When 
737      ///                   CRFrameObjectType = Unknown, it will figure it out 
738      ///                   automatically, preferring the telemetry since it has
739      ///                   the gcu timestamp
740      #[pyo3(signature = (filename, from_object = CRFrameObjectType::TelemetryPacket))]
741      fn add_crfile(&mut self, filename : String, from_object : CRFrameObjectType) {
742        let reader = CRReader::new(filename).expect("Unable to open file!");
743        // now we have a problem - from which frame should we get it?
744        // if we get it from the dedicated TOF stream it will be much 
745        // faster (if that is available) since it will be it's own 
746        // presence in the frame
747        //let address = &source.clone();
748        //let mut try_from_telly = false;
749        let tel_source    = "TelemetryPacketType.AnyTofHK";
750        for frame in reader {
751          match from_object { 
752            CRFrameObjectType::TelemetryPacket | CRFrameObjectType::Unknown => {
753              if frame.has(tel_source) {
754                let hk_res = frame.get::<TelemetryPacket>(tel_source);
755                match hk_res {
756                  Err(err) => {
757                    println!("Error unpacking! {err}");
758                  }
759                  Ok(hk) => {
760                    if hk.header.packet_type != <$class>::TEL_PACKET_TYPE  {
761                      // this is not the packet you are looking for 
762                      continue;
763                    }   
764                    let mut pos = 0;
765                    // subtract the 2020/1/1 midnight from the gcutime to make 
766                    // it f32
767                    let gcutime = hk.header.get_gcutime() as u64;
768                    match <$class>::from_bytestream(&hk.payload, &mut pos) {
769                      Err(err) => {
770                        println!("Error unpackin! {err}");
771                      }
772                      Ok(mut moni) => {
773                        self.add_timestamp(gcutime);
774                        moni.set_timestamp(gcutime - self.get_first_ts()); 
775                        self.add(moni);
776                        //self.timestamps.push(gcutime);
777                      }
778                    } 
779                  }
780                }
781              }
782            }
783            _ => ()
784          }
785        }
786      }
787      
788      /// Add an additional (Telemetry) file to the series 
789      ///
790      /// # Arguments:
791      ///   * filename    : The name of the (telemetry) file to add
792      fn add_telemetryfile(&mut self, filename : String) {
793        let reader = TelemetryPacketReader::new(filename, true, None, None, 0, 0);
794        for pack in reader {
795          if pack.header.packet_type == <$class>::TEL_PACKET_TYPE {
796            let mut pos = 0;
797            // subtract the 2020/1/1 midnight from the gcutime to make 
798            // it f32
799            let gcutime = pack.header.get_gcutime() as u64;
800            match <$class>::from_bytestream(&pack.payload, &mut pos) {
801              Err(err) => {
802                println!("Error unpackin! {err}");
803              }
804              Ok(mut moni) => {
805                self.add_timestamp(gcutime);
806                moni.set_timestamp(gcutime - self.get_first_ts()); 
807                self.add(moni);
808              }
809            }
810          }
811        }
812      }
813    }
814  }
815}
816
817