To run a local IoT network using a Mosquitto Raspberry Pi broker, install the broker via apt, configure listener 1883 and allow_anonymous true in your configuration file, and point your ESP32 client to the Pi’s static IP address. The most common failure point in 2026 is Mosquitto 2.0’s default security behavior, which blocks external connections unless explicitly bound to a network interface.
This guide walks through the exact hardware, the critical configuration parameters, the complete ESP32 C++ firmware, and a debugging matrix for the exact error strings you will encounter on the bench.
Hardware Spec Sheet & Pin Mapping
This build targets the ESP32-WROOM-32 DevKit V1 as the embedded sensor node, publishing environmental data to a Raspberry Pi 4 Model B (4GB) acting as the central broker. We use the BME280 I2C sensor for temperature, humidity, and barometric pressure.
Estimated Time: 45 minutes
Parts List
- Broker: Raspberry Pi 4 Model B (4GB RAM) or Raspberry Pi 5 (4GB)
- Client: ESP32-WROOM-32 DevKit V1 (30-pin variant)
- Sensor: BME280 I2C Breakout Board (3.3V logic compatible)
- Wiring: 22 AWG solid core jumper wires, half-size breadboard
- Power: 5V 3A USB-C power supply for the Pi, 5V 1A micro-USB for the ESP32
ESP32 to BME280 Pin Mapping Table
The ESP32 defaults to GPIO 21 and 22 for I2C communication. Do not use GPIO 6-11 (flash memory) or GPIO 34-39 (input-only).
| ESP32-WROOM-32 Pin | BME280 Breakout Pin | Wire Color (Standard) | Notes |
|---|---|---|---|
| 3V3 | VIN / VCC | Red | BME280 is strictly 3.3V. Do not use 5V. |
| GND | GND | Black | Common ground required. |
| GPIO 21 (SDA) | SDA | Yellow | Default I2C Data line. |
| GPIO 22 (SCL) | SCL | Blue | Default I2C Clock line. |
Mosquitto Configuration: The Data-Dense Breakdown
Before writing any firmware, the Raspberry Pi broker must be configured correctly. Install Mosquitto and the client tools on your Pi:
sudo apt update
sudo apt install mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
The biggest hurdle for embedded developers is the shift introduced in Eclipse Mosquitto 2.0. By default, if no listener is defined, Mosquitto only binds to localhost (127.0.0.1) and requires authentication. Your ESP32 will be instantly rejected. You must edit the configuration file at /etc/mosquitto/conf.d/default.conf.
/etc/mosquitto/mosquitto.conf file directly on Debian-based systems. Drop your custom configurations into the /etc/mosquitto/conf.d/ directory so they aren't overwritten during OS updates.
Below is the exact configuration matrix required for a functional local LAN broker. For deeper parameter definitions, refer to the official Mosquitto configuration documentation.
| Parameter | Value | Function & Impact | Production Warning |
|---|---|---|---|
listener |
1883 | Binds the broker to port 1883 on all network interfaces (0.0.0.0). Mandatory for ESP32 connections. | Restrict to specific IP in production. |
allow_anonymous |
true | Bypasses username/password authentication. Required for basic PubSubClient testing. | Disable and use password_file for deployed nodes. |
persistence |
true | Saves in-flight QoS 1 and 2 messages to disk. Survives Pi reboots without losing queued sensor data. | Ensure SD card has write cycles available. |
max_inflight_messages |
20 | Limits concurrent unacknowledged QoS 1/2 messages per client. Prevents broker memory exhaustion. | Increase only if using high-throughput telemetry. |
max_keepalive |
60 | Forces clients to ping the broker at least every 60 seconds, otherwise the broker drops the socket. | Match this to your ESP32 deep sleep intervals. |
After saving the file, restart the service and verify it is listening on the external interface:
sudo systemctl restart mosquitto
sudo ss -tulpn | grep 1883
You should see 0.0.0.0:1883, confirming the broker is accepting external LAN traffic.
ESP32 Sensor Node: Wiring & Compilable Code
The following C++ firmware is designed for the Arduino IDE (or PlatformIO) targeting the ESP32-WROOM-32 DevKit V1. It requires the PubSubClient and Adafruit BME280 libraries.
A critical detail often missed in embedded MQTT tutorials is the default packet size. PubSubClient defaults to a 256-byte buffer. If your JSON payload exceeds this, the ESP32 will silently drop the packet and disconnect. We explicitly increase this using setBufferSize().
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <ArduinoJson.h>
// --- NETWORK & BROKER DEFINITIONS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.50"; // Replace with your Raspberry Pi IP
const int mqtt_port = 1883;
const char* mqtt_topic = "sensors/lab/bme280";
// --- PIN DEFINITIONS (ESP32 DevKit V1) ---
#define I2C_SDA 21
#define I2C_SCL 22
#define STATUS_LED 2 // Built-in LED on most DevKits
// --- OBJECTS ---
WiFiClient espClient;
PubSubClient client(espClient);
Adafruit_BME280 bme;
// --- TIMING ---
unsigned long lastMsg = 0;
const long interval = 10000; // Publish every 10 seconds
void setup_wifi() {
delay(10);
Serial.print("Connecting to WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
int timeout = 0;
while (WiFi.status() != WL_CONNECTED && timeout < 40) {
delay(500);
Serial.print(".");
timeout++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected. IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi connection failed. Rebooting...");
ESP.restart();
}
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Create a random client ID to prevent session collisions on the Pi
String clientId = "ESP32-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("connected");
digitalWrite(STATUS_LED, HIGH);
} else {
Serial.print("failed, rc=");
Serial.print(client.state()); // Critical for debugging
Serial.println(" retrying in 5 seconds");
digitalWrite(STATUS_LED, LOW);
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(STATUS_LED, OUTPUT);
// Initialize I2C with explicit pins
Wire.begin(I2C_SDA, I2C_SCL);
if (!bme.begin(0x76, &Wire)) { // 0x76 or 0x77 depending on breakout
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
// Increase buffer size to handle JSON serialization safely
client.setBufferSize(512);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsigned long now = millis();
if (now - lastMsg > interval) {
lastMsg = now;
// Read sensor data
float temp = bme.readTemperature();
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100.0F;
// Serialize to JSON
StaticJsonDocument<256> doc;
doc["temperature"] = temp;
doc["humidity"] = hum;
doc["pressure"] = pres;
char buffer[256];
size_t n = serializeJson(doc, buffer);
// Publish with QoS 0 (Fire and forget for local LAN telemetry)
if (client.publish(mqtt_topic, buffer, n)) {
Serial.println("Published: " + String(buffer));
} else {
Serial.println("Publish failed. Buffer overflow or network drop.");
}
}
}
Debugging Matrix: Exact Errors & The First Three Checks
When your ESP32 fails to publish data to the Raspberry Pi, do not start rewriting code. Follow the First Three Checks rule to isolate the network layer before blaming the application layer.
The First Three Things to Check
- Broker Status: SSH into the Pi and run
sudo systemctl status mosquitto. If it saysactive (running), the service is up. If it failed, runsudo journalctl -u mosquitto -n 20to find syntax errors in your config file. - Firewall Rules: Raspberry Pi OS often defaults to UFW or iptables blocking inbound traffic. Run
sudo ufw allow 1883/tcpto ensure the MQTT port is open. - Subnet Routing: Ensure your ESP32 and Pi are on the exact same VLAN/Subnet. If your Pi is on
192.168.1.xand your ESP32 grabs an IP on192.168.50.x(common with IoT guest networks), the connection will time out.
Exact Error Strings & Ranked Causes
If the first three checks pass, consult this debugging matrix based on exact error strings from the Mosquitto logs (/var/log/mosquitto/mosquitto.log) and the ESP32 Serial Monitor.
| Exact Error String | Source | Ranked Causes | Fix |
|---|---|---|---|
Connection Refused: not authorised |
Mosquitto Log / ESP32 rc=5 | 1. Missing allow_anonymous true.2. Mosquitto 2.0 default security blocking external IPs. |
Add allow_anonymous true to conf.d/default.conf and restart service. |
Socket error on client <unknown>, disconnecting. |
Mosquitto Log | 1. ESP32 sending raw TCP instead of MQTT. 2. TLS handshake failure on port 1883. |
Verify client.setServer() is used, not raw WiFiClient writes. Ensure no SSL flags are set on port 1883. |
failed, rc=-2 |
ESP32 Serial Monitor | 1. Network unreachable (wrong IP). 2. Pi is offline or broker crashed. |
Ping the Pi IP from a laptop on the same WiFi. Verify mqtt_server string in code. |
Publish failed. Buffer overflow |
ESP32 Serial Monitor | 1. JSON payload exceeds default 256-byte PubSubClient limit. | Add client.setBufferSize(512); in the setup() function. |
log_dest file /var/log/mosquitto/mosquitto.log and log_type all to your configuration file. Tail it with tail -f /var/log/mosquitto/mosquitto.log while resetting your ESP32.
Scaling the Architecture: Extend or Simplify
Once your baseline Mosquitto Raspberry Pi broker is stable, you will inevitably need to adjust the architecture based on your deployment environment.
How to Simplify the Build
If maintaining a Raspberry Pi feels like overkill for a simple prototype, or if you are struggling with local network routing, drop the Pi entirely. Use a free cloud broker like HiveMQ Cloud or Adafruit IO. You will need to change the mqtt_server to their URL, switch the port to 8883 (TLS), and add the WiFiClientSecure library to your ESP32 code. This eliminates local firewall and IP routing headaches at the cost of internet dependency.
How to Extend the Build
For a robust home-lab or light industrial setup, extend the Pi broker using these three additions:
- Node-RED Dashboard: Install Node-RED on the Pi (
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered.sh)). Subscribe to thesensors/lab/bme280topic and pipe the JSON into a live gauge dashboard. - TimescaleDB Integration: Mosquitto is a message router, not a database. Use the
mosquitto-subCLI tool or a Python script to subscribe to all topics (#) and insert the payloads into a TimescaleDB instance for long-term historical trending. - mTLS Authentication: Move beyond
allow_anonymous. Generate a Certificate Authority (CA) using OpenSSL, issue client certificates for every ESP32, and configure Mosquitto to requirerequire_certificate trueon port 8883. This ensures that even if a rogue device joins your WiFi, it cannot inject fake sensor data into your broker.
By mastering the listener bindings and buffer allocations outlined above, your Mosquitto Raspberry Pi setup will transition from a fragile weekend prototype to a reliable backbone for your embedded IoT network.






