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_260_M01 // GDEW026M01 152x296, UC8151 (IL0373)
//#define GxEPD2_DRIVER_CLASS GxEPD2_270 // GDEW027W3 176x264, EK79652 (IL91874) //#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_371 // GDEW0371W7 240x416, UC8171 (IL0324)
#define GxEPD2_DRIVER_CLASS GxEPD2_420 // GDEW042T2 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_420_M01 // GDEW042M01 400x300, UC8176 (IL0398)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583 // GDEW0583T7 600x448, UC8179 (IL0371) //#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_583_T8 // GDEW0583T8 648x480, GD7965
//#define GxEPD2_DRIVER_CLASS GxEPD2_750 // GDEW075T8 640x384, UC8179 (IL0371) //#define GxEPD2_DRIVER_CLASS GxEPD2_750 // GDEW075T8 640x384, UC8179 (IL0371)
@ -114,8 +114,10 @@
// adapt the constructor parameters to your wiring // adapt the constructor parameters to your wiring
#if !IS_GxEPD2_1248(GxEPD2_DRIVER_CLASS) #if !IS_GxEPD2_1248(GxEPD2_DRIVER_CLASS)
#if defined(ARDUINO_LOLIN_D32_PRO) #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)); 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 #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)); 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 #else
// moved to epaper.c because of multiple definitions when in header // 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 #else // GxEPD2_1248
// Waveshare 12.48 b/w SPI display board and frame or Good Display 12.48 b/w panel GDEW1248T3 // 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 // 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, 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, /*cs_m1=*/ 23, /*cs_s1=*/ 22, /*cs_m2=*/ 16, /*cs_s2=*/ 19,
/*dc1=*/ 25, /*dc2=*/ 17, /*rst1=*/ 33, /*rst2=*/ 5, /*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)) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif #endif
// adapt the constructor parameters to your wiring // 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)); 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 #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)) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif #endif
// adapt the constructor parameters to your wiring // 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)); 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 #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)) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif #endif
// adapt the constructor parameters to your wiring // 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)); 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 #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)) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif #endif
// adapt the constructor parameters to your wiring // 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=*/ 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*/ 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 //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)) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif #endif
// adapt the constructor parameters to your wiring // 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)); 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 #endif

View file

@ -1,6 +1,8 @@
#ifndef EPAPER_H #ifndef EPAPER_H
#define EPAPER_H #define EPAPER_H
#include "timeseries.h"
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#include "GxEPD2_display_selection_new_style.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_init(void);
void epaper_test(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 #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" #include "epaper.h"
static GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> 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) void epaper_init(void)
{ {
display.init(); display.init(0, true, 2, false);
} }
void epaper_test(void) void epaper_test(void)
{ {
display.setRotation(0); display.setRotation(0);
display.setFont(&FreeMonoBold9pt7b); display.setFont(&FreeMonoBold9pt7b);
display.setTextColor(GxEPD_BLACK); display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh; int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("Hello World!", 0, 0, &tbx, &tby, &tbw, &tbh); display.getTextBounds("Hello World!", 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin: // center the bounding box by transposition of the origin:
uint16_t x = ((display.width() - tbw) / 2) - tbx; uint16_t x = ((display.width() - tbw) / 2) - tbx;
uint16_t y = ((display.height() - tbh) / 2) - tby; uint16_t y = ((display.height() - tbh) / 2) - tby;
display.setFullWindow(); display.setFullWindow();
display.firstPage(); display.firstPage();
do do
{ {
display.fillScreen(GxEPD_WHITE); display.fillScreen(GxEPD_WHITE);
display.setCursor(x, y); display.setCursor(x, y);
display.print("Hello World!"); display.print("Hello World!");
for(int i = -1; i <= 1; i++) { for(int i = -1; i <= 1; i++) {
display.drawCircle(display.width()/2, display.height()/2, tbw*15/10/2+i, GxEPD_RED); display.drawCircle(display.width()/2, display.height()/2, tbw*15/10/2+i, GxEPD_BLACK);
} }
}
while (display.nextPage());
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.setRotation(0);
display.setFullWindow(); display.setFullWindow();
display.firstPage(); display.firstPage();
do { do {
cb(&display); cb(&display);
} while(display.nextPage()); } while(display.nextPage());
display.hibernate(); display.hibernate();
} }
#if 0
// find minimum and maximum values in a vector. NaN is ignored. // 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) 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 // fallback values: will be used if the vector is empty or only consists of NaNs
*min = 0; *min = 0;
*max = 1; *max = 1;
for(auto &v: vec) { for(auto &v: vec) {
if(isnan(v)) { if(isnan(v)) {
continue; continue;
} }
if(first) { if(first) {
*min = *max = v; *min = *max = v;
first = false; first = false;
continue; continue;
} }
if(v < *min) { if(v < *min) {
*min = v; *min = v;
} }
if(v > *max) { if(v > *max) {
*max = v; *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) 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 trange = timeseries->tend - timeseries->tstart;
uint32_t tstep = timeseries->interval; uint32_t tstep = timeseries->interval;
if(trange == 0) { if(trange == 0) {
trange = 3600; trange = 3600;
} }
float vmin, vmax; float vmin, vmax;
minmax(timeseries->data, &vmin, &vmax); minmax(timeseries->data, &vmin, &vmax);
float vrange = vmax - vmin; float vrange = vmax - vmin;
Serial.print(F("Value range: ")); Serial.print(F("Value range: "));
Serial.print(vrange); Serial.print(vrange);
Serial.print(F(" Min: ")); Serial.print(F(" Min: "));
Serial.print(vmin); Serial.print(vmin);
Serial.print(F(" Max: ")); Serial.print(F(" Max: "));
Serial.println(vmax); Serial.println(vmax);
if(vrange == 0.0f) { if(vrange == 0.0f) {
vrange = 1.0f; vrange = 1.0f;
} }
// add some margin // add some margin
vmax += vrange * 0.05f; vmax += vrange * 0.05f;
vmin -= vrange * 0.05f; vmin -= vrange * 0.05f;
vrange = vmax - vmin; vrange = vmax - vmin;
bool firstpoint = true; bool firstpoint = true;
uint16_t last_x; uint16_t last_x;
uint16_t last_y; uint16_t last_y;
// set font for axis labels // set font for axis labels
display.setFont(&Org_01); display.setFont(&Org_01);
display.setTextColor(GxEPD_BLACK); display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby; uint16_t tbw, tbh; int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds("20:48", 0, 0, &tbx, &tby, &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 // actual graph area
uint16_t plot_x = x + 150; uint16_t plot_x = x + tbw;
uint16_t plot_y = y; uint16_t plot_y = y;
uint16_t plot_w = w - 150; uint16_t plot_w = w - tbw;
uint16_t plot_h = h - label_height - 5; uint16_t plot_h = h - label_height - 5;
// determine vertical tick interval // determine vertical tick interval
uint16_t max_vert_tick_count = plot_h / (label_height + 5); uint16_t max_vert_tick_count = plot_h / (label_height + 5);
float min_vert_tick_step = vrange / (float)max_vert_tick_count; 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 scale = 1e3f;
float accepted_scale = scale; float accepted_scale = scale;
bool scale_found = false; bool scale_found = false;
while((scale > 1e-3f) && !scale_found) { while((scale > 1e-3f) && !scale_found) {
float tmp_scale; float tmp_scale;
for(auto factor: TEST_FACTORS) { for(auto factor: TEST_FACTORS) {
tmp_scale = scale * factor; tmp_scale = scale * factor;
if(tmp_scale < min_vert_tick_step) { if(tmp_scale < min_vert_tick_step) {
scale_found = true; scale_found = true;
break; break;
} }
accepted_scale = tmp_scale; accepted_scale = tmp_scale;
} }
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 // draw vertical grid lines
for(uint64_t reltime = ((timeseries->tstart / time_tick_interval) + 1) * time_tick_interval - timeseries->tstart; for(uint64_t reltime = ((timeseries->tstart / time_tick_interval) + 1) * time_tick_interval - timeseries->tstart;
reltime < trange; reltime < trange;
reltime += time_tick_interval) { reltime += time_tick_interval) {
uint16_t px = plot_x + plot_w * reltime / trange; uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y; uint16_t py = plot_y;
uint16_t ph = plot_h + 2; 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; time_t t = reltime + timeseries->tstart;
tm *gmt = gmtime(&t); tm *gmt = gmtime(&t);
if(t % 86400 == 0) { if(t % 86400 == 0) {
strftime(s, sizeof(s), "%d.%m.", gmt); strftime(s, sizeof(s), "%d.%m.", gmt);
} else { } else {
strftime(s, sizeof(s), "%H:%M", gmt); 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.setCursor(px - tbw/2, y+h + tby);
display.print(s); display.print(s);
} }
// draw horizontal grid lines // draw horizontal grid lines
for(float tick_val = (float)((int)(vmin / accepted_scale) + 1) * accepted_scale; for(float tick_val = (float)((int)(vmin / accepted_scale) + 1) * accepted_scale;
tick_val < vmax; tick_val < vmax;
tick_val += accepted_scale) { tick_val += accepted_scale) {
uint16_t py = plot_y - (float)plot_h * (tick_val - vmax) / vrange; uint16_t py = plot_y - (float)plot_h * (tick_val - vmax) / vrange;
uint16_t px = plot_x - 3; uint16_t px = plot_x - 3;
uint16_t pw = plot_w + 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.setCursor(px - tbw - 2, py + tby + tbh);
display.print(s); display.print(s);
} }
size_t i = 0; size_t i = 0;
for(uint32_t reltime = 0; reltime < trange; reltime += tstep) { for(uint32_t reltime = 0; reltime < trange; reltime += tstep) {
if(i >= timeseries->data.size()) { if(i >= timeseries->data.size()) {
Serial.println(F("Index out of range! Time range does not match data points.")); Serial.println(F("Index out of range! Time range does not match data points."));
break; break;
} }
/*Serial.print(F("Processing data point at dt=")); /*Serial.print(F("Processing data point at dt="));
Serial.print(reltime); Serial.print(reltime);
Serial.print(F(""));*/ Serial.print(F(""));*/
float val = timeseries->data[i++]; float val = timeseries->data[i++];
if(isnan(val)) { if(isnan(val)) {
//Serial.println(F("Skipping NaN!")); //Serial.println(F("Skipping NaN!"));
firstpoint = true; firstpoint = true;
continue; continue;
} }
uint16_t px = plot_x + plot_w * reltime / trange; uint16_t px = plot_x + plot_w * reltime / trange;
uint16_t py = plot_y - (float)plot_h * (val - vmax) / vrange; uint16_t py = plot_y - (float)plot_h * (val - vmax) / vrange;
if(!firstpoint) { if(!firstpoint) {
/*Serial.print(F("Line from (")); /*Serial.print(F("Line from ("));
Serial.print(last_x); Serial.print(last_x);
Serial.print(F(",")); Serial.print(F(","));
Serial.print(last_y); Serial.print(last_y);
Serial.print(F(") to (")); Serial.print(F(") to ("));
Serial.print(px); Serial.print(px);
Serial.print(F(",")); Serial.print(F(","));
Serial.print(py); Serial.print(py);
Serial.println(F(")."));*/ 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 // draw it again, 1px below, to give it more weight
display.drawLine(last_x, last_y+1, px, py+1, GxEPD_RED); display.drawLine(last_x, last_y+1, px, py+1, GxEPD_BLACK);
} else { } else {
//Serial.println(F("First point, not drawn.")); //Serial.println(F("First point, not drawn."));
firstpoint = false; firstpoint = false;
} }
last_x = px; last_x = px;
last_y = py; 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);
} }
#endif

View file

@ -1,12 +1,26 @@
#include <Arduino.h> #include <Arduino.h>
#include <Adafruit_SCD30.h> #include <Adafruit_SCD30.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <epaper.h> #include <epaper.h>
Adafruit_SCD30 scd30; 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) 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! // Try to initialize!
if (!scd30.begin()) { if (!scd30.begin()) {
Serial.println("scd30: Failed to find SCD30 chip"); Serial.println("scd30: Failed to find SCD30 chip");
@ -14,6 +28,7 @@ void initSCD30(void)
} }
Serial.println("scd30: SCD30 Found!"); Serial.println("scd30: SCD30 Found!");
// TODO: check what this actually resets. Maybe also the calibration?
scd30.reset(); scd30.reset();
if (!scd30.setMeasurementInterval(15)){ // seconds if (!scd30.setMeasurementInterval(15)){ // seconds
@ -24,6 +39,32 @@ void initSCD30(void)
Serial.print(scd30.getMeasurementInterval()); Serial.print(scd30.getMeasurementInterval());
Serial.println(" seconds"); 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()) { if(!scd30.startContinuousMeasurement()) {
Serial.println("scd30: Could not start continuous measurement"); Serial.println("scd30: Could not start continuous measurement");
} }
@ -32,20 +73,113 @@ void initSCD30(void)
void setup(void) void setup(void)
{ {
Serial.begin(115200);
Serial.println("Hello World!");
delay(3000);
initSCD30(); initSCD30();
epaper_init(); epaper_init();
epaper_test(); 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) 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.dataReady()) {
if (!scd30.read()) { if (!scd30.read()) {
Serial.println("Error reading sensor data"); Serial.println("scd30: Error reading sensor data");
return; 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("CO2: ");
Serial.print(scd30.CO2, 3); Serial.print(scd30.CO2, 3);
Serial.println(" ppm"); Serial.println(" ppm");
@ -56,6 +190,24 @@ void loop(void)
Serial.print(scd30.relative_humidity, 2); Serial.print(scd30.relative_humidity, 2);
Serial.println(" %"); Serial.println(" %");
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); 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;
}