ESP8266 и Arduino IDE. Часть 9: WebSocket

Примечание

Оригинал статьи: martyncurrey.com

Этот урок объясняет, как реализовать WebSocket-связь с микроконтроллером ESP8266 с использованием Arduino IDE. Рассматриваются фундаментальные концепции, практическая реализация и два полных рабочих примера.

Что такое WebSocket?

WebSocket обеспечивает «истинную асинхронную или двустороннюю связь, при которой любая сторона может отправлять данные в любое время без запроса». В отличие от HTTP и AJAX, которые создают новое соединение для каждого запроса, WebSocket поддерживает открытое соединение. Протокол работает поверх TCP и является частью спецификации HTML5, поддерживаемой всеми современными браузерами.

Требования

Используется библиотека arduinoWebSockets от Markus Sattler (Links2004).

Библиотека WebSocket

Подключение:

#include <WebSocketsServer.h>

Инициализация сервера на определённом порту:

WebSocketsServer webSocket = WebSocketsServer(81);

Функция обработки событий

Основная функция обрабатывает события WebSocket с параметрами:

  • num: ID подключения клиента (максимум 5 одновременных подключений)

  • type: классификация события (0-10, включая ERROR, DISCONNECTED, CONNECTED, TEXT, BIN, PING, PONG)

  • payload: указатель на полученные данные

  • length: размер данных

Пример 1: Управление LED через WebSocket

Первый пример демонстрирует одностороннюю связь от веб-страницы к ESP8266 для управления светодиодом.

Схема

Светодиод с резистором подключён к пину D3.

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

Скетч: ESP8266_Part9_01_Websocket_LED

/*
 * Sketch: ESP8266_Part9_01_Websocket_LED
 * Intended to be run on an ESP8266
 */

String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";

String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body    { font-size:120%;}
    #main   { display: table; width: 300px; margin: auto;  padding: 10px 10px 10px 10px; border: 3px solid blue; border-radius: 10px; text-align:center;}
    .button { width:200px; height:40px; font-size: 110%;  }
  </style>
  <title>Websockets</title>
</head>
<body>
  <div id='main'>
    <h3>LED CONTROL</h3>
    <div id='content'>
      <p id='LED_status'>LED is off</p>
      <button id='BTN_LED'class="button">Turn on the LED</button>
    </div>
    <br />
   </div>
</body>

<script>
  var Socket;
  function init()
  {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
  }

  document.getElementById('BTN_LED').addEventListener('click', buttonClicked);
  function buttonClicked()
  {
    var btn = document.getElementById('BTN_LED')
    var btnText = btn.textContent || btn.innerText;
    if (btnText ==='Turn on the LED') { btn.textContent = "Turn off the LED"; document.getElementById('LED_status').textContent = 'LED is on';  sendText('1'); }
    else                              { btn.textContent = "Turn on the LED";  document.getElementById('LED_status').textContent = 'LED is off'; sendText('0'); }
  }

  function sendText(data)
  {
    Socket.send(data);
  }

  window.onload = function(e)
  {
    init();
  }
</script>
</html>
)=====";

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>

WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

byte pin_led = D3;

char ssid[] = "ssid";
char pass[] = "pass";

void setup()
{
  pinMode(pin_led, OUTPUT);
  digitalWrite(pin_led,LOW);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Serial started at 115200");
  Serial.println();

  Serial.print(F("Connecting to "));  Serial.println(ssid);
  WiFi.begin(ssid,pass);

  int count = 0;
  while ( (WiFi.status() != WL_CONNECTED) && count < 17)
  {
      Serial.print(".");  delay(500);  count++;
  }

  if (WiFi.status() != WL_CONNECTED)
  {
     Serial.println("");  Serial.print("Failed to connect to ");  Serial.println(ssid);
     while(1);
  }

  Serial.println("");
  Serial.println(F("[CONNECTED]"));   Serial.print("[IP ");  Serial.print(WiFi.localIP());
  Serial.println("]");

  server.begin();
  Serial.println("Server started");

  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}

void loop()
{
    webSocket.loop();

    WiFiClient client = server.available();
    if (!client)  {  return;  }

    client.flush();
    client.print( header );
    client.print( html_1 );
    Serial.println("New page served");

    delay(5);
}

void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          Serial.println("LED=off");
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          Serial.println("LED=on");
      }
  }

  else
  {
    Serial.print("WStype = ");   Serial.println(type);
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) { Serial.print((char) payload[i]); }
    Serial.println();
  }
}
Serial Monitor LED выключен - веб-интерфейс LED включён - веб-интерфейс

Пример 2: Двусторонняя связь

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

Демонстрация двусторонней связи

Схема подключения

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

Скетч: ESP8266_Part9_02_Websocket_LED_2Way

/*
 * Sketch: ESP8266_Part9_02_Websocket_LED_2Way
 * Intended to be run on an ESP8266
 */

String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";

String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body     { font-size:120%;}
    #main    { display: table; width: 300px; margin: auto;  padding: 10px 10px 10px 10px; border: 3px solid blue; border-radius: 10px; text-align:center;}
    #BTN_LED { width:200px; height:40px; font-size: 110%;  }
    p        { font-size: 75%; }
  </style>

  <title>Websockets</title>
</head>
<body>
  <div id='main'>
    <h3>LED CONTROL</h3>
    <div id='content'>
      <p id='LED_status'>LED is off</p>
      <button id='BTN_LED'class="button">Turn on the LED</button>
    </div>
    <p>Recieved data = <span id='rd'>---</span> </p>
    <br />
   </div>
</body>

<script>
  var Socket;
  function init()
  {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
    Socket.onmessage = function(event) { processReceivedCommand(event); };
  }


function processReceivedCommand(evt)
{
    document.getElementById('rd').textContent = evt.data;
    if (evt.data ==='0')
    {
        document.getElementById('BTN_LED').textContent = 'Turn on the LED';
        document.getElementById('LED_status').textContent = 'LED is off';
    }
    if (evt.data ==='1')
    {
        document.getElementById('BTN_LED').textContent = 'Turn off the LED';
        document.getElementById('LED_status').textContent = 'LED is on';
    }
}


  document.getElementById('BTN_LED').addEventListener('click', buttonClicked);
  function buttonClicked()
  {
    var btn = document.getElementById('BTN_LED')
    var btnText = btn.textContent || btn.innerText;
    if (btnText ==='Turn on the LED') { btn.textContent = 'Turn off the LED'; document.getElementById('LED_status').textContent = 'LED is on';  sendText('1'); }
    else                              { btn.textContent = 'Turn on the LED';  document.getElementById('LED_status').textContent = 'LED is off'; sendText('0'); }
  }

  function sendText(data)
  {
    Socket.send(data);
  }


  window.onload = function(e)
  {
    init();
  }
</script>


</html>
)=====";

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>

WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

byte pin_led = D3;
byte pin_switch = D6;

boolean LEDstatus = LOW;
boolean oldSwitchState = LOW;
boolean newSwitchState1 = LOW;
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;

char ssid[] = "ssid";
char pass[]= "password";

void setup()
{
  pinMode(pin_led, OUTPUT);
  digitalWrite(pin_led,LEDstatus);

  pinMode(pin_switch, INPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Serial started at 115200");
  Serial.println();

  Serial.print(F("Connecting to "));  Serial.println("ssid");
  WiFi.begin(ssid,pass);

  int count = 0;
  while ( (WiFi.status() != WL_CONNECTED) && count < 17)
  {
      Serial.print(".");  delay(500);  count++;
  }

  if (WiFi.status() != WL_CONNECTED)
  {
     Serial.println("");  Serial.print("Failed to connect to ");  Serial.println(ssid);
     while(1);
  }

  Serial.println("");
  Serial.println(F("[CONNECTED]"));   Serial.print("[IP ");  Serial.print(WiFi.localIP());
  Serial.println("]");

  server.begin();
  Serial.println("Server started");

  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}

void loop()
{
    checkSwitch();

    webSocket.loop();

    WiFiClient client = server.available();
    if (!client)  {  return;  }

    client.flush();
    client.print( header );
    client.print( html_1 );
    Serial.println("New page served");

    delay(5);
}

void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          LEDstatus = LOW;
          Serial.println("LED=off");
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          LEDstatus = HIGH;
          Serial.println("LED=on");
      }
  }

  else
  {
    Serial.print("WStype = ");   Serial.println(type);
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) { Serial.print((char) payload[i]); }
    Serial.println();
  }
}

void checkSwitch()
{
    newSwitchState1 = digitalRead(pin_switch);    delay(1);
    newSwitchState2 = digitalRead(pin_switch);    delay(1);
    newSwitchState3 = digitalRead(pin_switch);

    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        if ( newSwitchState1 != oldSwitchState )
        {
           if ( newSwitchState1 == HIGH )
           {
              LEDstatus = ! LEDstatus;

              if ( LEDstatus == HIGH ) { digitalWrite(pin_led, HIGH);  webSocket.broadcastTXT("1"); Serial.println("LED is ON"); }
              else                     { digitalWrite(pin_led, LOW);   webSocket.broadcastTXT("0"); Serial.println("LED is OFF"); }
           }
           oldSwitchState = newSwitchState1;
        }
   }
}
Serial Monitor двусторонней связи

Ключевой момент — обработчик onmessage: когда данные получены, мы знаем, что аппаратная кнопка на ESP8266 была нажата, и метки и текст кнопки на веб-странице обновляются для отражения нового статуса LED.

Сервер использует webSocket.broadcastTXT("1") или webSocket.broadcastTXT("0") для уведомления всех подключённых клиентов об изменении состояния.

Важные замечания

  • Необходимо дождаться события open (на стороне сайта) или WStype_CONNECTED (на стороне сервера) перед отправкой сообщений. В противном случае коммуникация не будет работать.

  • Для доступа из WAN (интернет) требуется проброс портов как для порта 80 (веб-страница), так и для порта 81 (WebSocket).

  • webSocket.loop() должен вызываться в основном цикле.

  • HTTP-сервер работает на порту 80; WebSocket — на порту 81.

Дополнительные ресурсы

  • MDN WebSocket API documentation

  • Tutorialspoint HTML5 WebSockets guide

  • Linode’s Introduction to WebSockets

  • Официальный GitHub-репозиторий arduinoWebSockets