ESP32 Веб-сервер: Управление шаговым двигателем (WebSocket)

В этом уроке вы узнаете, как создать веб-сервер на ESP32, который отображает веб-страницу для управления шаговым двигателем. Веб-страница позволяет ввести количество шагов и выбрать направление: по часовой стрелке или против часовой стрелки. Кроме того, она показывает текущее состояние двигателя: вращается он или остановлен. Связь между клиентом и сервером осуществляется по протоколу WebSocket. Все клиенты обновляются с текущим состоянием двигателя.

ESP32 Веб-сервер Управление шаговым двигателем WebSocket Arduino IDE

Этот урок является второй частью статьи ESP32 Веб-сервер: Управление шаговым двигателем (HTML-форма), но его также можно использовать как самостоятельное руководство.

Для лучшего понимания работы этого проекта рекомендуем ознакомиться со следующими уроками:

Необходимые условия

Прежде чем продолжить, убедитесь, что выполнены следующие условия.

1) Необходимые компоненты

Для выполнения этого урока вам понадобятся следующие компоненты:

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) Библиотеки

Для сборки этого проекта вам необходимо установить следующие библиотеки:

Вы можете установить эти библиотеки через менеджер библиотек Arduino. Откройте менеджер библиотек, нажав на значок Library на левой боковой панели.

Найдите ESPAsyncWebServer и установите ESPAsyncWebServer by ESP32Async.

Установка ESPAsyncWebServer ESP32 Arduino IDE

Затем установите библиотеку AsyncTCP. Найдите AsyncTCP и установите AsyncTCP by ESP32Async.

Установка AsyncTCP ESP32 Arduino IDE

5) Схема подключения

Следующая схема показывает подключение шагового двигателя к ESP32.

ESP32 с шаговым двигателем 28BYJ-48 и ULN2003A Схема подключения

Примечание: для питания драйвера ULN2003APG следует использовать внешний источник питания 5 В.

Драйвер двигателя

ESP32

IN1

GPIO 19

IN2

GPIO 18

IN3

GPIO 5

IN4

GPIO 17

Обзор проекта

На следующем изображении показана веб-страница, которую вы создадите для этого проекта.

Управление шаговым двигателем ESP32 Веб-сервер Демонстрация
  • Веб-страница отображает форму, в которой вы можете ввести количество шагов для перемещения двигателя и выбрать направление: по часовой стрелке или против часовой стрелки.

  • Она также показывает состояние двигателя: motor spinning или motor stopped. Кроме того, отображается значок шестерёнки, который вращается, пока двигатель работает. Шестерёнка вращается по часовой или против часовой стрелки в соответствии с выбранным направлением.

ESP32 Шаговый двигатель Веб-сервер WebSocket Принцип работы
  • Сервер и клиент обмениваются данными по протоколу 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-соединения и запуска/остановки анимации шестерёнки.

Организация файлов Arduino sketch index.html style.css script.js

Вы должны сохранить файлы 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.

Arduino IDE Open Sketch Folder для создания папки data

Внутри этой папки сохраните файлы HTML, CSS и JavaScript.

Затем загрузите код на плату ESP32. Убедитесь, что выбрана правильная плата и COM-порт. Также убедитесь, что вы указали свои сетевые учётные данные.

После загрузки кода необходимо загрузить файлы.

Нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.

Если эта опция отсутствует, значит вы не установили плагин загрузки файловой системы. Ознакомьтесь с этим уроком.

ESP32 Sketch Data Upload LittleFS Arduino IDE

Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. В противном случае загрузка не удастся.

Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку ESP32 EN/RST, и он должен вывести IP-адрес ESP32.

Демонстрация

Откройте веб-браузер или несколько окон веб-браузера в вашей локальной сети, и вы получите доступ к веб-странице для управления двигателем. Отправьте форму для управления двигателем.

ESP32 Шаговый двигатель Веб-сервер WebSocket Мотор вращается

Шестерёнка на веб-странице начинает вращаться в правильном направлении, и двигатель начинает работать.

28BYJ-48 подключён к модулю драйвера ULN2003

Когда он останавливается, шестерёнка на веб-странице и состояние двигателя изменяются соответственно.

ESP32 Шаговый двигатель Веб-сервер WebSocket Мотор остановлен

Обратите внимание, что если подключено несколько клиентов, все клиенты обновляют состояние двигателя практически мгновенно.

Заключение

В этом уроке вы узнали, как управлять шаговым двигателем с помощью веб-сервера на 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