First steps towards history plotting
This commit is contained in:
parent
8d5b1a425b
commit
1197e083f4
|
@ -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
|
||||
|
||||
|
|
|
@ -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
16
include/timeseries.h
Normal 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);
|
402
src/epaper.cpp
402
src/epaper.cpp
|
@ -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
|
||||
|
|
154
src/main.cpp
154
src/main.cpp
|
@ -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
18
src/timeseries.cpp
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue