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.

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.

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.

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″

Sensor BME280

ESP32 lite

Cabo USB de Dados

Conjunto de Fios Dupont

Breadboard
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
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.

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.

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.

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.

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

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:

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:

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:

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.

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

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ã (e–paper 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():

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.

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:

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.

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.

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:

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:

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:

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

