平常一个便宜的Modbus网关100左右,今天我们就来使用ESP8266+RS485模块来自制一个Modbus网关,并采集点表数据推送到ThingsPanel。
文章速览
- ✅ 总成本仅需10元
- 🔧 15分钟即可完成组装
- 📊 实时采集电表数据并上传至ThingsPanel
- 💡 完整代码和接线图解
为什么要自制Modbus网关?
市面上的Modbus网关动辄100元以上,而通过本教程,你只需要10元就能实现同样的功能。我们将使用ESP8266配合RS485模块,打造一个经济实惠的Modbus网关解决方案。
总价10块,你要是能找,还有更便宜的。
材料清单
- ESP8266模块
- MAX485模块
- 若干杜邦线
- USB数据线(烧录用)
接线方式
MAX485与ESP8266的接线方案:
| MAX485引脚 | ESP8266引脚 |
| ---------- | ----------- |
| VCC | 3.3V |
| GND | GND |
| DI | TX(GPIO1) |
| RO | RX(GPIO3) |
| RE+DE | D1(GPIO5) |
💡 重要提示: RE和DE需要短接后再连接到D1引脚,用一根线都连一起
电表RS485接线:
电表的 A+ 连接到 MAX485 的 A+
电表的 B- 连接到 MAX485 的 B-
需要安装的库:
- WiFiManager - 实现WiFi配网功能
- PubSubClient - MQTT通信支持
- ModbusMaster - Modbus协议支持
- ArduinoJson - JSON数据处理
开发环境配置
- 安装Arduino IDE
- 添加ESP8266开发板支持
- 安装上述依赖库
效果图
代码
刷写代码使用arduino IDE
代码中的MQTT连接参数需要在ThingsPanel中创建手动设备(不需要绑定设备配置模板),后即可得到。
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>
#include <ArduinoJson.h>
#include <WiFiManager.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
// MQTT设置,自行修改
const char* mqtt_server = "47.115.210.16";
const int mqtt_port = 1883;
char mqtt_username[40] = "fe917b16-d786-3297-8f1";
char mqtt_password[40] = "f129542";
char mqtt_client_id[40] = "mqtt_3243936a-c23";
const char* telemetry_topic = "devices/telemetry";
const char* control_topic = "devices/telemetry/control/3243936a-c237-8cc5-7ea9-ba586098c2b7";
// MAX485控制引脚
const int MAX485_DE_RE = 5; // 可以根据实际接线修改
const int RESET_BUTTON = 0; // Flash按钮作为重置按钮
// 全局对象
ModbusMaster node;
WiFiClient espClient;
PubSubClient client(espClient);
WiFiManager wifiManager;
ESP8266WebServer server(80);
// 数据项结构定义
struct DataItem {
const char* name;
uint16_t address;
float factor;
uint8_t registers;
};
// Modbus数据项配置,自行修改
const DataItem DATA_ITEMS[] = {
{"Voltage", 0, 0.1, 1},
{"Current", 3, 0.01, 2},
{"TotalActivePower", 7, 1.0, 1},
{"TotalReactivePower", 11, 1.0, 1},
{"TotalApparentPower", 15, 1.0, 1},
{"TotalPowerFactor", 19, 0.001, 1},
{"VoltageFrequency", 26, 0.01, 1}
};
// MQTT配置页面HTML
const char* CONFIG_HTML = R"html(
<!DOCTYPE html>
<html>
<head>
<title>MQTT Configuration</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input { width: 100%; padding: 5px; }
button { padding: 10px; background-color: #4CAF50; color: white; border: none; }
</style>
</head>
<body>
<h2>MQTT Configuration</h2>
<form method="POST" action="/save">
<div class="form-group">
<label>Username:</label>
<input type="text" name="username" value="%s">
</div>
<div class="form-group">
<label>Password:</label>
<input type="text" name="password" value="%s">
</div>
<div class="form-group">
<label>Client ID:</label>
<input type="text" name="client_id" value="%s">
</div>
<button type="submit">Save</button>
</form>
</body>
</html>)html";
// Modbus通信函数
void preTransmission() {
digitalWrite(MAX485_DE_RE, HIGH);
delayMicroseconds(50); // 发送前等待50微秒
}
void postTransmission() {
delayMicroseconds(50); // 发送后等待50微秒
digitalWrite(MAX485_DE_RE, LOW);
}
// EEPROM操作函数
void saveMQTTConfig() {
Serial.println("Saving MQTT configuration to EEPROM...");
EEPROM.begin(512);
// 写入初始化标记
EEPROM.write(0, 0x55);
int addr = 1;
for(int i = 0; i < 39; i++) {
EEPROM.write(addr+i, mqtt_username[i]);
EEPROM.write(addr+40+i, mqtt_password[i]);
EEPROM.write(addr+80+i, mqtt_client_id[i]);
}
EEPROM.commit();
EEPROM.end();
Serial.println("MQTT configuration saved");
Serial.printf("Username: %s\n", mqtt_username);
Serial.printf("Client ID: %s\n", mqtt_client_id);
}
void loadMQTTConfig() {
Serial.println("Loading MQTT configuration from EEPROM...");
EEPROM.begin(512);
memset(mqtt_username, 0, sizeof(mqtt_username));
memset(mqtt_password, 0, sizeof(mqtt_password));
memset(mqtt_client_id, 0, sizeof(mqtt_client_id));
uint8_t initialized = EEPROM.read(0);
if (initialized != 0x55) {
Serial.println("EEPROM not initialized, using default values");
strncpy(mqtt_username, "fe917b16-d786-3297-8f1", sizeof(mqtt_username)-1);
strncpy(mqtt_password, "f129542", sizeof(mqtt_password)-1);
strncpy(mqtt_client_id, "mqtt_3243936a-c23", sizeof(mqtt_client_id)-1);
saveMQTTConfig();
return;
}
int addr = 1;
for(int i = 0; i < 39; i++) {
mqtt_username[i] = EEPROM.read(addr+i);
mqtt_password[i] = EEPROM.read(addr+40+i);
mqtt_client_id[i] = EEPROM.read(addr+80+i);
}
EEPROM.end();
Serial.println("MQTT configuration loaded");
Serial.printf("Username: %s\n", mqtt_username);
Serial.printf("Client ID: %s\n", mqtt_client_id);
}
// Web服务器处理函数
void handleRoot() {
char html[2048];
snprintf(html, sizeof(html), CONFIG_HTML, mqtt_username, mqtt_password, mqtt_client_id);
server.send(200, "text/html", html);
}
void handleSave() {
if (server.hasArg("username")) {
strncpy(mqtt_username, server.arg("username").c_str(), sizeof(mqtt_username)-1);
}
if (server.hasArg("password")) {
strncpy(mqtt_password, server.arg("password").c_str(), sizeof(mqtt_password)-1);
}
if (server.hasArg("client_id")) {
strncpy(mqtt_client_id, server.arg("client_id").c_str(), sizeof(mqtt_client_id)-1);
}
saveMQTTConfig();
server.send(200, "text/html", "Configuration saved. Device will restart in 3 seconds...");
delay(3000);
ESP.restart();
}
// MQTT回调函数
void mqttCallback(char* topic, byte* payload, unsigned int length) {
Serial.printf("Message received on topic: %s\n", topic);
char message[length + 1];
memcpy(message, payload, length);
message[length] = '\0';
Serial.printf("Message content: %s\n", message);
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, message);
if (!error) {
if (doc.containsKey("switch")) {
const char* switchState = doc["switch"];
Serial.printf("Switch command: %s\n", switchState);
}
}
}
void reconnectMQTT() {
while (!client.connected()) {
Serial.printf("Attempting MQTT connection to %s:%d\n", mqtt_server, mqtt_port);
if (client.connect(mqtt_client_id, mqtt_username, mqtt_password)) {
Serial.println("MQTT connected");
client.subscribe(control_topic);
} else {
Serial.printf("MQTT connection failed, rc=%d\n", client.state());
Serial.println("Retrying in 5 seconds");
delay(5000);
}
}
}
// Modbus数据读取和发布
void readAndPublishData() {
Serial.println("\n--- Reading Modbus Data ---");
StaticJsonDocument<512> doc;
bool hasValidData = false;
for (const auto& item : DATA_ITEMS) {
Serial.printf("Reading %s from address %d...\n", item.name, item.address);
int retryCount = 0;
const int maxRetries = 3;
uint8_t result;
while (retryCount < maxRetries) {
result = node.readHoldingRegisters(item.address, item.registers);
if (result == node.ku8MBSuccess) {
float value = 0;
if (item.registers == 2) {
uint32_t combinedValue = (node.getResponseBuffer(0) << 16) | node.getResponseBuffer(1);
value = *((float*)&combinedValue) * item.factor;
} else {
value = node.getResponseBuffer(0) * item.factor;
}
doc[item.name] = value;
Serial.printf("%s: %.2f\n", item.name, value);
hasValidData = true;
break;
} else {
Serial.printf("Retry %d - Failed to read %s (error code: %d)\n",
retryCount + 1, item.name, result);
delay(100);
retryCount++;
}
}
if (retryCount == maxRetries) {
Serial.printf("Failed to read %s after %d attempts\n", item.name, maxRetries);
}
delay(100); // 读取间隔
}
if (hasValidData) {
char buffer[512];
serializeJson(doc, buffer);
if (client.publish(telemetry_topic, buffer)) {
Serial.printf("Published: %s\n", buffer);
} else {
Serial.println("Failed to publish data");
}
} else {
Serial.println("No valid data to publish");
}
}
// 重置按钮检查
void checkResetButton() {
if (digitalRead(RESET_BUTTON) == LOW) {
delay(50); // 消抖
if (digitalRead(RESET_BUTTON) == LOW) {
Serial.println("\nReset button pressed");
Serial.println("Clearing settings...");
wifiManager.resetSettings();
memset(mqtt_username, 0, sizeof(mqtt_username));
memset(mqtt_password, 0, sizeof(mqtt_password));
memset(mqtt_client_id, 0, sizeof(mqtt_client_id));
saveMQTTConfig();
delay(1000);
ESP.restart();
}
}
}
void setup() {
// 初始化串口,使用正确的配置
Serial.begin(9600, SERIAL_8E1); // 8数据位,偶校验,1停止位
Serial.println("\n\n=== Device Starting ===");
// 加载MQTT配置
loadMQTTConfig();
// 初始化MAX485
pinMode(MAX485_DE_RE, OUTPUT);
digitalWrite(MAX485_DE_RE, LOW);
// 配置重置按钮
pinMode(RESET_BUTTON, INPUT_PULLUP);
// 初始化Modbus
node.begin(1, Serial);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
// WiFi配置
wifiManager.setAPCallback([](WiFiManager *myWiFiManager) {
Serial.println("Entered config mode");
Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
Serial.printf("SSID: %s\n", myWiFiManager->getConfigPortalSSID());
});
wifiManager.setConnectTimeout(30);
if(!wifiManager.autoConnect("ESP8266_Meter_Config")) {
Serial.println("Failed to connect");
delay(3000);
ESP.restart();
}
Serial.printf("WiFi connected\nIP: %s\n", WiFi.localIP().toString().c_str());
// 启动Web服务器
server.on("/", handleRoot);
server.on("/save", handleSave);
server.begin();
// 配置MQTT客户端
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mqttCallback);
Serial.println("=== Setup completed ===\n");
}
void loop() {
server.handleClient();
checkResetButton();
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
static unsigned long lastReadTime = 0;
if (millis() - lastReadTime >= 10000) { // 每10秒读取一次
readAndPublishData();
lastReadTime = millis();
Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
}
}
使用指南
- 烧录程序后,设备会自动创建名为"ESP8266_Meter_Config"的WiFi热点
- 使用手机连接该热点并完成WiFi配置
- 约30秒后,即可在ThingsPanel平台查看数据
遇到问题
如果遇到问题,可以文末留言或者联系我们一起交流解决。