ESP32 e Arduino

ESP32 OLED MQTT: Mostre Mensagens MQTT ao Vivo em um Display SSD1306

Transforme um ESP32 e um OLED SSD1306 barato em um display MQTT ao vivo que mostra valores de sensores e mensagens de qualquer dispositivo, sinaliza leituras desatualizadas e exibe o próprio status de conexão.

Pequena tela SSD1306 em um ESP32 mostrando valores de temperatura em um display ESP32 OLED MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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

  1. 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.
  2. 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().
  3. Arredonde no publicador, não no display. Enviar 23.4 em vez de 23.41678 economiza bytes e espaço na tela.
  4. 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.
  5. 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ão connectmq/demo/oled-5d20ab/sensors/temp nã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().

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.