MQTT 協定與 Modbus 通訊的遠端監控與控制系統架構
這張圖片展示了一個結合 MQTT 協定與 Modbus 通訊的遠端監控與控制系統架構(主要透過 Node-RED 進行資料整合)。
系統包含三個核心部分,其運作功能說明如下:
1. ESP32 終端設備(硬體控制層)
2. MQTT Broker(訊息轉發中心)
3. Node-RED 與 Modbus 模擬器(核心邏輯與工業通訊整合)
總結運作情境
這套架構的功能主要是將工業級的 Modbus 協定與物聯網的 MQTT 協定進行橋接。例如:你可以透過操作 ModbusSim 模擬器(模擬中控室的 PLC 燈號開關),讓 Node-RED 偵測到變化後透過 MQTT 去點亮遠端的 ESP32 LED 燈;同理,遠端 ESP32 的狀態也可以即時同步回傳,寫入 ModbusSim 的暫存器中顯示。
WOKWI程式
#include <WiFi.h>
#include <PubSubClient.h>
// 1. Wi-Fi 與 MQTT 設定 (Wokwi 專用模擬 Wi-Fi)
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "mqttgo.io";
const int mqtt_port = 1883;
// MQTT 主題定義
const char* topic_control = "alex9ufo/ledcontrol";
const char* topic_status = "alex9ufo/ledstatus";
// 2. 定義 8 個 LED 的 GPIO 腳位
const int ledPins[8] = { 25, 26, 27, 2 ,4, 5, 18, 19};
WiFiClient espClient;
PubSubClient client(espClient);
// Wi-Fi 連線函式
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// 3. 核心:接收 MQTT 訊息的 Callback 函式
void callback(char* topic, byte* payload, unsigned int length) {
// 將接收到的 payload 轉換為 C++ String 格式
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("Message arrived on topic [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(message);
// 檢查訊息是否以 "led" 開頭,且長度足夠 (例如 led1on 最短 6 字元)
if (message.startsWith("led") && message.length() >= 6) {
// 取得 LED 編號 (索引從第 3 個字元開始,到找到 "on" 或 "off" 之前)
int stateIdx = -1;
if (message.indexOf("on") != -1) stateIdx = message.indexOf("on");
else if (message.indexOf("off") != -1) stateIdx = message.indexOf("off");
if (stateIdx != -1) {
// 擷取中間的數字字串並轉為整數
int ledNum = message.substring(3, stateIdx).toInt();
String action = message.substring(stateIdx); // "on" 或 "off"
// 檢查編號是否在 1 到 8 之間
if (ledNum >= 1 && ledNum <= 8) {
int pinIdx = ledNum - 1; // 陣列索引從 0 開始
String statusPayload = "";
if (action == "on") {
digitalWrite(ledPins[pinIdx], HIGH); // 點亮 LED
statusPayload = "LED" + String(ledNum) + "ON";
} else if (action == "off") {
digitalWrite(ledPins[pinIdx], LOW); // 熄滅 LED
statusPayload = "LED" + String(ledNum) + "OFF";
}
// 4. 即時回報狀態到 alex9ufo/ledstatus
if (statusPayload != "") {
client.publish(topic_status, statusPayload.c_str());
Serial.print("Published status: ");
Serial.println(statusPayload);
}
}
}
}
}
// MQTT 重新連線函式
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// 隨機產生 Client ID 避免重複衝突
String clientId = "ESP32Client-";
clientId += String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("connected");
// 連線成功後重新訂閱控制主題
client.subscribe(topic_control);
Serial.print("Subscribed to: ");
Serial.println(topic_control);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
// 初始化 8 個 LED 腳位為輸出模式,預設關閉 (LOW)
for (int i = 0; i < 8; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback); // 設定收到訊息時的處理函式
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop(); // 維持 MQTT 連線與處理底層通訊
}
| Data Addresses | Read | Write Single | Write Multiple |
| Discrete Output Coils 0xxxx | FC01 | FC05 | FC15 |
const coils = msg.payload;
let messagesToPublish = [];
for (let i = 0; i < 8; i++) {
let ledNumber = i + 1;
let stateStr = (coils[i] === true) ? "on" : "off";
let customPayload = "led" + ledNumber + stateStr;
messagesToPublish.push({
topic: "alex9ufo/ledcontrol",
payload: customPayload
});
}
return [messagesToPublish];
// 確保進來的是陣列
if (!Array.isArray(msg.payload)) {
return null;
}
const rawArray = msg.payload;
let resultArray = [false, false, false, false, false, false, false, false];
rawArray.forEach(str => {
if (typeof str === 'string' && str.startsWith("LED")) {
let stateIdx = -1;
if (str.indexOf("ON") != -1) stateIdx = str.indexOf("ON");
else if (str.indexOf("OFF") != -1) stateIdx = str.indexOf("OFF");
if (stateIdx != -1) {
let ledNum = parseInt(str.substring(3, stateIdx));
let action = str.substring(stateIdx);
if (ledNum >= 1 && ledNum <= 8) {
let arrayIndex = ledNum - 1;
resultArray[arrayIndex] = (action === "ON");
}
}
}
});
msg.payload = resultArray;
return msg;
Node-Red程式
[{"id":"de273b1b8bd36b17","type":"modbus-read","z":"56c70b10b2e55b6e","name":"","topic":"","showStatusActivities":false,"logIOActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"Coil","adr":"0","quantity":"8","rate":"10000","rateUnit":"ms","delayOnStart":false,"enableDeformedMessages":true,"startDelayTime":"","server":"54dde46e.53267c","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"x":150,"y":120,"wires":[[],["eb0999703abe22e8"]]},{"id":"eb0999703abe22e8","type":"function","z":"56c70b10b2e55b6e","name":"function ","func":"var data=msg.payload.data;\nmsg.payload=data;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":120,"wires":[["led_single_topic_fn"]]},{"id":"cceab653f17ba013","type":"function","z":"56c70b10b2e55b6e","name":"Read FC=15","func":"// Define your coil array values\nconst coilValues = msg.payload;\n\nmsg.payload = {\n \"value\": coilValues, // Array of booleans to write\n \"fc\": 15, // Function Code 15 (Write Multiple Coils)\n \"unitid\": 1, // Modbus Slave/Unit ID\n \"address\": 16, // Starting coil memory address\n \"quantity\": coilValues.length // Number of coils to write\n};\n\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":320,"wires":[["baf9113b483f99d0","d135dcbf561bc6db"]]},{"id":"f237104758129409","type":"debug","z":"56c70b10b2e55b6e","name":"debug 5","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":800,"y":340,"wires":[]},{"id":"baf9113b483f99d0","type":"debug","z":"56c70b10b2e55b6e","name":"debug 6","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":620,"y":280,"wires":[]},{"id":"d135dcbf561bc6db","type":"modbus-write","z":"56c70b10b2e55b6e","name":"","showStatusActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"MCoils","adr":"16","quantity":"8","server":"54dde46e.53267c","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":620,"y":340,"wires":[[],["f237104758129409"]]},{"id":"mq_l1","type":"mqtt out","z":"56c70b10b2e55b6e","name":"LED 1-8 MQTT 傳送端 (固定主題)","topic":"alex9ufo/ledcontrol","qos":"1","retain":"false","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7c89942e45b95822","x":780,"y":120,"wires":[]},{"id":"led_single_topic_fn","type":"function","z":"56c70b10b2e55b6e","name":"編碼為單一主題訊息","func":"const coils = msg.payload;\nlet messagesToPublish = [];\n\nfor (let i = 0; i < 8; i++) {\n let ledNumber = i + 1;\n let stateStr = (coils[i] === true) ? \"on\" : \"off\";\n let customPayload = \"led\" + ledNumber + stateStr;\n \n messagesToPublish.push({\n topic: \"alex9ufo/ledcontrol\",\n payload: customPayload\n });\n}\n\nreturn [messagesToPublish];","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":120,"wires":[["mq_l1"]]},{"id":"mqtt_in_status","type":"mqtt in","z":"56c70b10b2e55b6e","name":"訂閱 ledstatus","topic":"alex9ufo/ledstatus","qos":"1","datatype":"auto-detect","broker":"7c89942e45b95822","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":200,"wires":[["join_node_correct"]]},{"id":"join_node_correct","type":"join","z":"56c70b10b2e55b6e","name":"Join 收集8條","mode":"manual","build":"array","property":"payload","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"50","count":"8","x":330,"y":200,"wires":[["function_convert_correct"]]},{"id":"function_convert_correct","type":"function","z":"56c70b10b2e55b6e","name":"Function 轉換布林陣列","func":"// 確保進來的是陣列\nif (!Array.isArray(msg.payload)) {\n return null;\n}\n\nconst rawArray = msg.payload;\nlet resultArray = [false, false, false, false, false, false, false, false];\n\nrawArray.forEach(str => {\n if (typeof str === 'string' && str.startsWith(\"LED\")) {\n let stateIdx = -1;\n if (str.indexOf(\"ON\") != -1) stateIdx = str.indexOf(\"ON\");\n else if (str.indexOf(\"OFF\") != -1) stateIdx = str.indexOf(\"OFF\");\n \n if (stateIdx != -1) {\n let ledNum = parseInt(str.substring(3, stateIdx));\n let action = str.substring(stateIdx);\n \n if (ledNum >= 1 && ledNum <= 8) {\n let arrayIndex = ledNum - 1;\n resultArray[arrayIndex] = (action === \"ON\");\n }\n }\n }\n});\n\nmsg.payload = resultArray;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":200,"wires":[["debug_correct_out","cceab653f17ba013"]]},{"id":"debug_correct_out","type":"debug","z":"56c70b10b2e55b6e","name":"正確輸出布林陣列","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":790,"y":200,"wires":[]},{"id":"54dde46e.53267c","type":"modbus-client","name":"local","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":true,"queueLogEnabled":false,"failureLogEnabled":false,"tcpHost":"127.0.0.1","tcpPort":"502","tcpType":"TCP-RTU-BUFFERED","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","serialAsciiResponseStartDelimiter":"","unit_id":1,"commandDelay":1,"clientTimeout":1000,"reconnectOnTimeout":false,"reconnectTimeout":2000,"parallelUnitIdsAllowed":false,"showErrors":false,"showWarnings":true,"showLogs":true},{"id":"7c89942e45b95822","type":"mqtt-broker","name":"mqttgo","broker":"mqttgo.io","port":1883,"clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"dd649eb72b4bcf6f","type":"global-config","env":[],"modules":{"node-red-contrib-modbus":"5.45.2"}}]