First steps towards history plotting

This commit is contained in:
Thomas Kolb 2024-07-06 16:51:46 +02:00
parent 8d5b1a425b
commit 1197e083f4
6 changed files with 387 additions and 221 deletions

View file

@ -42,8 +42,8 @@
//#define GxEPD2_DRIVER_CLASS GxEPD2_260_M01 // GDEW026M01 152x296, UC8151 (IL0373)
//#define GxEPD2_DRIVER_CLASS GxEPD2_270 // GDEW027W3 176x264, EK79652 (IL91874)
//#define GxEPD2_DRIVER_CLASS GxEPD2_371 // GDEW0371W7 240x416, UC8171 (IL0324)
#define GxEPD2_DRIVER_CLASS GxEPD2_420 // GDEW042T2 400x300, UC8176 (IL0398)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420_M01 // GDEW042M01 400x300, UC8176 (IL0398)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420 // GDEW042T2 400x300, UC8176 (IL0398)
#define GxEPD2_DRIVER_CLASS GxEPD2_420_M01 // GDEW042M01 400x300, UC8176 (IL0398)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583 // GDEW0583T7 600x448, UC8179 (IL0371)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583_T8 // GDEW0583T8 648x480, GD7965
//#define GxEPD2_DRIVER_CLASS GxEPD2_750 // GDEW075T8 640x384, UC8179 (IL0371)
@ -114,8 +114,10 @@
// adapt the constructor parameters to your wiring
#if !IS_GxEPD2_1248(GxEPD2_DRIVER_CLASS)
#if defined(ARDUINO_LOLIN_D32_PRO)
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 0, /*RST=*/ 2, /*BUSY=*/ 15));
#elif defined(ARDUINO_ESP32_DEV) // e.g. TTGO T8 ESP32-WROVER
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 2, /*RST=*/ 0, /*BUSY=*/ 4));
#else
// moved to epaper.c because of multiple definitions when in header
@ -124,6 +126,7 @@ GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> displ
#else // GxEPD2_1248
// Waveshare 12.48 b/w SPI display board and frame or Good Display 12.48 b/w panel GDEW1248T3
// general constructor for use with all parameters, e.g. for Waveshare ESP32 driver board mounted on connection board
#error Wrong constructur
GxEPD2_BW < GxEPD2_1248, GxEPD2_1248::HEIGHT / 4 > display(GxEPD2_1248(/*sck=*/ 13, /*miso=*/ 12, /*mosi=*/ 14,
/*cs_m1=*/ 23, /*cs_s1=*/ 22, /*cs_m2=*/ 16, /*cs_s2=*/ 19,
/*dc1=*/ 25, /*dc2=*/ 17, /*rst1=*/ 33, /*rst2=*/ 5,
@ -142,6 +145,7 @@ GxEPD2_BW < GxEPD2_1248, GxEPD2_1248::HEIGHT / 4 > display(GxEPD2_1248(/*sck=*/
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ EPD_CS, /*DC=*/ 3, /*RST=*/ 2, /*BUSY=*/ 1));
#endif
@ -159,6 +163,7 @@ GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> displ
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif
@ -172,6 +177,7 @@ GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> displ
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=77*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif
@ -185,6 +191,7 @@ GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> displ
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/ 7, /*RST=*/ 6, /*BUSY=*/ 5));
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/ 3, /*RST=*/ 2, /*BUSY=*/ 1)); // my Seed XIOA0
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 3, /*DC=*/ 2, /*RST=*/ 1, /*BUSY=*/ 0)); // my other Seed XIOA0
@ -200,6 +207,7 @@ GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> displ
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#error Wrong constructur
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif

View file

@ -1,6 +1,8 @@
#ifndef EPAPER_H
#define EPAPER_H
#include "timeseries.h"
#include <GxEPD2_BW.h>
#include "GxEPD2_display_selection_new_style.h"
@ -9,8 +11,8 @@ typedef void (*epaper_callback)(GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HE
void epaper_init(void);
void epaper_test(void);
//void epaper_plot(uint16_t x, uint16_t y, uint16_t w, uint16_t h, timeseries_t *timeseries, uint32_t time_tick_interval);
void epaper_plot(uint16_t x, uint16_t y, uint16_t w, uint16_t h, timeseries_t *timeseries, uint32_t time_tick_interval);
void epaper_draw_and_hibernate(epaper_callback cb);
void epaper_draw_and_hibernate(epaper_callback cb, bool full_refresh);
#endif

16
include/timeseries.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <stdint.h>
#include <vector>
typedef struct {
uint64_t tstart, tend, interval;
std::vector<float> 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_append(timeseries_t *ts, float value);

View file

@ -8,286 +8,256 @@
#include "epaper.h"
static GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>
display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4));
display(GxEPD2_DRIVER_CLASS(/*CS=*/ 17, /*DC=*/ 18, /*RST=*/ 19, /*BUSY=*/ 22));
void epaper_init(void)
{
display.init();
display.init(0, true, 2, false);
}
void epaper_test(void)
{
display.setRotation(0);
display.setFont(&FreeMonoBold9pt7b);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("Hello World!", 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((display.width() - tbw) / 2) - tbx;
uint16_t y = ((display.height() - tbh) / 2) - tby;
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.setCursor(x, y);
display.print("Hello World!");
for(int i = -1; i <= 1; i++) {
display.drawCircle(display.width()/2, display.height()/2, tbw*15/10/2+i, GxEPD_RED);
}
}
while (display.nextPage());
display.setRotation(0);
display.setFont(&FreeMonoBold9pt7b);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("Hello World!", 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((display.width() - tbw) / 2) - tbx;
uint16_t y = ((display.height() - tbh) / 2) - tby;
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.setCursor(x, y);
display.print("Hello World!");
for(int i = -1; i <= 1; i++) {
display.drawCircle(display.width()/2, display.height()/2, tbw*15/10/2+i, GxEPD_BLACK);
}
Serial.println(F("Init done. Display going to powersave…"));
display.setCursor(10, FreeMonoBold9pt7b.yAdvance + 10);
for(uint16_t c = 0; c < 256; c++) {
display.print((char)c);
display.hibernate();
if((c % 32) == 31) {
display.println();
}
}
}
while (display.nextPage());
Serial.println(F("Init done. Display going to powersave…"));
}
void epaper_draw_and_hibernate(epaper_callback cb)
void epaper_draw_and_hibernate(epaper_callback cb, bool full_refresh)
{
display.setRotation(0);
display.setFullWindow();
display.firstPage();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
cb(&display);
} while(display.nextPage());
do {
cb(&display);
} while(display.nextPage());
display.hibernate();
display.hibernate();
}
#if 0
// find minimum and maximum values in a vector. NaN is ignored.
template<typename T>
template<typename T>
void minmax(const std::vector<T> &vec, T *min, T *max)
{
bool first = true;
bool first = true;
// fallback values: will be used if the vector is empty or only consists of NaNs
*min = 0;
*max = 1;
// fallback values: will be used if the vector is empty or only consists of NaNs
*min = 0;
*max = 1;
for(auto &v: vec) {
if(isnan(v)) {
continue;
}
for(auto &v: vec) {
if(isnan(v)) {
continue;
}
if(first) {
*min = *max = v;
first = false;
continue;
}
if(first) {
*min = *max = v;
first = false;
continue;
}
if(v < *min) {
*min = v;
}
if(v < *min) {
*min = v;
}
if(v > *max) {
*max = 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];
char s[32];
uint32_t trange = timeseries->tend - timeseries->tstart;
uint32_t tstep = timeseries->interval;
uint32_t trange = timeseries->tend - timeseries->tstart;
uint32_t tstep = timeseries->interval;
if(trange == 0) {
trange = 3600;
}
if(trange == 0) {
trange = 3600;
}
float vmin, vmax;
minmax(timeseries->data, &vmin, &vmax);
float vmin, vmax;
minmax(timeseries->data, &vmin, &vmax);
float vrange = vmax - vmin;
float vrange = vmax - vmin;
Serial.print(F("Value range: "));
Serial.print(vrange);
Serial.print(F(" Min: "));
Serial.print(vmin);
Serial.print(F(" Max: "));
Serial.println(vmax);
Serial.print(F("Value range: "));
Serial.print(vrange);
Serial.print(F(" Min: "));
Serial.print(vmin);
Serial.print(F(" Max: "));
Serial.println(vmax);
if(vrange == 0.0f) {
vrange = 1.0f;
}
if(vrange == 0.0f) {
vrange = 1.0f;
}
// add some margin
vmax += vrange * 0.05f;
vmin -= vrange * 0.05f;
vrange = vmax - vmin;
// add some margin
vmax += vrange * 0.05f;
vmin -= vrange * 0.05f;
vrange = vmax - vmin;
bool firstpoint = true;
uint16_t last_x;
uint16_t last_y;
bool firstpoint = true;
uint16_t last_x;
uint16_t last_y;
// set font for axis labels
display.setFont(&Org_01);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("20:48", 0, 0, &tbx, &tby, &tbw, &tbh);
// set font for axis labels
display.setFont(&Org_01);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("20:48", 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t label_height = tbh;
uint16_t label_height = tbh;
// actual graph area
uint16_t plot_x = x + 150;
uint16_t plot_y = y;
uint16_t plot_w = w - 150;
uint16_t plot_h = h - label_height - 5;
// actual graph area
uint16_t plot_x = x + tbw;
uint16_t plot_y = y;
uint16_t plot_w = w - tbw;
uint16_t plot_h = h - label_height - 5;
// determine vertical tick interval
uint16_t max_vert_tick_count = plot_h / (label_height + 5);
float min_vert_tick_step = vrange / (float)max_vert_tick_count;
// determine vertical tick interval
uint16_t max_vert_tick_count = plot_h / (label_height + 5);
float min_vert_tick_step = vrange / (float)max_vert_tick_count;
static const PROGMEM float TEST_FACTORS[] = {1.0f, 0.5f, 0.25f, 0.1f};
static const PROGMEM float TEST_FACTORS[] = {1.0f, 0.5f, 0.25f, 0.1f};
float scale = 1e3f;
float accepted_scale = scale;
bool scale_found = false;
float scale = 1e3f;
float accepted_scale = scale;
bool scale_found = false;
while((scale > 1e-3f) && !scale_found) {
float tmp_scale;
while((scale > 1e-3f) && !scale_found) {
float tmp_scale;
for(auto factor: TEST_FACTORS) {
tmp_scale = scale * factor;
if(tmp_scale < min_vert_tick_step) {
scale_found = true;
break;
}
accepted_scale = tmp_scale;
}
scale = tmp_scale;
}
for(auto factor: TEST_FACTORS) {
tmp_scale = scale * factor;
if(tmp_scale < min_vert_tick_step) {
scale_found = true;
break;
}
accepted_scale = tmp_scale;
}
scale = tmp_scale;
}
/* Start drawing */
/* Start drawing */
display.drawRect(plot_x, plot_y, plot_w, plot_h, GxEPD_BLACK);
display.drawRect(plot_x, plot_y, plot_w, plot_h, GxEPD_BLACK);
// draw vertical grid lines
for(uint64_t reltime = ((timeseries->tstart / time_tick_interval) + 1) * time_tick_interval - timeseries->tstart;
reltime < trange;
reltime += time_tick_interval) {
uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y;
uint16_t ph = plot_h + 2;
// draw vertical grid lines
for(uint64_t reltime = ((timeseries->tstart / time_tick_interval) + 1) * time_tick_interval - timeseries->tstart;
reltime < trange;
reltime += time_tick_interval) {
uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y;
uint16_t ph = plot_h + 2;
display.drawFastVLine(px, py, ph, GxEPD_BLACK);
display.drawFastVLine(px, py, ph, GxEPD_BLACK);
time_t t = reltime + timeseries->tstart;
tm *gmt = gmtime(&t);
if(t % 86400 == 0) {
strftime(s, sizeof(s), "%d.%m.", gmt);
} else {
strftime(s, sizeof(s), "%H:%M", gmt);
}
time_t t = reltime + timeseries->tstart;
tm *gmt = gmtime(&t);
if(t % 86400 == 0) {
strftime(s, sizeof(s), "%d.%m.", gmt);
} else {
strftime(s, sizeof(s), "%H:%M", gmt);
}
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(px - tbw/2, y+h + tby);
display.print(s);
}
display.setCursor(px - tbw/2, y+h + tby);
display.print(s);
}
// draw horizontal grid lines
for(float tick_val = (float)((int)(vmin / accepted_scale) + 1) * accepted_scale;
tick_val < vmax;
tick_val += accepted_scale) {
// draw horizontal grid lines
for(float tick_val = (float)((int)(vmin / accepted_scale) + 1) * accepted_scale;
tick_val < vmax;
tick_val += accepted_scale) {
uint16_t py = plot_y - (float)plot_h * (tick_val - vmax) / vrange;
uint16_t px = plot_x - 3;
uint16_t pw = plot_w + 3;
uint16_t py = plot_y - (float)plot_h * (tick_val - vmax) / vrange;
uint16_t px = plot_x - 3;
uint16_t pw = plot_w + 3;
display.drawFastHLine(px, py, pw, GxEPD_BLACK);
display.drawFastHLine(px, py, pw, GxEPD_BLACK);
snprintf(s, sizeof(s), timeseries->format, tick_val);
snprintf(s, sizeof(s), timeseries->format, tick_val);
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(px - tbw - 2, py + tby + tbh);
display.print(s);
}
display.setCursor(px - tbw - 2, py + tby + tbh);
display.print(s);
}
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;
}
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;
}
/*Serial.print(F("Processing data point at dt="));
Serial.print(reltime);
Serial.print(F(""));*/
/*Serial.print(F("Processing data point at dt="));
Serial.print(reltime);
Serial.print(F(""));*/
float val = timeseries->data[i++];
if(isnan(val)) {
//Serial.println(F("Skipping NaN!"));
float val = timeseries->data[i++];
if(isnan(val)) {
//Serial.println(F("Skipping NaN!"));
firstpoint = true;
continue;
}
firstpoint = true;
continue;
}
uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y - (float)plot_h * (val - vmax) / vrange;
uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y - (float)plot_h * (val - vmax) / vrange;
if(!firstpoint) {
/*Serial.print(F("Line from ("));
Serial.print(last_x);
Serial.print(F(","));
Serial.print(last_y);
Serial.print(F(") to ("));
Serial.print(px);
Serial.print(F(","));
Serial.print(py);
Serial.println(F(")."));*/
if(!firstpoint) {
/*Serial.print(F("Line from ("));
Serial.print(last_x);
Serial.print(F(","));
Serial.print(last_y);
Serial.print(F(") to ("));
Serial.print(px);
Serial.print(F(","));
Serial.print(py);
Serial.println(F(")."));*/
display.drawLine(last_x, last_y, px, py, GxEPD_RED);
display.drawLine(last_x, last_y, px, py, GxEPD_BLACK);
// draw it again, 1px below, to give it more weight
display.drawLine(last_x, last_y+1, px, py+1, GxEPD_RED);
} else {
//Serial.println(F("First point, not drawn."));
firstpoint = false;
}
// draw it again, 1px below, to give it more weight
display.drawLine(last_x, last_y+1, px, py+1, GxEPD_BLACK);
} else {
//Serial.println(F("First point, not drawn."));
firstpoint = false;
}
last_x = px;
last_y = py;
}
/* Draw large current value + unit */
size_t cur_val_idx = timeseries->data.size() - 1;
while((cur_val_idx > 0) && isnan(timeseries->data[cur_val_idx])) {
cur_val_idx--;
}
float current_value = timeseries->data[cur_val_idx];
display.setFont(&din1451e24pt7b);
display.setTextColor(GxEPD_RED);
char fmt[16];
snprintf(fmt, sizeof(fmt), "%s %%s", timeseries->format);
snprintf(s, sizeof(s), fmt, current_value, timeseries->unit);
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(plot_x - tbw - 35, plot_y + plot_h/2 - tby - tbh/2 - 5);
display.print(s);
uint16_t large_text_height = tbh;
time_t t = timeseries->tstart + trange * cur_val_idx / timeseries->data.size();
tm *gmt = gmtime(&t);
strftime(s, sizeof(s), "%Y-%m-%d %H:%M", gmt);
display.setFont(&Org_01);
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(plot_x - tbw - 35, plot_y + plot_h/2 - tby + large_text_height/2 + 5);
display.print(s);
last_x = px;
last_y = py;
}
}
#endif

View file

@ -1,12 +1,26 @@
#include <Arduino.h>
#include <Adafruit_SCD30.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <epaper.h>
Adafruit_SCD30 scd30;
static timeseries_t ts_scd30_co2, ts_scd30_temperature, ts_scd30_humidity;
#define REF_ALTITUDE 320 // meters
#define REF_CO2_PPM 425 // clean air according to DWD at 2024-06-17
void initSCD30(void)
{
timeseries_init(&ts_scd30_co2, 0, 15, "ppm", "%.1f");
timeseries_init(&ts_scd30_temperature, 0, 15, "°C", "%.1f");
timeseries_init(&ts_scd30_humidity, 0, 15, "%", "%.1f");
// Try to initialize!
if (!scd30.begin()) {
Serial.println("scd30: Failed to find SCD30 chip");
@ -14,6 +28,7 @@ void initSCD30(void)
}
Serial.println("scd30: SCD30 Found!");
// TODO: check what this actually resets. Maybe also the calibration?
scd30.reset();
if (!scd30.setMeasurementInterval(15)){ // seconds
@ -24,6 +39,32 @@ void initSCD30(void)
Serial.print(scd30.getMeasurementInterval());
Serial.println(" seconds");
uint16_t pressureOffset = scd30.getAmbientPressureOffset();
uint16_t altitudeOffset = scd30.getAltitudeOffset();
bool selfCalEnabled = scd30.selfCalibrationEnabled();
if(altitudeOffset != REF_ALTITUDE) {
Serial.println("scd30: updating altitude offset.");
if(!scd30.setAltitudeOffset(REF_ALTITUDE)) {
Serial.println("scd30: failed to set altitude offset!");
}
altitudeOffset = REF_ALTITUDE;
}
if(selfCalEnabled) {
Serial.println("scd30: self calibration was enabled -> disabling.");
scd30.selfCalibrationEnabled(false);
}
Serial.println("scd30: offset readout:");
Serial.print(" Pressure: ");
Serial.print(pressureOffset);
Serial.println(" mbar");
Serial.print(" Altitude: ");
Serial.print(altitudeOffset);
Serial.println(" m");
Serial.println();
if(!scd30.startContinuousMeasurement()) {
Serial.println("scd30: Could not start continuous measurement");
}
@ -32,20 +73,113 @@ void initSCD30(void)
void setup(void)
{
Serial.begin(115200);
Serial.println("Hello World!");
delay(3000);
initSCD30();
epaper_init();
epaper_test();
}
void draw_epaper_callback(GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> *display)
{
display->setRotation(1);
display->setTextColor(GxEPD_BLACK);
int16_t y = FreeSans12pt7b.yAdvance;
// first line: CO₂
display->setCursor(0, y);
display->setFont(&FreeSans12pt7b);
display->print("CO");
display->setFont(&FreeSans9pt7b);
display->print("2");
display->setFont(&FreeSans12pt7b);
display->print(": ");
y += FreeSans12pt7b.yAdvance;
display->setCursor(0, y);
display->setFont(&FreeSansBold12pt7b);
display->print(scd30.CO2, 2);
display->setFont(&FreeSans12pt7b);
display->print(" ppm");
epaper_plot(150, 5, 150, 65, &ts_scd30_co2, 900);
// second line: temperature
y = 75 + FreeSans12pt7b.yAdvance;
display->setCursor(0, y);
display->setFont(&FreeSans12pt7b);
display->print("Temp.: ");
y += FreeSans12pt7b.yAdvance;
display->setCursor(0, y);
display->setFont(&FreeSansBold12pt7b);
display->print(scd30.temperature, 2);
display->setFont(&FreeSans12pt7b);
display->print(" ");
// cheat a bit for the ° character
uint16_t stored_baseline_y = display->getCursorY();
uint16_t cursor_x = display->getCursorX();
display->setCursor(cursor_x, stored_baseline_y - FreeSans12pt7b.yAdvance/4);
display->print("o");
cursor_x = display->getCursorX();
display->setCursor(cursor_x, stored_baseline_y);
display->print("C");
epaper_plot(150, 80, 150, 65, &ts_scd30_temperature, 900);
// second line: Humidity
y = 150 + FreeSans12pt7b.yAdvance;
display->setCursor(0, y);
display->setCursor(0, y);
display->setFont(&FreeSans12pt7b);
display->print("Feuchte: ");
y += FreeSans12pt7b.yAdvance;
display->setCursor(0, y);
display->setFont(&FreeSansBold12pt7b);
display->print(scd30.relative_humidity, 1);
display->setFont(&FreeSans12pt7b);
display->print(" %rH");
epaper_plot(150, 155, 150, 65, &ts_scd30_humidity, 900);
}
void loop(void)
{
static bool calibrationDone = true; // change to false to recalibrate at REF_CO2_PPM
static uint32_t lastEPaperRefresh = 0;
uint32_t now = millis();
if(scd30.dataReady()) {
if (!scd30.read()) {
Serial.println("Error reading sensor data");
Serial.println("scd30: Error reading sensor data");
return;
}
timeseries_append(&ts_scd30_co2, scd30.CO2);
timeseries_append(&ts_scd30_temperature, scd30.temperature);
timeseries_append(&ts_scd30_humidity, scd30.relative_humidity);
Serial.print("CO2: ");
Serial.print(scd30.CO2, 3);
Serial.println(" ppm");
@ -56,6 +190,24 @@ void loop(void)
Serial.print(scd30.relative_humidity, 2);
Serial.println(" %");
Serial.println("");
if((now - lastEPaperRefresh) >= 60000) {
lastEPaperRefresh = now;
epaper_draw_and_hibernate(draw_epaper_callback, false);
}
if(!calibrationDone && now >= 300000) {
uint16_t calValue = scd30.getForcedCalibrationReference();
if(calValue != REF_CO2_PPM) {
Serial.println("scd30: Setting calibration reference value.");
if(!scd30.forceRecalibrationWithReference(REF_CO2_PPM)) {
Serial.println("scd30: Recalibration failed!");
}
}
calibrationDone = true;
}
}
delay(1000);

18
src/timeseries.cpp Normal file
View file

@ -0,0 +1,18 @@
#include "timeseries.h"
void timeseries_init(timeseries_t *ts, uint64_t tstart, uint64_t interval, 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)
{
ts->data.push_back(value);
ts->tend += ts->interval;
}