Skip to content

Contributed by WildboarG, organized by Ai-Thinker

[WB2] Devices Discoverable at a Glance

🏷️ Introduction

If you've used Xiaomi's smart home products, you've probably gone through something like this: you buy a Mijia sensor, smart plug, or smart light, plug it in, open the Mijia app, tap "Add Device", and the app quickly prompts — New Device Discovered.

The whole process barely requires you to care about network details: what's the device's IP address? Where is it on the LAN? Users never need to know.

But from a technical perspective, this is not a given. On an ordinary home network, the router only assigns IPs and forwards data; it doesn't proactively tell your phone "there's a new smart device now". So how does the Mijia app find this just-powered-on smart hardware among so many devices, in real time?

One answer is LAN auto-discovery technology, and mDNS is the most common and most important of these.

🔋 What is mDNS?


mDNS (Multicast DNS) is a protocol that implements domain name resolution within a LAN, allowing devices to discover and access each other without a central DNS server.

Traditional DNS relies on a server to resolve domain names into IP addresses, while mDNS broadcasts queries and responses within the LAN via multicast, achieving zero-configuration automatic device discovery.

In short, mDNS lets devices on a LAN find each other like this:

my-device.local → 192.168.1.50

No need to manually look up IPs, configure the hosts file, or rely on the DNS provided by the router.

Main features of mDNS:

  • Zero configuration: devices are discoverable right after power-on, with no manual IP or DNS configuration
  • LAN-scoped: only broadcasts on the local link, never crosses the router
  • DNS-format compatible: uses standard DNS messages, but sent via multicast
  • Service discovery support: with DNS-SD, it can discover services such as HTTP, SSH, printers

In simple terms:

传统 DNS 是互联网“电话簿”,mDNS 是局域网里的“邻居广播”

A quick take on multicast vs broadcast:

Click to expand full code
html
组播:一个订阅小群🎧
广播: 大喇叭通知📢

When your device subscribes to the multicast group 224.0.0.251, it broadcasts a record to the group about 我是谁 我在哪 我能做什么. Everyone in the group receives this message. Most smart electronic devices work this way, except each adds its vendor's private protocol for authentication. For example, Apple's Bonjour allows macOS and iOS devices to automatically discover services such as printers, shared folders, and AirPlay devices on the LAN.

mDNS didn't reinvent a protocol — it's built on existing network mechanisms:

ItemmDNS Value
Transport layer protocolUDP
Port number5353
IPv4 multicast address224.0.0.251
IPv6 multicast addressff02::fb
ScopeLocal link (Link-Local)

🔎 Use Cases


I used the WB2 to make a desk lamp that needs smartconfig provisioning, and the lamp can be switched on/off and its status queried via http. But I need to know its IP address.

One simple way: go to the router admin page and look up the IP of the newly added device

Click to expand: http://192.168.0.xxx:80/light
bash
[http://192.168.0.xxx:80/light](http://192.168.0.xxx:80/light)

Now I can control my device normally.

If I put multiple desk lamps in several bedrooms, I'd need to know the IP addresses of all the lamps.

So I introduced mDNS

We can access devices just like visiting a domain name.

Through mDNS, point my lamp's IP to a custom domain name bound to a local IP

Click to expand full code
c
http:
//台灯1.local  --> 192.168.0.100

This way, after smart provisioning, I don't need to know the device's IP address — I can access it by domain name.

With multiple devices

Click to expand full code
html
[http://台灯1.local](http://台灯1.local)
[http://卧室台灯.local](http://卧室台灯.local)
[http://客厅吸顶灯.local](http://客厅吸顶灯.local)
[http://卧室空调.local](http://卧室空调.local)
[http://卧室加湿器.local](http://卧室加湿器.local)

Of course, it's not limited to the http protocol — almost any common network service can do this.

📦 The Core of Service Discovery


What is a "record" in mDNS?

mDNS still uses standard DNS Resource Records (RR), the common ones being:

Record typePurpose
AHostname → IPv4
AAAAHostname → IPv6
PTRService type enumeration
SRVHost and port of the service
TXTAdditional service info
For example, the simplest host record:

my-device.local. A 192.168.1.50

Suppose there are multiple devices on the LAN:

  • dev1.local
  • dev2.local
  • dev3.local They may each provide:
  • HTTP
  • SSH
  • MQTT
  • Print services

With only A records, all you can do is:

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

But you don't know:

  • Which devices offer HTTP?
  • Which port is the service listening on?
  • Does one device have multiple services of the same type?

PTR and SRV exist precisely to solve these problems.

RecordResponsibility
PTRDiscover (what exists)
SRVLocate (where + port)
TXTDescribe (attributes/capabilities)
A/AAAAAddress resolution

If what you want to publish is not just a hostname but a service (like HTTP):

Example goal Publish an HTTP service: my-device.local:80

Records that need to be published (appear as a group)

mDNS service publishing usually includes four types of records:

  • PTR (service type → instance name)
Click to expand full code
c
_http._tcp.local. → My Web Server._http._tcp.local.
  • SRV (instance name → hostname + port)
Click to expand full code
c
My Web Server._http._tcp.local. SRV my-device.local:
80
  • TXT (service attributes)
Click to expand full code
c
My Web Server._http._tcp.local. TXT
"path=/"
  • A / AAAA (hostname → IP)
Click to expand full code
c
my-device.local. A
192.168
.1
.50

mDNS requires these records to be published together, otherwise service discovery will be incomplete.

mDNS Study Notes

A complete record of the mDNS protocol principles

Study Notes

🔧 Publishing an HTTP Service with WB2


Use the AI-WB2 to simulate an http-controlled lamp, and publish this service.

Quick Start:

Open the protocols/http_server example in the SDK Make quick modifications to the example;

  1. Delete the original index of http_server.c Change it to lamp control:
Click to expand full code
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);
        }
    }
}

From the code you can see we added three GET requests

Click to expand full code
c
light/on   --> 开灯
light/off  ——> 关灯
light/status -->查询灯的状态
  1. Add device discovery logic in wifi_execute.c
  • When wifi connects successfully and obtains an IP
  • A new thread runs the http service
  • Publish my http lamp control service
Click to expand full code
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
);
}

You can see in the code that I only added two functions: one starts the MDNS responder and registers the HTTP service, and one adds TXT description info.

Include in the header file:

Click to expand full code
c
#
include

<lwip/init.h>

#
include

<lwip/netif.h>

Also define this before including mdns_server.h

Click to expand full code
c
enum

mdns_sd_proto
 {

     DNSSD_PROTO_UDP =
0
,
     DNSSD_PROTO_TCP =
1

 };

#
include
 mdns_server.h

We defined the lamp's local domain name as mywb2light.local

Flash and Verify

  • Connect the dev board and flash
  • Check the log output

Through the serial port tool, we can see the service started normally, got IP 192.168.0.109, then the mDNS http server started

  • Then use an mDNS service discovery tool to check whether the service is published correctly

Click to expand full code
c
- hostname = [mywb2light.local]
- adress =
192.168
.0
.109

- prot =
80

The IP and domain name match, and the txt record stores my lamp control API

  • Open the browser and enter the domain name + api

⛏️ Extensions


Now you can access the services provided by the WB2 with a custom intranet domain name — no need to check the IP address every time, and multiple devices work fine too.

What's the use of this?

  • Multiple devices can be distinguished quickly;
  • Devices can be automatically discovered and registered by home-Assistant

Of course, for those with few devices, there's no need for home-Assistant.

You can use the ai-wv01-32s Xiao An smart assistant to automatically recognize the services on my intranet and register them as tools; wake it by voice to control the devices.

We publish the API for using the device in the TXT description so Xiao An knows how to control the device.

After the device powers on, it tells other devices on the intranet via multicast who it is, where it is, and what it can do. Xiao An AI is also listening on this multicast address; when it recognizes a new device, it automatically registers it as one of its internal tools, and the user can control it by voice.

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