Skip to Content

Gráfica de Temperatura en Pantalla e-Paper

Gráfica de Temperatura en Pantalla e-Paper

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.

Plotting temperature in cartesian coordinate system
Graficar la temperatura en un sistema de coordenadas cartesianas

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.

Plotting temperature in polar coordinate system
Graficar la temperatura en un sistema de coordenadas polares

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.

Screen shot of Polar Plotter
Captura de pantalla del graficador polar

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″

BME280

Sensor BME280

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cable de datos USB

Dupont wire set

Juego de cables Dupont

Half_breadboard56a

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.

BME280 breakout board
Placa breakout del BME280

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.

Frontal y trasera del módulo de pantalla e-Paper de 4.2″

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.

Parte trasera del módulo con interfaz SPI y controlador

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.

Connecting 4.2" e-Paper to ESP32 via SPI
Conexión de pantalla e-Paper de 4.2″ al ESP32 vía 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-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

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

Adafruit_GFX and GxEPD2 libraries in Library Manager
Librerías Adafruit_GFX y GxEPD2 en Library Manager

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

Adafruit_BME280 library in Library Manager
Librería Adafruit_BME280 en Library Manager

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:

Test output on 4.2" e-Paper display
Salida de prueba en pantalla e-Paper de 4.2″

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:

Connecting the BME280 to ESP32
Conexión del BME280 al ESP32

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.

Output of BME280 Sensor on Serial Monitor
Salida del sensor BME280 en Monitor Serial

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

Elements of Polar Plotter Display
Elementos del display del graficador polar

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

Polar axes with labels on e-Paper display
Ejes polares con etiquetas en pantalla e-Paper

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.

Relationship between Polar and Cartesian Coordinates
Relación entre coordenadas polares y 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:

Time arc on e-Paper display
Arco de tiempo en pantalla e-Paper

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.

Clock hour labels on e-Paper display
Etiquetas de horas del reloj en pantalla e-Paper

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.

Polar Plot of temperature data
Gráfico polar de datos de temperatura

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:

Datos del BME280 en pantalla e-Paper

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

Hora/Fecha en pantalla e-Paper

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:

Graficador completo en pantalla e-Paper

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