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.
This commit is contained in:
Thomas Kolb 2025-01-13 22:27:55 +01:00
parent fb026d93a5
commit 77e987bccc
4 changed files with 81 additions and 46 deletions

View file

@ -4,16 +4,19 @@
#include <deque>
typedef std::pair<uint64_t, float> timeseries_entry_t;
typedef std::deque< timeseries_entry_t > timeseries_data_t;
typedef struct {
uint64_t tstart, tend, interval;
std::deque<float> 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);

View file

@ -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<typename T>
template<typename T>
void minmax(const std::deque<T> &vec, T *min, T *max)
{
bool first = true;
@ -94,12 +94,42 @@ void minmax(const std::deque<T> &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!"));

View file

@ -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: ");

View file

@ -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<float>::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<float>::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;
}
}