API
shmimIntegrator.hpp
Go to the documentation of this file.
1 /** \file shmimIntegrator.hpp
2  * \brief The MagAO-X generic ImageStreamIO stream integrator
3  *
4  * \ingroup app_files
5  */
6 
7 #ifndef shmimIntegrator_hpp
8 #define shmimIntegrator_hpp
9 
10 #include <limits>
11 
12 #include <mx/improc/eigenCube.hpp>
13 #include <mx/improc/eigenImage.hpp>
14 
15 #include "../../libMagAOX/libMagAOX.hpp" //Note this is included on command line to trigger pch
16 #include "../../magaox_git_version.h"
17 
18 namespace MagAOX
19 {
20 namespace app
21 {
22 
23 struct darkShmimT
24 {
25  static std::string configSection()
26  {
27  return "darkShmim";
28  };
29 
30  static std::string indiPrefix()
31  {
32  return "dark";
33  };
34 };
35 
36 struct dark2ShmimT
37 {
38  static std::string configSection()
39  {
40  return "dark2Shmim";
41  };
42 
43  static std::string indiPrefix()
44  {
45  return "dark2";
46  };
47 };
48 
49 /** \defgroup shmimIntegrator ImageStreamIO Stream Integrator
50  * \brief Integrates (i.e. averages) an ImageStreamIO image stream.
51  *
52  * <a href="../handbook/operating/software/apps/shmimIntegrator.html">Application Documentation</a>
53  *
54  * \ingroup apps
55  *
56  */
57 
58 /** \defgroup shmimIntegrator_files ImageStreamIO Stream Integrator
59  * \ingroup shmimIntegrator
60  */
61 
62 /** MagAO-X application to control integrating (averaging) an ImageStreamIO stream
63  *
64  * \ingroup shmimIntegrator
65  *
66  */
67 class shmimIntegrator : public MagAOXApp<true>, public dev::shmimMonitor<shmimIntegrator>, public dev::shmimMonitor<shmimIntegrator,darkShmimT>,
68  public dev::shmimMonitor<shmimIntegrator,dark2ShmimT>, public dev::frameGrabber<shmimIntegrator>, public dev::telemeter<shmimIntegrator>
69 {
70 
71  //Give the test harness access.
72  friend class shmimIntegrator_test;
73 
74  friend class dev::shmimMonitor<shmimIntegrator>;
77  friend class dev::frameGrabber<shmimIntegrator>;
78  friend class dev::telemeter<shmimIntegrator>;
79 
80  //The base shmimMonitor type
82 
83  //The dark shmimMonitor type
85 
86  //The dark shmimMonitor type for a 2nd dark
88 
89  //The base frameGrabber type
91 
93 
94  ///Floating point type in which to do all calculations.
95  typedef float realT;
96 
97 public:
98  /** \name app::dev Configurations
99  *@{
100  */
101 
102  static constexpr bool c_frameGrabber_flippable = false; ///< app:dev config to tell framegrabber these images can not be flipped
103 
104  ///@}
105 
106 protected:
107 
108  /** \name Configurable Parameters
109  *@{
110  */
111 
112  unsigned m_nAverageDefault {10}; ///< The number of frames to average. Default 10.
113 
114  std::string m_fpsSource; ///< Device name for getting fps if time-based averaging is used. This device should have *.fps.current.
115 
116  float m_avgTime {0}; ///< If non zero, then m_nAverage adjusts automatically to keep a constant averaging time [sec]. Default 0.
117 
118  unsigned m_nUpdate {0}; ///< The rate at which to update the average. If 0 < m_nUpdate < m_nAverage then this is a moving averager. Default 0.
119 
120  bool m_continuous {true}; ///< Set to false in configuration to have this run once then stop until triggered.
121 
122  bool m_running {true}; ///< Set to false in configuration to have it not start averaging until triggered.
123 
124  std::string m_stateSource; ///< The source of the state string used for file management
125 
126  bool m_fileSaver {false}; ///< Set to true in configuration to have this save and reload files automatically.
127 
128  ///@}
129 
130  mx::improc::eigenCube<realT> m_accumImages; ///< Cube used to accumulate images
131 
132  mx::improc::eigenImage<realT> m_avgImage; ///< The average image.
133 
134  unsigned m_nAverage {10};
135 
136  float m_fps {0}; ///< Current FPS from the FPS source.
137 
138  unsigned m_nprocessed {0};
139  size_t m_currImage {0};
140  size_t m_sinceUpdate {0};
141  bool m_updated {false};
142 
143  bool m_imageValid {false};
144  std::string m_stateString;
145  bool m_stateStringValid {false};
147  bool m_stateStringChanged {false};
148  std::string m_fileSaveDir;
149 
150 
151  sem_t m_smSemaphore {0}; ///< Semaphore used to synchronize the fg thread and the sm thread.
152 
153  realT (*pixget)(void *, size_t) {nullptr}; ///< Pointer to a function to extract the image data as our desired type realT.
154 
155  ///Mutex for locking dark operations.
156  std::mutex m_darkMutex;
157 
158  mx::improc::eigenImage<realT> m_darkImage;
159  bool m_darkSet {false};
160  bool m_darkValid {false};
161  realT (*dark_pixget)(void *, size_t) {nullptr}; ///< Pointer to a function to extract the image data as our desired type realT.
162 
163  mx::improc::eigenImage<realT> m_dark2Image;
164  bool m_dark2Set {false};
165  bool m_dark2Valid {false};
166  realT (*dark2_pixget)(void *, size_t) {nullptr}; ///< Pointer to a function to extract the image data as our desired type realT.
167 
168 
169 public:
170  /// Default c'tor.
171  shmimIntegrator();
172 
173  /// D'tor, declared and defined for noexcept.
174  ~shmimIntegrator() noexcept
175  {}
176 
177  virtual void setupConfig();
178 
179  /// Implementation of loadConfig logic, separated for testing.
180  /** This is called by loadConfig().
181  */
182  int loadConfigImpl( mx::app::appConfigurator & _config /**< [in] an application configuration from which to load values*/);
183 
184  virtual void loadConfig();
185 
186  /// Startup function
187  /**
188  *
189  */
190  virtual int appStartup();
191 
192  /// Implementation of the FSM for shmimIntegrator.
193  /**
194  * \returns 0 on no critical error
195  * \returns -1 on an error requiring shutdown
196  */
197  virtual int appLogic();
198 
199  /// Shutdown the app.
200  /**
201  *
202  */
203  virtual int appShutdown();
204 
205 protected:
206 
207  int allocate( const dev::shmimT & dummy /**< [in] tag to differentiate shmimMonitor parents.*/);
208 
209  int processImage( void * curr_src, ///< [in] pointer to start of current frame.
210  const dev::shmimT & dummy ///< [in] tag to differentiate shmimMonitor parents.
211  );
212 
213  int allocate( const darkShmimT & dummy /**< [in] tag to differentiate shmimMonitor parents.*/);
214 
215  int processImage( void * curr_src, ///< [in] pointer to start of current frame.
216  const darkShmimT & dummy ///< [in] tag to differentiate shmimMonitor parents.
217  );
218 
219  int allocate( const dark2ShmimT & dummy /**< [in] tag to differentiate shmimMonitor parents.*/);
220 
221  int processImage( void * curr_src, ///< [in] pointer to start of current frame.
222  const dark2ShmimT & dummy ///< [in] tag to differentiate shmimMonitor parents.
223  );
224 
225  int findMatchingDark();
226 
227 
228  /** \name dev::frameGrabber interface
229  *
230  * @{
231  */
232 
233  /// Implementation of the framegrabber configureAcquisition interface
234  /**
235  * \returns 0 on success
236  * \returns -1 on error
237  */
238  int configureAcquisition();
239 
240  /// Implementation of the framegrabber fps interface
241  /**
242  * \todo this needs to infer the stream fps and return it
243  */
244  float fps()
245  {
246  if(m_fps > 0 && m_nAverage > 0 && (m_running || m_continuous))
247  {
248  return m_fps/m_nAverage;
249  }
250  else
251  { //this will cause a default averaging
252 
253  return 1.0;
254  }
255  }
256 
257  /// Implementation of the framegrabber startAcquisition interface
258  /**
259  * \returns 0 on success
260  * \returns -1 on error
261  */
262  int startAcquisition();
263 
264  /// Implementation of the framegrabber acquireAndCheckValid interface
265  /**
266  * \returns 0 on success
267  * \returns -1 on error
268  */
269  int acquireAndCheckValid();
270 
271  /// Implementation of the framegrabber loadImageIntoStream interface
272  /**
273  * \returns 0 on success
274  * \returns -1 on error
275  */
276  int loadImageIntoStream( void * dest /**< [in] */);
277 
278  /// Implementation of the framegrabber reconfig interface
279  /**
280  * \returns 0 on success
281  * \returns -1 on error
282  */
283  int reconfig();
284 
285  ///@}
286 
287  pcf::IndiProperty m_indiP_nAverage;
288 
289  pcf::IndiProperty m_indiP_avgTime;
290 
291  pcf::IndiProperty m_indiP_nUpdate;
292 
293  pcf::IndiProperty m_indiP_startAveraging;
294 
299 
300  pcf::IndiProperty m_indiP_fpsSource;
302 
303  pcf::IndiProperty m_indiP_fps; ///< this integrator's FPS
304 
305  pcf::IndiProperty m_indiP_stateSource;
307 
308  pcf::IndiProperty m_indiP_imageValid;
309 
310  /** \name Telemeter Interface
311  *
312  * @{
313  */
314  int checkRecordTimes();
315 
316  int recordTelem( const telem_fgtimings * );
317 
318  ///@}
319 
320 };
321 
322 inline
323 shmimIntegrator::shmimIntegrator() : MagAOXApp(MAGAOX_CURRENT_SHA1, MAGAOX_REPO_MODIFIED)
324 {
326  return;
327 }
328 
329 inline
331 {
335 
337  telemeterT::setupConfig(config);
338 
339  config.add("integrator.nAverage", "", "integrator.nAverage", argType::Required, "integrator", "nAverage", false, "unsigned", "The default number of frames to average. Default 10. Can be changed via INDI.");
340  config.add("integrator.fpsSource", "", "integrator.fpsSource", argType::Required, "integrator", "fpsSource", false, "string", "Device name for getting fps if time-based averaging is used. This device should have *.fps.current.");
341 
342  config.add("integrator.avgTime", "", "integrator.avgTime", argType::Required, "integrator", "avgTime", false, "float", "If non zero, then m_nAverage adjusts automatically to keep a constant averaging time [sec]. Default 0. Can be changed via INDI.");
343 
344  config.add("integrator.nUpdate", "", "integrator.nUpdate", argType::Required, "integrator", "nUpdate", false, "unsigned", "The rate at which to update the average. If 0 < m_nUpdate < m_nAverage then this is a moving averager. Default 0. If 0, then it is a simple average.");
345 
346  config.add("integrator.continuous", "", "integrator.continuous", argType::Required, "integrator", "continuous", false, "bool", "Flag controlling whether averaging is continuous or only when triggered. Default true.");
347  config.add("integrator.running", "", "integrator.running", argType::Required, "integrator", "running", false, "bool", "Flag controlling whether averaging is running at startup. Default true.");
348 
349  config.add("integrator.stateSource", "", "integrator.stateSource", argType::Required, "integrator", "stateSource", false, "string", "///< Device name for getting the state string for file management. This device should have *.state_string.current.");
350  config.add("integrator.fileSaver", "", "integrator.fileSaver", argType::Required, "integrator", "fileSaver", false, "bool", "Flag controlling whether this saves and reloads files automatically. Default false.");
351 
352 
353 }
354 
355 inline
356 int shmimIntegrator::loadConfigImpl( mx::app::appConfigurator & _config )
357 {
358 
360  darkMonitorT::loadConfig(config);
362 
364  telemeterT::loadConfig(config);
365 
366  _config(m_nAverageDefault, "integrator.nAverage");
368  _config(m_fpsSource, "integrator.fpsSource");
369  _config(m_avgTime, "integrator.avgTime");
370  _config(m_nUpdate, "integrator.nUpdate");
371 
372  _config(m_continuous, "integrator.continuous");
373 
374  _config(m_running, "integrator.running");
375 
376  _config(m_stateSource, "integrator.stateSource");
377  _config(m_fileSaver, "integrator.fileSaver");
378 
379  return 0;
380 }
381 
382 inline
384 {
385  loadConfigImpl(config);
386 }
387 
388 inline
390 {
391 
392  createStandardIndiNumber<unsigned>( m_indiP_nAverage, "nAverage", 1, std::numeric_limits<unsigned>::max(), 1, "%u");
393  m_indiP_nAverage["current"].set(m_nAverage);
394  m_indiP_nAverage["target"].set(m_nAverage);
395 
397  {
398  log<software_error>({__FILE__,__LINE__});
399  return -1;
400  }
401 
402  createStandardIndiNumber<float>( m_indiP_avgTime, "avgTime", 0, std::numeric_limits<float>::max(),0 , "%0.1f");
403  m_indiP_avgTime["current"].set(m_avgTime);
404  m_indiP_avgTime["target"].set(m_avgTime);
405 
407  {
408  log<software_error>({__FILE__,__LINE__});
409  return -1;
410  }
411 
412  createStandardIndiNumber<unsigned>( m_indiP_nUpdate, "nUpdate", 1, std::numeric_limits<unsigned>::max(), 1, "%u");
413  m_indiP_nUpdate["current"].set(m_nUpdate);
414  m_indiP_nUpdate["target"].set(m_nUpdate);
415 
417  {
418  log<software_error>({__FILE__,__LINE__});
419  return -1;
420  }
421 
424  {
425  log<software_error>({__FILE__,__LINE__});
426  return -1;
427  }
428 
429  if(m_fpsSource != "")
430  {
431  REG_INDI_SETPROP(m_indiP_fpsSource, m_fpsSource, std::string("fps"));
432  }
433 
434  CREATE_REG_INDI_RO_NUMBER(m_indiP_fps, "fps", "", "");
435  m_indiP_fps.add(pcf::IndiElement("current"));
436  m_indiP_fps["current"].set(0);
437 
438  if(m_fileSaver == true && m_stateSource != "")
439  {
440  REG_INDI_SETPROP(m_indiP_stateSource, m_stateSource, std::string("state_string"));
441 
442  createROIndiText( m_indiP_imageValid, "image_valid", "flag", "Image Valid", "Image", "Valid");
443  if(!m_imageValid) //making sure we stay up with default
444  {
445  m_indiP_imageValid["flag"] = "no";
446  }
447  else
448  {
449  m_indiP_imageValid["flag"] = "yes";
450  }
451 
453  {
454  log<software_error>({__FILE__,__LINE__});
455  return -1;
456  }
457 
458 
460 
461  //Create save directory.
462  errno = 0;
463  if( mkdir(m_fileSaveDir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0 )
464  {
465  if( errno != EEXIST)
466  {
467  std::stringstream logss;
468  logss << "Failed to create image directory (" << m_fileSaveDir << "). Errno says: " << strerror(errno);
469  log<software_critical>({__FILE__, __LINE__, errno, 0, logss.str()});
470 
471  return -1;
472  }
473  }
474  }
475 
476 
477  if(sem_init(&m_smSemaphore, 0,0) < 0)
478  {
479  log<software_critical>({__FILE__, __LINE__, errno,0, "Initializing S.M. semaphore"});
480  return -1;
481  }
482 
483  if(shmimMonitorT::appStartup() < 0)
484  {
485  return log<software_error,-1>({__FILE__, __LINE__});
486  }
487 
488  if(darkMonitorT::appStartup() < 0)
489  {
490  return log<software_error,-1>({__FILE__, __LINE__});
491  }
492 
493  if(dark2MonitorT::appStartup() < 0)
494  {
495  return log<software_error,-1>({__FILE__, __LINE__});
496  }
497 
498  if(frameGrabberT::appStartup() < 0)
499  {
500  return log<software_error,-1>({__FILE__, __LINE__});
501  }
502 
503  if(telemeterT::appStartup() < 0)
504  {
505  return log<software_error,-1>({__FILE__, __LINE__});
506  }
507 
509 
510  return 0;
511 }
512 
513 inline
515 {
516  if( shmimMonitorT::appLogic() < 0)
517  {
518  return log<software_error,-1>({__FILE__,__LINE__});
519  }
520 
521  if( darkMonitorT::appLogic() < 0)
522  {
523  return log<software_error,-1>({__FILE__,__LINE__});
524  }
525 
526  if( dark2MonitorT::appLogic() < 0)
527  {
528  return log<software_error,-1>({__FILE__,__LINE__});
529  }
530 
531  if( frameGrabberT::appLogic() < 0)
532  {
533  return log<software_error,-1>({__FILE__,__LINE__});
534  }
535 
536  if( telemeterT::appLogic() < 0)
537  {
538  return log<software_error,-1>({__FILE__,__LINE__});
539  }
540 
541  std::unique_lock<std::mutex> lock(m_indiMutex);
542 
543  if(shmimMonitorT::updateINDI() < 0)
544  {
545  log<software_error>({__FILE__, __LINE__});
546  }
547 
548  if(darkMonitorT::updateINDI() < 0)
549  {
550  log<software_error>({__FILE__, __LINE__});
551  }
552 
553  if(dark2MonitorT::updateINDI() < 0)
554  {
555  log<software_error>({__FILE__, __LINE__});
556  }
557 
558  if(frameGrabberT::updateINDI() < 0)
559  {
560  log<software_error>({__FILE__, __LINE__});
561  }
562 
563  if(m_running == false)
564  {
566  updateSwitchIfChanged(m_indiP_startAveraging, "toggle", pcf::IndiElement::Off, INDI_IDLE);
567 
568  if(m_fileSaver)
569  {
570  if(m_stateStringChanged) //So if not running and the state has changed, we check
571  {
572  if(findMatchingDark() < 0)
573  {
574  log<software_error>({__FILE__, __LINE__});
575  }
576  }
577 
578  if(!m_imageValid)
579  {
580  updateIfChanged(m_indiP_imageValid, "flag", "no");
581  }
582  else
583  {
584  updateIfChanged(m_indiP_imageValid, "flag", "yes");
585  }
586  }
587  }
588  else
589  {
591  updateSwitchIfChanged(m_indiP_startAveraging, "toggle", pcf::IndiElement::On, INDI_BUSY);
592  }
593 
596 
599 
602 
603  updateIfChanged(m_indiP_fps, "current", fps(), INDI_OK);
604 
605  return 0;
606 }
607 
608 inline
610 {
612 
614 
616 
618 
620 
621  return 0;
622 }
623 
624 inline
626 {
627  static_cast<void>(dummy); //be unused
628 
629  //This whole thing could invalidate the dark.
630  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
631 
632 
633  if(m_avgTime > 0 && m_fps > 0)
634  {
636  if(m_nAverage <= 0)
637  {
638  m_nAverage = 1;
639  }
640  log<text_log>("set nAverage to " + std::to_string(m_nAverage) + " based on FPS", logPrio::LOG_NOTICE);
641  }
642  else if(m_avgTime > 0 && m_fps == 0) //Haven't gotten the update yet so we keep going for now
643  {
645  {
647  log<text_log>("set nAverage to default " + std::to_string(m_nAverage), logPrio::LOG_NOTICE);
648  }
649  }
650 
651  if(m_nUpdate > 0)
652  {
654  m_accumImages.setZero();
655  }
656  else
657  {
658  m_accumImages.resize(1,1,1);
659  }
660 
661  m_nprocessed = 0;
662  m_currImage = 0;
663  m_sinceUpdate = 0;
664 
666  //m_avgImage.setZero();
667 
668  pixget = getPixPointer<realT>(shmimMonitorT::m_dataType);
669 
670  if(pixget == nullptr)
671  {
672  log<software_error>({__FILE__, __LINE__, "bad data type"});
673  return -1;
674  }
675 
678 
681 
684 
685 
687  {
688  m_darkValid = true;
689  }
690  else
691  {
692  m_darkValid = false;
693  }
694 
696  {
697  m_dark2Valid = true;
698  }
699  else
700  {
701  m_dark2Valid = false;
702  }
703 
704 
705  m_reconfig = true;
706 
707  return 0;
708 }
709 
710 inline
711 int shmimIntegrator::processImage( void * curr_src,
712  const dev::shmimT & dummy
713  )
714 {
715  static_cast<void>(dummy); //be unused
716 
717  if(!m_running) return 0;
718 
719  if(m_nUpdate == 0)
720  {
721  if(m_updated) return 0;
722  if(m_sinceUpdate == 0) m_avgImage.setZero();
723 
724  realT * data = m_avgImage.data();
725 
726  for(unsigned nn=0; nn < shmimMonitorT::m_width*shmimMonitorT::m_height; ++nn)
727  {
728  data[nn] += pixget(curr_src, nn);
729  }
730  ++m_sinceUpdate;
732  {
733  m_avgImage /= m_nAverage; ///\todo should this be /= m_sinceUpdate?
734 
735  if((m_darkSet && m_darkValid) && !(m_dark2Set && m_dark2Valid))
736  {
737  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
739  }
740  else if(!(m_darkSet && m_darkValid) && (m_dark2Set && m_dark2Valid))
741  {
742  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
744  }
745  else if((m_darkSet && m_darkValid) && (m_dark2Set && m_dark2Valid))
746  {
747  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
749  }
750 
751  m_updated = true;
752 
753  //Now tell the f.g. to get going
754  if(sem_post(&m_smSemaphore) < 0)
755  {
756  log<software_critical>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
757  return -1;
758  }
759 
760  m_sinceUpdate = 0;
761  if(!m_continuous)
762  {
763  m_running = false;
764  if(m_fileSaver)
765  {
767  {
768  m_imageValid = false;
769  log<text_log>("state changed during acquisition, not saving", logPrio::LOG_NOTICE);
770  }
771  else
772  {
773  m_imageValid = true;
774  m_stateStringChanged=false;
775 
776  ///\todo this should happen in a different less-real-time thread.
777  //Otherwise we save:
778  timespec fts;
779  clock_gettime(CLOCK_REALTIME, &fts);
780 
781  tm uttime;//The broken down time.
782 
783  if(gmtime_r(&fts.tv_sec, &uttime) == 0)
784  {
785  //Yell at operator but keep going
786  log<software_alert>({__FILE__,__LINE__,errno,0,"gmtime_r error. possible loss of timing information."});
787  }
788 
789  char cts[] = "YYYYMMDDHHMMSSNNNNNNNNN";
790  int rv = snprintf(cts, sizeof(cts), "%04i%02i%02i%02i%02i%02i%09i", uttime.tm_year+1900,
791  uttime.tm_mon+1, uttime.tm_mday, uttime.tm_hour, uttime.tm_min, uttime.tm_sec, static_cast<int>(fts.tv_nsec));
792 
793  if(rv != sizeof(cts)-1)
794  {
795  //Something is very wrong. Keep going to try to get it on disk.
796  log<software_alert>({__FILE__,__LINE__, errno, rv, "did not write enough chars to timestamp"});
797  }
798 
799  std::string fname = m_fileSaveDir + "/" + m_configName + "_" + m_stateString + "__T" + cts + ".fits";
800 
801  mx::fits::fitsFile<float> ff;
802  ff.write(fname, m_avgImage);
803  log<text_log>("Wrote " + fname);
804 
805  }
806  }
807  }
808  }
809  }
810  else
811  {
812  realT * data = m_accumImages.image(m_currImage).data();
813 
814  for(unsigned nn=0; nn < shmimMonitorT::m_width*shmimMonitorT::m_height; ++nn)
815  {
816  data[nn] = pixget(curr_src, nn);
817  }
818  ++m_nprocessed;
819  ++m_currImage;
821 
822  if(m_nprocessed < m_nAverage) //Check that we are burned in on first pass through cube
823  {
824  return 0;
825  }
826 
827  ++m_sinceUpdate;
828 
829  if(m_sinceUpdate >= m_nUpdate)
830  {
831  if(m_updated)
832  {
833  return 0; //In case f.g. thread is behind, we skip and come back.
834  }
835  //Don't use eigenCube functions to avoid any omp
836  m_avgImage.setZero();
837  for(size_t n =0; n < m_nAverage; ++n)
838  {
839  for(size_t ii=0; ii< shmimMonitorT::m_width; ++ii)
840  {
841  for(size_t jj=0; jj< shmimMonitorT::m_height; ++jj)
842  {
843  m_avgImage(ii,jj) += m_accumImages.image(n)(ii,jj);
844  }
845  }
846  }
848 
849  if(m_darkValid && m_darkSet)
850  {
851  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
853  }
854 
855  m_updated = true;
856 
857  //Now tell the f.g. to get going
858  if(sem_post(&m_smSemaphore) < 0)
859  {
860  log<software_critical>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
861  return -1;
862  }
863 
864  m_sinceUpdate = 0;
865  }
866  }
867  return 0;
868 }
869 
870 inline
872 {
873  static_cast<void>(dummy); //be unused
874 
875  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
876 
878  m_darkImage.setZero();
879 
880  dark_pixget = getPixPointer<realT>(darkMonitorT::m_dataType);
881 
882  if(dark_pixget == nullptr)
883  {
884  log<software_error>({__FILE__, __LINE__, "bad data type"});
885  m_darkSet = false;
886  m_darkValid = false;
887  return -1;
888  }
889 
890 
892  {
893  m_darkValid = true;
894  }
895  else
896  {
897  m_darkValid = false;
898  }
899 
900  return 0;
901 }
902 
903 inline
904 int shmimIntegrator::processImage( void * curr_src,
905  const darkShmimT & dummy
906  )
907 {
908  static_cast<void>(dummy); //be unused
909 
910  realT * data = m_darkImage.data();
911 
912  for(unsigned nn=0; nn < darkMonitorT::m_width*darkMonitorT::m_height; ++nn)
913  {
914  //data[nn] = *( (int16_t * ) (curr_src + nn*shmimMonitorT::m_typeSize));
915  data[nn] = dark_pixget(curr_src, nn);
916  }
917 
918  m_darkSet = true; //There is a dark set and ready to use, but it may or may not be valid.
919 
920  return 0;
921 }
922 
923 inline
925 {
926  static_cast<void>(dummy); //be unused
927 
928  std::unique_lock<std::mutex> lock(m_darkMutex); //Lock the mutex before messing with the dark.
929 
931  m_dark2Image.setZero();
932 
933  dark2_pixget = getPixPointer<realT>(dark2MonitorT::m_dataType);
934 
935  if(dark2_pixget == nullptr)
936  {
937  log<software_error>({__FILE__, __LINE__, "bad data type"});
938  m_dark2Set = false;
939  m_dark2Valid = false;
940  return -1;
941  }
942 
944  {
945  m_dark2Valid = true;
946  }
947  else
948  {
949  m_dark2Valid = false;
950  }
951 
952 
953  return 0;
954 }
955 
956 inline
957 int shmimIntegrator::processImage( void * curr_src,
958  const dark2ShmimT & dummy
959  )
960 {
961  static_cast<void>(dummy); //be unused
962 
963  realT * data = m_dark2Image.data();
964 
965  for(unsigned nn=0; nn < dark2MonitorT::m_width*dark2MonitorT::m_height; ++nn)
966  {
967  //data[nn] = *( (int16_t * ) (curr_src + nn*shmimMonitorT::m_typeSize));
968  data[nn] = dark2_pixget(curr_src, nn);
969  }
970 
971  m_dark2Set = true; //There is a dark set and ready to use, but it may or may not be valid.
972 
973  return 0;
974 }
975 
976 inline
978 {
979  std::vector<std::string> fnames = mx::ioutils::getFileNames(m_fileSaveDir, m_configName, "", ".fits");
980 
981  //getFileNames sorts, so these will be in oldest to newest order by lexical timestamp sort
982  //So we search in reverse to always pick newest
983  long N = fnames.size();
984  for(long n = N-1; n >= 0; --n)
985  {
986  std::string fn = mx::ioutils::pathStem(fnames[n]);
987 
988  if(fn.size() < m_configName.size()+1) continue;
989 
990  size_t st = m_configName.size()+1;
991  size_t ed = fn.find("__T");
992  if(ed == std::string::npos || ed - st < 2) continue;
993  std::string stateStr = fn.substr(st,ed-st);
994 
995  if(stateStr == m_stateString)
996  {
997  mx::fits::fitsFile<float> ff;
998  ff.read(m_avgImage, fnames[n]);
999 
1001  {
1002  //Means the camera has changed but stream hasn't caught up
1003  //(This happens on startup before stream connection completes.)
1004 
1005  // And possibly that we haven't turned the shmimMonitor on yet by switching to OPERATING
1007  {
1008  sleep(1); //wait for everything else to get initialized
1011  m_reconfig = true;
1012  }
1013  else
1014  {
1015  if(m_running) return 0;
1016  m_imageValid = false;
1017  m_stateStringChanged = true; //So we let appLogic try again next time around.
1018  return 0;
1019  }
1020  }
1021 
1022  if(m_running) return 0;
1023 
1024  m_updated = true;
1025  //Now tell the f.g. to get going
1026  if(sem_post(&m_smSemaphore) < 0)
1027  {
1028  log<software_critical>({__FILE__, __LINE__, errno, 0, "Error posting to semaphore"});
1029  return -1;
1030  }
1031  m_imageValid = true;
1032  m_stateStringChanged = false;
1033  log<text_log>("loaded last matching dark from disk", logPrio::LOG_NOTICE);
1034  return 0;
1035  }
1036  }
1037 
1038  if(m_running) return 0;
1039 
1040  m_imageValid = false;
1041  m_stateStringChanged = false; //stop trying b/c who else is going to add a dark?
1042  log<text_log>("dark is not valid", logPrio::LOG_WARNING);
1043 
1044  return 0;
1045 }
1046 
1047 inline
1049 {
1050  std::unique_lock<std::mutex> lock(m_indiMutex);
1051 
1052  ///\todo potential but verrrrry unlikely bug: shmimMonitorT could change these before allocate sets the lock above. Should use a local set of w/h instead.
1054  {
1055  //This means we haven't connected to the stream to average. so wait.
1056  lock.unlock(); //don't hold the lock for a whole second.
1057  sleep(1);
1058  return -1;
1059  }
1060 
1063  frameGrabberT::m_dataType = _DATATYPE_FLOAT;
1064 
1065  return 0;
1066 }
1067 
1068 inline
1070 {
1071  return 0;
1072 }
1073 
1074 inline
1076 {
1077  timespec ts;
1078 
1079  if(clock_gettime(CLOCK_REALTIME, &ts) < 0)
1080  {
1081  log<software_critical>({__FILE__,__LINE__,errno,0,"clock_gettime"});
1082  return -1;
1083  }
1084 
1085  ts.tv_sec += 1;
1086 
1087  if(sem_timedwait(&m_smSemaphore, &ts) == 0)
1088  {
1089  if( m_updated )
1090  {
1091  clock_gettime(CLOCK_REALTIME, &m_currImageTimestamp);
1092  return 0;
1093  }
1094  else
1095  {
1096  return 1;
1097  }
1098  }
1099  else
1100  {
1101  return 1;
1102  }
1103 }
1104 
1105 inline
1107 {
1109  m_updated = false;
1110  return 0;
1111 }
1112 
1113 inline
1115 {
1116  return 0;
1117 }
1118 
1119 INDI_NEWCALLBACK_DEFN(shmimIntegrator, m_indiP_nAverage)(const pcf::IndiProperty &ipRecv)
1120 {
1121  if(ipRecv.getName() != m_indiP_nAverage.getName())
1122  {
1123  log<software_error>({__FILE__, __LINE__, "invalid indi property received"});
1124  return -1;
1125  }
1126 
1127  unsigned target;
1128 
1129  if( indiTargetUpdate( m_indiP_nAverage, target, ipRecv, true) < 0)
1130  {
1131  log<software_error>({__FILE__,__LINE__});
1132  return -1;
1133  }
1134 
1135  m_nAverage = target;
1136 
1137  if(m_avgTime > 0 && m_fps > 0)
1138  {
1139  m_avgTime = m_nAverage/m_fps;
1140  }
1141 
1142  updateIfChanged(m_indiP_nAverage, "current", m_nAverage, INDI_IDLE);
1143  updateIfChanged(m_indiP_nAverage, "target", m_nAverage, INDI_IDLE);
1144 
1145  updateIfChanged(m_indiP_avgTime, "current", m_avgTime, INDI_IDLE);
1146  updateIfChanged(m_indiP_avgTime, "target", m_avgTime, INDI_IDLE);
1147 
1148  shmimMonitorT::m_restart = true;
1149 
1150  log<text_log>("set nAverage to " + std::to_string(m_nAverage), logPrio::LOG_NOTICE);
1151 
1152  return 0;
1153 }
1154 
1155 INDI_NEWCALLBACK_DEFN(shmimIntegrator, m_indiP_avgTime)(const pcf::IndiProperty &ipRecv)
1156 {
1157  if(ipRecv.getName() != m_indiP_avgTime.getName())
1158  {
1159  log<software_error>({__FILE__, __LINE__, "invalid indi property received"});
1160  return -1;
1161  }
1162 
1163  float target;
1164 
1165  if( indiTargetUpdate( m_indiP_avgTime, target, ipRecv, true) < 0)
1166  {
1167  log<software_error>({__FILE__,__LINE__});
1168  return -1;
1169  }
1170 
1171  m_avgTime = target;
1172 
1173  updateIfChanged(m_indiP_avgTime, "current", m_avgTime, INDI_IDLE);
1174  updateIfChanged(m_indiP_avgTime, "target", m_avgTime, INDI_IDLE);
1175 
1176  shmimMonitorT::m_restart = true;
1177 
1178  log<text_log>("set avgTime to " + std::to_string(m_avgTime), logPrio::LOG_NOTICE);
1179 
1180  return 0;
1181 }
1182 
1183 INDI_NEWCALLBACK_DEFN(shmimIntegrator, m_indiP_nUpdate)(const pcf::IndiProperty &ipRecv)
1184 {
1185  if(ipRecv.getName() != m_indiP_nUpdate.getName())
1186  {
1187  log<software_error>({__FILE__, __LINE__, "invalid indi property received"});
1188  return -1;
1189  }
1190 
1191  unsigned target;
1192 
1193  if( indiTargetUpdate( m_indiP_nUpdate, target, ipRecv, true) < 0)
1194  {
1195  log<software_error>({__FILE__,__LINE__});
1196  return -1;
1197  }
1198 
1199  m_nUpdate = target;
1200 
1201  updateIfChanged(m_indiP_nUpdate, "current", m_nUpdate, INDI_IDLE);
1202  updateIfChanged(m_indiP_nUpdate, "target", m_nUpdate, INDI_IDLE);
1203 
1204 
1205  shmimMonitorT::m_restart = true;
1206 
1207  log<text_log>("set nUpdate to " + std::to_string(m_nUpdate), logPrio::LOG_NOTICE);
1208 
1209  return 0;
1210 }
1211 
1212 INDI_NEWCALLBACK_DEFN(shmimIntegrator, m_indiP_startAveraging)(const pcf::IndiProperty &ipRecv)
1213 {
1214  if(ipRecv.getName() != m_indiP_startAveraging.getName())
1215  {
1216  log<software_error>({__FILE__, __LINE__, "invalid indi property received"});
1217  return -1;
1218  }
1219 
1220  if(!ipRecv.find("toggle")) return 0;
1221 
1222  if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::Off)
1223  {
1224  std::unique_lock<std::mutex> lock(m_indiMutex);
1225 
1226  m_running = false;
1227 
1228  state(stateCodes::READY);
1229 
1230  updateSwitchIfChanged(m_indiP_startAveraging, "toggle", pcf::IndiElement::Off, INDI_IDLE);
1231  }
1232 
1233  if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::On)
1234  {
1235  std::unique_lock<std::mutex> lock(m_indiMutex);
1236 
1237  if(m_fileSaver && !m_continuous) m_stateStringChanged = false; //We reset this here so we can detect a change at the end of the integration
1238 
1239  m_stateStringValidOnStart = m_stateStringValid;
1240  m_running = true;
1241 
1242  state(stateCodes::OPERATING);
1243 
1244  updateSwitchIfChanged(m_indiP_startAveraging, "toggle", pcf::IndiElement::On, INDI_BUSY);
1245  }
1246  return 0;
1247 }
1248 
1249 INDI_SETCALLBACK_DEFN( shmimIntegrator, m_indiP_fpsSource )(const pcf::IndiProperty &ipRecv)
1250 {
1251  if( ipRecv.getName() != m_indiP_fpsSource.getName())
1252  {
1253  log<software_error>({__FILE__, __LINE__, "Invalid INDI property."});
1254  return -1;
1255  }
1256 
1257  if( ipRecv.find("current") != true ) //this isn't valie
1258  {
1259  return 0;
1260  }
1261 
1262  std::lock_guard<std::mutex> guard(m_indiMutex);
1263 
1264  realT fps = ipRecv["current"].get<float>();
1265 
1266  if(fps != m_fps)
1267  {
1268  m_fps = fps;
1269  std::cout << "Got fps: " << m_fps << "\n";
1270  shmimMonitorT::m_restart = true;
1271  }
1272 
1273  return 0;
1274 }
1275 
1276 INDI_SETCALLBACK_DEFN( shmimIntegrator, m_indiP_stateSource )(const pcf::IndiProperty &ipRecv)
1277 {
1278  if( ipRecv.getName() != m_indiP_stateSource.getName())
1279  {
1280  log<software_error>({__FILE__, __LINE__, "Invalid INDI property."});
1281  return -1;
1282  }
1283 
1284  if( ipRecv.find("valid") == true )
1285  {
1286  bool stateStringValid;
1287  if(ipRecv["valid"].get<std::string>() == "yes") stateStringValid = true;
1288  else stateStringValid = false;
1289 
1290  if(stateStringValid != m_stateStringValid) m_stateStringChanged = true;
1291 
1292  m_stateStringValid = stateStringValid;
1293  }
1294 
1295  if( ipRecv.find("current") != true )
1296  {
1297  return 0;
1298  }
1299 
1300 
1301 
1302  std::lock_guard<std::mutex> guard(m_indiMutex);
1303 
1304  std::string ss = ipRecv["current"].get<std::string>();
1305 
1306  if(ss != m_stateString)
1307  {
1308  m_stateString = ss;
1309  m_imageValid = false; //This will mark the current dark invalid
1310  updateIfChanged(m_indiP_imageValid, "flag", "no");
1311  m_stateStringChanged = true; //We declare it changed. This can have two effects:
1312  // 1) if we are not currently integrating, it will start a lookup in appLogic
1313  // 2) if we are integrating, after it finishes it will not be declared valid and then we'll lookup in appLogic
1314  }
1315 
1316  return 0;
1317 }
1318 
1319 inline
1321 {
1323 }
1324 
1325 inline
1327 {
1328  return recordFGTimings(true);
1329 }
1330 
1331 } //namespace app
1332 } //namespace MagAOX
1333 
1334 #endif //shmimIntegrator_hpp
The base-class for MagAO-X applications.
Definition: MagAOXApp.hpp:73
void updateIfChanged(pcf::IndiProperty &p, const std::string &el, const T &newVal, pcf::IndiProperty::PropertyStateType ipState=pcf::IndiProperty::Ok)
Update an INDI property element value if it has changed.
Definition: MagAOXApp.hpp:3120
std::string m_configName
The name of the configuration file (minus .conf).
Definition: MagAOXApp.hpp:83
stateCodes::stateCodeT state()
Get the current state code.
Definition: MagAOXApp.hpp:2297
int registerIndiPropertyNew(pcf::IndiProperty &prop, int(*)(void *, const pcf::IndiProperty &))
Register an INDI property which is exposed for others to request a New Property for.
int createStandardIndiToggleSw(pcf::IndiProperty &prop, const std::string &name, const std::string &label="", const std::string &group="")
Create a standard R/W INDI switch with a single toggle element.
Definition: MagAOXApp.hpp:2543
void updateSwitchIfChanged(pcf::IndiProperty &p, const std::string &el, const pcf::IndiElement::SwitchStateType &newVal, pcf::IndiProperty::PropertyStateType ipState=pcf::IndiProperty::Ok)
Update an INDI switch element value if it has changed.
Definition: MagAOXApp.hpp:3144
std::string m_calibDir
The path to calibration files for MagAOX.
Definition: MagAOXApp.hpp:89
static int log(const typename logT::messageT &msg, logPrioT level=logPrio::LOG_DEFAULT)
Make a log entry.
Definition: MagAOXApp.hpp:1804
int registerIndiPropertyReadOnly(pcf::IndiProperty &prop)
Register an INDI property which is read only.
Definition: MagAOXApp.hpp:2655
int createROIndiText(pcf::IndiProperty &prop, const std::string &propName, const std::string &elName, const std::string &propLabel="", const std::string &propGroup="", const std::string &elLabel="")
Create a standard ReadOnly INDI Text property, with at least one element.
Definition: MagAOXApp.hpp:2434
std::mutex m_indiMutex
Mutex for locking INDI communications.
Definition: MagAOXApp.hpp:545
timespec m_currImageTimestamp
The timestamp of the current image.
uint32_t m_width
The width of the image, once deinterlaced etc.
int appShutdown()
Shuts down the framegrabber thread.
int loadConfig(mx::app::appConfigurator &config)
load the configuration system results
size_t m_typeSize
The size of the type, in bytes. Result of sizeof.
int updateINDI()
Update the INDI properties for this device controller.
uint8_t m_dataType
The ImageStreamIO type code.
bool m_reconfig
Flag to set if a camera reconfiguration requires a framegrabber reset.
int appLogic()
Checks the framegrabber thread.
uint32_t m_height
The height of the image, once deinterlaced etc.
int setupConfig(mx::app::appConfigurator &config)
Setup the configuration system.
uint32_t m_width
The width of the images in the stream.
int setupConfig(mx::app::appConfigurator &config)
Setup the configuration system.
int updateINDI()
Update the INDI properties for this device controller.
int appLogic()
Checks the shmimMonitor thread.
uint32_t m_height
The height of the images in the stream.
int appShutdown()
Shuts down the shmimMonitor thread.
uint8_t m_dataType
The ImageStreamIO type code.
int loadConfig(mx::app::appConfigurator &config)
load the configuration system results
bool m_getExistingFirst
If set to true by derivedT, any existing image will be grabbed and sent to processImage before waitin...
pcf::IndiProperty m_indiP_imageValid
int startAcquisition()
Implementation of the framegrabber startAcquisition interface.
dev::telemeter< shmimIntegrator > telemeterT
std::mutex m_darkMutex
Pointer to a function to extract the image data as our desired type realT.
pcf::IndiProperty m_indiP_nAverage
dev::frameGrabber< shmimIntegrator > frameGrabberT
int allocate(const dev::shmimT &dummy)
realT(* pixget)(void *, size_t)
INDI_SETCALLBACK_DECL(shmimIntegrator, m_indiP_fpsSource)
float fps()
Implementation of the framegrabber fps interface.
INDI_NEWCALLBACK_DECL(shmimIntegrator, m_indiP_nAverage)
int recordTelem(const telem_fgtimings *)
mx::improc::eigenCube< realT > m_accumImages
Cube used to accumulate images.
INDI_NEWCALLBACK_DECL(shmimIntegrator, m_indiP_avgTime)
float realT
Floating point type in which to do all calculations.
int loadConfigImpl(mx::app::appConfigurator &_config)
Implementation of loadConfig logic, separated for testing.
pcf::IndiProperty m_indiP_stateSource
pcf::IndiProperty m_indiP_startAveraging
int reconfig()
Implementation of the framegrabber reconfig interface.
shmimIntegrator()
Pointer to a function to extract the image data as our desired type realT.
pcf::IndiProperty m_indiP_fps
this integrator's FPS
dev::shmimMonitor< shmimIntegrator, dark2ShmimT > dark2MonitorT
realT(* dark_pixget)(void *, size_t)
float m_fps
Current FPS from the FPS source.
bool m_continuous
Set to false in configuration to have this run once then stop until triggered.
unsigned m_nUpdate
The rate at which to update the average. If 0 < m_nUpdate < m_nAverage then this is a moving averager...
int acquireAndCheckValid()
Implementation of the framegrabber acquireAndCheckValid interface.
INDI_SETCALLBACK_DECL(shmimIntegrator, m_indiP_stateSource)
bool m_running
Set to false in configuration to have it not start averaging until triggered.
pcf::IndiProperty m_indiP_fpsSource
mx::improc::eigenImage< realT > m_dark2Image
Pointer to a function to extract the image data as our desired type realT.
int loadImageIntoStream(void *dest)
Implementation of the framegrabber loadImageIntoStream interface.
realT(* dark2_pixget)(void *, size_t)
int processImage(void *curr_src, const dev::shmimT &dummy)
virtual int appStartup()
Startup function.
sem_t m_smSemaphore
Semaphore used to synchronize the fg thread and the sm thread.
unsigned m_nAverageDefault
The number of frames to average. Default 10.
dev::shmimMonitor< shmimIntegrator > shmimMonitorT
std::string m_stateSource
The source of the state string used for file management.
virtual int appShutdown()
Shutdown the app.
virtual int appLogic()
Implementation of the FSM for shmimIntegrator.
INDI_NEWCALLBACK_DECL(shmimIntegrator, m_indiP_nUpdate)
float m_avgTime
If non zero, then m_nAverage adjusts automatically to keep a constant averaging time [sec]....
INDI_NEWCALLBACK_DECL(shmimIntegrator, m_indiP_startAveraging)
~shmimIntegrator() noexcept
D'tor, declared and defined for noexcept.
int configureAcquisition()
Implementation of the framegrabber configureAcquisition interface.
mx::improc::eigenImage< realT > m_darkImage
std::string m_fpsSource
Device name for getting fps if time-based averaging is used. This device should have *....
dev::shmimMonitor< shmimIntegrator, darkShmimT > darkMonitorT
bool m_fileSaver
Set to true in configuration to have this save and reload files automatically.
static constexpr bool c_frameGrabber_flippable
app:dev config to tell framegrabber these images can not be flipped
mx::improc::eigenImage< realT > m_avgImage
The average image.
#define INDI_NEWCALLBACK(prop)
Get the name of the static callback wrapper for a new property.
Definition: indiMacros.hpp:208
#define REG_INDI_SETPROP(prop, devName, propName)
Register a SET INDI property with the class, using the standard callback name.
Definition: indiMacros.hpp:282
#define CREATE_REG_INDI_RO_NUMBER(prop, name, label, group)
Create and register a RO INDI property as a number, using the standard callback name.
Definition: indiMacros.hpp:385
@ OPERATING
The device is operating, other than homing.
Definition: stateCodes.hpp:55
@ READY
The device is ready for operation, but is not operating.
Definition: stateCodes.hpp:56
#define INDI_IDLE
Definition: indiUtils.hpp:28
#define INDI_BUSY
Definition: indiUtils.hpp:30
#define INDI_OK
Definition: indiUtils.hpp:29
std::ostream & cout()
void updateSwitchIfChanged(pcf::IndiProperty &p, const std::string &el, const pcf::IndiElement::SwitchStateType &newVal, indiDriverT *indiDriver, pcf::IndiProperty::PropertyStateType newState=pcf::IndiProperty::Ok)
Update the value of the INDI element, but only if it has changed.
Definition: indiUtils.hpp:212
const pcf::IndiProperty & ipRecv
Definition: MagAOXApp.hpp:3434
INDI_SETCALLBACK_DEFN(adcTracker, m_indiP_teldata)(const pcf
Definition: adcTracker.hpp:461
updateIfChanged(m_indiP_angle, "target", m_angle)
INDI_NEWCALLBACK_DEFN(acesxeCtrl, m_indiP_windspeed)(const pcf
Definition: acesxeCtrl.hpp:687
std::unique_lock< std::mutex > lock(m_indiMutex)
Definition: dm.hpp:24
constexpr static logPrioT LOG_WARNING
A condition has occurred which may become an error, but the process continues.
Definition: logPriority.hpp:43
constexpr static logPrioT LOG_NOTICE
A normal but significant condition.
Definition: logPriority.hpp:46
static std::string indiPrefix()
static std::string configSection()
static std::string configSection()
static std::string indiPrefix()
A device base class which saves telemetry.
Definition: telemeter.hpp:69
int appShutdown()
Perform telemeter application shutdown.
Definition: telemeter.hpp:274
int loadConfig(appConfigurator &config)
Load the device section from an application configurator.
Definition: telemeter.hpp:223
int appLogic()
Perform telemeter application logic.
Definition: telemeter.hpp:268
int setupConfig(appConfigurator &config)
Setup an application configurator for the device section.
Definition: telemeter.hpp:211
int appStartup()
Starts the telemetry log thread.
Definition: telemeter.hpp:241
int checkRecordTimes(const telT &tel, telTs... tels)
Check the time of the last record for each telemetry type and make an entry if needed.
Definition: telemeter.hpp:281
Software ERR log entry.
Log entry recording framegrabber timings.