WebSocket 服务器:控制输出 – ESP8266 NodeMCU

释放双眼,带上耳机,听听看~!

在本教程中,您将学习如何使用 WebSocket 通信协议构建具有 ESP8266 的 Web 服务器。例如,我们将向您展示如何构建一个网页来远程控制ESP8266输出。输出状态显示在网页上,并在所有客户端中自动更新。

WebSocket 服务器:控制输出 - ESP8266 NodeMCU

该ESP8266将使用 Arduino IDE 和 ESPAsyncWebServer 进行编程。我们也有类似的 ESP32 WebSocket 指南。

如果您一直在关注我们以前的一些 Web 服务器项目,例如这个项目,您可能已经注意到,如果您同时打开了多个选项卡(在相同或不同的设备上),除非您刷新网页,否则状态不会在所有选项卡中自动更新。为了解决这个问题,我们可以使用 WebSocket 协议——当发生更改时,所有客户端都可以收到通知,并相应地更新网页。

 WebSocket 简介

WebSocket 是客户端和服务器之间的持久连接,允许双方使用 TCP 连接进行双向通信。这意味着您可以在任何给定时间将数据从客户端发送到服务器,并从服务器发送到客户端。

WebSocket 服务器:控制输出 - ESP8266 NodeMCU

客户端通过称为 WebSocket 握手的过程与服务器建立 WebSocket 连接。握手以 HTTP 请求/响应开始,允许服务器在同一端口上处理 HTTP 连接以及 WebSocket 连接。建立连接后,客户端和服务器可以全双工模式发送 WebSocket 数据。

使用 WebSockets 协议,服务器(ESP8266板)可以在不请求的情况下向客户端或所有客户端发送信息。这也允许我们在发生更改时向 Web 浏览器发送信息。

这种变化可以是网页上发生的事情(您点击了一个按钮),也可以是发生在ESP8266端的事情,例如按下电路上的物理按钮。

 项目概况

这是我们将为这个项目构建的网页。

WebSocket 服务器:控制输出 - ESP8266 NodeMCU
  • ESP8266 Web 服务器显示一个网页,其中包含一个用于切换 GPIO 2 状态的按钮;
  • 为简单起见,我们控制 GPIO 2 – 板载 LED。您可以使用此示例来控制任何其他 GPIO;
  • 该接口显示当前 GPIO 状态。每当GPIO状态发生变化时,接口就会立即更新;
  • GPIO 状态在所有客户端中自动更新。这意味着,如果您在同一设备或不同设备上打开了多个 Web 浏览器选项卡,它们都会同时更新。

 它是如何工作的?

下图描述了单击“切换”按钮时发生的情况。

WebSocket 服务器:控制输出 - ESP8266 NodeMCU

当您单击“切换”按钮时,会发生什么情况:

  1. 点击“切换”按钮;
  2. 客户端(您的浏览器)通过 WebSocket 协议发送带有“切换”消息的数据;
  3. ESP8266(服务器)收到此消息,因此它知道它应该切换 LED 状态。如果 LED 之前熄灭,请将其打开;
  4. 然后,它通过 WebSocket 协议将具有新 LED 状态的数据发送到所有客户端;
  5. 客户端接收消息并相应地更新网页上的 LED 状态。这使我们能够在发生更改时几乎立即更新所有客户端。

 准备Arduino IDE

我们将使用 Arduino IDE 对 ESP8266 板进行编程,因此请确保已将其安装在 Arduino IDE 中。

(一)nodemcu初级:利用Arduino进行开发

安装库 – 异步 Web 服务器

为了构建 Web 服务器,我们将使用 ESPAsyncWebServer 库。该库需要 ESPAsyncTCP 库才能正常工作。单击下面的链接下载库。

这些库无法通过Arduino库管理器进行安装,因此您需要将库文件复制到Arduino安装库文件夹。或者,在Arduino IDE中,您可以转到“程序”>“包含库”>“添加.zip库”,然后选择刚刚下载的库。

ESP8266 NodeMCU WebSocket 服务器的代码

将以下代码复制到 Arduino IDE。

 

在以下变量中插入您的网络凭据,代码将立即起作用。

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

代码的工作原理

继续阅读以了解代码的工作原理,或跳到演示部分。

 导入库

导入必要的库以构建 Web 服务器。

#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

 网络凭据

在以下变量中插入网络凭据:

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

 GPIO输出

创建一个名为 ledState 的变量来保存 GPIO 状态,并创建一个名为 ledPin 的变量来引用要控制的 GPIO。在本例中,我们将控制板载 LED(连接到 GPIO 2)。

bool ledState = 0;
const int ledPin = 2;

AsyncWebServer 和 AsyncWebSocket

在端口 80 上创建一个 AsyncWebServer 对象。

AsyncWebServer server(80);

ESPAsyncWebServer 库包含一个 WebSocket 插件,可以轻松处理 WebSocket 连接。创建一个名为 ws 的 AsyncWebSocket 对象,以处理 /ws 路径上的连接。

AsyncWebSocket ws("/ws");

构建网页

index_html 变量包含构建网页和设置网页样式以及使用 WebSocket 协议处理客户端-服务器交互所需的 HTML、CSS 和 JavaScript。

注意:我们将构建网页所需的所有内容放在我们在Arduino程序上使用的index_html变量上。请注意,将 HTML、CSS 和 JavaScript 文件分开,然后上传到 ESP8266 文件系统并在代码中引用它们可能更实用。

推荐阅读:使用 SPIFFS ESP8266 Web 服务器(SPI 闪存文件系统)

下面是 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,其中包含“输出 – GPIO 2”文本。

<h2>Output - GPIO 2</h2>

之后,我们有一个显示当前GPIO状态的段落。

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

%STATE% 是 GPIO 状态的占位符。它将被发送网页时的ESP8266替换为当前值。HTML 文本上的占位符应介于 % 符号之间。这意味着这个 %STATE% 文本就像一个变量,然后将被替换为实际值。

将网页发送到客户端后,每当 GPIO 状态发生更改时,状态都需要动态更改。我们将通过 WebSocket 协议接收该信息。然后,JavaScript 会处理如何处理接收到的信息,以相应地更新状态。为了能够使用 JavaScript 处理该文本,文本必须具有我们可以引用的 id。在本例中,id 为 state ()。

最后,有一个段落带有切换 GPIO 状态的按钮。

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

请注意,我们已经为按钮指定了一个 id (id=“button”)。

JavaScript – 处理 WebSocket

JavaScript 在标记之间移动。它负责在浏览器中完全加载 Web 界面后立即初始化与服务器的 WebSocket 连接,并通过 WebSocket 处理数据交换。

<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 地址)。

创建一个名为 websocket 的新全局变量。

var websocket;

添加一个事件侦听器,该侦听器将在网页加载时调用 onload 函数。

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

当连接打开时,我们只需在控制台中打印一条消息并发送一条消息说“嗨”。ESP8266收到该消息,因此我们知道连接已初始化。

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

如果由于某种原因 Web 套接字连接关闭,我们在 2000 毫秒(2 秒)后再次调用 initWebSocket() 函数。

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

setTimeout() 方法在指定的毫秒数后调用函数或计算表达式。

最后,我们需要处理收到新消息时发生的情况。服务器(您的 ESP 板)将发送“1”或“0”消息。根据收到的消息,我们希望在显示状态的段落上显示“ON”或“OFF”消息。还记得那个带有 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(按钮)获取按钮,并添加一个类型为“click”的事件侦听器。

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

这意味着当您单击该按钮时,将调用切换函数。

toggle 函数使用 WebSocket 连接发送带有“toggle”文本的消息。

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

然后,ESP8266应处理收到此消息时发生的情况 – 切换当前 GPIO 状态。

处理 WebSocket – 服务器

之前,您已经了解了如何在客户端(浏览器)上处理 WebSocket 连接。现在,让我们来看看如何在服务器端处理它。

 通知所有客户

notifyClients() 函数通过一条消息通知所有客户端,其中包含您作为参数传递的任何内容。在这种情况下,每当发生更改时,我们都会通知所有客户端当前 LED 状态。

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

AsyncWebSocket 类提供了一个 textAll() 方法,用于将相同的消息发送到同时连接到服务器的所有客户端。

处理 WebSocket 消息

handleWebSocketMessage() 函数是一个回调函数,每当我们通过 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;
  }
}

type 参数表示发生的事件。它可以采用以下值:

  • WS_EVT_CONNECT客户端登录时;
  • WS_EVT_DISCONNECT客户端注销时;
  • WS_EVT_DATA从客户端接收到数据包时;
  • WS_EVT_PONG响应 ping 请求;
  • WS_EVT_ERROR从客户端收到错误时。

 初始化 WebSocket

最后,initWebSocket() 函数初始化 WebSocket 协议。

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

processor()

processor() 函数负责在 HTML 文本上搜索占位符,并在将网页发送到浏览器之前将它们替换为我们想要的任何内容。在我们的例子中,如果 ledState 为 1,我们将 %STATE% 占位符替换为 ON。否则,请将其替换为 OFF。

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

 setup()

在 setup() 中,初始化串行监视器以进行调试。

Serial.begin(115200);

将 ledPin 设置为 OUTPUT,并在程序首次启动时将其设置为 LOW。

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

初始化 Wi-Fi 并在串行监视器上打印ESP8266 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());

通过调用之前创建的 initWebSocket() 函数来初始化 WebSocket 协议。

initWebSocket();

 处理请求

当您在根/URL 上收到请求时,提供保存在 index_html 变量上的文本 – 您需要将处理器函数作为参数传递,以将占位符替换为当前 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);
}

请注意,我们都调用 cleanupClients() 方法。原因如下(来自 ESPAsyncWebServer 库 GitHub 页面的解释):

浏览器有时无法正确关闭 WebSocket 连接,即使在 JavaScript 中调用 close() 函数也是如此。这最终会耗尽 Web 服务器的资源,并导致服务器崩溃。定期从 main loop() 调用 cleanupClients() 函数,通过在超过最大客户端数时关闭最旧的客户端来限制客户端数量。这可以每个周期调用,但是,如果您希望使用更少的功率,那么每秒调用一次的频率就足够了。

 示范

在 ssid 和 password 变量上插入网络凭据后,您可以将代码上传到您的主板。不要忘记检查您是否选择了正确的电路板和 COM 端口。

上传代码后,以 115200 的波特率打开串行监视器,然后按下板载 EN/RST 按钮。应打印 ESP IP 地址。

在本地网络上打开浏览器并插入ESP8266 IP 地址。您应该可以访问网页以控制输出。

WebSocket 服务器:控制输出 - ESP8266 NodeMCU

单击按钮以切换 LED。您可以同时打开多个 Web 浏览器选项卡或从不同的设备访问 Web 服务器,每当有更改时,LED 状态都会在所有客户端中自动更新。

 

 总结

在本教程中,你学习了如何使用 ESP8266 设置 WebSocket 服务器。WebSocket 协议允许客户端和服务器之间进行全双工通信。初始化后,服务器和客户端可以在任何给定时间交换数据。

这非常有用,因为每当发生某些事情时,服务器都可以向客户端发送数据。例如,您可以向此设置添加一个物理按钮,按下该按钮会通知所有客户端更新 Web 界面。

在此示例中,我们向您展示了如何控制ESP8266的一个 GPIO。您可以使用此方法控制更多 GPIO。您还可以使用 WebSocket 协议在任何给定时间发送传感器读数或通知。

给TA打赏
共{{data.count}}人
人已打赏
Nodemcu/ESP8266Nodemcu/ESP8266-进阶动态

ESP8266两块板之间的客户端-服务器 Wi-Fi 通信 (NodeMCU)

2023-12-9 21:23:35

Nodemcu/ESP8266Nodemcu/ESP8266-进阶动态

ESP-MESH 网络协议 - ESP32 和 ESP8266使用(painlessMesh 库)

2023-12-9 21:49:52

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索
'); })();