Local MQTT Broker + Automation Bridge with Arduino UNO Q

Local MQTT Broker + Automation Bridge with Arduino UNO Q

01 Overview

This project turns the Arduino UNO Q into a local, cloud-free home automation hub. Instead of relying on a third-party cloud service, the UNO Q hosts its own MQTT broker (Mosquitto) and its own automation logic (Node-RED), while a small network of ESP32-based sensor and actuator nodes report to it and take commands from it.

Two Seeed Studio XIAO ESP32C3 boards act as sensor nodes (motion, ambient light, and sound), and a standard ESP32 dev kit acts as the actuator node, driving a relay-controlled lamp and an SH1106 OLED status display. A live Node-RED dashboard lets you monitor and override everything from a browser on the local network — no internet connection required for the system to function.

Project Use Case

This is aimed at makers and IoT learners who want to understand MQTT publish/subscribe architecture hands-on, without paying for or depending on a cloud IoT platform. It's a practical starting point for anyone building a local smart-home setup, a lab automation demo, or a teaching example of how sensor nodes, a broker, and an automation engine work together on a single local network.

02 Hardware and Software Components

Gather everything below before you start. This is your checklist — names, models, and versions only.

Hardware Components

Component Description
Arduino UNO Q Hub board. Runs Mosquitto broker and Node-RED (with dashboard) on its Linux (MPU) side.
Seeed Studio XIAO ESP32C3 (x2) Sensor nodes. One handles PIR + BH1750, the other handles the sound sensor.
ESP32 Dev Kit Actuator node. Subscribes to MQTT commands and drives the relay + OLED.
PIR Motion Sensor Digital motion detection input.
BH1750 Ambient Light Sensor I2C light level (lux) sensor.
Sound Detector Module (3-pin) Digital sound/clap detection input.
1-Channel Relay Module (mains-rated) Switches a 240V bulb based on MQTT commands.
SH1106 OLED Display Shows live connection and LED state on the actuator node.
USB-C Power Supply Powers the UNO Q.
WiFi Router Local network — must not have client/AP isolation enabled.

Software Tools

Software Version / Details
Arduino IDE Used to flash the XIAO ESP32C3 and ESP32 dev kit boards.
PubSubClient Arduino library by Nick O'Leary — handles MQTT on the ESP32/XIAO nodes.
BH1750 Library Arduino library for reading the BH1750 light sensor over I2C.
U8g2 Arduino library for driving the SH1106 OLED display.
Mosquitto MQTT broker, installed directly on the UNO Q's Linux side.
Node-RED Automation logic and dashboard, installed on the UNO Q.
node-red-dashboard Node-RED palette add-on for the live browser dashboard.

Project Files

All required sketches and the Node-RED flow are provided in the Code section below. Copy each one into the correct location as described in the Software Setup section.

File Description
xiao_sensor_node1.ino PIR + BH1750 sensor node sketch (XIAO ESP32C3 #1).
xiao_sensor_node2.ino Sound sensor node sketch, single-clap edge detection (XIAO ESP32C3 #2).
esp32_actuator_node.ino Relay + OLED actuator sketch, subscribes to MQTT commands (ESP32 Dev Kit).
automation_flow.js Node-RED Function node logic controlling the LED from light level and sound.
Safety Note: The relay switches 240V mains power. Keep all mains wiring (relay COM/NO and the bulb socket) fully enclosed and separated from the low-voltage control side. Never wire mains connections on an open breadboard.

03 Application Discussion

Here is what each component does and why it is part of this project.

Arduino UNO Q

The UNO Q is the hub of the system. Its Linux (MPU) side runs Mosquitto and Node-RED, giving it enough compute to act as a small always-on server, while its real-time MCU side is left free for future direct sensor work. In this project it does no direct sensor wiring — it works purely as a network hub.

XIAO ESP32C3 — Sensor Nodes

Two of these small, low-power boards act as independent sensor nodes over WiFi, each publishing readings to a specific MQTT topic. Splitting sensing across two lightweight boards keeps each node's job simple and easy to debug.

ESP32 Dev Kit — Actuator Node

This board has more GPIO headroom, which is useful since it drives two things at once: the relay (mains switching) and the OLED (status display). It subscribes to a single command topic and reacts the instant a new message arrives.

PIR Motion Sensor

Provides a simple HIGH/LOW motion signal, published so Node-RED's automation logic can react to movement in the room.

BH1750 Light Sensor

Reads ambient light level in lux over I2C. This is the primary automatic control for the LED — below a set threshold, the room is considered "dark."

Sound Detector Module

Detects a single clap or loud sound as a digital HIGH pulse. The sketch uses edge detection so it publishes exactly once per clap, which Node-RED then uses as a manual toggle override.

Relay + Mains Bulb

The physical output of the whole system — a 240V bulb switched by a mains-rated relay, controlled entirely by MQTT commands coming from Node-RED's automation logic.

SH1106 OLED Display

Gives an at-a-glance view of the actuator node's WiFi status, MQTT connection status, and current LED state, without needing to check Serial Monitor.

04 Hardware Setup

Wire the components to the board using the tables below.

XIAO ESP32C3 #1 — PIR + BH1750


Component Pin Board Pin Description
PIR VCC 3V3 Sensor power
PIR GND GND Ground
PIR OUT D2 Digital motion signal
BH1750 VCC 3V3 Sensor power
BH1750 GND GND Ground
BH1750 SDA D4 I2C data
BH1750 SCL D5 I2C clock

XIAO ESP32C3 #2 — Sound Sensor


Pin / Signal Connects To
Sensor 5V 5V
Sensor GND GND
Sensor OUT D2 (digital input)

ESP32 Dev Kit — Relay + OLED


Component Pin Board Pin Description
Relay VCC 5V Relay module power
Relay GND GND Ground
Relay IN GPIO 26 Switching signal from ESP32
Relay COM Mains Live (from wall) 240V mains — see safety note below
Relay NO Mains Live (to bulb) 240V mains — see safety note below
OLED VCC 3V3 Display power
OLED GND GND Ground
OLED SDA GPIO 21 I2C data
OLED SCL GPIO 22 I2C clock

Assembly Instructions

  1. Wire the PIR and BH1750 to XIAO ESP32C3 #1 as shown above, keeping I2C wiring short.
  2. Wire the sound sensor to XIAO ESP32C3 #2, adjusting the onboard sensitivity potentiometer once powered.
  3. Wire the OLED and relay's low-voltage side (VCC/GND/IN) to the ESP32 dev kit.
  4. Wire the relay's mains side (COM/NO) to the bulb socket and wall power, fully inside an enclosure.
  5. Double-check every mains connection before applying power — never test with the enclosure open.
  6. Power on all three nodes and the UNO Q on the same local WiFi network.
Note: The UNO Q itself has no sensors or actuators wired to it directly in this project — it works purely as the network hub, communicating with all three nodes over MQTT/WiFi.

05 Software Setup

Follow these steps in order. Do not skip any step.

Network Requirement: Every device in this project — the UNO Q, both XIAO boards, the ESP32 dev kit, and the computer used for Node-RED — must be on the same WiFi network, and that network must not have client/AP isolation enabled. Mobile hotspots and some SIM-based pocket routers commonly block device-to-device traffic even though internet access works fine; use a standard home router for reliable results.

Step 1 — Set Up the Arduino UNO Q

  1. Connect the UNO Q to your PC via USB-C and install Arduino App Lab.
  2. Complete App Lab's first-time setup, entering your WiFi credentials (this also enables SSH).
  3. Find the board's IP address: ip addr show wlan0 | grep "inet "
  4. SSH into the board: ssh arduino@<board-hostname>.local

Step 2 — Install and Configure Mosquitto

Run the following on the UNO Q over SSH:

Setting Value
Listener 1883 on 0.0.0.0 (not just localhost)
allow_anonymous true (local network testing)
Why this matters: By default Mosquitto only listens on localhost. Without opening it to 0.0.0.0, external devices like the XIAO nodes cannot reach the broker at all.

Local Broker Config File

path
/etc/mosquitto/conf.d/local.conf

Step 3 — Install Node-RED

Still over SSH on the UNO Q, run the official install script:

bash
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
node-red-start

Then install the dashboard palette from inside the Node-RED editor's Manage Palette menu: node-red-dashboard.

Step 4 — Install Libraries for the ESP32/XIAO Boards

In Arduino IDE, install these via Library Manager:

  • PubSubClient (by Nick O'Leary)
  • BH1750 (for XIAO ESP32C3 #1)
  • U8g2 (for the ESP32 dev kit's OLED)

Step 5 — Upload the Sketches

  1. Open each sketch from the Code section below and edit the values in its EDIT THESE block (WiFi SSID/password, broker IP).
  2. Select the correct board in Arduino IDE (XIAO_ESP32C3, or ESP32 Dev Module).
  3. Click Upload, then open Serial Monitor at 115200 baud to confirm WiFi and MQTT connect successfully.

Step 6 — Build the Node-RED Automation Flow

  1. Open Node-RED in your browser at http://<uno-q-ip>:1880
  2. Add three MQTT In nodes, all using the same broker (localhost:1883), subscribed to home/room/motion, home/room/light, and home/room/sound.
  3. Add one Function node using the automation logic in the Code section below.
  4. Add one MQTT Out node, same broker, publishing to home/room/light/set.
  5. Wire all three MQTT In nodes into the Function node, and the Function node into the MQTT Out node.
  6. Click Deploy.

06 Code

Copy each file below into the correct location as described in the Software Setup section. Read the Code Breakdown section to understand what each part does.

xiao_sensor_node1.ino — PIR + BH1750

Arduino / C++
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <BH1750.h>

// --- WiFi credentials ---
const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// --- MQTT broker (your UNO Q's IP) ---
const char* MQTT_BROKER = "192.168.1.42";
const int MQTT_PORT = 1883;

const int PIR_PIN = 2;

WiFiClient espClient;
PubSubClient client(espClient);
BH1750 lightMeter;

void setup_wifi() {
  delay(10);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
}

void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("XIAO_Sensor_Node_1")) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);

  Wire.begin();
  lightMeter.begin();

  setup_wifi();
  client.setServer(MQTT_BROKER, MQTT_PORT);
}

void loop() {
  if (!client.connected()) {
    reconnect_mqtt();
  }
  client.loop();

  bool motion = digitalRead(PIR_PIN) == HIGH;
  float lux = lightMeter.readLightLevel();

  client.publish("home/room/motion", motion ? "1" : "0");
  client.publish("home/room/light", String(lux).c_str());

  Serial.print("Motion: ");
  Serial.print(motion);
  Serial.print(" | Light: ");
  Serial.print(lux);
  Serial.println(" lux");

  delay(1000);
}

xiao_sensor_node2.ino — Sound Sensor (Single Clap Detection)

Arduino / C++
#include <WiFi.h>
#include <PubSubClient.h>

// ==== EDIT THESE ====
const char* WIFI_SSID     = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_BROKER   = "192.168.1.42";
const int   MQTT_PORT     = 1883;
const char* MQTT_TOPIC    = "home/room/sound";
const char* CLIENT_ID     = "XIAO_Sensor_Node_2";
const int   SOUND_PIN     = 2;
const bool  ACTIVE_HIGH   = true;   // flip to false if your module is active-low
const int   PUBLISH_DELAY_MS = 200; // how often to check
// =====================

WiFiClient espClient;
PubSubClient client(espClient);

bool wasDetected = false;  // tracks previous reading, so we only fire on a NEW clap

void setup_wifi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected, IP: " + WiFi.localIP().toString());
}

void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect(CLIENT_ID)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.println(client.state());
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(SOUND_PIN, INPUT);
  setup_wifi();
  client.setServer(MQTT_BROKER, MQTT_PORT);
}

void loop() {
  if (!client.connected()) reconnect_mqtt();
  client.loop();

  bool raw = digitalRead(SOUND_PIN) == HIGH;
  bool soundDetected = ACTIVE_HIGH ? raw : !raw;

  // Only publish when sound just started (wasn't detected last check, is detected now)
  if (soundDetected && !wasDetected) {
    client.publish(MQTT_TOPIC, "1");
    Serial.println("Clap detected! Published.");
  }

  wasDetected = soundDetected;
  delay(PUBLISH_DELAY_MS);
}

esp32_actuator_node.ino — Relay + OLED Actuator

Arduino / C++
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <U8g2lib.h>

// ==== EDIT THESE ====
const char* WIFI_SSID     = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_BROKER   = "192.168.1.42";
const int   MQTT_PORT     = 1883;
const char* SUB_TOPIC     = "home/room/light/set";
const char* CLIENT_ID     = "ESP32_Actuator_Node";
const int   RELAY_PIN     = 26;
const bool  RELAY_ACTIVE_HIGH = true;  // flip if your relay module is active-low
const int   OLED_SDA      = 21;
const int   OLED_SCL      = 22;
// =====================

WiFiClient espClient;
PubSubClient client(espClient);
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

String lastState = "0";
String connStatus = "Connecting...";

void updateOLED() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(0, 12, "MQTT Automation Node");
  u8g2.drawStr(0, 28, ("Status: " + connStatus).c_str());
  u8g2.drawStr(0, 44, ("LED: " + String(lastState == "1" ? "ON" : "OFF")).c_str());
  u8g2.sendBuffer();
}

void setRelay(String state) {
  bool on = (state == "1");
  digitalWrite(RELAY_PIN, (on == RELAY_ACTIVE_HIGH) ? HIGH : LOW);
  lastState = state;
  updateOLED();
}

void callback(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.println("Received: " + msg);
  setRelay(msg);
}

void setup_wifi() {
  connStatus = "WiFi connecting";
  updateOLED();
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  connStatus = "WiFi OK";
  updateOLED();
}

void reconnect_mqtt() {
  while (!client.connected()) {
    connStatus = "MQTT connecting";
    updateOLED();
    if (client.connect(CLIENT_ID)) {
      client.subscribe(SUB_TOPIC);
      connStatus = "Connected";
      updateOLED();
    } else {
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, RELAY_ACTIVE_HIGH ? LOW : HIGH);  // start OFF

  Wire.begin(OLED_SDA, OLED_SCL);
  u8g2.begin();
  updateOLED();

  setup_wifi();
  client.setServer(MQTT_BROKER, MQTT_PORT);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) reconnect_mqtt();
  client.loop();
}

automation_flow.js — Node-RED Function Node

javascript
flow.set(msg.topic, msg.payload);

let lux = parseFloat(flow.get("home/room/light")) || 0;
let sound = String(flow.get("home/room/sound"));
let LUX_THRESHOLD = 50;

let current = flow.get("led_state") || "0";
let controlMode = flow.get("control_mode") || "light"; // "light" or "clap"
let lastLuxState = flow.get("last_lux_state") || "bright";
let turnOn = (current === "1");

let luxState = (lux < LUX_THRESHOLD) ? "dark" : "bright";

// Rule 1: Light only works while in "light" control mode
if (msg.topic === "home/room/light" && controlMode === "light" && luxState !== lastLuxState) {
    turnOn = (luxState === "dark");
    flow.set("last_lux_state", luxState);
}

// Rule 2: Sound always wins, toggles regardless of current mode
if (msg.topic === "home/room/sound" && sound === "1") {
    turnOn = (current === "0");

    // Clap turned LED ON -> lock out light control
    // Clap turned LED OFF -> hand control back to light
    controlMode = turnOn ? "clap" : "light";
    flow.set("control_mode", controlMode);

    // Reset lux tracking so light doesn't immediately re-trigger once unlocked
    flow.set("last_lux_state", luxState);
}

flow.set("led_state", turnOn ? "1" : "0");

msg.payload = turnOn ? "1" : "0";
msg.topic = "home/room/light/set";
return msg;

07 Code Breakdown

Here is what each part of the code does. Read this after uploading.

Libraries

Library Purpose
WiFi.h Connects the ESP32/XIAO board to the local WiFi network.
PubSubClient.h Handles MQTT connect, publish, and subscribe.
Wire.h I2C communication for the BH1750 and OLED.
BH1750.h Reads light level in lux from the BH1750 sensor.
U8g2lib.h Drives the SH1106 OLED display.

Key Functions

setup_wifi()

Connects the board to the configured WiFi network and waits until the connection succeeds before continuing.

reconnect_mqtt()

Connects (or reconnects) to the Mosquitto broker. On the actuator node, it also re-subscribes to the command topic every time it reconnects, since a fresh connection loses any prior subscription.

callback()

Runs automatically on the actuator node whenever a new message arrives on the subscribed topic. It converts the raw payload to a string and passes it to setRelay().

setRelay()

Writes HIGH or LOW to the relay pin based on the received command, accounting for whether the relay module is active-high or active-low, then refreshes the OLED.

updateOLED()

Redraws the OLED with the current WiFi/MQTT status and LED state, called any time either changes.

General Program Workflow

  1. Board boots, connects to WiFi, then connects to the Mosquitto broker on the UNO Q.
  2. Sensor nodes read their sensor(s) and publish to their assigned MQTT topics on a regular interval (or on a detected edge, for the sound sensor).
  3. Node-RED's MQTT In nodes receive each published value and pass it into the Function node.
  4. The Function node applies the light-threshold and clap-toggle logic, decides the LED state, and publishes a command to home/room/light/set.
  5. The actuator node's callback() fires the instant that command arrives, switching the relay and updating the OLED.

08 Testing and Calibration

After uploading, verify each of the following to confirm the system is working correctly.

Broker Connectivity Test

From the UNO Q's SSH session, run mosquitto_sub -h localhost -t "home/room/motion" and confirm you see live 1/0 values as you wave your hand over the PIR. Repeat for home/room/light and home/room/sound.

Common Issue: If nothing appears, subscribe to everything with mosquitto_sub -h localhost -t "#" -v to confirm the exact topic and payload actually being sent, and check for typos in the topic string.

Node-RED Dashboard Test

Open http://<uno-q-ip>:1880 from a browser on the same network and confirm the flow editor loads. Add a Debug node off any MQTT In node to see live payloads.

Common Issue: If the page won't load even though Node-RED is confirmed running (sudo ss -tlnp | grep 1880 shows 0.0.0.0:1880), the network itself likely has client/AP isolation enabled. Confirm with a ping test between the laptop and the UNO Q, and switch to a router without isolation if it fails.

Actuator Response Test

Manually publish a command and confirm the relay and OLED react: mosquitto_pub -h localhost -t "home/room/light/set" -m "1".

Common Issue: If the OLED shows "Connected" but nothing happens, check for a data-type mismatch in Node-RED (payload arriving as a Buffer instead of a string) by setting the MQTT In node's Output field to "a String", or double-check the relay's active-high/active-low setting matches your module.

09 System Demonstration

The images below show the working system. Use these to verify your output matches what is expected.

Node-RED Automation Flow(With Dashboard)

Node-RED flow showing three MQTT In nodes feeding a Function node into an MQTT Out node

Actuator Node OLED Status Display

SH1106 OLED showing MQTT connection status and current LED state

Video Demonstration

10 Conclusion

This project shows that a full local automation system — sensing, decision-making, and control — can run entirely on a single Arduino UNO Q with no cloud dependency. Along the way it also surfaced one of the most common real-world IoT gotchas: network isolation on mobile hotspots and some ISP-issued routers, which can silently block device-to-device communication even when every other setting is correct.

Possible Improvements and Future Enhancements

  • Add MQTT username/password authentication instead of allow_anonymous true for a more production-ready setup.
  • Add a static IP / DHCP reservation for the UNO Q so its address never changes.
  • Expand the Node-RED dashboard with historical charts (e.g. light level over time) using InfluxDB.
  • Add more sensor/actuator nodes to the same broker for a larger automation demo.
  • Debounce the sound sensor further in hardware (sensitivity potentiometer) to reduce false triggers from background noise.

11 References

  • Arduino UNO Q official documentation
  • Eclipse Mosquitto documentation
  • Node-RED official documentation
  • PubSubClient library documentation (Nick O'Leary)
  • Seeed Studio XIAO ESP32C3 wiki

12 Project Authors

  • Franz Aldrich P. Cadungog
  • Jholaica M. Guimal
Local MQTT Broker + Automation Bridge – CreateLabz

 

#esp32Arduino uno qEsp32c3Light sensorPirSound detectorSound sensor

Leave a comment

All comments are moderated before being published