Час ночи, на улице минус восемь, а я сижу с ноутбуком на веранде дачи и смотрю в монитор порта: esp32 в теплице пишет одну и ту же строку — connect failed, rc=-2. История началась невинно: я перенёс брокер со старого ноутбука на Raspberry Pi, подвинул шкаф — и всё. Wi-Fi в теплице подключался, пинг до роутера шёл, а брокер молчал, как будто его там не было.
Три часа я потратил впустую, потому что библиотека не говорит, почему не вышло: rc=-2 — и всё, разбирайся сам. Разобрался к двум часам: при переносе я поставил на Pi свежий mosquitto второй версии, а он по умолчанию слушает только localhost — извне на порт 1883 не постучишься. TCP-соединение не открылось, и PubSubClient честно вернул код -2: соединение не состоялось ещё до всякого MQTT. Одна строка в конфиге брокера решила то, на что я потратил вечер.
После той ночи я завел правило: любой скетч с MQTT печатает код состояния при каждой неудаче, а не просто «не вышло». В этом уроке разберём все коды от -4 до 5 — каждый оказывается диагнозом с конкретным подозреваемым. И вторая половина урока — clientId: по моим шрамам, половина загадочных проблем подключения это не сеть, а паспорта устройств: два одинаковых имени, пробел внутри, превышенная длина.
Коды состояния: от -4 до 5
| Код | Имя в библиотеке | Что означает | Первое, что я проверяю |
|---|---|---|---|
| 0 | MQTT_CONNECTED | подключены, всё хорошо | можно публиковать и подписываться |
| -1 | MQTT_CONNECTION_TIMEOUT | TCP открылся, но ответа брокера нет | жив ли брокер, тот ли порт |
| -2 | MQTT_CONNECT_FAILED | TCP-соединение не открылось вообще | адрес, порт, firewall, слушает ли брокер извне |
| -3 | MQTT_CONNECTION_LOST | связь оборвалась посреди работы | client.loop() и окно keepalive 15 секунд |
| -4 | MQTT_DISCONNECTED | соединения нет — и не было | первый connect ещё не вызван или не удался |
| 1 | MQTT_CONNECT_BAD_PROTOCOL | брокер не понял версию протокола | устаревшая библиотека PubSubClient |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | брокеру не нравится имя клиента | пробел в имени, пустая строка, длина за пределами |
| 3 | MQTT_CONNECT_UNAVAILABLE | брокер не принимает подключений | лимит клиентов, рестарт сервера |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | неверный логин или пароль | setCredentials и содержимое файла паролей |
| 5 | MQTT_CONNECT_UNAUTHORIZED | логин верный, а доступа к серверу нет | права именно этого пользователя в брокере |
Как это читать на практике. client.connect() возвращает только да или нет, а подробности лежат в client.state() — он возвращает последний код из таблицы. Разница между -2 и -1 — это разница между «в дверь даже не постучали» и «постучали и не дождались»: минус два означает, что TCP-рукопожатие не состоялось, то есть проблема до брокера — адрес, порт, сеть, сам сервер выключен. Минус один — соединение открыли, а брокер не ответил за отведённое окно: сервер жив, но перегружен или это вообще не MQTT-порт.
Положительные коды 1–5 приходят уже от самого брокера: он получил запрос и отказал по делу. Это меняет направление поиска: с кодом 2 я лезу проверять имя клиента, с кодом 4 — пароль, с кодом 3 — логи сервера. Я не зазубривал таблицу: за месяц отладки она сама оседает в голове, потому что каждый код вы ловите руками. А пока — держите распечатку рядом, это рабочий инструмент.
Коды -3 и -4 заслуживают отдельного слова, потому что их путают между собой. Оба значат «связь пропала», но -3 вы получаете, когда соединение было и разорвалось в процессе — классика длинных пауз, когда client.loop() не успевает слать пинг и брокер отсекает молчуна по keepalive. Об этом окне в 15 секунд мы подробно говорили в уроке про публикацию с ESP32, там же — про reconnect-логику, без которой коды -3 и -4 превращаются из диагноза в приговор.
bool tryConnect() { Serial.print("Подключаюсь к брокеру... "); if (client.connect("esp32-balcony")) { Serial.println("готово, state=0"); return true; } Serial.print("не вышло, state="); Serial.println(client.state()); int st = client.state(); if (st == -2) { Serial.println("-2: TCP не открылся. Адрес верный? Порт открыт? Брокер слушает извне?"); } else if (st == -1) { Serial.println("-1: таймаут. Брокер жив, но не отвечает — другой порт?"); } else if (st == 2) { Serial.println("2: брокеру не нравится clientId"); } else if (st == 4 || st == 5) { Serial.println("4 или 5: проверь логин и пароль"); } else if (st == 3) { Serial.println("3: брокер отказал — смотри его лимиты и логи"); } return false;} // в setup(): client.setServer("192.168.1.50", 1883); tryConnect();// в reconnect(): while (!tryConnect()) { delay(3000); }Одна тонкость в работе с state(): код живёт до следующей попытки подключения. Вызвали connect второй раз — state() уже показывает результат новой попытки, старый пропал. Поэтому расшифровку я печатаю сразу после неудачи, в том же вызове, а не где-то потом в loop: потом уже поздно. Именно так устроена tryConnect() выше: одна попытка — один диагноз.
Второй приём, который экономит вечера: код состояния — это строки в моём журнале уровней. Ошибка подключения — это уровень error с числом, попытка — info. Когда через неделю смотришь лог и видишь подряд десять строк state=-2, а потом state=0, картина читается без бутылочного анализа: брокер был недоступен, потом ожил. Без кодов это было бы просто «какое-то время не работало».
И не путайте коды состояния с тем, что печатает сам Wi-Fi: проверка WiFi.status() и ожидание WL_CONNECTED — отдельный этаж, и пока он не зелёный, любые коды MQTT бессмысленны, а автореконнект сети разбирали в отдельном уроке. Порядок «сначала сеть, потом брокер» мы проходили в уроке про WiFi.begin и подключение к роутеру — там же про ожидание с точками вместо немого зависания.
clientId: паспорт устройства
Имя клиента — это не подпись в дашборде, а технический идентификатор, по которому брокер различает абонентов. Правил немного, но каждое из них я нарушал лично. Первое: имя должно быть уникальным среди всех клиентов брокера — то же правило из первого урока про устройство протокола, и в паре с ним работает второе: имя живёт в логах сервера, поэтому в нём должно угадываться место и роль. Мой формат — место-роль-номер: dacha-pump-1, home-balcony-thermo. Через полгода лог читается как журнал.
Третье правило — алфавит. Буквы, цифры, дефис — безопасно; пробел внутри имени я ставил один раз и получил отказ с кодом 2: брокер обрезал имя по пробелу и счёл его некорректным. Четвёртое — длина: старые брокеры на MQTT 3.1 держат лимит в 23 символа, современные уже не зажаты, но я всё равно режу имена до двадцати — привычка дешевле проверки совместимости. И пятое, о котором забывают: если имя генерируется из чипа — например, суффикс из последних байтов MAC-адреса — убедитесь, что оно стабильно между перезагрузками, иначе в брокере накопится зоопарк мёртвых клиентов.
У connect() есть и второй, необязательный аргумент — флаг чистой сессии. Пока скажу только, что он решает, помнит ли брокер подписки вашего клиента после разрыва; по умолчанию PubSubClient просит чистую сессию, и это станет главной ловушкой урока про subscribe и callback — там и вернёмся к нему с пруфом в виде неработающей лампы.
А теперь тупик, который я распутывал два дня. Собрал второй термометр, скопировал скетч — само собой, вместе с connect(esp32-balcony). Обе платы работали... по очереди. Каждые пару минут то одна, то другая отваливались и переподключались, графики рвались, а на беглый взгляд всё было исправно: соединение же есть! Диагноз нашёлся в логах брокера: пары connect и disconnect с одним именем шли с интервалом в секунды. Брокер честно считал их одним абонентом: новое подключение рвёт старое, и так по кругу. С тех пор clientId у меня параметр скетча, а не копипаста, а перед запуском нового устройства я поднимаю mosquitto_sub на всё и смотрю, кто как представился.
- Впишите в clientId пробел и запустите: скетч поймает это до попытки подключения. На настоящем брокере вы бы увидели отказ с кодом 2 — таблица кодов выше объясняет, почему.
- Поменяйте адрес брокера в setServer на несуществующий, например 192.168.1.200: в симуляторе подключение всё равно пройдёт, а вот на живом железе это ровно сценарий rc=-2. Прогоните этот случай на реальной плате и посмотрите, что напечатает tryConnect.
- Добавьте в tryConnect ветку для кода 5 с подсказкой про права пользователя. Проверка себя: чем код 5 отличается от кода 4, если оба про логин? Ответ: четвёртый — пароль неверный, пятый — верный, но этому пользователю сюда нельзя.
- Заведите текстовый файл с паспортами: clientId, место установки, топики, дата прошивки. Три строки на устройство; когда плат станет пять, этот файл окупится за первый же вечер отладки.
- connect() возвращает да или нет, подробности — в client.state(): код читают сразу после неудавшейся попытки
- -2 — TCP не открылся: адрес, порт, firewall, mosquitto слушает только localhost; -1 — открылся, но нет ответа
- Коды 1–5 приходят от брокера: 2 — плохой clientId, 4 — пароль, 5 — нет прав, 3 — брокер отказал целиком
- clientId уникален в масштабе брокера: формат место-роль-номер, без пробелов, до 20 символов, стабилен между перезагрузками
- Два клиента с одним именем воюют за паспорт: соединения рвутся по кругу, диагноз — в логах брокера
- 1. Что означает rc=-2 от client.connect()?
- 2. Слетел Wi-Fi, скетч переподключился, а связь с брокером рвётся каждые полминуты. Какой код вы вероятнее всего увидите в state() перед разрывом?
- 3. Mosquitto второй версии установлен заново, платы подключаются с кодом -2. Первое действие?
- 4. Два устройства подключаются с одним clientId. Что вы увидите?
- 5. Какой код вернёт брокер, если пароль верный, но этому пользователю запрещён доступ к серверу?
Частые вопросы по теме урока
- Что означает rc=-2 от client.connect()?
- MQTT_CONNECT_FAILED значит, что TCP-рукопожатие не состоялось: адрес, порт, firewall или брокер не слушает порт. Коды отказов брокера начинаются с 1. TCP-соединение не открылось: проблема до уровня MQTT
- Слетел Wi-Fi, скетч переподключился, а связь с брокером рвётся каждые полминуты. Какой код вы вероятнее всего увидите в state() перед разрывом?
- Код -3 (MQTT_CONNECTION_LOST) — связь оборвалась в процессе. Частая причина: client.loop() вызывается реже окна keepalive в 15 секунд, и брокер отрезает молчащего клиента. -3
- Mosquitto второй версии установлен заново, платы подключаются с кодом -2. Первое действие?
- Свежий mosquitto по умолчанию слушает только localhost: извне TCP не откроется, и клиент получит -2. Лечится строками listener 1883 0.0.0.0 и allow_anonymous в конфиге. Проверить конфиг брокера: слушает ли он 0.0.0.0, а не только localhost
- Два устройства подключаются с одним clientId. Что вы увидите?
- Брокер считает одинаковое имя одним абонентом: каждое новое подключение разрывает предыдущее. В логах это пары connect/disconnect с интервалом в секунды. Оба бесконечно переподключаются: новое соединение рвёт старое
- Какой код вернёт брокер, если пароль верный, но этому пользователю запрещён доступ к серверу?
- MQTT_CONNECT_UNAUTHORIZED: логин и пароль приняты, но прав нет. Код 4 — неверные сами учётные данные, код 2 — проблемы с именем клиента. 5