WebSocket ESP32 điều khiển đèn LED từ xa

Trong bài viết này, mình sẽ hướng dẫn bạn cách xây dựng Web Server thông qua giao thức WebSocket ESP32. Cụ thể, chúng ta có thể điều khiển đèn LED theo thời gian thực thông qua Website từ xa. Trên trang Web này sẽ hiển thị trạng thái đèn LED (output) đang bật hay tắt real time, chúng sẽ tự động cập nhật tại các máy khách.

Bạn sẽ lập trình ESP32 bằng Arduino IDE và ESPAsyncWebServer. Giao thức WebSocket ESP32 cho phép tất cả các máy khách đều có thể nhận được thông báo khi trạng thái output thay đổi và tự động cập nhật trên trang.

WebSocket ESP32 là gì?

Bạn có thể hiểu đơn giản, WebSocket ESP32 là một giao thức cho phép máy khách (Client) và máy chủ (Server) kết nối 2 chiều liên tục với nhau thông qua TCP. Do đó, chúng ta có thể gửi dữ liệu từ Client đến Server vào bất kỳ lúc nào.

WebSocket ESP32 là gì?
WebSocket ESP32 là gì?

Máy khách thiết lập kết nối WebSocket với máy chủ thông qua một quá trình có tên làWebSocket handshake (giao thức bắt tay). Quá trình này sử dụng mô hình gửi yêu cầu / phản hồi HTTP, các máy chủ xử lý yêu cầu HTTP và kết nối WebSocket trên cùng một cổng.

Sau khi kết nối thành công, cả máy chủ và máy khách đều có thể gửi dữ liệu WebSocket song song với nhau.

Khi sử dụng WebSocket, các mạch ESP32 có thể gửi thông tin trực tiếp đến máy chủ hoặc máy khách mà không cần phải yêu cầu. Điều này giúp chúng ta có thể gửi thông tin tới Website khi có thay đổi, ví dụ như khi bạn nhấn nút trên ESP32 và trạng thái đèn LED thay đổi, hoặc khi nhấn nút trên trang Web để bật / tắt đèn LED.

Giới thiệu dự án bật tắt đèn LED Real Time bằng WebSocket

Dưới đây là hình ảnh minh họa trang Web mà chúng ta sẽ xây dựng trong dự án này:

Điều khiển đèn LED real time với ESP32 qua WebSocket Server
Điều khiển đèn LED real time với ESP32 qua WebSocket Server

Cụ thể sẽ như sau:

  • ESP32 Web Server hiển thị một trang Web cho phép chúng ta thay đổi trạng thái output của cổng GPIO 2 (đây cũng là cổng mà mình kết nối với đèn LED trên board mạch. Bạn có thể lựa chọn bất kỳ GPIO nào khác để điều khiển nhé!)
  • Trên Web Server hiển thị trạng thái hiện tại của đèn LED (đang bật hay tắt). Khi có bất kỳ sự thay đổi trạng thái nào của đèn LED, chúng sẽ được cập nhật ngay lập tức trên giao diện
  • Trạng thái GPIO này được tự động cập nhật trên tất cả các máy khách. Điều này đồng nghĩa với việc nếu bạn mở cùng lúc nhiều tab trên cùng một thiết bị laptop hoặc trên nhiều thiết bị khác nhau thì chúng đều được cập nhật cùng lúc và theo thời gian thực.

Cách hệ thống hoạt động

Sau khi bạn nhấn vào nút thay đổi trạng thái đèn LED, đây chính xác là những gì diễn ra sau đó:

Cách hệ thống WebSocket ESP32 hoạt động
Cách hệ thống WebSocket ESP32 hoạt động

Cụ thể:

  • Bạn click nào nút thay đổi trạng thái trên Web Server của máy khách
  • Máy khách bạn đang dùng (chính xác là trình duyệt của bạn) gửi dữ liệu thông qua giao thức WebSocket Server với thông báo “toggle” (chuyển đổi)
  • Mạch ESP32 nhận thông tin này, và chúng sẽ thay đổi trạng thái đèn LED ngay lập tức. Nếu đèn LED đang tắt, ESP32 sẽ bật LED và ngược lại
  • Mạch ESP32 gửi trạng thái mới nhất của đèn LED lên tất cả các máy khách thông qua giao thức WebSocket Server
  • Tất cả các máy khách nhận được thông tin và cập nhật trạng thái đèn LED trên trang Web tương ứng hầu như là ngay lập tức theo thời gian thực

Dưới đây, bạn hãy cùng mình thực hiện dự án WebSocket ESP32 trên nhé!

Chuẩn bị

Cài ESP32 trong Arduino IDE

Chúng ta sẽ lập trình mạch ESP32 thông qua Arduino IDE. Do đó, bạn hãy cài đặt tiện ích này trong Arduino trước nhé! Link hướng dẫn: Cách lập trình ESP32 bằng Arduino IDE (Windows, Linux, Mac OS X)

Cài đặt thư viện Async Web Server

Để xây dựng Web Server, chúng ta sẽ sử dụng ESPAsyncWebServer. Tuy nhiên, để thư viện này hoạt động bình thường, bạn cần tải thêm thư viện AsyncTCP. Dưới đây mình có để sẵn đường link cho bạn:

Trên trình quản lý thư viện Arduino không có sẵn các thứ viện này. Do đó, khi tải về xong thì bạn cần sao chép chúng vào thư mục Arduino Installation Libraries (thư viện cài đặt Arduino) nhé! Hoặc bạn có thể truy cập vào Sketch Include Library > Add .zip Library để thêm thư viện vừa tải xuống đều được.

Lập trình

Dưới đây là đoạn code, bạn chỉ cần thay đổi thông tin mạng WiFi thành WiFi của mình và sử dụng nhé!

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

bool ledState = 0;
const int ledPin = 2;

// 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 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   /*.button:hover {background-color: #0f8b8d}*/
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>
<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>Output - GPIO 2</h2>
      <p class="state">state: <span id="state">%STATE%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }
  function onLoad(event) {
    initWebSocket();
    initButton();
  }
  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>
)rawliteral";

void notifyClients() {
  ws.textAll(String(ledState));
}

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, "toggle") == 0) {
      ledState = !ledState;
      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 == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
  return String();
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, 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(ledPin, ledState);
}
1

Bạn thay đổi thông tin mạng WiFi qua câu lệnh sau trong chương trình trên và nạp code để hệ thống hoạt động nhé!

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Dưới đây, mình sẽ giải thích chi tiết cách chương trình trên hoạt động cho bạn.

Cách chương trình hoạt động

Nếu đã hiểu về chương trình, bạn có thể trực tiếp bỏ qua phần này và chuyển sang tìm hiểu kết quả của dự án thông qua phần Kết quả.

Khai báo thư viện

Khai báo các thư viện cần dùng để xây dựng Web Server:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

Chèn thông tin mạng WiFi

Chèn thông tin mạng WiFi của bạn (gồm tên và mật khẩu WiFi) vào các mục sau:

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Đầu ra GPIO

Bạn cần tạo các biến:

  • ledState để lưu trạng thái chân GPIO
  • ledPin để khai báo chân GPIO mà bạn muốn điều khiển, theo dõi (trong dự án WebSocket Server này, để đơn giản hơn thì mình dùng LED có sẵn trên mạch ESP32 được kết nối với GPIO 2)
bool ledState = 0;
const int ledPin = 2;

AsyncWebServer và AsyncWebSocket

Tạo một AsyncWebServer ở cổng 80:

AsyncWebServer server(80);

Thư viện ESPAsyncWebServer đã bao gồm sẵn tiện ích WebSocket bên trong, cho phép chúng ta dễ dàng tạo ra các kết nối WebSocket. Chúng ta chỉ cần tạo đối tượng AsyncWebSocket tên là ws để xử lý casdc kết nối trên đường truyền ws:

AsyncWebSocket ws("/ws");

Xây dựng giao diện Web Page

Các biến index_html chứa đầy đủ HTML, Javascript và CSS cần thiết cho bạn có thể xây dựng một Web Page nhằm xử lý các tương tác giữa máy khách và máy chủ, thông qua giao thức WebSocket Server.

Lưu ý: Hiện tại mình đang đặt tất cả các thứ cần thiết để xây dựng Web Page trên biến index_html để sử dụng trong sketch của Arduino. Bạn có thể tách các tệp HTML, Javascript và CSS riêng sau đó upload chúng lên filesystem của ESP32 và khai báo chúng trong chương trình để đơn giản hơn nhé!

Dưới đây là chương trình trong b iến index_html để xây dựng giao diện Web Page:

<!DOCTYPE HTML>
<html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>
<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>Output - GPIO 2</h2>
      <p class="state">state: <span id="state">%STATE%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }

  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }
  window.addEventListener('load', onLoad);
  function onLoad(event) {
    initWebSocket();
    initButton();
  }

  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }
  function toggle(){
    websocket.send('toggle');
  }
</script>
</body>
</html>

CSS

Giữa các tag <style> </style>, mình đã viết sẵn các code để trang trí giao diện Web bằng CSS. Bạn có thể tự do sáng tạo giao diện Web tùy thích dựa trên đoạn code CSS này:

     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
 </style>

HTML

Giữa các thẻ <body> </body> chúng ta sẽ chèn nội dung để hiển thị tới người dùng:

<div class="topnav">
  <h1>ESP WebSocket Server</h1>
</div>
<div class="content">
  <div class="card">
    <h2>Output - GPIO 2</h2>
    <p class="state">state: <span id="state">%STATE%</span></p>
    <p><button id="button" class="button">Toggle</button></p>
  </div>
</div>

Dưới đây là tiêu đề của Web Page, mình đang đặt là ESP WebSocket Server, bạn có thể đổi chúng thành dòng text bất kỳ nào mà bạn thích:

<h1>ESP WebSocket Server</h1>

Bên dưới là tiêu đề H2 với dòng text Output – GPIO 2

<h2>Output - GPIO 2</h2>

Sau đó, chúng ta hiển thị trạng thái GPIO theo thời gian thực:

<p class="state">state: <span id="state">%STATE%</span></p>

STATE là trạng thái của GPIO, chúng sẽ được cập nhật khi ESP32 gửi giá trị trạng thái output mới lên Website. Trạng thái này cần đặt giữa các dấu %. Dấu % này đồng nghĩa với việc khai báo đoạn văn bản STATE giống như một biến và chúng có thể thay đổi được theo thời gian thực.

Khi trang Web hiển thị trên máy khách, các trạng thái này cần thay đổi linh hoạt theo trạng thái chân GPIO thực tế. Chúng ta sẽ nhận được các thông tin này thông qua giao thức WebSocket Server.

Sau đó, Javascript sẽ xử lý các thông tin nhận được để cập nhật trạng thái tương ứng. Để làm được điều đó, văn bản phải có ID để tham chiếu (trong trường hợp này là state (<span id=”state”>).

Cuối cùng, dưới đây là chương trình hiển thị nút nhấn để thay đổi trạng thái GPIO:

<p><button id="button" class="button">Toggle</button></p>

Chúng ta đã cấp id cho nút id=”button”

JavaScript – Làm việc với giao thức WebSocket

JavaScript là chương trình nằm giữa phần <script> </script>. Chúng có trách nhiệm khởi tạo kết nối WebSocket với máy chủ khi giao diện Web Page hiển thị trên máy khách. Đây cũng là nơi xử lý dữ liệu được trao đổi thông qua WebSocket Server:

<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }

  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var state;
    if (event.data == "1"){
      state = "ON";
    }
    else{
      state = "OFF";
    }
    document.getElementById('state').innerHTML = state;
  }

  window.addEventListener('load', onLoad);

  function onLoad(event) {
    initWebSocket();
    initButton();
  }

  function initButton() {
    document.getElementById('button').addEventListener('click', toggle);
  }

  function toggle(){
    websocket.send('toggle');
  }
</script>

Cụ thể, dưới đây là cách thức hoạt động của JavaScript:

Cổng vào là giao diện của WebSocket ESP32. Trong đó, window.location.hostname sẽ lấy địa chỉ IP của Web Server hiện tại.

var gateway = `ws://${window.location.hostname}/ws`;

Tạo một biến mới có tên là websocket:

var websocket;

Tạo một trình xử lý sự kiện với hàm onload sau khi Web Server được tải:

window.addEventListener('load', onload);

Hàm onload() sẽ gọi initWebSocket() để khởi tạo kết nối giữa WebSocket với máy chủ và initButton() để thêm trình xử lý sự kiện vào các nút:

function onload(event) {
  initWebSocket();
  initButton();
}

Các hàm initWebSocket() khởi tạo kết nối WebSocket trên cổng đã được xác định trước đó. Mình cũng chỉ định một số chức năng gọi lại khi mở, đóng kết nối WebSocket hoặc khi nhận được thông tin mới:

function initWebSocket() {
  console.log('Trying to open a WebSocket connection…');
  websocket = new WebSocket(gateway);
  websocket.onopen    = onOpen;
  websocket.onclose   = onClose;
  websocket.onmessage = onMessage;
}

Khi mở kết nối xong, chúng ta cần in một thông báo trong bảng điều khiển và gửi dòng tin nhắn có nội dung “hi”. Nếu mạch ESP32 nhận được thông tin này, kết nối WebSocket ESP32 đã được khởi tạo thành công:

function onOpen(event) {
  console.log('Connection opened');
  websocket.send('hi');
}

Nếu vì lý do nào khác mà kết nối WebSocket Server bị ngắt, mình gọi initWebSocket() hoạt động trở lại sau 2 giây:

function onClose(event) {
  console.log('Connection closed');
  setTimeout(initWebSocket, 2000);
} 

setTimeout() cho phép hệ thống chạy lại một lệnh sau một thời gian bạn chọn (đơn vị mili giây)

Cuối cùng, chúng ta cần lập trình để hệ thống biết cần làm gì sau khi nhận được thông tin mới. Cụ thể:

  • ESP32 gửi thông báo là 1 hoặc 0
  • Dựa trên thông báo này, Web Page sẽ hiển thị trạng thái là bật (ON) hoặc tắt (OFF) tại thanh trạng thái. Bạn nhờ sử dụng tag <span> với id=”state” nhé! Các thẻ này sẽ cấu hình giá trị là bật hoặc tắt:
function onMessage(event) {
  var state;
  if (event.data == "1"){
    state = "ON";
  }
  else{
    state = "OFF";
  }
  document.getElementById('state').innerHTML = state;
}

Hàm initButton() kết nối với nút qua ID và tạo một trình xử lý sự kiện click:

function initButton() {
  document.getElementById('button').addEventListener('click', toggle);
}

Điều này đồng nghĩa với khi người dùng click vào nút, hàm toggle (chuyển đổi) sẽ được gọi.

Lúc đó, hàm toggle sẽ gửi thông báo qua kết nối WebSocket với dòng chữ ‘toggle’:

function toggle(){
  websocket.send('toggle');
}

Sau đó, mạch ESP32 sẽ đưa ra hành vi xử lý kịp thời khi nhận được thông báo này – cụ thể là chuyển đổi trạng thái GPIO.

Xử lý WebSocket ESP32 tại máy chủ

Ở các bài hướng dẫn trước, IoTZone đã hướng dẫn bạn cách máy khách (trình duyệt) xử lý kết nối WebSocket. Bây giờ, hãy cùng tìm hiểu cách máy chủ xử lý ra sao nhé!

Thông báo cho tất cả máy khách

Các hàm notifyClients() sẽ thông báo cho tất cả máy khách các thông tin bạn muốn. Trong trường hợp này, mình sẽ gửi thông báo về trạng thái đèn LED mỗi khi có sự thay đổi:

void notifyClients() {
  ws.textAll(String(ledState));
}

Các lớp AsyncWebSocket cung cấp textAll() để gửi cùng một thông báo đồng thời đến tất cả các máy khách kết nối với Server.

Xử lý tin nhắn của WebSocket ESP32

Các hàm handleWebSocketMessage() là một hàm giúp chạy lại bất cứ khi nào chúng ta nhận được thông tin mới từ máy khách thông qua kết nối WebSocket ESP32:

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, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
  }
}

Nếu chúng ta nhận được thông báo toggle, chúng ta sẽ đổi giá trị của biến ledState. Ngoài ra, chúng ta cũng sẽ thông báo đến tất cả các máy khách thông qua hàm notifyClients(). Bằng cách này, tất cả các máy khách đều được cập nhật thông tin và hiển thị cùng một giao diện Web Page.

if (strcmp((char*)data, "toggle") == 0) {
  ledState = !ledState;
  notifyClients();
}

Cấu hình WebSocket Server

Bây giờ, chúng ta cần cấu hình trình xử lý sự kiện để xử lý các bước không đồng bộ trên giao thức WebSocket, thông qua onEvent() như bên dưới:

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;
  }
}

Type đại diện cho các trường hợp có thể xảy ra, bao gồm các giá trị như sau:

  • WS_EVT_CONNECT: Máy khách đã đăng nhập
  • WS_EVT_DISCONNECT: Máy khách đăng xuất
  • WS_EVT_DATA: Nhận dữ liệu từ máy khách
  • WS_EVT_PONG: Phản hồi yêu cầu ping
  • WS_EVT_ERROR: Khi nhận được lỗi từ máy khách

Khởi tạo WebSocket

Sử dụng hàm initWebSocket() để khởi tạo giao thức WebSocket Server:

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

Hàm processor()

Hàm processor() sẽ tìm kiếm các placeholder (đoạn nội dung có thể thay đổi được) trên văn bản HTML và thay đổi chúng thành bất cứ thông tin nào chúng ta muốn trước khi gửi Web Page tới trình duyệt trên máy khách.

Trong dự án này, mình sẽ thay thế %STATE% thành ON nếu ledState 1, ngược lại thì %STATE% là OFF:

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if (ledState){
      return "ON";
    }
    else{
      return "OFF";
    }
  }
}

setup()

Khởi tạo Serial Monitor để quan sát và gỡ lỗi khi cần:

Serial.begin(115200);

Cấu hình ledPin thành OUTPUT và cấu hình chúng thành LOW:

pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

Khởi tạo mạng WiFi và in địa chỉ IP của ESP32 ra màn hình Serial Monitor nếu kết nối WiFi thành công:

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());

Khởi tạo giao thức WebSocket thông qua initWebSocket()

initWebSocket();

Xử lý yêu cầu

Các yêu cầu sẽ được lưu lại trên index_html khi bạn nhận được yêu cầu thông qua URL. Lúc này, bạn cần sử dụng hàm processor như một đối số để thay thế phần placeholder thành trạng thái của chân GPIO hiện tại:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
});

Cuối cùng, hãy khởi động Server:

server.begin();

loop()

Chúng ta sẽ điều khiển đèn LED trong vòng lặp này.

void loop() {
  ws.cleanupClients();
  digitalWrite(ledPin, ledState);
}

Chúng ta sẽ sử dụng cleanupClients(), bởi trình trình duyệt đôi khi không kết nối đúng cách tới WebSocket ESP32, ngay cả khi chúng ta đã gọi hàm close() trong Javascript. Điều này sẽ khiến Web Server bị hết tài nguyên và gặp sự cố.

Do đó, việc gọi hàm cleanupClients() trong vòng lặp loop() sẽ giới hạn số lượng máy khách bằng cách đóng lần lượt các máy khách cũ nhất khi vượt quá số lượng máy khách tối đa. Bạn có thể gọi hàm này tại mỗi chu kỳ, tuy nhiên nếu bạn muốn dùng ít năng lượng hơn thì bạn chỉ cần gọi mỗi giây một lần là vừa phải.

Kết quả

Sau khi chèn thông tin mạng WiFI (gồm tên SSID và mật khẩu) vào các biến, bạn có thể nạp code vào trong mạch ESP32 của mình. Bạn nhớ là hãy chọn đúng mạch và cổng COM nhé!

Sau đó, bạn hãy mở cửa sổ Serial Monitor ở tốc độ 115200 và nhấn nút EN/RST trên mạch ESP32. Trên màn hình Serial sẽ in ra địa chỉ IP của mạch.

Lúc này, bạn chỉ cần mở điện thoại / laptop và dán địa chỉ IP ESP32 vào thanh tìm kiếm URL để quan sát và điều chỉnh trạng thái đầu ra (cụ thể là đèn LED) của mạch ESP32.

Khi bấm nào nút nhấn, trạng thái đèn LED sẽ thay đổi. Dù bạn truy cập vào từ nhiều tab, nhiều trình duyệt hoặc nhiều thiết bị khác nhau, thì trạng thái đèn LED vẫn tự động cập nhật đồng bộ ở tất cả các máy khách này mỗi khi có thay đổi.

Lời kết

Trên hướng dẫn này, mình đã hướng dẫn bạn chi tiết cách cấu hìnhmáy chủ WebSocket ESP32. Giao thức này cho phép chúng ta giao tiếp song song giữa Server và Client vào bất kỳ thời điểm nào, miễn là chúng đã khởi tạo xong.

Giao thức WebSocket Server rất hữu ích, vì máy chủ có thể gửi dữ liệu đến máy khách bất kỳ lúc nào. Ví dụ, bạn có thể xây dựng dự án sau: Khi nhấn nút trên mạch ESP32, giao diện trên Web Page sẽ tự động thay đổi và cập nhật.

Trong hường dẫn này, mình đã giới thiệu đến bạn cách điều khiển GPIO 2 trên mạch ESP32. Bạn cũng có thể làm tương tự để điều khiển nhiều chân GPIO hơn, hoặc là gửi thông báo, kết quả dữ liệu đọc được từ cảm biến vào bất kỳ lúc nào. Chúc các bạn thành công với dự án WebSocket ESP32 trên!

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *