在本教程中,您将学习如何使用 WebSocket 通信协议使用 ESP32 构建 Web 服务器。例如,我们将向您展示如何构建一个网页来远程控制 ESP32 输出。输出状态显示在网页上,并在所有客户端中自动更新。
ESP32 将使用 Arduino IDE 和 ESPAsyncWebServer 进行编程。
如果您一直在关注我们以前的一些网络服务器项目,您可能已经注意到,如果您同时打开了多个选项卡(在相同或不同的设备上),则状态根本不会更新除非您刷新网页,否则选项卡会自动出现。为了解决这个问题,我们可以使用 WebSocket 协议——当发生变化时可以通知所有客户端并相应地更新网页。
目录
介绍 WebSocket
WebSocket 是客户端和服务器之间的持久连接,允许双方使用 TCP 连接进行双向通信。这意味着您可以在任何给定时间将数据从客户端发送到服务器以及从服务器发送到客户端。
客户端通过称为WebSocket 握手的过程与服务器建立 WebSocket 连接。握手以 HTTP 请求/响应开始,允许服务器处理同一端口上的 HTTP 连接和 WebSocket 连接。一旦建立连接,客户端和服务器就可以以全双工模式发送 WebSocket 数据。
使用 WebSockets 协议,服务器(ESP32 板)可以向客户端或所有客户端发送信息而无需请求。这也允许我们在发生更改时向网络浏览器发送信息。
这种变化可以是网页上发生的事情(你点击了一个按钮),也可以是 ESP32 端发生的事情,比如按下电路上的物理按钮。
项目概况
这是我们将为这个项目构建的网页。
- ESP32 Web 服务器显示一个带有按钮的网页,用于切换 GPIO 2 的状态;
- 为简单起见,我们正在控制 GPIO 2——板载 LED。您可以使用此示例来控制任何其它 GPIO;
- 界面显示当前 GPIO 状态。每当GPIO状态发生变化时,接口都会瞬间更新;
- GPIO 状态会在所有客户端中自动更新。这意味着,如果您在同一设备或不同设备上打开了多个 Web 浏览器选项卡,它们都会同时更新。
这个怎么运作?
下图描述了单击“切换”按钮时发生的情况。
当您单击“切换”按钮时,会发生以下情况:
- 点击“切换”按钮;
- 客户端(您的浏览器)通过带有“切换”消息的 WebSocket 协议发送数据;
- ESP32(服务器)收到此消息,因此它知道应该切换 LED 状态。如果 LED 之前是关闭的,请将其打开;
- 然后,它也通过 WebSocket 协议向所有客户端发送具有新 LED 状态的数据;
- 客户端收到消息并相应地更新网页上的 LED 状态。这使我们能够在发生更改时几乎立即更新所有客户端。
准备 Arduino IDE
我们将使用 Arduino IDE 对 ESP32 板进行编程,因此请确保您已将其安装在您的 Arduino IDE 中。
安装库 – 异步 Web 服务器
为了构建 Web 服务器,我们将使用ESPAsyncWebServer库。该库需要AsyncTCP库才能正常工作。单击下面的链接下载库:
这些库无法通过 Arduino Library Manager 安装,因此您需要将库文件复制到 Arduino Installation Libraries 文件夹。或者,在您的 Arduino IDE 中,您可以转到 Sketch > Include Library > Add .zip Library 并选择您刚刚下载的库。
ESP32 WebSocket 服务器的代码
将以下代码复制到您的 Arduino IDE:
在以下变量中插入您的网络凭据,代码将立即运行。
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
代码如何运作
导入库
导入必要的库来构建 Web 服务器。
#include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h>
网络凭证
在以下变量中插入您的网络凭据:
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
GPIO 输出
创建一个名为领导状态保存 GPIO 状态和一个名为的变量引脚那是指您要控制的GPIO。在这种情况下,我们将控制板载 LED(连接到通用输入输出接口 2)。
bool ledState = 0; const int ledPin = 2;
AsyncWebServer 和 AsyncWebSocket
创建一个异步网络服务器端口 80 上的对象。
AsyncWebServer server(80);
这ESPAsyncWebServer库包含一个 WebSocket 插件,可以轻松处理 WebSocket 连接。创建一个异步WebSocket称为对象ws处理上的连接/ws小路。
AsyncWebSocket ws("/ws");
构建网页
这index_html变量包含构建和设置网页样式以及使用 WebSocket 协议处理客户端-服务器交互所需的 HTML、CSS 和 JavaScript。
注意:我们将构建网页所需的一切都放在index_html我们在 Arduino 程序上使用的变量。请注意,将 HTML、CSS 和 JavaScript 文件分开,然后上传到 ESP32 文件系统并在代码中引用它们可能更实用。
以下是index_html内容:
<!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
在。。之间<style> </style>标签我们包含样式以使用 CSS 设置网页样式。随意更改它以使网页看起来如您所愿。我们不会解释这个网页的 CSS 是如何工作的,因为它与这个 WebSocket 教程无关。
<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>
HTML
在。。之间<正文> </正文>标签我们添加对用户可见的网页内容。
<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>
标题 1 带有文本“ESP WebSocket Server”。随意修改该文本。
<h1>ESP WebSocket Server</h1>
然后,标题 2 带有“Output – GPIO 2”文本。
<h2>Output - GPIO 2</h2>
之后,我们有一个显示当前 GPIO 状态的段落。
<p class="state">state: <span id="state">%STATE%</span></p>
这%STATE%是 GPIO 状态的占位符。ESP32 在发送网页时将其替换为当前值。HTML 文本上的占位符应该介于%之间。这意味着这%STATE%文本就像一个变量,然后将替换为实际值。
将网页发送到客户端后,只要 GPIO 状态发生变化,状态就需要动态变化。我们将通过 WebSocket 协议接收该信息。然后,JavaScript 处理如何处理接收到的信息以相应地更新状态。为了能够使用 JavaScript 处理该文本,该文本必须有一个我们可以引用的 id。在这种情况下,id 是状态(<span id=”state”>)。
最后,有一段带有用于切换 GPIO 状态的按钮。
<p><button id="button" class="button">Toggle</button></p>
请注意,我们为按钮提供了一个 id (id=”button”).
JavaScript – 处理 WebSockets
JavaScript 介于<script> </script>标签。它负责在浏览器中完全加载 Web 界面后立即初始化与服务器的 WebSocket 连接,并通过 WebSockets 处理数据交换。
<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>
让我们看看这是如何工作的。
网关是 WebSocket 接口的入口点。
var gateway = `ws://${window.location.hostname}/ws`;
window.location.hostname获取当前页面地址(Web 服务器 IP 地址)。
创建一个名为的新全局变量网络套接字.
var websocket;
添加一个事件监听器,它将调用负载网页加载时的功能。
window.addEventListener('load', onload);
onload()函数调用initWebSocket()与服务器的 WebSocket 连接的函数和initButton()函数将事件侦听器添加到按钮。
function onload(event) { initWebSocket(); initButton(); }
initWebSocket()函数在前面定义的网关上初始化 WebSocket 连接。我们还为打开、关闭 WebSocket 连接或收到消息时分配了几个回调函数。
function initWebSocket() { console.log('Trying to open a WebSocket connection…'); websocket = new WebSocket(gateway); websocket.onopen = onOpen; websocket.onclose = onClose; websocket.onmessage = onMessage; }
当连接打开时,我们只需在控制台中打印一条消息并发送一条消息说“hi”。ESP32 收到该消息,因此我们知道连接已初始化。
function onOpen(event) { console.log('Connection opened'); websocket.send('hi'); }
如果由于某种原因 Web 套接字连接被关闭,我们调用initWebSocket()2000 毫秒(2 秒)后再次运行。
function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); }
setTimeout()方法在指定的毫秒数后调用函数或计算表达式。
最后,我们需要处理收到新消息时发生的情况。服务器(您的 ESP 板)将发送“1”或“0”消息。根据收到的消息,我们希望在显示状态的段落上显示“ON”或“OFF”消息。请记住<span>标签id=“state”? 我们将获取该元素并将其值设置为 ON 或 OFF。
function onMessage(event) { var state; if (event.data == "1"){ state = "ON"; } else{ state = "OFF"; } document.getElementById('state').innerHTML = state; }
initButton()函数通过其 id 获取按钮 (按钮) 并添加类型的事件侦听器’点击’.
function initButton() { document.getElementById('button').addEventListener('click', toggle); }
这意味着当您单击按钮时,切换函数被调用。
这切换函数使用 WebSocket 连接发送消息’切换’文本。
function toggle(){ websocket.send('toggle'); }
然后,ESP32 应该处理收到此消息时发生的情况——切换当前的 GPIO 状态。
处理 WebSockets – 服务器
之前,您已经了解了如何在客户端(浏览器)上处理 WebSocket 连接。现在,让我们看看如何在服务器端处理它。
通知所有客户
notifyClients()函数通过包含您作为参数传递的任何内容的消息通知所有客户端。在这种情况下,我们希望在发生变化时通知所有客户端当前 LED 状态。
void notifyClients() { ws.textAll(String(ledState)); }
这异步WebSocket类提供了一个textAll()向同时连接到服务器的所有客户端发送相同消息的方法。
处理 WebSocket 消息
这处理WebSocketMessage()是一个回调函数,只要我们通过 WebSocket 协议从客户端接收到新数据,就会运行该回调函数。
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(); } } }
如果我们收到“toggle”消息,我们将切换ledState变量的值。此外,我们通过调用notifyClients()函数通知所有客户端。这样,所有客户端都会收到更改通知,并相应地更新接口。
if (strcmp((char*)data, "toggle") == 0) { ledState = !ledState; notifyClients(); }
配置 WebSocket 服务器
现在我们需要配置一个事件监听器来处理 WebSocket 协议的不同异步步骤。这个事件处理程序可以通过onEvent()如下:
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 %sn", client->id(), client->remoteIP().toString().c_str()); break; case WS_EVT_DISCONNECT: Serial.printf("WebSocket client #%u disconnectedn", client->id()); break; case WS_EVT_DATA: handleWebSocketMessage(arg, data, len); break; case WS_EVT_PONG: case WS_EVT_ERROR: break; } }
这类型参数表示发生的事件。它可以采用以下值:
- WS_EVT_CONNECT当客户登录时;
- WS_EVT_DISCONNECT当客户退出时;
- WS_EVT_DATA当从客户端接收到数据包时;
- WS_EVT_PONG响应 ping 请求;
- WS_EVT_ERROR当从客户端收到错误时。
初始化 WebSocket
最后,初始化WebSocket()函数初始化 WebSocket 协议。
void initWebSocket() { ws.onEvent(onEvent); server.addHandler(&ws); }
processor()
这处理器()函数负责在 HTML 文本上搜索占位符,并在将网页发送到浏览器之前将它们替换为我们想要的任何内容。在我们的例子中,我们将替换%STATE%占位符上如果领导状态是1. 否则,将其替换为离开.
String processor(const String& var){ Serial.println(var); if(var == "STATE"){ if (ledState){ return "ON"; } else{ return "OFF"; } } }
setup()
在函数 setup() 里面,初始化串行监视器以进行调试。
Serial.begin(115200);
设置引脚作为一个输出并将其设置为低的当程序第一次启动时。
pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW);
初始化 Wi-Fi 并在串口监视器上打印 ESP32 IP 地址。
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());
通过调用初始化 WebSocket 协议初始化WebSocket()之前创建的函数。
initWebSocket();
处理请求
提供保存在index_html当您在根/ URL上收到请求时变量- 您需要传递处理器作为参数使用当前 GPIO 状态替换占位符。
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, processor); });
最后,启动服务器。
server.begin();
loop()
LED 将在loop()函数中被控制:
void loop() { ws.cleanupClients(); digitalWrite(ledPin, ledState); }
请注意,我们都称清理客户端()方法。原因如下(来自 ESPAsyncWebServer 库 GitHub 页面的解释):
浏览器有时不会正确关闭 WebSocket 连接,即使在关()函数在 JavaScript 中调用。这最终会耗尽 Web 服务器的资源并导致服务器崩溃。定期调用清理客户端()从主函数环形()当超过最大客户端数量时,通过关闭最旧的客户端来限制客户端数量。这可以在每个周期调用,但是,如果您希望使用更少的功率,那么每秒调用一次就足够了。
示范
将您的网络凭据插入到ssid和密码变量,您可以将代码上传到您的开发板。不要忘记检查您是否选择了正确的板和 COM 端口。
上传代码后,以 115200 的波特率打开串口监视器,然后按下板载的 EN/RST 按钮。应打印 ESP IP 地址。
在本地网络上打开浏览器并插入 ESP32 IP 地址。您应该可以访问网页以控制输出。
单击按钮以切换 LED。您可以同时打开多个 Web 浏览器选项卡或从不同设备访问 Web 服务器,只要有更改,所有客户端的 LED 状态都会自动更新。
总结
在本教程中,您学习了如何使用 ESP32 设置 WebSocket 服务器。WebSocket 协议允许客户端和服务器之间进行全双工通信。初始化后,服务器和客户端可以在任何给定时间交换数据。
这非常有用,因为服务器可以在发生任何事情时向客户端发送数据。例如,您可以在此设置中添加一个物理按钮,按下该按钮会通知所有客户端更新 Web 界面。
在本例中,我们向您展示了如何控制 ESP32 的一个 GPIO。您可以使用此方法来控制更多的 GPIO。您还可以使用 WebSocket 协议在任何给定时间发送传感器读数或通知。
我们希望您发现本教程很有用。我们打算使用 WebSocket 协议创建更多教程和示例。所以,敬请期待。
学习学习学习