Um display ESP32 OLED com MQTT é a forma mais simples de colocar dados ao vivo em uma mesa, em uma bancada ou no painel de uma máquina, sem celular nem navegador. Neste tutorial, você vai se inscrever em vários topics no broker.connectmq.com, exibir os valores mais recentes em uma tela SSD1306 de 128×64, marcar leituras como desatualizadas quando um sensor parar de reportar e mostrar o próprio status de Wi-Fi e MQTT do display. O mesmo sketch funciona sem alterações quando você passa para credenciais privadas.
O Que o Display Mostra
Um bom dashboard minúsculo responde a três perguntas num relance: qual é o valor, quão antigo ele é e se o próprio display está conectado. O layout usado aqui encaixa as três em uma tela de 0,96 polegada:
+--------------------------------+
| MQTT OK RSSI -58 |
| Temp 23.4 C |
| Humidity 48 % |
| Door CLOSED (12s) |
| Msg: Coffee is ready |
+--------------------------------+
Cada linha está ligada a um topic. O display se inscreve com um único curinga e encaminha as mensagens recebidas para as linhas comparando o último nível do topic.
| Topic | Linha | Exemplo de payload |
|---|---|---|
connectmq/demo/<id>/temp |
Temp | 23.4 |
connectmq/demo/<id>/humidity |
Humidity | 48 |
connectmq/demo/<id>/door |
Door | CLOSED |
connectmq/demo/<id>/msg |
Msg | Qualquer texto curto |
Em uma instalação real, os valores vêm de outros dispositivos: um ESP32 com sensor DHT22 publicando temperatura e umidade, um sensor de porta ou um script enviando notificações. O display não se importa com quem publica, e essa é a principal força do modelo publish/subscribe. Se esse desacoplamento é novidade para você, leia sobre o que um broker MQTT faz antes de começar.
Hardware e Ligações
- Placa de desenvolvimento ESP32.
- Módulo OLED SSD1306, 128×64 pixels, versão I2C (quatro pinos: GND, VCC, SCL, SDA).
- Quatro jumpers.
| Pino do OLED | Pino do ESP32 |
|---|---|
| GND | GND |
| VCC | 3V3 |
| SCL | GPIO 22 |
| SDA | GPIO 21 |
Os GPIOs 21 e 22 são os pinos I2C padrão nas placas ESP32 DevKit clássicas. Outras variantes, como o ESP32-S3 ou o C3, usam padrões diferentes, então passe seus pinos explicitamente para Wire.begin(SDA, SCL), como faz o sketch. A maioria dos módulos usa o endereço I2C 0x3C; alguns usam 0x3D. Se a tela continuar preta, rode um sketch de scanner I2C para confirmar o endereço.
Bibliotecas
Se você nunca gravou um ESP32 pela Arduino IDE, siga antes a configuração da placa no tutorial IoT com ESP32; ele cobre a instalação do pacote de placas esp32, a escolha da placa e da porta corretas e a leitura do monitor serial, e você vai precisar de tudo isso aqui. Depois, instale pelo Gerenciador de Bibliotecas do Arduino:
- PubSubClient, de Nick O'Leary.
- Adafruit SSD1306, da Adafruit, que também instala a Adafruit GFX Library e a Adafruit BusIO.
O Sketch ESP32 OLED MQTT
A regra de projeto principal: nunca desenhe dentro do callback do MQTT. O callback apenas guarda o valor e um timestamp e marca uma flag. O desenho acontece em loop(), a uma taxa fixa. Isso mantém o callback rápido, evita telas atualizadas pela metade e permite que o display atualize os contadores de "idade" mesmo quando nenhuma mensagem chega.
#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* CLIENT_ID = "oled-5d20ab"; // único
const char* BASE = "connectmq/demo/oled-5d20ab";
const unsigned long STALE_MS = 120000; // 2 minutos
Adafruit_SSD1306 display(128, 64, &Wire, -1);
WiFiClient net;
PubSubClient mqtt(net);
struct Row {
const char* key; // último nível do topic
const char* label;
const char* unit;
String value;
unsigned long at; // millis() no recebimento, 0 = nunca
};
Row rows[] = {
{"temp", "Temp", " C", "", 0},
{"humidity", "Humidity", " %", "", 0},
{"door", "Door", "", "", 0},
{"msg", "Msg:", "", "", 0},
};
const int ROW_COUNT = sizeof(rows) / sizeof(rows[0]);
bool dirty = true;
unsigned long lastDraw = 0, lastAttempt = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
const char* leaf = strrchr(topic, '/');
if (!leaf) return;
leaf++;
for (int i = 0; i < ROW_COUNT; i++) {
if (strcmp(leaf, rows[i].key) == 0) {
String v;
for (unsigned int j = 0; j < length && j < 40; j++) v += (char)payload[j];
rows[i].value = v;
rows[i].at = millis();
dirty = true;
return;
}
}
}
String ageText(unsigned long at) {
unsigned long s = (millis() - at) / 1000;
if (s < 60) return String(s) + "s";
if (s < 3600) return String(s / 60) + "m";
return String(s / 3600) + "h";
}
void draw() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Barra de status
display.setCursor(0, 0);
display.print("MQTT ");
display.print(mqtt.connected() ? "OK" : (WiFi.status() == WL_CONNECTED ? "..." : "NO WIFI"));
if (WiFi.status() == WL_CONNECTED) {
display.setCursor(80, 0);
display.print("RSSI ");
display.print(WiFi.RSSI());
}
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
int y = 14;
for (int i = 0; i < ROW_COUNT; i++) {
display.setCursor(0, y);
display.print(rows[i].label);
display.setCursor(i == 3 ? 30 : 60, y);
if (rows[i].at == 0) {
display.print("--");
} else {
bool stale = millis() - rows[i].at > STALE_MS;
String text = rows[i].value + rows[i].unit;
if (i == 3) text = text.substring(0, 16); // cabe na linha da mensagem
display.print(text);
if (stale && i != 3) {
display.setCursor(104, y);
display.print(ageText(rows[i].at)); // mostra a idade só quando desatualizado
}
}
y += 12;
}
display.display();
}
bool connectMqtt() {
if (!mqtt.connect(CLIENT_ID)) {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
return false;
}
String filter = String(BASE) + "/+";
mqtt.subscribe(filter.c_str(), 1);
Serial.println("Subscribed to " + filter);
return true;
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found");
while (true) delay(1000);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Connecting...");
display.display();
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setSocketTimeout(5);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
} else {
mqtt.loop();
}
}
// Redesenha com dados novos e uma vez por segundo para atualizar status e idades
if (dirty || millis() - lastDraw > 1000) {
draw();
dirty = false;
lastDraw = millis();
}
}
Por que retained messages importam para um display
Quando o ESP32 inicializa, ele não sabe de nada. Se os sensores publicam suas leituras como retained messages, o broker entrega ao display o valor mais recente de cada topic no momento em que ele se inscreve, e a tela é preenchida em um segundo. Sem retained messages, as linhas mostram -- até a próxima publicação de cada sensor, o que pode levar minutos no caso de um dispositivo a bateria. Se você controla os publicadores, retenha valores que mudam devagar, como temperatura, estado da porta e a última notificação.
Teste pela Linha de Comando
Publique alguns valores como se você fosse os sensores:
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/temp" -m "23.4"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/humidity" -m "48"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -t "connectmq/demo/oled-5d20ab/door" -m "CLOSED"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -t "connectmq/demo/oled-5d20ab/msg" -m "Coffee is ready"
Agora aperte o botão de reset do ESP32. Como os três primeiros foram enviados com -r, eles reaparecem quase imediatamente após a reconexão; a linha da mensagem fica vazia porque ela não foi retida. Espere dois minutos sem publicar, e o indicador de idade aparece ao lado de cada valor desatualizado.
Para apagar um valor retido, publique uma retained message vazia nesse topic:
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-5d20ab -r -n -t "connectmq/demo/oled-5d20ab/door"
Os dados retidos no servidor MQTT público (gratuito) ficam visíveis para qualquer pessoa que se inscreva, então use ali apenas valores de demonstração inofensivos.
Escolhendo Topics para um Display Compartilhado
A demonstração coloca todas as linhas sob o prefixo do próprio display, o que deixa o exemplo autossuficiente. Em uma instalação real, os sensores normalmente são donos dos seus topics, por exemplo home/livingroom/temp vindo de um dispositivo e home/garage/door de outro. Você tem duas opções. A primeira é se inscrever explicitamente em cada topic e mapear as strings completas dos topics para as linhas, que é a abordagem mais previsível e a indicada quando os topics vêm de partes diferentes da árvore. A segunda é combinar uma convenção, como um ramo display/ no qual os sensores, ou um pequeno fluxo do Node-RED, republicam os dados. Isso mantém o firmware do display genérico, e adicionar uma linha passa a ser uma mudança de configuração, e não de código. Seja qual for a escolha, lembre-se de que o servidor MQTT público permite 10 inscrições por cliente, então alguns poucos topics explícitos funcionam, mas dezenas não.
Dicas de Legibilidade para Telas Minúsculas
- Use texto tamanho 2 para o número mais importante. Uma tela de 128×64 comporta cerca de quatro linhas de texto tamanho 2, o suficiente para uma leitura grande mais uma linha de status.
- Alinhe os números à direita para que os dígitos não pulem quando os valores mudam. Meça a largura do texto com
display.getTextBounds(). - Arredonde no publicador, não no display. Enviar
23.4em vez de23.41678economiza bytes e espaço na tela. - Mostre explicitamente que o dado está desatualizado. Um valor congelado que parece atual é pior do que um vazio. O contador de idade é o indicador honesto mais barato.
- Cuidado com o burn-in. Os pixels de OLED envelhecem com o uso. Em painéis sempre ligados, reduza o contraste com
display.dim(true)ou desloque o layout em um pixel a cada poucos minutos.
Solução de Problemas
- Tela preta, sem erros. Endereço I2C errado ou SDA/SCL invertidos. Rode um scanner I2C.
- "SSD1306 not found" em uma placa S3 ou C3. Os pinos padrão são diferentes; passe os pinos certos para
Wire.begin(). - Texto embaralhado com a imagem deslocada. Você pode ter um módulo de 128×32; altere a altura no construtor.
- Os valores nunca atualizam, mas o status mostra OK. Os topics não batem. O curinga
+corresponde a exatamente um nível, entãoconnectmq/demo/oled-5d20ab/sensors/tempnão corresponderia. Inscreva-se em#durante a depuração. - O display congela por segundos. Uma reconexão bloqueante. Reduza o timeout do socket ou use o padrão de backoff não bloqueante de lógica de reconexão MQTT no ESP32.
Do Painel de Demonstração ao Display Permanente
Um display pendurado na parede de uma oficina não deve depender do servidor MQTT público. Crie um broker MQTT gerenciado no portal do ConnectMQ, conecte-se com o usuário, a senha e o client ID gerados via TLS na porta 8883 e aponte todos os seus sensores para o mesmo endpoint. Um broker MQTT na nuvem permite que o painel da sua casa mostre leituras de uma estufa do outro lado da cidade sem nenhum redirecionamento de portas.
O ConnectMQ também facilita a depuração: o explorador de topics ao vivo mostra exatamente quais topics existem e o que está retido em cada um, o que normalmente responde em segundos à pergunta "por que esta linha está vazia?". Para uma saída mais visual ao lado do texto, combine o OLED com uma fita de LED WS2812B controlada via MQTT como luz de status codificada por cores.
Comece a Usar o ConnectMQ
Acesse app.connectmq.com, escolha o plano Serverless gratuito (sem cartão de crédito) e crie um broker. Assim, seu display, seus sensores e seu celular podem compartilhar um endpoint seguro, e você pode acompanhar cada mensagem no portal enquanto constrói o projeto.
Perguntas frequentes
O display OLED pode se inscrever em topics de outros dispositivos?
Sim. Esse é o objetivo do MQTT: qualquer dispositivo pode publicar em um topic, e qualquer número de assinantes, incluindo este display, recebe a mensagem. Só os nomes dos topics precisam coincidir.
Por que não devo desenhar no OLED dentro do callback do MQTT?
Desenhar leva vários milissegundos via I2C, e várias mensagens podem chegar de uma vez. Guardar os valores no callback e desenhar a uma taxa fixa em loop() mantém o cliente responsivo e a tela consistente.
Isso funciona com displays SH1106?
Os módulos de 1,3 polegada costumam usar o controlador SH1106. Use uma biblioteca que o suporte, como a U8g2 ou um fork SH1106 do driver da Adafruit; a parte MQTT do sketch continua igual.
Como mostro um valor de um payload JSON?
Interprete o payload com o ArduinoJson no callback, extraia o campo de que você precisa e guarde-o como o valor da linha. Mantenha curto o parsing no callback e deixe o desenho em loop().



