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.

Project Difficulty: Intermediate (Requires basic Linux CLI and Arduino IDE experience)
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.

Callout Tip: Never edit the main /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

  1. Broker Status: SSH into the Pi and run sudo systemctl status mosquitto. If it says active (running), the service is up. If it failed, run sudo journalctl -u mosquitto -n 20 to find syntax errors in your config file.
  2. Firewall Rules: Raspberry Pi OS often defaults to UFW or iptables blocking inbound traffic. Run sudo ufw allow 1883/tcp to ensure the MQTT port is open.
  3. Subnet Routing: Ensure your ESP32 and Pi are on the exact same VLAN/Subnet. If your Pi is on 192.168.1.x and your ESP32 grabs an IP on 192.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.
Pro-Tip for Mosquitto Logging: By default, Mosquitto suppresses connection logs. To see exact error strings in real-time, add 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:

  1. 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 the sensors/lab/bme280 topic and pipe the JSON into a live gauge dashboard.
  2. TimescaleDB Integration: Mosquitto is a message router, not a database. Use the mosquitto-sub CLI tool or a Python script to subscribe to all topics (#) and insert the payloads into a TimescaleDB instance for long-term historical trending.
  3. mTLS Authentication: Move beyond allow_anonymous. Generate a Certificate Authority (CA) using OpenSSL, issue client certificates for every ESP32, and configure Mosquitto to require require_certificate true on 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.