WebSocket на ESP32/8266: управляем устройствами в реальном времени

01 октября 2025

Всем привет!

Если вы уже делали проекты на ESP32 или ESP8266 с веб-интерфейсом, то наверняка сталкивались с проблемой: страницу приходится постоянно обновлять, чтобы увидеть актуальные данные. Или кнопка срабатывает с задержкой, а обратной связи нет.

Классический HTTP работает по принципу запрос-ответ: клиент спрашивает, сервер отвечает. Для показаний датчиков это означает постоянные запросы к ESP каждые несколько секунд. Такой подход максимально прост, но нагружает микроконтроллер и тратит трафик.

WebSocket решает эту проблему раз и навсегда. Это двусторонний канал связи: после установки соединения и ESP, и браузер могут отправлять данные друг другу в любой момент. Показания с датчика изменились? ESP мгновенно отправит их в браузер. Нажали кнопку в интерфейсе? Команда моментально уйдёт на ESP.

Что понадобится:

Железо:

  • ESP32 или ESP8266
  • Светодиод (например GPIO2)

Софт:

  • Arduino IDE с поддержкой плат ESP
  • Библиотека ESPAsyncWebServer
  • Библиотека AsyncTCP (для ESP32) или ESPAsyncTCP (для ESP8266)

Рассмотрим два разных сценария использования, esp как сервер и как клиент.

Сценарий 1: ESP как сервер

Создадим простой проект: веб-страница с кнопкой для управления светодиодом и отображением состояния в реальном времени.

#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#else
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

const char* ssid = "SSID";
const char* password = "ПАРОЛЬ";

#ifdef ARDUINO_ARCH_ESP8266
const int LED_PIN = LED_BUILTIN;
#define LED_ON LOW
#define LED_OFF HIGH
const int SENSOR_PIN = A0;
#else
const int LED_PIN = 2;
#define LED_ON HIGH
#define LED_OFF LOW
const int SENSOR_PIN = 34;
#endif

bool ledState = false;

const char htmlPage[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP WebSocket</title>
<style>
body { font-family: system-ui, -apple-system, Arial, sans-serif; max-width: 640px; margin: 48px auto; padding: 0 16px; background:#f5f5f7; }
.card { background:#fff; padding:24px; border-radius:12px; box-shadow:0 2px 10px rgba(0,0,0,.06); }
h1 { margin:0 0 12px; font-size:24px; }
.status { font-size:18px; padding:12px; border-radius:8px; margin:12px 0; text-align:center; }
.on { background:#2e7d32; color:#fff; }
.off { background:#c62828; color:#fff; }
button { width:100%; padding:14px 16px; font-size:16px; border:0; border-radius:10px; cursor:pointer; background:#1976d2; color:#fff; }
.meta { margin-top:12px; color:#444; font-size:14px; display:grid; grid-template-columns:120px 1fr; gap:4px 8px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
</style>
</head>
<body>
<div class="card">
<h1>ESP WebSocket</h1>
<div id="status" class="status off">Соединение...</div>
<button id="btn">Переключить светодиод</button>
<div class="meta">
<div>Соединение:</div><div id="conn" class="mono">нет</div>
<div>Данные:</div><div id="sensor" class="mono">—</div>
</div>
</div>
<script>
let ws=null, timer=null;
const $s=document.getElementById('status'),$c=document.getElementById('conn'),$d=document.getElementById('sensor'),$b=document.getElementById('btn');
function setConn(x){$c.textContent=x?'подключено':'отключено';$c.style.color=x?'green':'red'}
function reconnect(){ if(timer) return; timer=setTimeout(()=>{timer=null;connect()},3000) }
function connect(){
try{ ws=new WebSocket('ws://'+window.location.host+'/ws'); }catch(e){ reconnect(); return; }
ws.onopen=()=>setConn(true);
ws.onmessage=e=>{
const m=e.data||'';
if(m==='ON'){ $s.className='status on'; $s.textContent='Светодиод включён'; }
else if(m==='OFF'){ $s.className='status off'; $s.textContent='Светодиод выключён'; }
else if(m.startsWith('SENSOR:')){ $d.textContent=m.split(':')[1]; }
};
ws.onclose=()=>{ setConn(false); $s.className='status off'; $s.textContent='Соединение потеряно'; reconnect(); };
ws.onerror=()=>{};
}
$b.addEventListener('click',()=>{ if(ws&&ws.readyState===WebSocket.OPEN) ws.send('TOGGLE'); else alert('Нет соединения') });
connect();
</script>
</body>
</html>
)rawliteral";

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

void onWsEvent(AsyncWebSocket *srv, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){
if(type==WS_EVT_CONNECT){ client->text(ledState?"ON":"OFF"); return; }
if(type==WS_EVT_DATA){
AwsFrameInfo *info=(AwsFrameInfo*)arg;
if(info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT){
String msg; msg.reserve(len);
for(size_t i=0;i<len;i++) msg+=(char)data[i];
if(msg=="TOGGLE"){
ledState=!ledState;
digitalWrite(LED_PIN, ledState?LED_ON:LED_OFF);
ws.textAll(ledState?"ON":"OFF");
}
}
}
}

void setup(){
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_OFF);

WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while(WiFi.status()!=WL_CONNECTED){ delay(500); Serial.print('.'); }
Serial.println(); Serial.print("IP: "); Serial.println(WiFi.localIP());

ws.onEvent(onWsEvent);
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){ r->send_P(200, "text/html", htmlPage); });
server.on("/health", HTTP_GET, [](AsyncWebServerRequest *r){ r->send(200, "text/plain", "ok"); });
server.begin();
}

void loop(){
ws.cleanupClients();
static unsigned long last=0;
if(millis()-last>5000){
last=millis();
int sensor=analogRead(SENSOR_PIN);
ws.textAll(String("SENSOR:")+sensor);
}
}

Краткий обзор библиотеки AsyncWebSocket

Библиотека предоставляет класс AsyncWebSocket для работы с WebSocket соединениями.

Основные методы:

// Инициализация WebSocket
AsyncWebSocket ws("/ws");

// Отправка текстовых сообщений
ws.textAll("Сообщение всем");           // Всем клиентам
client->text("Сообщение одному");       // Конкретному клиенту
ws.text(clientId, "По ID");             // Клиенту по ID

// Отправка бинарных данных
ws.binaryAll(data, length);             // Всем клиентам
client->binary(data, length);           // Конкретному клиенту

// Управление клиентами
ws.count();                             // Количество подключённых
ws.cleanupClients();                    // Очистка отключившихся клиентов
ws.closeAll();                          // Отключить всех
client->close();                        // Отключить конкретного

// Информация о клиенте
client->id();                           // Уникальный ID
client->status();                       // Статус соединения
client->remoteIP();                     // IP адрес клиента

События WebSocket (AwsEventType):

  • WS_EVT_CONNECT - клиент подключился
  • WS_EVT_DISCONNECT - клиент отключился
  • WS_EVT_DATA - получены данные
  • WS_EVT_PONG - ответ на ping
  • WS_EVT_ERROR - произошла ошибка

Обработка данных:

void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_DATA) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;

// Проверяем тип данных
if (info->opcode == WS_TEXT) {
// Текстовые данные
data[len] = 0;
String message = (char*)data;
// здесь обработай message
} else if (info->opcode == WS_BINARY) {
// Бинарные данные
// обработай массив байт data длиной len
}

// Проверяем, пришли ли данные целиком
if (info->final && info->index == 0 && info->len == len) {
// Пришло всё сообщение за раз
}
}
}

Сценарий 2: ESP как WebSocket-клиент

Рассмотрим обратную ситуацию: ESP подключается к внешнему серверу и отправляет данные. Это полезно, когда нужно собирать данные с множества устройств на одном сервере.

Для этого используем библиотеку WebSocketsClient:

#ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <WebSocketsClient.h>
#include <ArduinoJson.h>

const char* ssid = "SSID";
const char* password = "ПАРОЛЬ";

const char* ws_host = "192.168.1.100";
const int ws_port = 8080;
const char* ws_path = "/ws";

#ifdef ARDUINO_ARCH_ESP8266
const int LED_PIN = LED_BUILTIN;
#define LED_ON LOW
#define LED_OFF HIGH
const int SENSOR_PIN = A0;
#else
const int LED_PIN = 2;
#define LED_ON HIGH
#define LED_OFF LOW
const int SENSOR_PIN = 34;
#endif

WebSocketsClient webSocket;
String deviceId;

void handleCommand(const char* json){
StaticJsonDocument<256> doc;
if(deserializeJson(doc, json)) return;
const char* cmd = doc["cmd"] | "";
if(!strcmp(cmd,"LED_ON")) digitalWrite(LED_PIN, LED_ON);
if(!strcmp(cmd,"LED_OFF")) digitalWrite(LED_PIN, LED_OFF);
}

void webSocketEvent(WStype_t type, uint8_t * payload, size_t length){
if(type==WStype_DISCONNECTED) return;
if(type==WStype_CONNECTED){
StaticJsonDocument<128> d; d["type"]="hello"; d["device"]=deviceId;
String out; serializeJson(d,out); webSocket.sendTXT(out);
return;
}
if(type==WStype_TEXT){
handleCommand((char*)payload);
return;
}
}

void sendSensorData(){
int sensor=analogRead(SENSOR_PIN);
StaticJsonDocument<256> doc;
doc["device"]=deviceId;
doc["temp"]=random(20,30);
doc["humidity"]=random(40,60);
doc["adc"]=sensor;
doc["timestamp"]=millis();
String out; serializeJson(doc,out);
webSocket.sendTXT(out);
}

void setup(){
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_OFF);

deviceId = WiFi.macAddress();
deviceId.replace(":", "");

WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while(WiFi.status()!=WL_CONNECTED){ delay(500); Serial.print('.'); }
Serial.println(); Serial.print("IP: "); Serial.println(WiFi.localIP());

webSocket.begin(ws_host, ws_port, ws_path);
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
webSocket.enableHeartbeat(15000, 3000, 2);
}

void loop(){
webSocket.loop();
static unsigned long last=0;
if(millis()-last>10000){
last=millis();
if(webSocket.isConnected()) sendSensorData();
}
}

WebSocket сервер на Python (asyncio + websockets)

А на сервере развернем Python сервер для приёма данных:

import asyncio, json
import websockets

connected = {} # device_id -> websocket

async def handle(ws, path):
if path != "/ws":
await ws.close(code=1008, reason="bad path")
return

device_id = None
try:
async for msg in ws:
try:
data = json.loads(msg)
except json.JSONDecodeError:
continue

if data.get("type") == "hello":
device_id = data.get("device") or "unknown"
connected[device_id] = ws
await ws.send(json.dumps({"status":"ok","message":"registered"}))
continue

if "temp" in data or "humidity" in data or "adc" in data:
t = data.get("temp")
if isinstance(t,(int,float)) and t > 28:
await ws.send(json.dumps({"cmd":"LED_ON"}))

except websockets.exceptions.ConnectionClosed:
pass
finally:
if device_id and connected.get(device_id) is ws:
del connected[device_id]

async def main():
server = await websockets.serve(handle, "0.0.0.0", 8080, max_size=2**20, ping_interval=20, ping_timeout=10)
print("ws://0.0.0.0:8080/ws")
await server.wait_closed()

if __name__ == "__main__":
asyncio.run(main())

Для запуска установите библиотеку:

pip3 install websockets

Архитектура:

При работе ESP как клиента получается такая архитектура:


Преимущества такой схемы:

  • Централизация: все данные собираются на один сервер
  • Масштабируемость: легко добавить новые устройства
  • Надёжность: при потере связи автоматически переподключается
  • Гибкость: сервер может управлять, рассылать команды

Полезные советы

Очистка клиентов. При работе в режиме сервера обязательно вызывайте ws.cleanupClients() в loop() это освобождает память от отключившихся клиентов.

Ограничение подключений. ESP32 может держать ~10 одновременных соединений, ESP8266 меньше. Для большего количества клиентов используйте ESP как клиента, подключающегося к внешнему серверу.

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

Безопасность. В реальных проектах нужна авторизация. Можно проверять токен при подключении или использовать WSS (WebSocket Secure) для шифрования.

Переподключение. Всегда используйте автоматическое переподключение, ведь сеть может пропасть в любой момент.

JSON vs текст. Для структурированных данных используйте JSON, так удобнее и читаемее. Для простых команд подойдёт обычный текст.


WebSocket на ESP это просто, быстро и эффективно. IoT проекты будут реагировать мгновенно, а возможность подключения к серверу позволяет строить полноценные системы мониторинга, например для умных городов!

Удачи в ваших проектах!


Данная статья является собственностью Amperkot.ru. При перепечатке данного материала активная ссылка на первоисточник, не закрытая для индексации поисковыми системами, обязательна.


Поделиться: