ESP32 Websocket được sử dụng rất nhiều trong các ứng dụng Realtime, giúp các client có thể nhận được data một cách gần như tức thời. Phù hợp với nhiều ứng dụng khác nhau. Nhất là các ứng dụng điều khiển Smarthome, ……
Bài 3 Networking trong serie Lập trình ESP32 từ A tới Z
Websocket là gì?
WebSocket là một giao thức giao tiếp máy tính, hỗ trợ các channel giao tiếp full-duplex qua một kết nối TCP. Giao thức WebSocket được IETF chuẩn hóa RFC 6455 vào năm 2011. Hiện nay, API WebSocket trong Web IDL cũng đang được chuẩn hóa bởi W3C.
Sử dụng WebSockets, bạn có thể tạo một ứng dụng real-time đúng nghĩa như ứng dụng chat, phối hợp soạn thảo văn bản, giao dịch chứng khoán hay game online nhiều người chơi cùng lúc.
Trong ví dụ này chúng ta sẽ sử dụng websocket điều khiển led từ nhiều nguồn khác nhau: web browser, điện thoại, các máy tính khác nhau…
Lập trình ESP32 Websocket server
Chuẩn bị phần cứng
Chuẩn bị:
- ESP32 development board
- 2x 5mm LED
- 2x 330 Ohm trở
- Breadboard
- Dây cắm
Sơ đồ nguyên lý, tương tự 2 bài trước là ESP32 Webserver Station Mode và ESP32 Webserver AP Mode
Tạo một trang web sử dụng HTML và CSS
Chúng ta sẽ sử dụng các thẻ <head>, <body> để tạo đầu và thân trang.
Trong nội dung trang, thêm thẻ <style> để cấu hình màu sắc và kích thước các đối tượng sử dụng CSS
Thêm thẻ <script> để viết chương trình tạo websocket và xử lý
Chi tiết trong phần phía dưới
Tạo kết nối WebSocket viết bằng js
Để kết nối đến remote host, ta tạo một object WebSocket mới và truyền vào URL của endpoint đích.
var gateway = `ws://${window.location.hostname}/ws`;
Handshake
Khi tạo một kết nối WebSocket, bước đầu tiên là một handshake thông qua TCP mà ở đó client và server đều đồng ý để sử dụng giao thức WebSocket.
Kết nối WebSocket được khởi tạo bằng việc nâng cấp từ giao thức HTTP sang giao thức WebSocket trong quá trình handshake giữa client và server thông qua cùng một kết nối TCP. Header Upgrade được bao gồm trong request nhằm thông báo cho server rằng client muốn tạo lập một kết nối WebSocket.
Một khi được tạo lập các message WebSocket có thể được truyền đến và đi thông qua các method của WebSocket.
WebSocket Events
Bản chất bất đồng bộ của WebSocket có nghĩa là một khi một kết nối WebSocket được mở, ứng dụng sẽ lắng nghe những sự kiện. Để bắt đầu lắng nghe các sự kiện, bạn có thể thêm hàm callback vào object WebSocket hoặc sử dụng DOM method addEventListener() để thêm event listener.
Object WebSocket phát đi 4 sự kiện:
Open: Server phản hồi lại request để mở kết nối WebSocket. Event này thông báo rằng handshake thành công và kết nối Websocket được khởi tạo. Callback của sự kiện này là onopen.
Trong event này chúng ta chỉ in ra log: Connetion opened
function onOpen(event) { console.log('Connection opened'); }
Message: Server nhận được data chứa trong message từ client. Callback tương ứng của sự kiện này là onmessage.
Mỗi khi nhấn nút trên Browser nghĩa là Client đang gửi 1 message tới Server, sau khi sử lý dữ liệu trên Server, hàm này sẽ thay đổi Text Led1States và Led2States tương ứng với ON, OFF.
function onMessage(event) { var ledstate1; var ledstate2; if (event.data == "00"){ ledstate1 = "OFF"; ledstate2 = "OFF"; } else if(event.data == "01"){ ledstate1 = "OFF"; ledstate2 = "ON"; } else if(event.data == "10"){ ledstate1 = "ON"; ledstate2 = "OFF"; } else if(event.data == "11"){ ledstate1 = "ON"; ledstate2 = "ON"; } document.getElementById('state1').innerHTML = ledstate1; document.getElementById('state2').innerHTML = ledstate2; }
Error: Sự kiện xảy ra khi có bất kỳ lỗi nào trong kết nối WebSocket. Callback với sự kiện này là onerror.
Trong bài, mình không sử dụng sự kiện này
Load: Sự kiện xảy ra khi khởi tạo websocket, Callback với sự kiện này là onload()
Khi khởi tạo chúng ta sẽ khởi tạo websocket và nút nhấn trên web
function onLoad(event) { initWebSocket(); initButton(); }
Close: Kết nối được đóng lại. Callback tương ứng với sự kiện này là onclose.
function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); }
Các methods (phương thức) của WebSocket
WebSocket cung cấp 2 method:
send(): method send(data) truyền data trên kết nối. Nếu vì lý do nào đó, kết nối không tồn tại hoặc bị đóng, method này sẽ trả về exception về tình trạng không hợp lệ của kết nối.
websocket.send('toggle1');
close(): Method close() dùng để đóng kết nối hiện tại. Nếu kết nối đã bị đóng từ trước đó (có thể do lỗi kết nối), nó không gây ảnh hưởng gì. Method này nhận hai argument tùy chọn: code (dạng số) và reason (dạng text)
Ngoài ra trong bài này chúng ta còn sử dụng các hàm khởi tạo và thay đổi giái trị ON,OFF cho Led
function initButton() { document.getElementById('button1').addEventListener('click', toggle1); document.getElementById('button2').addEventListener('click', toggle2); } function toggle1(){ websocket.send('toggle1'); } function toggle2(){ websocket.send('toggle2'); }
Xử lý thông tin từ Client gửi lên Server
Trong code ESP32 chúng ta sẽ tạo ra các hàm Handle (xử lý) các sự kiện mà Client tạo ra.
Đầu tiên chúng ta sẽ khởi tạo các giá trị đèn led, wifi và include các thư viện.
Cài đặt các thư viện:
Chi tiết cách cài đặt thì các bạn đọc bài: Cài đặt platform io nhé!
Trong Setup chúng ta sẽ khởi tạo các chân led, kết nối tới wifi
Tiếp tới, khởi tạo webserver websocket. Sử dụng trang index_html đã code bên trên và hàm processer để xử lý dữ liệu
Trong loop chúng ta làm mới các client và ghi giá trị ra led 1 và 2.
Một số hàm tự tạo để xử lý như.
notifyClients
: trả về giá trị của led và gửi về các clienthandleWebSocketMessage
: Xử lý message của client, nếu có lệnh toggle1 và toggle 2 sẽ đảo trạng thái led và dùng notifyClients để gửi về client trạng thái led.onEvent
để xử lý các sự kiện khi kết nối websocketinitWebSocket
để khởi tạoprocessor
để sửa giá trị của STATES1 và STATES 2 trên webFull code
#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> // Replace with your network credentials const char* ssid = "Nha Tao"; const char* password = "25251325"; bool led1State = 0; bool led2State = 0; const int led1Pin = 26; const int led2Pin = 27; // 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 Websocket Web Server Websocket</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="data:,"> <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>Khue Nguyen Creator</h2> <p class="state1">LED1: <span id="state1">%STATE1%</span></p> <p><button id="button1" class="button1">BUTTON1</button></p> <p class="state1">LED2: <span id="state2">%STATE2%</span></p> <p><button id="button2" class="button2">BUTTON2</button></p> </div> </div> </body> </html> )rawliteral"; void notifyClients() { ws.textAll(String(led1State)+String(led2State)); } 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, "toggle1") == 0) { led1State = !led1State; notifyClients(); } if (strcmp((char*)data, "toggle2") == 0) { led2State = !led2State; 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 == "STATE1"){ if (led1State){ return "ON"; } else{ return "OFF"; } } if(var == "STATE2"){ if (led2State){ return "ON"; } else{ return "OFF"; } } } void setup(){ // Serial port for debugging purposes Serial.begin(115200); pinMode(led1Pin, OUTPUT); digitalWrite(led1Pin, LOW); pinMode(led2Pin, OUTPUT); digitalWrite(led2Pin, 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_P(200, "text/html", index_html, processor); }); // Start server server.begin(); } void loop() { ws.cleanupClients(); digitalWrite(led1Pin, led1State); digitalWrite(led2Pin, led2State); }
May là từ web sang :v