Un nodo ESP32 BME280 MQTT mide temperatura, humedad y presión barométrica con un único y pequeño chip I2C y las publica en un broker. En este tutorial conectarás el sensor, lo leerás en el modo forzado de bajo consumo, publicarás JSON en broker.connectmq.com y cambiarás el intervalo de publicación en remoto con un mensaje MQTT que sobrevive a los reinicios.
Por qué el BME280
El Bosch BME280 es una mejora muy popular respecto al DHT22. Se comunica por I2C, así que no hay una temporización frágil de un solo hilo, lee los tres valores en milisegundos y su salida de presión te permite seguir tendencias meteorológicas o estimar la altitud. También tiene un modo forzado de ahorro de energía en el que toma una medida y vuelve a dormir, lo que reduce el autocalentamiento.
Una advertencia antes de comprar: muchos módulos baratos etiquetados como "BME280" llevan en realidad un BMP280, que no tiene sensor de humedad. Los dos parecen idénticos. El sketch siguiente lee el ID del chip y te dice cuál tienes.
Componentes y librerías
- Una placa de desarrollo ESP32.
- Un módulo BME280 (versión de 3,3 V, o uno con regulador integrado).
- Cuatro cables puente.
- Arduino IDE 2.x con el paquete de placas esp32.
- Librerías: Adafruit BME280 Library, Adafruit Unified Sensor, Adafruit BusIO (se instalan como dependencias) y PubSubClient.
Conexiones
| Pin del BME280 | Pin del ESP32 (clásico) | Notas |
|---|---|---|
| VIN o VCC | 3V3 | No uses 5 V en módulos de 3,3 V sin regulador |
| GND | GND | |
| SCL | GPIO 22 | Reloj I2C por defecto |
| SDA | GPIO 21 | Datos I2C por defecto |
| SDO (si existe) | GND o 3V3 | Selecciona la dirección 0x76 o 0x77 |
| CSB (si existe) | 3V3 | Mantiene el chip en modo I2C |
En el ESP32-S3, C3 y C6 los pines I2C por defecto son distintos. Pasa los pines explícitamente a Wire.begin(SDA, SCL) como hace el sketch y ajusta los números a tu placa.
Diseño de topics con intervalo remoto
| Topic | Dirección | Retenido | Payload de ejemplo |
|---|---|---|---|
connectmq/demo/bme5t8n/telemetry |
Publica el dispositivo | No | {"temperature":21.84,"humidity":46.2,"pressure":1013.6} |
connectmq/demo/bme5t8n/config/interval/set |
Publicas tú | Sí | 30 |
connectmq/demo/bme5t8n/config/interval |
Publica el dispositivo | Sí | 30 |
connectmq/demo/bme5t8n/status |
Publica el dispositivo | Sí | online / offline |
El comando es retenido para que un dispositivo que estaba desconectado cuando lo enviaste lo reciba igualmente en la siguiente conexión. Después, el dispositivo informa del valor que realmente aplicó en un topic retenido distinto. Mantener los valores "deseado" e "informado" en topics diferentes evita bucles de realimentación y te permite ver de un vistazo si se aceptó un ajuste. La guía de niveles de QoS en MQTT explica por qué se usa QoS 1 en la suscripción al comando.
El 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"; // usa tu propio id aleatorio
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 el valor en 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 forzado: una medida bajo demanda y luego a dormir. Recomendado para monitorización meteorológica.
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperatura
Adafruit_BME280::SAMPLING_X1, // presión
Adafruit_BME280::SAMPLING_X1, // humedad
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 de inmediato
}
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(); // grados 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);
}
Probar las lecturas ESP32 BME280 MQTT y cambiar el intervalo
Observa todo lo que cuelga de tu prefijo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i bme-view-5t8n -t "connectmq/demo/bme5t8n/#" -v
Ahora cambia el intervalo a 10 segundos con un comando retenido:
mosquitto_pub -h broker.connectmq.com -p 1883 -i bme-cmd-5t8n -q 1 -r \
-t "connectmq/demo/bme5t8n/config/interval/set" -m "10"
En un momento, config/interval informa de 10 y la telemetría se acelera. Reinicia la placa: lee 10 de la flash e, incluso si la flash se borrara, el comando retenido lo volvería a fijar al conectar. Envía 2 y el dispositivo lo rechaza y vuelve a informar del valor actual. Para eliminar más adelante el comando retenido, publica un mensaje retenido vacío en el mismo topic con -r -n.
El área pública de broker.connectmq.com es un servidor MQTT público gratuito compartido y pensado para pruebas, así que cualquiera podría publicar en tu topic de comandos si lo adivinara. Es aceptable para una demostración, pero es precisamente la razón por la que los dispositivos de producción deben usar su propio broker autenticado.
Entender las lecturas
- La presión se informa a nivel de la estación en hPa. Los servicios meteorológicos dan la presión reducida al nivel del mar, así que tu valor será menor en altitud. Las tendencias importan más que el valor absoluto: un descenso constante durante unas horas suele preceder al mal tiempo.
- La altitud puede estimarse con
bme.readAltitude(1013.25), pero solo es tan buena como la presión a nivel del mar que proporciones, y varía con el tiempo meteorológico. - El autocalentamiento es mínimo en modo forzado con frecuencias de muestreo bajas. En modo normal con sobremuestreo y un tiempo de espera corto, el chip y el ESP32 cercano pueden calentar la lectura de forma apreciable.
Solución de problemas
- "No BME280 found". Ejecuta un sketch escáner I2C. Si no aparece ningún dispositivo, es el cableado; si aparece en una dirección inesperada, es la configuración del pin SDO.
- Humedad siempre 0 o NaN. Tienes un BMP280. La impresión del ID del chip lo confirma.
- Presión desviada un 10 por ciento. Olvidaste dividir los pascales entre 100, o la comparas con la presión a nivel del mar.
- Comando de intervalo ignorado. Comprueba que el topic coincide exactamente, incluidas mayúsculas y minúsculas, y que el payload es un número sin más.
- La placa se reinicia al conectar el Wi-Fi. Una alimentación USB débil provoca caídas de tensión en el ESP32. Usa un cable o una fuente mejores.
Configuración para producción
Para despliegues reales, crea un broker en app.connectmq.com y cambia el sketch a WiFiClientSecure en el puerto 8883 con el certificado ISRG Root X1, conectándote con YOUR_CLIENT_ID, YOUR_USERNAME y YOUR_PASSWORD. Así, solo los clientes con tus credenciales pueden enviar comandos de intervalo. Guardar las credenciales Wi-Fi en el código es el siguiente punto débil; la guía de configuración de WiFiManager muestra cómo introducirlas mediante un portal cautivo. Si estás comparando sensores, el tutorial de ESP32 con DHT22 cubre la opción más barata de un solo hilo.
Almacenar y representar los datos meteorológicos
El broker entrega mensajes; no es una base de datos. Para el historial meteorológico necesitas un suscriptor que escriba cada lectura en un sitio duradero. Tres patrones habituales funcionan bien con este payload:
- Telegraf con la entrada MQTT consumer se suscribe a
.../telemetry, analiza los campos JSON y los escribe en InfluxDB, listos para las gráficas de Grafana. - Node-RED se suscribe con un nodo
mqtt in, reorganiza el mensaje y escribe en cualquier base de datos para la que tenga un nodo, algo práctico si además quieres enviar notificaciones. - Un pequeño script de Python con paho-mqtt inserta filas en SQLite o PostgreSQL. Es la opción más transparente cuando estás aprendiendo.
Los tres se conectan al broker como clientes MQTT normales con sus propios client IDs, exactamente igual que el ESP32. En el portal de ConnectMQ aparecen junto a tus dispositivos, y el explorador de topics en vivo es una forma rápida de confirmar la forma del JSON antes de escribir código de análisis. Como el dispositivo incluye el interval activo en cada mensaje, tus gráficas también pueden mostrar cuándo cambió la frecuencia de muestreo, lo que evita interpretar un hueco como una caída.
Empieza con ConnectMQ
Un nodo meteorológico es tan útil como el broker que recoge sus datos. ConnectMQ es un broker MQTT gestionado con TLS, retained messages, MQTT 3.1.1 y 5.0 y un portal con explorador de topics en vivo y alertas por umbral, para que recibas un aviso cuando la presión cae rápido. El plan gratuito Serverless no requiere tarjeta de crédito. Si los conceptos son nuevos para ti, lee qué hace un broker MQTT y después explora la serie completa del tutorial de IoT con ESP32.
Preguntas frecuentes
¿Cuál es la diferencia entre el BME280 y el BMP280?
El BME280 mide temperatura, humedad y presión; el BMP280 solo mide temperatura y presión. Comparten encapsulado y librería, y el ID del chip (0x60 frente a 0x58) los distingue.
¿Por qué hacer configurable el intervalo de publicación por MQTT?
Te permite muestrear más rápido mientras depuras o durante un evento y más despacio el resto del tiempo, sin volver a grabar el firmware. Guardarlo en la flash conserva el ajuste entre reinicios.
¿Debe ser retenido el comando de intervalo?
Sí, para los ajustes. Un comando retenido llega al dispositivo aunque estuviera desconectado cuando lo enviaste, y el dispositivo informa del valor aplicado en su propio topic retenido.
¿Puedo alimentar el nodo BME280 con baterías?
Sí. Usa deep sleep entre lecturas, mantén el sensor en modo forzado y conéctate al Wi-Fi solo para publicar. El tiempo de Wi-Fi domina el consumo de energía.



