ESP8266 NodeMCU WebSocket сервер: управление выходами (Arduino IDE)

В этом руководстве вы узнаете, как создать веб-сервер на ESP8266 с использованием протокола связи WebSocket. В качестве примера мы покажем, как создать веб-страницу для удалённого управления выходами ESP8266. Состояние выхода отображается на веб-странице и автоматически обновляется у всех клиентов.

ESP8266 NodeMCU WebSocket сервер управление выходами Arduino IDE

ESP8266 будет программироваться с использованием Arduino IDE и ESPAsyncWebServer. У нас также есть аналогичное руководство по WebSocket для ESP32.

Если вы следили за некоторыми из наших предыдущих проектов веб-серверов, таких как этот, вы, возможно, заметили, что если у вас открыто несколько вкладок (на одном или на разных устройствах) одновременно, состояние не обновляется автоматически во всех вкладках, пока вы не обновите веб-страницу. Чтобы решить эту проблему, мы можем использовать протокол WebSocket – все клиенты могут быть уведомлены при изменении и обновить веб-страницу соответствующим образом.

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

Введение в WebSocket

WebSocket – это постоянное соединение между клиентом и сервером, которое обеспечивает двунаправленную связь между обеими сторонами с использованием TCP-соединения. Это означает, что вы можете отправлять данные от клиента к серверу и от сервера к клиенту в любой момент времени.

Схема работы WebSocket сервера ESP32 ESP8266

Клиент устанавливает WebSocket-соединение с сервером через процесс, известный как рукопожатие WebSocket (WebSocket handshake). Рукопожатие начинается с HTTP-запроса/ответа, что позволяет серверам обрабатывать HTTP-соединения и WebSocket-соединения на одном и том же порту. Как только соединение установлено, клиент и сервер могут обмениваться данными WebSocket в режиме полного дуплекса.

С помощью протокола WebSocket сервер (плата ESP8266) может отправлять информацию клиенту или всем клиентам без запроса. Это также позволяет нам отправлять информацию в веб-браузер при возникновении изменения.

Это изменение может быть чем-то, что произошло на веб-странице (вы нажали кнопку), или чем-то, что произошло на стороне ESP8266, например, нажатие физической кнопки в цепи.

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

Вот веб-страница, которую мы создадим для этого проекта.

Обзор проекта ESP8266 WebSocket сервер переключение выходов
  • Веб-сервер ESP8266 отображает веб-страницу с кнопкой для переключения состояния GPIO 2;

  • Для простоты мы управляем GPIO 2 – встроенным светодиодом. Вы можете использовать этот пример для управления любым другим GPIO;

  • Интерфейс показывает текущее состояние GPIO. При любом изменении состояния GPIO интерфейс обновляется мгновенно;

  • Состояние GPIO обновляется автоматически у всех клиентов. Это означает, что если у вас открыто несколько вкладок браузера на одном устройстве или на разных устройствах, все они обновляются одновременно.

Как это работает?

На следующем изображении показано, что происходит при нажатии на кнопку «Toggle».

ESP8266 NodeMCU WebSocket веб-сервер обновление всех клиентов как это работает

Вот что происходит, когда вы нажимаете на кнопку «Toggle»:

  1. Нажатие на кнопку «Toggle»;

  2. Клиент (ваш браузер) отправляет данные по протоколу WebSocket с сообщением «toggle»;

  3. ESP8266 (сервер) получает это сообщение, поэтому он знает, что нужно переключить состояние светодиода. Если светодиод был выключен, включить его;

  4. Затем он отправляет данные с новым состоянием светодиода всем клиентам также по протоколу WebSocket;

  5. Клиенты получают сообщение и обновляют состояние светодиода на веб-странице соответственно. Это позволяет нам обновлять все клиенты практически мгновенно при изменении.

Подготовка Arduino IDE

Мы будем программировать плату ESP8266 с использованием Arduino IDE, поэтому убедитесь, что она установлена в вашей Arduino IDE.

Установка библиотек – Async Web Server

Мы создадим веб-сервер, используя следующие библиотеки:

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

Код для ESP8266 NodeMCU WebSocket сервера

Скопируйте следующий код в вашу Arduino IDE.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp8266-nodemcu-websocket-server-arduino/
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

// Import required libraries
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

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

bool ledState = 0;
const int ledPin = 2;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   /*.button:hover {background-color: #0f8b8d}*/
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>ESP WebSocket Server</h1>
  </div>
  <div class="content">
    <div class="card">
      <h2>Output - GPIO 2</h2>
      <p class="state">state: <span id="state">%STATE%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }
  function onLoad(event) {
    initWebSocket();
    initButton();
  }
  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>
)rawliteral";

void notifyClients() {
  ws.textAll(String(ledState));
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
             void *arg, uint8_t *data, size_t len) {
    switch (type) {
      case WS_EVT_CONNECT:
        Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
        break;
      case WS_EVT_DISCONNECT:
        Serial.printf("WebSocket client #%u disconnected\n", client->id());
        break;
      case WS_EVT_DATA:
        handleWebSocketMessage(arg, data, len);
        break;
      case WS_EVT_PONG:
      case WS_EVT_ERROR:
        break;
    }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
  return String();
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

  initWebSocket();

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

  // Start server
  server.begin();
}

void loop() {
  ws.cleanupClients();
  digitalWrite(ledPin, ledState);
}

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

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

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

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

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

Импорт библиотек

Импортируйте необходимые библиотеки для создания веб-сервера.

#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

Сетевые учётные данные

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

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

Выход GPIO

Создайте переменную ledState для хранения состояния GPIO и переменную ledPin, которая ссылается на GPIO, которым вы хотите управлять. В данном случае мы будем управлять встроенным светодиодом (который подключён к GPIO 2).

bool ledState = 0;
const int ledPin = 2;

AsyncWebServer и AsyncWebSocket

Создайте объект AsyncWebServer на порту 80.

AsyncWebServer server(80);

Библиотека ESPAsyncWebServer включает плагин WebSocket, который упрощает обработку WebSocket-соединений. Создайте объект AsyncWebSocket с именем ws для обработки соединений по пути /ws.

AsyncWebSocket ws("/ws");

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

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

Примечание: мы размещаем всё необходимое для создания веб-страницы в переменной index_html, которую используем в скетче Arduino. Обратите внимание, что может быть более практично иметь отдельные файлы HTML, CSS и JavaScript, которые затем загружаются в файловую систему ESP8266 и на которые ссылаются в коде.

Рекомендуемое чтение: ESP8266 Web Server using SPIFFS (SPI Flash File System)

Вот содержимое переменной index_html:

<!DOCTYPE HTML>
<html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
  <div class="topnav">
    <h1>ESP WebSocket Server</h1>
  </div>
  <div class="content">
    <div class="card">
      <h2>Output - GPIO 2</h2>
      <p class="state">state: <span id="state">%STATE%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }

  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }

  window.addEventListener('load', onLoad);

  function onLoad(event) {
    initWebSocket();
    initButton();
  }

  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }

  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>

CSS

Между тегами <style></style> мы добавляем стили для оформления веб-страницы с помощью CSS. Вы можете изменить их по своему желанию, чтобы веб-страница выглядела так, как вам нравится. Мы не будем объяснять, как работает CSS для этой веб-страницы, поскольку это не относится к данному руководству по WebSocket.

<style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2 {
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
 </style>

HTML

Между тегами <body></body> мы добавляем содержимое веб-страницы, видимое пользователю.

<div class="topnav">
  <h1>ESP WebSocket Server</h1>
</div>
<div class="content">
  <div class="card">
    <h2>Output - GPIO 2</h2>
    <p class="state">state: <span id="state">%STATE%</span></p>
    <p><button id="button" class="button">Toggle</button></p>
  </div>
</div>

Есть заголовок 1 с текстом «ESP WebSocket Server». Вы можете изменить этот текст по своему усмотрению.

<h1>ESP WebSocket Server</h1>

Затем есть заголовок 2 с текстом «Output – GPIO 2».

<h2>Output - GPIO 2</h2>

После этого у нас есть абзац, отображающий текущее состояние GPIO.

<p class="state">state: <span id="state">%STATE%</span></p>

%STATE% – это заполнитель для состояния GPIO. Он будет заменён текущим значением ESP8266 в момент отправки веб-страницы. Заполнители в HTML-тексте должны быть заключены между знаками %. Это означает, что текст %STATE% подобен переменной, которая затем будет заменена фактическим значением.

После отправки веб-страницы клиенту состояние должно динамически изменяться при любом изменении состояния GPIO. Мы будем получать эту информацию по протоколу WebSocket. Затем JavaScript обрабатывает полученную информацию и обновляет состояние соответствующим образом. Чтобы иметь возможность обработать этот текст с помощью JavaScript, текст должен иметь идентификатор, на который мы можем ссылаться. В данном случае идентификатор – state (<span id="state">).

Наконец, есть абзац с кнопкой для переключения состояния GPIO.

<p><button id="button" class="button">Toggle</button></p>

Обратите внимание, что мы присвоили кнопке идентификатор (id="button").

JavaScript – обработка WebSocket

JavaScript размещается между тегами <script></script>. Он отвечает за инициализацию WebSocket-соединения с сервером, как только веб-интерфейс полностью загружен в браузере, и за обмен данными через WebSocket.

<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }

  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }

  window.addEventListener('load', onLoad);

  function onLoad(event) {
    initWebSocket();
    initButton();
  }

  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }

  function toggle(){
    websocket.send('toggle');
  }
</script>

Давайте рассмотрим, как это работает.

Gateway – это точка входа в интерфейс WebSocket.

var gateway = `ws://${window.location.hostname}/ws`;

window.location.hostname получает текущий адрес страницы (IP-адрес веб-сервера).

Создайте новую глобальную переменную с именем websocket.

var websocket;

Добавьте обработчик событий, который будет вызывать функцию onload при загрузке веб-страницы.

window.addEventListener('load', onload);

Функция onload() вызывает функцию initWebSocket() для инициализации WebSocket-соединения с сервером и функцию initButton() для добавления обработчиков событий к кнопкам.

function onload(event) {
  initWebSocket();
  initButton();
}

Функция initWebSocket() инициализирует WebSocket-соединение по ранее определённому шлюзу (gateway). Мы также назначаем несколько функций обратного вызова для случаев, когда WebSocket-соединение открывается, закрывается или когда получено сообщение.

function initWebSocket() {
  console.log('Trying to open a WebSocket connection...');
  websocket = new WebSocket(gateway);
  websocket.onopen    = onOpen;
  websocket.onclose   = onClose;
  websocket.onmessage = onMessage;
}

Когда соединение открывается, мы просто выводим сообщение в консоль и отправляем сообщение «hi». ESP8266 получает это сообщение, поэтому мы знаем, что соединение было инициализировано.

function onOpen(event) {
  console.log('Connection opened');
  websocket.send('hi');
}

Если по какой-то причине WebSocket-соединение закрывается, мы снова вызываем функцию initWebSocket() через 2000 миллисекунд (2 секунды).

function onClose(event) {
  console.log('Connection closed');
  setTimeout(initWebSocket, 2000);
}

Метод setTimeout() вызывает функцию или вычисляет выражение после указанного количества миллисекунд.

Наконец, нам нужно обработать, что происходит при получении нового сообщения. Сервер (ваша плата ESP) будет отправлять либо «1», либо «0». В соответствии с полученным сообщением мы хотим отобразить «ON» или «OFF» в абзаце, показывающем состояние. Помните тег <span> с id="state"? Мы получим этот элемент и установим его значение в ON или OFF.

function onMessage(event) {
  var state;
  if (event.data == "1"){
    state = "ON";
  }
  else{
    state = "OFF";
  }
  document.getElementById('state').innerHTML = state;
}

Функция initButton() получает кнопку по её идентификатору (button) и добавляет обработчик событий типа 'click'.

function initButton() {
  document.getElementById('button').addEventListener('click', toggle);
}

Это означает, что при нажатии на кнопку вызывается функция toggle.

Функция toggle отправляет сообщение через WebSocket-соединение с текстом 'toggle'.

function toggle(){
  websocket.send('toggle');
}

Затем ESP8266 должен обработать, что происходит при получении этого сообщения – переключить текущее состояние GPIO.

Обработка WebSocket – серверная сторона

Ранее вы видели, как обрабатывать WebSocket-соединение на стороне клиента (браузера). Теперь давайте рассмотрим, как обрабатывать его на стороне сервера.

Уведомление всех клиентов

Функция notifyClients() уведомляет всех клиентов сообщением, содержащим то, что вы передадите в качестве аргумента. В данном случае мы хотим уведомить всех клиентов о текущем состоянии светодиода при каждом изменении.

void notifyClients() {
  ws.textAll(String(ledState));
}

Класс AsyncWebSocket предоставляет метод textAll() для одновременной отправки одного и того же сообщения всем клиентам, подключённым к серверу.

Обработка сообщений WebSocket

Функция handleWebSocketMessage() – это функция обратного вызова, которая будет выполняться каждый раз, когда мы получаем новые данные от клиентов по протоколу WebSocket.

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}

Если мы получаем сообщение «toggle», мы переключаем значение переменной ledState. Кроме того, мы уведомляем всех клиентов, вызывая функцию notifyClients(). Таким образом, все клиенты уведомляются об изменении и обновляют интерфейс соответствующим образом.

if (strcmp((char*)data, "toggle") == 0) {
  ledState = !ledState;
  notifyClients();
}

Настройка сервера WebSocket

Теперь нам нужно настроить обработчик событий для обработки различных асинхронных шагов протокола WebSocket. Этот обработчик событий можно реализовать, определив onEvent() следующим образом:

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
 void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
      break;
  }
}

Аргумент type представляет событие, которое произошло. Он может принимать следующие значения:

  • WS_EVT_CONNECT – когда клиент подключился;

  • WS_EVT_DISCONNECT – когда клиент отключился;

  • WS_EVT_DATA – когда получен пакет данных от клиента;

  • WS_EVT_PONG – в ответ на запрос ping;

  • WS_EVT_ERROR – когда получена ошибка от клиента.

Инициализация WebSocket

Наконец, функция initWebSocket() инициализирует протокол WebSocket.

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

processor()

Функция processor() отвечает за поиск заполнителей в HTML-тексте и замену их нужными значениями перед отправкой веб-страницы в браузер. В нашем случае мы заменим заполнитель %STATE% на ON, если ledState равно 1. В противном случае заменим его на OFF.

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
}

setup()

В setup() инициализируем Serial Monitor для отладки.

Serial.begin(115200);

Настроим ledPin как OUTPUT и установим его в LOW при первом запуске программы.

pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

Инициализируем Wi-Fi и выведем IP-адрес ESP8266 в Serial Monitor.

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi..");
}

// Print ESP Local IP Address
Serial.println(WiFi.localIP());

Инициализируем протокол WebSocket, вызвав ранее созданную функцию initWebSocket().

initWebSocket();

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

Отправляем текст, сохранённый в переменной index_html, при получении запроса по корневому URL / – вам нужно передать функцию processor в качестве аргумента для замены заполнителей текущим состоянием GPIO.

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

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

server.begin();

loop()

Светодиод будет физически управляться в loop().

void loop() {
  ws.cleanupClients();
  digitalWrite(ledPin, ledState);
}

Обратите внимание, что мы также вызываем метод cleanupClients(). Вот почему (объяснение со страницы библиотеки ESPAsyncWebServer на GitHub):

Браузеры иногда не закрывают WebSocket-соединение корректно, даже когда функция close() вызывается в JavaScript. В конечном итоге это исчерпает ресурсы веб-сервера и приведёт к его сбою. Периодический вызов функции cleanupClients() из основного loop() ограничивает количество клиентов, закрывая самого старого клиента при превышении максимального количества клиентов. Эту функцию можно вызывать каждый цикл, однако, если вы хотите использовать меньше энергии, достаточно вызывать её раз в секунду.

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

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

После загрузки кода откройте Serial Monitor на скорости 115200 бод и нажмите встроенную кнопку EN/RST. Должен отобразиться IP-адрес ESP.

Откройте браузер в вашей локальной сети и введите IP-адрес ESP8266. Вы должны получить доступ к веб-странице для управления выходом.

Обзор проекта ESP8266 WebSocket сервер переключение выходов

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

Заключение

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

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

В этом примере мы показали, как управлять одним GPIO ESP8266. Вы можете использовать этот метод для управления большим количеством GPIO. Вы также можете использовать протокол WebSocket для отправки показаний датчиков или уведомлений в любой момент времени.

Мы надеемся, что это руководство было для вас полезным. Мы намерены создать больше руководств и примеров с использованием протокола WebSocket. Так что следите за обновлениями.

Узнайте больше о ESP8266 с нашими ресурсами: