Skip to content

由 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 是局域网里的“邻居广播”

简答理解组播与广播:

📜 点击展开完整代码
html
组播:一个订阅小群🎧
广播: 大喇叭通知📢

当你的设备订阅了组播 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
bash
[http://192.168.0.xxx:80/light](http://192.168.0.xxx:80/light)

正常的控制我的设备

如果我多个卧室内放了多个台灯,我需要知道多个灯的ip地址.

于是我就引入了Mdns

我们可以像访问域名一样访问设备。

通过mdns将我的灯的ip自定义域名指向本地ip

📜 点击展开完整代码
c
http:
//台灯1.local  --> 192.168.0.100

这样就可智能配网后,我不需要知道设备的ip地址,通过域名就可以访问。

当有多个设备

📜 点击展开完整代码
html
[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.local
  • dev2.local
  • dev3.local 它们可能分别提供:
  • HTTP
  • SSH
  • MQTT
  • 打印服务

如果只有 A 记录,你只能做到:

“我知道这个名字对应哪个 IP”

但你不知道

  • 哪些设备提供 HTTP?
  • 服务监听在哪个端口?
  • 一台设备是否有多个同类服务?

PTR 和 SRV 正是为了解决这些问题而存在的。

记录责任
PTR发现 (有哪些)
SRV定位 (在哪 + 端口)
TXT描述 (属性能力)
A/AAAA地址解析

如果你要发布的不只是主机名,而是一个服务(比如 HTTP):

【示例目标】 发布一个 HTTP 服务: my-device.local:80

需要发布的记录(成组出现)

mDNS 服务发布通常包含 四类记录

  • PTR(服务类型 → 实例名)
📜 点击展开完整代码
c
_http._tcp.local. → My Web Server._http._tcp.local.
  • SRV(实例名 → 主机名 + 端口)
📜 点击展开完整代码
c
My Web Server._http._tcp.local. SRV my-device.local:
80
  • TXT(服务属性)
📜 点击展开完整代码
c
My Web Server._http._tcp.local. TXT
"path=/"
  • A / AAAA(主机名 → IP)
📜 点击展开完整代码
c
my-device.local. A
192.168
.1
.50

mDNS 要求这些记录一起发布,否则服务发现会不完整。

MDNS学习笔记

完整记录mDNS协议原理

阅读笔记

🔧用WB2发布一个http服务


使用AI-WB2模拟一个http请求的台灯,并发布这个服务。

快速开始:

在sdk中打开例程protocols/http_server 在例程上快速修改;

  1. 删除原来的http_server.c的index 改成灯控:
📜 点击展开完整代码
c
#
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请求

📜 点击展开完整代码
c
light/on   --> 开灯
light/off  ——> 关灯
light/status -->查询灯的状态
  1. 在wifi_execute.c中添加设备发现的逻辑
  • 当wifi连接成功获取ip
  • 新线程运行http服务
  • 发布我的http灯控服务
📜 点击展开完整代码
c
#
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解析信息。

在头文件中引入:

📜 点击展开完整代码
c
#
include

<lwip/init.h>

#
include

<lwip/netif.h>

还要再引入mdns_server.h之前定义

📜 点击展开完整代码
c
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服务发现工具查看是否服务正常发布

📜 点击展开完整代码
c
- hostname = [mywb2light.local]
- adress =
192.168
.0
.109

- prot =
80

ip和域名对上了,而且txt记录里存放我的控制台灯的api

  • 用浏览器打开输入域名+api

⛏️拓展


现在可以用自定义内网域名访问wb2提供的服务了,不需要每次都要查看ip地址,多设备也ok。

这样做有什么用?

  • 多设备可以快速区分;
  • 设备可以能被home-Assistant自动发现注册

当然对于设备不多的小伙伴,没必要使用home-Assistant ,

可以用ai-wv01-32s小安智能助手自动识别我内网下的服务自动注册为工具,通过语音唤醒就可以调用各个设备。

我们把设备如何使用的API发布在TXT描述信息了方便小安知道如何控制设备。

设备上电后,在组播中告诉内网设备 【我是谁】【我在哪】【我能干嘛】,小安AI 也在监听这个组播地址,当识别到新设备自动注册为自己的内部工具,用户就可以语音控制。

Released under the MIT License. Build Time 2026-09-11 14:52:23