Line data Source code
1 : /** \file flowRPM.hpp
2 : * \brief The MagAO-X flowRPM application header.
3 : *
4 : * \ingroup flowRPM_files
5 : */
6 :
7 : #ifndef flowRPM_hpp
8 : #define flowRPM_hpp
9 :
10 : #include <cerrno>
11 : #include <cmath>
12 : #include <fstream>
13 : #include <sstream>
14 :
15 : #include "../../libMagAOX/libMagAOX.hpp" // Note this is included on command line to trigger pch
16 : #include "../../magaox_git_version.h"
17 :
18 : /** \defgroup flowRPM
19 : * \brief The MagAO-X application to convert a file-based fan reading into flow telemetry.
20 : *
21 : * <a href="../handbook/operating/software/apps/flowRPM.html">Application Documentation</a>
22 : *
23 : * \ingroup apps
24 : *
25 : */
26 :
27 : /** \defgroup flowRPM_files
28 : * \ingroup flowRPM
29 : */
30 :
31 : namespace MagAOX
32 : {
33 : namespace app
34 : {
35 :
36 : /// The MagAO-X flow-from-RPM monitor.
37 : /**
38 : * \ingroup flowRPM
39 : */
40 : class flowRPM : public MagAOXApp<true>, public dev::telemeter<flowRPM>
41 : {
42 : // Give the test harness access.
43 : friend class flowRPM_test;
44 :
45 : friend class dev::telemeter<flowRPM>;
46 :
47 : public:
48 : /// Status returned by the file parser.
49 : enum class parseStatus
50 : {
51 : success,
52 : fileReadError,
53 : missingTimestamp,
54 : malformedTimestamp,
55 : missingRecord,
56 : malformedRecord,
57 : wrongDescriptor,
58 : wrongUnits,
59 : badStatus,
60 : badValue,
61 : staleReading
62 : };
63 :
64 : /// Parsed result of the current file contents.
65 : struct parseResult
66 : {
67 : parseStatus m_status{ parseStatus::fileReadError }; ///< Outcome of the parse attempt.
68 : double m_flowRate{ -999.0 }; ///< Displayed flow rate in LPM or the bad-value sentinel.
69 : double m_age{ -999.0 }; ///< Displayed age in seconds or the bad-value sentinel.
70 : timespec m_sourceTs{ 0, 0 }; ///< Parsed source timestamp when available.
71 : };
72 :
73 : /// The telemeter base type.
74 : typedef dev::telemeter<flowRPM> telemeterT;
75 :
76 : protected:
77 : /** \name Configurable Parameters - Data
78 : *
79 : * @{
80 : */
81 :
82 : /// Path to the file written by the systemd producer.
83 : std::string m_inputPath{ "/tmp/fac_flow.txt" };
84 :
85 : /// Maximum allowed source age in seconds before the reading is treated as stale.
86 : double m_maxAge{ 60.0 };
87 :
88 : /// Expected fan descriptor in the pipe-delimited record.
89 : std::string m_fanDescriptor{ "CHA_FAN1" };
90 :
91 : /// Bad-value sentinel published on parse or availability failures.
92 : double m_badValue{ -999.0 };
93 :
94 : /// Minimum interval between repeated logs for the same persistent error.
95 : double m_errorLogInterval{ 60.0 };
96 :
97 : ///@}
98 :
99 : /** \name Runtime State - Data
100 : *
101 : * @{
102 : */
103 :
104 : /// Current published flow rate in LPM or the bad-value sentinel.
105 : double m_flowRate{ -999.0 };
106 :
107 : /// Current published age in seconds or the bad-value sentinel.
108 : double m_age{ -999.0 };
109 :
110 : /// Timestamp parsed from the input file for the currently displayed value.
111 : timespec m_sourceTs{ 0, 0 };
112 :
113 : /// Whether the current displayed value is valid.
114 : bool m_haveValidReading{ false };
115 :
116 : /// Last flow value written to telemetry.
117 : double m_lastTelemFlowRate{ std::numeric_limits<double>::quiet_NaN() };
118 :
119 : /// Last validity state written to telemetry.
120 : bool m_lastTelemValid{ false };
121 :
122 : /// Time of the most recent error log emission.
123 : timespec m_lastErrorLogTs{ 0, 0 };
124 :
125 : /// Key for the most recently logged error class.
126 : std::string m_lastErrorKey;
127 :
128 : /// Read-only status property exposing flow rate and age.
129 : pcf::IndiProperty m_indiP_status;
130 :
131 : ///@}
132 :
133 : public:
134 : /// Default c'tor.
135 : flowRPM();
136 :
137 : /// D'tor, declared and defined for noexcept.
138 : ~flowRPM() noexcept;
139 :
140 : /// Set up the application configuration.
141 : virtual void setupConfig();
142 :
143 : /// Implementation of loadConfig logic, separated for testing.
144 : virtual int
145 : loadConfigImpl( mx::app::appConfigurator &_config /**< [in] application configuration from which to load */ );
146 :
147 : /// Load the application configuration.
148 : virtual void loadConfig();
149 :
150 : /// Perform application startup.
151 : virtual int appStartup();
152 :
153 : /// Implementation of the FSM for flowRPM.
154 : virtual int appLogic();
155 :
156 : /// Shut the application down.
157 : virtual int appShutdown();
158 :
159 : /** \name Parser Helpers
160 : *
161 : * @{
162 : */
163 :
164 : /// Parse a source timestamp line into a timespec.
165 : int parseTimestamp( timespec &ts, /**< [out] parsed timestamp */
166 : const std::string &line /**< [in] source timestamp line */
167 : ) const;
168 :
169 : /// Parse the sensor record line into a flow rate in LPM.
170 : parseStatus parseRecordLine( double &flowRate, /**< [out] parsed flow rate in LPM */
171 : const std::string &line /**< [in] sensor record line */
172 : ) const;
173 :
174 : /// Parse the two-line file contents using a supplied current time.
175 : int parseFileContents( parseResult &result, /**< [out] parse result */
176 : const std::string &contents, /**< [in] raw file contents */
177 : const timespec &now /**< [in] current time for age calculation */
178 : ) const;
179 :
180 : /// Read the configured file and parse its current contents.
181 : virtual int readAndParse( parseResult &result, /**< [out] parse result */
182 : const timespec &now /**< [in] current time for age calculation */
183 : ) const;
184 :
185 : /// Get the string key used for a parse status in log rate limiting.
186 : static std::string statusKey( parseStatus status /**< [in] parse status to stringify */ );
187 :
188 : /// Get the configured input path.
189 : const std::string &inputPath() const;
190 :
191 : /// Get the configured maximum reading age in seconds.
192 : double maxAge() const;
193 :
194 : /// Get the configured fan descriptor.
195 : const std::string &fanDescriptor() const;
196 :
197 : /// Get the configured bad-value sentinel.
198 : double badValue() const;
199 :
200 : /// Get the configured repeated-error log interval.
201 : double errorLogInterval() const;
202 :
203 : /// Get the currently published flow rate.
204 : double flowRate() const;
205 :
206 : /// Get the currently published age.
207 : double age() const;
208 :
209 : /// Get whether the current published value is valid.
210 : bool haveValidReading() const;
211 :
212 : ///@}
213 :
214 : /** \name Telemeter
215 : *
216 : * @{
217 : */
218 :
219 : /// Check whether telemetry records need to be forced.
220 : int checkRecordTimes();
221 :
222 : /// Record telemetry when requested by the telemeter helper.
223 : int recordTelem( const logger::telem_flowrpm * /**< [in] telemetry tag used for overload resolution */ );
224 :
225 : /// Record the currently displayed flow state to telemetry.
226 : virtual int recordFlow( bool force = false /**< [in] force a telemetry record even if unchanged */ );
227 :
228 : /// Reconcile a newly parsed result against the currently displayed state.
229 : parseResult reconcileResult( const parseResult &result, /**< [in] newly parsed file result */
230 : const timespec &now /**< [in] current time */
231 : ) const;
232 :
233 : /// Publish a parse result to INDI and runtime state.
234 : virtual int publishResult( const parseResult &result /**< [in] parse result to publish */ );
235 :
236 : /// Determine whether a repeated error should be logged now.
237 : bool shouldLogError( const std::string &key, /**< [in] error key under consideration */
238 : const timespec &now /**< [in] current time */
239 : );
240 :
241 : ///@}
242 : };
243 :
244 : namespace flowRPMDetail
245 : {
246 :
247 : /// Trim leading and trailing ASCII whitespace from a token.
248 132 : inline std::string trimToken( const std::string &token )
249 : {
250 132 : const std::string::size_type first = token.find_first_not_of( " \t\r" );
251 :
252 132 : if( first == std::string::npos )
253 : {
254 10 : return "";
255 : }
256 :
257 127 : const std::string::size_type last = token.find_last_not_of( " \t\r" );
258 127 : return token.substr( first, last - first + 1 );
259 : }
260 :
261 : /// Convert a timespec to fractional seconds.
262 32 : inline double timespecToDouble( const timespec &ts )
263 : {
264 32 : return static_cast<double>( ts.tv_sec ) + 1e-9 * static_cast<double>( ts.tv_nsec );
265 : }
266 :
267 : /// Measure elapsed seconds between two timestamps.
268 16 : inline double elapsedSeconds( const timespec &start, const timespec &end )
269 : {
270 16 : return timespecToDouble( end ) - timespecToDouble( start );
271 : }
272 :
273 : /// Split a pipe-delimited sensor line into trimmed fields.
274 16 : inline std::vector<std::string> splitPipeDelimited( const std::string &line )
275 : {
276 16 : std::vector<std::string> fields;
277 16 : std::stringstream ss( line );
278 16 : std::string field;
279 :
280 111 : while( std::getline( ss, field, '|' ) )
281 : {
282 95 : fields.push_back( trimToken( field ) );
283 : }
284 :
285 32 : return fields;
286 16 : }
287 :
288 : /// Split file contents into non-empty logical lines.
289 18 : inline std::vector<std::string> splitLogicalLines( const std::string &contents )
290 : {
291 18 : std::vector<std::string> lines;
292 18 : std::stringstream ss( contents );
293 18 : std::string line;
294 :
295 53 : while( std::getline( ss, line ) )
296 : {
297 35 : if( !line.empty() && line.back() == '\r' )
298 : {
299 6 : line.pop_back();
300 : }
301 :
302 35 : if( trimToken( line ).empty() )
303 : {
304 4 : continue;
305 : }
306 :
307 31 : lines.push_back( line );
308 : }
309 :
310 36 : return lines;
311 18 : }
312 :
313 : } // namespace flowRPMDetail
314 :
315 287 : inline flowRPM::flowRPM() : MagAOXApp( MAGAOX_CURRENT_SHA1, MAGAOX_REPO_MODIFIED )
316 : {
317 41 : return;
318 0 : }
319 :
320 41 : inline flowRPM::~flowRPM() noexcept
321 : {
322 41 : }
323 :
324 3 : inline void flowRPM::setupConfig()
325 : {
326 42 : config.add( "input.path",
327 : "",
328 : "input.path",
329 : argType::Required,
330 : "input",
331 : "path",
332 : false,
333 : "string",
334 : "Path to the file containing the two-line flow RPM record." );
335 42 : config.add( "input.maxAge",
336 : "",
337 : "input.maxAge",
338 : argType::Required,
339 : "input",
340 : "maxAge",
341 : false,
342 : "double",
343 : "Maximum source age in seconds before the reading is treated as stale. Default is 60." );
344 42 : config.add( "input.fanDescriptor",
345 : "",
346 : "input.fanDescriptor",
347 : argType::Required,
348 : "input",
349 : "fanDescriptor",
350 : false,
351 : "string",
352 : "Expected descriptor token in the source file. Default is CHA_FAN1." );
353 42 : config.add( "input.badValue",
354 : "",
355 : "input.badValue",
356 : argType::Required,
357 : "input",
358 : "badValue",
359 : false,
360 : "double",
361 : "Bad-value sentinel published when the file can not be read or parsed. Default is -999." );
362 42 : config.add( "input.errorLogInterval",
363 : "",
364 : "input.errorLogInterval",
365 : argType::Required,
366 : "input",
367 : "errorLogInterval",
368 : false,
369 : "double",
370 : "Minimum interval in seconds between repeated logs of the same persistent error. Default is 60." );
371 :
372 3 : TELEMETER_SETUP_CONFIG( config );
373 3 : }
374 :
375 2 : inline int flowRPM::loadConfigImpl( mx::app::appConfigurator &_config )
376 : {
377 4 : _config( m_inputPath, "input.path" );
378 4 : _config( m_maxAge, "input.maxAge" );
379 4 : _config( m_fanDescriptor, "input.fanDescriptor" );
380 4 : _config( m_badValue, "input.badValue" );
381 2 : _config( m_errorLogInterval, "input.errorLogInterval" );
382 :
383 2 : TELEMETER_LOAD_CONFIG( _config );
384 :
385 2 : return 0;
386 : }
387 :
388 3 : inline void flowRPM::loadConfig()
389 : {
390 3 : if( loadConfigImpl( config ) < 0 )
391 : {
392 1 : m_shutdown = 1;
393 : }
394 3 : }
395 :
396 9 : inline int flowRPM::appStartup()
397 : {
398 63 : createROIndiNumber( m_indiP_status, "status", "Flow Status" );
399 54 : indi::addNumberElement<double>( m_indiP_status, "flow_rate", -1e9, 1e9, 0.0, "%0.3f", "Flow Rate [LPM]" );
400 45 : indi::addNumberElement<double>( m_indiP_status, "age", -1e9, 1e9, 0.0, "%0.3f", "Age [s]" );
401 18 : m_indiP_status["flow_rate"] = m_badValue;
402 18 : m_indiP_status["age"] = m_badValue;
403 9 : registerIndiPropertyReadOnly( m_indiP_status );
404 :
405 9 : TELEMETER_APP_STARTUP;
406 :
407 9 : state( stateCodes::READY );
408 :
409 9 : return 0;
410 : }
411 :
412 9 : inline int flowRPM::appLogic()
413 : {
414 : timespec now;
415 9 : parseResult result;
416 9 : parseResult displayResult;
417 :
418 9 : clock_gettime( CLOCK_REALTIME, &now );
419 :
420 9 : if( readAndParse( result, now ) < 0 )
421 : {
422 2 : return log<software_error, -1>( { __FILE__, __LINE__, "unexpected failure while reading flow file" } );
423 : }
424 :
425 8 : displayResult = reconcileResult( result, now );
426 :
427 8 : const bool wasValid = m_haveValidReading;
428 8 : const bool isValid = ( displayResult.m_status == parseStatus::success );
429 :
430 8 : if( publishResult( displayResult ) < 0 )
431 : {
432 2 : return log<software_error, -1>( { __FILE__, __LINE__, "failed to publish flow result" } );
433 : }
434 :
435 7 : if( isValid )
436 : {
437 5 : if( !wasValid )
438 : {
439 4 : log<text_log>( "Recovered valid flow reading from " + m_inputPath + ".", logPrio::LOG_NOTICE );
440 : }
441 :
442 5 : m_lastErrorKey.clear();
443 : }
444 : else
445 : {
446 2 : const std::string key = statusKey( result.m_status );
447 :
448 2 : if( shouldLogError( key, now ) )
449 : {
450 2 : log<software_error>( { __FILE__, __LINE__, "flowRPM " + key + " for " + m_inputPath } );
451 : }
452 2 : }
453 :
454 7 : if( recordFlow() < 0 )
455 : {
456 2 : return log<software_error, -1>( { __FILE__, __LINE__, "error recording flow telemetry" } );
457 : }
458 :
459 6 : TELEMETER_APP_LOGIC;
460 :
461 6 : return 0;
462 : }
463 :
464 1 : inline int flowRPM::appShutdown()
465 : {
466 1 : TELEMETER_APP_SHUTDOWN;
467 :
468 1 : return 0;
469 : }
470 :
471 19 : inline int flowRPM::parseTimestamp( timespec &ts, const std::string &line ) const
472 : {
473 19 : std::stringstream ss( line );
474 19 : long long sec = 0;
475 19 : long long nsec = 0;
476 :
477 19 : if( !( ss >> sec >> nsec ) )
478 : {
479 1 : return -1;
480 : }
481 :
482 18 : char trailing = '\0';
483 18 : if( ss >> trailing )
484 : {
485 2 : return -1;
486 : }
487 :
488 16 : if( nsec < 0 || nsec >= 1000000000LL )
489 : {
490 2 : return -1;
491 : }
492 :
493 14 : ts.tv_sec = static_cast<time_t>( sec );
494 14 : ts.tv_nsec = static_cast<long>( nsec );
495 :
496 14 : return 0;
497 19 : }
498 :
499 16 : inline flowRPM::parseStatus flowRPM::parseRecordLine( double &flowRate, const std::string &line ) const
500 : {
501 16 : const std::vector<std::string> fields = flowRPMDetail::splitPipeDelimited( line );
502 :
503 16 : if( fields.size() != 6 )
504 : {
505 1 : return parseStatus::malformedRecord;
506 : }
507 :
508 15 : if( fields[1] != m_fanDescriptor )
509 : {
510 2 : return parseStatus::wrongDescriptor;
511 : }
512 :
513 13 : if( fields[4] != "RPM" )
514 : {
515 2 : return parseStatus::wrongUnits;
516 : }
517 :
518 11 : if( fields[5] != "'OK'" )
519 : {
520 2 : return parseStatus::badStatus;
521 : }
522 :
523 9 : char *end = nullptr;
524 9 : errno = 0;
525 9 : double value = std::strtod( fields[3].c_str(), &end );
526 :
527 9 : if( errno != 0 || end == fields[3].c_str() || *end != '\0' )
528 : {
529 2 : return parseStatus::badValue;
530 : }
531 :
532 7 : flowRate = value / 1000.0;
533 :
534 7 : return parseStatus::success;
535 16 : }
536 :
537 17 : inline int flowRPM::parseFileContents( parseResult &result, const std::string &contents, const timespec &now ) const
538 : {
539 17 : result.m_status = parseStatus::fileReadError;
540 17 : result.m_flowRate = m_badValue;
541 17 : result.m_age = m_badValue;
542 17 : result.m_sourceTs = { 0, 0 };
543 :
544 17 : const std::vector<std::string> lines = flowRPMDetail::splitLogicalLines( contents );
545 :
546 17 : if( lines.empty() )
547 : {
548 2 : result.m_status = parseStatus::missingTimestamp;
549 2 : return 0;
550 : }
551 :
552 15 : if( parseTimestamp( result.m_sourceTs, lines[0] ) < 0 )
553 : {
554 2 : result.m_status = parseStatus::malformedTimestamp;
555 2 : return 0;
556 : }
557 :
558 13 : if( lines.size() < 2 )
559 : {
560 2 : result.m_status = parseStatus::missingRecord;
561 2 : return 0;
562 : }
563 :
564 11 : if( lines.size() != 2 )
565 : {
566 1 : result.m_status = parseStatus::malformedRecord;
567 1 : result.m_age = std::max( 0.0, flowRPMDetail::elapsedSeconds( result.m_sourceTs, now ) );
568 1 : return 0;
569 : }
570 :
571 10 : double parsedFlowRate = m_badValue;
572 :
573 10 : result.m_status = parseRecordLine( parsedFlowRate, lines[1] );
574 10 : result.m_age = std::max( 0.0, flowRPMDetail::elapsedSeconds( result.m_sourceTs, now ) );
575 :
576 10 : if( result.m_status != parseStatus::success )
577 : {
578 4 : return 0;
579 : }
580 :
581 6 : if( result.m_age > m_maxAge )
582 : {
583 1 : result.m_status = parseStatus::staleReading;
584 1 : return 0;
585 : }
586 :
587 5 : result.m_flowRate = parsedFlowRate;
588 5 : return 0;
589 17 : }
590 :
591 8 : inline int flowRPM::readAndParse( parseResult &result, const timespec &now ) const
592 : {
593 8 : std::ifstream ifs( m_inputPath.c_str() );
594 :
595 8 : result.m_status = parseStatus::fileReadError;
596 8 : result.m_flowRate = m_badValue;
597 8 : result.m_age = m_badValue;
598 8 : result.m_sourceTs = { 0, 0 };
599 :
600 8 : if( !ifs )
601 : {
602 3 : return 0;
603 : }
604 :
605 5 : std::stringstream buffer;
606 5 : buffer << ifs.rdbuf();
607 :
608 5 : return parseFileContents( result, buffer.str(), now );
609 8 : }
610 :
611 14 : inline std::string flowRPM::statusKey( parseStatus status )
612 : {
613 14 : switch( status )
614 : {
615 1 : case parseStatus::success:
616 2 : return "success";
617 3 : case parseStatus::fileReadError:
618 6 : return "open_failed";
619 1 : case parseStatus::missingTimestamp:
620 2 : return "missing_timestamp";
621 1 : case parseStatus::malformedTimestamp:
622 2 : return "timestamp_parse_failed";
623 1 : case parseStatus::missingRecord:
624 2 : return "missing_record";
625 1 : case parseStatus::malformedRecord:
626 2 : return "record_parse_failed";
627 1 : case parseStatus::wrongDescriptor:
628 2 : return "wrong_descriptor";
629 1 : case parseStatus::wrongUnits:
630 2 : return "wrong_units";
631 1 : case parseStatus::badStatus:
632 2 : return "bad_status";
633 1 : case parseStatus::badValue:
634 2 : return "bad_value";
635 1 : case parseStatus::staleReading:
636 2 : return "stale";
637 1 : default:
638 2 : return "unknown";
639 : }
640 : }
641 :
642 2 : inline const std::string &flowRPM::inputPath() const
643 : {
644 2 : return m_inputPath;
645 : }
646 :
647 4 : inline double flowRPM::maxAge() const
648 : {
649 4 : return m_maxAge;
650 : }
651 :
652 2 : inline const std::string &flowRPM::fanDescriptor() const
653 : {
654 2 : return m_fanDescriptor;
655 : }
656 :
657 26 : inline double flowRPM::badValue() const
658 : {
659 26 : return m_badValue;
660 : }
661 :
662 2 : inline double flowRPM::errorLogInterval() const
663 : {
664 2 : return m_errorLogInterval;
665 : }
666 :
667 7 : inline double flowRPM::flowRate() const
668 : {
669 7 : return m_flowRate;
670 : }
671 :
672 6 : inline double flowRPM::age() const
673 : {
674 6 : return m_age;
675 : }
676 :
677 7 : inline bool flowRPM::haveValidReading() const
678 : {
679 7 : return m_haveValidReading;
680 : }
681 :
682 6 : inline int flowRPM::checkRecordTimes()
683 : {
684 6 : return telemeterT::checkRecordTimes( logger::telem_flowrpm() );
685 : }
686 :
687 1 : inline int flowRPM::recordTelem( const logger::telem_flowrpm * )
688 : {
689 1 : return recordFlow( true );
690 : }
691 :
692 7 : inline int flowRPM::recordFlow( bool force )
693 : {
694 7 : if( force || m_flowRate != m_lastTelemFlowRate || m_haveValidReading != m_lastTelemValid )
695 : {
696 4 : telem<logger::telem_flowrpm>( logger::telem_flowrpm::messageT( m_flowRate, m_age, m_haveValidReading ) );
697 :
698 4 : m_lastTelemFlowRate = m_flowRate;
699 4 : m_lastTelemValid = m_haveValidReading;
700 : }
701 :
702 7 : return 0;
703 : }
704 :
705 10 : inline flowRPM::parseResult flowRPM::reconcileResult( const parseResult &result, const timespec &now ) const
706 : {
707 10 : parseResult displayResult = result;
708 :
709 10 : if( result.m_status == parseStatus::success )
710 : {
711 5 : return displayResult;
712 : }
713 :
714 5 : if( !m_haveValidReading )
715 : {
716 2 : return displayResult;
717 : }
718 :
719 3 : const double lastGoodAge = std::max( 0.0, flowRPMDetail::elapsedSeconds( m_sourceTs, now ) );
720 :
721 3 : if( lastGoodAge > m_maxAge )
722 : {
723 1 : displayResult.m_status = parseStatus::staleReading;
724 1 : displayResult.m_flowRate = m_badValue;
725 1 : displayResult.m_age = lastGoodAge;
726 1 : displayResult.m_sourceTs = m_sourceTs;
727 1 : return displayResult;
728 : }
729 :
730 2 : displayResult.m_status = parseStatus::success;
731 2 : displayResult.m_flowRate = m_flowRate;
732 2 : displayResult.m_age = lastGoodAge;
733 2 : displayResult.m_sourceTs = m_sourceTs;
734 :
735 2 : return displayResult;
736 : }
737 :
738 11 : inline int flowRPM::publishResult( const parseResult &result )
739 : {
740 11 : m_flowRate = result.m_flowRate;
741 11 : m_age = result.m_age;
742 11 : m_sourceTs = result.m_sourceTs;
743 11 : m_haveValidReading = ( result.m_status == parseStatus::success );
744 :
745 22 : updateIfChanged( m_indiP_status,
746 33 : std::vector<std::string>( { "flow_rate", "age" } ),
747 22 : std::vector<double>( { m_flowRate, m_age } ),
748 11 : m_haveValidReading ? INDI_OK : INDI_ALERT );
749 :
750 11 : return 0;
751 33 : }
752 :
753 6 : inline bool flowRPM::shouldLogError( const std::string &key, const timespec &now )
754 : {
755 6 : if( key != m_lastErrorKey )
756 : {
757 4 : m_lastErrorKey = key;
758 4 : m_lastErrorLogTs = now;
759 4 : return true;
760 : }
761 :
762 2 : if( flowRPMDetail::elapsedSeconds( m_lastErrorLogTs, now ) >= m_errorLogInterval )
763 : {
764 1 : m_lastErrorLogTs = now;
765 1 : return true;
766 : }
767 :
768 1 : return false;
769 : }
770 :
771 : } // namespace app
772 : } // namespace MagAOX
773 :
774 : #endif // flowRPM_hpp
|