ESP32:Примеры/MQTT-клиент на базе ESP32
MQTT-клиент на базе ESP32
В этом примере мы продемонстрируем, как использовать MQTT для обмена данными между двумя платами ESP32. Мы сконструируем простой проект, чтобы проиллюстрировать самые важные концепты MQTT.
Рисунок ниже схематически показывает, как будет устроен этот проект. Он будет использовать две ESP32-платы: ESP32 #1 и ESP32 #2.
- ESP32 #1 подключена к светодиоду и считывает температуру с датчика DS18B20;
- ESP32 #2 подключена к кнопке, и нажатие на нее будет включать/выключать светодиод, подключенный к ESP32 #1;
- ESP32 #2 подключена к LCD-дисплею (передающему данные по I2C), на котором будут печататься данные о температуре, полученные от ESP32 #1;
На схеме ниже показано, какие функции в этом проекте выполняет MQTT-брокер.
- ESP32 #1 подписана на топик «esp32/led» и публикует данные о температуре в топик «esp32/temperature»;
- При нажатии на кнопку, подключенную к ESP32 #2, эта плата будет публиковать соответствующее сообщение в топик «esp32/led», с помощью которого управляется светодиод, подключенный к ESP32 #1;
Справочная информация
Использование MQTT с ESP32: введение
Этот Раздел будет введением в тему MQTT и то, как ее можно использовать вместе с ESP32.
Аббревиатура MQTT означает «message queuing telemetry transport», что можно перевести как «передача сообщений о телеметрии при помощи очереди». Это упрощенная система публикации и подписки, позволяющая публиковать сообщения и, будучи клиентом, получать их.
MQTT – это простой протокол передачи данных, предназначенный для устройств с низкой пропускной способностью. Это делает его идеальным для проектов в области интернета вещей. MQTT позволяет отправлять команды для управления устройствами вывода данных, считывать и публиковать данные от датчиков и многое другое.
Базовые концепты MQTT
В MQTT есть несколько базовых концептов, которые нужно знать и понимать:
- Публикация/подписка;
- Сообщения;
- Топики;
- Брокер;
Публикация/подписка
Первый концепт – это система публикации и подписки. Ее суть в том, что устройство может публиковать сообщения в топик или быть подписано на него, чтобы получать его сообщения.
Для примера представьте такую ситуацию:
- Устройство 1 публикует данные в топик;
- Устройство 2 подписано на топик, куда публикует свои данные Устройство 1;
- Это позволяет Устройству 2 получать сообщения Устройства 1;
Сообщения – это фрагменты информации, переходящие от одного устройства к другому. Это могут быть и данные, и команды.
Топики
Еще один важный концепт MQTT – это топики. Для устройства-отправителя это место, куда оно может публиковать свои сообщения, а для устройства-получателя это место, к сообщениям которого оно может проявить заинтересованность.
Топики представляются в виде строк, разбитых на части при помощи прямых слешей («/»). Каждый слеш означает уровень топика. Ниже – пример топика для лампы, находящейся кабинете, который в свою очередь находится у вас дома.
Итак, если представить сценарий, при котором вы управляете лампой в своем домашнем кабинете при помощи ESP32 и MQTT, то он будет выглядеть примерно так:
- У нас есть устройство, которое публикует сообщения «вкл» и «выкл» в топик «home/office/lamp»;
- На этот топик подписана ESP32, управляющая включением/выключением лампы;
- Таким образом, когда в этом топике публикуется новое сообщение, ESP32 получает команду «вкл» или «выкл» и, соответственно, включает либо выключает лампу;
Первым устройством может быть ESP32, ESP8266 или контроллер, на который установлена платформа для домашней автоматизации вроде Node-RED, Home Assistant, Domoticz или OpenHAB.
Брокер
Наконец, вам также нужно понимать, что такое «брокер». Он в основном ответственен за получение всех сообщений, фильтрацию сообщений, принятие решений о том, кто в них заинтересован, и отправку этих сообщений всем подписанным клиентам.
Брокеры бывают разными. Есть, к примеру, брокер Mosquitto, установленный на Raspberry Pi, или бесплатный облачный брокер CloudMQTT.
Мы будем использовать брокер Mosquitto, установленный на Raspberry Pi.
Итого
Итак, в этом Разделе мы научились следующему:
- MQTT – это коммуникационный протокол, хорошо подходящий для проектов в области интернета вещей;
- В MQTT устройства могут публиковать сообщения в топики и быть подписаны на другие топики, чтобы получать их сообщения;
- При использовании MQTT необходим брокер. Он получает все сообщения и отправляет их подписанным клиентам;
Установка MQTT-брокера Mosquitto на Raspberry Pi
В этом Разделе мы установим брокер Mosquitto на Raspberry Pi. Как уже говорилось в предыдущем разделе, брокер в основном ответственен за получение всех сообщений, фильтрацию сообщений, принятие решение о том, кто в них заинтересован, и публикацию этих сообщений для всех подписанных клиентов.
Брокеры бывают разными. В этом примере мы будем использовать брокер Mosquitto, установленный на Raspberry Pi.
Откройте новое окно терминала Raspberry Pi.
Чтобы установить брокер Mosquitto, введите следующие команды:
sudo apt update
sudo apt install -y mosquitto mosquitto-clients
Чтобы Mosquitto автоматически запускался при загрузке, впишите следующее:
sudo systemctl enable mosquitto.service
Проверяем, установлен ли Mosquitto
Отправляем команду:
mosquitto -v
Эта команда возвращает версию Mosquitto, установленную на Raspberry Pi. Она должна быть 1.4.x или выше.
IP-адрес Raspberry Pi
Чтобы узнать IP-адрес своей Raspberry Pi, впишите в терминал следующую команду:
hostname -I
В нашем случае IP-адрес Raspberry Pi – это «192.168.1.2». Сохраните его, т.к. он понадобится нам далее, чтобы ESP32 можно было подключить к MQTT-брокеру Mosquitto.
Итого
Итак, в этом Разделе мы научились устанавливать брокер Mosquitto на Raspberry Pi.
Устанавливаем библиотеки
Нам нужно установить в IDE Arduino необходимые библиотеки, которые понадобятся для того, чтобы использовать вместе с ESP32 протокол MQTT – «AsyncTCP» и «async MQTT client».
Устанавливаем библиотеку «AsyncTCP»
- Кликните тут, чтобы загрузить ZIP-архив библиотеки;
- Распакуйте скачанный архив. У вас должна получиться папка «AsyncTCP-master»;
- Переименуйте ее на «AsyncTCP»;
- Переместите папку «AsyncTCP» в папку «libraries», которая находится в папке, куда установлена ваша IDE Arduino;
- Перезапустите IDE Arduino если она была запущена;
Устанавливаем библиотеку «async MQTT client»
- Кликните тут, чтобы загрузить ZIP-архив библиотеки;
- Распакуйте скачанный архив. У вас должна получиться папка «async-mqtt-client-master»;
- Переименуйте ее на «async_mqtt_client»;
- Переместите папку «async_mqtt_client» в папку «libraries», которая находится внутри папки, куда установлена ваша IDE Arduino;
- Перезапустите IDE Arduino если она была запущена;
Нам также нужно установить библиотеки «OneWire» (автор – Пол Стоффреген) и «Arduino Temperature Control» – они необходимы для использования датчика DS18B20.
Устанавливаем библиотеку «OneWire»
- Кликните тут, чтобы скачать ZIP-архив библиотеки;
- Распакуйте скачанный архив. У вас должна получиться папка «OneWire-master»;
- Переименуйте папку «OneWire-master» на «OneWire»;
- Переместите папку «OneWire» в папку «libraries», которая находится внутри папки, куда установлена ваша IDE Arduino;
- Перезапустите IDE Arduino если она была запущена;
Устанавливаем библиотеку «Arduino Temperature Control»
- Кликните тут, чтобы скачать ZIP-архив библиотеки;
- Распакуйте скачанный архив. У вас должна получиться папка «Arduino-Temperature-Control-Library-master»;
- Переименуйте папку «Arduino-Temperature-Control-Library-master» на «DallasTemperature»;
- Переместите папку «DallasTemperature» в папку «libraries», находящуюся внутри папки, куда установлена ваша IDE Arduino;
- Перезапустите IDE Arduino если она была запущена;
Устанавливаем библиотеку «LiquidCrystal I2C»
- Кликните тут, чтобы скачать ZIP-архив библиотеки;
- Распакуйте скачанный архив. У вас должна получиться папка под названием «LiquidCrystal_I2C-master»;
- Переименуйте ее на «LiquidCrystal_I2C»;
- Переместите папку «LiquidCrystal_I2C» в папку «libraries», которая находится внутри папки, куда установлена ваша IDE Arduino;
- Перезапустите IDE Arduino если она была запущена;
ESP32 #1
Что нужно сделать перед загрузкой кода
Чтобы этот код заработал сразу, безо всяких дополнительных настроек, впишите в строчках ниже SSID и пароль для своей WiFi-сети:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
Вам также нужно будет ввести IP-адрес MQTT-брокера Mosquitto. О том, как найти IP-адрес Raspberry Pi, рассказывается в предыдущем Разделе.
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
Теперь вы можете загрузить этот код как есть, и он сразу начнет работать. Но мы рекомендуем также ознакомиться с текстом ниже, в котором объясняется, как именно он работает.
Как работает этот код
Фрагмент кода ниже импортирует в скетч все необходимые библиотеки:
#include <WiFi.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
Как уже было сказано выше, вам также нужно будет вписать в скетч SSID и пароль для своей WiFi-сети, а также указать IP-адрес своей Raspberry Pi. Это делается в этом фрагменте:
// поменяйте SSID и пароль в двух строчках ниже,
// чтобы ESP32 могла подключиться к вашему роутеру:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// вставьте в переменную «MQTT_HOST» IP-адрес своей Raspberry Pi,
// чтобы она могла подключиться к MQTT-брокеру Mosquitto:
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
#define MQTT_PORT 1883
Создаем объект для управления MQTT-клиентом и таймеры, которые понадобятся для повторного подключения к MQTT-брокеру или WiFi-роутеру, если связь вдруг оборвется.
// создаем объекты для управления MQTT-клиентом:
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
Далее объявляем еще несколько переменных: одну для хранения данных о температуре и две вспомогательные таймерные переменные – для чтения опубликованных данных каждые 5 секунд.
String temperatureString = ""; // переменная для хранения
// данных о температуре
unsigned long previousMillis = 0; // здесь хранится информация о том,
// когда в последний раз
// была опубликована температура
const long interval = 5000; // интервал между публикациями
// данных от датчика
Затем задаем контакт, к которому подключен светодиод, и начальное состояние для него.
const int ledPin = 25; // GPIO-контакт, к которому
// подключен светодиод
int ledState = LOW; // текущее состояние
// выходного контакта
Наконец, задаем контакт для датчика DS18B20 и создаем объекты для его работы.
// GPIO-контакт, к которому подключен датчик DS18B20:
const int oneWireBus = 32;
// делаем так, чтобы объект «oneWire»
// коммуницировал с любыми OneWire-устройствами:
OneWire oneWire(oneWireBus);
// передаем объект «oneWire» датчику температуры:
DallasTemperature sensors(&oneWire);
MQTT-функции: подключение к WiFi, подключение к MQTT, WiFi-события
В коде нет комментариев для функций, о которых пойдет речь ниже. Эти функции – часть библиотеки «async MQTT client», и их названия, собственно, говорят сами за себя. К примеру, функция connectToWiFi() подключает ESP32 к WiFi-роутеру:
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
// "Подключаемся к WiFi..."
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
Функция connectToMqtt() подключает ESP32 к MQTT-брокеру.
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
// "Подключаемся к MQTT..."
mqttClient.connect();
}
Функция WiFiEvent() ответственна за управление WiFi-событиями. К примеру, после успешного подключения к роутеру и MQTT-брокеру она напечатает IP-адрес ESP32. Кроме того, при обрыве соединения она запустит таймер и попытается возобновить подключение.
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("WiFi connected"); // "Подключились к WiFi"
Serial.println("IP address: "); // "IP-адрес: "
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("WiFi lost connection");
// "WiFi-связь потеряна"
// делаем так, чтобы ESP32
// не переподключалась к MQTT
// во время переподключения к WiFi:
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
}
}
Подписываемся на MQTT-топик
Задача функции onMqttConnect() – подписка ESP32 на топики. Вы можете добавить в эту функцию дополнительные топики, на которые в дальнейшем подпишется ESP32. Но если оставить код без изменений, ESP32 подпишется только на топик «esp32/led».
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT."); // "Подключились к MQTT."
Serial.print("Session present: "); // "Текущая сессия: "
Serial.println(sessionPresent);
// ESP32 подписывается на топик esp32/led
uint16_t packetIdSub = mqttClient.subscribe("esp32/led", 0);
Serial.print("Subscribing at QoS 0, packetId: "); // "Подписка при QoS 0, ID пакета: "
Serial.println(packetIdSub);
}
Следующая строчка – важная часть этого фрагмента. Она подписывает ESP32 на MQTT-топик при помощи метода subscribe().
uint16_t packetIdSub = mqttClient.subscribe("esp32/led", 0);
Параметрами у этого метода служат MQTT-топик, на который нужно подписать ESP32, и уровень качества обслуживания (англ. «quality of service» или просто «QoS»). Более подробно о том, что это такое, можно почитать тут.
Еще MQTT-функции: отключение, подписка, отписка и публикация
Если ESP32 потеряет соединение с MQTT-брокером, функция onMqttDisconnect() напечатает в мониторе порта соответствующее сообщение:
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.println("Disconnected from MQTT.");
// "Отключились от MQTT."
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
Если ESP32 подпишется на MQTT-топик, функция onMqttSubscribe() напечатает ID пакета и уровень качества обслуживания (QoS):
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.println("Subscribe acknowledged.");
// "Подписка подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
Serial.print(" qos: ");
Serial.println(qos);
}
Если ESP32 отпишется от топика, функция onMqttUnsubscribe() напечатает сообщение об этом.
void onMqttUnsubscribe(uint16_t packetId) {
Serial.println("Unsubscribe acknowledged.");
// "Отписка подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
И если в MQTT-топике будет опубликовано новое сообщение, функция onMqttPublish() напечатает в мониторе порта ID этого пакета.
void onMqttPublish(uint16_t packetId) {
Serial.println("Publish acknowledged.");
// "Публикация подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
Получаем MQTT-сообщения
Если в топик, на который подписана ESP32 (в данном случае – в топик «esp32/led»), придет новое сообщение, будет выполнена функция onMqttMessage(). В ней задается то, что произойдет при появлении в этом топике нового сообщения, и вы эти события можете отредактировать.
// этой функцией управляется то, что происходит
// при получении того или иного сообщения в топике «esp32/led»;
// (если хотите, можете ее отредактировать):
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
String messageTemp;
for (int i = 0; i < len; i++) {
//Serial.print((char)payload[i]);
messageTemp += (char)payload[i];
}
// проверяем, получено ли MQTT-сообщение в топике «esp32/led»:
if (strcmp(topic, "esp32/led") == 0) {
// если светодиод выключен, включаем его (и наоборот):
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// задаем светодиоду значение переменной «ledState»:
digitalWrite(ledPin, ledState);
}
Serial.println("Publish received.");
// "Опубликованные данные получены."
Serial.print(" message: "); // " сообщение: "
Serial.println(messageTemp);
Serial.print(" topic: "); // " топик: "
Serial.println(topic);
Serial.print(" qos: "); // " уровень обслуживания: "
Serial.println(properties.qos);
Serial.print(" dup: "); // " дублирование сообщения: "
Serial.println(properties.dup);
Serial.print(" retain: "); // "сохраненные сообщения: "
Serial.println(properties.retain);
Serial.print(" len: "); // " размер: "
Serial.println(len);
Serial.print(" index: "); // " индекс: "
Serial.println(index);
Serial.print(" total: "); // " суммарно: "
Serial.println(total);
}
Во фрагменте выше при помощи оператора if() проверяется, было ли в топике «esp32/led» опубликовано новое сообщение. Вот эта строчка:
if (strcmp(topic, "esp32/led") == 0) {
Внутри этого оператора if() можно задать собственную логику. Например, еще один оператор if(), который включает/выключает светодиод каждый раз, когда в этот топик приходит соответствующее сообщение (в скетче реализован как раз такой вариант).
// если светодиод выключен, включаем его (и наоборот):
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// задаем светодиоду значение переменной «ledState»:
digitalWrite(ledPin, ledState);
}
Все эти функции, которые мы рассмотрели выше – это функции обратного вызова и потому выполняются асинхронно.
setup()
Теперь давайте перейдем к блоку setup(). Запускаем датчик DS18B20, делаем контакт светодиода выходным, задаем ему значение «LOW» и запускаем последовательную коммуникацию.
// Запускаем датчик DS18B20:
sensors.begin();
// задаем контакт, к которому подключен светодиод,
// как выходной контакт, и присваиваем ему значение «LOW»:
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
Serial.begin(115200);
В двух строчках ниже создаются таймеры для повторного подключения к MQTT-брокеру и WiFi-роутеру в случае, если соединение будет потеряно.
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
Строчка ниже задает, чтобы при подключении ESP32 к WiFi-сети была запущена функция обратного вызова WiFiEvent(), которая напечатает данные о WiFi-связи (какие именно – см. выше).
WiFi.onEvent(WiFiEvent);
Наконец, присваиваем все функции обратного вызова, созданные выше. Это значит, что все эти функции будут вызваны автоматически – в случае, если они понадобятся. К примеру, когда ESP32 подключится к брокеру, автоматически будет вызвана функция onMqttConnect() и т.д.
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onMessage(onMqttMessage);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
loop()
В блоке loop() создаем таймер, позволяющий публиковать новые температурные данные в топике «esp32/temperature» каждые 5 секунд.
unsigned long currentMillis = millis();
// каждые X секунд («interval» = 5 секунд)
// в топик «esp32/temperature»
// будет публиковаться новое MQTT-сообщение:
if (currentMillis - previousMillis >= interval) {
// сохраняем в переменную «previousMillis» время,
// когда в последний раз были опубликованы новые данные:
previousMillis = currentMillis;
// новые данные о температуре:
sensors.requestTemperatures();
temperatureString = " " + String(sensors.getTempCByIndex(0)) + "C " +
String(sensors.getTempFByIndex(0)) + "F";
Serial.println(temperatureString);
// публикуем MQTT-сообщение в топике «esp32/temperature»
// с температурой в градусах Цельсия и Фаренгейта:
uint16_t packetIdPub2 = mqttClient.publish("esp32/temperature", 2, true, temperatureString.c_str());
Serial.print("Publishing on topic esp32/temperature at QoS 2, packetId: ");
// "Публикация в топик «esp32/temperature»
// при QoS 2, ID пакета: "
Serial.println(packetIdPub2);
}
Добавление новых топиков для публикации/подписки
Это очень простой пример, но он иллюстрирует, как работает публикация и подписка в MQTT.
Если вы хотите добавить в код дополнительные топики для публикации, просто продублируйте в loop() следующие три строчки (но не забудьте заменить название топика). Собственно, за публикацию данных в топик здесь отвечает только строчка с методом publish(), а две остальные просто печатают в монитор порта информацию о публикации и публикуемых данных.
uint16_t packetIdPub2 = mqttClient.publish("esp32/temperature", 2, true, temperatureString.c_str());
Serial.print("Publishing on topic esp32/temperature at QoS 2, packetId: ");
// "Публикация в топик «esp32/temperature»
// при QoS 2, ID пакета: "
Serial.println(packetIdPub2);
Если вам нужно задать в коде дополнительные топики для подписки, перейдите к функции onMqttConnect(), продублируйте строчку ниже, но также не забудьте поменять в ней название топика на тот, на который хотите подписаться.
uint16_t packetIdSub = mqttClient.subscribe("esp32/led", 0);
Наконец, в функции onMqttMessage() можно задать, что произойдет, когда в заданный топик придет новое сообщение.
Загружаем код
Запитайте Raspberry Pi, убедитесь, что на ней запущен MQTT-брокер Mosquitto, и загрузите этот код на ESP32.
Откройте монитор порта на скорости 115200 бод и проверьте, подключилась ли ESP32 к WiFi-роутеру и MQTT-брокеру.
Как видите, все работает, как надо!
А это значит, что пора переходить к следующем Разделу, где мы настроим вторую часть этого проекта – плату ESP32 #2.
ESP32 #2
Что нужно сделать перед загрузкой кода
Как и в прошлый раз, чтобы код сразу же заработал, вам нужно вписать в строчках ниже SSID и пароль для WiFi-сети, а также IP-адрес MQTT-брокера.
// вставьте в строчках ниже SSID и пароль для своей WiFi-сети,
// чтобы ESP32 могла подключиться к вашему WiFi-роутеру:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// задайте в переменной «MQTT_HOST» IP-адрес своей Raspberry Pi,
// чтобы ESP32 могла подключиться к MQTT-брокеру Mosquitto:
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
Как работает этот код
Большую часть скетча мы пропустим, т.к. она уже объяснялась в Разделе выше.
В строчках ниже вписываем SSID и пароль для своей WiFi-сети, а также IP-адрес MQTT-брокера.
// вставьте в строчках ниже SSID и пароль для своей WiFi-сети,
// чтобы ESP32 могла подключиться к вашему WiFi-роутеру:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// задайте в переменной «MQTT_HOST» IP-адрес своей Raspberry Pi,
// чтобы ESP32 могла подключиться к MQTT-брокеру Mosquitto:
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
В двух строчках ниже задаем количество столбцов и рядов LCD-дисплея. Мы используем LCD-дисплей, у которого экран размером 16х2 символов.
// задаем количество столбцов и рядов для LCD-дисплея:
const int lcdColumns = 16;
const int lcdRows = 2;
Байтовый массив ниже предназначен для того, чтобы показать на LCD-дисплее иконку термометра:
// иконка термометра:
byte thermometerIcon[8] = {
B00100,
B01010,
B01010,
B01010,
B01010,
B10001,
B11111,
B01110
};
Затем задаем контакт, к которому подключена кнопка, переменную для текущего состояния кнопки, переменную для последнего состояния кнопки и вспомогательные переменные для создания таймера антидребезга (чтобы избежать «фейковых» нажатий на кнопку).
const int buttonPin = 32; // GPIO-контакт, к которому
// подключена кнопка
int buttonState; // текущее значение
// входного контакта (кнопки)
int lastButtonState = LOW; // предыдущее значение
// входного контакта (кнопки)
unsigned long lastDebounceTime = 0; // время, когда в последний раз
// был переключен
// выходной контакт
unsigned long debounceDelay = 50; // время антидребезга
// (увеличьте, если
// выходное значение «прыгает»)
MQTT-функции
О функциях connectToWiFi(), connectToMQTT() и WiFiEvent() уже рассказывалось в Разделе выше.
В функции onMqttConnect() добавляются топики для подписки.
// в эту функцию можно добавить новые топики для подписки:
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT."); // "Подключились к MQTT."
Serial.print("Session present: "); // "Текущая сессия: "
Serial.println(sessionPresent);
uint16_t packetIdSub = mqttClient.subscribe("esp32/temperature", 0);
Serial.print("Subscribing at QoS 0, packetId: ");
// "Подписка при QoS 0, ID пакета: "
Serial.println(packetIdSub);
}
В нашем случае ESP32 подписана только на топик «esp32/temperature», но функцию onMqttConnect() можно отредактировать и добавить в нее новые топики для подписки.
// эта функция управляет тем, что произойдет
// при получении топиком «esp32/temperature» того или иного сообщения
// (эту функцию можно отредактировать):
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
String messageTemp;
for (int i = 0; i < len; i++) {
//Serial.print((char)payload[i]);
messageTemp += (char)payload[i];
}
if (strcmp(topic, "esp32/temperature") == 0) {
lcd.clear();
lcd.setCursor(1, 0);
lcd.write(0);
lcd.print(" Temperature"); // " Температура"
lcd.setCursor(0, 1);
lcd.print(messageTemp);
}
(...)
Как уже рассказывалось в предыдущем Разделе, в функции onMqttMessage() мы задаем, что произойдет, когда в топик, на который подписана ESP32, придет новое сообщение. В эту функцию можно добавить дополнительные операторы if() – для проверки других топиков и содержимого сообщений.
В нашем случае, когда в топик «esp32/temperature» придет новое сообщение, ESP32 покажет его на LCD-дисплее, который к ней подключен.
setup()
В блоке setup() мы запускаем LCD-дисплей, включаем подсветку и создаем иконку термометра, которую затем покажем на LCD-дисплее.
// инициализируем LCD-дисплей:
lcd.init();
// включаем подсветку LCD-дисплея:
lcd.backlight();
// создаем иконку термометра:
lcd.createChar(0, thermometerIcon);
Делаем контакт, к которому подключена кнопка, входным.
// делаем контакт, к которому подключена кнопка, входным контактом:
pinMode(buttonPin, INPUT);
Затем создаем таймеры для повторного подключения.
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
И задаем все функции обратного вызова для WiFi- и MQTT-событий, созданных нами ранее.
WiFi.onEvent(WiFiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onMessage(onMqttMessage);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
loop()
В блоке loop() публикуем MQTT-сообщение в топик «esp32/led», если кнопка была нажата.
Мы также используем здесь фрагмент из скетча-примера «Debounce», предназначенного для того, чтобы избежать «дребезга» («фальшивых» нажатий на кнопку).
Если кнопка была нажата, оператор if() ниже вернет «true», а в топике «esp32/led» будет опубликовано сообщение «toogle».
if (buttonState == HIGH) {
mqttClient.publish("esp32/led", 0, true, "toggle");
Serial.println("Publishing on topic esp32/led topic at QoS 0");
// "Публикация в топик «esp32/led» при QoS 0"
}
Вот и все. Можете загрузить этот код на ESP32.
Демонстрация
Запитайте Raspberry Pi, убедитесь, что на ней запущен MQTT-брокер Mosquitto, откройте монитор порта и задайте в нем скорость передачи данных на 115200 бод.
Проверьте, подключилась ли ESP32 к WiFi-роутеру и MQTT-брокеру. В моем случае, как видно на скриншоте ниже, все работает как надо:
Теперь подключите питание к обеим платам и оставьте включенным MQTT-брокер Mosquitto.
ESP32 #2 мгновенно получит данные о температуре от ESP32 #1 в градусах Цельсия и Фаренгейта.
Если нажать на кнопку, это тут же переключит состояние светодиода, подключенного к ESP32 #1.
Эту систему можно легко расширить, добавив дополнительные платы ESP32. Кроме того, вы можете создать дополнительные MQTT-топики, через которые можно отправлять команды и данные от датчиков. Также ко всей этой системе можно добавить панель управления при помощи платформы домашней автоматизации вроде Node-RED, Home Assistant, Domoticz или OpenHAB.
Итого
В этом примере мы показали вам, как передавать данные между двумя ESP32 при помощи протокола MQTT.
Необходимое оборудование
- Плата ESP32 - 2шт.;
- Кнопка - 1шт.;
- Резистор на 110 кОм - 1шт.;
- Резистор на 330 Ом - 1 шт.;
- Резистор на 4,7кОм - 1шт.;
- LCD-дисплей (16х2, I2C) - 1шт.;
- Светодиод - 1шт.;
- Температурный датчик DS18B20 - 1шт.;
- Провода-перемычки;
- Макетная плата - 2 шт.;
- Плата Raspberry Pi - 1шт.
- Карта памяти для Raspberry Pi - 1шт.;
- Блок питания для Raspberry Pi - 1шт.;
Схема
ESP32 #1
- Подключите светодиод к контакту GPIO25 (через резистор на 330 Ом)
- Подключите датчик DS18B20 к контакту GPIO32 (через подтягивающий резистор на 4.7 кОм)
ESP32 #2
- Подключение кнопки: одну ножку подключите к 3.3-вольтовому контакту, а другую – (через стягивающий резистор на 110 кОм) к контакту GPIO32;
- Подключение LCD-дисплея: подключите контакт SDA к GPIO21, а SCL – к GPIO22. LCD-дисплей работает на 5 вольтах, поэтому его нужно подключить к контактам Vin и GND;
В качестве подсказки также можно использовать схему ниже:
Код
ESP32 #1
/*********
Руи Сантос
Более подробно о проекте на: https://randomnerdtutorials.com
*********/
#include <WiFi.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// поменяйте SSID и пароль в двух строчках ниже,
// чтобы ESP32 могла подключиться к вашему роутеру:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// вставьте в переменную «MQTT_HOST» IP-адрес своей Raspberry Pi,
// чтобы ESP32 могла подключиться к MQTT-брокеру Mosquitto:
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
#define MQTT_PORT 1883
// создаем объекты для управления MQTT-клиентом:
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
String temperatureString = ""; // переменная для хранения
// данных о температуре
unsigned long previousMillis = 0; // здесь хранится информация о том,
// когда в последний раз
// была опубликована температура
const long interval = 5000; // интервал между публикациями
// данных от датчика
const int ledPin = 25; // GPIO-контакт, к которому
// подключен светодиод
int ledState = LOW; // текущее состояние
// выходного контакта
// GPIO-контакт, к которому подключен датчик DS18B20:
const int oneWireBus = 32;
// делаем так, чтобы объект «oneWire»
// коммуницировал с любыми OneWire-устройствами:
OneWire oneWire(oneWireBus);
// передаем объект «oneWire» датчику температуры:
DallasTemperature sensors(&oneWire);
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
// "Подключаемся к WiFi..."
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
// "Подключаемся к MQTT... "
mqttClient.connect();
}
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("WiFi connected"); // "Подключились к WiFi"
Serial.println("IP address: "); // "IP-адрес: "
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("WiFi lost connection");
// "WiFi-связь потеряна"
// делаем так, чтобы ESP32
// не переподключалась к MQTT
// во время переподключения к WiFi:
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
}
}
// в этом фрагменте добавляем топики,
// на которые будет подписываться ESP32:
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT."); // "Подключились по MQTT."
Serial.print("Session present: "); // "Текущая сессия: "
Serial.println(sessionPresent);
// подписываем ESP32 на топик «esp32/led»:
uint16_t packetIdSub = mqttClient.subscribe("esp32/led", 0);
Serial.print("Subscribing at QoS 0, packetId: ");
// "Подписываемся при QoS 0, ID пакета: "
Serial.println(packetIdSub);
}
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.println("Disconnected from MQTT.");
// "Отключились от MQTT."
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.println("Subscribe acknowledged.");
// "Подписка подтверждена."
Serial.print(" packetId: "); // " ID пакета: "
Serial.println(packetId);
Serial.print(" qos: "); // " Уровень качества обслуживания: "
Serial.println(qos);
}
void onMqttUnsubscribe(uint16_t packetId) {
Serial.println("Unsubscribe acknowledged.");
// "Отписка подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
void onMqttPublish(uint16_t packetId) {
Serial.println("Publish acknowledged.");
// "Публикация подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
// этой функцией управляется то, что происходит
// при получении того или иного сообщения в топике «esp32/led»;
// (если хотите, можете ее отредактировать):
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
String messageTemp;
for (int i = 0; i < len; i++) {
//Serial.print((char)payload[i]);
messageTemp += (char)payload[i];
}
// проверяем, получено ли MQTT-сообщение в топике «esp32/led»:
if (strcmp(topic, "esp32/led") == 0) {
// если светодиод выключен, включаем его (и наоборот):
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// задаем светодиоду значение из переменной «ledState»:
digitalWrite(ledPin, ledState);
}
Serial.println("Publish received.");
// "Опубликованные данные получены."
Serial.print(" message: "); // " сообщение: "
Serial.println(messageTemp);
Serial.print(" topic: "); // " топик: "
Serial.println(topic);
Serial.print(" qos: "); // " уровень обслуживания: "
Serial.println(properties.qos);
Serial.print(" dup: "); // " дублирование сообщения: "
Serial.println(properties.dup);
Serial.print(" retain: "); // "сохраненные сообщения: "
Serial.println(properties.retain);
Serial.print(" len: "); // " размер: "
Serial.println(len);
Serial.print(" index: "); // " индекс: "
Serial.println(index);
Serial.print(" total: "); // " суммарно: "
Serial.println(total);
}
void setup() {
// запускаем датчик DS18B20:
sensors.begin();
// делаем контакт, к которому подключен светодиод,
// выходным контактом, и присваиваем ему значение «LOW»:
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
Serial.begin(115200);
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
WiFi.onEvent(WiFiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onMessage(onMqttMessage);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
connectToWifi();
}
void loop() {
unsigned long currentMillis = millis();
// каждые X секунд («interval» = 5 секунд)
// в топик «esp32/temperature»
// будет публиковаться новое MQTT-сообщение:
if (currentMillis - previousMillis >= interval) {
// сохраняем в переменную «previousMillis» время,
// когда в последний раз были опубликованы новые данные:
previousMillis = currentMillis;
// новые данные о температуре:
sensors.requestTemperatures();
temperatureString = " " + String(sensors.getTempCByIndex(0)) + "C " + String(sensors.getTempFByIndex(0)) + "F";
Serial.println(temperatureString);
// публикуем MQTT-сообщение в топике «esp32/temperature»
// с температурой в градусах Цельсия и Фаренгейта:
uint16_t packetIdPub2 = mqttClient.publish("esp32/temperature", 2, true, temperatureString.c_str());
Serial.print("Publishing on topic esp32/temperature at QoS 2, packetId: ");
// "Публикация в топик «esp32/temperature»
// при QoS 2, ID пакета: "
Serial.println(packetIdPub2);
}
}
ESP32 #2
/*********
Руи Сантос
Более подробно о проекте на: https://randomnerdtutorials.com
*********/
#include <WiFi.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <LiquidCrystal_I2C.h>
// вставьте в строчках ниже SSID и пароль для своей WiFi-сети,
// чтобы ESP32 могла подключиться к вашему WiFi-роутеру:
#define WIFI_SSID "REPLACE_WITH_YOUR_SSID"
#define WIFI_PASSWORD "REPLACE_WITH_YOUR_PASSWORD"
// задайте в переменной «MQTT_HOST» IP-адрес своей Raspberry Pi,
// чтобы ESP32 могла подключиться к MQTT-брокеру Mosquitto:
#define MQTT_HOST IPAddress(192, 168, 1, XXX)
#define MQTT_PORT 1883
// создаем объекты для управления MQTT-клиентом:
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
// задаем количество столбцов и рядов для LCD-дисплея:
const int lcdColumns = 16;
const int lcdRows = 2;
// создаем объект LCD-дисплея,
// присваивая ему адрес, а также количество столбцов и рядов;
// (если вам неизвестен адрес дисплея,
// запустите скетч для сканирования I2C-устройств):
LiquidCrystal_I2C lcd(0x27, lcdColumns, lcdRows);
// иконка термометра:
byte thermometerIcon[8] = {
B00100,
B01010,
B01010,
B01010,
B01010,
B10001,
B11111,
B01110
};
const int buttonPin = 32; // GPIO-контакт, к которому
// подключена кнопка
int buttonState; // текущее значение
// входного контакта (кнопки)
int lastButtonState = LOW; // предыдущее значение
// входного контакта (кнопки)
unsigned long lastDebounceTime = 0; // время, когда в последний раз
// был переключен
// выходной контакт
unsigned long debounceDelay = 50; // период антидребезга
// (увеличьте, если
// выходное значение «прыгает»)
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
// "Подключаемся к WiFi..."
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
// "Подключаемся к MQTT..."
mqttClient.connect();
}
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
Serial.println("WiFi connected"); // "Подключились к WiFi"
Serial.println("IP address: "); // "IP-адрес: "
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("WiFi lost connection");
// "WiFi-связь потеряна"
// делаем так, чтобы ESP32
// не переподключалась к MQTT
// во время переподключения к WiFi:
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
}
}
// в эту функцию можно добавить новые топики для подписки:
void onMqttConnect(bool sessionPresent) {
Serial.println("Connected to MQTT."); // "Подключились к MQTT."
Serial.print("Session present: "); // "Текущая сессия: "
Serial.println(sessionPresent);
uint16_t packetIdSub = mqttClient.subscribe("esp32/temperature", 0);
Serial.print("Subscribing at QoS 0, packetId: ");
// "Подписка при QoS 0, ID пакета: "
Serial.println(packetIdSub);
}
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.println("Disconnected from MQTT.");
// "Отключились от MQTT."
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.println("Subscribe acknowledged.");
// "Подписка подтверждена."
Serial.print(" packetId: "); // " ID пакета: "
Serial.println(packetId);
Serial.print(" qos: "); // " уровень качества обслуживания: "
Serial.println(qos);
}
void onMqttUnsubscribe(uint16_t packetId) {
Serial.println("Unsubscribe acknowledged.");
// "Отписка подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
void onMqttPublish(uint16_t packetId) {
Serial.println("Publish acknowledged.");
// "Публикация подтверждена."
Serial.print(" packetId: ");
Serial.println(packetId);
}
// эта функция управляет тем, что произойдет
// при получении топиком «esp32/temperature» того или иного сообщения
// (эту функцию можно отредактировать):
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
String messageTemp;
for (int i = 0; i < len; i++) {
//Serial.print((char)payload[i]);
messageTemp += (char)payload[i];
}
if (strcmp(topic, "esp32/temperature") == 0) {
lcd.clear();
lcd.setCursor(1, 0);
lcd.write(0);
lcd.print(" Temperature"); // " Температура"
lcd.setCursor(0, 1);
lcd.print(messageTemp);
}
Serial.println("Publish received.");
// "Опубликованные данные получены."
Serial.print(" message: "); // " сообщение: "
Serial.println(messageTemp);
Serial.print(" topic: "); // " топик: "
Serial.println(topic);
Serial.print(" qos: "); // " уровень качества обслуживания: "
Serial.println(properties.qos);
Serial.print(" dup: "); // " дублирование сообщения: "
Serial.println(properties.dup);
Serial.print(" retain: "); // " сохраненные сообщения: "
Serial.println(properties.retain);
Serial.print(" len: "); // " размер: "
Serial.println(len);
Serial.print(" index: "); // " индекс: "
Serial.println(index);
Serial.print(" total: "); // " суммарно: "
Serial.println(total);
}
void setup() {
// инициализируем LCD-дисплей:
lcd.init();
// включаем подсветку LCD-дисплея:
lcd.backlight();
// создаем иконку термометра:
lcd.createChar(0, thermometerIcon);
// делаем контакт, к которому подключена кнопка, входным контактом:
pinMode(buttonPin, INPUT);
Serial.begin(115200);
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
WiFi.onEvent(WiFiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onUnsubscribe(onMqttUnsubscribe);
mqttClient.onMessage(onMqttMessage);
mqttClient.onPublish(onMqttPublish);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
connectToWifi();
}
void loop() {
// считываем состояние кнопки
// и сохраняем его в локальную переменную:
int reading = digitalRead(buttonPin);
// если состояние кнопки изменилось
// (из-за шума или нажатия), сбрасываем таймер:
if (reading != lastButtonState) {
// сбрасываем таймер антидребезга:
lastDebounceTime = millis();
}
// если состояние кнопки изменилось после периода антидребезга:
if ((millis() - lastDebounceTime) > debounceDelay) {
// и если новое значение кнопки
// отличается от того, что сейчас хранится в «buttonState»:
if (reading != buttonState) {
buttonState = reading;
// публикуем MQTT-сообщение в топике «esp32/led»,
// чтобы переключить (включить или выключить) светодиод:
if (buttonState == HIGH) {
mqttClient.publish("esp32/led", 0, true, "toggle");
Serial.println("Publishing on topic esp32/led topic at QoS 0");
// "Публикация в топик «esp32/led» при QoS 0"
}
}
}
// сохраняем текущее значение кнопки в переменную «lastButtonState»;
// в результате во время следующего прохода через loop()
// оно будет считаться предыдущим значением кнопки:
lastButtonState = reading;
}
См.также
Внешние ссылки