From cb5e9cd32ffc7d97fc41e760d86d4169da9dd7ee Mon Sep 17 00:00:00 2001 From: TD-er Date: Thu, 27 Oct 2022 17:30:07 +0200 Subject: [PATCH] [GPS] Use custom GPS view of plugin stats data --- docs/source/Plugin/_Plugin.rst | 2 + src/_P082_GPS.ino | 18 ++++ src/src/DataStructs/PluginStats.cpp | 106 ++++++++++++++++++-- src/src/DataStructs/PluginStats.h | 29 +++++- src/src/PluginStructs/P002_data_struct.cpp | 14 ++- src/src/PluginStructs/P082_data_struct.cpp | 110 ++++++++++++++++++--- src/src/PluginStructs/P082_data_struct.h | 21 ++-- 7 files changed, 259 insertions(+), 41 deletions(-) diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index 1604c0a79..30d87d72f 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -93,6 +93,8 @@ For example using just like normal task value data: * ``[bme#temp.avg]`` Compute the average over the last N samples in the historic buffer (typically: 64 samples on ESP32, 16 on ESP8266) * ``[bme#temp.avgX]`` Compute the average over the last X samples (or less if there are less samples available) +* ``[bme#temp.stddev]`` Compute the standard deviation over the last N samples in the historic buffer (typically: 64 samples on ESP32, 16 on ESP8266) +* ``[bme#temp.stddevX]`` Compute the standard deviation over the last X samples (or less if there are less samples available) * ``[bme#temp.max]`` Refer to the maximum recorded sample since the last ``resetpeaks``. N.B. Not all tasks log the min and max peaks. * ``[bme#temp.min]`` See ``[bme#temp.max]`` diff --git a/src/_P082_GPS.ino b/src/_P082_GPS.ino index 4653c1677..3c660de1b 100644 --- a/src/_P082_GPS.ino +++ b/src/_P082_GPS.ino @@ -315,6 +315,24 @@ boolean Plugin_082(uint8_t function, struct EventStruct *event, String& string) break; } +# if FEATURE_PLUGIN_STATS + case PLUGIN_WEBFORM_LOAD_SHOW_STATS: + { + P082_data_struct *P082_data = + static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P082_data) { + for (uint8_t i = 0; i < P082_NR_OUTPUT_VALUES; ++i) { + const uint8_t pconfigIndex = i + P082_QUERY1_CONFIG_POS; + if (P082_data->webformLoad_show_stats(event, i, static_cast(PCONFIG(pconfigIndex)))) { + success = true; // Something added + } + } + } + break; + } +# endif // if FEATURE_PLUGIN_STATS + case PLUGIN_INIT: { if (P082_TIMEOUT < 100) { P082_TIMEOUT = P082_DEFAULT_FIX_TIMEOUT; diff --git a/src/src/DataStructs/PluginStats.cpp b/src/src/DataStructs/PluginStats.cpp index 831bfa83f..8189ee298 100644 --- a/src/src/DataStructs/PluginStats.cpp +++ b/src/src/DataStructs/PluginStats.cpp @@ -12,6 +12,7 @@ PluginStats::PluginStats(uint8_t nrDecimals, float errorValue) : _nrDecimals(nrDecimals) { + _errorValueIsNaN = isnan(_errorValue); resetPeaks(); } @@ -45,14 +46,10 @@ float PluginStats::getSampleAvg(PluginStatsBuffer_t::index_t lastNrSamples) cons } PluginStatsBuffer_t::index_t samplesUsed = 0; - const bool errorValueIsNaN = isnan(_errorValue); - for (; i < _samples.size(); ++i) { - if (!isnan(_samples[i])) { - if (errorValueIsNaN || !essentiallyEqual(_errorValue, _samples[i])) { - ++samplesUsed; - sum += _samples[i]; - } + if (usableValue(_samples[i])) { + ++samplesUsed; + sum += _samples[i]; } } @@ -60,6 +57,32 @@ float PluginStats::getSampleAvg(PluginStatsBuffer_t::index_t lastNrSamples) cons return sum / samplesUsed; } +float PluginStats::getSampleStdDev(PluginStatsBuffer_t::index_t lastNrSamples) const +{ + float variance = 0.0f; + const float average = getSampleAvg(lastNrSamples); + if (!usableValue(average)) { return 0.0f; } + + PluginStatsBuffer_t::index_t i = 0; + + if (lastNrSamples < _samples.size()) { + i = _samples.size() - lastNrSamples; + } + PluginStatsBuffer_t::index_t samplesUsed = 0; + + for (; i < _samples.size(); ++i) { + if (usableValue(_samples[i])) { + ++samplesUsed; + const float diff = _samples[i] - average; + variance += diff * diff; + } + } + if (samplesUsed < 2) { return 0.0f; } + + variance /= samplesUsed; + return sqrtf(variance); +} + float PluginStats::operator[](PluginStatsBuffer_t::index_t index) const { if (index < _samples.size()) { return _samples[index]; } @@ -98,6 +121,22 @@ bool PluginStats::plugin_get_config_value_base(struct EventStruct *event, String } } } + } else if (command.startsWith(F("stddev"))) { + if (command.equals(F("stddev"))) { // [taskname#valuename.stddev] Std deviation of the last N kept samples + value = getSampleStdDev(); + success = true; + } else { + // Check for "stddevN", where N is the number of most recent samples to use. + int nrSamples = 0; + + if (validIntFromString(command.substring(3), nrSamples)) { + if (nrSamples > 0) { + // [taskname#valuename.stddevN] Std. deviation over N most recent samples + value = getSampleStdDev(nrSamples); + success = true; + } + } + } } if (success) { @@ -112,6 +151,8 @@ bool PluginStats::webformLoad_show_stats(struct EventStruct *event) const if (webformLoad_show_avg(event)) { somethingAdded = true; } + if (webformLoad_show_stdev(event)) { somethingAdded = true; } + if (webformLoad_show_peaks(event)) { somethingAdded = true; } if (somethingAdded) { @@ -134,18 +175,51 @@ bool PluginStats::webformLoad_show_avg(struct EventStruct *event) const return false; } -bool PluginStats::webformLoad_show_peaks(struct EventStruct *event) const +bool PluginStats::webformLoad_show_stdev(struct EventStruct *event) const { - if (hasPeaks()) { + const float stdDev = getSampleStdDev(); + if (usableValue(stdDev) && getNrSamples() > 1) { + addRowLabel(getLabel() + F(" std. dev")); + addHtmlFloat(stdDev, _nrDecimals); + addHtml(' ', '('); + addHtmlInt(getNrSamples()); + addHtml(F(" samples)")); + return true; + } + return false; +} + +bool PluginStats::webformLoad_show_peaks(struct EventStruct *event, bool include_peak_to_peak) const +{ + if (hasPeaks() && getNrSamples() > 1) { addRowLabel(getLabel() + F(" Peak Low/High")); addHtmlFloat(getPeakLow(), _nrDecimals); addHtml('/'); addHtmlFloat(getPeakHigh(), _nrDecimals); + + if (include_peak_to_peak) { + addRowLabel(getLabel() + F(" Peak-to-peak")); + addHtmlFloat(getPeakHigh() - getPeakLow(), _nrDecimals); + } return true; } return false; } +void PluginStats::webformLoad_show_val( + struct EventStruct *event, + const String & label, + double value, + const String & unit) const +{ + addRowLabel(getLabel() + label); + addHtmlFloat(value, _nrDecimals); + + if (!unit.isEmpty()) { + addUnit(unit); + } +} + # if FEATURE_CHART_JS void PluginStats::plot_ChartJS_dataset() const { @@ -170,6 +244,16 @@ void PluginStats::plot_ChartJS_dataset() const # endif // if FEATURE_CHART_JS +bool PluginStats::usableValue(float value) const +{ + if (!isnan(value)) { + if (_errorValueIsNaN || !essentiallyEqual(_errorValue, value)) { + return true; + } + } + return false; +} + PluginStats_array::PluginStats_array() { for (size_t i = 0; i < VARS_PER_TASK; ++i) { @@ -312,7 +396,9 @@ bool PluginStats_array::webformLoad_show_stats(struct EventStruct *event) const for (size_t i = 0; i < VARS_PER_TASK; ++i) { if (_plugin_stats[i] != nullptr) { - if (_plugin_stats[i]->webformLoad_show_stats(event)) { somethingAdded = true; } + if (_plugin_stats[i]->webformLoad_show_stats(event)) { + somethingAdded = true; + } } } return somethingAdded; diff --git a/src/src/DataStructs/PluginStats.h b/src/src/DataStructs/PluginStats.h index 012757931..7c135e0e5 100644 --- a/src/src/DataStructs/PluginStats.h +++ b/src/src/DataStructs/PluginStats.h @@ -70,18 +70,34 @@ public: // Compute average over last N stored values float getSampleAvg(PluginStatsBuffer_t::index_t lastNrSamples) const; + // Compute the standard deviation over all stored values + float getSampleStdDev() const { + return getSampleStdDev(_samples.size()); + } + + // Compute the standard deviation over last N stored values + float getSampleStdDev(PluginStatsBuffer_t::index_t lastNrSamples) const; + + float operator[](PluginStatsBuffer_t::index_t index) const; // Support task value notation to 'get' statistics // Notations like [taskname#taskvalue.avg] can then be used to compute the average over a number of samples. - bool plugin_get_config_value_base(struct EventStruct *event, - String & string) const; + bool plugin_get_config_value_base(struct EventStruct *event, + String & string) const; - bool webformLoad_show_stats(struct EventStruct *event) const; + bool webformLoad_show_stats(struct EventStruct *event) const; - bool webformLoad_show_avg(struct EventStruct *event) const; - bool webformLoad_show_peaks(struct EventStruct *event) const; + bool webformLoad_show_avg(struct EventStruct *event) const; + bool webformLoad_show_stdev(struct EventStruct *event) const; + bool webformLoad_show_peaks(struct EventStruct *event, + bool include_peak_to_peak = true) const; + void webformLoad_show_val( + struct EventStruct *event, + const String & label, + double value, + const String & unit) const; const String& getLabel() const { @@ -121,11 +137,14 @@ public: private: + bool usableValue(float value) const; + float _minValue; float _maxValue; PluginStatsBuffer_t _samples; float _errorValue; + bool _errorValueIsNaN; uint8_t _nrDecimals = 3u; }; diff --git a/src/src/PluginStructs/P002_data_struct.cpp b/src/src/PluginStructs/P002_data_struct.cpp index f6ab383c4..3715d703d 100644 --- a/src/src/PluginStructs/P002_data_struct.cpp +++ b/src/src/PluginStructs/P002_data_struct.cpp @@ -287,12 +287,16 @@ bool P002_data_struct::webformLoad_show_stats(struct EventStruct *event) { bool somethingAdded = false; - if (getPluginStats(0) != nullptr) { - if (getPluginStats(0)->webformLoad_show_avg(event)) { somethingAdded = true; } + const PluginStats* stats = getPluginStats(0); - if (getPluginStats(0)->hasPeaks()) { - formatADC_statistics(F("ADC Peak Low"), getPluginStats(0)->getPeakLow(), true); - formatADC_statistics(F("ADC Peak High"), getPluginStats(0)->getPeakHigh(), true); + if (stats != nullptr) { + if (stats->webformLoad_show_avg(event)) { somethingAdded = true; } + + if (stats->webformLoad_show_stdev(event)) { somethingAdded = true; } + + if (stats->hasPeaks()) { + formatADC_statistics(F("ADC Peak Low"), stats->getPeakLow(), true); + formatADC_statistics(F("ADC Peak High"), stats->getPeakHigh(), true); somethingAdded = true; } } diff --git a/src/src/PluginStructs/P082_data_struct.cpp b/src/src/PluginStructs/P082_data_struct.cpp index 4b75723f2..9b5c0ae65 100644 --- a/src/src/PluginStructs/P082_data_struct.cpp +++ b/src/src/PluginStructs/P082_data_struct.cpp @@ -3,7 +3,7 @@ #ifdef USES_P082 -// Needed also here for PlatformIO's library finder as the .h file +// Needed also here for PlatformIO's library finder as the .h file // is in a directory which is excluded in the src_filter # include # include @@ -161,7 +161,7 @@ bool P082_data_struct::loop() { done = true; break; default: - done = true; + done = true; break; } } @@ -221,8 +221,8 @@ bool P082_data_struct::storeCurPos(unsigned int maxAge_msec) { } _distance += distanceSinceLast(maxAge_msec); - _last_lat = gps->location.lat(); - _last_lng = gps->location.lng(); + _last_lat = gps->location.lat(); + _last_lng = gps->location.lng(); return true; } @@ -243,11 +243,12 @@ double P082_data_struct::distanceSinceLast(unsigned int maxAge_msec) { // @param age is the time in msec since the last update of the time + // additional centiseconds given by the GPS. bool P082_data_struct::getDateTime( - struct tm& dateTime, - uint32_t& age, - bool& updated, - bool& pps_sync) { + struct tm& dateTime, + uint32_t & age, + bool & updated, + bool & pps_sync) { updated = false; + if (!isInitialized()) { return false; } @@ -257,9 +258,9 @@ bool P082_data_struct::getDateTime( } if (_pps_time != 0) { - age = timePassedSince(_pps_time); + age = timePassedSince(_pps_time); _pps_time = 0; - pps_sync = true; + pps_sync = true; if ((age > P082_TIMESTAMP_AGE) || (gps->time.age() > age)) { return false; @@ -370,7 +371,7 @@ bool P082_data_struct::setDynamicModel(P082_DynamicModel model) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -406,7 +407,7 @@ bool P082_data_struct::writeToGPS(const uint8_t* data, size_t size) { if (size != easySerial->write(data, size)) { addLog(LOG_LEVEL_ERROR, F("GPS : Written less bytes than expected")); return false; - } + } return true; } } @@ -414,4 +415,89 @@ bool P082_data_struct::writeToGPS(const uint8_t* data, size_t size) { return false; } +# if FEATURE_PLUGIN_STATS +bool P082_data_struct::webformLoad_show_stats(struct EventStruct *event, uint8_t var_index, P082_query query_type) +{ + bool somethingAdded = false; + + const PluginStats *stats = getPluginStats(var_index); + + + if (stats != nullptr) { + if (stats->webformLoad_show_avg(event)) { + somethingAdded = true; + } + + bool show_custom = false; + double dist_p2p = 0.0; + double dist_stddev = 0.0; + + if (gps != nullptr) { + switch (query_type) { + case P082_query::P082_QUERY_LAT: + show_custom = true; + // Compute distance between min and max peak + dist_p2p = gps->distanceBetween( + stats->getPeakLow(), _last_lng, + stats->getPeakHigh(), _last_lng); + dist_stddev = gps->distanceBetween( + _last_lat, _last_lng, + _last_lat + stats->getSampleStdDev(), _last_lng); + break; + case P082_query::P082_QUERY_LONG: + show_custom = true; + // Compute distance between min and max peak + dist_p2p = gps->distanceBetween( + _last_lat, stats->getPeakLow(), + _last_lat, stats->getPeakHigh()); + // Compute distance for std.dev + dist_stddev = gps->distanceBetween( + _last_lat, _last_lng, + _last_lat, _last_lng + stats->getSampleStdDev()); + break; + default: + break; + } + } + + // Only show standard deviation in meters, which is more useful than std. dev in degrees. + if (somethingAdded) { + if (show_custom) { + stats->webformLoad_show_val( + event, + F(" std. dev"), + dist_stddev, + F("m")); + } else { + stats->webformLoad_show_stdev(event); + } + } + + if (stats->webformLoad_show_peaks(event, !show_custom)) { + somethingAdded = true; + + if (show_custom) { + stats->webformLoad_show_val( + event, + F(" Peak-to-peak coordinates"), + stats->getPeakHigh() - stats->getPeakLow(), + F("deg")); + stats->webformLoad_show_val( + event, + F(" Peak-to-peak distance"), + dist_p2p, + F("m")); + } + } + + if (somethingAdded) { + addFormSeparator(4); + } + } + return somethingAdded; +} + +# endif // if FEATURE_PLUGIN_STATS + + #endif // ifdef USES_P082 diff --git a/src/src/PluginStructs/P082_data_struct.h b/src/src/PluginStructs/P082_data_struct.h index 91f9dd75f..6af6fab8a 100644 --- a/src/src/PluginStructs/P082_data_struct.h +++ b/src/src/PluginStructs/P082_data_struct.h @@ -108,6 +108,10 @@ struct P082_data_struct : public PluginTaskData_base { bool setDynamicModel(P082_DynamicModel model); #endif +# if FEATURE_PLUGIN_STATS + bool webformLoad_show_stats(struct EventStruct *event, uint8_t var_index, P082_query query_type); +# endif // if FEATURE_PLUGIN_STATS + private: #ifdef P082_USE_U_BLOX_SPECIFIC // Compute checksum @@ -134,15 +138,14 @@ public: double _distance = 0.0; - - unsigned long _pps_time = 0; - unsigned long _last_measurement = 0; - uint32_t _last_time = 0; - uint32_t _last_date = 0; - uint32_t _last_setSystemTime = 0; - uint32_t _start_sentence = 0; - uint32_t _start_prev_sentence = 0; - uint32_t _start_sequence = 0; + unsigned long _pps_time = 0; + unsigned long _last_measurement = 0; + uint32_t _last_time = 0; + uint32_t _last_date = 0; + uint32_t _last_setSystemTime = 0; + uint32_t _start_sentence = 0; + uint32_t _start_prev_sentence = 0; + uint32_t _start_sequence = 0; # ifdef P082_SEND_GPS_TO_LOG String _lastSentence; String _currentSentence;