发帖
7 1 0

【WB2】设备快速被发现

WildboarG
论坛元老

36

主题

256

回帖

7505

积分

论坛元老

积分
7505
Ai-WB2系列 536 7 2025-12-17 13:52:51
[i=s] 本帖最后由 WildboarG 于 2025-12-21 22:38 编辑 [/i]

🏷️引言

如果你用过小米的智能家居产品,大概都经历过这样的过程:
买回一个米家传感器、智能插座或者智能灯,接通电源,打开米家 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

正常的控制我的设备

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

于是我就引入了Mdns

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

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

http://台灯1.local  --> 192.168.0.100

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

当有多个设备

http://台灯1.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(服务类型 → 实例名)
    _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.50
    

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

MDNS学习笔记

完整记录mDNS协议原理

MDNS卡片大图

🔧用WB2发布一个http服务


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

快速开始:

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

  1. 删除原来的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 -->查询灯的状态
  1. 在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

烧录验证

  • 连接开发板烧录
  • 查看日志信息

1.png

通过串口工具看到服务已经正常启动,分配ip:192.168.0.109 然后启动了mDNS http server

  • 我们再用mdns服务发现工具查看是否服务正常发布

2.png

- hostname = [mywb2light.local]
- adress = 192.168.0.109
- prot = 80

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

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

3.png

4.png

⛏️拓展


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

这样做有什么用?

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

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

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

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

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

──── 1人觉得很赞 ────

使用道具 举报

牛,你比我厉害
2025-12-17 15:40:12

佬,别开玩笑了,我是小菜鸡
2025-12-17 17:21:20
666
2025-12-17 22:23:34
哇,这个好厉害
2025-12-18 12:04:23
厉害
2025-12-18 17:34:26
iiv 发表于 2025-12-17 22:23
哇,这个好厉害

七哥,搞起来
2025-12-23 22:34:19

哇,这个好厉害
您需要登录后才可以回帖 立即登录
高级模式
返回
统计信息
  • 会员数: 30626 个
  • 话题数: 44727 篇