用 mDNS 实现逆变器与电表的自动通信

在工业物联网和智能家居场景中,我们经常会遇到这样的问题:同一局域网内的两个设备(比如逆变器和电表)想要通信,但彼此不知道对方的 IP 地址。手动配置 IP 既繁琐又容易出错,而传统的 DNS 服务又依赖于服务器,无法满足零配置的需求。

这时候,**mDNS(Multicast Domain Name System,多播域名系统)** 就成了我们的最佳选择。它是一种零配置网络协议,能让设备在局域网内自动发现彼此,无需手动配置 IP 或依赖 DNS 服务器。

本文将结合实际项目场景,详细介绍 mDNS 的原理、实现方式,以及如何在 ESP32 平台上进行代码开发,最终实现逆变器自动发现并连接电表的功能。


一、场景与问题

我们的场景非常典型:

  • 电表:作为数据采集端,需要将电力数据(电压、电流、功率等)上报。
  • 逆变器:作为控制端,需要从电表获取数据进行逻辑处理。
  • 网络环境:两者连接到同一个 WiFi 局域网。

核心痛点:

  1. 逆变器不知道电表的 IP 地址,无法建立连接。
  2. 手动配置 IP 地址在大规模部署时效率低下,且容易出错。
  3. 希望实现 "上电即通" 的零配置体验。

二、mDNS 是什么?

mDNS,全称多播域名系统,是一种零配置网络协议。它的核心作用是在局域网内实现设备的自动发现和域名解析。

简单来说,mDNS 让设备可以:

  1. 给自己起个名字 :比如 meter.local,并向局域网内的所有设备广播这个名字和对应的 IP 地址。
  2. 通过名字找人:当其他设备(如逆变器)需要和它通信时,只需要喊出这个名字,就能自动获取到它的 IP 地址。

mDNS 的工作流程分为两个阶段:

1. 注册阶段(服务端 / 电表侧)

  • 设备(电表)启动时,向局域网的 mDNS 多播地址(224.0.0.251:5353)发送一个 "声明报文",宣布自己的主机名(如 meter.local)和 IP 地址。
  • 如果需要发布服务(如自动发现),设备还会注册服务类型(如 _meter._tcp),并在报文中包含服务的端口、IP 等信息。
  • 局域网内的其他设备会缓存这些信息,以便后续快速解析。

2. 发现阶段(客户端 / 逆变器侧)

  • 当客户端(逆变器)需要访问 meter.local 时,会向局域网多播发送一个 "查询报文":"谁是 meter.local?请告诉我你的 IP 地址。"
  • 拥有 meter.local 主机名的设备(电表)收到查询后,会单播响应自己的 IP 地址。
  • 如果客户端查询的是服务类型(如 _meter._tcp),电表会响应服务的详细信息(IP、端口、设备名称等),客户端即可自动连接。

三、基础实现:注册主机名,实现 "ping 通域名"

这是最基础的 mDNS 应用,也是我们项目中 "顾永盛" 提到的 "没有做服务,就是单独的域名" 的场景。

1. 电表端代码(注册名字)

电表启动后,先连接 WiFi,然后注册 mDNS 主机名 meter.local。这样,局域网内的任何设备都可以通过 ping meter.local 来获取它的 IP 地址。

c

运行

复制代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"

// WiFi名称和密码(和逆变器连同一个)
#define WIFI_SSID      "你的WiFi名称"
#define WIFI_PASS      "你的WiFi密码"
static const char *TAG = "电表-基础版";

// 初始化mDNS:给电表起固定名字 meter.local
static void start_mdns_service(void)
{
    // 初始化mDNS
    mdns_init();
    // 核心:设置主机名 → 域名就是 meter.local
    mdns_hostname_set("meter");
    ESP_LOGI(TAG, "电表mDNS启动完成,域名:meter.local");
}

// WiFi连接(标准代码)
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    if (event_id == WIFI_EVENT_STA_START) esp_wifi_connect();
    if (event_id == WIFI_EVENT_STA_CONNECTED) ESP_LOGI(TAG, "WiFi连接成功");
}

static void wifi_init(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL));
    wifi_config_t wifi_cfg = {
        .sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());
}

void app_main(void)
{
    // 初始化NVS
    nvs_flash_init();
    // 连接WiFi
    wifi_init();
    // 等待WiFi连接稳定
    vTaskDelay(pdMS_TO_TICKS(3000));
    // 启动mDNS(关键代码)
    start_mdns_service();

    // 电表后续:启动TCP服务器,等待逆变器连接
    // tcp_server_start();
}

2. 逆变器端代码(解析名字)

逆变器启动后,连接到同一个 WiFi,然后通过 mDNS 查询 meter.local,自动获取电表的 IP 地址。

c

运行

复制代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "lwip/sockets.h"

#define WIFI_SSID      "你的WiFi名称"
#define WIFI_PASS      "你的WiFi密码"
static const char *TAG = "逆变器-基础版";

// 核心函数:通过域名 meter.local 获取电表IP
static void get_meter_ip(void)
{
    esp_ip4_addr_t ip;
    // 关键:解析 mDNS 域名 → 自动得到IP
    esp_err_t err = mdns_query_a("meter.local", 2000, &ip);
    
    if (err == ESP_OK) {
        // 成功!打印电表IP
        char ip_str[16];
        sprintf(ip_str, IPSTR, IP2STR(&ip));
        ESP_LOGI(TAG, "✅ 成功获取电表IP:%s", ip_str);
        
        // 逆变器拿到IP后,直接连接电表的TCP端口(比如10241)
        // tcp_connect(ip, 10241);
    } else {
        ESP_LOGE(TAG, "❌ 未找到电表");
    }
}

void app_main(void)
{
    nvs_flash_init();
    wifi_init(); // 连同一个WiFi
    vTaskDelay(pdMS_TO_TICKS(3000));
    
    // 自动获取电表IP
    get_meter_ip();
}

效果验证

  • 电脑连接到同一个 WiFi,打开命令行输入 ping meter.local,如果能 ping 通,说明 mDNS 基础功能正常。
  • 逆变器的串口日志会打印出电表的 IP 地址。

四、进阶实现:注册服务广播,实现 "零配置自动发现"

基础实现虽然解决了 IP 获取的问题,但逆变器仍然需要知道电表的域名 meter.local。在更复杂的场景中,我们希望逆变器连域名都不用记,上电后自动扫描局域网内的电表服务,然后自动连接。

这就是 "鑫" 提到的 "广播服务",我们可以通过注册 mDNS 服务类型来实现。

1. 电表端代码(广播服务)

电表不仅注册主机名,还额外注册一个服务类型 _meter._tcp,并广播自己的 TCP 端口(如 10241)。

c

运行

复制代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"

// ==================== 配置项 ====================
#define WIFI_SSID      "你的WiFi名称"
#define WIFI_PASS      "你的WiFi密码"
#define METER_TCP_PORT 10241  // 电表TCP端口
static const char *TAG = "电表-服务端";

// ==================== mDNS服务注册(核心) ====================
static void mdns_service_start(void) {
    // 1. 初始化mDNS
    ESP_ERROR_CHECK(mdns_init());
    // 2. 设置主机名(可选,兼容ping)
    mdns_hostname_set("meter");
    // 3. 【进阶核心】注册电表服务:_meter._tcp  端口10241
    ESP_ERROR_CHECK(
        mdns_service_add(
            "智能电表",          // 服务名称(自定义)
            "_meter",           // 服务类型(固定,逆变器按这个搜索)
            "_tcp",             // 协议(TCP)
            METER_TCP_PORT,     // 服务端口
            NULL, 0             // 无额外参数
        )
    );
    ESP_LOGI(TAG, "✅ 电表服务广播成功:_meter._tcp.local,端口:%d", METER_TCP_PORT);
}

// ==================== WiFi连接(标准代码) ====================
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    if (event_id == WIFI_EVENT_STA_START) esp_wifi_connect();
    if (event_id == WIFI_EVENT_STA_CONNECTED) ESP_LOGI(TAG, "WiFi连接成功");
}

static void wifi_init(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL));
    wifi_config_t wifi_cfg = {
        .sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());
}

// ==================== 主函数 ====================
void app_main(void) {
    wifi_init();
    vTaskDelay(pdMS_TO_TICKS(3000));
    mdns_service_start();  // 启动mDNS广播

    // 后续:启动TCP服务器,等待逆变器连接
    // tcp_server_init(METER_TCP_PORT);
}

2. 逆变器端代码(自动发现服务)

逆变器启动后,不再查询特定的域名,而是直接扫描局域网内的 _meter._tcp 服务,自动获取电表的 IP 和端口。

c

运行

复制代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "lwip/sockets.h"

// ==================== 配置项 ====================
#define WIFI_SSID      "你的WiFi名称"
#define WIFI_PASS      "你的WiFi密码"
static const char *TAG = "逆变器-客户端";

// ==================== 自动发现电表服务(核心) ====================
static void find_meter_service(void) {
    mdns_result_t *results = NULL;
    // 搜索局域网内的 _meter._tcp 服务(和电表注册的一致)
    esp_err_t err = mdns_query_ptr("_meter", "_tcp", 2000, 1, &results);

    if (err != ESP_OK || !results) {
        ESP_LOGE(TAG, "❌ 未搜索到电表服务");
        return;
    }

    // 自动获取 电表IP + 端口
    char ip_str[16];
    sprintf(ip_str, IPSTR, IP2STR(&results->addr->u_addr.ip4.addr));
    uint16_t port = results->port;

    ESP_LOGI(TAG, "==================================");
    ESP_LOGI(TAG, "✅ 发现电表服务!");
    ESP_LOGI(TAG, "电表IP:%s", ip_str);
    ESP_LOGI(TAG, "电表端口:%d", port);
    ESP_LOGI(TAG, "==================================");

    // ==================== 直接连接TCP(零配置) ====================
    // tcp_connect(ip_str, port);

    mdns_query_results_free(results); // 释放资源
}

// ==================== WiFi连接(和电表一致) ====================
static void wifi_init(void) {
    // 代码和电表端完全相同,直接复制
    ESP_ERROR_CHECK(nvs_flash_init());
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    wifi_config_t wifi_cfg = {.sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}};
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
    esp_wifi_start();
    vTaskDelay(pdMS_TO_TICKS(3000));
}

// ==================== 主函数 ====================
void app_main(void) {
    wifi_init();
    find_meter_service(); // 自动搜电表
}

运行效果:逆变器的串口日志会直接打印出电表的 IP 和端口,实现了真正的零配置连接。


五、TCP vs UDP:一对一还是一对多?

在我们的项目中,TCP 用于可靠的功率数据传输,而 UDP 用于 mDNS 设备发现。理解它们的 "一对一" 和 "一对多" 特性至关重要。

表格

协议 单个连接 / 收发 服务端支持 典型场景 你的项目用法
TCP 一对一 一对多(多独立连接) 可靠传输、功率采集 EMS 服务端 ↔ 多台电表(一对一连接,一对多通信)
UDP 一对一 / 一对多 / 多对多 天然一对多 广播、发现、实时传输 mDNS 设备发现(一对多广播)

1. TCP:像打电话

  • 单个连接是严格一对一:就像两个人打电话,一条通话链路只能有两个人。
  • 服务端可以一对多:就像客服热线,一个服务端可以同时接多个客户端的来电,每通电话都是独立的一对一连接。

在我们的项目中,EMS 作为 TCP 服务端,可以同时对接多台电表,每台电表与 EMS 之间都是独立的 TCP 连接,保证了数据传输的可靠性。

2. UDP:像发传单 / 广播

  • 天生无连接,灵活多变:UDP 没有 "建立连接" 的步骤,发数据包就像扔传单或广播喊话。
  • 单播(一对一):逆变器单独给某台电表发消息。
  • 广播 / 组播(一对多):1 台设备发消息,同一个局域网内所有设备都能收到。这就是 mDNS 使用 UDP 的原因,电表广播自己的信息,所有逆变器都能收到。

六、总结

通过 mDNS 技术,我们成功解决了局域网内设备自动发现的问题,实现了逆变器与电表的零配置通信。

  1. 基础版:通过注册 mDNS 主机名,实现了通过域名获取 IP 地址的功能。
  2. 进阶版:通过注册 mDNS 服务类型,实现了完全自动的服务发现,逆变器无需任何配置即可连接到电表。
  3. 协议选择:TCP 用于可靠的功率数据传输,UDP 用于高效的设备发现,两者各司其职,完美配合。
相关推荐
fygfh.6 小时前
Linux的系统架构浅析
linux·arm开发·系统架构
忆和熙1 天前
ARM Load/Store指令、伪指令(ARM处理器指令系统——ARM指令集初学,下篇)
arm开发·arm指令
忆和熙1 天前
ARM数据处理指令(ARM处理器指令系统——ARM指令集初学,上篇)
arm开发·arm指令
EnglishJun1 天前
ARM嵌入式学习(一) --- 入门51
arm开发·学习
路溪非溪2 天前
systemd简介和使用总结
linux·arm开发·驱动开发
想要成为计算机高手3 天前
研究 telegrip - SO100 Robot Arm Teleoperation System
arm开发·机器人·开源·具身智能·摇操·telegrip
编码如写诗3 天前
【k8s】arm架构从零开始在线/离线部署k8s1.34.5+KubeSphere3.4.1
arm开发·架构·kubernetes
EVERSPIN3 天前
BLE蓝牙水表蓝牙芯片方案
arm开发·蓝牙芯片·蓝牙芯片方案
银河麒麟操作系统3 天前
银河麒麟桌面操作系统V10SP1(全X86/ARM架构)【进程资源限制与性能优化实践】技术文章
arm开发·性能优化·架构