Uma estação meteorológica DIY com MQTT fornece leituras locais de vento, chuva, temperatura, umidade e pressão que qualquer dashboard, sistema de automação residencial ou controlador de irrigação, de um quintal a uma fazenda de café, pode assinar. Neste tutorial você vai ligar sensores meteorológicos comuns a um ESP32, publicar uma mensagem JSON por minuto em um broker e usar um subscriber em Python para registrar as leituras e desenhar um gráfico. Tudo é testado primeiro no servidor MQTT público (gratuito), feito para testes e protótipos, e depois migrado para credenciais privadas.
Peças e Ligações do Nó da Estação Meteorológica DIY com MQTT
O projeto usa peças fáceis de encontrar e bem documentadas. Os sensores de vento e chuva seguem o estilo clássico dos kits "weather meter": um anemômetro de conchas e um pluviômetro de báscula, ambos com reed switches que fecham brevemente a cada rotação ou basculamento.
| Peça | Finalidade | Conexão no ESP32 |
|---|---|---|
| Placa ESP32 DevKit | Wi-Fi e MQTT | USB ou fonte de 5 V |
| Módulo BME280 (I2C) | Temperatura, umidade, pressão | SDA GPIO 21, SCL GPIO 22, 3,3 V, GND |
| Anemômetro de conchas (reed switch) | Velocidade do vento | Um fio no GPIO 27, o outro no GND |
| Pluviômetro de báscula | Precipitação | Um fio no GPIO 26, o outro no GND |
| Abrigo contra radiação | Mantém o BME280 fora do sol direto | Mecânico |
| Caixa à prova de intempéries | Protege o ESP32 | Mecânico |
Os reed switches só precisam de um pull-up; o sketch ativa os pull-ups internos do ESP32, então cada fechamento leva o pino a nível baixo. Esses kits costumam incluir também um cata-vento; ele usa uma escada de resistores que você lê com um pino ADC, e pode ser adicionado depois que o básico estiver funcionando.
Duas constantes de calibração vêm do datasheet do sensor. No kit comum usado aqui, um fechamento do anemômetro por segundo corresponde a 2,4 km/h, e cada basculamento equivale a 0,2794 mm de chuva. Se o seu hardware for diferente, altere as duas constantes no topo do sketch.
Design de Topics e Payload
Use um único topic de telemetria com um payload JSON compacto, além de um topic de status retido controlado pelo Last Will do MQTT:
connectmq/demo/<random-id>/weather/station1/telemetry (JSON every 60 s)
connectmq/demo/<random-id>/weather/station1/status (online/offline, retained)
{
"temp_c": 14.8,
"hum_pct": 71.2,
"press_hpa": 1012.6,
"wind_kmh": 9.6,
"gust_kmh": 16.8,
"rain_mm": 0.28,
"rain_today_mm": 3.07,
"rssi": -61,
"uptime_s": 86412
}
Enviar médias por minuto em vez de cada pulso mantém o tráfego mínimo e bem dentro dos limites do servidor MQTT público, de 10 mensagens por segundo e 64 KB por mensagem. Um valor de rajada (a maior média de 3 segundos dentro do minuto) preserva a informação que a média esconderia. O guia do servidor MQTT público lista os demais limites, como 8 níveis de topic e 10 assinaturas por cliente, que este projeto respeita com folga.
Por que não reter a mensagem de telemetria? Você pode, e em uma estação meteorológica isso costuma ajudar: um dashboard aberto entre duas publicações mostra imediatamente a última leitura em vez de um widget vazio. A contrapartida é que uma estação que parou de funcionar há horas continua parecendo informar seus últimos valores, e é exatamente por isso que o topic status retido e separado, com Last Will, é importante. Exiba ambos e deixe a leitura em cinza quando o status for offline.
O Sketch do ESP32
Instale estas bibliotecas pelo Library Manager do Arduino: PubSubClient de Nick O'Leary, Adafruit BME280 Library e Adafruit Unified Sensor. Altere as credenciais do Wi-Fi e o ID aleatório no prefixo do topic.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/k3x9q2/weather/station1";
const int PIN_WIND = 27;
const int PIN_RAIN = 26;
const float KMH_PER_HZ = 2.4; // anemômetro: 1 fechamento/s = 2,4 km/h
const float MM_PER_TIP = 0.2794; // pluviômetro: 1 basculamento = 0,2794 mm
WiFiClient net;
PubSubClient mqtt(net);
Adafruit_BME280 bme;
volatile uint32_t windCount = 0, rainCount = 0;
volatile uint32_t lastWindUs = 0, lastRainMs = 0;
void IRAM_ATTR onWind() {
uint32_t now = micros();
if (now - lastWindUs > 5000) { windCount++; lastWindUs = now; } // debounce de 5 ms
}
void IRAM_ATTR onRain() {
uint32_t now = millis();
if (now - lastRainMs > 200) { rainCount++; lastRainMs = now; } // debounce de 200 ms
}
char clientId[32];
char statusTopic[96];
float rainTodayMm = 0;
float maxGust = 0;
uint32_t lastGustCount = 0, lastGustMs = 0, lastPublishMs = 0, windAtPublish = 0;
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId, nullptr, nullptr, statusTopic, 1, true, "offline")) {
mqtt.publish(statusTopic, "online", true);
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_WIND, INPUT_PULLUP);
pinMode(PIN_RAIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_WIND), onWind, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_RAIN), onRain, FALLING);
if (!bme.begin(0x76) && !bme.begin(0x77)) {
Serial.println("BME280 not found, check wiring");
}
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
snprintf(clientId, sizeof(clientId), "wx-%llx", ESP.getEfuseMac());
snprintf(statusTopic, sizeof(statusTopic), "%s/status", PREFIX);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
lastPublishMs = lastGustMs = millis();
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
uint32_t now = millis();
// Rajada: maior média de 3 segundos dentro da janela de publicação
if (now - lastGustMs >= 3000) {
noInterrupts(); uint32_t c = windCount; interrupts();
float gust = (c - lastGustCount) / ((now - lastGustMs) / 1000.0) * KMH_PER_HZ;
if (gust > maxGust) maxGust = gust;
lastGustCount = c; lastGustMs = now;
}
if (now - lastPublishMs >= 60000) {
noInterrupts();
uint32_t w = windCount; uint32_t r = rainCount; rainCount = 0;
interrupts();
float seconds = (now - lastPublishMs) / 1000.0;
float windKmh = (w - windAtPublish) / seconds * KMH_PER_HZ;
float rainMm = r * MM_PER_TIP;
rainTodayMm += rainMm; // zere à meia-noite pelo logger ou com NTP
char payload[256];
snprintf(payload, sizeof(payload),
"{\"temp_c\":%.1f,\"hum_pct\":%.1f,\"press_hpa\":%.1f,"
"\"wind_kmh\":%.1f,\"gust_kmh\":%.1f,\"rain_mm\":%.2f,"
"\"rain_today_mm\":%.2f,\"rssi\":%d,\"uptime_s\":%lu}",
bme.readTemperature(), bme.readHumidity(), bme.readPressure() / 100.0,
windKmh, maxGust, rainMm, rainTodayMm, WiFi.RSSI(), now / 1000);
char topic[96];
snprintf(topic, sizeof(topic), "%s/telemetry", PREFIX);
mqtt.publish(topic, payload);
Serial.println(payload);
windAtPublish = w;
maxGust = 0;
lastPublishMs = now;
}
}
Abra o monitor serial a 115200 baud. Gire o anemômetro com a mão e incline o balde do pluviômetro algumas vezes; a mensagem do minuto seguinte deve mostrar valores diferentes de zero. Em outro terminal, confirme que o broker está recebendo os dados:
mosquitto_sub -h broker.connectmq.com -p 1883 -i wx-viewer-5521 -t "connectmq/demo/k3x9q2/weather/#" -v
Se a placa nunca conectar, o tutorial de ESP32 com MQTT mostra passo a passo como resolver problemas de Wi-Fi e de broker.
Registrando e Plotando as Leituras
O broker entrega mensagens ao vivo; ele não armazena o histórico para você. Um subscriber Python curto pode acrescentar cada leitura a um arquivo CSV e gerar um gráfico. Instale as dependências:
pip install "paho-mqtt>=2.0" matplotlib
import csv
import json
import os
import sys
from datetime import datetime, timezone
import paho.mqtt.client as mqtt
TOPIC = "connectmq/demo/k3x9q2/weather/station1/telemetry"
CSV_FILE = "weather.csv"
FIELDS = ["time", "temp_c", "hum_pct", "press_hpa", "wind_kmh", "gust_kmh", "rain_mm"]
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
client.subscribe(TOPIC, qos=1)
def on_message(client, userdata, msg):
data = json.loads(msg.payload)
row = {"time": datetime.now(timezone.utc).isoformat(timespec="seconds")}
row.update({k: data.get(k) for k in FIELDS[1:]})
new_file = not os.path.exists(CSV_FILE)
with open(CSV_FILE, "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if new_file:
writer.writeheader()
writer.writerow(row)
print(row)
def plot():
import matplotlib.pyplot as plt
times, temp, wind, gust, rain = [], [], [], [], []
with open(CSV_FILE) as f:
for r in csv.DictReader(f):
times.append(datetime.fromisoformat(r["time"]))
temp.append(float(r["temp_c"]))
wind.append(float(r["wind_kmh"]))
gust.append(float(r["gust_kmh"]))
rain.append(float(r["rain_mm"]))
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(10, 8))
ax1.plot(times, temp); ax1.set_ylabel("Temp (C)")
ax2.plot(times, wind, label="wind"); ax2.plot(times, gust, label="gust")
ax2.set_ylabel("km/h"); ax2.legend()
ax3.bar(times, rain, width=0.0007); ax3.set_ylabel("Rain (mm/min)")
fig.tight_layout()
fig.savefig("weather.png", dpi=120)
print("Saved weather.png")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "plot":
plot()
else:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="wx-logger-8812")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()
Execute python weather_logger.py para coletar dados e python weather_logger.py plot para gerar weather.png. Em uma instalação permanente, muita gente troca o CSV por um InfluxDB alimentado pelo Telegraf ou por um dashboard no Node-RED; ambos se conectam ao broker como clientes MQTT comuns.
Checklist de Precisão
Uma estação meteorológica é tão boa quanto o local onde está instalada. Percorra esta lista antes de confiar nos números:
- Proteja o sensor de temperatura. Um BME280 sob sol direto ou dentro de uma caixa fechada mede vários graus a mais. Use um abrigo contra radiação ventilado.
- Instale o anemômetro alto e desobstruído. Telhados e muros criam turbulência; um mastro acima dos obstáculos próximos fornece um vento muito mais representativo.
- Nivele o pluviômetro e mantenha-o longe de paredes e árvores que bloqueiem ou pinguem água.
- Mantenha o ESP32 longe do BME280. O calor da própria placa distorce as leituras se os dois estiverem na mesma caixa.
- Faça debounce no software e verifique a fiação. Cabos longos sem blindagem captam ruído que parece basculamento fantasma de chuva.
- Zere o total diário de chuva de forma consistente, seja à meia-noite local no logger, seja no ESP32 com horário NTP.
Ampliando sua Estação Meteorológica DIY com MQTT
Quando a estação básica estiver estável, alguns acréscimos a tornam bem mais útil.
Direção do vento. O cata-vento da maioria dos kits alterna entre resistores internos conforme gira. Ligue-o como divisor de tensão com um resistor fixo (o datasheet do kit sugere um valor) a um pino ADC1, como o GPIO 34, leia com analogRead() e associe cada faixa de tensão a um dos 16 pontos da rosa dos ventos. Use apenas pinos ADC1, porque o ADC2 não pode ser usado enquanto o Wi-Fi estiver ativo no ESP32. Publique a direção em graus, que é mais fácil de plotar do que "NNE".
Energia solar e bateria. Um painel pequeno, uma célula de lítio e um controlador de carga podem manter a estação fora da rede, mas um ESP32 sempre ligado com Wi-Fi consome demais para um painel pequeno no inverno. Use deep sleep entre as publicações e conte os pulsos do anemômetro com o coprocessador ULP ou um chip contador dedicado, ou aceite um intervalo de publicação maior. Inclua a tensão da bateria no JSON para perceber problemas com antecedência.
Resumos diários. Faça o logger publicar um resumo diário retido (temperatura mínima e máxima, chuva total, rajada máxima) em um topic separado logo após a meia-noite. Assim, ferramentas de automação residencial podem usar a chuva do dia anterior para pular um ciclo de irrigação sem ler todo o histórico.
Valores derivados. Ponto de orvalho e sensação térmica são fáceis de calcular a partir da temperatura e da umidade, no logger ou no Node-RED. Calcule-os mais adiante no fluxo, e não no ESP32, para poder mudar a fórmula sem regravar o firmware.
Tirando a Estação do Broker de Testes
O servidor MQTT público é ideal para validar a montagem, mas não para dados de produção: tudo nele pode ser lido por qualquer pessoa e o prefixo do topic é a sua única separação. Para uma estação permanente, especialmente uma que alimenta um sistema de irrigação automática, use um broker privado com TLS e credenciais. Um broker MQTT gerenciado como o ConnectMQ oferece isso sem que você precise manter um servidor em casa. Troque o cliente para WiFiClientSecure, a porta para 8883 e use o usuário, a senha e o client ID do portal:
#include <WiFiClientSecure.h>
WiFiClientSecure net; // substitui o WiFiClient
// no setup(): carregue o certificado ISRG Root X1 do Let's Encrypt
// net.setCACert(ISRG_ROOT_X1_PEM);
mqtt.setServer("broker.connectmq.com", 8883);
// conecte com: mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", statusTopic, 1, true, "offline");
O explorador de topics ao vivo do portal ConnectMQ passa então a mostrar cada mensagem assim que ela chega, e regras de alerta por limite podem avisar sobre geada ou vento forte sem código extra. Em uma propriedade rural, a mesma telemetria pode alimentar um gêmeo digital que combina clima, umidade do solo e irrigação. Mais ideias voltadas ao campo estão no artigo sobre IoT na agricultura inteligente e no guia de automação com IoT, mais abrangente.
Publique seus Dados Meteorológicos no ConnectMQ
O ConnectMQ é um broker MQTT gerenciado com TLS, acesso via WebSocket para dashboards no navegador e um cliente no próprio navegador para verificações rápidas. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, e leve sua estação do servidor MQTT público para credenciais privadas em minutos.
Perguntas frequentes
Com que frequência uma estação meteorológica DIY deve publicar via MQTT?
Uma vez por minuto é um bom padrão para valores médios. Isso mantém o tráfego mínimo e ainda captura as rajadas, desde que você envie a velocidade máxima de vento em 3 segundos junto com a média.
Por que minhas leituras de temperatura estão altas demais?
Quase sempre por causa do aquecimento solar ou do calor do próprio ESP32. Coloque o BME280 em um abrigo contra radiação ventilado, longe do microcontrolador e do regulador de tensão.
Posso usar um BME680 ou SHT31 no lugar do BME280?
Sim. Troque a biblioteca e as funções de leitura; a parte MQTT continua igual, desde que você mantenha os nomes dos campos do JSON.
O broker guarda meu histórico meteorológico?
Não. Brokers MQTT entregam mensagens ao vivo e o último valor retido. Armazene o histórico com um subscriber, como o logger Python acima, com o Telegraf e um banco de dados de séries temporais ou com o Node-RED.



