ESP8266自制10块钱的Modbus独立网关,采集电表数据发ThingsPanel

文章速览

  • ✅ 总成本仅需10元 是之前的十分之一
  • 🔧 15分钟即可完成组装
  • 📊 实时采集电表数据并上传至ThingsPanel
  • 💡 完整代码和接线图解

为什么要自制Modbus网关?

市面上的Modbus网关动辄100元以上,而通过本教程,你只需要10元就能实现同样的功能。我们将使用ESP8266配合RS485模块,打造一个经济实惠的Modbus网关解决方案。
在这里插入图片描述

数据已经上线

在这里插入图片描述

材料清单

  • ESP8266模块
  • MAX485模块
  • 若干杜邦线
  • USB数据线(烧录用)

详细接线教程

在这里插入图片描述

第一步:MAX485与ESP8266连接

MAX485引脚ESP8266引脚
VCC3.3V
GNDGND
DITX(GPIO1)
RORX(GPIO3)
RE+DED1(GPIO5)

💡 重要提示: RE和DE需要短接后再连接到D1引脚

第二步:电表RS485接线

  • 电表A+ → MAX485 A+
  • 电表B- → MAX485 B-

软件环境搭建

在这里插入图片描述

所需库文件

  1. WiFiManager - 实现WiFi配网功能
  2. PubSubClient - MQTT通信支持
  3. ModbusMaster - Modbus协议支持
  4. ArduinoJson - JSON数据处理

开发环境配置

  1. 安装Arduino IDE
  2. 添加ESP8266开发板支持
  3. 安装上述依赖库

代码实现与功能说明

代码中的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());
  }
}

核心功能解析

  1. 自动配网功能

    • 首次使用自动进入配网模式
    • 通过手机轻松配置WiFi
  2. 数据采集能力

    • 支持多种电表参数读取
    • 可自定义采集频率

使用指南

  1. 烧录程序后,设备会自动创建名为"ESP8266_Meter_Config"的WiFi热点
  2. 使用手机连接该热点并完成WiFi配置
  3. 约30秒后,即可在ThingsPanel平台查看数据

遇到问题

如果遇到问题,可以文末留言或者联系我们一起交流解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值