Web Socket Server và Client với ESP8266

Trong bài viết này chúng ta sẽ tìm hiểu về WebSocket, cách để biến ESP8266 thành WebSocket Server và WebSocket Client. Việc sử dụng giao thức websocket sẽ có nhiều lợi ích cho các kết nối 2 chiều, luôn được duy trì và có độ trễ thấp. Chúng ta cùng tìm hiểu thôi

WebSocket Server là gì?

WebSoket là công nghệ hỗ trợ giao tiếp hai chiều giữa client và server bằng cách sử dụng một TCP socket để tạo một kết nối liên tục, hiệu quả và ít tốn kém. Mặc dù được thiết kế để chuyên sử dụng cho các ứng dụng web, ta vẫn có thể đưa chúng vào bất kì loại ứng dụng nào.
WebSockets mới xuất hiện trong HTML5, cho phép các kênh giao tiếp song song hai chiều và hiện đã được hỗ trợ trong nhiều trình duyệt. Kết nối được mở thông qua một HTTP request (yêu cầu HTTP), với những header đặc biệt thông báo cho Server (có hỗ trợ) chuyển sang kết nối Websocket. Kết nối này
được duy trì để bạn có thể gởi và nhận dữ liệu một cách liên tục, không đứt quãng, và không cần bất kỳ HTTP header (overhead) nào khác.
Websocket hỗ trợ cho các trình duyệt phổ biến hiện nay như: Google Chrome, Microsoft Edge, Internet Explorer, Firefox, Safari và Opera.

Ưu điểm

WebSockets cung cấp khả năng giao tiếp hai chiều với kết nối được duy trì, có độ trễ thấp, giúp Server dễ dàng giao tiếp với Client. Do đó, websocket sẽ phù hợp cho các ứng dụng real-time, người dùng sẽ không mất thời gian phải reload lại trình duyệt để cập nhật thông tin mới nhất như khi sử dụng giao thức HTTP.

Nhược điểm

Giao thức Websocket chưa được tất cả các trình duyệt đã có hiện nay hỗ trợ. Websocket cũng đòi hỏi các ứng dụng web trên server để hỗ trợ nó.

Biến ESP8266 thành WebSocket Server

Chúng ta sẽ viết chương trình sử dụng ESP8266 như 1 Websocket Server và trình
duyệt như là một Web Socket Client để cập nhật trạng thái nút nhấn, cũng như điều khiển đèn LED trên board NodeMCU theo thời gian thực thông qua Trình duyệt.

Yêu cầu

  • Khởi động 1 Webserver (có hỗ trợ Websocket) trên chip ESP8266.
  • Khi truy cập vào địa chỉ IP của ESP8266 sẽ trả về 1 file HTML bao gồm nội dung của đoạn Javascript thiết lập kết nối Websocket đến ESP8266 đồng thời lắng nghe các gói tin từ ESP8266 Server.
  • Khi nhấn nút trên board ESP8266 sẽ gởi nội dung trạng thái nút nhất đến Web Browser hiển thị checkbox, nhấn nút là check, không nhấn nút là uncheck.
  • Đồng thời khi nhấn checkbox trên trình duyệt sẽ thay đổi trạng thái đèn LED trên board.

Chuẩn bị

  • Phần cứng NodeMCU
  • Phần mềm 2 thư viện ESPAsyncWebServerESPAsyncTCP, 2 thư viện này đi với nhau dùng cho việc thiết lập HTTP server và websocket server cho ESP8266, và xử lí các sự kiện trên server-client

Chương trình

#include <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
const char* ssid = "SCKT";
const char* password = "huhuhu";
const int LED = 2;
const int BTN = 0;
// để đưa đoạn code HTML vào chương trình Arduino, cần chuyển đổi code HTML sang dạng char
const char index_html[] PROGMEM = ""
                                  "<!DOCTYPE HTML>"
                                  "<html>"
                                  "<head>"
                                  " <title>ESP8266 WebSocket</title>"
                                  "</head>"
                                  "<body>"
                                  " <div> Webscoket status <span id=\"status\" style=\"font-weight: bold;\"> disconnected </span> </div>"
                                  " <div> ESP8266 Button Status <input type=\"checkbox\" id=\"btn\" name=\"btn\" /> </div>"
                                  " <div> Control LED <input type=\"checkbox\" id=\"led\" name=\"led\" disabled=\"true\" /> </div>"
                                  " <script type=\"text/javascript\">"
                                  " var button = document.getElementById('btn');"
                                  " var led = document.getElementById('led');"
                                  " var status = document.getElementById('status');"
                                  " var url = window.location.host;"
                                  " var ws = new WebSocket('ws://' + url + '/ws');"
                                  " ws.onopen = function()"
                                  " {"
                                  " status.text = 'Connected';"
                                  " led.disabled = false;"
                                  " };"
                                  " ws.onmessage = function(evt)"
                                  " {"
                                  " if(evt.data == 'BTN_PRESSED') {"
                                  " button.checked = true;"
                                  " } else if(evt.data == 'BTN_RELEASE') {"
                                  " button.checked = false;"
                                  " }"
                                  " };"
                                  " ws.onclose = function() {"
                                  " led.disabled = true;"
                                  " status.text = 'Disconnected';"
                                  " };"
                                  " led.onchange = function() {"
                                  " var status = 'LED_OFF';"
                                  " if (led.checked) {"
                                  " status = 'LED_ON';"
                                  " }"
                                  " ws.send(status)"
                                  " }"
                                  " </script>"
                                  "</body>"
                                  "</html>";
AsyncWebServer server(8000);
AsyncWebSocket ws("/ws");
// Hàm xử lí sự kiện trên Server khi client là browser phát sự kiện
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data,
               size_t len) {
  if (type == WS_EVT_DATA && len > 0) { // type: loại sự kiện mà server nhận được. Nếu sự kiện nhận được là từ websocket thì bắt đầu xử lí
    data[len] = 0;
    String data_str = String((char*)data); // ép kiểu, đổi từ kiểu char sang String
    if (data_str == "LED_ON") {
      digitalWrite(LED, 0); // Khi client phát sự kiện "LED_ON" thì server sẽ bật LED
    } else if (data_str == "LED_OFF") {
      digitalWrite(LED, 1); // Khi client phát sự kiện "LED_OFF" thì server sẽ tắt LED
    }
  }
}
void setup()
{
  pinMode(LED, OUTPUT);
  pinMode(BTN, INPUT);
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.printf("STA: Failed!\n");
    WiFi.disconnect(false);
    delay(1000);
    WiFi.begin(ssid, password);
  }
  ws.onEvent(onWsEvent); // gọi hàm onWsEvent
  server.addHandler(&ws);
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send_P(200, "text/html", index_html); // trả về file index.html trên giao diện browser khi browser truy cập vào IP của server
  });
  server.begin(); // khởi động server
}
void loop()
{
  static bool isPressed = false;
  if (!isPressed && digitalRead(BTN) == 0) { //Nhấn nút nhấn GPIO0
    isPressed = true;
    ws.textAll("BTN_PRESSED");
  } else if (isPressed && digitalRead(BTN)) { //Nhả nút nhấn GPIO0
    isPressed = false;
    ws.textAll("BTN_RELEASE");
  }
}

Kết quả

Sau nạp code trên Arduino, ta vào browser, truy cập vào địa chỉ IP của ESP8266 đã trả về trên Serial Monitor cùng với port đã thiết lập trên server, ở trường hợp này là 192.168.137.122:8000, thao tác trên checkbox bạn sẽ thấy thay đổi trạng thái của LED

Websocket Client

Trong một số ứng dụng khác, chúng ta có 1 Server Websocket để thực hiện các tác vụ thời gian thực như Ứng dụng điện thoại, trình duyệt Web. ESP8266 có thể kết nối trực tiếp vào các server này như 1 Websocket Client để tiếp nhận, hoặc gởi thông tin thông qua Websocket.
Slack và Firebase đều là ví dụ điển hình cho Websocket. Chúng ta sẽ sử dụng Node.js để tự xây dựng 1 Web server, vừa đóng vai trò là 1 Websocket Server. Có những tính năng:

  • Có file index.html chứa các đoạn mã javascript tạo kết nối Websocket giữa trình duyệt với Server, giống như phần Server Nodejs
  • Cho phép kết nối Websocket đến, bao gồm từ trình duyệt, hay từ ESP8266
  • Server sẽ broadcast tất cả các gói tin từ bất kỳ 1 client nào gởi đến, tới tất cả các client còn lại.

Với tính năng như trên thì bạn có thể hình dung như sau: Nếu 1 cửa sổ trình duyệt có kết nối Websocket đến Server, khi nhấn 1 checkbox, thì sẽ gởi về server trạng thái của checkbox đó.

Ví dụ: LED_ON, server nhận được sẽ gởi dữ liệu LED_ON đến các client còn lại (gồm cả ESP8266), và client còn lại sẽ hiển thị trạng thái checkbox này là  đang bật.

Node.js Websocket Server

Ở phần trước chúng ta đã biến ESP8266 thành Websocket Server, nhúng toàn bộ website vào ESP8266, giờ thì chúng ta phải tạo một Websocket Server mới nằm trên desktop với Nodejs. Port thay đổi từ 8000 -> 3000 để khỏi đụng mấy port khác của máy

Chương trình file index.html

<!DOCTYPE HTML>
<html>
	<head>
		<title>ESP8266 WebSocket</title>
	</head>
	<body>
		<div> Webscoket status 
			<span id="status" style="font-weight: bold;"> disconnected </span>
		</div>
		<div> ESP8266 Button Status 
			<input type="checkbox" id="btn" name="btn" />
		</div>
		<div> Control LED 
			<input type="checkbox" id="led" name="led" disabled="true" />
		</div>
		<script type="text/javascript">
            var button = document.getElementById('btn');
            var led = document.getElementById('led');
            var url = window.location.host; // hàm trả về url của trang hiện tại kèm theo port
            var ws = new WebSocket('ws://' + url + '/ws'); // mở 1 websocket với port 8000
            console.log('connecting...')
            ws.onopen = function() //khi websocket được mở thì hàm này sẽ được thưc hiện
            {
                document.getElementById('status').innerHTML = 'Connected';
                led.disabled = false; //khi websocket được mở, mới cho phép
                console.log('connected...')
            };
            ws.onmessage = function(evt) // sự kiện xảy ra khi client nhận dữ liệu từ server
            {
                console.log(evt.data)
                if (evt.data == 'BTN_PRESSED') {
                    button.checked = true;
                } else if (evt.data == 'BTN_RELEASE') {
                    button.checked = false;
                } else if (evt.data == 'LED_OFF') {
                    led.checked = false;
                } else if (evt.data == 'LED_ON') {
                    led.checked = true;
                }
            };
            ws.onclose = function() { // hàm này sẽ được thực hiện khi đóng websocket
                led.disabled = true;
                document.getElementById('status').innerHTML = 'Connected';
            };
            led.onchange = function() { // thực hiện thay đổi bật/tắt led
                var led_status = 'LED_OFF';
                if (led.checked) {
                    led_status = 'LED_ON';
                }
                ws.send(led_status)
            }
            </script>
	</body>
</html>

Chương trình file server.js

var fs = require('fs');
var url = require('url');
var http = require('http');
var WebSocket = require('ws');
// function gửi yêu cầu(response) từ phía server hoặc nhận yêu cầu (request) của client gửi lên
function requestHandler(request, response) {
    fs.readFile('./index.html', function(error, content) {
        response.writeHead(200, {
            'Content-Type': 'text/html'
        });
        response.end(content);
    });
}
// create http server
var server = http.createServer(requestHandler);
var ws = new WebSocket.Server({
    server
});
var clients = [];

function broadcast(socket, data) {
    console.log(clients.length);
    for (var i = 0; i < clients.length; i++) {
        if (clients[i] != socket) {
            clients[i].send(data);
        }
    }
}
ws.on('connection', function(socket, req) {
    clients.push(socket);
    socket.on('message', function(message) {
        console.log('received: %s', message);
        broadcast(socket, message);
    });
    socket.on('close', function() {
        var index = clients.indexOf(socket);
        clients.splice(index, 1);
        console.log('disconnected');
    });
});
server.listen(3000);
console.log('Server listening on port 3000');

Start server với lệnh

npm install ws
npm install
nodemon server.js

ESP8266 Websocket Client

Sau khi có server rồi thì ta biến ESP8266 thành WebSocket Client với việc sử dụng thư viện arduinoWebSockets

Chương trình

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WebSocketsClient.h> //https://github.com/Links2004/arduinoWebSockets
WebSocketsClient webSocket;
const char* ssid = "SCKT"; //Đổi thành wifi của bạn
const char* password = "huhu123"; //Đổi pass luôn
const char* ip_host = "192.168.1.150"; //Đổi luôn IP host của PC nha
const uint16_t port = 3000; //Port thích đổi thì phải đổi ở server nữa
const int LED = 2;
const int BTN = 0;
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
    case WStype_DISCONNECTED:
      Serial.printf("[WSc] Disconnected!\n");
      break;
    case WStype_CONNECTED:
      {
        Serial.printf("[WSc] Connected to url: %s\n", payload);
      }
      break;
    case WStype_TEXT:
      Serial.printf("[WSc] get text: %s\n", payload);
      if (strcmp((char*)payload, "LED_ON") == 0) {
        digitalWrite(LED, 0); // Khi client phát sự kiện "LED_ON" thì server sẽ bật LED
      } else if (strcmp((char*)payload, "LED_OFF") == 0) {
        digitalWrite(LED, 1); // Khi client phát sự kiện "LED_OFF" thì server sẽ tắt LED
      }
      break;
    case WStype_BIN:
      Serial.printf("[WSc] get binary length: %u\n", length);
      break;
  }
}
void setup() {
  pinMode(LED, OUTPUT);
  pinMode(BTN, INPUT);
  Serial.begin(115200);
  Serial.println("ESP8266 Websocket Client");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  webSocket.begin(ip_host, port);
  webSocket.onEvent(webSocketEvent);
}
void loop() {
  webSocket.loop();
  static bool isPressed = false;
  if (!isPressed && digitalRead(BTN) == 0) { //Nhấn nút nhấn GPIO0
    isPressed = true;
    webSocket.sendTXT("BTN_PRESSED");
  } else if (isPressed && digitalRead(BTN)) { //Nhả nút nhấn GPIO0
    isPressed = false;
    webSocket.sendTXT("BTN_RELEASE");
  }
}

Kết quả

Chương trình bạn có thể tham khảo tại

hocarm/nodejs-socketio-tutorial
Contribute to hocarm/nodejs-socketio-tutorial development by creating an account on GitHub.

Tạm kết

Vậy là trong bài viết này chúng ta đã có thể biến ESP8266 thành một WebSocket Server để các client khác như browser có thể truy cập vào, ngoài ra ta cũng có thể biết được cách build một WebSocket Server với nodeJS và biến ESP8266 cũng như browser thành WebSocket Client để có thể kết nối vào server cũng như dùng server để điều khiển ESP8266 client sáng tắt LED. Tất cả đã sẵn sàng để chúng ta có hành trang đưa mọi thứ lên mây rồi.