ESP32 Environmental Sensor with MQTT

Wire three sensors to an ESP32 and publish live environmental data via MQTT.

Introduction

30 Second Summary

Imagine knowing the exact water level in your rain barrel, the temperature outside, and whether it's raining without ever stepping outside. Smart home monitoring systems collect this data automatically, but the sensors that feed them don't appear out of thin air.

In this project, you will build a multi-sensor environmental monitoring node using an ESP32 microcontroller. Your device will read water level, temperature, pressure, and rainfall data from three different sensors, then publish everything to the internet over MQTT every 10 seconds.

What You'll Build

A working IoT sensor node that autonomously collects environmental data and publishes structured JSON readings to any MQTT-compatible system over WiFi.

By the end of this project, you'll have:

  • A wired breadboard prototype with three sensors (BMP280, ultrasonic, and raindrop) connected to an ESP32.
  • A complete Arduino sketch that reads temperature, pressure, water level distance, and rain status using I2C, GPIO, and ADC protocols.
  • Verified MQTT messages viewable in a test client, proving end-to-end data flow from physical sensor to network.
  • Secret Mission: Add ESP32 deep sleep to wake up, read, publish, and sleep again for solar power efficiency.

Are there any prerequisites?

You need a basic understanding of C/C++ syntax (variables, functions, loops) and all the hardware listed in the project description: an ESP32 dev board, BMP280 sensor, JSN-SR04T ultrasonic sensor, raindrop sensor module, breadboard, jumper wires, and a USB cable.

No prior experience with IoT, sensors, or MQTT is required. This project teaches those from scratch.

Wire and Read the BMP280

Your Arduino IDE is configured and your ESP32 is ready to receive code. Now it's time to connect your first sensor and start reading real environmental data.

The BMP280 is a temperature and pressure sensor that communicates using the I2C protocol. I2C lets multiple sensors share just two wires for data, making it ideal for projects with several sensors on one microcontroller. In this step, you'll wire the BMP280 to your ESP32 and write a sketch that reads temperature, pressure, and altitude.

In this step, get ready to:
  • Wire the BMP280 sensor to the ESP32 using I2C.
  • Write a sketch that reads temperature, pressure, and altitude.
  • Verify live sensor data in the Serial Monitor.
Wire the BMP280 and create the sketch

The BMP280 uses four wires to communicate with the ESP32. Two carry power, and two carry I2C data (SDA for data, SCL for clock).

  • Disconnect your ESP32 from USB before wiring.
  • Using jumper wires and your breadboard, make the following connections:
  • BMP280 VIN to ESP32 3.3V.
  • BMP280 GND to ESP32 GND.
  • BMP280 SCL to ESP32 GPIO22.
  • BMP280 SDA to ESP32 GPIO21.

Why GPIO21 and GPIO22?

These are the ESP32's default I2C pins. The Wire library automatically uses GPIO21 for SDA (data) and GPIO22 for SCL (clock) unless you tell it otherwise.

  • Reconnect your ESP32 via USB after wiring is complete.
  • In Arduino IDE, create a new sketch by clicking File then New Sketch.
  • Save the sketch immediately by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Name it sensor_node and click Save.
  • Delete any existing code in the sketch, then paste the following setup code:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>

// Create BMP280 sensor object
Adafruit_BMP280 bmp;

void setup() {
  // Start serial communication for debugging
  Serial.begin(115200);
  delay(1000);

  // Try connecting to BMP280 at address 0x76, fall back to 0x77
  if (!bmp.begin(0x76)) {
    Serial.println("BMP280 not found! Check wiring. Trying 0x77...");
    if (!bmp.begin(0x77)) {
      Serial.println("BMP280 not found at 0x77 either. Check connections.");
    }
  }
}

What does this code do?

  • Wire.h enables I2C communication on the ESP32's default pins.
  • Adafruit_BMP280 bmp creates a sensor object you'll use to read temperature and pressure.
  • bmp.begin(0x76) tries to connect to the sensor at I2C address 0x76 (the most common address for BMP280 modules). If it fails, the code tries 0x77 as a fallback.
  • You won't see output yet. The next chunk adds the loop() function that reads sensor values.
Read sensor data in the loop

The loop() function runs continuously after setup() finishes. Each iteration reads the BMP280 and prints the values to the Serial Monitor.

  • Add this loop() function below your setup() function:
void loop() {
  // Read temperature in Celsius
  float temperature = bmp.readTemperature();
  // Read pressure in Pascals, convert to hPa
  float pressure = bmp.readPressure() / 100.0;
  // Read altitude using sea-level pressure reference
  float altitude = bmp.readAltitude(1013.25);

  // Print all readings to Serial Monitor
  Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" C");
  Serial.print("Pressure: "); Serial.print(pressure); Serial.println(" hPa");
  Serial.print("Altitude: "); Serial.print(altitude); Serial.println(" m");

  // Wait 2 seconds before next reading
  delay(2000);
}

What does this code do?

  • bmp.readTemperature() returns the temperature in Celsius.
  • bmp.readPressure() returns atmospheric pressure in Pascals. Dividing by 100 converts it to hectopascals (hPa), the standard weather unit.
  • bmp.readAltitude(1013.25) estimates your altitude above sea level using the standard sea-level pressure of 1013.25 hPa as a reference.
  • delay(2000) pauses for 2 seconds between readings so the Serial Monitor stays readable.
  • Save the sketch by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Make sure you've saved your sketch before moving to the next substep.

ⓧ I'd like to double check the full code

Here's the complete sensor_node.ino at this point:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>

Adafruit_BMP280 bmp;

void setup() {
  Serial.begin(115200);
  delay(1000);
  if (!bmp.begin(0x76)) {
    Serial.println("BMP280 not found! Check wiring. Trying 0x77...");
    if (!bmp.begin(0x77)) {
      Serial.println("BMP280 not found at 0x77 either. Check connections.");
    }
  }
}

void loop() {
  float temperature = bmp.readTemperature();
  float pressure = bmp.readPressure() / 100.0;
  float altitude = bmp.readAltitude(1013.25);
  Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" C");
  Serial.print("Pressure: "); Serial.print(pressure); Serial.println(" hPa");
  Serial.print("Altitude: "); Serial.print(altitude); Serial.println(" m");
  delay(2000);
}
Upload and verify readings

Your sketch is ready to flash onto the ESP32. Uploading compiles the code and writes it to the board's memory.

  • Click the Upload button (the right-facing arrow icon in the top toolbar).
  • Wait for the console at the bottom to show "Done uploading." This may take 10-20 seconds.
  • Switch back to the Serial Monitor (Tools then Serial Monitor).
  • Make sure the baud rate is set to 115200 in the dropdown at the top right of the Serial Monitor.

You should see new lines appearing every 2 seconds with temperature (expect 20-30 C at room temperature), pressure (around 1000-1020 hPa), and altitude in meters.

  • Try breathing gently on the sensor. The temperature reading should rise slightly, confirming you're reading live data.

Seeing 0 or NaN values?

  • Double-check your SDA and SCL wires. SDA must go to GPIO21 and SCL must go to GPIO22.
  • If Serial Monitor says "BMP280 not found", your sensor may use address 0x77 instead of 0x76. The code already tries both, so check physical connections first.
  • Make sure the baud rate in the Serial Monitor matches 115200. A mismatch produces garbled text.

Help me troubleshoot my BMP280 readings on ESP32. I'm getting 0 or NaN values in the Serial Monitor.

Your BMP280 is reading live environmental data over I2C. Next up, you'll wire the ultrasonic distance sensor and rain sensor to add water-level monitoring and rain detection to your node.

Wire and Read the Ultrasonic and Rain Sensors

Your BMP280 is reading temperature and pressure like a charm. Now it is time to add spatial awareness to your sensor node.

In this step, you will wire two more sensors to your ESP32. The ultrasonic sensor measures water level distance using GPIO trigger/echo pulses. The raindrop sensor detects rainfall using analog-to-digital conversion (ADC). Together with the BMP280, these give your node a complete picture of the local environment.

In this step, get ready to:
  • Wire the JSN-SR04T ultrasonic sensor and configure its GPIO pins.
  • Wire the raindrop sensor module and configure its analog/digital pins.
  • Write a distance measurement function and read all sensors in the loop.
Wire the JSN-SR04T ultrasonic sensor

The JSN-SR04T is a waterproof ultrasonic distance sensor. It works by sending a short ultrasonic pulse from a TRIG pin and measuring how long the echo takes to return on an ECHO pin. The time-of-flight tells you how far away the nearest surface is.

  • Wire the JSN-SR04T to your ESP32 on the breadboard using these connections:
  • JSN-SR04T VCC to ESP32 5V (Vin pin).
  • JSN-SR04T GND to ESP32 GND.
  • JSN-SR04T TRIG to ESP32 GPIO5.
  • JSN-SR04T ECHO to ESP32 GPIO18.

Why use the 5V Vin pin?

The JSN-SR04T requires 5V to operate its ultrasonic transducer. The ESP32's 3.3V output is not enough. The Vin pin passes through the USB 5V supply directly.

Voltage divider for the ECHO pin

The ECHO pin outputs 5V but ESP32 GPIOs are rated for 3.3V. For prototyping, the ESP32 can tolerate brief 5V pulses. For a permanent installation, add a voltage divider using a 1k ohm resistor in series and a 2k ohm resistor to ground on the ECHO line.

Now add the ultrasonic pin definitions and their pin modes to your sketch.

  • In sensor_node.ino, find the line that says Adafruit_BMP280 bmp;.
  • Add these pin definitions directly above that line (after the #include statements):
#define TRIG_PIN 5
#define ECHO_PIN 18
  • Inside setup(), add these two lines after delay(1000); and before the bmp.begin() block:
// Configure ultrasonic sensor pins
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);

What do these pin modes do?

TRIG_PIN is set as OUTPUT because the ESP32 sends a short pulse to trigger the ultrasonic measurement. ECHO_PIN is set as INPUT because it receives the returning echo signal from the sensor.

Wire the raindrop sensor module

The raindrop sensor module (MH-RD) has two outputs. The analog output (AO) gives a value from 0 to 4095 representing how much water is on the sensor board. The digital output (DO) gives a simple yes/no rain detection based on a threshold you set with the onboard potentiometer.

  • Wire the raindrop sensor module to your ESP32 using these connections:
  • Raindrop module VCC to ESP32 3.3V.
  • Raindrop module GND to ESP32 GND.
  • Raindrop module AO (analog output) to ESP32 GPIO34.
  • Raindrop module DO (digital output) to ESP32 GPIO26.

Why GPIO34 for the analog output?

GPIO34 is one of the ESP32's input-only pins on the ADC1 channel. Input-only pins are ideal for analog reads because they have no output circuitry that could interfere with the measurement. The analogRead() function returns a value from 0 to 4095 on the ESP32's 12-bit ADC. A lower value means more water is present on the sensor.

  • Add the raindrop pin definitions in sensor_node.ino directly below the ultrasonic defines you just added:
#define RAIN_ANALOG_PIN 34
#define RAIN_DIGITAL_PIN 26
  • Inside setup(), add this line after pinMode(ECHO_PIN, INPUT);:
pinMode(RAIN_DIGITAL_PIN, INPUT);

Why no pinMode for RAIN_ANALOG_PIN?

GPIO34 is a dedicated input-only pin. The ESP32 automatically treats it as an analog input when you call analogRead(), so no explicit pinMode() is needed. The digital output pin (GPIO26) does need to be set as INPUT to read the comparator's HIGH/LOW signal.

Write the distance measurement function and update the loop

Now that both sensors are wired and their pins are configured, you will write a function to trigger the ultrasonic sensor and calculate distance from the echo duration. Then you will update the loop to read all three sensors together.

  • In sensor_node.ino, add the following readDistance() function between Adafruit_BMP280 bmp; and void setup():
// Measure distance using ultrasonic trigger/echo
float readDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duration == 0) return -1.0;
  return duration * 0.034 / 2.0;
}

What does this function do?

The function sends a 10-microsecond HIGH pulse on the TRIG pin to start a measurement. It then listens on the ECHO pin using pulseIn(), which returns how many microseconds the echo took to come back.

The formula duration * 0.034 / 2.0 converts that time to centimeters. Sound travels at 0.034 cm per microsecond, and you divide by 2 because the pulse travels to the object and back. If pulseIn() returns 0 (timeout, no echo received), the function returns -1.0 to indicate an invalid reading.

Next, replace the entire loop() function with this updated version that reads all three sensors.

  • Select the entire void loop() { ... } function in your sketch and replace it with this code:
void loop() {
  // Read BMP280 temperature and pressure
  float temperature = bmp.readTemperature();
  float pressure = bmp.readPressure() / 100.0;

  // Read ultrasonic distance
  float waterDistance = readDistance();

  // Read rain sensor analog and digital values
  int rainAnalog = analogRead(RAIN_ANALOG_PIN);
  int rainDigital = digitalRead(RAIN_DIGITAL_PIN);
  bool rainDetected = (rainDigital == LOW);

  // Print all sensor readings to Serial Monitor
  Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" C");
  Serial.print("Pressure: "); Serial.print(pressure); Serial.println(" hPa");
  Serial.print("Water Distance: "); Serial.print(waterDistance); Serial.println(" cm");
  Serial.print("Rain Analog: "); Serial.println(rainAnalog);
  Serial.print("Rain Detected: "); Serial.println(rainDetected ? "YES" : "NO");

  delay(2000);
}

What changed in the loop?

The altitude reading is removed because you will not need it for the MQTT payload later. Three new reads are added: readDistance() for the ultrasonic sensor, analogRead(RAIN_ANALOG_PIN) for the rain intensity value (0-4095), and digitalRead(RAIN_DIGITAL_PIN) for rain detection.

The digital rain pin reads LOW when rain is detected and HIGH when dry. That is why rainDetected is set to true when the pin reads LOW.

  • Save sensor_node.ino.
  • Click the **Upload** button (right arrow icon) in the Arduino IDE toolbar to upload the sketch to your ESP32.
  • Open the **Serial Monitor** (Tools > Serial Monitor) and confirm the baud rate is set to 115200.
  • Place your hand about 30cm in front of the ultrasonic sensor and watch the Water Distance value change.
  • Touch the raindrop sensor board with a wet finger and watch the Rain Analog value drop and Rain Detected switch to YES.

Seeing -1.00 cm for Water Distance?

A value of -1.0 means the ultrasonic sensor did not receive an echo within the timeout. Make sure nothing is blocking the sensor head and that an object is within 20cm to 600cm range. Also double-check that TRIG is wired to GPIO5 and ECHO to GPIO18.

If Rain Analog stays at 4095 even with a wet finger, verify that AO is connected to GPIO34 and VCC is connected to 3.3V (not 5V).

Help me troubleshoot my ultrasonic or rain sensor readings on ESP32.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>

#define TRIG_PIN 5
#define ECHO_PIN 18
#define RAIN_ANALOG_PIN 34
#define RAIN_DIGITAL_PIN 26

Adafruit_BMP280 bmp;

float readDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duration == 0) return -1.0;
  return duration * 0.034 / 2.0;
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(RAIN_DIGITAL_PIN, INPUT);
  if (!bmp.begin(0x76)) {
    Serial.println("BMP280 not found! Check wiring. Trying 0x77...");
    if (!bmp.begin(0x77)) {
      Serial.println("BMP280 not found at 0x77 either. Check connections.");
    }
  }
}

void loop() {
  float temperature = bmp.readTemperature();
  float pressure = bmp.readPressure() / 100.0;
  float waterDistance = readDistance();
  int rainAnalog = analogRead(RAIN_ANALOG_PIN);
  int rainDigital = digitalRead(RAIN_DIGITAL_PIN);
  bool rainDetected = (rainDigital == LOW);
  Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" C");
  Serial.print("Pressure: "); Serial.print(pressure); Serial.println(" hPa");
  Serial.print("Water Distance: "); Serial.print(waterDistance); Serial.println(" cm");
  Serial.print("Rain Analog: "); Serial.println(rainAnalog);
  Serial.print("Rain Detected: "); Serial.println(rainDetected ? "YES" : "NO");
  delay(2000);
}

All three sensors are now reading and printing data. Next up, you will connect the ESP32 to WiFi and publish these readings to an MQTT broker so any device on your network can receive them.

Connect to WiFi and Publish via MQTT

Your ESP32 can now read temperature, pressure, water level distance, and rain detection from three different sensors. The data prints to the Serial Monitor, but it stays trapped on your desk.

In this step, you will connect the ESP32 to your WiFi network and publish all sensor readings to an MQTT broker over the internet. This means any device on your network (or the internet) can subscribe to your sensor data in real time.

In this step, get ready to:
  • Connect your ESP32 to WiFi and confirm its IP address.
  • Set up an MQTT client that reconnects automatically.
  • Format sensor data as JSON and publish it to a public MQTT broker every 10 seconds.
Add WiFi and MQTT configuration

Before your ESP32 can send data anywhere, it needs WiFi credentials and an MQTT broker address. You will use test.mosquitto.org as a free public broker that requires no account or authentication on port 1883.

  • In your sensor_node.ino sketch, select all the code at the top of the file (from the first #include line down through the Adafruit_BMP280 bmp; line) and replace it with the following:
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <PubSubClient.h>

// WiFi credentials
const char* ssid = "your-wifi-ssid-here";
const char* password = "your-wifi-password-here";

// MQTT broker
const char* mqttServer = "test.mosquitto.org";
const int mqttPort = 1883;
const char* mqttClientId = "esp32-sensor-node";
const char* mqttTopic = "home/sensors/environment";

// Pin definitions
#define TRIG_PIN 5
#define ECHO_PIN 18
#define RAIN_ANALOG_PIN 34
#define RAIN_DIGITAL_PIN 26

// Objects
Adafruit_BMP280 bmp;
WiFiClient espClient;
PubSubClient client(espClient);

What does this code do?

  • WiFi.h gives the ESP32 its WiFi networking stack. PubSubClient.h adds the MQTT publish/subscribe protocol on top.
  • ssid and password hold your home WiFi credentials. Replace the placeholder strings with your actual network name and password.
  • WiFiClient espClient creates the underlying TCP connection. PubSubClient client(espClient) layers MQTT messaging on top of that TCP connection.
  • mqttTopic defines the channel name where your sensor data will be published. Any subscriber listening to home/sensors/environment receives your messages.

What is MQTT?

MQTT is a lightweight messaging protocol designed for constrained devices and low-bandwidth networks. It uses a publish/subscribe model where devices publish messages to a topic, and any interested client subscribes to that topic to receive them.

The broker (test.mosquitto.org) sits in the middle. It receives messages from publishers and forwards them to all subscribers on that topic. Your ESP32 publishes. Your computer subscribes to verify the data arrives.

  • Replace your-wifi-ssid-here with your actual WiFi network name, and your-wifi-password-here with your actual WiFi password.

You will not see a change yet. The next chunk adds the functions that use these variables.

Write the WiFi and MQTT connection functions

Your ESP32 needs two helper functions. One connects to WiFi and waits until it gets an IP address. The other connects to the MQTT broker and retries every 5 seconds if the connection fails.

  • Add the connectWiFi() function directly below the PubSubClient client(espClient); line by pasting this code:
// Function to connect to WiFi
void connectWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Connected! IP: ");
  Serial.println(WiFi.localIP());
}

What does this code do?

  • WiFi.begin(ssid, password) tells the ESP32 to start connecting to your WiFi network using the credentials you defined above.
  • The while loop blocks execution until WiFi.status() returns WL_CONNECTED. Each half-second it prints a dot so you can watch the progress in Serial Monitor.
  • Once connected, it prints the ESP32's local IP address so you know it joined the network successfully.
  • Add the connectMQTT() function directly below connectWiFi() by pasting this code:
// Function to connect to MQTT broker
void connectMQTT() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect(mqttClientId)) {
      Serial.println("connected!");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" retrying in 5 seconds");
      delay(5000);
    }
  }
}

What does this code do?

  • client.connected() checks whether the MQTT session is still alive. If not, the while loop retries.
  • client.connect(mqttClientId) attempts to authenticate with the broker using the client ID esp32-sensor-node. The public broker at test.mosquitto.org requires no username or password.
  • If the connection fails, client.state() prints a numeric error code (useful for debugging), then the function waits 5 seconds before retrying.

Now you need to call these functions from setup() so WiFi and the MQTT server are configured when the ESP32 boots.

  • In your setup() function, find the closing brace } at the very end of setup.
  • Add these two lines just before that closing brace:
  connectWiFi();
  client.setServer(mqttServer, mqttPort);
  • Save sensor_node.ino and click the Upload button (right arrow icon) to flash it to your ESP32.
  • Open the Serial Monitor (Tools > Serial Monitor) and set the baud rate to 115200. You should see dots printing as the ESP32 connects to WiFi, followed by a line that says Connected! IP: and your local IP address.

Not seeing an IP address?

  • Double-check that your ssid and password strings exactly match your WiFi network name and password. WiFi credentials are case-sensitive.
  • Make sure you are connecting to a 2.4 GHz network. The ESP32 does not support 5 GHz WiFi bands.
  • If the dots print indefinitely, press the RST button on your ESP32 to reboot and try again.

Still stuck? Help me debug my ESP32 WiFi connection. I see dots printing but it never connects.

Format JSON and publish sensor data to MQTT

With WiFi and MQTT connected, you can now restructure your loop() function. Instead of printing raw values to the Serial Monitor, it will format all readings into a JSON string and publish it to the broker every 10 seconds.

  • Replace your entire existing loop() function with the following code:
void loop() {
  // Ensure MQTT connection
  if (!client.connected()) {
    connectMQTT();
  }
  client.loop();

  // Read BMP280
  float temperature = bmp.readTemperature();
  float pressure = bmp.readPressure() / 100.0;

  // Read ultrasonic distance
  float waterDistance = readDistance();

  // Read rain sensor
  int rainAnalog = analogRead(RAIN_ANALOG_PIN);
  int rainDigital = digitalRead(RAIN_DIGITAL_PIN);
  bool rainDetected = (rainDigital == LOW);

  // Format JSON payload
  char payload[256];
  snprintf(payload, sizeof(payload),
    "{\"temperature\": %.1f, \"pressure\": %.1f, \"water_distance_cm\": %.1f, \"rain_analog\": %d, \"rain_detected\": %s}",
    temperature, pressure, waterDistance, rainAnalog, rainDetected ? "true" : "false");

  // Publish to MQTT
  if (client.publish(mqttTopic, payload)) {
    Serial.print("Published: ");
    Serial.println(payload);
  } else {
    Serial.println("Publish failed!");
  }

  // Wait 10 seconds before next reading
  delay(10000);
}

What does this code do?

  • client.loop() keeps the MQTT connection alive by processing incoming/outgoing packets. Call it every iteration.
  • snprintf formats all five sensor values into a single JSON string. The char payload[256] buffer is large enough for this payload (the PubSubClient default maximum packet size is 256 bytes).
  • client.publish(mqttTopic, payload) sends the JSON string to the topic home/sensors/environment. Any subscribed client receives it instantly.
  • The 10-second delay sets your reporting interval. Your ESP32 reads, formats, publishes, then sleeps for 10 seconds.

Why JSON for sensor data?

JSON gives each value a named key (like temperature or rain_detected). Any system that receives the message can parse it without guessing which number is which. This is critical when you add more sensors later or when multiple devices publish to the same broker.

  • Save sensor_node.ino and upload it to your ESP32.
  • Open the Serial Monitor. You should see the WiFi connection sequence, then every 10 seconds a line starting with Published: followed by your JSON payload.

Now verify the messages are actually arriving at the broker by subscribing from your computer.

  • Open a web browser and go to the HiveMQ web client at http://www.hivemq.com/demos/websocket-client/.
  • In the Host field, enter test.mosquitto.org. Set Port to 8081 (the WebSocket port for this broker).
  • Click Connect.
  • In the Subscriptions section, enter home/sensors/environment as the topic and click Subscribe.
  • Watch the Messages panel. Within 10 seconds you should see your ESP32's JSON messages appearing with live sensor values.

MQTT Explorer as an alternative

MQTT Explorer is a free desktop app that gives you a tree view of all topics on a broker. If you prefer a richer interface, download it and connect to test.mosquitto.org on port 1883. Your topic home/sensors/environment appears in the tree with live values updating.

Messages not showing up in the web client?

  • Check your Serial Monitor. If it says Publish failed! then the MQTT connection dropped. The ESP32 will reconnect on the next loop iteration.
  • Make sure the topic you subscribed to in the HiveMQ client exactly matches home/sensors/environment. MQTT topics are case-sensitive.
  • If the Serial Monitor shows Published: lines but the web client shows nothing, the public broker may be under heavy load. Wait 30 seconds and refresh the page.

Still stuck? Help me debug my ESP32 MQTT publishing. I see Published lines in the Serial Monitor but no messages in the web client.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before continuing.

ⓧ I'd like to double check the full code

Here is the complete sensor_node.ino at the end of this step:

#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <PubSubClient.h>

// WiFi credentials
const char* ssid = "your-wifi-ssid-here";
const char* password = "your-wifi-password-here";

// MQTT broker
const char* mqttServer = "test.mosquitto.org";
const int mqttPort = 1883;
const char* mqttClientId = "esp32-sensor-node";
const char* mqttTopic = "home/sensors/environment";

// Pin definitions
#define TRIG_PIN 5
#define ECHO_PIN 18
#define RAIN_ANALOG_PIN 34
#define RAIN_DIGITAL_PIN 26

// Objects
Adafruit_BMP280 bmp;
WiFiClient espClient;
PubSubClient client(espClient);

// Function to connect to WiFi
void connectWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Connected! IP: ");
  Serial.println(WiFi.localIP());
}

// Function to connect to MQTT broker
void connectMQTT() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect(mqttClientId)) {
      Serial.println("connected!");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" retrying in 5 seconds");
      delay(5000);
    }
  }
}

// Function to read ultrasonic distance
float readDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duration == 0) return -1.0;
  float distance = duration * 0.034 / 2.0;
  return distance;
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(RAIN_DIGITAL_PIN, INPUT);
  if (!bmp.begin(0x76)) {
    Serial.println("BMP280 not found! Check wiring. Trying 0x77...");
    if (!bmp.begin(0x77)) {
      Serial.println("BMP280 not found at 0x77 either. Check connections.");
    }
  }
  connectWiFi();
  client.setServer(mqttServer, mqttPort);
}

void loop() {
  if (!client.connected()) {
    connectMQTT();
  }
  client.loop();
  float temperature = bmp.readTemperature();
  float pressure = bmp.readPressure() / 100.0;
  float waterDistance = readDistance();
  int rainAnalog = analogRead(RAIN_ANALOG_PIN);
  int rainDigital = digitalRead(RAIN_DIGITAL_PIN);
  bool rainDetected = (rainDigital == LOW);
  char payload[256];
  snprintf(payload, sizeof(payload),
    "{\"temperature\": %.1f, \"pressure\": %.1f, \"water_distance_cm\": %.1f, \"rain_analog\": %d, \"rain_detected\": %s}",
    temperature, pressure, waterDistance, rainAnalog, rainDetected ? "true" : "false");
  if (client.publish(mqttTopic, payload)) {
    Serial.print("Published: ");
    Serial.println(payload);
  } else {
    Serial.println("Publish failed!");
  }
  delay(10000);
}

Your sensor node is now a fully networked device. It reads physical data from three sensors and publishes it to the internet every 10 seconds. In the Secret Mission, you will make this setup solar-friendly by putting the ESP32 into deep sleep between readings.

Secret mission

Add Deep Sleep for Solar Power

Your sensor node works, but it draws 80-240mA constantly with WiFi active. For solar-powered deployment, that drains a battery fast. Can you restructure the sketch to wake up, read, publish, and sleep, dropping idle current to approximately 10 microamps?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely on local hardware with a free public MQTT broker, so there are no ongoing costs.

Resources you used:

  • MQTT messages published to test.mosquitto.org (public broker, messages expire automatically).
  • Arduino sketch files saved locally (sensor_node.ino and sensor_node_deepsleep.ino).
  • Libraries installed in Arduino IDE (Adafruit BMP280, PubSubClient).

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with a follow-up project.

  • Leave the ESP32 plugged in via USB. It continues reading sensors and publishing MQTT data every 10 seconds.
  • When you build your next project (such as a Raspberry Pi with Home Assistant), you can point it at the same MQTT topic home/sensors/environment to receive live data.
  • Your Arduino IDE libraries and board packages remain installed for future ESP32 projects.

Pause - I'll come back to this later

Shut down the running sensor node without removing anything from your computer.

  • Unplug the ESP32 from USB. The sensor node stops immediately.
  • Your sketch files and installed libraries remain in Arduino IDE, ready for when you come back.
  • Nothing persists that costs money. The public MQTT broker does not store messages long-term.

Delete - I don't want to use this again

Remove all project files and libraries from your computer.

  • Unplug the ESP32 from USB.
  • Delete your sketch files (sensor_node.ino and sensor_node_deepsleep.ino) from wherever you saved them on your computer.
  • To remove the installed libraries, open Arduino IDE. Go to Sketch > Include Library > Manage Libraries. Search for "Adafruit BMP280" and click Remove. Repeat for "PubSubClient".
  • MQTT messages on test.mosquitto.org expire automatically. No action needed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional IoT environmental monitoring node from scratch. Your ESP32 is now autonomously reading water level, temperature, pressure, and rainfall data, then publishing it all as structured JSON over WiFi to an MQTT broker.

You've learned how to:

  • Wired and programmed three sensors (BMP280 via I2C, JSN-SR04T ultrasonic via GPIO trigger/echo, and raindrop module via ADC) to a single ESP32.
  • Connected the ESP32 to WiFi and published structured JSON sensor data to an MQTT broker over the network.
  • Verified end-to-end data flow from physical sensor to network message using MQTT Explorer or the HiveMQ web client.
  • Secret Mission: Implemented deep sleep for solar-compatible power efficiency, reducing idle current draw from 80-240mA to approximately 10 microamps.

Ready to quiz yourself?