先分清名字、实例和服务类型
主机名是配置前缀加MAC后3字节,例如hixinwu-n06-ab0cff.local;服务实例是可浏览的人类可读名称;_http._tcp表示服务类型。浏览PTR找到实例,再解析SRV/TXT得到目标主机和端口,最后解析地址并访问HTTP。
本机注册成功不等于电脑或手机已经发现。本例页面把service_registered与client_discovery=not-measured分开;当前已有构建与主机名函数输入检查,没有本站两类客户端或ESP32网络实测。
- 做到这里,你应该看到
- 能说明-B浏览、-L解析实例、-G解析主机地址分别检查哪一层。
参考:[2] mdns1.9.1固定提交API:初始化、主机名、实例与服务注册[4] 固定提交mDNS指南:hostname、instance、service[6] RFC6763 §4–6:实例、浏览和解析
完整工程固定组件,不从旧SDK目录抄mDNS
下载包固定IDF5.5.5与espressif/mdns==1.9.1,Registry对应官方提交3bfa00389de6f0d6d40efda8bea808380899a43d;真实dependencies.lock记录组件hash。managed_components由构建解析,不放进下载包,也不以删锁更新代替网络排错。
指定板只接USB-to-UART,115200监视。Demo configuration填写自有WPA2凭据,默认占位符不发起连接;本地sdkconfig含密码不分享。README给出安装、默认构建和网络路径构建命令,作者已经实际完成两种构建并核对Wi-Fi、HTTP、mDNS符号,仍不代表无线可用。
idf.py --version
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p REPLACE_WITH_UART_PORT flash monitor参考:[1] Espressif mdns1.9.1 Registry:固定版本与仓库提交[2] mdns1.9.1固定提交API:初始化、主机名、实例与服务注册[7] ESP-IDF5.5.5 HTTP Server请求与响应接口
把配置名限制在可解释的范围
前缀限定1–56个小写ASCII字母、数字或中划线,首尾不用中划线;不要填.local、空格、引号或下划线。追加6位十六进制MAC后缀后最长63字节。该后缀降低普通重名概率,不是全网唯一或认证身份。
非法前缀使mDNS关闭,但不会使HTTP/IP后路也关闭。固定组件在冲突时可能给host或instance追加/更改后缀,configured_name不保证是最终仲裁名;以客户端实际浏览/解析结果为准,必要时直接用当前IP。
// SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static inline bool discovery_hostname(char *out, size_t cap, const char *prefix, const uint8_t mac[6]) {
if (!out || !cap || !prefix || !mac) return false;
out[0]='\0'; size_t n=strlen(prefix);
if (n<1 || n>56 || prefix[0]=='-' || prefix[n-1]=='-') return false;
for(size_t i=0;i<n;i++) if (!((prefix[i]>='a'&&prefix[i]<='z') || (prefix[i]>='0'&&prefix[i]<='9') || prefix[i]=='-')) return false;
int k=snprintf(out,cap,"%s-%02x%02x%02x",prefix,mac[3],mac[4],mac[5]);
if(k<0 || (size_t)k>=cap) {out[0]='\0'; return false;} return true;
}
参考:[2] mdns1.9.1固定提交API:初始化、主机名、实例与服务注册[3] mdns1.9.1固定提交实现:STA事件、冲突改名与服务状态[5] RFC6762 §3与§5:.local链路范围及组播查询
先启动HTTP,再注册发现服务
HTTP80提供静态根页和/status有界JSON,页面没有外部CDN。HTTP线程从单元素队列读快照,主循环写网络与服务状态。初始化或注册mDNS失败时释放已初始化组件、10秒后重试,HTTP继续保留。
每次新ONLINE或IP改变,串口打印本次当前IP。离线时旧IP不是继续可达的保证;service_registered仍可能true,表示组件内登记而不是网络在线。只读页面没有控制或账号,限定可信局域网,不做端口映射。
通过DEMO_DISABLE_MDNS可主动取消注册再重刷;等旧缓存消退后名字应失效,而当前IP页应保留。这个故障/恢复实验仍需实板执行,不能把源代码分支当已验证网络后路。
// SPDX-License-Identifier: MIT
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "mdns.h"
#include "sdkconfig.h"
#include "net.h"
#include "discovery_policy.h"
static const char *TAG="N06";
#ifdef CONFIG_DEMO_DISABLE_MDNS
static const bool disable_mdns=true;
#else
static const bool disable_mdns=false;
#endif
typedef struct {char host[64], ip[16]; bool online, registered;} snapshot_t;
static QueueHandle_t snapshots;
static const char page[]="<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>N06 discovery</title><h1>ESP32 discovery status</h1><p>This page is read-only. Registration does not prove client discovery.</p><pre id=s>Loading /status</pre><p>Fallback: use the current IP printed on the serial console.</p><script>async function tick(){try{let r=await fetch('/status',{cache:'no-store'});if(!r.ok)throw Error(r.status);document.getElementById('s').textContent=JSON.stringify(await r.json(),null,2)}catch(e){document.getElementById('s').textContent='Connection unavailable: '+e.message}}tick();setInterval(tick,2000)</script></html>";
static esp_err_t root_get(httpd_req_t *req) {httpd_resp_set_type(req,"text/html; charset=utf-8"); return httpd_resp_send(req,page,HTTPD_RESP_USE_STRLEN);}
static esp_err_t status_get(httpd_req_t *req) {
snapshot_t s={0}; if(xQueuePeek(snapshots,&s,0)!=pdTRUE) return httpd_resp_send_err(req,HTTPD_500_INTERNAL_SERVER_ERROR,"snapshot not ready");
char body[256]; int n=snprintf(body,sizeof body,"{\"configured_host\":\"%s.local\",\"ip\":\"%s\",\"online\":%s,\"service_registered\":%s,\"client_discovery\":\"not-measured\"}",s.host,s.ip,s.online?"true":"false",s.registered?"true":"false");
if(n<0 || n>=(int)sizeof body) return httpd_resp_send_err(req,HTTPD_500_INTERNAL_SERVER_ERROR,"snapshot overflow");
httpd_resp_set_type(req,"application/json"); httpd_resp_set_hdr(req,"Cache-Control","no-store"); return httpd_resp_send(req,body,n);
}
static bool start_http(httpd_handle_t *server) {
httpd_config_t cfg=HTTPD_DEFAULT_CONFIG();cfg.server_port=80;
esp_err_t err=httpd_start(server,&cfg);
if(err==ESP_OK) {httpd_uri_t root={.uri="/",.method=HTTP_GET,.handler=root_get};httpd_uri_t status={.uri="/status",.method=HTTP_GET,.handler=status_get};err=httpd_register_uri_handler(*server,&root);if(err==ESP_OK)err=httpd_register_uri_handler(*server,&status);}
if(err!=ESP_OK) {ESP_LOGE(TAG,"HTTP start=%s",esp_err_to_name(err));if(*server){httpd_stop(*server);*server=NULL;}return false;}return true;
}
static bool start_mdns(const char *host) {
esp_err_t err=mdns_init(); bool initialized=err==ESP_OK;
if(err==ESP_OK)err=mdns_hostname_set(host);
if(err==ESP_OK)err=mdns_instance_name_set(host);
mdns_txt_item_t txt[]={{"path","/"},{"board","s3-n8r8"},{"sdk","idf-5.5.5"}};
if(err==ESP_OK)err=mdns_service_add(NULL,"_http","_tcp",80,txt,3);
if(err!=ESP_OK){ESP_LOGW(TAG,"mDNS registration=%s; HTTP/IP retained",esp_err_to_name(err));if(initialized)mdns_free();return false;}
ESP_LOGI(TAG,"service_registered=true configured_host=%s.local _http._tcp:80; client discovery NOT measured",host);return true;
}
void app_main(void) {
snapshots=xQueueCreate(1,sizeof(snapshot_t));if(!snapshots){ESP_LOGE(TAG,"queue allocation failed");return;}
uint8_t mac[6]; ESP_ERROR_CHECK(esp_read_mac(mac,ESP_MAC_WIFI_STA));
snapshot_t s={.ip="0.0.0.0"}; bool name_ok=discovery_hostname(s.host,sizeof s.host,CONFIG_DEMO_HOST_PREFIX,mac);
if(!name_ok){strcpy(s.host,"invalid-config");ESP_LOGE(TAG,"invalid prefix: mDNS disabled, HTTP/IP fallback retained");}
xQueueOverwrite(snapshots,&s);net_start();httpd_handle_t server=NULL;unsigned retry=0;bool was_online=false;
for(;;){
s.online=net_online();
if(s.online){
esp_netif_ip_info_t ip;esp_netif_t *sta=esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if(sta && esp_netif_get_ip_info(sta,&ip)==ESP_OK){char current[16];snprintf(current,sizeof current,IPSTR,IP2STR(&ip.ip));if(!was_online||strcmp(s.ip,current)){strcpy(s.ip,current);ESP_LOGI(TAG,"IP fallback http://%s/ configured_name=%s.local",s.ip,s.host);}}
if(!server)start_http(&server);
// HTTP is established first and is independent of mDNS.
if(server && name_ok && !disable_mdns && !s.registered && retry==0){s.registered=start_mdns(s.host);retry=10;}
}
if(retry)--retry;
xQueueOverwrite(snapshots,&s);was_online=s.online;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
参考:[2] mdns1.9.1固定提交API:初始化、主机名、实例与服务注册[3] mdns1.9.1固定提交实现:STA事件、冲突改名与服务状态[7] ESP-IDF5.5.5 HTTP Server请求与响应接口
电脑命令行与手机浏览器分别留证
电脑与板连接同一可互访LAN。macOS先curl串口当前IP确认HTTP,再dns-sd -B浏览_http._tcp local,复制实际实例给-L解析,最后-G查询返回的实际主机地址。持续监听命令用Ctrl+C退出,不能将实例字符串直接当hostname。
手机记录OS和浏览器版本,在地址栏完整输入http://实际主机名.local/,再用http://当前IP/对照,避免被当搜索词。这只检查该手机的主机名访问和IP后路,不等于枚举DNS-SD服务,也不推断所有OS浏览器都支持.local。
两类客户端观测字段目前为空。不能把同一电脑两个终端当两类设备,也不能用本机帮助命令冒充板发布成功。
curl --max-time 5 http://REPLACE_WITH_CURRENT_IP/status
dns-sd -B _http._tcp local
dns-sd -L 'REPLACE_WITH_ACTUAL_INSTANCE_NAME' _http._tcp local
dns-sd -G v4 REPLACE_WITH_ACTUAL_HOST.local参考:[4] 固定提交mDNS指南:hostname、instance、service[6] RFC6763 §4–6:实例、浏览和解析
名字失败且IP成功,才把问题缩到发现层
先用正常配置通过IP访问,再勾DEMO_DISABLE_MDNS、重建重刷。检查新固件的registered=false,等待缓存过期后分别从电脑和手机访问当前IP;取消禁用后再浏览、解析和访问。也可故意填写带点前缀,检查报配置错但IP仍可用。
若IP也失败,先检查Wi-Fi、DHCP、同一可互访LAN、AP隔离和HTTP日志;若IP成功再检查UDP5353组播、客户端解析器、防火墙、VPN与旧缓存。.local属于本地链路,本例不宣称跨VLAN自动发现。跨网段需要管理员明确配置的发现代理和路由。
| 观测 | 先检查 | 记录要求 |
|---|---|---|
| 只有名字失败 | 注册/实际冲突名/客户端/组播/缓存 | 电脑与手机分别记录 |
| IP也失败 | 网络、当前IP、隔离、HTTP启动 | 串口ONLINE与本次IP |
| 禁用发现后IP可用 | 保留HTTP独立性 | 实板待验,不能由构建推断 |
参考:[3] mdns1.9.1固定提交实现:STA事件、冲突改名与服务状态[5] RFC6762 §3与§5:.local链路范围及组播查询[6] RFC6763 §4–6:实例、浏览和解析
同源输入检查与后续作品
host_check.c直接包含discovery_policy.h,实际通过5组检查:固定MAC后缀、9种非法前缀、63字节边界、超长与小buffer/null。它没有执行组播、HTTP、冲突仲裁或手机解析。依赖和固件摘要另有证据,实板维度继续pending。
网络模块使用RAM配置、断线退避和IP事件更新;NVS初始化问题不自动擦除。只要当前IP能访问就保留后路,再解决发现;不要把改名、换SDK或擦Flash当通用修复。后续可把这个可达性入口用于查看MQTT设备状态。
cc -std=c11 -Wall -Wextra -Werror host_check.c -o /tmp/n06-host
/tmp/n06-host参考:[2] mdns1.9.1固定提交API:初始化、主机名、实例与服务注册[3] mdns1.9.1固定提交实现:STA事件、冲突改名与服务状态[7] ESP-IDF5.5.5 HTTP Server请求与响应接口
资料来源与版本说明
按固定版本官方源码与具体板级核对;实际构建、主机输入检查和实板是分开记录的状态。没有把预期步骤写成本站已运行结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。