In questo tutorial costruiremo un Plotter di Temperatura su un display e-Paper da 4,2″ utilizzando un ESP32 e il sensore BME280.
Un modo comune per visualizzare i dati di temperatura è tramite un grafico a linee in un cartesian coordinate system, dove il tempo è sull’asse x e la temperatura o altri dati ambientali sull’asse y. Vedi la figura di esempio qui sotto, dove la temperatura (linea rossa) varia tra 15° e 25° e il tempo tra l’una e le dodici.

Tracciare la temperatura in coordinate cartesiane ha il vantaggio che i cambiamenti di temperatura sono facili da vedere, ma ignora il fatto che il tempo è ciclico, ad esempio le 24 ore di un giorno si ripetono. In questo caso un polar coordinate system è spesso una scelta migliore. Vedi il grafico di esempio qui sotto che mostra la temperatura intorno a un cerchio o anello con le ore dell’orologio segnate.

Poiché l’asse del tempo segue le ore dell’orologio, è un po’ più facile vedere quale fosse la temperatura a un certo orario. Ma soprattutto, il grafico è continuo, cioè le 12:00 e le 11:59 sono vicine tra loro. Possiamo tracciare diversi giorni senza dover scorrere il grafico, per esempio.
In questo tutorial imparerai come costruire un Plotter Polare per i dati di temperatura. Il Plotter mostrerà la temperatura attuale, l’umidità e la pressione dell’aria oltre a data e ora. Lo screenshot qui sotto mostra come sarà il Plotter completato.

Nota che ho evidenziato la curva della temperatura tracciata in rosso. Poiché useremo un display e-Paper in scala di grigi, non ci sarà colore. Questo ci porta ai componenti necessari.
Componenti necessari
Sto usando l’ESP32 lite come microprocessore, perché è economico e ha un’interfaccia di ricarica batteria, che permette di far funzionare il plotter con una batteria LiPo. Qualsiasi altro ESP32 o ESP8266 con memoria sufficiente andrà bene, ma preferibilmente scegli uno con interfaccia di ricarica batteria.

Display e-Paper da 4,2″

Sensore BME280

ESP32 lite

Cavo dati USB

Set di fili Dupont

Breadboard
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Sensore BME280
Useremo il BME280 sensor per misurare i dati di temperatura per il nostro Plotter di Temperatura. Nota però che il BME280 permette anche di misurare umidità e pressione dell’aria, che potremmo anche tracciare.
Un’altra caratteristica del BME280 è la modalità sleep a basso consumo, dove consuma solo 0,1µA (3,6 μA in modalità normale). In combinazione con un e-Paper questo crea un setup a basso consumo che può funzionare a batteria.
Il sensore BME280 è molto piccolo e solitamente viene fornito su una breakout board con interfaccia I2C; vedi l’immagine sotto. L’interfaccia I2C lo rende molto facile da collegare e usare.

Il sensore può misurare pressione da 300 hPa a 1100 hPa, temperatura da -40°C a +85°C e umidità da 0% a 100%. Per maggiori informazioni sul BME280 e le sue applicazioni dai un’occhiata ai How To Use BME280 Pressure Sensor With Arduino e ai Weather Station on e-Paper Display tutorial.
Display e-Paper da 4,2″
Il display e-Paper usato in questo progetto è un modulo da 4,2″, con risoluzione 400×300 pixel, 4 livelli di grigio, tempo di refresh parziale di 0,4 secondi e un controller integrato con interfaccia SPI.

Nota che il modulo ha un piccolo jumper/interruttore sul retro per passare da SPI a 4 fili a SPI a 3 fili. Useremo il default SPI a 4 fili, quindi non dovresti dover cambiare nulla.

Il modulo display funziona a 3,3V o 5V, ha una corrente di sleep molto bassa di 0,01µA e consuma solo circa 26,4mW durante il refresh. Per maggiori informazioni sugli e-Paper in generale, dai un’occhiata ai seguenti due tutorial: Interfacing Arduino To An E-ink Display e Partial Refresh of e-Paper Display.
Collegamento e test dell’e-Paper
Per prima cosa colleghiamo e testiamo il funzionamento dell’e-Paper. L’immagine seguente mostra il cablaggio completo tra ESP32 e display per alimentazione e interfaccia SPI.

Qui sotto una tabella con tutte le connessioni per comodità. Nota che puoi alimentare il display con 3,3V o 5V ma l’ESP32-lite ha solo uscita a 3,3V e le linee dati SPI devono essere a 3,3V!
| Display e-Paper | ESP32 lite |
|---|---|
| CS/SS | 5 |
| SCL/SCK | 18 |
| SDA/DIN/MOSI | 23 |
| BUSY | 15 |
| RES/RST | 2 |
| DC | 0 |
| VCC | 3.3V |
| GND | G |
Installare la libreria GxEPD2
Prima di poter disegnare o scrivere sul display e-Paper dobbiamo installare due librerie. La Adafruit_GFX graphics library, che fornisce un set comune di primitive grafiche (testo, punti, linee, cerchi, ecc.). E la GxEPD2 library, che fornisce il driver grafico per il display e-Paper.
Installa queste librerie nel modo usuale. Dopo l’installazione dovrebbero apparire nel Library Manager come segue:

Installare la libreria Adafruit_BME280
Poiché useremo il sensore BME280 per misurare temperatura, umidità e pressione, dobbiamo anche installare la Adafruit_BME280 library. Installala come al solito e dovrebbe comparire nel Library Manager come segue:

Testare il display e-Paper
Una volta installate le librerie, ti consiglio di eseguire il seguente codice di test per assicurarti che il display funzioni.
#include "GxEPD2_BW.h"
//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
void setup() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
epd.setTextColor(GxEPD_BLACK);
epd.setTextSize(2);
epd.setFullWindow();
epd.fillScreen(GxEPD_WHITE);
epd.setCursor(90, 190);
epd.print("Makerguides");
epd.display();
epd.hibernate();
}
void loop() {}
Il codice stampa il testo “Makerguides” e dopo un po’ di sfarfallio del display (refresh completo) dovresti vederlo sul display come mostrato sotto:

Se non funziona, c’è qualcosa che non va. Probabilmente il display non è cablato correttamente o è stato scelto il driver sbagliato. La riga critica del codice è questa, dove specifichiamo il driver per il display da 4,2″:
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
La libreria Readme GxEPD2 elenca tutti i display supportati e puoi trovare i dettagli nei file header, ad esempio GxEPD2.h. Trova il driver specifico per il tuo display. Potrebbe richiedere qualche tentativo.
Collegare e testare il sensore di temperatura
Grazie all’interfaccia I2C collegare il sensore BME280 all’ESP32 è molto semplice. Collega SCL del sensore al pin 25 e SDA al pin 33 dell’ESP32. Poi collega a terra i pin GND e il pin 3.3V dell’ESP32 al VIN del sensore BME280. Vedi lo schema completo con i collegamenti e-Paper e BME280 qui sotto:

Ora assicuriamoci che il sensore BME280 funzioni, mentre il display e-Paper è collegato.
Test del sensore BME280
Il codice di test seguente usa il sensore BME280 per misurare temperatura, pressione dell’aria e umidità relativa e stampa i valori misurati sul Monitor Seriale.
#include "Adafruit_BME280.h"
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
Wire.begin(33, 25);
bme.begin(0x76, &Wire);
}
void loop() {
Serial.print("Temperature in degC = ");
Serial.println(bme.readTemperature());
Serial.print("Pressure in hPa = ");
Serial.println(bme.readPressure() / 100.0F);
Serial.print("Humidity in %RH = ");
Serial.println(bme.readHumidity());
Serial.println();
delay(5000);
}
Nota che stiamo usando I2C software e dobbiamo specificare i pin a cui sono collegati SDA e SCL chiamando Wire.begin(33, 25). Il BME280 tipicamente usa l’indirizzo I2C 0x76. Se non riesci a leggere dati, forse il tuo BME280 ha un indirizzo diverso e devi cambiare bme.begin(0x76, &Wire) di conseguenza.
Se tutto funziona correttamente, dovresti vedere dati simili a quelli seguenti stampati sul Monitor Seriale. Assicurati di impostare la velocità di trasmissione a 115200.

Con questo pronto, ora possiamo scrivere il codice per il Plotter di Temperatura.
Codice per un Plotter di Temperatura su e-Paper
In questa sezione implementeremo il codice per il Plotter. Lo screenshot seguente del display del plotter mostra gli elementi e le funzionalità che avrà.

Sull’anello esterno tracceremo la temperatura nel tempo (linea rossa). L’anello esterno mostra anche le etichette delle ore e l’intervallo di temperatura. I piccoli punti neri lungo l’anello esterno (arco del tempo) indicano l’ora corrente – nell’immagine sopra sono circa 15 minuti dopo le 11.
Al centro dell’anello stamperemo la temperatura attuale (18,3°), l’umidità relativa attuale (47%) e la pressione dell’aria (1005mb). Sotto stampiamo anche l’ora corrente (Lu 11:14) e la data (09/09/24).
Qui sotto trovi il codice completo per il Plotter di Temperatura. Dagli una rapida occhiata per avere un’idea generale, poi ne discuteremo i dettagli.
#include "WiFi.h"
#include "esp_sntp.h"
#include "Adafruit_BME280.h"
#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const float TMAX = 25.0;
const float TMID = 20.0;
const float TMIN = 15.0;
const int RMAX = R - 5;
const int RMID = R - 25;
const int RMIN = R - 45;
//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Adafruit_BME280 bme;
GFXcanvas1 canvas(W, H);
void initDisplay() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
canvas.setTextColor(BLACK);
canvas.setTextSize(1);
canvas.setFont();
canvas.fillScreen(WHITE);
drawDottedCircle(CW, CH, RMAX, 2);
drawDottedCircle(CW, CH, RMID, 8);
drawDottedCircle(CW, CH, RMIN, 2);
printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
void initSensor() {
Wire.begin(33, 25); // sda, scl, Software I2C
bme.begin(0x76, &Wire);
}
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
canvas.setCursor(x - w / 2, y - h / 2);
canvas.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
void drawDottedCircle(float x, float y, float r, float s) {
int n = int(TWO_PI / (s / r));
for (int i = 0; i < n; i++) {
float alpha = TWO_PI * i / n;
int cx, cy;
polar2cart(x, y, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
}
int curSeconds() {
static struct tm t;
getLocalTime(&t);
return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}
float sec2angle(int sec) {
return TWO_PI * sec / (12 * 60 * 60);
}
void drawTimeArc() {
int cx, cy;
float secs = curSeconds();
float alpha = sec2angle(secs);
int n = round(6 * 12 * alpha / TWO_PI);
for (int i = 0; i < n; i++) {
float a = alpha * i / n;
polar2cart(CW, CH, RMIN - 7, a, cx, cy);
canvas.fillCircle(cx, cy, 2, BLACK);
}
}
void drawClockLabels() {
int cx, cy;
canvas.setFont();
for (int h = 1; h <= 12; h++) {
float alpha = sec2angle(h * 60 * 60);
polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
}
}
void plotTemp() {
float temp = bme.readTemperature();
temp = constrain(temp, TMIN, TMAX);
float c = (RMAX - RMIN) / (TMAX - TMIN);
float r = RMIN + (temp - TMIN) * c;
int cx, cy;
float alpha = sec2angle(curSeconds());
polar2cart(CW, CH, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
void printBMEData() {
float temp = bme.readTemperature();
canvas.setFont(&FreeSansBold24pt7b);
printfAt(CW, CH + 5, "%.1f", temp);
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100;
canvas.setFont();
printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}
void printTimeDate() {
static struct tm t;
getLocalTime(&t);
canvas.setFont(&FreeSansBold9pt7b);
printfAt(CW, CH + 35, "%s %2d:%02d",
DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
printfAt(CW, CH + 55, "%02d/%02d/%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}
void drawAll() {
canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
drawClockLabels();
drawTimeArc();
printBMEData();
printTimeDate();
plotTemp();
}
void drawCanvas() {
epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}
void partialRefresh(const void* pv) {
drawAll();
epd.setPartialWindow(0, 0, W, H);
drawCanvas();
}
void fullRefresh(const void* pv) {
epd.setFullWindow();
drawCanvas();
}
void setup() {
initSensor();
initDisplay();
setTimezone();
}
void loop() {
static uint16_t iter = 0;
if (iter % 50 == 0)
syncTime();
if (iter % 120 == 0)
epd.drawPaged(fullRefresh, 0);
iter = (iter + 1) % 1000;
epd.drawPaged(partialRefresh, 0);
epd.hibernate();
delay(30 * 1000);
}
Librerie
Iniziamo includendo le librerie WiFi.h e esp_snt.h, necessarie per sincronizzare l’orologio del plotter con un server SNTP via Wi-Fi.
#include "WiFi.h" #include "esp_sntp.h"
Questo ci permette di riportare la temperatura esattamente al momento in cui è stata misurata. Dai un’occhiata al tutorial How to synchronize ESP32 clock with SNTP server per maggiori dettagli.
Poi includiamo la libreria Adafruit_BME280 library necessaria per leggere temperatura, umidità e pressione dal sensore BME280.
#include "Adafruit_BME280.h"
Infine includiamo il file header GxEPD2_BW.h per il display e-Paper in bianco e nero (BW). Se hai un display a 3 colori includeresti GxEPD2_3C.h, o GxEPD2_4C.h per un display a 4 colori, e GxEPD2_7C.h per un display a 7 colori.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans9pt7b.h" #include "Fonts/FreeSansBold9pt7b.h" #include "Fonts/FreeSansBold24pt7b.h"
Includiamo anche tre file di font, che usiamo per mostrare le etichette del plotter, la temperatura, la data e altre informazioni. Puoi trovare una panoramica dei AdaFruit GFX fonts qui.
Costanti
Definiamo alcune costanti. Soprattutto, dovrai definire SSID e PASSWORD per il tuo WiFi, e la TIMEZONE in cui vivi.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
La specifica del fuso orario sopra “AEST-10AEDT,M10.1.0,M4.1.0/3” è per l’Australia, corrispondente all’Australian Eastern Standard Time (AEST) con aggiustamenti per l’ora legale.
Le parti di questa definizione di fuso orario sono le seguenti
- AEST: Australian Eastern Standard Time
- -10: offset UTC di 10 ore avanti rispetto al Tempo Coordinato Universale (UTC)
- AEDT: Australian Eastern Daylight Time
- M10.1.0: transizione all’ora legale la prima domenica di ottobre
- M4.1.0/3: ritorno all’ora standard la prima domenica di aprile, con differenza di 3 ore da UTC.
Per trovare la definizione del tuo fuso orario dai un’occhiata al Posix Timezones Database.
Poi abbiamo costanti per le dimensioni (W,H) del display e-Paper, il punto centrale (CW, CH) e il raggio massimo R di un cerchio sul display. Nota che larghezza e altezza sono invertite, poiché ruotiamo il display (setRotation(1)) nella funzione initDisplay.
const int H = GxEPD2_420_GDEY042T81::WIDTH; const int W = GxEPD2_420_GDEY042T81::HEIGHT; const int CW = W / 2; const int CH = H / 2; const int R = min(W, H) / 2;
Per comodità definiamo anche due costanti per il colore nero e bianco, e i nomi dei giorni:
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
Infine definiamo i valori massimo, medio e minimo per la temperatura (TMAX, TMID, TMIN) e i corrispondenti raggi dell’asse polare (RMAX, RMID, RMIN).
const float TMAX = 25.0; const float TMID = 20.0; const float TMIN = 15.0; const int RMAX = R - 5; const int RMID = R - 25; const int RMIN = R - 45;
Oggetti
Creiamo ora gli oggetti per il display e-Paper, il sensore BME280 e una canvas.
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15)); Adafruit_BME280 bme; GFXcanvas1 canvas(W, H);
L’oggetto epd rappresenta il display (e–paper display). Questa definizione di oggetto deve corrispondere al tipo di display e-Paper che hai. Qui abbiamo un display da 4,2 pollici (= _420). Vedi la libreria Readme for GxEPD2 e il file GxEPD2.h per i display supportati.
La canvas è un buffer su cui possiamo disegnare in background. Serve per tracciare la curva della temperatura e per il refresh parziale. Per maggiori dettagli dai un’occhiata al tutorial Partial Refresh of e-Paper Display.
Funzione initDisplay
La funzione initDisplay() inizializza il display e-Paper, imposta colore testo, dimensione e font per la canvas e la riempie di bianco. Poi disegna gli assi polari per temperatura minima, media e massima, dopo di che stampa le etichette della temperatura sugli assi.
void initDisplay() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
canvas.setTextColor(BLACK);
canvas.setTextSize(1);
canvas.setFont();
canvas.fillScreen(WHITE);
drawDottedCircle(CW, CH, RMAX, 2);
drawDottedCircle(CW, CH, RMID, 8);
drawDottedCircle(CW, CH, RMIN, 2);
printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}
Lo screenshot seguente mostra come appare dopo la chiamata a initDisplay():

Funzione setTimezone
La funzione setTimezone() imposta il FUSO ORARIO per l’orologio interno dell’ESP32. Come detto prima, puoi trovare altre definizioni di fuso orario nel Posix Timezones Database.
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
Funzione syncTime
La funzione syncTime crea una connessione WiFi e sincronizza l’orologio interno dell’ESP32 con il server SNTP “pool.ntp.org” chiamando configTzTime().
Puoi specificare altri o più server SNTP a cui connetterti. Dai un’occhiata al tutorial How to synchronize ESP32 clock with SNTP server per maggiori informazioni.
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
Funzione initSensor
La funzione initSensor() inizializza il sensore BME280. Assicurati che SDA e SCL siano collegati ai pin 33 e 25 dell’ESP32, come specificato. Assicurati anche che l’indirizzo I2C 0x76 sia corretto per il tuo sensore.
void initSensor() {
Wire.begin(33, 25); // SDA, SCL
bme.begin(0x76, &Wire);
}
Funzione polar2cart
La funzione polar2cart() converte coordinate polari date dal punto centrale x, y, raggio r e angolo alpha in coordinate cartesiane che vengono restituite in cx e cy.
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
L’immagine seguente illustra come la funzione polar2cart() converte coordinate polari in cartesiane.

Abbiamo bisogno di questa funzione, per esempio, per convertire le ore dell’orologio su un cerchio nelle coordinate cartesiane x, y usate dal display e-Paper.
Funzioni printAt e printfAt
Le funzioni printAt() e printfAt() stampano testo alle coordinate x, y sul display e-Paper.
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
canvas.setCursor(x - w / 2, y - h / 2);
canvas.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
La funzione printAt() permette di stampare solo testo semplice, mentre la printfAt() funziona come printf e permette di fornire un formato per stampare testo e dati, ad esempio printfAt(x, y, "%02d-%02d-%02d", m, h, y);
Funzione drawDottedCircle
La funzione drawDottedCircle() disegna semplicemente un cerchio tratteggiato con centro in x, y, raggio r e spazio s tra i punti. La funzione è usata per disegnare gli assi polari lungo cui viene tracciata la temperatura.
void drawDottedCircle(float x, float y, float r, float s) {
int n = int(TWO_PI / (s / r));
for (int i = 0; i < n; i++) {
float alpha = TWO_PI * i / n;
int cx, cy;
polar2cart(x, y, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
}
Funzione curSeconds
La funzione curSeconds() ottiene l’ora corrente e la converte in secondi. Per esempio, 8:50:10 viene convertito in 8 * 60 * 60 +50 * 60 +10 = 31810 secondi.
int curSeconds() {
static struct tm t;
getLocalTime(&t);
return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}
Funzione sec2angle
La funzione sec2angle() converte il tempo fornito in secondi in un angolo sull’orologio.
float sec2angle(int sec) {
return TWO_PI * sec / (12 * 60 * 60);
}
Funzione drawTimeArc
La funzione drawTimeArc() disegna i punti neri che indicano l’ora corrente sull’anello (arco del tempo). Come vedi usa le funzioni curSeconds() e sec2angle() per convertire l’ora corrente in secondi e in un angolo che determina la fine dell’arco del tempo.
void drawTimeArc() {
int cx, cy;
float secs = curSeconds();
float alpha = sec2angle(secs);
int n = round(6 * 12 * alpha / TWO_PI);
for (int i = 0; i < n; i++) {
float a = alpha * i / n;
polar2cart(CW, CH, RMIN - 7, a, cx, cy);
canvas.fillCircle(cx, cy, 2, BLACK);
}
}
L’immagine seguente mostra la fine dell’arco del tempo:

Funzione drawClockLabels
La funzione drawClockLabels() disegna semplicemente le etichette delle ore sull’anello.
void drawClockLabels() {
int cx, cy;
canvas.setFont();
for (int h = 1; h <= 12; h++) {
float alpha = sec2angle(h * 60 * 60);
polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
}
}
L’immagine seguente di una sezione del display mostra le etichette delle ore.

Funzione plotTemp
La funzione plotTemp() traccia la temperatura nel tempo in coordinate polari. Prima legge la temperatura attuale, la limita all’intervallo TMIN a TMAX, poi la scala a un raggio r. Successivamente converte l’ora corrente in un angolo alpha, e la temperatura viene tracciata come un singolo pixel alle coordinate polari alpha, r.
void plotTemp() {
float temp = bme.readTemperature();
temp = constrain(temp, TMIN, TMAX);
float c = (RMAX - RMIN) / (TMAX - TMIN);
float r = RMIN + (temp - TMIN) * c;
int cx, cy;
float alpha = sec2angle(curSeconds());
polar2cart(CW, CH, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
Nota che tutto il disegno avviene sulla canvas e quindi non è immediatamente visibile. L’immagine seguente mostra come appare l’output della funzione quando viene mostrato.

Nota che la traccia della temperatura è evidenziata in rosso ma in realtà appare come una linea nera.
Funzione printBMEData
La funzione printBMEData() stampa la temperatura attuale, l’umidità relativa e la pressione dell’aria al centro del grafico.
void printBMEData() {
float temp = bme.readTemperature();
canvas.setFont(&FreeSansBold24pt7b);
printfAt(CW, CH + 5, "%.1f", temp);
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100;
canvas.setFont();
printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}
Ecco un’immagine di esempio dell’output:

Funzione printTimeDate
La funzione printTimeDate() stampa il nome del giorno corrente, l’ora e la data al centro del grafico.
void printTimeDate() {
static struct tm t;
getLocalTime(&t);
canvas.setFont(&FreeSansBold9pt7b);
printfAt(CW, CH + 35, "%s %2d:%02d",
DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
printfAt(CW, CH + 55, "%02d/%02d/%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}
L’output appare così:

Funzione drawAll
La funzione drawAll() chiama tutte le funzioni necessarie per disegnare il display completo del plotter. Questo include le etichette dell’orologio, l’arco del tempo, i dati del sensore, l’ora e il tracciamento della temperatura.
void drawAll() {
canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
drawClockLabels();
drawTimeArc();
printBMEData();
printTimeDate();
plotTemp();
}
Produce il seguente output:

Funzione drawCanvas
La funzione drawCanvas() interpreta la canvas come bitmap e la mostra sull’e-Paper. È in questo momento che ciò che è disegnato sulla canvas diventa visibile sul display.
void drawCanvas() {
epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}
Funzione partialRefresh
La funzione partialRefresh() chiama drawAll() per ridisegnare tutto sulla canvas e poi esegue un refresh parziale dell’e-Paper. Questo è molto più veloce (<0,5 sec) di un refresh completo ma lascia immagini residue.
oid partialRefresh(const void* pv) {
drawAll();
epd.setPartialWindow(0, 0, W, H);
drawCanvas();
}
Per maggiori informazioni dai un’occhiata al tutorial Partial Refresh of e-Paper Display.
Funzione fullRefresh
La funzione fullRefresh() esegue un refresh completo, che è molto più lento del refresh parziale e fa sfarfallare il display e-Paper ma rimuove tutte le immagini residue.
void fullRefresh(const void* pv) {
epd.setFullWindow();
drawCanvas();
}
Funzione setup
La funzione setup() inizializza il sensore BME280 e il display, e imposta il fuso orario.
void setup() {
initSensor();
initDisplay();
setTimezone();
}
Funzione loop
La funzione loop() causa essenzialmente un aggiornamento del display ogni 30 secondi. A seconda del numero di aggiornamenti (iter), viene eseguito un refresh parziale o completo. In particolare, un refresh completo viene eseguito ogni 120 iterazioni, cioè 120*30/60 = 60 minuti.
Allo stesso modo, l’orologio interno dell’ESP32 viene sincronizzato con il server SNTP circa ogni 50*30/60 = 25 minuti.
void loop() {
static uint16_t iter = 0;
if (iter % 50 == 0)
syncTime();
if (iter % 120 == 0)
epd.drawPaged(fullRefresh, 0);
iter = (iter + 1) % 1000;
epd.drawPaged(partialRefresh, 0);
epd.hibernate();
delay(30 * 1000);
}
E con questo si conclude la descrizione del codice.
Conclusioni
In questo tutorial hai imparato a costruire un Plotter Polare che mostra la temperatura (e altri dati) su un display e-Paper da 4,2″ usando un ESP32 e un sensore BME280.
Ci sono molti modi in cui potresti modificare o estendere questo Plotter. Innanzitutto, la risoluzione della temperatura nell’implementazione attuale è piuttosto bassa, poiché l’anello in cui la temperatura è tracciata è abbastanza sottile. Potresti ridurre il testo al centro e rendere l’anello più largo per una migliore risoluzione.
Oltre alla temperatura, sarebbe bello monitorare anche la pressione dell’aria e l’umidità nel tempo. Questo potrebbe essere fatto usando tre canvas (per temperatura, pressione e umidità) e alternandoli, periodicamente o aggiungendo un pulsante al circuito.
Potresti anche monitorare e tracciare la velocità Wi-Fi nel tempo, dato che l’ESP32 è comunque connesso a internet. In generale, qualsiasi parametro che vuoi misurare nell’arco di una giornata (es. light intensity, air quality, dust, …) o altro intervallo temporale periodico è una buona scelta per un Plotter Polare. Gli angoli del display sarebbero anche un buon spazio per mostrare dati di previsione meteo.
Infine, una parola sul deep-sleep. Sarebbe bello mettere l’ESP32 in deep-sleep tra un aggiornamento e l’altro del display, ma non è semplice, perché tutta la memoria, incluso il disegno sulla canvas, viene persa in deep sleep e la canvas è troppo grande per RTC memory. Invece, dovremmo memorizzare la serie temporale della temperatura in un piccolo array che entra nella memoria RTC. Non impossibile, ma un po’ più limitante.
Molte idee con cui giocare. Buon divertimento con il tinkering : )

