From 77e987bccce020f9b1a1b20f5867e10b07dd5345 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Mon, 13 Jan 2025 22:27:55 +0100 Subject: [PATCH] Store timestamp for each value in timeseries This fixes a bug that caused the time values in the plots drift away with increasing device uptime. --- include/timeseries.h | 13 +++++++----- src/epaper.cpp | 47 +++++++++++++++++++++++++++++++++----------- src/main.cpp | 34 +++++++++++++++++--------------- src/timeseries.cpp | 33 +++++++++++++++++++------------ 4 files changed, 81 insertions(+), 46 deletions(-) diff --git a/include/timeseries.h b/include/timeseries.h index c77ca74..7e247ee 100644 --- a/include/timeseries.h +++ b/include/timeseries.h @@ -4,16 +4,19 @@ #include +typedef std::pair timeseries_entry_t; +typedef std::deque< timeseries_entry_t > timeseries_data_t; + typedef struct { - uint64_t tstart, tend, interval; - std::deque data; + uint64_t tstart, tend; + timeseries_data_t data; const char *unit; const char *format; } timeseries_t; -void timeseries_init(timeseries_t *ts, uint64_t tstart, uint64_t interval, const char *unit, const char *format); +void timeseries_init(timeseries_t *ts, uint64_t tstart, const char *unit, const char *format); -void timeseries_append(timeseries_t *ts, float value); +void timeseries_append(timeseries_t *ts, uint64_t timestamp, float value); // remove oldest values -void timeseries_prune(timeseries_t *ts, float duration); +void timeseries_prune(timeseries_t *ts, uint64_t duration); diff --git a/src/epaper.cpp b/src/epaper.cpp index aa65b90..a6d3d8a 100644 --- a/src/epaper.cpp +++ b/src/epaper.cpp @@ -64,7 +64,7 @@ void epaper_draw_and_hibernate(epaper_callback cb, bool full_refresh) } // find minimum and maximum values in a vector. NaN is ignored. - template +template void minmax(const std::deque &vec, T *min, T *max) { bool first = true; @@ -94,12 +94,42 @@ void minmax(const std::deque &vec, T *min, T *max) } } +void minmax(const timeseries_data_t &ts, float *min, float *max) +{ + bool first = true; + + // fallback values: will be used if the vector is empty or only consists of NaNs + *min = 0; + *max = 1; + + for(auto &p: ts) { + float v = p.second; + if(isnan(v)) { + continue; + } + + if(first) { + *min = *max = v; + first = false; + continue; + } + + if(v < *min) { + *min = v; + } + + if(v > *max) { + *max = v; + } + } +} + void epaper_plot(uint16_t x, uint16_t y, uint16_t w, uint16_t h, timeseries_t *timeseries, uint32_t time_tick_interval) { char s[32]; uint32_t trange = timeseries->tend - timeseries->tstart; - uint32_t tstep = timeseries->interval; + uint32_t tstep = trange / timeseries->data.size(); if(trange == 0) { trange = 3600; @@ -216,17 +246,10 @@ void epaper_plot(uint16_t x, uint16_t y, uint16_t w, uint16_t h, timeseries_t *t } size_t i = 0; - for(uint32_t reltime = 0; reltime < trange; reltime += tstep) { - if(i >= timeseries->data.size()) { - Serial.println(F("Index out of range! Time range does not match data points.")); - break; - } + for(auto &p: timeseries->data) { + uint32_t reltime = p.first - timeseries->tstart; - /*Serial.print(F("Processing data point at dt=")); - Serial.print(reltime); - Serial.print(F("… "));*/ - - float val = timeseries->data[i++]; + float val = timeseries->data[i++].second; if(isnan(val)) { //Serial.println(F("Skipping NaN!")); diff --git a/src/main.cpp b/src/main.cpp index 04a97bc..c0e2e9a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,14 +99,14 @@ void wifi_setup(void) void initSCD30(void) { - timeseries_init(&ts_scd30_co2, timeClient.getEpochTime(), 15, "ppm", "%.0f"); - timeseries_init(&ts_scd30_temperature, timeClient.getEpochTime(), 15, "°C", "%.1f"); - timeseries_init(&ts_scd30_humidity, timeClient.getEpochTime(), 15, "%", "%.1f"); - timeseries_init(&ts_bme680_temperature, timeClient.getEpochTime(), 15, "°C", "%.1f"); - timeseries_init(&ts_bme680_humidity, timeClient.getEpochTime(), 15, "%", "%.1f"); - timeseries_init(&ts_bme680_pressure, timeClient.getEpochTime(), 15, "hPa", "%.2f"); - timeseries_init(&ts_bme680_gas_resistance, timeClient.getEpochTime(), 15, "kOhm", "%.1f"); - timeseries_init(&ts_bme680_db, timeClient.getEpochTime(), 15, "dB", "%.2f"); + timeseries_init(&ts_scd30_co2, timeClient.getEpochTime(), "ppm", "%.0f"); + timeseries_init(&ts_scd30_temperature, timeClient.getEpochTime(), "°C", "%.1f"); + timeseries_init(&ts_scd30_humidity, timeClient.getEpochTime(), "%", "%.1f"); + timeseries_init(&ts_bme680_temperature, timeClient.getEpochTime(), "°C", "%.1f"); + timeseries_init(&ts_bme680_humidity, timeClient.getEpochTime(), "%", "%.1f"); + timeseries_init(&ts_bme680_pressure, timeClient.getEpochTime(), "hPa", "%.2f"); + timeseries_init(&ts_bme680_gas_resistance, timeClient.getEpochTime(), "kOhm", "%.1f"); + timeseries_init(&ts_bme680_db, timeClient.getEpochTime(), "dB", "%.2f"); // Try to initialize! if (!scd30.begin()) { @@ -433,9 +433,10 @@ void loop(void) // skip the first readout because the CO2 reading is then invalid if(!first_scd30_readout) { - timeseries_append(&ts_scd30_co2, scd30.CO2); - timeseries_append(&ts_scd30_temperature, scd30.temperature); - timeseries_append(&ts_scd30_humidity, scd30.relative_humidity); + uint64_t updatetime = timeClient.getEpochTime(); + timeseries_append(&ts_scd30_co2, updatetime, scd30.CO2); + timeseries_append(&ts_scd30_temperature, updatetime, scd30.temperature); + timeseries_append(&ts_scd30_humidity, updatetime, scd30.relative_humidity); } else { first_scd30_readout = false; } @@ -444,10 +445,11 @@ void loop(void) // milliseconds), but the SCD30 is running autonomously, so this does not // disturb that measurement. if(bme680.performReading()) { - timeseries_append(&ts_bme680_temperature, bme680.temperature); - timeseries_append(&ts_bme680_humidity, bme680.humidity); - timeseries_append(&ts_bme680_pressure, bme680.pressure / 100.0); - timeseries_append(&ts_bme680_gas_resistance, bme680.gas_resistance / 1000.0); + uint64_t updatetime = timeClient.getEpochTime(); + timeseries_append(&ts_bme680_temperature, updatetime, bme680.temperature); + timeseries_append(&ts_bme680_humidity, updatetime, bme680.humidity); + timeseries_append(&ts_bme680_pressure, updatetime, bme680.pressure / 100.0); + timeseries_append(&ts_bme680_gas_resistance, updatetime, bme680.gas_resistance / 1000.0); bme680_max_resistance *= BME680_MAX_ADAPTATION_FACTOR; if(bme680.gas_resistance > bme680_max_resistance) { @@ -455,7 +457,7 @@ void loop(void) } bme680_db = 10 * log10(bme680.gas_resistance / bme680_max_resistance); - timeseries_append(&ts_bme680_db, bme680_db); + timeseries_append(&ts_bme680_db, updatetime, bme680_db); Serial.println("BME680:"); Serial.print("Temperature: "); diff --git a/src/timeseries.cpp b/src/timeseries.cpp index 39cace8..5dd0d60 100644 --- a/src/timeseries.cpp +++ b/src/timeseries.cpp @@ -1,32 +1,39 @@ #include "timeseries.h" -void timeseries_init(timeseries_t *ts, uint64_t tstart, uint64_t interval, const char *unit, const char *format) +void timeseries_init(timeseries_t *ts, uint64_t tstart, const char *unit, const char *format) { ts->tstart = tstart; ts->tend = tstart; - ts->interval = interval; ts->unit = unit; ts->format = format; } -void timeseries_append(timeseries_t *ts, float value) +void timeseries_append(timeseries_t *ts, uint64_t timestamp, float value) { - ts->data.push_back(value); - ts->tend += ts->interval; + ts->data.push_back( {timestamp, value} ); + ts->tend = timestamp; + + if(ts->data.size() == 1) { + ts->tstart = timestamp; + } } -void timeseries_prune(timeseries_t *ts, float duration) +void timeseries_prune(timeseries_t *ts, uint64_t duration) { - std::deque::size_type to_keep = duration / ts->interval; - if(to_keep >= ts->data.size()) { - return; + timeseries_data_t::iterator iter = ts->data.begin(); + + uint64_t new_tstart = ts->tend - duration; + + while(iter != ts->data.end() && iter->second < new_tstart) { + iter = ts->data.erase(iter); } - std::deque::size_type to_delete = ts->data.size() - to_keep; - ts->data.erase(ts->data.begin(), ts->data.begin() + to_delete); - - ts->tstart += to_delete * ts->interval; + if(iter != ts->data.end()) { + ts->tstart = iter->first; + } else { + ts->tstart = ts->tend; + } }