Котёл в подвале я завёл на контроллере с веб-мордой — той самой страницей-пультом из прошлого урока про WebServer, только кнопка там включает не свет, а циркуляционный насос. Полгода закладка http://192.168.1.47/ работала безотказно, пока гроза не выключила роутер на минуту. DHCP после перезагрузки раздал адреса заново, плата получила другой, и закладка молча умерла: выяснить это можно было только из списка клиентов роутера, а он открывался с ноутбука, но не из подвала, где я стоял с телефоном и мокрыми руками. Неделю после этого я заходил к плате по старой памяти, пока не поймал себя на мысли: я помню номер квартиры, но не помню адрес датчика в котельной — значит, дело не в памяти, а в неверном слое.
Первый ход — закрепить адрес: статический IP в скетче или DHCP-резервация на роутере решают вопрос наполовину. Но адрес всё равно остаётся числами, которые надо помнить, а устройств с веб-мордой у меня к тому моменту было семь. Второй ход радикальнее: пусть плата сама назовётся в сети — mDNS, и заходить по имени kotel.local. Как эфир отвечает на вопрос «кто здесь» — multicast на 224.0.0.251, порт 5353 — разобрано в базовом уроке про .local-имена; за фундаментом, ограничениями и отладкой эфира — туда.
Здесь прикладной этаж, которого тому уроку не хватило для котельной. Имя по роли, а не по номеру: kotel, nasos, kalitka — через год никто не помнит, что esp32-3 это дверь гаража. Сервисная реклама addService, благодаря которой плата видна в списках сети как веб-сервер. Явный announce после begin и после каждого восстановления связи. И план Б для телефонов и роутеров, которые multicast душат, — с ним я и ходил в подвал, пока не добрался до основной сети. Мой критерий простой: если в адресе есть хоть одна цифра, через полгода вы её перепутаете, а имя по роли вы вспомните даже ночью без очков.
begin, addService, announce: три вызова и их порядок
MDNS.begin("kotel") регистрирует метку — без точек и без суффикса, .local библиотека допишет сама; вернёт false, если в имени точка, строка пустая или сеть ещё не поднята. Поэтому порядок один на все проекты: WiFi.begin(), статусный цикл, MDNS.begin(), addService(), announce(). Проверка возврата begin — не украшение: false честно говорит, что имени в эфире не будет, и лучше узнать это из монитора в тот же вечер, чем из подвала месяц спустя.
MDNS.addService("http", "tcp", 80) — реклама сервиса: плата объявляет не просто «я здесь», а «у меня веб-сервер на 80-м». После этого она видна в списках сервисов у Bonjour-браузеров и в avahi-browse как kotel._http._tcp, и http://kotel.local/ открывается без указания порта. Сервисов можно объявить несколько — список растёт вызовами addService: так же я рекламирую OTA-порт, а в разделе про MQTT так объявляют брокера на 1883.
MDNS.announce() — явное объявление в эфир, и на него у меня железное правило: шлю сразу после begin и повторяю после каждого восстановления связи. Дело не в паранойе: реконнект WiFi мог сменить адрес, соседи держат старый кэш, а анонс вежливо обновляет запись. Стек шлёт анонсы и сам, но после обрыва я не жду его расписания — момент возврата связи ловит урок про авто-реконнект, оттуда же флаг, который я ставлю рядом.
#include <WiFi.h>#include <ESPmDNS.h>#include <WebServer.h> WebServer server(80);const int NASOS = 26; // циркуляционный насос через реле-модульbool nasos = false; void setup() { Serial.begin(115200); WiFi.begin("DachaNet", "superParol123"); while (WiFi.status() != WL_CONNECTED) { // сперва сеть... delay(400); Serial.print("."); } Serial.println(); Serial.print("ip="); Serial.println(WiFi.localIP()); // адрес know-by-router — печатаем для плана Б if (MDNS.begin("kotel")) { // метка без точек, .local допишут сами Serial.println("mdns: отвечаю как kotel.local"); MDNS.addService("http", "tcp", 80); // реклама: веб-сервер на 80-м MDNS.announce(); // и сразу объявление в эфир } else { Serial.println("mdns ne podnyalsya: imya krivoe ili set ne gotova"); } server.on("/", []() { server.send(200, "text/html", String("<h1>Kotel</h1><p>nasos: ") + (nasos ? "VKL" : "VYKL") + "</p>"); }); server.on("/nasos/on", []() { nasos = true; digitalWrite(NASOS, HIGH); server.send(200, "text/plain", "nasos vklyuchen"); }); server.begin();} bool bylObryv = false; // связь рвалась — надо анонсировать заново void loop() { server.handleClient(); if (WiFi.status() != WL_CONNECTED) { bylObryv = true; return; } if (bylObryv) { // связь вернулась: адрес мог смениться bylObryv = false; MDNS.announce(); Serial.println("mdns: rekonnekt — obyavilsya zanovo"); }}В скетче два места, на которые я смотрю при отладке. Первое — блок после begin: если он вернул false, дальше можно не искать, имени не будет вовсе. Второе — флаг bylObryv: без него пришлось бы звать announce() в каждом loop(), а так объявление уходит один раз на восстановление связи. Строчку с печатью WiFi.localIP() я оставляю даже в «именных» скетчах: она и есть план Б — адрес для закладки-дублёра печатается прямо в монитор при старте.
Второй инструмент из той же библиотеки — MDNS.queryService("http", "tcp"): плата сама опрашивает эфир и печатает в монитор порта табличку соседей с именами, адресами и портами. Раз в минуту в отладочном скетче — и за вечер у вас реестр всех плат в доме, собранный без списка клиентов роутера. Сам обмен вопрос-ответ в локалке занимает единицы-десятки миллисекунд; секунды, если они появляются, добавляет браузер — кэш и повторные попытки, о них ниже.
Про кэш коротко, потому что развёрнуто это в базовом уроке: устройства помнят ответы mDNS с TTL около 120 секунд, поэтому после смены адреса или имени старое может отвечать ещё пару минут из кэша ноутбука. Мой порядок при «имя вдруг не то»: перезагрузить плату, подождать три минуты, и только потом перешивать — половина таких вечеров у меня заканчивалась на первом же шаге.
Четвёртое правило — именная политика на весь дом: метка должна быть уникальной в вашей сети, потому что вторая плата с begin("kotel") начинает спорить за эфир — по имени отвечает то одна, то другая, и выглядит это как самопроизвольные глюки. Диагностика в одну команду: MDNS.queryService("http", "tcp") выведет метку с двумя разными адресами — вот они, близнецы. После того вечера я завёл список имён в блокноте роутера: kotel, nasos, kalitka, shkaf — и новое имя появляется там раньше, чем в скетче.
У того же конфликта есть вторая сторона: плата, стартовавшая с begin("kotel") позже старожила, не всегда вступает в спор. begin() вернул true, монитор чист, а по имени отвечает только первая — новичок проиграл проверку уникальности и молчит, никаких подсказок в логе. Я теперь проверяю каждого новичка сразу queryService: вместо двух адресов в таблице один — вот и молчун. Скорость эфира тут не виновата: вопрос-ответ по UDP 5353 в здоровой локалке укладывается в 10-50 мс, а секундные паузы делает кэш, который честно держит запись до конца своего TTL и за свежим ответом в сеть не ходит.
| Ситуация | Что происходит | Что делать |
|---|---|---|
| macOS, iPhone | Bonjour из коробки: имя открывается сразу | просто набрать kotel.local в браузере |
| Android + роутер с IGMP-snooping | multicast душится, имя молчит при живом WiFi | IP-закладка с DHCP-резервацией по MAC |
| Windows без Bonjour-сервиса | резолвера .local в системе нет | поставить Bonjour-сервис или идти по IP |
| гостевая сеть | изоляция клиентов режет multicast | перевести телефон в основную сеть |
| после перезагрузки роутера | DHCP выдал плате новый адрес | резервация + announce после реконнекта |
Когда имя — не серебряная пуля
Граница mDNS — граница сети: multicast-вопросы роутер наружу не выпускает, поэтому kotel.local не откроется ни с работы, ни через обычный VPN-туннель. Для загородного дома это неважно — вы и так ходите к плате по WiFi, — но если пульт нужен из офиса, я либо открываю страницу через туннель с маршрутизацией мультикаста, либо честно иду по IP с пробросом порта и ключом в пути. Имя обслуживает локалку и отлично это осознаёт.
Второе ограничение — имя не заменяет безопасность: метку слышат все соседи по вашей WiFi-сети, и найдёт kotel.local любой, кто откроет список Bonjour-сервисов. Для ночника и термометра я живу без пароля, для насоса с реле уже ставлю проверку ключа в обработчике — та же пара строк, что в уроке про WebServer. Слои стоят дёшево, когда их продумываешь заранее, и стоят дорого, когда добавляешь после первого чужого визита.
Третье — резервация не отменяет имя, а дополняет его: скрипты Node-RED и повторители ходят по IP, люди — по имени. У меня в проекте записаны оба адреса: имя в URL-закладке для меня, IP из DHCP-резервации для автоматизаций. Когда сосед спросил, зачем плате и имя, и адрес, я ответил как водителю навигатора: имя — чтобы найти, адрес — чтобы доехать, и оба они должны не меняться. В тот же вечер в подвале сработали оба слоя: телефон перекинулся из гостевой сети в основную, kotel.local открылся, а IP-закладка пригодилась, когда я проверял диагноз по резервации.
Симулятор: анонсы по расписанию
Сам эфир mDNS движок не эмулирует, и строку «кто здесь kotel.local?» на живой плате печатает не скетч, а стек. Модель поэтому честно разделяет роли: WiFi в симе настоящий — с подключением, статусным циклом и адресом, а анонсы шлёт millis-планировщик каждые 15 секунд, ровно та периодика, с которой стек напоминает о себе после begin. Кнопка D2 изображает запрос из сети: нажали — плата отвечает именем, адресом и портом. LED вспыхивает на каждом анонсе — на живой плате светодиод не мигает, но в отладке я вешаю точно такую же вспышку на Serial-событие, чтобы видеть пульс платы из другого конца комнаты.
Попробуйте так: нажмите кнопку — в журнале пройдёт пара «запрос — ответ» с адресом, который выдал DHCP; подождите 15 секунд — придёт очередной announce с именем, сервисом и портом. На живой плате те же события идут без вашего участия: проверка — открыть http://kotel.local/ с телефона или запустить avahi-browse на ноутбуке и увидеть kotel._http._tcp в списке.
- 1. Какое имя ждёт MDNS.begin()?
- 2. За появление платы в списках сервисов сети отвечает:
- 3. MDNS.announce() я шлю повторно после:
- 4. Android не открывает kotel.local, но по IP страница есть. Чаще всего:
- 5. Чтобы IP-фоллбэк не менялся после перезагрузок роутера, я:
- Имя по роли: kotel.local, nasos.local — через год не понадобится табличка «что это за esp32-3».
- Порядок вызовов: WiFi → статусный цикл → MDNS.begin(метка без точек) → addService("http","tcp",80) → MDNS.announce().
- announce шлю после begin и после каждого реконнекта: адрес мог смениться, кэш соседей стоит обновить.
- queryService("http","tcp") печатает реестр плат прямо в монитор — карта дома за вечер и без роутера.
- Android и репитеры душат multicast: IP-закладка с DHCP-резервацией — обязательный план Б рядом с именной.
Частые вопросы по теме урока
- Какое имя ждёт MDNS.begin()?
- Библиотека ждёт голую метку: суффикс .local допишет сама, точки и порт в имени дают false. Суффикс .local зарезервирован под mDNS и не маршрутизируется в интернете. kotel — метка без точек
- За появление платы в списках сервисов сети отвечает:
- addService рекламирует сервис — веб-сервер на 80-м, брокера на 1883 и так далее. Без него плата отвечает на прямой вопрос по имени, но в списках сети не видна. MDNS.addService("http", "tcp", 80)
- MDNS.announce() я шлю повторно после:
- Реконнект мог сменить адрес, а соседи держат старый кэш — анонс обновляет запись. Остальные варианты либо не относятся к mDNS, либо разорили бы эфир. восстановления связи WiFi
- Android не открывает kotel.local, но по IP страница есть. Чаще всего:
- При живом WiFi и работающем IP виной обычно IGMP-snooping на роутере, репитер или гостевая сеть: multicast-вопросы не доходят. План Б — IP-закладка с DHCP-резервацией. multicast душится роутером или репитером
- Чтобы IP-фоллбэк не менялся после перезагрузок роутера, я:
- Адрес закрепляет статический IP в скетче или DHCP-резервация на роутере по MAC платы. Второе имя и смена порта не влияют на выдачу DHCP вообще. ставлю статический адрес в скетче или резервацию DHCP по MAC