ESP32 e Arduino

Estação Meteorológica ESP32 BME280 MQTT com Intervalo Configurável Remotamente

Leia temperatura, umidade e pressão de um BME280 via I2C, publique JSON no broker.connectmq.com e altere o intervalo de publicação em tempo de execução com um comando MQTT retido salvo na flash.

Módulo BME280 conectado a um ESP32 via I2C publicando dados meteorológicos em uma estação esp32 bme280 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 nó ESP32 BME280 MQTT mede temperatura, umidade e pressão barométrica com um único chip I2C pequeno e publica os valores em um broker. Neste tutorial, você vai ligar o sensor, lê-lo no modo forçado de baixo consumo, publicar JSON no broker.connectmq.com e alterar o intervalo de publicação remotamente com uma mensagem MQTT que sobrevive a reinicializações.

Por que o BME280

O Bosch BME280 é uma evolução popular em relação ao DHT22. Ele usa I2C, então não há temporização frágil de fio único, lê os três valores em milissegundos e sua saída de pressão permite acompanhar tendências do tempo ou estimar a altitude. Ele também tem um modo forçado de economia de energia, no qual faz uma medição e volta a dormir, o que reduz o autoaquecimento.

Um aviso antes de comprar: muitos módulos baratos rotulados como "BME280" na verdade trazem um BMP280, que não tem sensor de umidade. Os dois parecem idênticos. O sketch abaixo lê o ID do chip e informa qual você tem.

Peças e bibliotecas

  • Uma placa de desenvolvimento ESP32.
  • Um módulo BME280 (versão 3,3 V ou com regulador na placa).
  • Quatro jumpers.
  • Arduino IDE 2.x com o pacote de placas esp32.
  • Bibliotecas: Adafruit BME280 Library, Adafruit Unified Sensor, Adafruit BusIO (instaladas como dependências) e PubSubClient.

Ligação

Pino do BME280 Pino do ESP32 (clássico) Observações
VIN ou VCC 3V3 Não use 5 V em módulos de 3,3 V sem regulador
GND GND
SCL GPIO 22 Clock I2C padrão
SDA GPIO 21 Dados I2C padrão
SDO (se houver) GND ou 3V3 Seleciona o endereço 0x76 ou 0x77
CSB (se houver) 3V3 Mantém o chip no modo I2C

No ESP32-S3, C3 e C6, os pinos I2C padrão são diferentes. Passe pinos explícitos para Wire.begin(SDA, SCL), como o sketch faz, e ajuste os números para a sua placa.

Design de topics com intervalo remoto

Topic Direção Retido Exemplo de payload
connectmq/demo/bme5t8n/telemetry Dispositivo publica Não {"temperature":21.84,"humidity":46.2,"pressure":1013.6}
connectmq/demo/bme5t8n/config/interval/set Você publica Sim 30
connectmq/demo/bme5t8n/config/interval Dispositivo publica Sim 30
connectmq/demo/bme5t8n/status Dispositivo publica Sim online / offline

O comando é retido para que um dispositivo que estava offline quando você o enviou ainda o receba na próxima conexão. O dispositivo então informa o valor efetivamente aplicado em um topic retido separado. Manter os valores "desejado" e "reportado" em topics diferentes evita loops de realimentação e permite ver rapidamente se uma configuração foi aceita. O guia de níveis de QoS do MQTT explica por que QoS 1 é usado na assinatura de comandos.

O sketch ESP32 BME280 MQTT

#include <WiFi.h>
#include <Wire.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* PREFIX        = "connectmq/demo/bme5t8n";   // use seu próprio id aleatório
const int I2C_SDA = 21, I2C_SCL = 22;
const uint32_t MIN_INTERVAL_S = 5, MAX_INTERVAL_S = 3600;

Adafruit_BME280 bme;
WiFiClient   net;
PubSubClient mqtt(net);
Preferences  prefs;

String clientId;
String tTelemetry, tIntervalSet, tIntervalState, tStatus;
uint32_t intervalS = 60;
unsigned long lastPublish = 0;
bool hasHumidity = true;

void reportInterval() {
  char buf[12];
  snprintf(buf, sizeof(buf), "%lu", (unsigned long)intervalS);
  mqtt.publish(tIntervalState.c_str(), buf, true);
}

void onMessage(char* topic, byte* payload, unsigned int len) {
  if (tIntervalSet != topic) return;
  char buf[12];
  len = min(len, (unsigned int)sizeof(buf) - 1);
  memcpy(buf, payload, len);
  buf[len] = '\0';

  long requested = atol(buf);
  if (requested < (long)MIN_INTERVAL_S || requested > (long)MAX_INTERVAL_S) {
    Serial.printf("Ignoring interval %s (allowed %lu-%lu)\n", buf,
                  (unsigned long)MIN_INTERVAL_S, (unsigned long)MAX_INTERVAL_S);
    reportInterval();               // reafirma o valor em uso
    return;
  }
  if ((uint32_t)requested != intervalS) {
    intervalS = requested;
    prefs.putUInt("interval", intervalS);
    Serial.printf("Interval set to %lu s\n", (unsigned long)intervalS);
  }
  reportInterval();
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(400);
  Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(clientId.c_str(), tStatus.c_str(), 1, true, "offline")) {
      mqtt.publish(tStatus.c_str(), "online", true);
      mqtt.subscribe(tIntervalSet.c_str(), 1);
      reportInterval();
      Serial.println("MQTT connected");
    } else {
      Serial.printf("MQTT failed, state %d\n", mqtt.state());
      delay(5000);
    }
  }
}

bool startSensor() {
  Wire.begin(I2C_SDA, I2C_SCL);
  if (!bme.begin(0x76, &Wire) && !bme.begin(0x77, &Wire)) return false;

  uint8_t id = bme.sensorID();
  hasHumidity = (id == 0x60);
  Serial.printf("Sensor chip ID 0x%02X (%s)\n", id, hasHumidity ? "BME280" : "BMP280, no humidity");

  // Modo forçado: uma medição sob demanda e depois sono. Recomendado para monitoramento do tempo.
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,   // temperatura
                  Adafruit_BME280::SAMPLING_X1,   // pressão
                  Adafruit_BME280::SAMPLING_X1,   // umidade
                  Adafruit_BME280::FILTER_OFF);
  return true;
}

void setup() {
  Serial.begin(115200);
  prefs.begin("bme-node", false);
  intervalS = prefs.getUInt("interval", 60);

  if (!startSensor()) {
    Serial.println("No BME280 found at 0x76 or 0x77. Check wiring.");
    while (true) delay(1000);
  }

  clientId       = "esp32-bme280-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  tTelemetry     = String(PREFIX) + "/telemetry";
  tIntervalSet   = String(PREFIX) + "/config/interval/set";
  tIntervalState = String(PREFIX) + "/config/interval";
  tStatus        = String(PREFIX) + "/status";

  connectWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);
  connectMqtt();
  lastPublish = millis() - intervalS * 1000UL;   // publica imediatamente
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (millis() - lastPublish < intervalS * 1000UL) return;
  lastPublish = millis();

  if (!bme.takeForcedMeasurement()) {
    Serial.println("Forced measurement failed");
    return;
  }
  float t = bme.readTemperature();           // graus C
  float p = bme.readPressure() / 100.0f;     // Pa -> hPa
  float h = hasHumidity ? bme.readHumidity() : NAN;

  char payload[200];
  if (hasHumidity) {
    snprintf(payload, sizeof(payload),
             "{\"temperature\":%.2f,\"humidity\":%.1f,\"pressure\":%.1f,\"interval\":%lu}",
             t, h, p, (unsigned long)intervalS);
  } else {
    snprintf(payload, sizeof(payload),
             "{\"temperature\":%.2f,\"pressure\":%.1f,\"interval\":%lu}",
             t, p, (unsigned long)intervalS);
  }
  mqtt.publish(tTelemetry.c_str(), payload);
  Serial.println(payload);
}

Testando as leituras ESP32 BME280 MQTT e alterando o intervalo

Acompanhe tudo abaixo do seu prefixo:

mosquitto_sub -h broker.connectmq.com -p 1883 -i bme-view-5t8n -t "connectmq/demo/bme5t8n/#" -v

Agora mude o intervalo para 10 segundos com um comando retido:

mosquitto_pub -h broker.connectmq.com -p 1883 -i bme-cmd-5t8n -q 1 -r \
  -t "connectmq/demo/bme5t8n/config/interval/set" -m "10"

Em instantes, config/interval informa 10 e a telemetria acelera. Reinicie a placa: ela lê 10 da flash e, mesmo que a flash fosse apagada, o comando retido o definiria de novo na conexão. Envie 2 e o dispositivo rejeita o valor e informa novamente o atual. Para remover o comando retido depois, publique uma mensagem retida vazia no mesmo topic com -r -n.

O servidor MQTT público (gratuito) é compartilhado, então qualquer pessoa poderia publicar no seu topic de comando se o adivinhasse. Isso é aceitável para uma demonstração ou protótipo, mas nunca para dados de produção, e é exatamente por isso que dispositivos de produção devem usar um broker próprio e autenticado.

Entendendo as leituras

  • A pressão é informada no nível da estação, em hPa. Os serviços meteorológicos informam a pressão reduzida ao nível do mar, então seu número será menor em altitude. As tendências importam mais do que o valor absoluto: uma queda constante ao longo de algumas horas costuma anteceder piora no tempo.
  • A altitude pode ser estimada com bme.readAltitude(1013.25), mas ela só é tão boa quanto a pressão ao nível do mar que você informa e varia com o tempo.
  • O autoaquecimento é mínimo no modo forçado com taxas de amostragem baixas. No modo normal, com oversampling e tempo de standby curto, o chip e o ESP32 ao lado podem aquecer a leitura de forma perceptível.

Solução de problemas

  1. "No BME280 found". Rode um sketch scanner de I2C. Nenhum dispositivo significa problema de ligação; um dispositivo em endereço inesperado indica a configuração do pino SDO.
  2. Umidade sempre 0 ou NaN. Você tem um BMP280. A impressão do ID do chip confirma.
  3. Pressão cerca de 10 por cento errada. Você esqueceu de dividir os pascals por 100, ou está comparando com a pressão ao nível do mar.
  4. Comando de intervalo ignorado. Verifique se o topic confere exatamente, incluindo maiúsculas e minúsculas, e se o payload é um número simples.
  5. A placa reinicia durante a conexão Wi-Fi. Uma alimentação USB fraca causa brownout no ESP32. Use um cabo ou fonte melhor.

Configuração para produção

Para implantações reais, crie um broker em app.connectmq.com e mude o sketch para WiFiClientSecure na porta 8883 com o certificado ISRG Root X1, conectando com YOUR_CLIENT_ID, YOUR_USERNAME e YOUR_PASSWORD. Assim, apenas clientes com suas credenciais podem enviar comandos de intervalo. Guardar as credenciais de Wi-Fi no código é o próximo ponto fraco; o guia de configuração com WiFiManager mostra como informá-las por um captive portal. Se você está comparando sensores, o tutorial ESP32 DHT22 cobre a opção mais barata de fio único.

Armazenando e gerando gráficos dos dados meteorológicos

O broker entrega mensagens; ele não é um banco de dados. Para ter histórico do tempo, você precisa de um assinante que grave cada leitura em um lugar durável. Três padrões comuns funcionam bem com este payload:

  1. Telegraf com o input MQTT consumer assina .../telemetry, interpreta os campos JSON e os grava no InfluxDB, prontos para gráficos no Grafana.
  2. Node-RED assina com um nó mqtt in, remodela a mensagem e grava em qualquer banco de dados para o qual tenha um nó, o que é prático se você também quer enviar notificações.
  3. Um pequeno script Python usando paho-mqtt insere linhas no SQLite ou PostgreSQL. É a opção mais transparente quando você está aprendendo.

Os três se conectam ao broker como clientes MQTT comuns, com seus próprios client IDs, exatamente como o ESP32. No portal do ConnectMQ, eles aparecem ao lado dos seus dispositivos, e o explorador de topics ao vivo é uma forma rápida de confirmar a estrutura do JSON antes de escrever qualquer código de parsing. Como o dispositivo inclui o interval ativo em cada mensagem, seus gráficos também podem mostrar quando a taxa de amostragem mudou, o que evita confundir uma lacuna com uma queda.

Comece com o ConnectMQ

Um nó meteorológico só é tão útil quanto o broker que coleta seus dados. O ConnectMQ é um broker MQTT gerenciado com TLS, mensagens retidas, MQTT 3.1.1 e 5.0 e um portal com explorador de topics ao vivo e alertas por limite, para você ser avisado quando a pressão cair rápido. O plano gratuito Serverless não exige cartão de crédito. Se os conceitos são novos para você, leia o que faz um broker MQTT e depois explore a série completa tutorial de IoT com ESP32.

Perguntas frequentes

Qual é a diferença entre o BME280 e o BMP280?

O BME280 mede temperatura, umidade e pressão; o BMP280 mede apenas temperatura e pressão. Eles compartilham o encapsulamento e a biblioteca, e o ID do chip (0x60 versus 0x58) os diferencia.

Por que tornar o intervalo de publicação configurável via MQTT?

Isso permite amostrar mais rápido durante a depuração ou um evento e mais devagar no restante do tempo, sem regravar o firmware. Salvar o valor na flash mantém a configuração entre reinicializações.

O comando de intervalo deve ser retido?

Sim, para configurações. Um comando retido chega ao dispositivo mesmo que ele estivesse offline quando você o enviou, e o dispositivo informa o valor aplicado em seu próprio topic retido.

Posso alimentar o nó BME280 com bateria?

Sim. Use deep sleep entre as leituras, mantenha o sensor no modo forçado e conecte ao Wi-Fi apenas para publicar. O tempo de Wi-Fi domina o consumo de energia.

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.