ESP8266 и Arduino IDE. Часть 9: WebSocket
Примечание
Оригинал статьи: martyncurrey.com
Этот урок объясняет, как реализовать WebSocket-связь с микроконтроллером ESP8266 с использованием Arduino IDE. Рассматриваются фундаментальные концепции, практическая реализация и два полных рабочих примера.
Что такое WebSocket?
WebSocket обеспечивает «истинную асинхронную или двустороннюю связь, при которой любая сторона может отправлять данные в любое время без запроса». В отличие от HTTP и AJAX, которые создают новое соединение для каждого запроса, WebSocket поддерживает открытое соединение. Протокол работает поверх TCP и является частью спецификации HTML5, поддерживаемой всеми современными браузерами.
Требования
Используется библиотека arduinoWebSockets от Markus Sattler (Links2004).
Подключение:
#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();
}
}
Пример 2: Двусторонняя связь
Этот продвинутый пример добавляет аппаратный переключатель к ESP8266, позволяя управлять светодиодом как из веб-интерфейса, так и с физического переключателя. Веб-страница автоматически получает обновления статуса.
Схема подключения
Скетч: 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;
}
}
}
Ключевой момент — обработчик 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