ESP32 Modbus RTU转MQTTゲートウェイ実践:RS485データの収集上传 Remote controlOTA与工业级稳定性完全チュートリアル

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

ESP32作为一款性价比极高的IoT微コントローラーは,内置WiFi和蓝牙,配合Modbus RTUマスター库和MQTTクライアント側は库,可以轻松実装RS485デバイスデータの収集并上传到MQTTServer。本記事详细介绍如何用ESP32搭建Modbus RTU转MQTTゲートウェイ,包括硬件配線、库インストール、ModbusData Acquisition、MQTT发布订阅、OTAアップグレード、断线再接続以及完全的工业级ゲートウェイ代码。

一、方案概述

1.1 なぜか。用ESP32做Modbusゲートウェイ

  • 成本极低:ESP32开发板仅20-30元,RS485モジュール5-10元,整体成本远低于商业ゲートウェイ
  • 内置WiFi:无需额外网络モジュール,直接连接WiFi上传データ
  • 性能充足:双核240MHz,520KB SRAM,可同时処理Modbus采集和MQTTCommunication
  • Arduino生态:丰富的库サポート,ModbusMaster、PubSubClient等成熟库直接可用
  • サポートOTA:可远程アップグレード固件,无需现场维护
  • 多路シリアルポート:ESP32有3路硬件シリアルポート,可同时连接多条RS485バス

1.2 系统架构

┌──────────────────────────────────────────────────────────────┐
│                    MQTT Broker(Server)                       │
│              EMQX / Mosquitto / アリ·ユンIoT                     │
└───────────────────────────┬──────────────────────────────────┘
                            │ MQTT (WiFi)
┌───────────────────────────▼──────────────────────────────────┐
│                    ESP32 ゲートウェイ                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  WiFi管理    │  │ MQTTクライアント側は  │  │  Modbus RTUmaster     │  │
│  │ (connect/再接続)  │  │(PubSubClient)│  │  (ModbusMaster)    │  │
│  └─────────────┘  └──────┬──────┘  └──────────┬──────────┘  │
└───────────────────────────┼─────────────────────┼────────────┘
                            │                     │ UART2 (TX2/RX2)
                            │                     ▼
                            │              ┌──────────────┐
                            │              │ RS485モジュール    │
                            │              │ (MAX485/SP3485)│
                            │              └──────┬───────┘
                            │                     │ A/B
                            │              ┌──────▼───────┐
                            │              │  RS485バス    │
                            │              │  ┌────────┐  │
                            │              │  │Temperature & Humidity   │  │
                            │              │  │Sensors   │  │
                            │              │  └────────┘  │
                            │              │  ┌────────┐  │
                            │              │  │パワーメーター │  │
                            │              │  └────────┘  │
                            │              └──────────────┘

二、硬件准备

2.1 所需材料

  • ESP32开发板:おすすめESP32-WROOM-32或ESP32-S3
  • RS485モジュール:MAX485、SP3485或孤立している。RS485モジュール(おすすめ工业级带絶縁)
  • 杜邦线:若干
  • 120Ω終端抵抗:バス两端各一个(ロング·ディスタンス必需)
  • Modbus RTUdevice:Temperature & Humidity Sensor、パワーメーター等(测试用)
  • 5V/3.3V电源:给ESP32和RS485モジュール供电

2.2 配線説明

ESP32引脚RS485モジュール説明
GPIO17 (TX2)DIESP32送信→RS485入力
GPIO16 (RX2)RORS485出力→ESP32受信
GPIO4DE/RE方向控制(高=send,低=receive)
3.3VVCC电源(注意モジュール电压,いくつかは5V)
GNDGND共地
-ARS485 A线(機器の接続A)
-BRS485 B线(機器の接続B)

Precautions

  • RS485モジュールもしそうなら5V供电,DI引脚需要电平转换(ESP32是3.3V),或直接使用する。3.3V兼容的モジュール
  • DE和RE引脚短接在一起,由ESP32一个GPIO控制方向
  • バス两端必须接120Ω終端抵抗(A-B之间)
  • 工业环境推奨される使用孤立している。RS485モジュール,防止地环路损坏ESP32
  • 多条バス可使用ESP32的UART1和UART2分别连接

三、开发环境与库インストール

// Arduino IDE中インストールESP32开发板サポート:
// 文件 → 首选项 → 附加开发板管理器网址:
// https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
// Tools → 开发板 → 开发板管理器 → Search"ESP32" → インストール

// 需要インストール的库(Tools → 管理库 → Search):
// 1. ModbusMaster by Doc Walker (4-20ma)
//    - Modbus RTUマスター库,サポートすべて機能コード
// 2. PubSubClient by Nick O'Leary
//    - MQTTクライアント側は库,轻量稳定
// 3. ArduinoJson by Benoit Blanchon
//    - JSON序列化/反序列化(可选,用于構造化データ)
// 4. WiFiManager by tzapu (可选)
//    - WiFi設定门户,无需硬编码WiFiパスワード

// PlatformIO用户在platformio.ini中追加:
// [env:esp32dev]
// platform = espressif32
// board = esp32dev
// framework = arduino
// lib_deps =
//     4-20ma/ModbusMaster@^2.0.1
//     knolleary/PubSubClient@^2.8
//     bblanchon/ArduinoJson@^6.21.0

四、Basic Editionより:Modbus RTUデータの収集并发布MQTT

// esp32_modbus_mqtt_basic.ino - Basic EditionよりModbus RTU转MQTTゲートウェイ
#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>

// ====== 設定区 ======
const char* WIFI_SSID = "your_wifi_ssid";
const char* WIFI_PASSWORD = "your_wifi_password";

const char* MQTT_SERVER = "broker.emqx.io";  // MQTTサーバー·サーバーアドレス
const uint16_t MQTT_PORT = 1883;              // MQTTポート
const char* MQTT_CLIENT_ID = "esp32-gateway-001";
const char* MQTT_USER = "";                   // MQTTユーザー名(无则留空)
const char* MQTT_PASSWORD = "";               // MQTTパスワード(无则留空)
const char* MQTT_TOPIC = "modbus/gateway/001/data";  // 发布主题

// Modbus設定
#define MODBUS_BAUD 9600          // Baud rate
#define MODBUS_SLAVE_ID 1         // slaveID
#define MODBUS_RX_PIN 16          // RX引脚
#define MODBUS_TX_PIN 17          // TX引脚
#define MODBUS_DE_PIN 4           // DE/RE控制引脚

// 采集間隔
const uint32_t POLL_INTERVAL = 5000;  // 5秒采集一次

// ====== 全局对象 ======
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
ModbusMaster node;  // Modbusマスター对象

// ====== Modbus方向控制回调 ======
// ModbusMaster库需要一个函数来控制RS485的收发方向
void modbusPreTransmission() {
  digitalWrite(MODBUS_DE_PIN, HIGH);  // 切换到送信モード
  delayMicroseconds(10);              // 待機中RS485芯片切换
}

void modbusPostTransmission() {
  delayMicroseconds(10);              // 待機中最后一バイト単位。送信完毕
  digitalWrite(MODBUS_DE_PIN, LOW);   // 切换到受信モード
}

// ====== WiFiconnect ======
void connectWiFi() {
  Serial.print("connectWiFi: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFiConnected");
  Serial.print("IPaddress: ");
  Serial.println(WiFi.localIP());
}

// ====== MQTTconnect ======
void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("connectMQTTServer...");

    // connectMQTT(带遗嘱メッセージ:オフライン时通知)
    bool connected;
    if (strlen(MQTT_USER) > 0) {
      connected = mqtt.connect(
        MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD,
        "modbus/gateway/001/status", 1, true, "offline"
      );
    } else {
      connected = mqtt.connect(
        MQTT_CLIENT_ID,
        NULL, NULL,
        "modbus/gateway/001/status", 1, true, "offline"
      );
    }

    if (connected) {
      Serial.println("Connected");
      mqtt.publish("modbus/gateway/001/status", "online", true);  // 上线通知
    } else {
      Serial.print("失敗,rc=");
      Serial.print(mqtt.state());
      Serial.println(",5秒后重试");
      delay(5000);
    }
  }
}

// ====== readModbus設備のデータ ======
struct SensorData {
  float temperature;   // temperature
  float humidity;      // 湿度
  uint16_t status;     // ステータス字
  bool valid;          // データありますか?效
};

SensorData readModbusData() {
  SensorData data = {0, 0, 0, false};

  // Read and hold register0-2(temperature、湿度、status)
  uint8_t result = node.readHoldingRegisters(0, 3);

  if (result == node.ku8MBSuccess) {
    // 読み取り成功,从レスポンスバッファ取得データ
    uint16_t rawTemp = node.getResponseBuffer(0);  // 温度元の值
    uint16_t rawHum = node.getResponseBuffer(1);   // 湿度元の值
    data.status = node.getResponseBuffer(2);       // ステータス字

    // 转换为实际值(假设分辨率0.1)
    data.temperature = rawTemp / 10.0;
    data.humidity = rawHum / 10.0;
    data.valid = true;

    Serial.printf("temperature: %.1f°C, 湿度: %.1f%%, status: %d\n",
                  data.temperature, data.humidity, data.status);
  } else {
    Serial.printf("ModbusRead failed,エラー·コード: %d\n", result);
  }

  // クリアレスポンスバッファ,准备下一次読み取り
  node.clearResponseBuffer();

  return data;
}

// ====== 发布データ到MQTT ======
void publishData(const SensorData& data) {
  // 构建JSONメッセージ
  char payload[128];
  snprintf(payload, sizeof(payload),
    "{\"temperature\":%.1f,\"humidity\":%.1f,\"status\":%d,\"ts\":%lu}",
    data.temperature, data.humidity, data.status, millis());

  // 发布到MQTT(QoS=0,retain=false)
  mqtt.publish(MQTT_TOPIC, payload);
  Serial.printf("已发布到 %s: %s\n", MQTT_TOPIC, payload);
}

// ====== setup ======
void setup() {
  Serial.begin(115200);
  delay(100);

  // 初期化RS485方向控制引脚
  pinMode(MODBUS_DE_PIN, OUTPUT);
  digitalWrite(MODBUS_DE_PIN, LOW);  // デフォルト受信モード

  // 初期化Modbus(使用UART2)
  Serial2.begin(MODBUS_BAUD, SERIAL_8N1, MODBUS_RX_PIN, MODBUS_TX_PIN);
  node.begin(MODBUS_SLAVE_ID, Serial2);

  // 登録方向控制回调
  node.preTransmission(modbusPreTransmission);
  node.postTransmission(modbusPostTransmission);

  // connectWiFi
  connectWiFi();

  // 設定MQTT
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  mqtt.setBufferSize(512);  // 增大MQTTバッファ
}

// ====== loop ======
void loop() {
  // 确保WiFi和MQTTconnect
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }
  if (!mqtt.connected()) {
    connectMQTT();
  }
  mqtt.loop();  // 処理MQTTメッセージ

  // 定时采集并发布
  static uint32_t lastPoll = 0;
  if (millis() - lastPoll >= POLL_INTERVAL) {
    lastPoll = millis();

    SensorData data = readModbusData();
    if (data.valid && mqtt.connected()) {
      publishData(data);
    }
  }
}

五、工业级增强版

Basic Editionより功能可用,但工业场景还需要多デバイスサポート、多レジスタマップ、設定管理、ウォッチドッグ、OTA等功能。以下是增强版的关键モジュール。

5.1 多デバイス多レジスタ設定

// 定义デバイス和レジスタマップ
struct RegisterConfig {
  const char* name;       // データ名称(MQTT字段名)
  uint16_t address;       // register address
  uint8_t functionCode;   // function code 3=レジスタを保持する。, 4=入力レジスタ。
  uint8_t count;          // Reading quantity
  float multiplier;       // 乘数(转换系数)
  float divider;          // 除数
};

struct DeviceConfig {
  uint8_t slaveId;                // slaveID
  const char* name;               // Device Name
  RegisterConfig* registers;      // レジスタ設定配列
  uint8_t regCount;               // number of registers
};

// device1:Temperature & Humidity Sensor
RegisterConfig sensor1Regs[] = {
  {"temperature", 0, 3, 1, 10},   // temperature,address0,除以10
  {"humidity",    1, 3, 1, 10},   // 湿度,address1,除以10
  {"dew_point",   2, 3, 1, 10},   // 露点,address2,除以10
};

// device2:パワーメーター
RegisterConfig meterRegs[] = {
  {"voltage",  0, 3, 1, 10},     // 电压
  {"current",  1, 3, 1, 100},    // 电流
  {"power",    2, 3, 2, 1000},   // 功率(32位,21つのレジスタ)
  {"energy",   4, 3, 2, 1},      // 电能(32位)
};

DeviceConfig devices[] = {
  {1, "temp_sensor_1", sensor1Regs, 3},
  {2, "power_meter_1",  meterRegs,   4},
};
const uint8_t DEVICE_COUNT = 2;

5.2 通用データの収集函数

#include <ArduinoJson.h>

// 読み取り单个デバイス的すべてのレジスタ并构建JSON
String readDeviceToJson(DeviceConfig& dev) {
  StaticJsonDocument<512> doc;
  doc["device"] = dev.name;
  doc["slave_id"] = dev.slaveId;
  JsonObject data = doc.createNestedObject("data");

  bool anySuccess = false;

  for (uint8_t i = 0; i < dev.regCount; i++) {
    RegisterConfig& reg = dev.registers[i];
    node.setSlaveId(dev.slaveId);  // 設定スレーブID

    uint8_t result;
    if (reg.functionCode == 3) {
      result = node.readHoldingRegisters(reg.address, reg.count);
    } else {
      result = node.readInputRegisters(reg.address, reg.count);
    }

    if (result == node.ku8MBSuccess) {
      float value;
      if (reg.count == 1) {
        // 16位值
        uint16_t raw = node.getResponseBuffer(0);
        value = raw * reg.multiplier / reg.divider;
      } else {
        // 32位值(前の単語。)
        uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) |
                       node.getResponseBuffer(1);
        value = raw * reg.multiplier / reg.divider;
      }
      data[reg.name] = value;
      anySuccess = true;
    } else {
      data[reg.name] = nullptr;  // 読み取り失敗标记为null
    }

    node.clearResponseBuffer();
    delay(50);  // リクエスト間隔,避免バス冲突
  }

  if (!anySuccess) return "";

  String output;
  serializeJson(doc, output);
  return output;
}

5.3 MQTTRemote control(订阅下行命令)

// MQTT回调函数:処理ダウンタウン制御命令
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  Serial.printf("收到メッセージ [%s]: ", topic);
  for (unsigned int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // ParseJSONCommand
  StaticJsonDocument<256> doc;
  DeserializationError error = deserializeJson(doc, payload, length);
  if (error) {
    Serial.println("JSON解析の失敗");
    return;
  }

  // 命令格式:{"slave_id":1,"function":"write_register","address":10,"value":250}
  uint8_t slaveId = doc["slave_id"] | 1;
  const char* function = doc["function"] | "";
  uint16_t address = doc["address"] | 0;
  uint16_t value = doc["value"] | 0;

  node.setSlaveId(slaveId);

  if (strcmp(function, "write_register") == 0) {
    // function code06:write single register
    uint8_t result = node.writeSingleRegister(address, value);
    Serial.printf("写レジスタ %d=%d, common error troubleshooting: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失敗");
  }
  else if (strcmp(function, "write_coil") == 0) {
    // function code05:write single coil
    uint8_t result = node.writeSingleCoil(address, value ? 0xFF00 : 0x0000);
    Serial.printf("写コイル %d=%d, common error troubleshooting: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失敗");
  }
  else if (strcmp(function, "read_register") == 0) {
    // function code03:读レジスタ并回复
    uint8_t result = node.readHoldingRegisters(address, 1);
    if (result == node.ku8MBSuccess) {
      uint16_t regVal = node.getResponseBuffer(0);
      char resp[64];
      snprintf(resp, sizeof(resp), "{\"address\":%d,\"value\":%d}", address, regVal);
      mqtt.publish("modbus/gateway/001/response", resp);
      node.clearResponseBuffer();
    }
  }
}

// 在setup中登録回调和订阅
void setup() {
  // ... 其他初期化 ...
  mqtt.setCallback(mqttCallback);
}

// 在connectMQTT中订阅命令主题
void connectMQTT() {
  // ... 连接逻辑 ...
  if (connected) {
    mqtt.subscribe("modbus/gateway/001/command");  // 订阅下行命令
  }
}

5.4 ウォッチドッグ与系统稳定性

#include <esp_task_wdt.h>

// 初期化ウォッチドッグ(30秒タイムアウト)
void initWatchdog() {
  esp_task_wdt_init(30, true);  // 30秒タイムアウト,允许panic
  esp_task_wdt_add(NULL);       // 追加当前任务(loop)
  Serial.println("ウォッチドッグ已启动,タイムアウト30秒");
}

// 在loop中喂狗
void loop() {
  esp_task_wdt_reset();  // 喂狗
  // ... 其他逻辑 ...
}

// WiFi再接続优化(非阻塞)
void maintainWiFi() {
  static uint32_t lastCheck = 0;
  if (millis() - lastCheck < 10000) return;  // 每10秒チェック一次
  lastCheck = millis();

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi断开,尝试再接続...");
    WiFi.reconnect();
  }
}

// Modbus通信故障计数
struct DeviceState {
  uint8_t errorCount;
  bool offline;
};
DeviceState deviceStates[10];  // 最多10个デバイス

void handleModbusError(uint8_t slaveId) {
  deviceStates[slaveId].errorCount++;
  if (deviceStates[slaveId].errorCount >= 3 && !deviceStates[slaveId].offline) {
    deviceStates[slaveId].offline = true;
    // 发布オフライン告警
    char topic[64], payload[64];
    snprintf(topic, sizeof(topic), "modbus/gateway/001/device/%d/status", slaveId);
    snprintf(payload, sizeof(payload), "{\"status\":\"offline\",\"errors\":%d}",
             deviceStates[slaveId].errorCount);
    mqtt.publish(topic, payload, true);
  }
}

void handleModbusSuccess(uint8_t slaveId) {
  if (deviceStates[slaveId].offline) {
    deviceStates[slaveId].offline = false;
    deviceStates[slaveId].errorCount = 0;
    // 发布恢复通知
    char topic[64];
    snprintf(topic, sizeof(topic), "modbus/gateway/001/device/%d/status", slaveId);
    mqtt.publish(topic, "{\"status\":\"online\"}", true);
  }
  deviceStates[slaveId].errorCount = 0;
}

5.5 OTA远程アップグレード

#include <ArduinoOTA.h>

void initOTA() {
  ArduinoOTA.setHostname("esp32-modbus-gateway");
  ArduinoOTA.setPassword("admin123");  // OTAパスワード

  ArduinoOTA.onStart([]() {
    Serial.println("开始OTAアップグレード...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nアップグレード完成,再起動中...");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("アップグレード进度: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("OTAError[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("认证失敗");
    else if (error == OTA_BEGIN_ERROR) Serial.println("开始失敗");
    else if (error == OTA_CONNECT_ERROR) Serial.println("接続の失敗");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("受信失敗");
    else if (error == OTA_END_ERROR) Serial.println("结束失敗");
  });

  ArduinoOTA.begin();
  Serial.println("OTA已就绪");
}

// 在loop処理中ですOTA
void loop() {
  ArduinoOTA.handle();
  // ... 其他逻辑 ...
}

六、完全的工业级ゲートウェイ代码框架

// esp32_modbus_mqtt_gateway.ino - 完全工业级Modbus RTU转MQTTゲートウェイ
#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>
#include <ArduinoJson.h>
#include <ArduinoOTA.h>
#include <esp_task_wdt.h>

// ====== 設定 ======
#define WIFI_SSID "your_ssid"
#define WIFI_PASS "your_password"
#define MQTT_HOST "broker.emqx.io"
#define MQTT_PORT 1883
#define MQTT_USER ""
#define MQTT_PASS ""
#define GATEWAY_ID "gw001"

#define MODBUS_BAUD 9600
#define MODBUS_RX 16
#define MODBUS_TX 17
#define MODBUS_DE 4
#define POLL_INTERVAL 5000

// ====== 全局对象 ======
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
ModbusMaster node;

// ====== Modbus方向控制 ======
void preTx() { digitalWrite(MODBUS_DE, HIGH); delayMicroseconds(10); }
void postTx() { delayMicroseconds(10); digitalWrite(MODBUS_DE, LOW); }

// ====== デバイス定义 ======
struct RegDef { const char* name; uint16_t addr; uint8_t fc; uint8_t cnt; float div; };
struct DevDef { uint8_t id; const char* name; RegDef* regs; uint8_t regCnt; };

RegDef regs1[] = {
  {"temp", 0, 3, 1, 10}, {"hum", 1, 3, 1, 10}, {"status", 2, 3, 1, 1}
};
RegDef regs2[] = {
  {"vol", 0, 3, 1, 10}, {"cur", 1, 3, 1, 100}, {"pow", 2, 3, 2, 1000}
};
DevDef devices[] = {
  {1, "sensor1", regs1, 3},
  {2, "meter1", regs2, 3}
};
const uint8_t DEV_CNT = 2;

// ====== MQTT回调 ======
void onMqttMessage(char* topic, byte* payload, unsigned int len) {
  StaticJsonDocument<256> doc;
  if (deserializeJson(doc, payload, len)) return;

  uint8_t sid = doc["slave_id"] | 1;
  const char* func = doc["function"] | "";
  uint16_t addr = doc["address"] | 0;
  uint16_t val = doc["value"] | 0;

  node.setSlaveId(sid);
  if (!strcmp(func, "write_reg")) {
    node.writeSingleRegister(addr, val);
  } else if (!strcmp(func, "write_coil")) {
    node.writeSingleCoil(addr, val ? 0xFF00 : 0);
  }
}

// ====== 连接管理 ======
void connectWiFi() {
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print("."); }
  Serial.println("\nWiFi OK: " + WiFi.localIP().toString());
}

void connectMQTT() {
  while (!mqtt.connected()) {
    String clientId = String("esp32-") + GATEWAY_ID;
    String statusTopic = String("modbus/") + GATEWAY_ID + "/status";
    bool ok = mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                           statusTopic.c_str(), 1, true, "offline");
    if (ok) {
      mqtt.publish(statusTopic.c_str(), "online", true);
      String cmdTopic = String("modbus/") + GATEWAY_ID + "/cmd";
      mqtt.subscribe(cmdTopic.c_str());
      Serial.println("MQTT OK");
    } else {
      Serial.printf("MQTT fail rc=%d, retry in 5s\n", mqtt.state());
      delay(5000);
    }
  }
}

// ====== Data Acquisition ======
void pollAndPublish() {
  StaticJsonDocument<1024> doc;
  doc["gateway"] = GATEWAY_ID;
  doc["timestamp"] = millis();
  JsonArray devArr = doc.createNestedArray("devices");

  for (uint8_t d = 0; d < DEV_CNT; d++) {
    DevDef& dev = devices[d];
    node.setSlaveId(dev.id);

    JsonObject devObj = devArr.createNestedObject();
    devObj["id"] = dev.id;
    devObj["name"] = dev.name;
    JsonObject data = devObj.createNestedObject("data");

    for (uint8_t r = 0; r < dev.regCnt; r++) {
      RegDef& reg = dev.regs[r];
      uint8_t res = (reg.fc == 3) ?
        node.readHoldingRegisters(reg.addr, reg.cnt) :
        node.readInputRegisters(reg.addr, reg.cnt);

      if (res == node.ku8MBSuccess) {
        float v;
        if (reg.cnt == 1) {
          v = node.getResponseBuffer(0) / reg.div;
        } else {
          uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) |
                          node.getResponseBuffer(1);
          v = raw / reg.div;
        }
        data[reg.name] = v;
      } else {
        data[reg.name] = nullptr;
      }
      node.clearResponseBuffer();
      delay(30);
    }
  }

  String output;
  serializeJson(doc, output);
  String topic = String("modbus/") + GATEWAY_ID + "/data";
  mqtt.publish(topic.c_str(), output.c_str());
  Serial.println("Published: " + output);
}

// ====== setup & loop ======
void setup() {
  Serial.begin(115200);
  pinMode(MODBUS_DE, OUTPUT);
  digitalWrite(MODBUS_DE, LOW);

  Serial2.begin(MODBUS_BAUD, SERIAL_8N1, MODBUS_RX, MODBUS_TX);
  node.begin(1, Serial2);
  node.preTransmission(preTx);
  node.postTransmission(postTx);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMqttMessage);
  mqtt.setBufferSize(1024);
  connectMQTT();

  ArduinoOTA.setHostname("esp32-modbus-gw");
  ArduinoOTA.begin();

  esp_task_wdt_init(30, true);
  esp_task_wdt_add(NULL);

  Serial.println("ゲートウェイ启动完成");
}

void loop() {
  esp_task_wdt_reset();
  ArduinoOTA.handle();

  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();

  static uint32_t last = 0;
  if (millis() - last >= POLL_INTERVAL) {
    last = millis();
    pollAndPublish();
  }
}

七、MQTTサーバーの構築とデータ表示

# 方式一:使用公共MQTTServer(测试用)
# EMQX公共サーバー·サーバー:broker.emqx.io:1883
# 用MQTTXクライアントサブスクリプション modbus/gw001/data 表示データ

# 方式二:本地搭建Mosquitto(Docker)
docker run -d --name mosquitto -p 1883:1883 -p 9001:9001 \
  eclipse-mosquitto

# 方式三:EMQX(功能更丰富,带Web管理界面)
docker run -d --name emqx -p 1883:1883 -p 18083:18083 \
  emqx/emqx:latest
# 管理界面:http://localhost:18083 (admin/public)

# 用命令行订阅表示データ
mosquitto_sub -h broker.emqx.io -t "modbus/gw001/data" -v

# 用命令行送信控制命令
mosquitto_pub -h broker.emqx.io -t "modbus/gw001/cmd" \
  -m '{"slave_id":1,"function":"write_register","address":10,"value":250}'

八、よくある質問与トラブルシューティング

問題原因解決方法
Modbus読み取りすべて失敗配線エラー或DE引脚未控制チェックA/B线是否接反,DE引脚是否连接并在代码中控制
偶尔読み取り失敗ボーレート不マッチング或終端抵抗缺失核对ボーレート/check digit,バス两端加120Ω电阻
読み取りデータ乱码シリアルポート設定エラーConfirmSERIAL_8N1与デバイス一致,チェックRX/TX是否接反
MQTT接続の失敗WiFi未连接或サーバー·サーバーアドレスエラーチェックシリアルポート日志中的IP和MQTTステータス码
MQTTメッセージ丢失PubSubClientバッファ太小mqtt.setBufferSize(1024)增大バッファ
ESP32频繁再起動ウォッチドッグタイムアウト或内存不足チェックloop中ありますか?长时间阻塞,减小JSONドキュメント大小
多デバイス冲突slaveID重复或リクエスト間隔短すぎる确保每个デバイスID唯一,リクエスト间加delay(30-50ms)
32位データは間違ってる字序エラー尝试交換高低字:raw=(buf[1]<<16)|buf[0]
OTAアップグレード失敗パスワードエラー或网络不稳定ConfirmArduinoOTAパスワード,使用5GHz以下WiFi
RS485モジュール发热电压不マッチング或ショート確認モジュール電源電圧と電圧,チェックA/B是否ショート

九、上級扩展

  • 双バスゲートウェイ:使用UART1和UART2分别连接两条RS485バス,作成2つModbusMaster对象
  • Modbus TCPslave:ESP32也可以作为Modbus TCPServer,用modbus-esp32库実装
  • データ本地缓存:网络中断时用SPIFFS/LittleFS缓存データ,恢复后补传
  • NTP时间同步:経由NTP取得真实时间戳,替代millis()
  • WiFiManager配网:首次启动启动APモード,手机設定WiFi和MQTTparameter,无需硬编码
  • MQTT TLS加密:使用WiFiClientSecureconnect8883ポート,サポート证书验证
  • 低消費電力はモード:电池供电场景使用深度睡眠,定时唤醒采集上传
  • Web管理界面:ESP32运行WebServer,浏览器表示リアルタイムデータ和修改設定

十、学习リソース

  • ModbusMaster库:https://github.com/4-20ma/ModbusMaster
  • PubSubClient库:https://github.com/knolleary/pubsubclient
  • ESP32 Arduino核心:https://github.com/espressif/arduino-esp32
  • EMQX MQTTServer:https://www.emqx.io/
  • MQTTXクライアント側は:https://mqttx.app/
  • ArduinoJson:https://arduinojson.org/

ESP32搭配ModbusMaster和PubSubClient库,可以用极低的成本実装工业级Modbus RTU转MQTTゲートウェイ。本記事提供的从Basic Editionより到工业级增强版完全に。代码,カバー了多デバイス采集、MQTT双向通信、OTAアップグレード、ウォッチドッグ等关键功能,可直接使用する。于实际项目。建议先在桌面用一个Modbusデバイス测试経由,再部署到工业现场,并注意RS485バス的配線和終端抵抗設定。

📦

VIP专属:ESP32 Modbus RTU转MQTTGatewayコードパッケージ

ModbusMaster+PubSubClient完全な実装:RS485采集、MQTT上报、OTAアップグレード、ウォッチドッグ、低消費電力は。

Activate VIP即可下载完全代码,同时解锁 30+ 工程実践パッケージの内容:スクリプトのデバッグ、クイックチェック·テーブル、项目模板、トラブルシューティング案例……

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support