本教程展示了如何构建一个 ESP32 Web 服务器,该服务器显示一个带有多个滑块的网页。滑块控制不同 PWM 通道的占空比,以控制多个 LED 的亮度。您可以使用此项目可以替换 LED 来控制需要 PWM 信号的直流电机或其它执行器。客户端和 ESP32 之间的通信是使用 WebSocket 协议完成的。此外,只要有变化,所有客户端都会同时更新其滑块值。
您还可以修改本教程中提供的代码,将滑块添加到您的项目中,以设置阈值或您需要在代码中使用的任何其它值。
对于这个项目,ESP32 板将使用 Arduino 内核进行编程。您可以使用Arduino IDE或任何其它合适的 IDE。
* 该项目展示了如何使用一个滑块构建 Web 服务器,但它使用 HTTP 请求——在本教程中,我们将使用 WebSocket 协议。
目录
项目概况
下图显示了我们将为这个项目构建的网页:
- 网页包含三张卡片;
- 每张卡片都有一段显示卡片标题(Fader 1、Fader 2、Fader 3);
- 每张卡中都有一个范围滑块,您可以移动它来设置相应 LED 的亮度;
- 在每张卡片中,另一段显示当前 LED 亮度(百分比);
- 当您为滑块设置新位置时,它会更新所有客户端(如果您打开了多个 Web 浏览器选项卡(或多个设备),它们几乎会在发生更改时同时更新)。
这个怎么运作?
- ESP 托管一个 Web 服务器,该服务器显示一个带有三个滑块的网页;
- 当您为滑块设置新位置时,客户端通过 WebSocket 协议将滑块编号和滑块值发送到服务器。例如,如果您将滑块编号 3 设置为位置编号 40,它将发送此消息3s40到服务器。
- 服务器 (ESP) 接收滑块编号和相应的值,并相应地调整 PWM 占空比。此外,它还用新的当前滑块值通知所有其它客户端——这使我们几乎可以立即更新所有客户端。
- ESP32 输出具有相应占空比的 PWM 信号来控制 LED 亮度。占空比为 0% 表示 LED 完全关闭,占空比为 50% 表示 LED 半亮,占空比为 100% 表示 LED 全亮;
- 每当您打开一个新的 Web 浏览器窗口(这是新客户端连接时),它都会向 ESP32(也通过 WebSocket 协议)发送一条消息,其中包含该消息获取值. 当 ESP32 收到此消息时,它会发送当前滑块值。这样,每当您打开一个新选项卡时,它总是显示当前和更新的值。
先决条件
在继续本教程之前,请确保检查以下所有先决条件。
1) 所需零件
要关注这个项目,您需要:
- ESP32开发板
- 3x LED
- 3x 220Ohm 电阻
- 面包板
- 连接线
如果没有三个 LED 来测试这个项目,也可以简单地在串行监视器中查看结果或使用其它需要 PWM 信号才能运行的执行器。
2) Arduino IDE 和 ESP32 开发板插件
我们将使用 Arduino IDE 对 ESP32 进行编程。因此,您必须安装 ESP32 插件。如果您还没有,请按照下一个教程:
3) 文件系统上传插件
要将构建此项目所需的 HTML、CSS 和 JavaScript 文件上传到 ESP32 闪存 (SPIFFS),我们将使用 Arduino IDE 插件: SPIFFS Filesystem uploader。如果您还没有安装文件系统上传器插件,请按照下一个教程安装:
4) 库
要构建此项目,您需要安装以下库:
您可以使用 Arduino Library Manager 安装第一个库。转到 Sketch > Include Library > Manage Libraries 并搜索库名称。
这ESPAsyncWebServer和异步TCP库无法通过 Arduino Library Manager 安装,因此您需要将库文件复制到 Arduino Installation Libraries 文件夹。或者,在您的 Arduino IDE 中,您可以转到 Sketch > Include Library > Add .zip Library 并选择您刚刚下载的库。
原理图
将三个 LED 连接到 ESP32。我们正在使用 GPIO 12、13 和 14。您可以使用任何其它合适的 GPIO。
推荐阅读: ESP32 Pinout Reference:您应该使用哪些 GPIO 引脚?
组织你的文件
为了使项目井井有条并更容易理解,我们将创建四个文件来构建 Web 服务器:
- 处理 Web 服务器的Arduino 程序;
- index.html:定义网页的内容;
- sytle.css:设置网页样式;
- script.js:对网页的行为进行编程——处理当你移动滑块时发生的事情,发送、接收和解释通过 WebSocket 协议接收到的消息。
您应该将 HTML、CSS 和 JavaScript 文件保存在 Arduino 程序文件夹中名为 data 的文件夹中,如上图所示。我们会将这些文件上传到 ESP32 文件系统 (SPIFFS)。
您可以下载所有项目文件:
HTML 文件
将以下内容复制到index.html文件。
<!DOCTYPE html> <html> <head> <title>ESP IOT DASHBOARD</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/png" href="favicon.png"> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <div class="topnav"> <h1>Multiple Sliders</h1> </div> <div class="content"> <div class="card-grid"> <div class="card"> <p class="card-title">Fader 1</p> <p class="switch"> <input type="range" onchange="updateSliderPWM(this)" id="slider1" min="0" max="100" step="1" value ="0" class="slider"> </p> <p class="state">Brightness: <span id="sliderValue1"></span> %</p> </div> <div class="card"> <p class="card-title"> Fader 2</p> <p class="switch"> <input type="range" onchange="updateSliderPWM(this)" id="slider2" min="0" max="100" step="1" value ="0" class="slider"> </p> <p class="state">Brightness: <span id="sliderValue2"></span> %</p> </div> <div class="card"> <p class="card-title"> Fader 3</p> <p class="switch"> <input type="range" onchange="updateSliderPWM(this)" id="slider3" min="0" max="100" step="1" value ="0" class="slider"> </p> <p class="state">Brightness: <span id="sliderValue3"></span> %</p> </div> </div> </div> <script src="script.js"></script> </body> </html>
让我们快速浏览一下 HTML 文件中最相关的部分。
创建滑块
以下标签为第一个滑块(Fader 1)创建卡片。
<div class="card">
<p class="card-title">Fader 1</p>
<p class="switch">
<input type="range" onchange="updateSliderPWM(this)" id="slider1" min="0" max="100" step="1" value ="0" class="slider">
</p>
<p class="state">Brightness: <span id="sliderValue1"></span> %</p>
</div>
第一段显示卡片的标题(Fader 1)。您可以将文本更改为您想要的任何内容。
<p class="card-title">Fader 1</p>
要在 HTML 中创建滑块,请使用<input>标签。这<input>标签指定用户可以输入数据的字段。
输入类型多种多样。要定义滑块,请使用类型属性与范围。在滑块中,您还需要使用分钟和最大限度属性(在这种情况下,0和100, 分别)。
您还需要定义其它属性,例如:
- 这步属性指定有效数字之间的间隔。在我们的例子中,我们将其设置为1;
- 这班级设置滑块样式(class=“switch”);
- 这ID这样我们就可以使用 JavaScript (id=”slider1″);
- 这改变调用函数的属性(updateSliderPWM(this)) 当您为滑块设置新位置时。此函数(在 JavaScript 文件中定义)通过 WebSocket 协议将当前滑块值发送到客户端。这这个关键字是指 HTML 滑块元素。
滑块位于带有switch名称。所以,这里是实际创建滑块的标签。
<p class="switch"> <input type="range" onchange="updateSliderPWM(this)" id="slider1" min="0" max="100" step="1" value ="0" class="slider"> </p>
最后,有一段带有<span>标记,以便我们可以通过引用其 id (id=”sliderValue1″)。
<p class="state">Brightness: <span id="sliderValue1"></span> %</p>
创建更多滑块
要创建更多滑块,您需要复制创建完整卡片的所有 HTML 标记。但是,首先,您需要考虑每个滑块和滑块值都需要一个唯一的 id。在我们的例子中,我们有三个具有以下 id 的滑块:滑块1,滑块2,滑块3,以及具有以下 id 的滑块值的三个占位符:Fader1,Fader2,Fader3.
例如,这是 2 号滑块的卡片。
<div class="card">
<p class="card-title"> Fader 2</p>
<p class="switch">
<input type="range" onchange="updateSliderPWM(this)" id="slider2" min="0" max="100" step="1" value ="0" class="slider">
</p>
<p class="state">Brightness: <span id="sliderValue2"></span> %</p>
</div>
CSS 文件
将以下内容复制到style.css文件。
html { font-family: Arial, Helvetica, sans-serif; display: inline-block; text-align: center; } h1 { font-size: 1.8rem; color: white; } p { font-size: 1.4rem; } .topnav { overflow: hidden; background-color: #0A1128; } body { margin: 0; } .content { padding: 30px; } .card-grid { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .card-title { font-size: 1.2rem; font-weight: bold; color: #034078 } .state { font-size: 1.2rem; color:#1282A2; } .slider { -webkit-appearance: none; margin: 0 auto; width: 100%; height: 15px; border-radius: 10px; background: #FFD65C; outline: none; } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 30px; height: 30px; border-radius: 50%; background: #034078; cursor: pointer; } .slider::-moz-range-thumb { width: 30px; height: 30px; border-radius: 50% ; background: #034078; cursor: pointer; } .switch { padding-left: 5%; padding-right: 5%; }
让我们快速浏览一下设置滑块样式的 CSS 文件的相关部分。在此示例中,我们需要使用供应商前缀外貌属性。
.slider { -webkit-appearance: none; margin: 0 auto; width: 100%; height: 15px; border-radius: 10px; background: #FFD65C; outline: none; } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 30px; height: 30px; border-radius: 50%; background: #034078; cursor: pointer; } .slider::-moz-range-thumb { width: 30px; height: 30px; border-radius: 50% ; background: #034078; cursor: pointer; } .switch { padding-left: 5%; padding-right: 5%; }
Vendor前缀
Vendor前缀允许浏览器在新的 CSS 功能被完全支持之前支持它们。最常用的浏览器使用以下前缀:
- -webkit- Chrome、Safari、较新版本的 Opera、几乎所有 iOS 浏览器、
- -moz- 火狐,
- -o- Opera 的旧版本,
- -ms- Microsoft Edge 和 Internet Explorer。
Vendor前缀是临时的。一旦您使用的浏览器完全支持这些属性,您就不需要它们了。您可以使用以下参考来检查您使用的属性是否需要前缀:http ://shouldprefix.com/
让我们来看看.滑块选择器(设置滑块本身的样式):
.slider { -webkit-appearance: none; margin: 0 auto; width: 100%; height: 15px; border-radius: 10px; background: #FFD65C;outline: none; }
环境-webkit-外观至没有任何覆盖应用于 Google Chrome、Safari 和 Android 浏览器中滑块的默认 CSS 样式。
-webkit-appearance: none;
设置-webkit-appearance为none 自动在其父容器内对齐滑块。
margin: 0 auto;
滑块的宽度设置为100%和高度为15像素。 边界半径被设定为10像素.
margin: 0 auto; width: 100%; height: 15px; border-radius: 10px;
设置滑块的背景颜色并设置outline为none.
background: #FFD65C; outline: none;
然后,格式化滑块手柄。利用-webkit-适用于 Chrome、Opera、Safari 和 Edge 网络浏览器和-moz-对于火狐。
.slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 30px; height: 30px; border-radius: 50%; background: #034078; cursor: pointer; } .slider::-moz-range-thumb { width: 30px; height: 30px; border-radius: 50% ; background: #034078; cursor: pointer; }
设置-webkit-外观和外貌属性没有任何覆盖默认属性。
-webkit-appearance: none; appearance: none;
设置一个具体的宽度,高度和边界半径的处理程序。使用 a标签 设置相同的宽度和高度边界半径的50%的圆圈。
width: 30px; height: 30px; border-radius: 50%;
然后,设置背景颜色并设置光标到一个指针.
background: #034078; cursor: pointer;
随意使用滑块属性给它一个不同的外观。
JavaScript 文件
将以下内容复制到script.js文件。
var gateway = `ws://${window.location.hostname}/ws`; var websocket; window.addEventListener('load', onload); function onload(event) { initWebSocket(); } function getValues(){ websocket.send("getValues"); } function initWebSocket() { console.log('Trying to open a WebSocket connection…'); websocket = new WebSocket(gateway); websocket.onopen = onOpen; websocket.onclose = onClose; websocket.onmessage = onMessage; } function onOpen(event) { console.log('Connection opened'); getValues(); } function onClose(event) { console.log('Connection closed'); setTimeout(initWebSocket, 2000); } function updateSliderPWM(element) { var sliderNumber = element.id.charAt(element.id.length-1); var sliderValue = document.getElementById(element.id).value; document.getElementById("sliderValue"+sliderNumber).innerHTML = sliderValue; console.log(sliderValue); websocket.send(sliderNumber+"s"+sliderValue.toString()); } function onMessage(event) { console.log(event.data); var myObj = JSON.parse(event.data); var keys = Object.keys(myObj); for (var i = 0; i < keys.length; i++){ var key = keys[i]; document.getElementById(key).innerHTML = myObj[key]; document.getElementById("slider"+ (i+1).toString()).value = myObj[key]; } }
以下是这段代码的作用列表:
- 初始化与服务器的 WebSocket 连接;
- 向服务器发送消息以获取当前滑块值;
- 使用响应更新网页上的滑块值;
- 通过 WebSocket 协议处理数据交换。
让我们看一下这段 JavaScript 代码,看看它是如何工作的。
网关是 WebSocket 接口的入口点。window.location.hostname获取当前页面地址(Web 服务器 IP 地址)。
var gateway = ws://${window.location.hostname}/ws;
创建一个名为的新全局变量网络套接字.
var websocket;
添加一个事件监听器,它将调用负载网页加载时的功能。
window.addEventListener('load', onload);
这onload()函数调用初始化WebSocket()初始化与服务器的 WebSocket 连接的函数。
function onload(event) { initWebSocket(); }
这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; }
请注意,当 websocket 连接打开时,我们将调用获取值功能。
function onOpen(event) { console.log('Connection opened'); getValues(); }
这getStates()函数向服务器发送消息获取值获取所有滑块的当前值。然后,我们必须处理在服务器端(ESP32)收到该消息时发生的情况。
function getStates(){ websocket.send("getValues"); }
我们处理通过 websocket 协议收到的消息onMessage()功能。
function onMessage(event) { console.log(event.data); var myObj = JSON.parse(event.data); var keys = Object.keys(myObj); for (var i = 0; i < keys.length; i++){ var key = keys[i]; document.getElementById(key).innerHTML = myObj[key]; document.getElementById("slider"+ (i+1).toString()).value = myObj[key]; } }
服务器以 JSON 格式发送状态,例如:
{ sliderValue1 : 20; sliderValue2: 50; sliderValue3: 0; }
这onMessage()函数简单地遍历所有值并将它们放置在 HTML 页面上的相应位置。
当您移动滑块更新时,PWM()函数会运行。
function updateSliderPWM(element) { var sliderNumber = element.id.charAt(element.id.length-1); var sliderValue = document.getElementById(element.id).value; document.getElementById("sliderValue"+sliderNumber).innerHTML = sliderValue; console.log(sliderValue); websocket.send(sliderNumber+"s"+sliderValue.toString()); }
此函数从滑块中获取值并使用正确的值更新相应的段落。此函数还会向服务器发送消息,以便 ESP32 更新 LED 亮度。
websocket.send(sliderNumber+"s"+sliderValue.toString());
消息以以下格式发送:
- 滑块编号s滑块值
例如,如果您将 3 号滑块移动到位置 40,它将发送以下消息:
3s40
Arduino程序
将以下代码复制到您的 Arduino IDE 中
#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "SPIFFS.h" #include <Arduino_JSON.h> // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Create a WebSocket object AsyncWebSocket ws("/ws"); // Set LED GPIO const int ledPin1 = 12; const int ledPin2 = 13; const int ledPin3 = 14; String message = ""; String sliderValue1 = "0"; String sliderValue2 = "0"; String sliderValue3 = "0"; int dutyCycle1; int dutyCycle2; int dutyCycle3; // setting PWM properties const int freq = 5000; const int ledChannel1 = 0; const int ledChannel2 = 1; const int ledChannel3 = 2; const int resolution = 8; //Json Variable to Hold Slider Values JSONVar sliderValues; //Get Slider Values String getSliderValues(){ sliderValues["sliderValue1"] = String(sliderValue1); sliderValues["sliderValue2"] = String(sliderValue2); sliderValues["sliderValue3"] = String(sliderValue3); String jsonString = JSON.stringify(sliderValues); return jsonString; } // Initialize SPIFFS void initFS() { if (!SPIFFS.begin()) { Serial.println("An error has occurred while mounting SPIFFS"); } else{ Serial.println("SPIFFS mounted successfully"); } } // Initialize WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); } void notifyClients(String sliderValues) { ws.textAll(sliderValues); } 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; message = (char*)data; if (message.indexOf("1s") >= 0) { sliderValue1 = message.substring(2); dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255); Serial.println(dutyCycle1); Serial.print(getSliderValues()); notifyClients(getSliderValues()); } if (message.indexOf("2s") >= 0) { sliderValue2 = message.substring(2); dutyCycle2 = map(sliderValue2.toInt(), 0, 100, 0, 255); Serial.println(dutyCycle2); Serial.print(getSliderValues()); notifyClients(getSliderValues()); } if (message.indexOf("3s") >= 0) { sliderValue3 = message.substring(2); dutyCycle3 = map(sliderValue3.toInt(), 0, 100, 0, 255); Serial.println(dutyCycle3); Serial.print(getSliderValues()); notifyClients(getSliderValues()); } if (strcmp((char*)data, "getValues") == 0) { notifyClients(getSliderValues()); } } } 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; } } void initWebSocket() { ws.onEvent(onEvent); server.addHandler(&ws); } void setup() { Serial.begin(115200); pinMode(ledPin1, OUTPUT); pinMode(ledPin2, OUTPUT); pinMode(ledPin3, OUTPUT); initFS(); initWiFi(); // configure LED PWM functionalitites ledcSetup(ledChannel1, freq, resolution); ledcSetup(ledChannel2, freq, resolution); ledcSetup(ledChannel3, freq, resolution); // attach the channel to the GPIO to be controlled ledcAttachPin(ledPin1, ledChannel1); ledcAttachPin(ledPin2, ledChannel2); ledcAttachPin(ledPin3, ledChannel3); initWebSocket(); // Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); }); server.serveStatic("/", SPIFFS, "/"); // Start server server.begin(); } void loop() { ledcWrite(ledChannel1, dutyCycle1); ledcWrite(ledChannel2, dutyCycle2); ledcWrite(ledChannel3, dutyCycle3); ws.cleanupClients(); }
代码如何运作
让我们快速浏览一下这个项目的相关部分。
在以下变量中插入您的WIFI网络凭据以将 ESP32 连接到您的本地网络:
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
getSliderValues()函数使用当前滑块值创建一个 JSON 字符串。
String getSliderValues(){ sliderValues["sliderValue1"] = String(sliderValue1); sliderValues["sliderValue2"] = String(sliderValue2); sliderValues["sliderValue3"] = String(sliderValue3); String jsonString = JSON.stringify(sliderValues); return jsonString; }
notifyClients()函数用当前滑块值通知所有客户端。每当您为滑块设置新位置时,调用此函数允许我们通知所有客户端的更改。
void notifyClients(String sliderValues) { ws.textAll(sliderValues); }
handleWebSocketMessage(),顾名思义,处理服务器通过 WebSocket 协议从客户端接收消息时发生的事情。我们在 JavaScript 文件中看到,服务器可以接收获取值消息或带有滑块编号和滑块值的消息。
当它收到获取值消息,它发送当前的滑块值。
if (strcmp((char*)data, "getValues") == 0) { notifyClients(getSliderValues()); }
如果它收到另一条消息,我们检查哪个滑块对应于该消息并更新相应的占空比值。最后,我们通知所有客户发生了变化。这是滑块 1 的示例:
if (message.indexOf("1s") >= 0) { sliderValue1 = message.substring(2); dutyCycle1 = map(sliderValue1.toInt(), 0, 100, 0, 255); Serial.println(dutyCycle1); Serial.print(getSliderValues()); notifyClients(getSliderValues()); }
在 loop()函数里面 ,我们更新 PWM 通道的占空比来调整 LED 的亮度。
void loop() { ledcWrite(ledChannel1, dutyCycle1); ledcWrite(ledChannel2, dutyCycle2); ledcWrite(ledChannel3, dutyCycle3); ws.cleanupClients(); }
上传代码和文件
插入网络凭据后,保存代码。转到 Sketch > Show Sketch Folder,然后创建一个名为 data的文件夹。
在该文件夹中,应该保存 HTML、CSS 和 JavaScript 文件。
然后,将代码上传到您的 ESP32 开发板。确保您选择了正确的板和 COM 端口。此外,请确保您已添加网络凭据。
上传代码后,需要上传文件。转到 工具 > ESP32 数据程序上传 并等待文件上传。
全部上传成功后,以 115200 的波特率打开串口监视器。按下 ESP32 EN/RST 按钮,它应该会打印 ESP32 IP 地址。
示范
在本地网络上打开浏览器并粘贴 ESP32 IP 地址。您应该可以访问 Web 服务器页面来控制 LED 的亮度。
移动滑块以控制 LED 的亮度。
打开多个选项卡或使用其它设备连接到 Web 服务器,并注意滑块值几乎会在发生更改时立即更新。
总结
在本教程中,您学习了如何使用 ESP32 构建 Web 服务器,该服务器为具有多个滑块的网页提供服务。滑块允许您控制连接到 ESP32 的 LED 的亮度。此外,我们使用 WebSocket 协议在 ESP32 和客户端之间进行通信。
我们希望您从本教程中学到了很多东西。如果您成功遵循本教程并让项目正常运行,请在下面的评论中告诉我吧。