ESP32: Веб-сервер с панелью датчиков на ESP-NOW (ESP-NOW + Wi-Fi)

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

ESP32: Веб-сервер с панелью датчиков ESP-NOW с использованием Arduino IDE (ESP-NOW и Wi-Fi одновременно)

У нас есть другие руководства, связанные с ESP-NOW, которые могут вас заинтересовать:

Смотрите видеоурок

Одновременное использование ESP-NOW и Wi-Fi

Одновременное использование ESP-NOW и Wi-Fi: приёмник ESP-NOW с веб-сервером и платы-отправители ESP-NOW

Есть несколько моментов, которые нужно учитывать, если вы хотите использовать Wi-Fi для размещения веб-сервера и одновременно использовать ESP-NOW для получения показаний датчиков от других плат:

  • Платы-отправители ESP32 должны использовать тот же канал Wi-Fi, что и плата-приёмник.

  • Канал Wi-Fi платы-приёмника автоматически назначается вашим Wi-Fi роутером.

  • Режим Wi-Fi платы-приёмника должен быть установлен как точка доступа и станция одновременно (WIFI_AP_STA).

  • Вы можете установить тот же канал Wi-Fi вручную, или добавить небольшой фрагмент кода на отправитель, чтобы установить его канал Wi-Fi таким же, как у платы-приёмника.

Обзор проекта

На следующей диаграмме показан общий обзор проекта, который мы будем создавать.

Веб-сервер приёмника ESP-NOW и платы ESP32, отправляющие показания температуры и влажности через ESP-NOW
  • Две платы-отправители ESP32 отправляют показания температуры и влажности DHT22 через ESP-NOW на одну плату-приёмник ESP32 (конфигурация ESP-NOW «многие к одному»);

  • Плата-приёмник ESP32 принимает пакеты и отображает показания на веб-сервере;

  • Веб-сервер автоматически обновляется каждый раз, когда получает новое показание, используя Server-Sent Events (SSE).

Предварительные требования

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

Arduino IDE

Мы будем программировать платы ESP32 с помощью Arduino IDE, поэтому перед началом работы с этим руководством убедитесь, что у вас установлена плата ESP32 в Arduino IDE.

Установка библиотек

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

Вы можете установить библиотеки через Менеджер библиотек Arduino. Перейдите в Sketch > Include Library > Manage Libraries и найдите названия библиотек для их установки.

Чтобы узнать больше о датчике температуры DHT11 или DHT22, прочитайте наше руководство: ESP32 с датчиком температуры и влажности DHT11/DHT22 с использованием Arduino IDE.

Необходимые компоненты

Для этого руководства вам потребуется несколько плат ESP32. Мы будем использовать три платы ESP32. Также вам понадобятся:

Получение MAC-адреса платы-приёмника

Для отправки сообщений через ESP-NOW вам нужно знать MAC-адрес платы-приёмника. Каждая плата имеет уникальный MAC-адрес (узнайте, как Получить и изменить MAC-адрес ESP32).

Загрузите следующий код на вашу плату-приёмник ESP32, чтобы получить её MAC-адрес.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/get-change-esp32-esp8266-mac-address-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#ifdef ESP32
  #include <WiFi.h>
  #include <esp_wifi.h>
#else
  #include <ESP8266WiFi.h>
#endif

void setup(){
  Serial.begin(115200);

  Serial.print("ESP Board MAC Address: ");
  #ifdef ESP32
    WiFi.mode(WIFI_STA);
    WiFi.STA.begin();
    uint8_t baseMac[6];
    esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
    if (ret == ESP_OK) {
      Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
                    baseMac[0], baseMac[1], baseMac[2],
                    baseMac[3], baseMac[4], baseMac[5]);
    } else {
      Serial.println("Failed to read MAC address");
    }
  #else
    Serial.println(WiFi.macAddress());
  #endif
}

void loop(){

}

Просмотреть исходный код

После загрузки кода нажмите кнопку RST/EN, и MAC-адрес должен отобразиться в Serial Monitor.

MAC-адрес платы ESP32 в Serial Monitor Arduino IDE

ESP32 Приёмник (ESP-NOW + Веб-сервер)

Плата-приёмник ESP32 принимает пакеты от плат-отправителей и размещает веб-сервер для отображения последних полученных показаний.

Загрузите следующий код на вашу плату-приёмник — код подготовлен для получения показаний от двух разных плат.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-esp-now-wi-fi-web-server/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <esp_now.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#include <Arduino_JSON.h>

// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
  int id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

struct_message incomingReadings;

JSONVar board;

AsyncWebServer server(80);
AsyncEventSource events("/events");

// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
  // Copies the sender mac address to a string
  char macStr[18];
  Serial.print("Packet received from: ");
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.println(macStr);
  memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));

  board["id"] = incomingReadings.id;
  board["temperature"] = incomingReadings.temp;
  board["humidity"] = incomingReadings.hum;
  board["readingId"] = String(incomingReadings.readingId);
  String jsonString = JSON.stringify(board);
  events.send(jsonString.c_str(), "new_readings", millis());

  Serial.printf("Board ID %u: %u bytes\n", incomingReadings.id, len);
  Serial.printf("t value: %4.2f \n", incomingReadings.temp);
  Serial.printf("h value: %4.2f \n", incomingReadings.hum);
  Serial.printf("readingID value: %d \n", incomingReadings.readingId);
  Serial.println();
}

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP-NOW DASHBOARD</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="icon" href="data:,">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    p {  font-size: 1.2rem;}
    body {  margin: 0;}
    .topnav { overflow: hidden; background-color: #2f4468; color: white; font-size: 1.7rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
    .reading { font-size: 2.8rem; }
    .packet { color: #bebebe; }
    .card.temperature { color: #fd7e14; }
    .card.humidity { color: #1b78e2; }
  </style>
</head>
<body>
  <div class="topnav">
    <h3>ESP-NOW DASHBOARD</h3>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> BOARD #1 - TEMPERATURE</h4><p><span class="reading"><span id="t1"></span> &deg;C</span></p><p class="packet">Reading ID: <span id="rt1"></span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> BOARD #1 - HUMIDITY</h4><p><span class="reading"><span id="h1"></span> &percnt;</span></p><p class="packet">Reading ID: <span id="rh1"></span></p>
      </div>
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> BOARD #2 - TEMPERATURE</h4><p><span class="reading"><span id="t2"></span> &deg;C</span></p><p class="packet">Reading ID: <span id="rt2"></span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> BOARD #2 - HUMIDITY</h4><p><span class="reading"><span id="h2"></span> &percnt;</span></p><p class="packet">Reading ID: <span id="rh2"></span></p>
      </div>
    </div>
  </div>
<script>
if (!!window.EventSource) {
 var source = new EventSource('/events');

 source.addEventListener('open', function(e) {
  console.log("Events Connected");
 }, false);
 source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
 }, false);

 source.addEventListener('message', function(e) {
  console.log("message", e.data);
 }, false);

 source.addEventListener('new_readings', function(e) {
  console.log("new_readings", e.data);
  var obj = JSON.parse(e.data);
  document.getElementById("t"+obj.id).innerHTML = obj.temperature.toFixed(2);
  document.getElementById("h"+obj.id).innerHTML = obj.humidity.toFixed(2);
  document.getElementById("rt"+obj.id).innerHTML = obj.readingId;
  document.getElementById("rh"+obj.id).innerHTML = obj.readingId;
 }, false);
}
</script>
</body>
</html>)rawliteral";

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);

  // Set the device as a Station and Soft Access Point simultaneously
  WiFi.mode(WIFI_AP_STA);

  // Set device as a Wi-Fi Station
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
  }
  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info
  esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", index_html);
  });

  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);
  server.begin();
}

void loop() {
  static unsigned long lastEventTime = millis();
  static const unsigned long EVENT_INTERVAL_MS = 5000;
  if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
    events.send("ping",NULL,millis());
    lastEventTime = millis();
  }
}

Просмотреть исходный код

Как работает код

Сначала подключите необходимые библиотеки.

#include <esp_now.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#include <Arduino_JSON.h>

Библиотека Arduino_JSON нужна, потому что мы будем создавать JSON-переменную с данными, полученными от каждой платы. Эта JSON-переменная будет использоваться для отправки всей необходимой информации на веб-страницу, как вы увидите далее в этом проекте.

Вставьте ваши учётные данные сети в следующие строки, чтобы ESP32 мог подключиться к вашей локальной сети.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Структура данных

Затем создайте структуру, содержащую данные, которые мы будем получать. Мы назвали эту структуру struct_message, и она содержит идентификатор платы, показания температуры и влажности, а также идентификатор чтения.

typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

Создайте новую переменную типа struct_message с именем incomingReadings, которая будет хранить значения переменных.

struct_message incomingReadings;

Создайте JSON-переменную с именем board.

JSONVar board;

Создайте асинхронный веб-сервер на порту 80.

AsyncWebServer server(80);

Создание источника событий

Для автоматического отображения информации на веб-сервере при поступлении нового показания мы будем использовать Server-Sent Events (SSE).

Следующая строка создаёт новый источник событий на /events.

AsyncEventSource events("/events");

Server-Sent Events позволяют веб-странице (клиенту) получать обновления от сервера. Мы будем использовать это для автоматического отображения новых показаний на странице веб-сервера, когда приходит новый пакет ESP-NOW.

Важно: Server-sent events не поддерживаются в Internet Explorer.

Функция OnDataRecv()

Функция OnDataRecv() будет выполняться при получении нового пакета ESP-NOW.

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {

Внутри этой функции выведите MAC-адрес отправителя:

// Copies the sender mac address to a string
char macStr[18];
Serial.print("Packet received from: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
         mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);

Скопируйте информацию из переменной incomingData в структурную переменную incomingReadings.

memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));

Затем создайте строковую JSON-переменную с полученной информацией (переменная jsonString):

board["id"] = incomingReadings.id;
board["temperature"] = incomingReadings.temp;
board["humidity"] = incomingReadings.hum;
board["readingId"] = String(incomingReadings.readingId);
String jsonString = JSON.stringify(board);

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

board = {
  "id": "1",
  "temperature": "24.32",
  "humidity" = "65.85",
  "readingId" = "2"
}

После сбора всех полученных данных в переменной jsonString отправьте эту информацию в браузер как событие («new_readings»).

events.send(jsonString.c_str(), "new_readings", millis());

Позже мы увидим, как обрабатывать эти события на стороне клиента.

Наконец, выведите полученную информацию в Serial Monitor Arduino IDE для целей отладки:

Serial.printf("Board ID %u: %u bytes\n", incomingReadings.id, len);
Serial.printf("t value: %4.2f \n", incomingReadings.temp);
Serial.printf("h value: %4.2f \n", incomingReadings.hum);
Serial.printf("readingID value: %d \n", incomingReadings.readingId);
Serial.println();

Создание веб-страницы

Переменная index_html содержит весь HTML, CSS и JavaScript для создания веб-страницы. Мы не будем подробно разбирать работу HTML и CSS. Мы рассмотрим только то, как обрабатывать события, отправленные сервером.

Обработка событий

Создайте новый объект EventSource и укажите URL страницы, отправляющей обновления. В нашем случае это /events.

if (!!window.EventSource) {
 var source = new EventSource('/events');

После создания экземпляра источника событий вы можете начать прослушивание сообщений от сервера с помощью addEventListener().

Это стандартные слушатели событий, как показано в документации AsyncWebServer.

source.addEventListener('open', function(e) {
  console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
 if (e.target.readyState != EventSource.OPEN) {
   console.log("Events Disconnected");
 }
}, false);

source.addEventListener('message', function(e) {
 console.log("message", e.data);
}, false);

Затем добавьте слушатель событий для «new_readings».

source.addEventListener('new_readings', function(e) {

Когда ESP32 получает новый пакет, он отправляет JSON-строку с показаниями как событие («new_readings») клиенту. Следующие строки определяют, что происходит, когда браузер получает это событие.

console.log("new_readings", e.data);
var obj = JSON.parse(e.data);
document.getElementById("t"+obj.id).innerHTML = obj.temperature.toFixed(2);
document.getElementById("h"+obj.id).innerHTML = obj.humidity.toFixed(2);
document.getElementById("rt"+obj.id).innerHTML = obj.readingId;
document.getElementById("rh"+obj.id).innerHTML = obj.readingId;

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

setup()

В setup() установите ESP32-приёмник как точку доступа и Wi-Fi станцию:

WiFi.mode(WIFI_AP_STA);

Следующие строки подключают ESP32 к вашей локальной сети и выводят IP-адрес и канал Wi-Fi:

// Set device as a Wi-Fi Station
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Setting as a Wi-Fi Station..");
}
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Wi-Fi Channel: ");
Serial.println(WiFi.channel());

Инициализируйте ESP-NOW.

if (esp_now_init() != ESP_OK) {
  Serial.println("Error initializing ESP-NOW");
  return;
}

Зарегистрируйте функцию обратного вызова OnDataRecv, чтобы она выполнялась при поступлении нового пакета ESP-NOW.

esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));

Обработка запросов

Когда вы обращаетесь к IP-адресу ESP32 по корневому URL /, отправляется текст, хранящийся в переменной index_html, для построения веб-страницы.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html);
});

Источник событий сервера

Настройте источник событий на сервере.

events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
 );
  server.addHandler(&events);

Наконец, запустите сервер.

server.begin();

loop()

В loop() отправляйте ping каждые 5 секунд. Это используется для проверки на стороне клиента, работает ли ещё сервер.

static unsigned long lastEventTime = millis();
static const unsigned long EVENT_INTERVAL_MS = 5000;
if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
  events.send("ping",NULL,millis());
  lastEventTime = millis();
}

Следующая диаграмма показывает, как работают Server-sent Events в этом проекте и как обновляются значения без перезагрузки веб-страницы.

Обзор проекта ESP32 ESP-NOW веб-сервер с Server-Sent Events

После загрузки кода на плату-приёмник нажмите встроенную кнопку EN/RST. IP-адрес ESP32 должен быть выведен в Serial Monitor, а также канал Wi-Fi.

ESP-NOW получение IP-адреса ESP32 и канала Wi-Fi

Схема подключения ESP32 отправителя

Платы-отправители ESP32 подключены к датчику температуры и влажности DHT22. Вывод данных подключён к GPIO 4. Вы можете выбрать любой другой подходящий GPIO (прочитайте Руководство по выводам ESP32). Следуйте следующей схеме подключения.

Схема подключения ESP32 к датчику температуры и влажности DHT22

Код ESP32 отправителя (ESP-NOW)

Каждая плата-отправитель будет отправлять структуру через ESP-NOW, которая содержит идентификатор платы (чтобы вы могли определить, какая плата отправила показания), температуру, влажность и идентификатор чтения. Идентификатор чтения — это целое число для отслеживания количества отправленных сообщений.

ESP32 отправитель и приёмник с ESP-NOW с использованием Arduino IDE

Загрузите следующий код на каждую из ваших плат-отправителей. Не забудьте увеличить номер id для каждой платы-отправителя и вставить ваш SSID в переменную WIFI_SSID.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-esp-now-wi-fi-web-server/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 1

// Digital pin connected to the DHT sensor
#define DHTPIN 4

// Uncomment the type of sensor in use:
//#define DHTTYPE    DHT11     // DHT 11
#define DHTTYPE    DHT22     // DHT 22 (AM2302)
//#define DHTTYPE    DHT21     // DHT 21 (AM2301)

DHT dht(DHTPIN, DHTTYPE);

//MAC Address of the receiver
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

//Structure example to send data
//Must match the receiver structure
typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

esp_now_peer_info_t peerInfo;

//Create a struct_message called myData
struct_message myData;

unsigned long previousMillis = 0;   // Stores last time temperature was published
const long interval = 10000;        // Interval at which to publish sensor readings

unsigned int readingId = 0;

// Insert your SSID
constexpr char WIFI_SSID[] = "REPLACE_WITH_YOUR_SSID";

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
      for (uint8_t i=0; i<n; i++) {
          if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
              return WiFi.channel(i);
          }
      }
  }
  return 0;
}

float readDHTTemperature() {
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  // Read temperature as Celsius (the default)
  float t = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  //float t = dht.readTemperature(true);
  // Check if any reads failed and exit early (to try again).
  if (isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return 0;
  }
  else {
    Serial.println(t);
    return t;
  }
}

float readDHTHumidity() {
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  float h = dht.readHumidity();
  if (isnan(h)) {
    Serial.println("Failed to read from DHT sensor!");
    return 0;
  }
  else {
    Serial.println(h);
    return h;
  }
}

// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\nLast Packet Send Status:\t");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

void setup() {
  //Init Serial Monitor
  Serial.begin(115200);

  dht.begin();

  // Set device as a Wi-Fi Station and set channel
  WiFi.mode(WIFI_STA);

  int32_t channel = getWiFiChannel(WIFI_SSID);

  WiFi.printDiag(Serial); // Uncomment to verify channel number before
  esp_wifi_set_promiscuous(true);
  esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
  esp_wifi_set_promiscuous(false);
  WiFi.printDiag(Serial); // Uncomment to verify channel change after

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_register_send_cb(esp_now_send_cb_t(OnDataSent));

  // Register peer
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.encrypt = false;

  // Add peer
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    // Save the last time a new reading was published
    previousMillis = currentMillis;
    //Set values to send
    myData.id = BOARD_ID;
    myData.temp = readDHTTemperature();
    myData.hum = readDHTHumidity();
    myData.readingId = readingId++;

    //Send message via ESP-NOW
    esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
    if (result == ESP_OK) {
      Serial.println("Sent with success");
    }
    else {
      Serial.println("Error sending the data");
    }
  }
}

Просмотреть исходный код

Как работает код

Начните с подключения необходимых библиотек:

#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

Установка ID платы

Определите идентификатор платы-отправителя ESP32, например, установите BOARD_ID 1 для ESP32 Sender #1 и т.д.

// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 1

Датчик DHT

Определите пин, к которому подключён датчик DHT. В нашем примере он подключён к GPIO 4.

#define DHTPIN 4

Выберите тип используемого датчика DHT. Мы используем DHT22.

// Uncomment the type of sensor in use:
//#define DHTTYPE    DHT11     // DHT 11
#define DHTTYPE    DHT22     // DHT 22 (AM2302)
//#define DHTTYPE    DHT21     // DHT 21 (AM2301)

Создайте объект DHT на пине и типе, определённых ранее.

DHT dht(DHTPIN, DHTTYPE);

MAC-адрес приёмника

Вставьте MAC-адрес приёмника в следующую строку (например):

uint8_t broadcastAddress[] = {0x30, 0xAE, 0xA4, 0x15, 0xC7, 0xFC};

Структура данных

Затем создайте структуру, содержащую данные, которые мы хотим отправить. Структура struct_message содержит идентификатор платы, показание температуры, показание влажности и идентификатор чтения.

typedef struct struct_message {
    int id;
    float temp;
    float hum;
    int readingId;
} struct_message;

Создайте новую переменную типа struct_message с именем myData, которая хранит значения переменных.

struct_message myData;

Интервал таймера

Создайте вспомогательные переменные таймера для публикации показаний каждые 10 секунд. Вы можете изменить время задержки в переменной interval.

unsigned long previousMillis = 0;  // Stores last time temperature was published
const long interval = 10000;       // Interval at which to publish sensor readings

Инициализируйте переменную readingId — она отслеживает количество отправленных показаний.

unsigned int readingId = 0;

Смена канала Wi-Fi

Теперь мы получим канал Wi-Fi приёмника. Это полезно, потому что позволяет автоматически назначить тот же канал Wi-Fi плате-отправителю.

Для этого вы должны вставить ваш SSID в следующую строку:

constexpr char WIFI_SSID[] = "REPLACE_WITH_YOUR_SSID";

Затем функция getWiFiChannel() сканирует вашу сеть и получает её канал.

int32_t getWiFiChannel(const char *ssid) {
  if (int32_t n = WiFi.scanNetworks()) {
    for (uint8_t i=0; i<n; i++) {
      if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
        return WiFi.channel(i);
      }
    }
  }
  return 0;
}

Этот фрагмент кода был предложен Stephane (одним из наших читателей). Вы можете посмотреть его полный пример здесь.

Чтение температуры

Функция readDHTTemperature() считывает и возвращает температуру с датчика DHT. Если она не может получить показания температуры, возвращает 0.

float readDHTTemperature() {
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  // Read temperature as Celsius (the default)
  float t = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  //float t = dht.readTemperature(true);
  // Check if any reads failed and exit early (to try again).
  if (isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return 0;
  }
  else {
    Serial.println(t);
    return t;
  }
}

Чтение влажности

Функция readDHTHumidity() считывает и возвращает влажность с датчика DHT. Если она не может получить показания влажности, возвращает 0.

float readDHTHumidity() {
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  float h = dht.readHumidity();
  if (isnan(h)) {
    Serial.println("Failed to read from DHT sensor!");
    return 0;
  }
  else {
    Serial.println(h);
    return h;
  }
}

Примечание: чтобы узнать больше о получении температуры и влажности с датчиков DHT22 или DHT11, прочитайте: ESP32 с датчиком температуры и влажности DHT11/DHT22 с использованием Arduino IDE.

Функция обратного вызова OnDataSent

Функция обратного вызова OnDataSent() будет выполняться при отправке сообщения. В данном случае эта функция выводит информацию о том, было ли сообщение успешно доставлено или нет.

void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\nLast Packet Send Status:\t");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

setup()

Инициализируйте Serial Monitor.

Serial.begin(115200);

Установите ESP32 в режим Wi-Fi станции.

WiFi.mode(WIFI_STA);

Установите его канал таким же, как канал Wi-Fi приёмника:

int32_t channel = getWiFiChannel(WIFI_SSID);

WiFi.printDiag(Serial); // Uncomment to verify channel number before
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
WiFi.printDiag(Serial); // Uncomment to verify channel change after

Инициализируйте ESP-NOW.

if (esp_now_init() != ESP_OK) {
  Serial.println("Error initializing ESP-NOW");
  return;
}

После успешной инициализации ESP-NOW зарегистрируйте функцию обратного вызова, которая будет вызываться при отправке сообщения. В данном случае зарегистрируйте ранее созданную функцию OnDataSent().

esp_now_register_send_cb(OnDataSent);

Добавление пира

Для отправки данных на другую плату (приёмник) необходимо добавить её как пира. Следующие строки регистрируют и добавляют приёмник как пира.

// Register peer
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.encrypt = false;

// Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK){
  Serial.println("Failed to add peer");
  return;
}

loop()

В loop() проверьте, настало ли время получить и отправить новые показания.

unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
  // Save the last time a new reading was published
  previousMillis = currentMillis;

Отправка сообщения ESP-NOW

Наконец, отправьте структуру сообщения через ESP-NOW.

// Send message via ESP-NOW
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
if (result == ESP_OK) {
  Serial.println("Sent with success");
}
else {
  Serial.println("Error sending the data");
}

Рекомендуемое чтение: Начало работы с ESP-NOW (ESP32 с Arduino IDE)

Загрузите код на ваши платы-отправители. Вы должны заметить, что платы изменяют свой канал Wi-Fi на канал платы-приёмника. В нашем случае платы изменили свой номер канала Wi-Fi на 6.

Изменение канала Wi-Fi ESP-NOW ESP32

Демонстрация

После загрузки кода на все платы, если всё работает как ожидается, плата-приёмник ESP32 должна начать получать показания датчиков от других плат.

Демонстрация ESP32 ESP-NOW веб-сервер панель датчиков ESP-NOW и Wi-Fi показания датчиков

Откройте браузер в вашей локальной сети и введите IP-адрес ESP32.

Панель датчиков ESP32 ESP-NOW веб-сервер в браузере

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

Адаптивная мобильная панель датчиков ESP32 ESP-NOW веб-сервер

Заключение

В этом руководстве вы узнали, как использовать ESP-NOW и Wi-Fi для настройки веб-сервера, принимающего пакеты ESP-NOW от нескольких плат (конфигурация «многие к одному»).

Кроме того, вы также использовали Server-Sent Events для автоматического обновления веб-страницы каждый раз при получении нового пакета без перезагрузки веб-страницы.

Надеемся, вам понравился этот проект. Другие проекты/руководства, которые могут вам понравиться:

Мы хотим поблагодарить наших читателей, которые помогли улучшить это руководство, а именно Stephane Calderoni и Lee Davidson за их ценный вклад. Спасибо.


Источник: ESP32: ESP-NOW Web Server Sensor Dashboard (ESP-NOW + Wi-Fi)