Skip to Content

Plotador de Temperatura em Ecrã e-Paper

Plotador de Temperatura em Ecrã e-Paper

Neste tutorial, vamos construir um Plotter de Temperatura num ecrã e-Paper de 4,2″ usando um ESP32 e o sensor BME280.

Uma forma comum de mostrar dados de temperatura é através de um gráfico de linhas em cartesian coordinate system, onde o tempo está no eixo x e a temperatura ou outros dados ambientais no eixo y. Veja a figura de exemplo abaixo, onde a temperatura (linha vermelha) varia entre 15° e 25° e o tempo entre a 1 e as 12 horas.

Plotting temperature in cartesian coordinate system
Plotar temperatura em sistema de coordenadas cartesianas

Plotar temperatura em coordenadas cartesianas tem a vantagem de facilitar a visualização das variações de temperatura, mas ignora o facto de o tempo ser cíclico, por exemplo, as 24 horas do dia repetem-se. Neste caso, um polar coordinate system é frequentemente uma escolha melhor. Veja o gráfico de exemplo abaixo que mostra a temperatura ao longo de um círculo ou anel com as horas do relógio marcadas.

Plotting temperature in polar coordinate system
Plotar temperatura em sistema de coordenadas polares

Como o eixo do tempo segue as horas do relógio, torna-se um pouco mais fácil ver qual era a temperatura numa determinada hora. Mas, mais importante, o gráfico é contínuo, pois as 12 horas e as 11:59 estão lado a lado. Podemos plotar vários dias sem necessidade de rolar o gráfico, por exemplo.

Neste tutorial, vai aprender a construir um Plotter Polar para dados de temperatura. O Plotter mostrará a temperatura atual, humidade e pressão do ar, além da hora e data. A captura de ecrã abaixo mostra como o Plotter completo vai ficar.

Screen shot of Polar Plotter
Captura de ecrã do Plotter Polar

Note que destaquei a curva de temperatura plotada a vermelho. Como vamos usar um ecrã e-Paper a tons de cinza, não haverá cor. O que nos leva às peças necessárias.

Peças Necessárias

Estou a usar o ESP32 lite como microprocessador, pois é barato e tem interface de carregamento de bateria, permitindo usar o plotter com uma bateria LiPo. Qualquer outro ESP32 ou ESP8266 com memória suficiente também serve, mas de preferência escolha um com interface de carregamento de bateria.

Ecrã e-Paper de 4,2″

BME280

Sensor BME280

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cabo USB de Dados

Dupont wire set

Conjunto de Fios 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.

Sensor BME280

Vamos usar o BME280 sensor para medir os dados de temperatura para o nosso Plotter de Temperatura. Note, no entanto, que o BME280 também permite medir humidade e pressão do ar, que também poderíamos plotar.

Outra característica do BME280 é o modo de baixo consumo, sleep mode, onde consome apenas 0,1µA (3,6 μA em modo normal). Em combinação com um e-Paper, isto cria um sistema de baixo consumo que pode funcionar a bateria.

O sensor BME280 é pequeno e normalmente vem numa placa breakout com interface I2C; veja a imagem abaixo. A interface I2C torna a ligação e uso muito simples.

BME280 breakout board
Placa breakout do BME280

O sensor pode medir pressão de 300 hPa a 1100 hPa, temperatura de -40°C a +85°C, e humidade de 0% a 100%. Para mais informações sobre o BME280 e suas aplicações, consulte os tutoriais How To Use BME280 Pressure Sensor With Arduino e Weather Station on e-Paper Display.

Ecrã e-Paper de 4,2″

O ecrã e-Paper usado neste projeto é um módulo de 4,2″, com resolução de 400×300 pixels, 4 níveis de cinza, tempo de atualização parcial de 0,4 segundos, e controlador embutido com interface SPI.

Frente e verso do módulo e-Paper de 4,2″

Note que o módulo tem um pequeno jumper/chave na parte de trás para alternar entre SPI de 4 fios e SPI de 3 fios. Vamos usar o SPI de 4 fios por defeito, portanto não precisa alterar nada.

Parte traseira do módulo com interface SPI e controlador

O módulo funciona a 3,3V ou 5V, tem corrente de sleep muito baixa de 0,01µA e consome cerca de 26,4mW durante a atualização. Para mais informações sobre ecrãs e-Paper, veja os tutoriais Interfacing Arduino To An E-ink Display e Partial Refresh of e-Paper Display.

Ligação e Teste do e-Paper

Primeiro, vamos ligar e testar o funcionamento do e-Paper. A imagem seguinte mostra a ligação completa entre o ESP32 e o ecrã para alimentação e interface SPI.

Connecting 4.2" e-Paper to ESP32 via SPI
Ligação do e-Paper de 4,2″ ao ESP32 via SPI

Abaixo uma tabela com todas as ligações para conveniência. Note que pode alimentar o ecrã com 3,3V ou 5V, mas o ESP32-lite tem apenas saída de 3,3V e as linhas de dados SPI devem ser 3,3V!

Ecrã e-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

Instalar a biblioteca GxEPD2

Antes de podermos desenhar ou escrever no ecrã e-Paper, precisamos instalar duas bibliotecas. A biblioteca Adafruit_GFX de gráficos, que fornece um conjunto comum de primitivas gráficas (texto, pontos, linhas, círculos, etc.). E a biblioteca GxEPD2, que fornece o driver gráfico para o ecrã e-Paper.

Basta instalar estas bibliotecas da forma habitual. Após a instalação, devem aparecer no Library Manager assim:

Adafruit_GFX and GxEPD2 libraries in Library Manager
Bibliotecas Adafruit_GFX e GxEPD2 no Library Manager

Instalar a biblioteca Adafruit_BME280

Como vamos usar o sensor BME280 para medir temperatura, humidade e pressão do ar, também precisamos instalar a biblioteca Adafruit_BME280. Instale-a normalmente e deverá aparecer no Library Manager assim:

Adafruit_BME280 library in Library Manager
Biblioteca Adafruit_BME280 no Library Manager

Testar o ecrã e-Paper

Depois de instalar as bibliotecas, sugiro executar o código de teste seguinte para garantir que o ecrã 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() {}

O código imprime o texto “Makerguides” e, após algum piscar do ecrã (atualização completa), deverá vê-lo no seu ecrã como mostrado abaixo:

Test output on 4.2" e-Paper display
Saída de teste no ecrã e-Paper de 4,2″

Se não aparecer, algo está errado. Muito provavelmente o ecrã não está corretamente ligado ou o driver do ecrã escolhido está errado. A linha crítica do código é esta, onde especificamos o driver para o ecrã de 4,2″:

GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

A biblioteca Readme GxEPD2 lista todos os ecrãs suportados e pode encontrar os detalhes nos ficheiros header, por exemplo GxEPD2.h. Encontre o driver específico para o seu ecrã. Pode ser necessário algum teste e erro.

Ligação e Teste do Sensor de Temperatura

Devido à interface I2C, ligar o sensor BME280 ao ESP32 é muito simples. Basta ligar o SCL do sensor ao pino 25 e o SDA ao pino 33 do ESP32. Depois ligue os pinos GND ao terra e o pino 3.3V do ESP32 ao VIN do sensor BME280. Veja o diagrama completo de ligações, com o e-Paper e o BME280 abaixo:

Connecting the BME280 to ESP32
Ligação do BME280 ao ESP32

De seguida, vamos garantir que o sensor BME280 funciona, enquanto o ecrã e-Paper está ligado.

Testar o sensor BME280

O código de teste seguinte usa o sensor BME280 para medir temperatura, pressão do ar e humidade relativa e imprime os valores medidos no 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);
}

Note que estamos a usar I2C por software e temos de especificar os pinos a que as linhas SDA e SCL estão ligadas, chamando Wire.begin(33, 25). O BME280 normalmente usa o endereço I2C 0x76. Se não conseguir ler dados, talvez o seu BME280 tenha um endereço diferente e terá de alterar bme.begin(0x76, &Wire), conforme necessário.

Se tudo funcionar bem, deverá ver dados semelhantes aos seguintes no Monitor Serial. Certifique-se de definir a velocidade de transmissão para 115200.

Output of BME280 Sensor on Serial Monitor
Saída do sensor BME280 no Monitor Serial

Com isso pronto, agora podemos escrever o código para o Plotter de Temperatura.

Código para um Plotter de Temperatura no e-Paper

Nesta secção vamos implementar o código para o Plotter. A captura de ecrã do display do plotter mostra os elementos e funcionalidades que terá.

Elements of Polar Plotter Display
Elementos do Display do Plotter Polar

No anel exterior vamos plotar a temperatura ao longo do tempo (linha vermelha). O anel exterior também mostra as etiquetas das horas e o intervalo de temperatura. Os pequenos pontos pretos ao longo do anel exterior (arco do tempo) indicam a hora atual – na imagem acima são cerca de 15 minutos passados das 11 horas.

No centro do anel vamos mostrar a temperatura atual (18,3°), a humidade relativa atual (47%) e a pressão do ar (1005mb). Abaixo disso mostramos também a hora atual (Seg 11:14) e a data (09/09/24).

Abaixo encontra o código completo para o Plotter de Temperatura. Dê uma vista rápida para ter uma ideia geral, depois discutiremos os detalhes.

#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);
}

Bibliotecas

Começamos por incluir as bibliotecas WiFi.h e esp_snt.h, que vamos precisar para sincronizar o relógio do plotter com um servidor de tempo SNTP pela internet via Wi-Fi.

#include "WiFi.h"
#include "esp_sntp.h"

Isto permite reportar a temperatura exatamente na hora em que foi medida. Veja o tutorial How to synchronize ESP32 clock with SNTP server para mais detalhes.

De seguida incluímos a Adafruit_BME280 library necessária para ler temperatura, humidade e pressão do sensor BME280.

#include "Adafruit_BME280.h"

Finalmente, incluímos o ficheiro header GxEPD2_BW.h para o ecrã e-Paper a preto e branco (BW). Se tiver um ecrã a 3 cores, incluiria GxEPD2_3C.h, ou GxEPD2_4C.h para um ecrã a 4 cores, e GxEPD2_7C.h para um ecrã a 7 cores.

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

Também incluímos três ficheiros de fontes, que usamos para mostrar as etiquetas do plotter, temperatura, data e outras informações. Pode encontrar uma visão geral das AdaFruit GFX fonts aqui.

Constantes

De seguida definimos algumas constantes. Mais importante, terá de definir o SSID e PASSWORD para o seu WiFi, e o TIMEZONE onde vive.

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

A especificação do fuso horário acima “AEST-10AEDT,M10.1.0,M4.1.0/3” é para a Austrália, que corresponde ao Australian Eastern Standard Time (AEST) com ajustes de horário de verão.

As partes desta definição de fuso horário são as seguintes

  • AEST: Australian Eastern Standard Time
  • -10: Deslocamento UTC de 10 horas à frente do Tempo Universal Coordenado (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: Transição para horário de verão ocorre no 1º domingo de outubro
  • M4.1.0/3: Transição de volta para horário padrão ocorre no 1º domingo de abril, com diferença de 3 horas em relação ao UTC.

Para encontrar as definições do seu fuso horário, consulte o Posix Timezones Database.

Depois temos constantes para as dimensões (W,H) do ecrã e-Paper, o ponto central (CW, CH) e o raio máximo R de um círculo no ecrã. Note que largura e altura estão trocadas, pois rodamos o ecrã (setRotation(1)) na função 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 conveniência definimos também duas constantes para as cores preto e branco, e os nomes dos dias:

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };

Finalmente, definimos os valores máximo, médio e mínimo para a temperatura (TMAX, TMID, TMIN) e os respetivos raios do eixo 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

De seguida criamos objetos para o ecrã e-Paper, o sensor BME280 e uma 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);

O objeto epd representa o ecrã (epaper display). Esta definição de objeto deve corresponder ao tipo de ecrã e-Paper que tem. Aqui temos um ecrã de 4,2 polegadas (= _420). Veja a biblioteca Readme for GxEPD2 e o ficheiro GxEPD2.h para os ecrãs suportados.

A canvas é um buffer onde podemos desenhar em segundo plano. É necessária para plotar a curva de temperatura e para a atualização parcial. Para mais detalhes, veja o tutorial Partial Refresh of e-Paper Display.

Função initDisplay

A função initDisplay() inicializa o ecrã e-Paper, define a cor do texto, tamanho e fonte para a canvas e preenche a canvas com branco. Depois desenha os eixos de coordenadas polares para a temperatura mínima, média e máxima, após o que imprime as etiquetas de temperatura nos eixos.

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);
}

A captura de ecrã seguinte mostra como fica depois de chamar initDisplay():

Polar axes with labels on e-Paper display
Eixos polares com etiquetas no ecrã e-Paper

Função setTimezone

A função setTimezone() define o FUSO HORÁRIO para o relógio interno do ESP32. Como mencionado antes, pode encontrar outras definições de fuso horário no Posix Timezones Database.

void setTimezone() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

Função syncTime

A função syncTime cria uma ligação WiFi e depois sincroniza o relógio interno do ESP32 com o servidor SNTP “pool.ntp.org” chamando configTzTime().

Pode especificar outros, ou mesmo múltiplos servidores SNTP para ligar. Veja o tutorial How to synchronize ESP32 clock with SNTP server para mais informações.

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
}

Função initSensor

A função initSensor() inicializa o sensor BME280. Certifique-se que SDA e SCL estão ligados aos pinos 33 e 25 do ESP32, conforme especificado. Também confirme que o endereço I2C 0x76 está correto para o seu sensor.

void initSensor() {
  Wire.begin(33, 25);  // SDA, SCL
  bme.begin(0x76, &Wire);
}

Função polar2cart

A função polar2cart() converte coordenadas polares dadas pelo ponto central x, y, raio r e ângulo alpha em coordenadas cartesianas que são devolvidas em 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));
}

A imagem seguinte ilustra como a função polar2cart() converte coordenadas polares em cartesianas.

Relationship between Polar and Cartesian Coordinates
Relação entre Coordenadas Polares e Cartesianas

Precisamos desta função, por exemplo, para converter as horas do relógio num círculo para as coordenadas cartesianas x, y que o ecrã e-Paper usa.

Funções printAt e printfAt

As funções printAt() e printfAt() imprimem texto nas coordenadas x, y no ecrã 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);
}

A função printAt() permite imprimir apenas texto simples, enquanto a printfAt() funciona como printf e permite fornecer um especificador de formato para imprimir texto e dados, por exemplo printfAt(x, y, "%02d-%02d-%02d", m, h, y);

Função drawDottedCircle

A função drawDottedCircle() desenha simplesmente um círculo pontilhado com ponto central x, y, raio r e espaço s entre os pontos. A função é usada para desenhar os eixos polares ao longo dos quais a temperatura é plotada.

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);
  }
}

Função curSeconds

A função curSeconds() obtém a hora atual e converte-a em segundos. Por exemplo, 8:50:10 seria convertido em 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;
}

Função sec2angle

A função sec2angle() converte o tempo fornecido em segundos num ângulo no relógio.

float sec2angle(int sec) {
  return TWO_PI * sec / (12 * 60 * 60);
}

Função drawTimeArc

A função drawTimeArc() desenha os pontos pretos que indicam a hora atual no anel (arco do tempo). Como pode ver, usa as funções curSeconds() e sec2angle() para converter a hora atual em segundos e num ângulo que determina o fim do arco do 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);
  }
}

A imagem seguinte mostra o fim do arco do tempo:

Time arc on e-Paper display
Arco do tempo no ecrã e-Paper

Função drawClockLabels

A função drawClockLabels() simplesmente desenha as etiquetas das horas do relógio no anel.

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);
  }
}

A imagem seguinte de uma secção do ecrã mostra as etiquetas das horas do relógio.

Clock hour labels on e-Paper display
Etiquetas das horas do relógio no ecrã e-Paper

Função plotTemp

A função plotTemp() plota a temperatura ao longo do tempo em coordenadas polares. Primeiro lê a temperatura atual, limita-a ao intervalo TMIN a TMAX, e depois escala para um raio r. De seguida, a hora atual é convertida num ângulo alpha, e a temperatura é então plotada como um único pixel nas 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);
}

Note que todo o desenho ocorre na canvas e, portanto, não é imediatamente visível. A imagem seguinte mostra como fica a saída da função quando exibida.

Polar Plot of temperature data
Gráfico Polar dos dados de temperatura

Note que o gráfico de temperatura está novamente destacado a vermelho, mas na realidade aparece como uma linha preta.

Função printBMEData

A função printBMEData() imprime a temperatura atual, humidade relativa e pressão do ar no centro do 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);
}

Aqui uma imagem de exemplo da saída:

Dados do BME280 no ecrã e-Paper

Função printTimeDate

A função printTimeDate() imprime o nome do dia atual, hora e data no centro do 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);
}

A saída é a seguinte:

Hora/Data no ecrã e-Paper

Função drawAll

A função drawAll() chama todas as funções necessárias para desenhar o display completo do plotter. Isto inclui as etiquetas do relógio, o arco do tempo, os dados do sensor, a hora e o plot da temperatura.

void drawAll() {
  canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
  drawClockLabels();
  drawTimeArc();
  printBMEData();
  printTimeDate();
  plotTemp();
}

Produz a seguinte saída:

Plotter completo no ecrã e-Paper

Função drawCanvas

A função drawCanvas() interpreta a canvas como bitmap e exibe-a no ecrã e-Paper. É aqui que o que está desenhado na canvas realmente se torna visível no ecrã.

void drawCanvas() {
  epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}

Função partialRefresh

A função partialRefresh() chama drawAll() para redesenhar tudo na canvas e depois realiza uma atualização parcial do ecrã e-Paper. Isto é muito mais rápido (<0,5 seg) do que uma atualização completa, mas deixa imagens residuais.

oid partialRefresh(const void* pv) {
  drawAll();
  epd.setPartialWindow(0, 0, W, H);
  drawCanvas();
}

Para mais informações, veja o tutorial Partial Refresh of e-Paper Display.

Função fullRefresh

A função fullRefresh() realiza uma atualização completa, que é muito mais lenta que a parcial e faz o ecrã e-Paper piscar, mas remove todas as imagens residuais.

void fullRefresh(const void* pv) {
  epd.setFullWindow();
  drawCanvas();
}

Função setup

A função setup() inicializa o sensor BME280 e o ecrã, e define o fuso horário.

void setup() {
  initSensor();
  initDisplay();
  setTimezone();  
}

Função loop

A função loop() basicamente atualiza o ecrã a cada 30 segundos. Dependendo do número de atualizações (iter), é feita uma atualização parcial ou completa. Especificamente, uma atualização completa é feita a cada 120 iterações, o que corresponde a 120*30/60 = 60 minutos.

De forma semelhante, o relógio interno do ESP32 é sincronizado com o servidor SNTP cerca de 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);
}

E assim termina a descrição do código.

Conclusões

Neste tutorial aprendeu a construir um Plotter Polar que mostra temperatura (e outros dados) num ecrã e-Paper de 4,2″ usando um ESP32 e um sensor BME280.

Existem muitas formas de modificar ou expandir este Plotter. Em primeiro lugar, a resolução da temperatura na implementação atual é bastante baixa, pois o anel onde a temperatura é plotada é relativamente fino. Poderia reduzir o texto no centro e alargar o anel para melhor resolução.

Além da temperatura, seria interessante monitorizar também a pressão do ar e a humidade ao longo do tempo. Isto poderia ser feito tendo três canvases (para temperatura, pressão e humidade) e alternando entre elas, periodicamente ou adicionando um botão ao circuito.

Poderia até monitorizar e plotar a velocidade do Wi-Fi ao longo do tempo, já que o ESP32 está ligado à internet. Em geral, qualquer parâmetro que queira medir ao longo de um dia (ex.: light intensity, air quality, dust, …) ou outro intervalo de tempo periódico é uma boa escolha para um Plotter Polar. Os cantos do ecrã também seriam um bom espaço para mostrar dados de previsão meteorológica.

Finalmente, uma palavra sobre deep-sleep. Seria bom colocar o ESP32 em deep-sleep entre atualizações do ecrã, mas isso não é simples, pois toda a memória, incluindo o desenho na canvas, é perdida no deep sleep e a canvas é demasiado grande para RTC memory. Em vez disso, teríamos de guardar a série temporal da temperatura num pequeno array que caiba na memória RTC. Não é impossível, mas é um pouco mais limitativo.

Muitas ideias para experimentar. Feliz bricolage : )