由 WildboarG 贡献,Ai-Thinker 进行整理
【WB2】设备快速被发现
🏷️引言
如果你用过小米的智能家居产品,大概都经历过这样的过程: 买回一个米家传感器、智能插座或者智能灯,接通电源,打开米家 App,点击“添加设备”,很快 App 就会提示——发现新设备。
整个过程几乎不需要你关心网络细节: 设备的 IP 地址是什么? 它在局域网里的哪个位置? 这些问题,用户从来不需要知道。
但从技术角度看,这件事其实并不“理所当然”。在一个普通的家庭网络中,路由器只负责分配 IP 和转发数据,并不会主动告诉手机“现在多了一台智能设备”。那么,米家 App 是如何在众多设备中,第一时间找到这台刚刚上电的智能硬件的?
答案之一,就是 局域网自动发现技术,而 mDNS 正是其中最常见、也最重要的一种。
🔋什么是mDNS?
mDNS(Multicast DNS)是一种在局域网内实现域名解析的协议,它允许设备在没有中心 DNS 服务器的情况下互相发现和访问。
传统 DNS 依赖一个服务器来将域名解析为 IP 地址,而 mDNS 则通过组播(multicast)方式在局域网内广播查询与响应,实现设备的零配置自动发现。
简单来说,mDNS 能让局域网中的设备像这样互相找到对方:
my-device.local → 192.168.1.50
而不需要手动查 IP、配置 hosts 文件或依赖路由器提供的 DNS。
mDNS 的主要特点:
- 零配置:设备上电即可被发现,无需手动配置 IP 或 DNS
- 局域网内有效:只在本地链路广播,不会穿过路由器
- 兼容 DNS 格式:使用标准 DNS 报文,但通过组播发送
- 支持服务发现:配合 DNS-SD,可以发现 HTTP、SSH、打印机等服务
简单理解:
传统 DNS 是互联网“电话簿”,mDNS 是局域网里的“邻居广播”。
简答理解组播与广播:
📜 点击展开完整代码
组播:一个订阅小群🎧
广播: 大喇叭通知📢当你的设备订阅了组播 224.0.0.251 ,它会向群内广播一条关于 我是谁 我在哪 我能做什么 的记录。 群内所有人都会接收到这条消息。而大部分的智能电子设备也就是这么干的,不过各自添加了厂商的私有协议以便认证。比如苹果的Bonjour,允许 macOS、iOS 设备在局域网内自动发现打印机、共享文件夹、AirPlay 设备等服务。
mDNS 并没有重新发明一套协议,而是建立在现有网络机制之上:
| 项目 | mDNS 取值 |
|---|---|
| 传输层协议 | UDP |
| 端口号 | 5353 |
| IPv4 组播地址 | 224.0.0.251 |
| IPv6 组播地址 | ff02::fb |
| 作用范围 | 本地链路(Link-Local) |
🔎使用场景
我用wb2做了一个台灯,需要smartconfig配网,灯可以通过http进行开关和状态查询? 但是我需要知道ip地址
一种简单的方式:我去路由器后台查看新加入的设备IP
📜 点击展开:[http://192.168.0.xxx:80/light
[http://192.168.0.xxx:80/light](http://192.168.0.xxx:80/light)正常的控制我的设备
如果我多个卧室内放了多个台灯,我需要知道多个灯的ip地址.
于是我就引入了Mdns
我们可以像访问域名一样访问设备。
通过mdns将我的灯的ip自定义域名指向本地ip
📜 点击展开完整代码
http:
//台灯1.local --> 192.168.0.100这样就可智能配网后,我不需要知道设备的ip地址,通过域名就可以访问。
当有多个设备
📜 点击展开完整代码
[http://台灯1.local](http://台灯1.local)
[http://卧室台灯.local](http://卧室台灯.local)
[http://客厅吸顶灯.local](http://客厅吸顶灯.local)
[http://卧室空调.local](http://卧室空调.local)
[http://卧室加湿器.local](http://卧室加湿器.local)当然 并不局限于http协议,几乎常见的网络服务都可以这么做。
📦服务发现的核心
mDNS 中的“记录”是什么?
mDNS 使用的仍然是标准 DNS Resource Record(RR),常见的有:
| 记录类型 | 作用 |
|---|---|
| A | 主机名 → IPv4 |
| AAAA | 主机名 → IPv6 |
| PTR | 服务类型枚举 |
| SRV | 服务所在主机与端口 |
| TXT | 服务附加信息 |
| 例如最简单的一条主机记录: |
my-device.local. A 192.168.1.50
假设局域网中有多台设备:
dev1.localdev2.localdev3.local它们可能分别提供:- HTTP
- SSH
- MQTT
- 打印服务
如果只有 A 记录,你只能做到:
“我知道这个名字对应哪个 IP”
但你不知道:
- 哪些设备提供 HTTP?
- 服务监听在哪个端口?
- 一台设备是否有多个同类服务?
PTR 和 SRV 正是为了解决这些问题而存在的。
| 记录 | 责任 |
|---|---|
| PTR | 发现 (有哪些) |
| SRV | 定位 (在哪 + 端口) |
| TXT | 描述 (属性能力) |
| A/AAAA | 地址解析 |
如果你要发布的不只是主机名,而是一个服务(比如 HTTP):
【示例目标】 发布一个 HTTP 服务: my-device.local:80
需要发布的记录(成组出现)
mDNS 服务发布通常包含 四类记录:
- PTR(服务类型 → 实例名)
📜 点击展开完整代码
_http._tcp.local. → My Web Server._http._tcp.local.- SRV(实例名 → 主机名 + 端口)
📜 点击展开完整代码
My Web Server._http._tcp.local. SRV my-device.local:
80- TXT(服务属性)
📜 点击展开完整代码
My Web Server._http._tcp.local. TXT
"path=/"- A / AAAA(主机名 → IP)
📜 点击展开完整代码
my-device.local. A
192.168
.1
.50mDNS 要求这些记录一起发布,否则服务发现会不完整。
MDNS学习笔记
完整记录mDNS协议原理

🔧用WB2发布一个http服务
使用AI-WB2模拟一个http请求的台灯,并发布这个服务。
快速开始:
在sdk中打开例程protocols/http_server 在例程上快速修改;
- 删除原来的http_server.c的index 改成灯控:
📜 点击展开完整代码
#
include
"http_server.h"
#
include
<string.h>
#
include
<stdio.h>
#
define
SERVER_PORT 80
/* 模拟灯的状态 */
static
int
light_state =
0
;
// 0=OFF, 1=ON
/* HTTP 响应头 */
static
const
char
http_txt_hdr[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
;
/* 404 响应 */
static
const
char
http_404[] =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"Not Found\r\n"
;
static
void
web_http_server
(
struct
netconn *conn)
{
struct
netbuf
*
inputbuf
;
char
*buf;
u16_t
buflen;
err_t
err;
err = netconn_recv(conn, &inputbuf);
if
(err != ERR_OK)
return
;
netbuf_data(inputbuf, (
void
**)&buf, &buflen);
printf
(
"HTTP Request:\n%.*s\n"
, buflen, buf);
/* 只处理 GET */
if
(buflen >=
5
&&
strncmp
(buf,
"GET /"
,
5
) ==
0
)
{
netconn_write(conn, http_txt_hdr,
sizeof
(http_txt_hdr) -
1
, NETCONN_NOCOPY);
/* GET /light/on */
if
(
strncmp
(buf +
5
,
"light/on"
,
8
) ==
0
)
{
light_state =
1
;
netconn_write(conn,
"LIGHT ON\r\n"
,
10
, NETCONN_NOCOPY);
printf
(
"Light turned ON\n"
);
}
/* GET /light/off */
else
if
(
strncmp
(buf +
5
,
"light/off"
,
9
) ==
0
)
{
light_state =
0
;
netconn_write(conn,
"LIGHT OFF\r\n"
,
11
, NETCONN_NOCOPY);
printf
(
"Light turned OFF\n"
);
}
/* GET /light/status */
else
if
(
strncmp
(buf +
5
,
"light/status"
,
12
) ==
0
)
{
if
(light_state)
netconn_write(conn,
"LIGHT STATUS: ON\r\n"
,
18
, NETCONN_NOCOPY);
else
netconn_write(conn,
"LIGHT STATUS: OFF\r\n"
,
19
, NETCONN_NOCOPY);
}
else
{
netconn_write(conn,
"UNKNOWN CMD\r\n"
,
13
, NETCONN_NOCOPY);
}
}
else
{
netconn_write(conn, http_404,
sizeof
(http_404) -
1
, NETCONN_NOCOPY);
}
netconn_close(conn);
netbuf_delete(inputbuf);
}
void
http_server_start
(
void
*pvParameters)
{
struct
netconn
*
conn
, *
newconn
;
err_t
err;
conn = netconn_new(NETCONN_TCP);
netconn_bind(conn,
NULL
, SERVER_PORT);
netconn_listen(conn);
printf
(
"HTTP server started on port %d\n"
, SERVER_PORT);
while
(
1
)
{
err = netconn_accept(conn, &newconn);
if
(err == ERR_OK)
{
web_http_server(newconn);
netconn_delete(newconn);
}
}
}从代码中可以看出我们添加了三个get请求
📜 点击展开完整代码
light/on --> 开灯
light/off ——> 关灯
light/status -->查询灯的状态- 在wifi_execute.c中添加设备发现的逻辑
- 当wifi连接成功获取ip
- 新线程运行http服务
- 发布我的http灯控服务
📜 点击展开完整代码
#
include
"wifi_execute.h"
#
include
<lwip/init.h>
#
include
<lwip/netif.h>
#
define
STA_SSID
"OpenWrt"
#
define
STA_PASSWORD
"timeless"
enum
mdns_sd_proto
{
DNSSD_PROTO_UDP =
0
,
DNSSD_PROTO_TCP =
1
};
#
include
<mdns_server.h>
static
wifi_conf_t
conf = {
.country_code =
"CN"
,
};
/**
* @brief 向mdns服务添加TXT记录
* @param service 服务结构体
* @param txt_userdata 要添加的txt描述信息
**/
static
void
srv_txt
(
struct
mdns_service *service,
void
*txt_userdata)
{
err_enum_t
res;
/* Web 根路径 */
res = mdns_resp_add_service_txtitem(service,
"path=/"
,
6
);
/* 灯控 API 描述 */
res |= mdns_resp_add_service_txtitem(service,
"api=/light"
,
10
);
res |= mdns_resp_add_service_txtitem(service,
"on=/light/on"
,
13
);
res |= mdns_resp_add_service_txtitem(service,
"off=/light/off"
,
15
);
res |= mdns_resp_add_service_txtitem(service,
"status=/light/status"
,
21
);
if
(res != ERR_OK) {
// printf("mDNS TXT add failed\n");
}
}
/**
* @brief 启动 MDNS 响应器并注册 HTTP 服务
* @param netif 要注册的网络接口
* @return 服务的槽位号(成功)或 -1(失败)
*/
int
mdns_responder_starts
(
struct
netif *netif)
{
int
ret, slot =
-1
;
if
(netif ==
NULL
) {
printf
(
"netif is NULL\r\n"
);
return
-1
;
}
// 1. 初始化 MDNS 响应器:打开 UDP 5353 端口 [4]
mdns_resp_init();
// 2. 将网络接口添加到 MDNS,设置主机名为 "mdns.local"
// TTL 设置为 3600 秒
ret = mdns_resp_add_netif(netif,
"mywb2light"
,
3600
);
if
(ret != ERR_OK) {
mdns_resp_deinit();
printf
(
"add netif failed:%d\r\n"
, ret);
return
-1
;
}
// 3. 添加 HTTP 服务 (_http._tcp)
// 服务名称: "mdns"
// 服务类型: "_http"
// 协议: TCP (DNSSD_PROTO_TCP)
// 端口: 80
// TTL: 3600
// TXT 回调: srv_txt
slot = mdns_resp_add_service(netif,
"mdns"
,
"_http"
, DNSSD_PROTO_TCP,
80
,
3600
, srv_txt,
NULL
);
if
(slot <
0
) {
mdns_resp_remove_netif(netif);
mdns_resp_deinit();
printf
(
"add server failed:%d\r\n"
, slot);
return
-1
;
}
printf
(
"mDNS HTTP Service registered successfully on slot %d.\r\n"
, slot);
return
slot;
}
country_code_type country_code = WIFI_COUNTRY_CODE_CN;
static
wifi_interface_t
g_wifi_sta_interface =
NULL
;
static
int
g_wifi_sta_is_connected =
0
;
wifi_sta_reconnect_t
sta_reconn_params = {
15
,
10
};
// set connection interval = 15, reconntction times = 10
void
wifi_background_init
(country_code_type country_code)
{
char
*country_code_string[WIFI_COUNTRY_CODE_MAX] = {
"CN"
,
"JP"
,
"US"
,
"EU"
};
/* init wifi background*/
strcpy
(conf.country_code, country_code_string[country_code]);
wifi_mgmr_start_background(&conf);
/* enable scan hide ssid */
wifi_mgmr_scan_filter_hidden_ssid(
0
);
}
int
wifi_sta_connect
(
void
)
{
g_wifi_sta_interface = wifi_mgmr_sta_enable();
if
(g_wifi_sta_is_connected ==
1
)
{
printf
(
"sta has connect\n"
);
return
0
;
}
else
{
wifi_mgmr_sta_autoconnect_enable();
printf
(
"connect to wifi %s\n"
, STA_SSID);
return
wifi_mgmr_sta_connect(g_wifi_sta_interface, STA_SSID, STA_PASSWORD,
NULL
,
NULL
,
0
,
0
);
}
}
static
void
wifi_event_cb
(
input_event_t
*event,
void
*private_data)
{
static
char
*ssid;
static
char
*password;
printf
(
"[APP] [EVT] event->code %d\r\n"
, event->code);
printf
(
"[SYS] Memory left is %d Bytes\r\n"
, xPortGetFreeHeapSize());
switch
(event->code)
{
case
CODE_WIFI_ON_AP_STARTED:
{
printf
(
"[APP] [EVT] AP INIT DONE %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_AP_STOPPED:
{
printf
(
"[APP] [EVT] AP STOP DONE %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_INIT_DONE:
{
printf
(
"[APP] [EVT] INIT DONE %lld\r\n"
, aos_now_ms());
wifi_mgmr_start_background(&conf);
wifi_sta_connect();
}
break
;
case
CODE_WIFI_ON_MGMR_DONE:
{
printf
(
"[APP] [EVT] MGMR DONE %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_SCAN_DONE:
{
printf
(
"[APP] [EVT] SCAN Done %lld\r\n"
, aos_now_ms());
// wifi_mgmr_cli_scanlist();
}
break
;
case
CODE_WIFI_ON_DISCONNECT:
{
g_wifi_sta_is_connected =
0
;
printf
(
"wifi sta disconnected\n"
);
printf
(
"[APP] [EVT] disconnect %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_CONNECTING:
{
printf
(
"[APP] [EVT] Connecting %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_CMD_RECONNECT:
{
printf
(
"[APP] [EVT] Reconnect %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_CONNECTED:
{
printf
(
"wifi sta connected\n"
);
printf
(
"[APP] [EVT] connected %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_PRE_GOT_IP:
{
printf
(
"[APP] [EVT] connected %lld\r\n"
, aos_now_ms());
}
break
;
case
CODE_WIFI_ON_GOT_IP:
{
printf
(
"WIFI STA GOT IP\n"
);
printf
(
"[APP] [EVT] GOT IP %lld\r\n"
, aos_now_ms());
/* create http server task */
xTaskCreate(http_server_start, (
char
*)
"http server"
,
1024
*
4
,
NULL
,
15
,
NULL
);
g_wifi_sta_is_connected =
1
;
struct
netif
*
netif
=
wifi_mgmr_sta_netif_get();
// 依赖于您的 SDK 实现
if
(netif) {
if
(mdns_responder_starts(netif) >=
0
) {
printf
(
"mDNS HTTP Service Discovery started.\r\n"
);
}
else
{
printf
(
"mDNS startup failed.\r\n"
);
}
}
else
{
printf
(
"Error: Could not retrieve active netif.\r\n"
);
}
}
break
;
case
CODE_WIFI_ON_PROV_SSID:
{
printf
(
"[APP] [EVT] [PROV] [SSID] %lld: %s\r\n"
,
aos_now_ms(),
event->value ? (
const
char
*)event->value :
"UNKNOWN"
);
if
(ssid)
{
vPortFree(ssid);
ssid =
NULL
;
}
ssid = (
char
*)event->value;
}
break
;
case
CODE_WIFI_ON_PROV_BSSID:
{
printf
(
"[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n"
,
aos_now_ms(),
event->value ? (
const
char
*)event->value :
"UNKNOWN"
);
if
(event->value)
{
vPortFree((
void
*)event->value);
}
}
break
;
case
CODE_WIFI_ON_PROV_PASSWD:
{
printf
(
"[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n"
, aos_now_ms(),
event->value ? (
const
char
*)event->value :
"UNKNOWN"
);
if
(password)
{
vPortFree(password);
password =
NULL
;
}
password = (
char
*)event->value;
}
break
;
case
CODE_WIFI_ON_PROV_CONNECT:
{
printf
(
"connecting to %s:%s...\r\n"
, ssid, password);
}
break
;
case
CODE_WIFI_ON_PROV_DISCONNECT:
{
printf
(
"[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n"
, aos_now_ms());
}
break
;
default
:
{
printf
(
"[APP] [EVT] Unknown code %u, %lld\r\n"
, event->code, aos_now_ms());
/*nothing*/
}
}
void
wifi_execute
(
void
*pvParameters)
{
aos_register_event_filter(EV_WIFI, wifi_event_cb,
NULL
);
static
uint8_t
stack_wifi_init =
0
;
if
(
1
== stack_wifi_init)
{
printf
(
"Wi-Fi Stack Started already!!!\r\n"
);
return
;
}
stack_wifi_init =
1
;
printf
(
"Wi-Fi init successful\r\n"
);
hal_wifi_start_firmware_task();
/*Trigger to start Wi-Fi*/
aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE,
0
);
vTaskDelete(
NULL
);
}可以在代码中看到,我只是添加了两个函数,一个启动 MDNS 响应器并注册 HTTP 服务,一个添加TXT解析信息。
在头文件中引入:
📜 点击展开完整代码
#
include
<lwip/init.h>
#
include
<lwip/netif.h>还要再引入mdns_server.h之前定义
📜 点击展开完整代码
enum
mdns_sd_proto
{
DNSSD_PROTO_UDP =
0
,
DNSSD_PROTO_TCP =
1
};
#
include
mdns_server.h我们定义了灯的本地域名为 mywb2light.local
烧录验证
- 连接开发板烧录
- 查看日志信息

通过串口工具看到服务已经正常启动,分配ip:192.168.0.109 然后启动了mDNS http server
- 我们再用mdns服务发现工具查看是否服务正常发布

📜 点击展开完整代码
- hostname = [mywb2light.local]
- adress =
192.168
.0
.109
- prot =
80ip和域名对上了,而且txt记录里存放我的控制台灯的api
- 用浏览器打开输入域名+api


⛏️拓展
现在可以用自定义内网域名访问wb2提供的服务了,不需要每次都要查看ip地址,多设备也ok。
这样做有什么用?
- 多设备可以快速区分;
- 设备可以能被home-Assistant自动发现注册
当然对于设备不多的小伙伴,没必要使用home-Assistant ,
可以用ai-wv01-32s小安智能助手自动识别我内网下的服务自动注册为工具,通过语音唤醒就可以调用各个设备。
我们把设备如何使用的API发布在TXT描述信息了方便小安知道如何控制设备。
设备上电后,在组播中告诉内网设备 【我是谁】【我在哪】【我能干嘛】,小安AI 也在监听这个组播地址,当识别到新设备自动注册为自己的内部工具,用户就可以语音控制。

