Merge pull request #4320 from TD-er/feature/GPS_custom_stats_view

[GPS] Use custom GPS view of plugin stats data + add std.dev
This commit is contained in:
TD-er
2022-10-27 17:55:07 +02:00
committed by GitHub
7 changed files with 259 additions and 41 deletions
+2
View File
@@ -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]``
+18
View File
@@ -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<P082_data_struct *>(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<P082_query>(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;
+96 -10
View File
@@ -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;
+24 -5
View File
@@ -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;
};
+9 -5
View File
@@ -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;
}
}
+98 -12
View File
@@ -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 <TinyGPS++.h>
# include <ESPeasySerial.h>
@@ -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
+12 -9
View File
@@ -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;