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. |
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
- Wire the PIR and BH1750 to XIAO ESP32C3 #1 as shown above, keeping I2C wiring short.
- Wire the sound sensor to XIAO ESP32C3 #2, adjusting the onboard sensitivity potentiometer once powered.
- Wire the OLED and relay's low-voltage side (VCC/GND/IN) to the ESP32 dev kit.
- Wire the relay's mains side (COM/NO) to the bulb socket and wall power, fully inside an enclosure.
- Double-check every mains connection before applying power — never test with the enclosure open.
- Power on all three nodes and the UNO Q on the same local WiFi network.
05 Software Setup
Follow these steps in order. Do not skip any step.
Step 1 — Set Up the Arduino UNO Q
- Connect the UNO Q to your PC via USB-C and install Arduino App Lab.
- Complete App Lab's first-time setup, entering your WiFi credentials (this also enables SSH).
- Find the board's IP address:
ip addr show wlan0 | grep "inet " - 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) |
Local Broker Config File
/etc/mosquitto/conf.d/local.conf
Step 3 — Install Node-RED
Still over SSH on the UNO Q, run the official install script:
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
- Open each sketch from the Code section below and edit the values in its EDIT THESE block (WiFi SSID/password, broker IP).
- Select the correct board in Arduino IDE (XIAO_ESP32C3, or ESP32 Dev Module).
- Click Upload, then open Serial Monitor at 115200 baud to confirm WiFi and MQTT connect successfully.
Step 6 — Build the Node-RED Automation Flow
- Open Node-RED in your browser at
http://<uno-q-ip>:1880 - Add three MQTT In nodes, all using the same broker (localhost:1883), subscribed to
home/room/motion,home/room/light, andhome/room/sound. - Add one Function node using the automation logic in the Code section below.
- Add one MQTT Out node, same broker, publishing to
home/room/light/set. - Wire all three MQTT In nodes into the Function node, and the Function node into the MQTT Out node.
- 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
#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)
#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
#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
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
- Board boots, connects to WiFi, then connects to the Mosquitto broker on the UNO Q.
- 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).
- Node-RED's MQTT In nodes receive each published value and pass it into the Function node.
- The Function node applies the light-threshold and clap-toggle logic, decides the LED state, and publishes a command to
home/room/light/set. - 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.
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.
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".
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)
Actuator Node OLED Status Display
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 truefor 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
