En este tutorial construiremos un graficador de temperatura en una pantalla e-Paper de 4.2″ usando un ESP32 y el sensor BME280.
Una forma común de mostrar datos de temperatura es mediante un gráfico de líneas en un cartesian coordinate system, donde el tiempo está en el eje x y la temperatura u otros datos ambientales en el eje y. Mira la figura de ejemplo a continuación, donde la temperatura (línea roja) varía entre 15° y 25° y el tiempo entre la 1 y las 12 en punto.

Graficar la temperatura en coordenadas cartesianas tiene la ventaja de que los cambios de temperatura son fáciles de ver, pero ignora el hecho de que el tiempo es cíclico, por ejemplo, las 24 horas del día se repiten. En este caso, un polar coordinate system suele ser una mejor opción. Mira el gráfico de ejemplo a continuación que muestra la temperatura alrededor de un círculo o anillo con las horas del reloj marcadas.

Dado que el eje del tiempo sigue las horas del reloj, es un poco más fácil ver qué temperatura había a una hora determinada. Pero lo más importante es que el gráfico es continuo, ya que las 12 en punto y las 11:59 están una al lado de la otra. Podemos graficar varios días sin necesidad de desplazar el gráfico, por ejemplo.
En este tutorial aprenderás a construir un graficador polar para datos de temperatura. El graficador mostrará la temperatura actual, humedad y presión atmosférica además de la hora y fecha. La captura de pantalla a continuación muestra cómo se verá el graficador terminado.

Ten en cuenta que he resaltado la curva de temperatura graficada en rojo. Como vamos a usar una pantalla e-Paper en escala de grises, no habrá color. Esto nos lleva a las piezas necesarias.
Piezas necesarias
Estoy usando el ESP32 lite como microprocesador, ya que es barato y tiene una interfaz de carga de batería, lo que permite alimentar el graficador con una batería LiPo. Cualquier otro ESP32 o ESP8266 con suficiente memoria también funcionará, pero preferiblemente consigue uno con interfaz de carga de batería.

Pantalla e-Paper de 4.2″

Sensor BME280

ESP32 lite

Cable de datos USB

Juego de cables Dupont

Protoboard
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.
Sensor BME280
Vamos a usar el BME280 sensor para medir los datos de temperatura para nuestro graficador. Ten en cuenta, sin embargo, que el BME280 también permite medir humedad y presión atmosférica, que también podríamos graficar.
Otra característica del BME280 es que tiene un modo de bajo consumo, modo de suspensión, donde consume solo 0.1µA (3.6 μA en modo normal). En combinación con un e-Paper, esto crea un sistema de bajo consumo que puede funcionar con batería.
El sensor BME280 es pequeño y normalmente viene en una placa breakout con interfaz I2C; mira la imagen a continuación. La interfaz I2C facilita mucho la conexión y uso.

El sensor puede medir presión desde 300 hPa hasta 1100 hPa, temperatura de -40°C a +85°C y humedad de 0% a 100%. Para más información sobre el BME280 y sus aplicaciones, echa un vistazo a los How To Use BME280 Pressure Sensor With Arduino y los Weather Station on e-Paper Display tutoriales.
Pantalla e-Paper de 4.2″
La pantalla e-Paper usada en este proyecto es un módulo de 4.2″, con resolución de 400×300 píxeles, 4 niveles de gris, tiempo de refresco parcial de 0.4 segundos y un controlador integrado con interfaz SPI.

Ten en cuenta que el módulo tiene un pequeño jumper o interruptor en la parte trasera para cambiar de SPI de 4 hilos a SPI de 3 hilos. Usaremos el SPI de 4 hilos por defecto aquí, así que no deberías cambiar nada.

El módulo funciona a 3.3V o 5V, tiene un consumo en reposo muy bajo de 0.01µA y consume solo unos 26.4mW durante el refresco. Para más información sobre pantallas e-Paper en general, consulta los siguientes dos tutoriales: Interfacing Arduino To An E-ink Display y Partial Refresh of e-Paper Display.
Conectar y probar la pantalla e-Paper
Primero, conectemos y probemos la función de la pantalla e-Paper. La siguiente imagen muestra el cableado completo entre el ESP32 y la pantalla para alimentación e interfaz SPI.

A continuación una tabla con todas las conexiones para mayor comodidad. Ten en cuenta que puedes alimentar la pantalla con 3.3V o 5V, pero el ESP32-lite solo tiene salida de 3.3V y las líneas de datos SPI deben ser de 3.3V.
| Pantalla 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 |
Instalar la librería GxEPD2
Antes de poder dibujar o escribir en la pantalla e-Paper necesitamos instalar dos librerías. La Adafruit_GFX de gráficos, que provee un conjunto común de primitivas gráficas (texto, puntos, líneas, círculos, etc.). Y la GxEPD2, que provee el software controlador gráfico para la pantalla e-Paper.
Solo instala estas librerías de la manera habitual. Después de la instalación deberían aparecer en el Library Manager así:

Instalar la librería Adafruit_BME280
Como vamos a usar el sensor BME280 para medir temperatura, humedad y presión atmosférica, también necesitamos instalar la librería Adafruit_BME280. Instálala como de costumbre y debería aparecer en el Library Manager así:

Probar la pantalla e-Paper
Una vez instaladas las librerías, te sugiero ejecutar el siguiente código de prueba para asegurarte de que la pantalla funciona.
#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() {}
El código imprime el texto «Makerguides» y después de un parpadeo de la pantalla (refresco completo) deberías verlo en tu pantalla como se muestra abajo:

Si no es así, algo está mal. Lo más probable es que la pantalla no esté cableada correctamente o que se haya elegido un controlador de pantalla incorrecto. La línea crítica de código es esta, donde especificamos el controlador para la pantalla de 4.2″:
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
La librería Readme GxEPD2 lista todas las pantallas soportadas y puedes encontrar los detalles en los archivos header, por ejemplo GxEPD2.h. Encuentra el controlador específico para tu pantalla. Esto puede requerir prueba y error.
Conectar y probar el sensor de temperatura
Debido a la interfaz I2C, conectar el sensor BME280 al ESP32 es muy sencillo. Solo conecta SCL del sensor al pin 25 y SDA al pin 33 del ESP32. Luego conecta tierra (GND) y el pin de 3.3V del ESP32 al VIN del sensor BME280. Mira el diagrama completo de conexiones, con la pantalla e-Paper y el BME280 a continuación:

Ahora asegurémonos de que el sensor BME280 funciona, mientras la pantalla e-Paper está conectada.
Probar el sensor BME280
El siguiente código de prueba usa el sensor BME280 para medir temperatura, presión atmosférica y humedad relativa, e imprime los valores medidos en el Monitor Serial.
#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);
}
Ten en cuenta que estamos usando I2C por software y debemos especificar los pines a los que están conectadas las líneas SDA y SCL llamando a Wire.begin(33, 25). El BME280 típicamente usa la dirección I2C 0x76. Si no puedes leer datos, tal vez tu BME280 tiene una dirección diferente y debes cambiar bme.begin(0x76, &Wire), en consecuencia.
Si todo funciona bien, deberías ver datos similares a los siguientes impresos en el Monitor Serial. Asegúrate de configurar la velocidad en baudios a 115200.

Con eso listo, ahora podemos escribir el código para el graficador de temperatura.
Código para un graficador de temperatura en e-Paper
En esta sección implementaremos el código para el graficador. La siguiente captura de pantalla del display del graficador muestra los elementos y funcionalidades que tendrá.

En el anillo exterior graficaremos la temperatura a lo largo del tiempo (línea roja). El anillo exterior también muestra las etiquetas de las horas y el rango de temperatura. Los pequeños puntos negros a lo largo del anillo exterior (arco de tiempo) indican la hora actual – en la imagen arriba son unos 15 minutos pasados de las 11 en punto.
En el centro del anillo imprimiremos la temperatura actual (18.3°), la humedad relativa actual (47%) y la presión atmosférica (1005mb). Debajo de eso también imprimimos la hora actual (Lu 11:14) y la fecha (09/09/24).
A continuación encontrarás el código completo para el graficador de temperatura. Échale un vistazo rápido para tener una idea general, luego discutiremos sus detalles.
#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);
}
Librerías
Comenzamos incluyendo las librerías WiFi.h y esp_snt.h, que necesitaremos para sincronizar el reloj del graficador con un servidor de tiempo SNTP por internet vía Wi-Fi.
#include "WiFi.h" #include "esp_sntp.h"
Esto nos permite reportar la temperatura exactamente en el momento en que fue medida. Mira el tutorial How to synchronize ESP32 clock with SNTP server para más detalles.
Luego incluimos la librería Adafruit_BME280 library necesaria para leer datos de temperatura, humedad y presión del sensor BME280.
#include "Adafruit_BME280.h"
Finalmente, incluimos el archivo header GxEPD2_BW.h para la pantalla e-Paper en blanco y negro (BW). Si tienes una pantalla de 3 colores incluirías GxEPD2_3C.h, o GxEPD2_4C.h para una pantalla de 4 colores, y GxEPD2_7C.h para una de 7 colores, en su lugar.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans9pt7b.h" #include "Fonts/FreeSansBold9pt7b.h" #include "Fonts/FreeSansBold24pt7b.h"
También incluimos tres archivos de fuentes que usamos para mostrar las etiquetas del graficador, temperatura, fecha y otra información. Puedes encontrar un resumen de las AdaFruit GFX fonts aquí.
Constantes
Luego definimos algunas constantes. Lo más importante es que definas el SSID y PASSWORD para tu WiFi, y la TIMEZONE en la que vives.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
La especificación de zona horaria anterior «AEST-10AEDT,M10.1.0,M4.1.0/3» es para Australia, que corresponde a la Hora Estándar del Este Australiano (AEST) con ajustes de horario de verano.
Las partes de esta definición de zona horaria son las siguientes
- AEST: Hora Estándar del Este Australiano
- -10: Desfase UTC de 10 horas adelantado respecto al Tiempo Universal Coordinado (UTC)
- AEDT: Hora de Verano del Este Australiano
- M10.1.0: La transición al horario de verano ocurre el primer domingo de octubre
- M4.1.0/3: La transición de vuelta al horario estándar ocurre el primer domingo de abril, con una diferencia de 3 horas respecto a UTC.
Para encontrar las definiciones de tu zona horaria, echa un vistazo a Posix Timezones Database.
Luego tenemos constantes para las dimensiones (W,H) de la pantalla e-Paper, su punto central (CW, CH) y el radio máximo R de un círculo en la pantalla. Ten en cuenta que ancho y alto están intercambiados, ya que rotamos la pantalla (setRotation(1)) en la función 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;
Para mayor comodidad también definimos dos constantes para el color negro y blanco, y los nombres de los días:
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
Finalmente, definimos los valores máximo, medio y mínimo para la temperatura (TMAX, TMID, TMIN) y los radios correspondientes del eje polar (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;
Objetos
Luego creamos objetos para la pantalla e-Paper, el sensor BME280 y un 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);
El objeto epd representa la pantalla (e–paper display). Esta definición de objeto debe coincidir con el tipo de pantalla e-Paper que tienes. Aquí tenemos una pantalla e-Paper de 4.2 pulgadas (= _420). Consulta la librería Readme for GxEPD2 y el archivo GxEPD2.h para las pantallas soportadas.
El canvas es un buffer donde podemos dibujar en segundo plano. Es necesario para graficar la curva de temperatura y el refresco parcial. Para más detalles, mira el tutorial Partial Refresh of e-Paper Display.
Función initDisplay
La función initDisplay() inicializa la pantalla e-Paper, establece el color de texto, tamaño y fuente para el canvas y lo llena de blanco. Luego dibuja los ejes de coordenadas polares para la temperatura mínima, media y máxima, tras lo cual imprime las etiquetas de temperatura en el eje.
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);
}
La siguiente captura muestra cómo se ve después de llamar a initDisplay():

Función setTimezone
La función setTimezone() establece la ZONA HORARIA para el reloj interno del ESP32. Como se mencionó antes, puedes encontrar otras definiciones de zona horaria en Posix Timezones Database.
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
Función syncTime
La función syncTime crea una conexión WiFi y luego sincroniza el reloj interno del ESP32 con el servidor SNTP «pool.ntp.org» llamando a configTzTime().
Puedes especificar otros servidores SNTP, o incluso varios, para conectarte. Mira el tutorial How to synchronize ESP32 clock with SNTP server para más información.
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
Función initSensor
La función initSensor() inicializa el sensor BME280. Asegúrate de que SDA y SCL estén conectados a los pines 33 y 25 del ESP32, como se especifica. También asegúrate de que la dirección I2C 0x76 sea correcta para tu sensor.
void initSensor() {
Wire.begin(33, 25); // SDA, SCL
bme.begin(0x76, &Wire);
}
Función polar2cart
La función polar2cart() convierte coordenadas polares dadas por el punto central x, y, radio r y ángulo alpha a coordenadas cartesianas que se devuelven en cx y 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));
}
La siguiente imagen ilustra cómo la función polar2cart() convierte coordenadas polares a cartesianas.

Necesitamos esta función, por ejemplo, para convertir las horas del reloj en un círculo a las coordenadas cartesianas x, y que usa la pantalla e-Paper.
Funciones printAt y printfAt
Las funciones printAt() y printfAt() imprimen texto en las coordenadas x, y en la pantalla 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 función printAt() permite imprimir solo texto plano, mientras que printfAt() funciona como printf y permite usar un especificador de formato para imprimir texto y datos, por ejemplo printfAt(x, y, "%02d-%02d-%02d", m, h, y);
Función drawDottedCircle
La función drawDottedCircle() dibuja un círculo punteado con centro en x, y, radio r y espacio s entre puntos. La función se usa para dibujar los ejes polares a lo largo de los cuales se grafica 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);
}
}
Función curSeconds
La función curSeconds() obtiene la hora actual y la convierte en segundos. Por ejemplo, 8:50:10 se convierte en 8 * 60 * 60 +50 * 60 +10 = 31810 segundos.
int curSeconds() {
static struct tm t;
getLocalTime(&t);
return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}
Función sec2angle
La función sec2angle() convierte el tiempo dado en segundos en un ángulo en el reloj.
float sec2angle(int sec) {
return TWO_PI * sec / (12 * 60 * 60);
}
Función drawTimeArc
La función drawTimeArc() dibuja los puntos negros que indican la hora actual en el anillo (arco de tiempo). Como ves, usa las funciones curSeconds() y sec2angle() para convertir la hora actual a segundos y a un ángulo que determina el final del arco de tiempo.
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);
}
}
La siguiente imagen muestra el final del arco de tiempo:

Función drawClockLabels
La función drawClockLabels() simplemente dibuja las etiquetas de las horas del reloj en el anillo.
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);
}
}
La siguiente imagen de una sección de la pantalla muestra las etiquetas de las horas del reloj.

Función plotTemp
La función plotTemp() grafica la temperatura a lo largo del tiempo en coordenadas polares. Primero lee la temperatura actual, la limita al rango TMIN a TMAX, y luego la escala a un radio r. Después, la hora actual se convierte en un ángulo alpha, y la temperatura se grafica como un solo píxel en coordenadas polares 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);
}
Ten en cuenta que todo el dibujo ocurre en el canvas y por lo tanto no es visible inmediatamente. La siguiente imagen muestra cómo se ve la salida de la función cuando se muestra.

Ten en cuenta que la gráfica de temperatura está resaltada en rojo, pero en realidad aparece como una línea negra.
Función printBMEData
La función printBMEData() imprime la temperatura actual, humedad relativa y presión atmosférica en el centro del gráfico.
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);
}
Aquí una imagen de ejemplo de la salida:

Función printTimeDate
La función printTimeDate() imprime el nombre del día actual, la hora y la fecha en el centro del gráfico.
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);
}
La salida se ve así:

Función drawAll
La función drawAll() llama a todas las funciones necesarias para dibujar el display completo del graficador. Esto incluye las etiquetas del reloj, el arco de tiempo, los datos del sensor, la hora y la gráfica de temperatura.
void drawAll() {
canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
drawClockLabels();
drawTimeArc();
printBMEData();
printTimeDate();
plotTemp();
}
Produce la siguiente salida:

Función drawCanvas
La función drawCanvas() interpreta el canvas como un bitmap y lo muestra en la pantalla e-Paper. Aquí es cuando lo que se dibujó en el canvas realmente se vuelve visible en la pantalla.
void drawCanvas() {
epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}
Función partialRefresh
La función partialRefresh() llama a drawAll() para redibujar todo en el canvas y luego realiza un refresco parcial de la pantalla e-Paper. Esto es mucho más rápido (<0.5 seg) que un refresco completo pero deja imágenes residuales.
oid partialRefresh(const void* pv) {
drawAll();
epd.setPartialWindow(0, 0, W, H);
drawCanvas();
}
Para más información, mira el tutorial Partial Refresh of e-Paper Display.
Función fullRefresh
La función fullRefresh() realiza un refresco completo, que es mucho más lento que el parcial y hace que la pantalla e-Paper parpadee, pero elimina todas las imágenes residuales.
void fullRefresh(const void* pv) {
epd.setFullWindow();
drawCanvas();
}
Función setup
La función setup() inicializa el sensor BME280 y la pantalla, y establece la zona horaria.
void setup() {
initSensor();
initDisplay();
setTimezone();
}
Función loop
La función loop() básicamente actualiza la pantalla cada 30 segundos. Dependiendo del número de actualizaciones (iter), se realiza un refresco parcial o completo. Específicamente, se hace un refresco completo cada 120 iteraciones, lo que equivale a 120*30/60 = 60 minutos.
De manera similar, el reloj interno del ESP32 se sincroniza con el servidor SNTP aproximadamente cada 50*30/60 = 25 minutos.
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);
}
Y con eso se completa la descripción del código.
Conclusiones
En este tutorial aprendiste a construir un graficador polar que muestra temperatura (y otros datos) en una pantalla e-Paper de 4.2″ usando un ESP32 y un sensor BME280.
Hay muchas formas en que podrías modificar o ampliar este graficador. Primero, la resolución de temperatura de la implementación actual es bastante baja, ya que el anillo donde se grafica la temperatura es bastante delgado. Podrías reducir el texto en el centro y hacer el anillo más ancho para una mejor resolución.
Además de la temperatura, sería bueno también monitorear la presión atmosférica y la humedad a lo largo del tiempo. Esto podría lograrse teniendo tres canvas (para temperatura, presión y humedad), y alternar entre ellos, ya sea periódicamente o añadiendo un botón al circuito.
Incluso podrías monitorear y graficar la velocidad de Wi-Fi a lo largo del tiempo, ya que el ESP32 está conectado a internet de todos modos. En general, cualquier parámetro que quieras medir durante un día (por ejemplo, light intensity, air quality, dust, …) u otro intervalo de tiempo periódico es una buena opción para un graficador polar. Las esquinas de la pantalla también serían un buen espacio para mostrar datos de pronóstico del tiempo.
Finalmente, una palabra sobre el deep-sleep. Sería bueno poner el ESP32 en deep-sleep entre actualizaciones de pantalla, pero esto no es sencillo, ya que toda la memoria, incluyendo el dibujo en el canvas, se pierde en deep sleep y el canvas es demasiado grande para RTC memory. En su lugar, tendríamos que almacenar la serie temporal de temperatura en un pequeño arreglo que quepa en la memoria RTC. No es imposible, pero es un poco más limitante.
Muchas ideas para experimentar. ¡Feliz bricolaje : )

