294 lines
7.1 KiB
C++
294 lines
7.1 KiB
C++
#include <algorithm>
|
||
|
||
#include <Adafruit_GFX.h>
|
||
#include <Fonts/FreeMonoBold9pt7b.h>
|
||
#include <Font_din1451e.h>
|
||
#include <Fonts/Org_01.h>
|
||
|
||
#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));
|
||
|
||
void epaper_init(void)
|
||
{
|
||
display.init();
|
||
}
|
||
|
||
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());
|
||
|
||
Serial.println(F("Init done. Display going to powersave…"));
|
||
|
||
display.hibernate();
|
||
}
|
||
|
||
void epaper_draw_and_hibernate(epaper_callback cb)
|
||
{
|
||
display.setRotation(0);
|
||
display.setFullWindow();
|
||
display.firstPage();
|
||
|
||
do {
|
||
cb(&display);
|
||
} while(display.nextPage());
|
||
|
||
display.hibernate();
|
||
}
|
||
|
||
#if 0
|
||
// find minimum and maximum values in a vector. NaN is ignored.
|
||
template<typename T>
|
||
void minmax(const std::vector<T> &vec, T *min, T *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 &v: vec) {
|
||
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;
|
||
|
||
if(trange == 0) {
|
||
trange = 3600;
|
||
}
|
||
|
||
float vmin, vmax;
|
||
minmax(timeseries->data, &vmin, &vmax);
|
||
|
||
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);
|
||
|
||
if(vrange == 0.0f) {
|
||
vrange = 1.0f;
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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;
|
||
|
||
// 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;
|
||
|
||
// 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};
|
||
|
||
float scale = 1e3f;
|
||
float accepted_scale = scale;
|
||
bool scale_found = false;
|
||
|
||
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;
|
||
}
|
||
|
||
/* Start drawing */
|
||
|
||
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;
|
||
|
||
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);
|
||
}
|
||
|
||
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||
|
||
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) {
|
||
|
||
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);
|
||
|
||
snprintf(s, sizeof(s), timeseries->format, tick_val);
|
||
|
||
display.getTextBounds(s, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||
|
||
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;
|
||
}
|
||
|
||
/*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!"));
|
||
|
||
firstpoint = true;
|
||
continue;
|
||
}
|
||
|
||
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(")."));*/
|
||
|
||
display.drawLine(last_x, last_y, px, py, GxEPD_RED);
|
||
|
||
// 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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
#endif
|