Перейти к содержимому

Телеметрия на ESP8266 + MQTT. Пошаговое руководство по созданию DIY-проекта с удаленным управлением

Добрый день, уважаемый читатель!

Данная статья ориентирована в первую очередь на начинающих программистов, которые только начинают свой творческий путь программирования микроконтроллеров, в данном случае – ESP8266.

Повторив этот проект вы можете создать с помощью микроконтроллера ESP8266, WiFi, MQTT и некоторой доли творчества:

  • Дистанционный контроль и управление вашими устройствам, причем не только в локальной сетке, но и из любой точки планеты, где есть интернет
  • Удаленное управление реле и нагрузкой, например включение и отключение освещения или котла
  • Обратный контроль состояния нагрузки с устройства – получило ли оно вашу команду или нет
  • Удаленный мониторинг логических состояний на входах микроконтроллера, что позволяет создать простенькую охранную систему например
  • Удаленный мониторинг температуры и влажности в помещении, на улице, котла и т.д.
  • Оперативные уведомления о различных событиях в Telegram *
  • Отправка данных на сторонние сервисы (Thing Speak, Народный мониторинг, Open Monitoring) для отслеживания изменений во времени и построения графиков *

При этом я не ставлю Вам “жестких” рамок, у Вас всегда остается свобода для собственного творчества. При творческом подходе не сложно прикрутить управление нагрузкой по автоматическим сценариям, например включение котла по температуре. И все это с дистанционным управлением.

Примечания: функции, отмеченные * и курсивом обсудим в следующих статьях цикла про ESP8266 и Arduino-framework

 

В данной статье я подробно и по шагам расскажу как:

  1. Создать Arduino – проект для ESP8266 в более продвинутой и удобной среде разработки Visual Studio Code + PlatformIO.
  2. Подключить ESP8266 к вашей сети WiFi.
  3. Подключить необходимые библиотеки к проекту.
  4. Получить точное текущее время с NTP-сервера в интернете.
  5. Подключиться к MQTT-брокеру, в том числе и по TLS/SSL-протоколу
  6. Создать устройство для дистанционного управления реле через MQTT
  7. Опубликовать состояние цифровых входов (например нажатие на кнопку) на MQTT-брокере
  8. Прочитать и опубликовать температуру с датчика температуры и влажности, например DHT22

 

Вам понадобятся:

В конце статьи, как обычно, есть ссылка на готовый проект на GitHub (вам останется только изменить данные для подключения на свои), так что если Вам лень читать – листайте в конец статьи. Осторожно – многа букав…


Подготовка среды разработки

Если вы заглянете в демо-проекты kotyara12/arduino, то, наверное, заметите, что в архиве нет никаких файлов .ino, а есть main.cpp и platformio.ini. Это означает, что проекты собраны не в классической Arduino IDE, а в Visual Studio Code + PlatformIO.

<sarcazm>Конечно, вы можете легко перенести код из main.cpp в классическую Arduino IDE. Но я не буду рассказывать, как это сделать, вот такой уж я вредный. Вместо этого я расскажу, как сделать все в ненавистной для многих IDE Visual Studio Code и плагине к ней PlatformIO. А перенести в Arduino IDE вы и без меня смогёте.</sarcazm>

Ну а если серьёзно – то инструкций как создать проект в Arduino IDE в интернете – пруд пруди. Не вижу смысла делать еще одну.

Итак, для начала нам понадобится собственно статья разработки. Как установить эту связку я уже рассказывал на канале и на сайте, повторяться не стоит. Стоит только отметить, что на этапе 8. Устанавливаем платформы… нужно поставить Espressif 8266:

Для этого:

  1. Нажмите “домик” PIO Home на нижней панели VS Code или значок PlatformIO на левой боковой панели, а затем выберите в списке Open.
  2. Перейдите на вкладку “Platforms
  3. В строке поиска введите 8266 и нажмите Enter
  4. Поиск выдаст единственный результат – Espressif 8266, нажмите кнопку Install (у меня это уже Uninstall).
  5. Подождите пару минут…

Создаем проект

Вновь возвращаемся на страницу PIO Home (“домик” PIO Home на нижней панели VS Code или значок PlatformIO на левой боковой панели, а затем выберите в списке Open). Но на этот раз нажмите New Project:

Выбираем название проекта (Name), плату (Board) и Framework Arduino, и нажимаем Finish:

Затем придется немного подождать, пока PlatformIO создаст необходимые файлы и папки… Иногда по непонятным причинам этот процесс наглухо зависает, и его приходится запускать заново.


 

Каталоги проекта

PlatformIO создаст в выбранном каталоге несколько подкаталогов (подпапок)

  • .pio\ – папка для “служебного пользования” PlatformIO, здесь будут создаваться файлы, здесь можно будет найти скомпилированный бинарник для загрузки в устройство “вручную”.
  • .vscode\ – ещё одна папка для “служебного пользования” PlatformIO, здесь хранятся настройки VSCode. Здесь есть что можно поправить, в некоторых случаях.
  • include\ – этот каталог предназначен для заголовочных файлов проекта. Надеюсь, вы знаете, что это такое ?.
  • lib\ – сюда можно поместить локальные библиотеки проекта. “Локальные” в данном случае я понимаю как использующиеся только в данном проекте, но не в других. Сюда можно поместить части проекта, отвечающие за ту или иную функциональность проекта. Я, например, помещаю сюда файлы “прикладных” задач проекта, то есть конкретно ту автоматизацию, которую и будет выполнять данное устройство. Если у Вас есть библиотеки, которые используются сразу в нескольких ваших проектах, их лучше поместить в каталог “вне” каталога проекта для совместного использования. Я знаю, есть любители кидать в каждый проектов свои “локальные” копии общих библиотек, но я категорически не поддерживаю подобную практику, так как это приводит к огромным проблемам при дальнейшем обновлении кода.
  • src\ – здесь, собственно, и находится исходники вашего проекта. У меня, обычно, здесь только файл main.c,которые только запускает все нужные задачи и сервисы. Но ничто не мешает накидать сюда прикладных файлов, игнорируя каталог lib (см. выше), но тогда заголовочные файлы прикладных задач вам придется положить в папку include.
  • test\ – здесь будут ваши автоматизированные тесты вашего же кода, когда вы их создадите.

Кроме этого, в каталоге проекта появится и несколько файлов:

  • .gitignore – файл, в котором вы можете указать, какие из файлов не нужно загружать в GitHub.
  • platformio.ini – файл настроек проекта PlatformIO. Именно по этому файлу VSCode “понимает”, что это проект PlatformIO, а не какой-либо ещё. Этот файл будет открыт по умолчанию, давайте его сразу и рассмотрим.

Настройка проекта

platformio.ini – основной файл параметров проекта на PlatformIO в стандартном формате, придуманном ещё во времена Windows 3.1. В нем хранятся все данные о проекте – платы, порты, скорости, фильтры, скрипты и много чего ещё. Таким образом, открывая очередной проект, вам не потребуется заново выбирать плату и её параметры, как это делается в Arduino IDE.

В новом проекте он выглядит просто и незамысловато:

Совет: Если нажать на ссылку https://docs.platformio.org/page/projectconf.html в комментариях к файлу, то вы попадете в справочную систему PlatformIO, где вы сможете узнать обо всех доступных опциях. Здесь я упомяну только некоторые из них, которыми я чаще всего пользуюсь.

Единственная пока секция [env:nodemcuv2] задает параметры компиляции проекта:

  • platform = espressif8266 указывает, что для компиляции проекта будем использовать платформу espressif8266
  • board = nodemcuv2 определяет, что проект будет собран для платы Node MCU v2 (ESP-12F)
  • framework = arduino ну и собственно здесь мы должны указать, какой фреймворк будем использовать

Файл platformio.ini позволяет указать сразу несколько таких секций. Например, вы можете собирать одновременно один и тот же проект сразу для нескольких разных плат одновременно. Примеры смотрите в справочной системе https://docs.platformio.org/page/projectconf.html

Кроме секции [env:nodemcuv2] вы можете создать отдельную секцию [env] (то есть без указания целевой платформы) – в этом случае параметры этой секции будут считаться “общими” для всех указанных целей.

Скорость COM-порта

Что здесь стоит сразу же добавить? Во первых, нужно указать скорость обмена данными через USB / COM-порт в двух режимах: режиме монитора и режиме загрузки прошивки в плату. Номер COM-порта можно не указывать – если вы используете только одно подключение (одну плату), то автовыбор порта делает свою работу замечательно.

По умолчанию для большинства проектов Arduino используется скорость 9600 бод, поэтому в опции monitor_speed ставим цифру 9600. А вот для загрузки прошивки upload_speed можно выбрать скорость и повыше (чтобы снизить время прошивки); ESP8266 прошиваются нормально на скорости 921600 бод.

Фильтры COM-монитора.

Следующее, что можно поправить – настроить фильтры для монитора COM-порта. Доступны такие фильтры (подробнее смотри здесь):

  • default – удалить типичные коды управления терминалом из ввода
  • colorize – применение разных цветов для разного типа сообщений (хм, на самом деле это не работает)
  • debug – выводить и отправленное и полученное
  • direct – пересылать все данные без обработок, нужен для вывода цветных отладочных логов (ошибки – красным, предупреждения – желтым и т.д.)
  • hexlify – печать данных в шестнадцатеричном представлении для каждого символа
  • log2file – записывать данные в файл «platformio-device-monitor-%date%.log», расположенный в текущем рабочем каталоге
  • nocontrol – удалить все контрольные коды, в т.ч. CR+LF
  • printable – показать десятичный код для всех символов, отличных от ASCII, и заменить большинство управляющих кодов
  • time – добавить временную метку с миллисекундами для каждой новой строки (но это будет временная метка компьютера, а не самого устройства)
  • send_on_enter – отправить текст на устройство на ENTER
  • esp8266_exception_decoder – декодер исключений Espressif 8266, удобно, но уж очень медленно, я не использую
  • esp32_exception_decoder – декодер исключений то же самое, но для Espressif 32

Желаемые фильтры можно перечислить либо через запятую, либо в каждой строке, как вам удобнее (мне – в каждой строке, так как в таким случае их очень удобно “комментарить”). Сделать это можно в секции [env:nodemcuv2] для конкретного устройства, либо в секции [env] сразу для всех устройств, сколько бы секций вы не создали.

Я пользуюсь в основном direct и log2file – иногда очень записывать протоколы вывода в файл для последующего анализа.

На этом пока оставим этот файл, но вкладку редактора закрывать ещё рано, он нам ещё понадобится. Не забудьте сохранить изменения с помощью Ctrl+S.


Редактор кода

Ок, теперь можно начинать программировать. Для начала создадим подключение к точке доступа WiFi. Откройте главный файл проекта – main.cpp, он находится в папке src. Как видите, он ничем не отличается от типичного шаблона скетча в Arduino IDE:

Первое, что потребуется сделать – настроить COM-порт для вывода отладочных сообщений:

/**************************************************************************
 * Основные функции
 *************************************************************************/

void setup() {
  // Настройка COM-порта
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");
};

voild loop() {
};

Цифры в Serial.begin() должны соответствовать тому, что вы указали ранее в platformio.ini. Пустой Serial.println(); нужен просто для того, чтобы перевести указатель на новую строку, так как при старте MCU в порт часто попадает мусор.


 

Подключение к WiFi точке доступа

Перед созданием кода, который будет выполнять подключение к точке доступа, необходимо выполнить некоторые подготовительные операции:

  • Добавьте #include <ESP8266WiFi.h> в начало файла, чтобы подключить стандартную библиотеку ESP8266WiFi к проекту.
  • Добавьте пару строк const char*, в которых укажите имя вашей сети (SSID) и пароль подключения к ней (да, да, в открытом виде, но вы же собираетесь публиковать это где попало)
#include <Arduino.h>
#include <ESP8266WiFi.h>

/**************************************************************************
 * Настройки проекта
 *************************************************************************/

// Параметры подключения к WiFi
const char* wifiSSID  = "k12iot";
const char* wifiPASS  = "*********";

У вас должно получиться примерно так:

Я настоятельно рекомендую вам расставлять комментарии где это только возможно. Это сильно облегчит вам жизнь, когда вам срочно понадобиться доработать проект спустя пару лет его эксплуатации

Способы подключения к WiFi

Я могу предложить несколько вариантов логики подключения к WiFi. Разумеется, предложенные мной в статье способы не исчерпывают всех возможных вариантов, вы можете найти другие или придумать свои. Рассматривайте это просто как измышления на тему…

 

1. Можно запихнуть весь код подключения к WiFi в функцию setup(). Так часто делается в примерах для esp8266:

void setup() {
  // Настройка COM-порта
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");

  // Настраиваем WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(wifiSSID, wifiPASS);

  // Ждем, пока подключение не будет установлено
  Serial.print("Connecting to WiFi AP ");
  Serial.print(wifiSSID);
  Serial.print(" ");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  };

  // Подключение успешно установлено
  Serial.println(" ок");
  Serial.print("WiFi connected, obtained IP address: ");
  Serial.println(WiFi.localIP());
}

На первый взгляд, всё это очень просто и хорошо.

Можно заподозрить, что подключение к WiFi осуществляется только один раз. То есть при любом отключении или перезагрузке роутера устройство не сможет вновь подключиться к сети. На самом деле это не так. Дело в том, что класс WiFi (с которым мы работаем) имеет “свойство” AutoReconnect (на самом деле физически его нет, но так проще для понимания) и оно по умолчанию включено. Управлять этим “свойством” можно с помощью методов getAutoReconnect() и setAutoReconnect(bool). То есть наше устройство должно автоматически попытаться восстановить подключение при его потере.

Но как мы узнаем о том, что подключение было потеряно и вновь установлено? Ведь после переподключения нам потребуется вновь подключиться к MQTT брокеру и т.д. и т.п. Для того, что бы отловить изменения состояния WiFi подключения можно предложить как минимум два способа:

  • правильный способ – создать и зарегистрировать функции обратного вызова (callbacks) для событий onStationModeDisconnected (соединение потеряно) и onStationModeGotIP (получен IP-адрес, это финальная стадия подключения). Но функции обратного вызова вызывают затруднение у новичков, поэтому мы пока не будем рассматривать этот способ (если будет необходимо – пишите в комментариях)
  • простой способ – просто проверять состояние в каждой итерации главного цикла, чтобы предпринять необходимые действия. Например так:
void loop() {
  // Проверяем подключение к WiFi
  if (wifiConnected()) {
    // Соединение с интернетом есть, можно работать
  } else {
    // Соединения с интернетом нет, нужно что-то предпринимать
  };
  delay(1000);
};

И это действительно работает, хотя и не очень надёжно. Почему-то этот алгоритм “пропускает” некоторые отключения и восстановления от WiFi (и дело тут не в большой задержке). Зато это очень просто.

Но если вдуматься – такой подход имеет очень серьезный изъян. Если на момент запуска подключение невозможно установить по любой причине, то устройство никогда не выйдет на главный цикл и “зависнет” на ожидании подключения. И даже принудительный выход из цикла ожидания ничего толкового не дает – ведь подключение так и не было установлено. Лично для меня это категорически неприемлемо. И я никому не посоветую так делать. Так что же делать?

 

2. Перенесем подключение к сети в цикл loop(). Для этого просто каждый раз проверяем состояние подключения, и если оно отсутствует, “вручную” попытаемся восстановить его.

void setup() {
  // Настройка COM-порта
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");
}

void loop() {
  // Проверяем подключение к WiFi
  if (WiFi.status() == WL_CONNECTED) {
    // Подключение к WiFi установлено, здесь можно выполнить цикл MQTT-клиента
  } else {
    // Настраиваем WiFi 
    WiFi.mode(WIFI_STA);
    WiFi.begin(wifiSSID, wifiPASS); 

    // Ждем, пока подключение не будет установлено 
    Serial.print("Connecting to WiFi AP "); 
    Serial.print(wifiSSID);
    Serial.print(" ");
    while (WiFi.status() != WL_CONNECTED) { 
      Serial.print("."); 
      delay(500); 
    }; 
    
    // Подключение успешно установлено 
    Serial.println(" ок");
    Serial.print("WiFi connected, obtained IP address: "); 
    Serial.println(WiFi.localIP());   
  };

  // Что-то делаем ещё... 
  // Например измеряем температуру и управляем котлом отопления
}

В этом случае в каждом цикле loop() мы будем точно знать, есть ли подключение к сети, и, в случае необходимости, сможем легко предпринять дополнительные действия без callback-ов, например заново переподключиться к MQTT-брокеру.

Уже гораздо лучше. Но здесь есть большие грабли с очень крепкой дубовой ручкой, которой обязательно прилетит вам в лоб в самый неожиданный момент. Найдете сами? Ладно, даю подсказку:

Догадались? Нет? Дело в том, что если нет подключения к WiFi, ваша программа никогда не выйдет из цикла, обведенного красным. А, следовательно, программа не выполнит что-то ещё, пока нет подключения к сети. Другими словами, температура не будет измерена, реле не будет выключено и т.д. Допустим в процессе работы вы включили насос, а затем… сгорел роутер, досадно, обидно, но… насос останется работать, пока вы не выключите устройство вместе с насосом.

Это очень опасный момент, когда вы доверяете своему устройству управление потенциально опасными устройствами – котлом, поливом и т.д.

Что же делать, как же быть? Есть два пути решения этой проблемы.

 

3. Принудительно ограничить время работы цикла ожидания подключения.

Всё тоже самое, но цикл ожидания немного изменим.

// Настраиваем WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(wifiSSID, wifiPASS);

// И ждем подключения 60 циклов по 0,5 сек - это 30 секунд
int i = 0;
while (WiFi.status() != WL_CONNECTED) {
  i++;
  if (i > 60) {
    // Если в течение этого времени не удалось подключиться - выходим с false
    // Бесконечно ждать подключения опасно - если подключение было разорвано во время работы
    // нужно всё равно "обслуживать" реле и датчики, иначе может случиться беда
    Serial.println("");
    Serial.println("Connection failed!");
    return false;
  };
  Serial.print(".");
  delay(500);
};

// Подключение успешно установлено
Serial.println(" ок");
Serial.print("WiFi connected, obtained IP address: ");
Serial.println(WiFi.localIP());
return true;

Вуаля, теперь ожидание не будет превышать 30 секунд. А за 30 секунд, надеюсь, ничего страшного не произойдет. Хотя это и “костыль”, но задача решена.

 

4. Переделать всю работу с WiFi на систему по событиям. Самый “правильный” вариант. В этом случае вам потребуется создать кучку функций обратного вызова (callbacks), которые будут асинхронно вызываться в нужные вам моменты времени: при установке соединения и при потере оного. И вы сможете сделать всё, что требуется в нужный момент времени без каких-либо циклов ожидания. В своих проектах я использую эту технику. Но в виду некоторой сложности данного подхода для начинающих (а эта графомания всё-таки для начинающих ардуинщиков), пока не будем рассматривать этот вариант. А если у вас есть некоторый опыт, вы сможете соорудить такое и без моей помощи.

 

Функция проверки подключения к WiFi

Теперь уже таки можно написать достаточно простую функцию подключения к AP. Лично я избегаю включать “чистый” код в setup() и loop(), а выношу все специфичные операции в отдельные функции, так зачастую удобнее. Например так может выглядеть функция, которая проверяет и восстанавливает подключение:

// Подключение к WiFi точке доступа
bool wifiConnected()
{
  // Если подключение активно, то просто выходим и возвращаем true
  if (WiFi.status() != WL_CONNECTED) {
    // ... иначе пробуем подключиться к сети
    Serial.print("Connecting to WiFi AP ");
    Serial.print(wifiSSID);
    Serial.print(" ");
    
    // Настраиваем WiFi
    WiFi.mode(WIFI_STA);
    WiFi.begin(wifiSSID, wifiPASS);

    // И ждем подключения 60 циклов по 0,5 сек - это 30 секунд
    int i = 0;
    while (WiFi.status() != WL_CONNECTED) {
      i++;
      if (i > 60) {
        // Если в течение этого времени не удалось подключиться - выходим с false
        // Бесконечно ждать подключения опасно - если подключение было разорвано во время работы
        // нужно всё равно "обслуживать" реле и датчики, иначе может случиться беда
        Serial.println("");
        Serial.println("Connection failed!");
        return false;
      };
      Serial.print(".");
      delay(500);
    };

    // Подключение успешно установлено
    Serial.println(" ок");
    Serial.print("WiFi connected, obtained IP address: ");
    Serial.println(WiFi.localIP());
  };
  return true;
}

Тогда основные функции setup() и loop() будут выглядеть так:

void setup() {
  // Настройка COM-порта
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");
}

void loop() {
  // Проверяем подключение к WiFi
  if (wifiConnected()) {
    // Здесь мы будем что-то делать, если подключение к сети установлено
  };

  // Что-то делаем ещё... 
  // Например измеряем температуру и управляем котлом отопления 
}

Компилируем build, загружаем в плату upload. Плата должна корректно отрабатывать все отключения от WiFi сети.

В первый раз подключение успешно установлено за ~4сек, затем была перезагрузка роутера, вначале за 30 секунд установить соединение не удалось, был выход из цикла ожидания, затем удалось

 


 

MQTT протокол

Для начала давайте чуть-чуть об теории MQTT протокола. MQTT — это протокол обмена сообщениями по шаблону издатель-подписчик (pub/sub). Первоначальную версию в 1999 году опубликовали Энди Стэнфорд-Кларк из IBM и Арлен Ниппер из Cirrus Link. Они рассматривали MQTT как способ поддержания связи между машинами в сетях с ограниченной пропускной способностью или непредсказуемой связью.

Система связи, построенная на MQTT, состоит из клиентов-издателей publisher, сервера-брокера broker и одного или нескольких клиентов-подписчиков subsriber. В обычных условиях клиенты не могут общаться напрямую друг с другом, и весь обмен данными происходит только через какого-либо брокера. Издатель и подписчик ничего не знают друг о друге, но должны знать о том, по какому адресу / каналу topic передавать или ждать данные. Одно и то же устройство может быть одновременно и издателем и подписчиком (но на разные топики). Это сильно облегчает задачу передачи данных из-за NAT-а (то есть из обычных локальных сетей).

При этом, асинхронность протокола MQTT предусматривает, что издатель и подписчик могут быть онлайн в разное время, терять пакеты, и быть недоступны. Брокер в случае необходимости позаботится о том, чтобы сохранить в памяти последние данные, полученные от издателя, и обеспечить их доставку заинтересованным подписчикам.

Кратенько рассмотрим термины, которые нам просто необходимо знать для работы с MQTT протоколом.

Broker

Брокер — это центральный узел MQTT, обеспечивающий взаимодействие клиентов. Обмен данными между клиентами происходит только через брокера. В его задачи входит получение данных от клиентов-издателей, обработка и сохранение, доставка полученных данных подписчикам, а так же контроль за доставкой сообщений в случае необходимости. Брокеров может быть несколько, в том числе связанных между собой так называемыми “мостами”.

Message

Сообщения (message) содержат в себе информацию, которую один участник сети на базе протокола MQTT (издатель) хочет передать другим (своим подписчикам). В качестве сообщения может быть всё, что угодно – текст, значения температуры, состояние датчиков и т.д. и т.п. И даже двоичные данные.

Publish

Это процесс передачи сообщения брокеру. То есть простым языком, я подошел к брокеру и сказал “кнопка нажата“. Брокер теоретически должен услышать это сообщение, записать (при необходимости) и передать дальше подписчикам. Почему теоретически? Потому что есть особенности протокола (QoS), которые мы разберем чуть ниже. Пока берем за данность, что я сказал что-то брокеру используя механизм publish и он это услышал и обработал как положено.

Subscribe

Сообщение было отправлено брокеру, но как его получить клиенту? Правильно – подписаться. Тогда сервер передаст вам все поступающие сообщения на интересующую вас тему. Чтобы определить на какую тему мы хотим получать эти сообщения, используется механизм Topic

Topic

Топик (его ещё иногда называют темой или каналом) – это своего рода адрес для передачи тех или иных данных. Для того, чтобы клиент-подписчик смог получить опубликованные данные, он должен заранее знать, в каком топике (по сути по какому адресу) их будет отправлять издатель. По аналогии с обычными газетами и почтой:

Издатель печатает несколько газет (топиков) – “Аргументы и факты“, “Ведомости” и “Гудок“. Подписчик может подписаться на один из указанных топиков или несколько. А может подписаться на журнал “Новости адруинщика” (почему нет?). Почтальон (брокер) принесет подписчику только те газеты (сообщения), которые напечатал (отправил) издатель, и на которые подписался подписчик. То есть вы должны заранее знать, на что подписываться.

Топик может быть простым (например temperature) и состоящим из нескольких отдельных частей, разделенных слешем “/” (например home/kitchen/temperature). Для чего нужны эти “/“? А для того, дабы выстроить определенную иерархию данных. По аналогии с приведенным выше примером с почтой это могут быть топики “Газеты/Аргументы и факты” и “Журналы/Мурзилка“. Чем то это очень похоже на структуру каталогов и файлов на дисках вашего контупера.

Во-первых строках письма это даёт более простое понимание множества данных:

Но самое главное – это сильно облегчает подписку на данные путем использования подстановочных знаков wildcards. MQTT протокол предусматривает два типа wildcards при подписках на топики:

  • # (твой дом тюрьма решетка) позволяет элегантно подписаться на все субтопики одном махом
  • + (плюсик) позволяет подписаться на все топики одного уровня

Ну например: у нас есть в кухне и в спальне по два выключателя и один в гараже. Для этого мы формируем на выключателях в топики в виде:

В гостиной:

home/kitchen/switch1
home/kitchen/switch2

В спальне:

home/bedroom/switch1
home/bedroom/switch2

В гараже:

garage/switch1

Допустим мы хотим управлять всеми выключателями в доме, но нам абсолютно не интересен гараж. В обычном режиме нам пришлось бы подписаться на все топики отдельно:

mqttClient.subscribe("home/kitchen/switch1");
mqttClient.subscribe("home/kitchen/switch2");
mqttClient.subscribe("home/bedroom/switch1");
mqttClient.subscribe("home/bedroom/switch2");

Геморой? Не то слово! Гораздо проще и надежнее сделать это так:

mqttClient.subscribe("home/#");

Бонусом мы автоматически получаем подписку на все еще заранее неизвестные топики в доме (впрочем иногда это лишнее).

Хорошо, а если нас интересует только выключатель 1, но не важно в какой комнате. Тогда напишем так:

mqttClient.subscribe("home/+/switch1");

Но чтобы пользоваться этими возможностями, нужно заранее продумывать структуру топиков для своих устройств, а не сочинять их с бухты-барахты.

<вредный совет>Таки если вы желаете сделать себе нервы и выесть моск, создавайте топики максимально простыми и случайным образом. Это очень поможет вам в дальнейшем. Тормоза придумали трусы, а иерархию – ботаники.</вредный совет>

Служебные сообщения

В принципе есть несколько типов служебных сообщений MQTT протокола:

  • Birth Message – которое сообщает миру что “я родился и живой”
  • Last Will and Testament (LWT) которое сообщает что после этого сообщения считать меня мертвым
  • Keep Alive – которые сообщают брокеру что “я все еще живой” и стандартно посылаются каждые 60 секунд. Если брокер не получил это сообщение от клиента, то он принудительно пингует его чтобы выяснить жив ли тот, и если выясняется что он неживой, то брокер публикует за клиента LWT сообщение, чтобы все узнали что тот скончался.

Соответственно получение брокером Birth Message от устройства, переводит устройство в режим ONLINE, а после того как брокер получает от устройства LWT сообщение, либо когда сам принимает решение что тот скончался (проверив устройство на доступность), то переводит статус устройства в режим OFFLINE.

На практике нам понадобится только LWT, все остальные служебные сообщения отправляются библиотекой самостоятельно и без нашего ведома. Как создать LWT и как допилить его под удобную индикацию статуса устройства будет рассказано чуть ниже.

Retain или Retained

Протокол MQTT никоим образом не предназначен для накопления данных на сервере с последующим отображением в виде графиков. Максимум что может храниться на сервере – последнее опубликованное издателем сообщение, если оно было отправлено с флагом retain или retained (в разных версиях может называться по разному, но суть одна и та же). Это позволяет клиенту-подписчику при подключении к брокеру сразу же получить последние интересующие его данные, а не ждать когда их отправит издатель в очередной раз. Однако использование этого флага требует известной аккуратности, неверное его использование может привести к невразумительным проблемам.

QoS

QoS расшифровывается как Quality Of Service, то есть качество предоставляемой услуги. Для MQTT этот показатель отвечает за вероятность прохождения пакета между двумя точками сети. Флаг QoS может принимать следующие значения, основанные на надежности передачи сообщения:

  • 0 = не более одного раза: отправитель пересылает сообщение получателю и забывает об этом. Сообщения могут быть потеряны в канале связи или продублированы (если отправитель посчитал что пакет потерян и отправил его повторно, но первый пакет таки дошёл до получателя).
  • 1 = по крайней мере один раз: получатель подтверждает доставку сообщения. Если подтверждение не было получено, отправитель должен отправить его еще раз и так до получения подтверждения о получении. Сообщения могут дублироваться, но доставка гарантирована
  • 2 = ровно один раз: сервер обеспечивает гарантированную доставку. Сообщения поступают точно один раз без потери или дублирования. Самый медленный и машинотрудозатратный вариант, так как отправитель и получатель дополнительно обмениваются подтверждениями.

На этом с теорией закончим, переходим непосредственно к программированию. Но для этого вначале необходимо подключить библиотеку MQTT клиента к нашему проекту.


 

Подключаем сторонние библиотеки

Для работы с MQTT протоколом нам понадобится сторонняя библиотека, например PubSubClient. Существует две популярные версии данной библиотеки (хотя может быть и больше):

Чем они отличаются? В основной версии значение QoS можно указать только при подписке. А при публикации – нет! Я считаю, что это не есть хорошо (к слову, разработчики MQTT клиента для ESP-IDF считают так же, поскольку в ESP32 реализован “по второму типу”).

Лично я предпочитаю пользоваться версией от Imroy. Но в данном примере я буду использовать основную ветку. Просто потому что во 99.9999999% примеров в сети используется именно она.

Если вы работали с Arduino IDE, то, наверное, знаете, что там библиотеки можно достаточно легко и просто скачать и установить с помощью встроенного менеджера библиотек. В PlatformIO всё на первый взгляд сложнее, но на самом деле всё не так страшно, да и возможностей гораздо больше. Я настоятельно рекомендую ознакомиться со статьей Подключение библиотек к проекту PlatformIO, где это описано подробнее, но для начала достаточно усвоить самые простые способы. Итак, если очень кратенько, существует несколько способов подключить сторонние библиотеки:

  • Локальные библиотеки проекта. Для этого потребуется скачать архив библиотеки с GitHub и распаковать его содержимое в каталог \lib вашего проекта. В этом случае PIO автоматически “подхватит” эти библиотеки и ничего нигде настраивать не нужно. Но я настоятельно не рекомендую так делать. Почему? Потому что вам придется хранить отдельную копию библиотек для каждого из своих проектов. Это занимает дополнительное место на дисках, но самое главное – если необходимо обновить библиотеки, вам придется сделать это в каждом из своих проектах. Ручками. Как всегда – за свою лень в начале мы заплатим многократным трудом впоследствии, чудес не бывает.
  • Общие локальные библиотеки. Этот способ очень удобно использовать, когда одни и те же библиотеки используется сразу в нескольких проектах. Допустим, все общие библиотеки мы будем складывать в папку C:\PlatformIO\libs. Создаем такой каталог, вручную скачиваем архив с GitHub, и вручную распаковываем содержимое в только что созданный каталог. Всё как и в предыдущем случае, только целевая папка изменилась. Теперь нам нужно “подключить” каталог C:\PlatformIO\libs к нашему проекту через параметр lib_extra_dirs. Для этого потребуется указать только каталог lib_extra_dirs = C:\PlatformIO\libs, все вложенные библиотеки не более одного уровня вложенности “подхватятся” автоматически. Открываем файл platformio.ini (помните, я вас предупреждал не закрывать его) и добавляем такие строчки:

Этот способ идеален для “своих” общих библиотек, но для “чужих” лучше использовать следующий

  • Публичные библиотеки. Я настолько ленив, что мне даже лень скачивать и распаковывать библиотеки вручную. Да ещё потом и обновлять их придется в случае чего. Данунафиг. PlatformIO предоставляет очень удобный способ подключения публичных библиотек напрямую с GitHub или из каталога библиотек PlatfortmIO. Просто укажите прямые ссылки на них в параметре lib_deps – и вам не придется следить за скачиванием и обновлением этих библиотек – PlatformIO всё возьмет на себя:

Вот так гораздо лучше

Просто? Очень! Нужно только знать, где найти эту самую нужную библиотеку. На гитхабе конечно! Ну да гуголь вам в помощь.

Теперь можно добавить библиотеку в исходный код нашего проекта: #include <PubSubClient.h> и начинать писать код подключения к брокеру.

 


Подключение к MQTT-серверу без шифрования

Для начала давайте попробуем подключиться к брокеру в открытом виде, без TLS-шифрования. Я использую такой вариант только для подключения к своему локальному брокеру, который находится в моей локальной сети, относительно защищенной от интернета NAT-ом. Но для примера вполне сойдет.

Для подключения необходимо предварительно запастить следующими данными:

  • адрес сервера в сети
  • порт сервера (обычно 1883, но вполне может быть и другой)
  • имя пользователя
  • пароль пользователя

Все это вы должны заранее узнать на том сервере, к которому вы собираетесь подключиться. Я достаточно давно уже пользуюсь wqtt.ru (он платный, но стоит относительно недорого и без существенных ограничений на текущий момент !!!не реклама!!!). В данном примере нам понадобятся:

Порт берем пока самый простой, первый по списку. Не перепутайте, иначе ничего не выйдет. Заведем их в программу в виде констант:

// Параметры подключения к MQTT брокеру 
// Примечание: использовать статический mqttClientId оптимальнее с точки зрения фрагментации кучи, только не забывайте изменять его на разных устройствах
const char* mqttServer = "xx.wqtt.ru";
const int mqttPort = 1234;
const char* mqttClientId = "esp8266_demo1";
const char* mqttUser = "u_xxxx";
const char* mqttPass = "xXxXxXxX";

Далее, нам потребуется создать две глобальные переменные:

WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);

Первая – это экземпляр WiFi клиента, который требуется для работы MQTT-клиента. Вторая – MQTT-клиент собственной персоной.

Далее пишем такую функцию, которая будет подключать наше устройство к MQTT-брокеру:

// Подключение к MQTT брокеру :: версия для PubSubClient от knolleary ("стандартная")
bool mqttConnected()
{
  if (!mqttClient.connected()) {
    Serial.print("Connecting to MQTT broker: ");
    // Настраиваем MQTT клиент
    mqttClient.setServer(mqttServer, mqttPort);
    
    // Пробуем подключиться с LWT сообщением "offline"
    if (mqttClient.connect(mqttClientId, mqttUser, mqttPass)) {
      Serial.println("ok");
    } else {
      Serial.print("failed, error code: ");
      Serial.print(mqttClient.state());
      Serial.println("!");
    };
    return mqttClient.connected();
  };
  return true;
}

По аналогии с предыдущим случаем, здесь мы проверяем, есть ли подключение к брокеру; и если нет – указываем параметры сервера и подключаемся. Всё предельно просто.

Во многих примерах ClientID генерируется “на лету” из части MAC-адреса устройства, но я не вижу необходимости тратить на это свободную память кучи. Ибо нефиг. Только не забывайте менять его от проекта к проекту (он должен быть уникальным).

Затем дополняем основной цикл loop() следующими строками:

void setup() {
  // Настройка COM-порта
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");
}
void loop() {
  // Проверяем подключение к WiFi
  if (wifiConnected()}
    // Проверяем подключение к MQTT
    if (mqttConnected()) {
      // Основной цикл клиента MQTT
      mqttClient.loop();
    };
  };

  // Что-то делаем ещё... 
  // Например измеряем температуру и управляем котлом отопления 
}

Если соединение с брокером есть, в каждой итерации loop() обязательно вызываем mqttClient.loop(), дабы отправлять сообщения на сервер и получать входящие сообщения от сервера.

В результате, если всё сделано правильно, мы должны получить примерно следующее:

Прелэстно, прелэстно… К MQTT брокеру мы подключились, но пока мы не научили наше устройство что-либо отправлять и получать с него.


Добавляем LWT и статус устройства

Добавим топик, который позволит нам отслеживать состояние устройства – в сети оно или нет. Для этого воспользуемся функционалом, предоставляемым нам MQTT протоколом, который называется LWT (Last Will and Testament, «последняя воля и завещание») для уведомления заинтересованных сторон об отключении клиента.

  • Пусть для примера топик статуса будет “demo/status” (хотя на практике тут должно бы быть что-то вроде “home/boiler/status” или “garage/status“).
  • В рабочем состоянии в этом топике должно быть “online“, когда устройство выключено или связи нет, то “offline” (хотя вполне можно использовать и просто “1” и “0“).
  • Для такого типа информации оптимальнее всего использовать Qos = 1 (так как повторное получение никому не помешает), а вот retain лучше поставить 1 или true.

Определим константы:

Совет: Я всегда стараюсь использовать константы, а не прописываю постоянные значения прямо “в коде”. Ибо прямое написание ведет к многократным потерям в производительности труда при модификациях кода и многочисленным ошибкам. Надо ли объяснять почему так? Хотя для компилятора, конечно же, всё равно. Это очень плохая практика, для меня это практически табу. Если вы будете так делать, то вы сами себе злобный Буратино.

// Топик LWT, одновременно это топик статуса устройства:
// - в рабочем состоянии в этом топике будет сообщение "online"
// - когда устройство по какой-либо причине отключится от сервера, сервер примерно через 60 секунд опубликует LWT "offline"
const char* mqttTopicDeviceStatus    = "demo/status";
const char* mqttDeviceStatusOn       = "online";    
const char* mqttDeviceStatusOff      = "offline";
const int   mqttDeviceStatusQos      = 1;
const bool  mqttDeviceStatusRetained = true; 

Осталось немного модифицировать код подключения к брокеру:

// Подключение к MQTT брокеру :: версия для PubSubClient от knolleary ("стандартная")
bool mqttConnected()
{
  if (!mqttClient.connected()) {
    Serial.print("Connecting to MQTT broker: ");
    // Настраиваем MQTT клиент
    mqttClient.setServer(mqttServer, mqttPort);
    
    // Пробуем подключиться с LWT сообщением "offline"
    if (mqttClient.connect(mqttClientId, mqttUser, mqttPass, 
         mqttTopicDeviceStatus, mqttDeviceStatusQos, mqttDeviceStatusRetained, mqttDeviceStatusOff)) {
      Serial.println("ok");
      
      // Публикуем статус устройства в тот же топик, что и LWT, но с содержимым "online"
      mqttClient.publish(mqttTopicDeviceStatus, mqttDeviceStatusOn, mqttDeviceStatusRetained);
    } else {
      Serial.print("failed, error code: ");
      Serial.print(mqttClient.state());
      Serial.println("!");
    };
    return mqttClient.connected();
  };
  return true;
}

То есть сразу же при подключении мы отправляем на сервер параметры LWT сообщения. Но сразу после подключения в этом топике будет пусто, так как “offline” там появится только после того, как устройство будет отключено. Дабы исправить это недоразумение, мы сразу же публикуем в этот же самый топик состояние “online“. Вот и всё, что требовалось.

 


 

Добавим SSL / TLS

Подключаться к публичному облачному брокеру в открытом виде – плохой вариант. В этом случае все ваши данные, в том числе логины и пароли, передаются в открытом виде. И более-менее продвинутый кулхацкер сможет перехватить их. Способов изобретено множество – от аппаратных до программных. Например можно создать “подставной” промежуточный сервер брокер или внедрить в вашу сеть сниффер (вопрос только зачем ему это? ну так зачем у нас молодняк дорожные знаки и остановки курочит?). Не будем облегчать кулхацкерам задачу, ну и заодно и себе тоже.

К чему это я? Давайте модифицируем подключение к брокеру с использованием SSL или TLS шифрования. Это дает не только полное шифрование всех передаваемых данных, но и гарантирует, что вы подключаетесь к настоящему серверу, а не подставному, созданному специально для перехвата (хотя вряд ли кто-то когда либо будет создавать сервер-перехватчик специально для вас, если только вы не подпольный миллионер на пенсии).

Я не буду углубляться в теорию TLS-соединений. В сети найдется немало материалов на эту тему, а я не являюсь специалистом в криптографии. Одно я знаю наверняка – для того чтобы установить зашифрованное TLS-соединение, вашему устройству как минимум понадобится сертификат сервера (впрочем не самого сервера, но об этом ниже). Сертификат сервера несет в себе несколько функций:

  • подтверждает подлинность сервера, которому он был выдан
  • содержит в себе ключи шифрования сервера, необходимые для установки зашифрованного канала связи

Отсюда вывод, что без сертификата сервера шифрованное соединение установить не получится. В принципе, сертификат сервера скачает библиотека TLS-соединения самостоятельно, при обращении к серверу. Нам требуется только его проверить – но как это сделать?

Все сертификаты сайтов выданы и подписаны какими-либо центрами сертификации, которые так же имеют свой сертификат. Центр сертификации не обязательно должен быть один, их может быть несколько в цепочке, подписанных один за другим. Например: сертификат для сайта wqtt.ru был выдан ЦС R3, а тому, в свою очередь, выдал сертификат ISRG Root X1.

Первый сертификат в списке называется “корневым”. Если корневому сертификату мы доверяем целиком и полностью, то остальные “вложенные” сертификаты мы сможем проверить “по цепочке”. Поэтому для проверки сертификата любого сайта мы должны иметь “всего лишь” список доверенных корневых сертификатов, которым мы доверяем. Это намного меньше, чем хранить сертификаты всех сайтов, но всё равно это внушительный общем информации, который просто не влезет в память микроконтроллера (впрочем в ESP-IDF имеется возможность подключить к проекту такой список, который называется tls bundle, но речь сейчас не о нём). Поэтому на Arduino обычно ограничиваются подключением с проекту одного или нескольких корневых сертификатов, в зависимости от того, к каким сайтам требуется доступ. Например для подключения к брокеру wqtt.ru нам потребуется подключить сертификат ISRG Root X1.

Но у всех сертификатов (и корневых и не только) имеется встроенная головная боль для ардуино-программистов, и называется она “срок действия сертификата”. После заранее определенного в сертификате срока он считается “испорченным”. Кроме того, сертификат может быть отозван досрочно, если есть подозрения на его компрометацию. И тогда таким “порченным” корневым сертификатом ничего проверить уже будет нельзя. И нам потребуется оперативно заменить его в прошивке. Поэтому для критичных устройств необходимо предусмотреть вариант резервного варианта подключения в открытом виде, дабы не потерять контроль над устройством. Обычно для корневых доверенных сертификатов срок действия достаточно велик, чтобы не очень беспокоиться по этому поводу, но событие в календарик добавить всё-таки стоит.

Как получить файл корневого сертификата

Итак, нам нужен файл корневого сертификата. Самое простое в windows – использовать обычный браузер, например хром. Если у вас установлен антивирус, придется его отключить на время, так как антивирусные программы подменяют сертификаты ЦС своими собственными и это ни к чему хорошему не приведет. Затем откройте нужный вам сайт и найдите замочек в адресной строке, затем кликните на этот замочек:

Затем кликните на строку “Безопасное подключение”, а затем на “Действительный сертификат”:

Откроется окошечко просмотра сертификата, где мы должны перейти на вкладку “Подробнее”, выделить в иерархии верхний сертификат и нажать “Экспорт”:

Затем просто указываем, где бы мы хотели сохранить файл и нажимаем ОК. В итоге у вас в выбранной папке должен нарисоваться новый файлик с вот таким примерно содержанием:

Это и есть то, что нам нужно!

Добавим к нашему проекту константу static const char ISRG_Root_x1[] PROGMEM = R”EOF()EOF”; а затем скопируйте содержимое сертификата и вставьте между круглых скобок. У вас должно получиться нечто вроде этого:

// Корневой сертификат ISRG Root x1, действителен до 4 июня 2035 года
static const char ISRG_Root_x1[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";

Но это ещё не все. Не забудьте скопировать другой номер порта в настройках MQTT, его мы забьем в отдельную константу mqttPortTLS (хотя вполне можно и в старой переменной изменить значение):

Но и это ещё не всё! Вспомните про срок действия сертификата – нам нужно время! Причем в прямом смысле слова. Если к вашему устройству подключены аппаратные часы реального времени, то можно получить время с них. Иначе нам придется получить время с SNTP-сервера. Как это сделать, я уже писал на этом сайте ранее, повторяться не буду. Здесь приведу лишь готовую функцию получения актуального времени, я добавил этот блок в wifiConnected() сразу после подключения:

...

// Подключение успешно установлено
Serial.println("");
Serial.print("WiFi connected, obtained IP address: ");
Serial.println(WiFi.localIP());

// Для работы TLS-соединения нужны корректные дата и время, получаем их с NTP серверов
configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov");
// Ждем, пока локальное время синхронизируется
Serial.print("Waiting for NTP time sync: ");
i = 0;
time_t now = time(nullptr);
while (now < 1000000000) {
  now = time(nullptr);
  i++;
  if (i > 60) {
    // Если в течение этого времени не удалось подключиться - выходим с false
    // Бесконечно ждать подключения опасно - если подключение было разорвано во время работы
    // нужно всё равно "обслуживать" реле и датчики, иначе может случиться беда
    Serial.println("");
    Serial.println("Time sync failed!");
    return false;
  };
  Serial.print(".");
  delay(500);
}

// Время успешно синхронизировано, выводим его в монитор порта
// Только для целей отладки, этот блок можно исключить
Serial.println("");
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print("Current time: ");
Serial.print(asctime(&timeinfo));

Добавим новую глобальную переменную для списка доверенных сертификатов и немного подправим существующую:

// Корневой сертификат
BearSSL::X509List certISRG(ISRG_Root_x1);
BearSSL::WiFiClientSecure wifiClient;
PubSubClient mqttClient(wifiClient);

После этого уже можно смело добавить корневой сертификат в список доверенных:

...

// Время успешно синхронизировано, выводим его в монитор порта
// Только для целей отладки, этот блок можно исключить
Serial.println("");
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print("Current time: ");
Serial.print(asctime(&timeinfo));

// Теперь можно привязать корневой сертификат к клиенту WiFi
wifiClient.setTrustAnchors(&certISRG);

Вот теперь всё готово к TLS-соединению!

Необходимо только не забыть заменить порт подключения в параметрах сервера, иначе при подключении будет возвращена ошибка:

Ну вот и всё, этого достаточно, можно пробовать:

Как видите, это совсем не сложно. А страху-то шо было! Крип-то-гра-фия! О как!


 

Дистанционное управление реле

У нас всё готово для решения практических задач. Вначале давайте сделаем так называемую “умную розетку” или “умную лампочку“, а по сути “тупой пульт дистанционного управления с телефона“, гы… Хотя что нам одна лампочка или розетка. Давайте сразу три или четыре. Пощёлкаем орешки релюшками “по полной программе”.

Перво-наперво нужно определить выводы, к которым подключены наши реле, например так:

// Номер выводов для подключения реле
const int gpioRelay1 = 14;
const int gpioRelay2 = 15;
const int gpioRelay3 = 16;

// Уровни управления реле
const int relayOn    = 0x1;
const int relayOff   = 0x0;

И не забыть настроить эти GPIO на режим “на выход”:

void setup() {
  // Настройка вывода логов в порт serial 0
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");

  // Настройка выводов
  pinMode(gpioRelay1, OUTPUT);
  pinMode(gpioRelay2, OUTPUT);
  pinMode(gpioRelay3, OUTPUT);

  // Сброс всех реле в исходное состояние "отключено"
  digitalWrite(gpioRelay1, relayOff);
  digitalWrite(gpioRelay2, relayOff);
  digitalWrite(gpioRelay3, relayOff);
}

Затем определим топики, через которые будем подавать команды. Пусть это будут такие топики:

// Топики для внешнего управления реле
const char* mqttTopicControlRelay1   = "demo/relay1/control";
const char* mqttTopicControlRelay2   = "demo/relay2/control";
const char* mqttTopicControlRelay3   = "demo/relay3/control";
const int   mqttRelayControlQos      = 1;

Я не зря добавил в конце каждого топика /control – не спешите обрезать, он нам ещё пригодится.

И сразу же определим текстовые команды для этих топиков:

// Текстовое отображение для состояния реле
const char* mqttRelayStatusOn        = "on";
const char* mqttRelayStatusOff       = "off";
const bool  mqttRelayStatusRetained  = false; 

В принципе, можно не заморачиваться с “on” и “off”, а сделать “1” и “0” – но ни на суть, ни на сложность кода это никак не повлияет.

Теперь, дабы получать команды извне, добавляем подписку на эти топики. Проще всего сделать это сразу после подключения к брокеру:

...
Serial.print("Connecting to MQTT broker: ");
// Настраиваем MQTT клиент
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCallback(mqttOnIncomingMsg);

// Пробуем подключиться с LWT сообщением "offline"
if (mqttClient.connect(mqttClientId, mqttUser, mqttPass, mqttTopicDeviceStatus, mqttDeviceStatusQos, mqttDeviceStatusRetained, mqttDeviceStatusOff)) {
  Serial.println("ok");
  
  // Публикуем статус устройства в тот же топик, что и LWT, но с содержимым "online"
  mqttClient.publish(mqttTopicDeviceStatus, mqttDeviceStatusOn, mqttDeviceStatusRetained);

  // Подписываемся на топики управления реле: поскольку топики заранее известны, никаких # и + здесь не нужно!
  mqttClient.subscribe(mqttTopicControlRelay1, mqttRelayControlQos);
  mqttClient.subscribe(mqttTopicControlRelay2, mqttRelayControlQos);
  mqttClient.subscribe(mqttTopicControlRelay3, mqttRelayControlQos);
} else {
  ...
};

Подписаться то мы подписались, а как мы узнаем, что с сервера пришла команда на включение или выключение реле? Правильно – нужно таки создать функцию-обработчик, используя прототип std::function<void(char*, uint8_t*, unsigned int)>

Я написал такую функцию. Вначале нам необходимо выделить полезную информацию из входящего потока байт. Для пущей надежности я добавил функции обрезки “лишних” пробелов из текстовых строк и приведение их к нижнему регистру. Затем можно сравнить топик и данные с заранее определенными и решить как поступить в данном случае:

// Функция обратного вызова при поступлении входящего сообщения от брокера
void mqttOnIncomingMsg(char* topic, byte* payload, unsigned int length)
{
  // Для более корректного сравнения строк приводим их к нижнему регистру и обрезаем пробелы с краев
  String _topic = topic;
  _topic.toLowerCase();
  _topic.trim();

  String _payload;
  for (unsigned int i = 0; i < length; i++) {
    _payload += String((char)payload[i]);
  };
  _payload.toLowerCase();
  _payload.trim();

  // Вывод поступившего сообщения в лог, больше никакого смысла этот блок кода не несет, можно исключить
  Serial.print("Message arrived [");
  Serial.print(_topic.c_str());
  Serial.print("]: ");
  Serial.print(_payload.c_str());
  Serial.println();

  // Сравниваем с топиками
  if (_topic.equals(mqttTopicControlRelay1)) {
    // это топик управления реле 1
    if (_payload.equals(mqttRelayStatusOn)) digitalWrite(gpioRelay1, relayOn);
    if (_payload.equals(mqttRelayStatusOff)) digitalWrite(gpioRelay1, relayOff);
  } else if (_topic.equals(mqttTopicControlRelay2)) {
    // это топик управления реле 2
    if (_payload.equals(mqttRelayStatusOn)) digitalWrite(gpioRelay2, relayOn);
    if (_payload.equals(mqttRelayStatusOff)) digitalWrite(gpioRelay2, relayOff);
  } else if (_topic.equals(mqttTopicControlRelay3)) {
    // это топик управления реле 3
    if (_payload.equals(mqttRelayStatusOn)) digitalWrite(gpioRelay3, relayOn);
    if (_payload.equals(mqttRelayStatusOff)) digitalWrite(gpioRelay3, relayOff);
  } else {
    Serial.println("Failed to recognize incoming topic!");
  };
}

Не забудьте зарегистрировать этот обработчик в любом удобном месте, например так:

// Подключение к MQTT брокеру :: версия для PubSubClient от knolleary ("стандартная")
bool mqttConnected()
{
  if (!mqttClient.connected()) {
    Serial.print("Connecting to MQTT broker: ");
    // Настраиваем MQTT клиент
    mqttClient.setServer(mqttServer, mqttPort);
    mqttClient.setCallback(mqttOnIncomingMsg);

Прошиваем контроллер, проверяем работу.

 


Добавим обратную связь на управление реле

Всё это работает, но не забываем, что отправленное сообщение управления может где-то заблудиться на длинной дороге в проводках и кабелёчках. Например устройство может быть просто выключено, или что то ещё…

Давайте добавим функцию обратной связи, которая будет точно гарантировать, что наше устройство точно выполненное нашу команду. Кроме того, перенесём фактическое управление реле из обработчика входящих команд в другую функцию, а обработчик просто будет переключать нужные переменные.

Добавим топики для публикации фактического состояния реле, они будут немного отличаться от топиков управления, например так:

// Топики для внешнего управления реле
const char* mqttTopicControlRelay1   = "demo/relay1/control";
const char* mqttTopicControlRelay2   = "demo/relay2/control";
const char* mqttTopicControlRelay3   = "demo/relay3/control";
const int   mqttRelayControlQos      = 1;

// Топики для публикации состояния реле
const char* mqttTopicStatusRelay1    = "demo/relay1/status";
const char* mqttTopicStatusRelay2    = "demo/relay2/status";
const char* mqttTopicStatusRelay3    = "demo/relay3/status";

Затем добавим глобальные переменные для хранения текущих состояний реле и для хранения полученных “команд”:

// Текущее состояние реле
byte gpioStatus1 = relayOff;
byte gpioStatus2 = relayOff;
byte gpioStatus3 = relayOff;

// Полученное ("новое") состояние реле
byte relaySetStatus1 = relayOff;
byte relaySetStatus2 = relayOff;
byte relaySetStatus3 = relayOff;

После этого модифицируем наш обработчик следующим образом:

// Сравниваем с топиками
if (_topic.equals(mqttTopicControlRelay1)) {
  // это топик управления реле 1
  if (_payload.equals(mqttRelayStatusOn)) relaySetStatus1 = relayOn;
  if (_payload.equals(mqttRelayStatusOff)) relaySetStatus1 = relayOff;
} else if (_topic.equals(mqttTopicControlRelay2)) {
  // это топик управления реле 2
  if (_payload.equals(mqttRelayStatusOn)) relaySetStatus2 = relayOn;
  if (_payload.equals(mqttRelayStatusOff)) relaySetStatus2 = relayOff;
} else if (_topic.equals(mqttTopicControlRelay3)) {
  // это топик управления реле 3
  if (_payload.equals(mqttRelayStatusOn)) relaySetStatus3 = relayOn;
  if (_payload.equals(mqttRelayStatusOff)) relaySetStatus3 = relayOff;
} else {
  Serial.println("Failed to recognize incoming topic!");
};

Отлично. Осталось написать код, который будет сравнивать “старое” и “новое” состояние реле и в случае необходимости щёлкать ими:

// Переключение состояния реле
void mqttChangeRelaysState()
{
  // Новое состояние реле 1 отличается от текущего, требуется переключение
  if (relaySetStatus1 != gpioStatus1) {
    gpioStatus1 = relaySetStatus1;
    digitalWrite(gpioRelay1, gpioStatus1);
    
    // gpioStatus1 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff 
    // это то же самое, что и
    // if (gpioStatus1 == relayOn) { mqttRelayStatusOn } else { mqttRelayStatusOff }
    mqttClient.publish(mqttTopicStatusRelay1, (gpioStatus1 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);
    
    Serial.print("Relay 1 has changed its state: ");
    Serial.println(gpioStatus1 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff);
  };

  // Новое состояние реле 2 отличается от текущего, требуется переключение
  if (relaySetStatus2 != gpioStatus2) {
    gpioStatus2 = relaySetStatus2;
    digitalWrite(gpioRelay2, gpioStatus2);

    // gpioStatus2 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff 
    // это то же самое, что и
    // if (gpioStatus2 == relayOn) { mqttRelayStatusOn } else { mqttRelayStatusOff }
    mqttClient.publish(mqttTopicStatusRelay2, (gpioStatus2 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);
    
    Serial.print("Relay 2 has changed its state: ");
    Serial.println(gpioStatus2 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff);
  };

  // Новое состояние реле 3 отличается от текущего, требуется переключение
  if (relaySetStatus3 != gpioStatus3) {
    gpioStatus3 = relaySetStatus3;
    digitalWrite(gpioRelay3, gpioStatus3);

    // gpioStatus3 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff 
    // это то же самое, что и
    // if (gpioStatus3 == relayOn) { mqttRelayStatusOn } else { mqttRelayStatusOff }
    mqttClient.publish(mqttTopicStatusRelay3, (gpioStatus3 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);
    
    Serial.print("Relay 3 has changed its state: ");
    Serial.println(gpioStatus3 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff);
  };
}

Если значение relayCommandX и relayStatusX отличаются друг от друга, то переключаем выход, к которому подключено реле и публикуем новое состояние реле в “ответный” топик.

Но это ещё не всё. Хорошо бы при подключении к MQTT серверу опубликовать текущие состояния реле, так как во время временного отключения от сервера состояния реле могло и измениться (например если у вас будет какой-либо автоматический сценарий). Это просто:

Serial.println("ok");

// Публикуем статус устройства в тот же топик, что и LWT, но с содержимым "online"
mqttClient.publish(mqttTopicDeviceStatus, mqttDeviceStatusOn, mqttDeviceStatusRetained);

// Подписываемся на топики управления реле: поскольку топики зарашее известны, никаких # и + здесь не нужно!
mqttClient.subscribe(mqttTopicControlRelay1, mqttRelayControlQos);
mqttClient.subscribe(mqttTopicControlRelay2, mqttRelayControlQos);
mqttClient.subscribe(mqttTopicControlRelay3, mqttRelayControlQos);

// Публикуем текущее состояние реле
// gpioStatus1 == 1 ? mqttRelayStatusOn : mqttRelayStatusOff 
// это то же самое, что и
// if (gpioStatus1 == relayOn) { mqttRelayStatusOn } else { mqttRelayStatusOff }
mqttClient.publish(mqttTopicStatusRelay1, (gpioStatus1 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);
mqttClient.publish(mqttTopicStatusRelay2, (gpioStatus2 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);
mqttClient.publish(mqttTopicStatusRelay3, (gpioStatus3 == relayOn ? mqttRelayStatusOn : mqttRelayStatusOff), mqttRelayStatusRetained);

Ну и не забудьте добавить эту функцию в основной цикл:

void loop() {
  // Проверяем подключение к WiFi и MQTT
  if (wifiConnected() && mqttConnected()) {
    // Основной цикл клиента MQTT
    mqttClient.loop();

    ///////////////////////////////////////////////////////////////
    // Ваша прошивка здесь может делать что-то еще...
    // Состояние: Когда все подключено и все работает
    ///////////////////////////////////////////////////////////////
  } else {
    // Даже если ничего не вышло с WiFi подключением, вы можете что-то сделать автономно
    // Например переключить реле по датчику температуры
    // Ваш роутер может просто выйти из строя, но автоматика должна продолжать работать

    ///////////////////////////////////////////////////////////////
    // Ваша прошивка здесь может делать что-то еще...
    // Состояние: Когда с подключениями приключилась беда
    ///////////////////////////////////////////////////////////////

    // Но в данном случае мы просто чуть-чуть подождем и попробуем подключиться снова
    delay(1000);
  };

  // Управление реле
  mqttChangeRelaysState();

  ///////////////////////////////////////////////////////////////
  // Ваша прошивка здесь может делать что-то еще...
  // Состояние: Вообще пофигу на всё
  ///////////////////////////////////////////////////////////////
}

Ещё раз всё компилируем:

А на брокере в это время:

С чем вас и поздравляю! Вы только что сделали умную розетку, да ещё и с обратной связью. Думаю, что теперь вы сможете придумать, как управлять реле не по команде, а в автоматическом режиме.


 

Добавим публикацию состояния цифрового входа

А что, если нам нужно периодически считывать состояние входа и публиковать его состояние при изменении? Давайте попробуем…

Конечно, оптимальнее было бы использовать прерывания, но не будем пока о них. Сделаем “по простому”, выжмем из процессора все соки…

Для этого нам понадобится как минимум ещё одна переменная inputStatus1 – для хранения последнего считанного значения. Иначе нам пришлось бы публиковать состояние входа в каждом цикле, а это лишняя и абсолютно бесполезная нагрузка на процессор, сеть и брокер:

// Последнее опубликованное состояние реле
// Здесь маленькая хитрость: публикация в топик происходит только при наличии изменений
// (то есть когда gpioStatus1 != digitalRead()), то при первом подключении ничего не будет опубликовано
// Дабы форсировать события, проинициализируем переменные заведомо ложными данными, которых не будет при любом состоянии входов
byte gpioStatus1 = 2;
byte gpioStatus2 = 2;
byte gpioStatus3 = 2;

Но вы можете сделать входов столько, сколько позволят свободные выводы ESP.

Ну и константы, топики и прочее, куда же без них:

// Топики для публикации состояния входов
const char* mqttTopicStatusInput1    = "demo/input1/status";
const char* mqttTopicStatusInput2    = "demo/input2/status";
const char* mqttTopicStatusInput3    = "demo/input3/status";

// Текстовое отображение для состояния входов
const char* mqttInputStatusLow       = "0";
const char* mqttInputStatusHigh      = "1";
const bool  mqttInputStatusRetained  = false; 

Сама функция чтения не сложнее предыдущих:

// Контроль состояния входов
void mqttReadInputsState()
{
  // Читаем вход 1
  byte gpioNewState1 = digitalRead(gpioInput1);
  if (gpioNewState1 != gpioStatus1) {
    gpioStatus1 = gpioNewState1;
  
    // gpioStatus1 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow
    // это то же самое, что и
    // if (gpioStatus1 == HIGH) { mqttInputStatusHigh } else { mqttInputStatusLow }
    mqttClient.publish(mqttTopicStatusInput1, (gpioStatus1 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow), mqttInputStatusRetained);

    Serial.print("Input 1 has changed its state: ");
    Serial.println(gpioStatus1 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow);
  };

  // Читаем вход 2
  byte gpioNewState2 = digitalRead(gpioInput2);
  if (gpioNewState2 != gpioStatus2) {
    gpioStatus2 = gpioNewState2;
  
    // gpioStatus2 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow
    // это то же самое, что и
    // if (gpioStatus2 == HIGH) { mqttInputStatusHigh } else { mqttInputStatusLow }
    mqttClient.publish(mqttTopicStatusInput2, (gpioStatus2 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow), mqttInputStatusRetained);

    Serial.print("Input 2 has changed its state: ");
    Serial.println(gpioStatus2 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow);
  };

  // Читаем вход 3
  byte gpioNewState3 = digitalRead(gpioInput3);
  if (gpioNewState3 != gpioStatus3) {
    gpioStatus3 = gpioNewState3;
  
    // gpioStatus3 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow
    // это то же самое, что и
    // if (gpioStatus3 == HIGH) { mqttInputStatusHigh } else { mqttInputStatusLow }
    mqttClient.publish(mqttTopicStatusInput3, (gpioStatus3 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow), mqttInputStatusRetained);

    Serial.print("Input 3 has changed its state: ");
    Serial.println(gpioStatus3 == HIGH ? mqttInputStatusHigh : mqttInputStatusLow);
  };
}

Осталось вызывать её из главного цикла:

void loop() {
  // Проверяем подключение к WiFi и MQTT
  if (wifiConnected() && mqttConnected()) {
    // Основной цикл клиента MQTT
    mqttClient.loop();
    ///////////////////////////////////////////////////////////////
    // Ваша прошивка здесь может делать что-то еще...
    // Состояние: Когда все подключено и все работает
    ///////////////////////////////////////////////////////////////
  } else {
    // Даже если ничего не вышло с WiFi подключением, вы можете что-то сделать автономно
    // Например переключить реле по датчику температуры
    // Ваш роутер может просто выйти из строя, но автоматика должна продолжать работать
    ///////////////////////////////////////////////////////////////
    // Ваша прошивка здесь может делать что-то еще...
    // Состояние: Когда с подключениями приключилась беда
    ///////////////////////////////////////////////////////////////

    // Но в данном случае мы просто чуть-чуть подождем и попробуем подключиться снова
    delay(1000);
  };
  
  // Контроль состояния входов 
  mqttReadInputsState();

  // Управление реле
  mqttChangeRelaysState();

  ///////////////////////////////////////////////////////////////
  // Ваша прошивка здесь может делать что-то еще...
  // Состояние: Вообще пофигу на всё
  ///////////////////////////////////////////////////////////////
}

И таки да, не забудьте настроить используемые GPIO:

void setup() {
  // Настройка вывода логов в порт serial 0
  Serial.begin(9600);
  Serial.println();
  Serial.println("Demo project FOR ESP8266");

  // Настройка выводов на вход
  pinMode(gpioInput1, INPUT);
  pinMode(gpioInput2, INPUT);
  pinMode(gpioInput3, INPUT);

  ...

В результат вы должны прочить следующее:

и на брокере:

 


Измеряем температуру и влажность в помещении

Теперь давайте попробуем подключить какой-нибудь датчик и прочитать с него данные. Для примера я возьму вот такой AM2302, так как на нем уже есть резистор подтяжки шины single bus:

Его я подключил к выводу D1 или GPIO5 (кстати, отличный справочник по выводам ESP8266).

Для работы с данным сенсоров так же потребуется подключить сторонние библиотеки, например от Adafruit:

#include <Adafruit_Sensor.h>
#include <DHT.h>

Но перед этим добавим ссылки на них в файл platformio.ini

; Подключаем публичные библиотеки
lib_deps = 
    ; Библиотека для подключения к MQTT брокеру
    https://github.com/knolleary/pubsubclient
    ; Библиотеки для работы с DHT22
    https://github.com/adafruit/Adafruit_Sensor
    https://github.com/adafruit/DHT-sensor-library
    ; Библиотека Arduino One Wire
    https://github.com/PaulStoffregen/OneWire

Ну а дальше просто копипастим код из примера к драйверу DHT22. Ну и добавляем код для публикации на брокере:

Создадим глобальную переменную для DHT22:

// Номера выводов для датчиков
const int gpioDHT22   = 5;

// Датчик температуры и влажности
DHT dht(gpioDHT22, DHT22);

 

Определим необходимые топики:

// Топики для публикации температуры и влажности
const char* mqttTopicTemperature     = "demo/dht22/temperature";
const char* mqttTopicHumidity        = "demo/dht22/humidity";
const bool  mqttSensorsRetained      = false; 

 

Проинициализируем сенсор в функции setup():

void setup() {
  ...

  // Инициализация датчика температуры
  dht.begin();
}

 

И можно, наконец, прочитать и опубликовать данные:

// Чтение температуры и влажности
void readTemperature()
{
  // Читаем данные с сенсора
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  // Вывод считанных данных в лог
  Serial.print(F("Humidity: "));
  Serial.print(h);
  Serial.print(F("%  Temperature: "));
  Serial.print(t);
  Serial.println(F("°C "));

  // Публикуем данные на сервере
  String str_temp(t);
  mqttClient.publish(mqttTopicTemperature, str_temp.c_str(), mqttSensorsRetained);
  String str_humd(h);
  mqttClient.publish(mqttTopicHumidity, str_humd.c_str(), mqttSensorsRetained);
}

 

Но вот вызывать эту функцию каждую итерацию цикла loop() не следует, поэтому добавим простейший псевдотаймер на millis():

// Чтение и публикация температуры каждые 10 секунд
static unsigned long lastTempRead = 0;
if ((millis() - lastTempRead) >= 10000) {
  lastTempRead = millis();
  readTemperature();
};

 

Если вы все сделали правильно, то должны получить примерно такой результат:

А на сервере в это же самое время можно увидеть следующее:


Ссылки

Как всегда, пример можно скачать в готовом виде с GitHub: github.com/kotyara12/arduino/tree/master/arduino_eps8266_dzen

 

 

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 5 оценок ]

16 комментариев для “Телеметрия на ESP8266 + MQTT. Пошаговое руководство по созданию DIY-проекта с удаленным управлением”

  1. Connecting to MQTT broker: failed, error code: -2!
    id клиента менял, но всё равно эта ошибка.
    в чём может быть ошибка?

    1. Добрый день!
      В чем угодно: неверный сервер, неверный порт, неверный сертификат, неверный логин, неверный пароль…

  2. Добрый день
    попробовал проєкт “arduino_eps8266_relays_no_ssl” ругается:
    ==========================================================================
    src\main.cpp: In function ‘bool wifiConnected()’:
    src\main.cpp:106:10: error: invalid use of member function ‘bool ESP8266WiFiSTAClass::setAutoConnect(bool)’ (did you forget the ‘()’ ?)
    106 | WiFi.setAutoConnect = false;
    | ~~~~~^~~~~~~~~~~~~~
    src\main.cpp:107:10: error: invalid use of member function ‘bool ESP8266WiFiSTAClass::setAutoReconnect(bool)’ (did you forget the ‘()’ ?)
    107 | WiFi.setAutoReconnect = false;
    ==========================================================================
    Заметил что в проєкте “arduino_eps8266_relays_ssl” єтих строк нету… Можете пояснить почему так?
    Спасибо!

    1. Это ошибка. Нужно писать:
      WiFi.setAutoConnect(false);
      WiFi.setAutoReconnect(false);
      Без этих строк работать тоже будет, но вы должны понимать, почему это нужно или нет.

      Я не проверял все версии. Я проверял только SSL, других я не использую. И вам не советую.

  3. Дмитрий

    Помогите! Несколько раз переустанавливал VS и PIO. В любом проекте ругается на Arduino.h :
    Обнаружены ошибки #include. Измените includePath. Волнистые линии отключены для этой единицы трансляции (C:\Users\dmser\Desktop\arduino-master\arduino-master\arduino_eps8266_relays_no_ssl\src\main.cpp).C/C++(1696)
    не удается открыть источник файл “pins_arduino.h” (dependency of “Arduino.h”)C/C++(1696)

    1. Скорее всего, соответствующая платформа не установлена. Перейдите на PIO Home и установите нужную – Espressif 32 или Espressif 8266.

  4. Дмитрий

    Установлена Espressif 8266. Даже в примере Arduino Blink Выдает ошибку: Обнаружены ошибки #include. Измените includePath. Волнистые линии отключены для этой единицы трансляции (C:\Users\dmser\Documents\PlatformIO\Projects\230708-153109-arduino-blink\src\main.cpp).
    не удается открыть источник файл “pins_arduino.h” (dependency of “Arduino.h”) и так в любом проекте.

    Пробовал устанавливать ранние версии PIO, результат тот же. За 3 дня облазил весь инет, ничего не помогает.
    Попытался поставить PIO на Atom. Atom в репозитории не находит PIO…

    1. А проект-то компилируется?
      Если это просто IntelliSence ругается, а проект компилируетя, то это ерунда. Поправьте пути к платформам в c_cpp_properties.json и будет вам щасье.
      Об этом даже вроде бы статья была соответствующая

    1. Если не компилируется, значит у вас действительно проблема. Но я не телепат и на расстоянии не могу определить, что не так.
      Кстати, а вы учли, что в статье на Дзене настройки для ESP32 и Вам нужно указывать совсем другие пути?
      Вот например в моем случае (под ваш профиль его тоже корректировать нужно)
      “includePath”: [
      “C:/PlatformIO/arduino/arduino_eps8266_dzen/include”,
      “C:/PlatformIO/arduino/arduino_eps8266_dzen/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/tools/sdk/include”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/cores/esp8266”,
      “C:/Users/kotyara12/.platformio/packages/toolchain-xtensa/include”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lwip2/include”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/variants/nodemcu”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ArduinoOTA”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/DNSServer/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/EEPROM”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266AVRISP/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPClient/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPUpdateServer/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266LLMNR”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266NetBIOS”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SSDP”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SdFat/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WebServer/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFi/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFiMesh/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266httpUpdate/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266mDNS/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Ethernet/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/GDBStub/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Hash/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/I2S/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/LittleFS/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Netdump/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/SD/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/SDFS/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/SPI”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/SPISlave/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Servo/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/SoftwareSerial/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/TFT_Touch_Shield_V2”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Ticker/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/Wire”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/esp8266/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/lwIP_PPP/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/lwIP_enc28j60/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/lwIP_w5100/src”,
      “C:/Users/kotyara12/.platformio/packages/framework-arduinoespressif8266/libraries/lwIP_w5500/src”,
      “”
      ],

  5. Дмитрий

    Pio перестраивает файл. Удалил VScode, очистил вручную комп от папок и файлов VS и PIO. Устанавливаю всё заново.
    PIO установился в C:\Users\dmser\.vscode\extensions\platformio.platformio-ide-3.2.0-win32-x64
    папки .platformio нигде нет.
    path: C:\Users\dmser\.vscode\extensions\platformio.platformio-ide-3.2.0-win32-x64\scripts
    Команда pio – Имя “pio” не распознано как имя командлета, функции, файла сценария или выполняемой программы.

    1. Поищите папку .platformio прямо в корне диска C:\ – возможно она там…

      Если pio не распознается как команда – значит путь к ней не зарегистрирован в переменных окружения
      https://kotyara12.ru/iot/pio_install/, раздел “Установка” пункт 6-7

    2. Дмитрий

      К сожалению, изменить коммент не могу, PIO встал на место после обновления, path прописал, остался трабл с этим:
      Команда pio – Имя “pio” не распознано как имя командлета, функции, файла сценария или выполняемой программы.

  6. Отлично! Повторил проект, всё работает, добавил датчики 18b20, на смартфоне отображается температура, состояние (online/offline). Хороший сайт, очень много информации и всё в одном месте, благодарность автору!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *