Skip to Content

Tracciatore di Temperatura su Display e-Paper

Tracciatore di Temperatura su Display e-Paper

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.

Plotting temperature in cartesian coordinate system
Tracciare la temperatura in un sistema di coordinate cartesiane

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.

Plotting temperature in polar coordinate system
Tracciare la temperatura in un sistema di coordinate polari

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.

Screen shot of Polar Plotter
Screenshot del Plotter Polare

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″

BME280

Sensore BME280

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cavo dati USB

Dupont wire set

Set di fili Dupont

Half_breadboard56a

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.

BME280 breakout board
Breakout board BME280

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.

Fronte e retro del modulo display e-Paper da 4,2″

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.

Retro del modulo display con interfaccia SPI e controller

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.

Connecting 4.2" e-Paper to ESP32 via SPI
Collegamento e-Paper da 4,2″ a ESP32 via 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-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

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:

Adafruit_GFX and GxEPD2 libraries in Library Manager
Librerie Adafruit_GFX e GxEPD2 nel Library Manager

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:

Adafruit_BME280 library in Library Manager
Libreria Adafruit_BME280 nel Library Manager

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:

Test output on 4.2" e-Paper display
Output di test su display e-Paper da 4,2″

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:

Connecting the BME280 to ESP32
Collegamento del BME280 all’ESP32

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.

Output of BME280 Sensor on Serial Monitor
Output del sensore BME280 sul Monitor Seriale

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à.

Elements of Polar Plotter Display
Elementi del display del Plotter Polare

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 (epaper 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():

Polar axes with labels on e-Paper display
Assi polari con etichette sul display e-Paper

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.

Relationship between Polar and Cartesian Coordinates
Relazione tra coordinate polari e 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:

Time arc on e-Paper display
Arco del tempo sul display e-Paper

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.

Clock hour labels on e-Paper display
Etichette delle ore sul display e-Paper

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.

Polar Plot of temperature data
Grafico polare dei dati di temperatura

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:

Dati BME280 sul display e-Paper

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ì:

Ora/Data sul display e-Paper

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:

Plotter completo sul display e-Paper

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 : )