ESP32 Веб-сервер: Управление шаговым двигателем (WebSocket)
В этом уроке вы узнаете, как создать веб-сервер на ESP32, который отображает веб-страницу для управления шаговым двигателем. Веб-страница позволяет ввести количество шагов и выбрать направление: по часовой стрелке или против часовой стрелки. Кроме того, она показывает текущее состояние двигателя: вращается он или остановлен. Связь между клиентом и сервером осуществляется по протоколу WebSocket. Все клиенты обновляются с текущим состоянием двигателя.
Этот урок является второй частью статьи ESP32 Веб-сервер: Управление шаговым двигателем (HTML-форма), но его также можно использовать как самостоятельное руководство.
Для лучшего понимания работы этого проекта рекомендуем ознакомиться со следующими уроками:
Необходимые условия
Прежде чем продолжить, убедитесь, что выполнены следующие условия.
1) Необходимые компоненты
Для выполнения этого урока вам понадобятся следующие компоненты:
ESP32 (читайте Лучшие платы разработки ESP32)
Источник питания 5 В
2) Arduino IDE и дополнение для плат ESP32
Мы будем программировать ESP32 с помощью Arduino IDE. Поэтому у вас должно быть установлено дополнение для ESP32. Следуйте этому уроку, если вы ещё этого не сделали:
Если вы хотите использовать VS Code с расширением PlatformIO, следуйте этому уроку для программирования ESP32:
3) Плагин загрузки файловой системы
Для загрузки файлов HTML, CSS и JavaScript в файловую систему ESP32 (LittleFS), мы будем использовать плагин для Arduino IDE: LittleFS Filesystem uploader. Следуйте этому уроку для установки плагина, если вы ещё этого не сделали:
Если вы используете VS Code с расширением PlatformIO, прочитайте следующий урок, чтобы узнать, как загружать файлы в файловую систему:
4) Библиотеки
Для сборки этого проекта вам необходимо установить следующие библиотеки:
ESPAsyncWebServer от ESP32Async
AsyncTCP от ESP32Async
Вы можете установить эти библиотеки через менеджер библиотек Arduino. Откройте менеджер библиотек, нажав на значок Library на левой боковой панели.
Найдите ESPAsyncWebServer и установите ESPAsyncWebServer by ESP32Async.
Затем установите библиотеку AsyncTCP. Найдите AsyncTCP и установите AsyncTCP by ESP32Async.
5) Схема подключения
Следующая схема показывает подключение шагового двигателя к ESP32.
Примечание: для питания драйвера ULN2003APG следует использовать внешний источник питания 5 В.
Драйвер двигателя |
ESP32 |
|---|---|
IN1 |
GPIO 19 |
IN2 |
GPIO 18 |
IN3 |
GPIO 5 |
IN4 |
GPIO 17 |
Обзор проекта
На следующем изображении показана веб-страница, которую вы создадите для этого проекта.
Веб-страница отображает форму, в которой вы можете ввести количество шагов для перемещения двигателя и выбрать направление: по часовой стрелке или против часовой стрелки.
Она также показывает состояние двигателя: motor spinning или motor stopped. Кроме того, отображается значок шестерёнки, который вращается, пока двигатель работает. Шестерёнка вращается по часовой или против часовой стрелки в соответствии с выбранным направлением.
Сервер и клиент обмениваются данными по протоколу WebSocket.
Когда вы нажимаете кнопку GO!, вызывается JavaScript-функция, которая отправляет сообщение по протоколу WebSocket со всей информацией: количество шагов и направление (3). Сообщение имеет следующий формат:
steps&direction
Так, если вы отправите 2000 шагов и направление по часовой стрелке, будет отправлено следующее сообщение:
2000&CW
Одновременно состояние двигателя на веб-странице изменится, и шестерёнка начнёт вращаться в соответствующем направлении (2).
Затем сервер получает сообщение (4) и вращает двигатель соответственно (5).
Когда двигатель перестаёт вращаться (6), ESP отправляет сообщение клиенту(ам), также по протоколу WebSocket, информируя об остановке двигателя (7).
Клиент(ы) получают это сообщение и обновляют состояние двигателя на веб-странице (8).
Организация файлов
Файлы, которые вы хотите загрузить в файловую систему ESP, должны быть размещены в папке data внутри папки проекта. Мы переместим три файла в эту папку:
index.html — для построения веб-страницы;
style.css — для стилизации веб-страницы;
script.js — для обработки WebSocket-соединения и запуска/остановки анимации шестерёнки.
Вы должны сохранить файлы HTML, CSS и JavaScript в папку data внутри папки Arduino-скетча, как показано на предыдущей диаграмме. Мы загрузим эти файлы в файловую систему ESP32 (LittleFS).
Вы можете скачать все файлы проекта:
HTML-файл
Создайте файл с именем index.html со следующим содержимым:
<!DOCTYPE html>
<html>
<head>
<title>Stepper Motor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>
<div class="topnav">
<h1>Stepper Motor Control <i class="fas fa-cogs"></i></h1>
</div>
<div class="content">
<form>
<input type="radio" id="CW" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" id="CCW" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" id="steps" name="steps">
</form>
<button onclick="submitForm()">GO!</button>
<p>Motor state: <span id="motor-state">Stopped</span></p>
<p><i id="gear" class="fas fa-cog"></i> </p>
</div>
</body>
<script src="script.js"></script>
</html>
Этот HTML-файл очень похож на тот, что использовался в предыдущем уроке. Вы можете нажать здесь для полного объяснения HTML-файла.
Мы добавили id к HTML-элементам, которыми хотим управлять с помощью JavaScript — радиокнопкам и полю ввода:
радиокнопка по часовой стрелке: id=»CW»
радиокнопка против часовой стрелки: id=»CCW»
поле ввода количества шагов: id=»steps»
<input type="radio" id="CW" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" id="CCW" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" id="steps" name="steps">
Мы хотим отправлять результаты формы на сервер (ESP32) по протоколу WebSocket. Поэтому мы добавили кнопку, которая при нажатии (событие onclick) вызывает пользовательскую JavaScript-функцию submitForm(), отправляющую результаты на сервер, как вы увидите далее в разделе JavaScript.
<button onclick="submitForm()">GO!</button>
Кроме того, мы добавили абзац для отображения состояния двигателя. Мы добавили тег <span> с id motor-state, чтобы иметь возможность манипулировать текстом между тегами <span> с помощью JavaScript.
<p>Motor state: <span id="motor-state">Stopped</span></p>
Наконец, есть абзац с шестерёнкой с id=»gear». Нам нужен этот id для анимации шестерёнки.
<p><i id="gear" class="fas fa-cog"></i> </p>
Не забудьте, что вам нужно подключить JavaScript-файл (script.js) в HTML-файле следующим образом:
<script src="script.js"></script>
CSS-файл
Создайте файл с именем style.css со следующим содержимым:
html {
font-family: Arial, Helvetica, sans-serif;
}
h1 {
font-size: 1.8rem;
color: white;
}
p{
font-size: 20px;
text-align: center;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
text-align: center;
}
body {
margin: 0;
}
.content {
padding: 20px;
max-width: max-content;
margin: 0 auto;
}
input[type=number], select {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
form{
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}
button {
background-color: #034078;
border: none;
padding: 14px 20px;
text-align: center;
font-size: 20px;
border-radius: 4px;
transition-duration: 0.4s;
width: 100%;
color: white;
cursor: pointer;
}
button:hover {
background-color: #1282A2;
}
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 50%;
width: 16px;
height: 16px;
border: 2px solid #999;
transition: 0.2s all linear;
margin-right: 5px;
position: relative;
top: 4px;
}
input[type="radio"]:checked{
border: 6px solid #1282A2;
}
#motor-state{
font-weight: bold;
color: red;
}
#gear{
font-size:100px;
color:#2d3031cb;
}
.spin {
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
.spin-back {
-webkit-animation:spin-back 4s linear infinite;
-moz-animation:spin-back 4s linear infinite;
animation:spin-back 4s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
Мы уже рассматривали, как работает CSS для HTML-формы. Вы можете нажать здесь для подробного объяснения. Давайте рассмотрим основные части для этого урока.
Мы форматируем текст состояния двигателя: жирный шрифт (bold) и красный цвет (red). Для обращения к конкретному id в CSS используется # перед id (#motor-state).
#motor-state{
font-weight: bold;
color: red;
}
Следующие строки форматируют значок шестерёнки — цвет и размер. Помните, что его id — gear, поэтому мы обращаемся к нему через #gear:
#gear{
font-size:100px;
color:#2d3031cb;
}
Затем мы форматируем два класса spin и spin-back, которые пока не привязаны ни к одному HTML-элементу. Мы привяжем классы spin и spin-back к шестерёнке с помощью JavaScript, когда двигатель начнёт вращаться.
Эти классы используют свойство animation для вращения шестерёнки. Чтобы узнать больше о свойстве animation, рекомендуем ознакомиться с этим кратким уроком.
.spin {
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
.spin-back {
-webkit-animation:spin-back 4s linear infinite;
-moz-animation:spin-back 4s linear infinite;
animation:spin-back 4s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }
JavaScript-файл
Создайте файл с именем script.js со следующим содержимым:
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onload);
var direction;
function onload(event) {
initWebSocket();
}
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');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function submitForm(){
const rbs = document.querySelectorAll('input[name="direction"]');
direction;
for (const rb of rbs) {
if (rb.checked) {
direction = rb.value;
break;
}
}
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
var steps = document.getElementById("steps").value;
websocket.send(steps+"&"+direction);
}
function onMessage(event) {
console.log(event.data);
direction = event.data;
if (direction=="stop"){
document.getElementById("motor-state").innerHTML = "motor stopped"
document.getElementById("motor-state").style.color = "red";
document.getElementById("gear").classList.remove("spin", "spin-back");
}
else if(direction=="CW" || direction=="CCW"){
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
}
}
Давайте посмотрим, как работает JavaScript для этого проекта.
Gateway — это точка входа в интерфейс WebSocket. window.location.hostname получает текущий адрес страницы (IP-адрес веб-сервера).
var gateway = `ws://${window.location.hostname}/ws`;
Создайте новую глобальную переменную websocket.
var websocket;
Создайте ещё одну глобальную переменную direction, которая будет хранить текущее направление двигателя: по часовой стрелке, против часовой стрелки или остановлен.
var direction;
Добавьте обработчик события, который вызовет функцию onload при загрузке веб-страницы.
window.addEventListener('load', onload);
Функция onload() вызывает функцию initWebSocket() для инициализации WebSocket-соединения с сервером.
function onload(event) {
initWebSocket();
}
Функция initWebSocket() инициализирует WebSocket-соединение по заданному ранее gateway. Мы также назначаем несколько функций обратного вызова, которые будут срабатывать при открытии, закрытии соединения или при получении сообщения.
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');
}
Если по какой-либо причине WebSocket-соединение закрывается, вызываем функцию initWebSocket() снова через 2000 миллисекунд (2 секунды).
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
Наконец, нам нужно обработать, что происходит при отправке формы и при получении клиентом нового сообщения (событие onMessage).
При отправке формы вызывается функция submitForm():
function submitForm(){
Сначала мы определяем, какая радиокнопка выбрана. Значение выбранной радиокнопки сохраняется в переменной direction.
const rbs = document.querySelectorAll('input[name="direction"]');
var direction;
for (const rb of rbs) {
if (rb.checked) {
direction = rb.value;
break;
}
}
Затем мы меняем текст состояния двигателя на motor spinning… и его цвет на синий. Мы обращаемся к HTML-элементу по его id motor-state.
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
Затем мы проверяем, выбрано ли направление по часовой или против часовой стрелки, чтобы вращать шестерёнку в правильном направлении. Для этого мы добавляем класс spin или spin-back к элементу с id gear.
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
Мы получаем введённое количество шагов и сохраняем его в переменной steps.
var steps = document.getElementById("steps").value;
Затем мы отправляем сообщение по протоколу WebSocket на сервер (ESP32) с количеством шагов и направлением, разделёнными символом &.
websocket.send(steps+"&"+direction);
Сервер (ваша плата ESP) отправит сообщение, когда придёт время изменить состояние двигателя. Когда это произойдёт, мы сохраняем сообщение в переменной direction.
Мы проверяем содержимое сообщения и соответственно изменяем состояние двигателя и анимацию шестерёнки.
function onMessage(event) {
console.log(event.data);
direction = event.data;
if (direction=="stop"){
document.getElementById("motor-state").innerHTML = "motor stopped"
document.getElementById("motor-state").style.color = "red";
document.getElementById("gear").classList.remove("spin", "spin-back");
}
else if(direction=="CW" || direction=="CCW"){
document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";
if (direction=="CW"){
document.getElementById("gear").classList.add("spin");
}
else{
document.getElementById("gear").classList.add("spin-back");
}
}
}
Скетч Arduino
Перед загрузкой вы можете воспользоваться следующей ссылкой:
Скопируйте следующий код в Arduino IDE. Вставьте свои сетевые учётные данные, и он сразу заработает.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp32-websocket/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <Stepper.h>
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
String message = "";
// 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");
//Variables to save values from HTML form
String direction ="STOP";
String steps;
bool newRequest = false;
// Initialize LittleFS
void initLittleFS() {
if (!LittleFS.begin(true)) {
Serial.println("An error has occurred while mounting LittleFS");
}
else{
Serial.println("LittleFS 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 state) {
ws.textAll(state);
}
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;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Serial.print("steps");
Serial.println(steps);
Serial.print("direction");
Serial.println(direction);
notifyClients(direction);
newRequest = true;
}
}
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());
//Notify client of motor current state when it first connects
notifyClients(direction);
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);
}
void setup() {
// Serial port for debugging purposes
Serial.begin(115200);
initWiFi();
initWebSocket();
initLittleFS();
myStepper.setSpeed(5);
// Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/index.html", "text/html");
});
server.serveStatic("/", LittleFS, "/");
server.begin();
}
void loop() {
if (newRequest){
if (direction == "CW"){
myStepper.step(steps.toInt());
Serial.print("CW");
}
else{
myStepper.step(-steps.toInt());
}
newRequest = false;
notifyClients("stop");
}
ws.cleanupClients();
}
Скетч Arduino очень похож на предыдущий урок, но обрабатывает взаимодействие клиент-сервер с использованием протокола WebSocket. Давайте посмотрим, как он работает, или перейдите к разделу демонстрации.
Подключение библиотек
Сначала подключите необходимые библиотеки. WiFi, AsyncTCP и ESPAsyncWebServer — для создания веб-сервера, библиотека LittleFS — для работы с файловой системой ESP32, и библиотека Stepper — для управления шаговым двигателем.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <Stepper.h>
Выводы шагового двигателя и количество шагов на оборот
Определите количество шагов на оборот вашего шагового двигателя — в нашем случае это 2048:
const int stepsPerRevolution = 2048; // change this to fit the number of steps per revolution
Определите выводы подключения двигателя. В этом примере мы подключаем к GPIO 19, 18, 5 и 17, но вы можете использовать любые другие подходящие GPIO.
#define IN1 19
#define IN2 18
#define IN3 5
#define IN4 17
Инициализируйте экземпляр библиотеки Stepper с именем myStepper. Передайте в качестве аргументов количество шагов на оборот и выводы. В случае шагового двигателя 28BYJ-48 порядок выводов: IN1, IN3, IN2, IN4 — для вашего двигателя он может отличаться.
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
Сетевые учётные данные
Вставьте свои сетевые учётные данные в следующие строки.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer и AsyncWebSocket
Создайте объект AsyncWebServer с именем server на порту 80.
AsyncWebServer server(80);
Библиотека ESPAsyncWebServer включает плагин WebSocket, который упрощает обработку WebSocket-соединений. Создайте объект AsyncWebSocket с именем ws для обработки соединений по пути /ws.
AsyncWebSocket ws("/ws");
Инициализация переменных
Следующие переменные будут хранить направление и количество шагов, полученных по протоколу WebSocket. При первом запуске программы двигатель остановлен.
String direction ="stop";
String steps;
Переменная newRequest будет использоваться для проверки поступления нового запроса. Затем в loop() мы запустим вращение двигателя при получении нового запроса — когда переменная newRequest равна true.
bool newRequest = false;
initLittleFS()
Функция initLittleFS() инициализирует файловую систему ESP32.
void initLittleFS() {
if (!LittleFS.begin(true)) {
Serial.println("An error has occurred while mounting LittleFS");
}
else{
Serial.println("LittleFS mounted successfully");
}
}
initWiFi()
Функция initWiFi() инициализирует WiFi.
// 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());
}
Обработка WebSocket — Сервер
Ранее вы видели, как обрабатывать WebSocket-соединение на стороне клиента (браузера). Теперь давайте посмотрим, как это делается на стороне сервера.
Уведомление всех клиентов
Функция notifyClients() уведомляет всех клиентов сообщением, содержащим то, что вы передаёте в качестве аргумента. В данном случае мы хотим уведомить всех клиентов о текущем состоянии двигателя при каждом изменении.
void notifyClients(String state) {
ws.textAll(state);
}
Класс 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;
message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Serial.print("steps");
Serial.println(steps);
Serial.print("direction");
Serial.println(direction);
notifyClients(direction);
newRequest = true;
}
}
Мы разбиваем сообщение, чтобы получить количество шагов и направление.
message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());
Затем мы уведомляем всех клиентов о направлении двигателя, чтобы все клиенты обновили состояние двигателя на веб-интерфейсе.
notifyClients(direction);
Наконец, устанавливаем переменную newRequest в true, чтобы двигатель начал вращаться в loop().
newRequest = true;
Настройка 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 %s\n", client->id(), client->remoteIP().toString().c_str());
//Notify client of motor current state when it first connects
notifyClients(direction);
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 представляет тип события. Он может принимать следующие значения:
WS_EVT_CONNECT — когда клиент подключается;
WS_EVT_DISCONNECT — когда клиент отключается;
WS_EVT_DATA — когда от клиента получен пакет данных;
WS_EVT_PONG — в ответ на запрос ping;
WS_EVT_ERROR — когда от клиента получена ошибка.
Есть раздел для уведомления клиента о текущем состоянии двигателя при первом подключении:
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
//Notify client of motor current state when it first connects
notifyClients(direction);
break;
Инициализация WebSocket
Наконец, функция initWebSocket() инициализирует протокол WebSocket.
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
setup()
В setup() инициализируем Serial Monitor.
Serial.begin(115200);
Вызываем функцию initWiFi() для инициализации WiFi.
initWiFi();
Вызываем функцию initWebSocket() для инициализации WebSocket.
initWebSocket();
И устанавливаем скорость шагового двигателя в об/мин.
myStepper.setSpeed(5);
Обработка запросов
Затем настраиваем веб-сервер. Когда вы получаете запрос по корневому URL (/) — это происходит при обращении к IP-адресу ESP — отправляем HTML-текст для построения веб-страницы:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/index.html", "text/html");
});
Когда HTML-файл загружается в вашем браузере, он запрашивает файлы CSS и JavaScript. Эти статические файлы сохранены в том же каталоге (LittleFS). Поэтому мы можем просто добавить следующую строку для обслуживания файлов в каталоге по запросу от корневого URL. CSS и JavaScript файлы будут обслуживаться автоматически.
server.serveStatic("/", LittleFS, "/");
Наконец, запускаем сервер.
server.begin();
loop()
Давайте рассмотрим секцию loop().
Если переменная newRequest равна true, мы проверяем направление вращения: CW или CCW. Если CW, мы перемещаем двигатель на количество шагов, сохранённое в переменной steps, используя метод step() объекта myStepper. Для вращения двигателя против часовой стрелки нужно просто передать количество шагов со знаком минус (-).
if (direction == "CW"){
// Spin the stepper clockwise direction
myStepper.step(steps.toInt());
}
else{
// Spin the stepper counterclockwise direction
myStepper.step(-steps.toInt());
}
После вращения двигателя устанавливаем переменную newRequest в false, чтобы можно было обнаружить новые запросы.
newRequest = false;
Кроме того, уведомляем всех клиентов о том, что двигатель остановился.
notifyClients("stop");
Загрузка кода и файлов
После вставки сетевых учётных данных сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку data.
Внутри этой папки сохраните файлы HTML, CSS и JavaScript.
Затем загрузите код на плату ESP32. Убедитесь, что выбрана правильная плата и COM-порт. Также убедитесь, что вы указали свои сетевые учётные данные.
После загрузки кода необходимо загрузить файлы.
Нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.
Если эта опция отсутствует, значит вы не установили плагин загрузки файловой системы. Ознакомьтесь с этим уроком.
Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. В противном случае загрузка не удастся.
Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку ESP32 EN/RST, и он должен вывести IP-адрес ESP32.
Демонстрация
Откройте веб-браузер или несколько окон веб-браузера в вашей локальной сети, и вы получите доступ к веб-странице для управления двигателем. Отправьте форму для управления двигателем.
Шестерёнка на веб-странице начинает вращаться в правильном направлении, и двигатель начинает работать.
Когда он останавливается, шестерёнка на веб-странице и состояние двигателя изменяются соответственно.
Обратите внимание, что если подключено несколько клиентов, все клиенты обновляют состояние двигателя практически мгновенно.
Заключение
В этом уроке вы узнали, как управлять шаговым двигателем с помощью веб-сервера на ESP32. Веб-сервер предоставляет веб-страницу для управления шаговым двигателем с помощью формы, результаты которой отправляются на ESP32 по протоколу WebSocket.
Это часть 3 серии уроков по управлению шаговым двигателем с помощью веб-сервера. Вы можете следовать частям 1 и 2 по следующей ссылке:
Если вы хотите узнать больше о HTML, CSS, JavaScript и протоколах связи клиент-сервер для создания веб-серверов ESP32 и ESP8266 с нуля, как мы это сделали в этом уроке, обязательно ознакомьтесь с электронной книгой:
—
Источник: :doc:`ESP32 Web Server: Control Stepper Motor (WebSocket) <../stepper-motor-esp32-websocket/index>` — Random Nerd Tutorials, Rui Santos & Sara Santos