Всем привет!
Если вы уже делали проекты на ESP32 или ESP8266 с веб-интерфейсом, то наверняка сталкивались с проблемой: страницу приходится постоянно обновлять, чтобы увидеть актуальные данные. Или кнопка срабатывает с задержкой, а обратной связи нет.
Классический HTTP работает по принципу запрос-ответ: клиент спрашивает, сервер отвечает. Для показаний датчиков это означает постоянные запросы к ESP каждые несколько секунд. Такой подход максимально прост, но нагружает микроконтроллер и тратит трафик.
WebSocket решает эту проблему раз и навсегда. Это двусторонний канал связи: после установки соединения и ESP, и браузер могут отправлять данные друг другу в любой момент. Показания с датчика изменились? ESP мгновенно отправит их в браузер. Нажали кнопку в интерфейсе? Команда моментально уйдёт на ESP.
Железо:
Софт:
Рассмотрим два разных сценария использования, 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
для работы с 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 адрес клиента
WS_EVT_CONNECT
- клиент подключилсяWS_EVT_DISCONNECT
- клиент отключилсяWS_EVT_DATA
- получены данныеWS_EVT_PONG
- ответ на pingWS_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) {
// Пришло всё сообщение за раз
}
}
}
Рассмотрим обратную ситуацию: 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();
}
}
А на сервере развернем 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. При перепечатке данного материала активная ссылка на первоисточник, не закрытая для индексации поисковыми системами, обязательна.
Комментарии