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

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

Веб-сервер ESP8266 NodeMCU для управления шаговым двигателем через WebSocket Arduino IDE

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

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

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

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

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

Вы можете использовать приведённые выше ссылки или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!

2) Arduino IDE и дополнение для плат ESP8266

Мы будем программировать ESP8266 с помощью Arduino IDE. Поэтому у вас должно быть установлено дополнение ESP8266. Следуйте следующему руководству, если вы ещё этого не сделали:

Если вы хотите использовать VS Code с расширением PlatformIO, следуйте следующему руководству, чтобы научиться программировать ESP8266:

3) Плагин загрузки файловой системы

Для загрузки HTML, CSS и JavaScript файлов, необходимых для сборки этого проекта, в файловую систему ESP8266 (LittleFS) мы будем использовать плагин для Arduino IDE: LittleFS Filesystem uploader. Следуйте следующему руководству, чтобы установить плагин загрузки файловой системы, если вы ещё этого не сделали:

Если вы используете VS Code с расширением PlatformIO, прочитайте следующее руководство, чтобы узнать, как загружать файлы в файловую систему:

4) Библиотеки

Существуют различные способы управления шаговыми двигателями с помощью микроконтроллера. Для управления шаговым двигателем с ESP8266 мы будем использовать библиотеку AccelStepper. Эта библиотека позволяет легко перемещать двигатель на заданное количество шагов, устанавливать его скорость, ускорение и многое другое. Библиотека имеет отличную документацию, объясняющую, как использовать её методы. Вы можете ознакомиться с ней здесь.

Выполните следующие шаги для установки библиотеки в вашей Arduino IDE.

  1. Перейдите в Sketch > Include Library > Manage Libraries…

  2. Найдите «accelstepper».

  3. Установите библиотеку AccelStepper от Mike McCauley. Мы используем версию 1.61.0.

Мы будем строить веб-сервер с использованием следующих библиотек:

Вы можете установить эти библиотеки с помощью Arduino Library Manager. Перейдите в Sketch > Include Library > Manage Libraries и найдите названия библиотек.

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

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

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

Примечание

Вы должны питать драйвер двигателя ULN2003 от внешнего источника питания 5 В.

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

ESP8266

IN1

GPIO 5

IN2

GPIO 4

IN3

GPIO 14

IN4

GPIO 12

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

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

ESP8266 NodeMCU управление шаговым двигателем WebSocket — обзор проекта
  • Веб-страница отображает форму, в которой вы можете ввести количество шагов, на которое вы хотите переместить двигатель, и выбрать направление: по часовой стрелке или против часовой стрелки.

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

ESP32 ESP8266 шаговый двигатель веб-сервер 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-коммуникации и запуска/остановки анимации шестерёнки.

ESP8266 организация файлов — скетч Arduino, index.html, style.css, script.js

Вы должны сохранить HTML, CSS и JavaScript файлы внутри папки data внутри папки скетча Arduino, как показано на предыдущей диаграмме. Мы загрузим эти файлы в файловую систему ESP8266 (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">

Мы хотим отправить результаты формы на сервер (ESP8266) через протокол WebSocket. Поэтому мы добавили кнопку, которая при нажатии (событие onclick) вызывает пользовательскую JavaScript-функцию submitForm(), которая отправляет результаты на сервер, как вы увидите позже в разделе JavaScript.

<button onclick="submitForm()">GO!</button>

Кроме того, мы также добавили абзац для отображения состояния двигателя. Мы добавили тег <span> с идентификатором motor-state, чтобы иметь возможность управлять текстом между тегами <span> с помощью JavaScript.

<p>Motor state: <span id="motor-state">Stopped</span></p>

Наконец, есть абзац, отображающий шестерёнку с id=»gear». Нам нужен этот идентификатор, чтобы заставить шестерёнку двигаться.

<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-формы. Вы можете нажать здесь для подробного объяснения. Давайте рассмотрим соответствующие части для этого руководства.

Мы форматируем текст состояния двигателя: жирный шрифт (font-weight: bold) и красный цвет (color: 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');
    document.getElementById("motor-state").textContent = "motor stopped";
    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").textContent = "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").textContent = "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").textContent = "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-соединение по шлюзу, определённому ранее. Мы также назначаем несколько функций обратного вызова, которые будут вызываться при открытии, закрытии WebSocket-соединения или при получении сообщения.

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").textContent = "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 на сервер (ESP8266) с количеством шагов и направлением, разделёнными символом &.

websocket.send(steps+"&"+direction);

Сервер (ваша плата ESP) отправит сообщение, когда придёт время изменить состояние двигателя. Когда это произойдёт, мы сохраняем сообщение в переменной direction.

Мы проверяем содержимое сообщения и соответственно изменяем состояние двигателя и анимацию шестерёнки.

function onMessage(event) {
  console.log(event.data);
  direction = event.data;
  if (direction=="stop"){
    document.getElementById("motor-state").textContent = "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").textContent = "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
  Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp8266-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 <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <AccelStepper.h>

#define IN1 5
#define IN2 4
#define IN3 14
#define IN4 12
AccelStepper stepper(AccelStepper::HALF4WIRE, 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 notifyStop = false;

// Initialize LittleFS
void initFS() {
  if (!LittleFS.begin()) {
    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);
    notifyStop = true;
    if (direction == "CW"){
      Serial.print("CW");
      stepper.move(steps.toInt());
    }
    else{
      Serial.print("CCW");
      stepper.move(-steps.toInt());
    }
  }
}

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();
  initFS();
  stepper.setMaxSpeed(1000);
  stepper.setAcceleration(100);

  // 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 (stepper.distanceToGo() == 0 && notifyStop == true){
    direction = "stop";
    notifyClients(direction);
    notifyStop = false;
  }
  ws.cleanupClients();
  stepper.run();
}

Просмотреть исходный код

Скетч Arduino очень похож на это руководство, но он обрабатывает связь клиент-сервер с использованием протокола WebSocket. Давайте посмотрим, как он работает, или перейдите к разделу демонстрации.

Подключение библиотек

Сначала подключите необходимые библиотеки. ESP8266WiFi, ESPAsyncTCP и ESPAsyncWebServer для создания веб-сервера, библиотеку LittleFS для использования файловой системы ESP8266 и библиотеку AccelStepper для управления шаговым двигателем.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include "LittleFS.h"
#include <AccelStepper.h>

Пины шагового двигателя

Определите входные пины двигателя. В этом примере мы подключаем к GPIO 5, 4, 14 и 12, но вы можете использовать любые другие подходящие GPIO.

#define IN1 5
#define IN2 4
#define IN3 14
#define IN4 12

Инициализируйте экземпляр библиотеки AccelStepper с именем stepper. Передайте в качестве аргументов: AccelStepper::HALF4WIRE, чтобы указать, что мы управляем шаговым двигателем четырьмя проводами, и входные пины. В случае шагового двигателя 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;

Переменная notifyStop будет использоваться для проверки того, достиг ли двигатель желаемой позиции, и уведомления всех клиентов о том, что двигатель остановлен.

bool notifyStop = false;

initFS()

Функция initFS() инициализирует файловую систему ESP8266 (LittleFS).

// Initialize LittleFS
void initFS() {
  if (!LittleFS.begin()) {
    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);
    notifyStop = true;
    if (direction == "CW"){
      Serial.print("CW");
      stepper.move(steps.toInt());
    }
    else{
      Serial.print("CCW");
      stepper.move(-steps.toInt());
    }
  }
}

Мы разбиваем сообщение, чтобы получить количество шагов и направление.

message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());

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

notifyClients(direction);

Мы устанавливаем переменную notifyStop в true, чтобы позже иметь возможность уведомить об остановке двигателя.

notifyStop = true;

Затем устанавливаем количество шагов, на которое вы хотите переместить двигатель, в зависимости от направления (отрицательное количество шагов для направления против часовой стрелки и положительное количество шагов для направления по часовой стрелке). Для этого мы используем функцию move(). Функция move() не перемещает двигатель напрямую, она просто задаёт новое количество шагов, на которое мы хотим переместить двигатель. Затем двигатель перейдёт в нужную позицию по одному шагу за раз, используя функцию run() в loop().

if (direction == "CW"){
  Serial.print("CW");
  stepper.move(steps.toInt());
}
else{
  Serial.print("CCW");
  stepper.move(-steps.toInt());
}

Примечание

Функция move() устанавливает целевую позицию относительно текущей позиции. Функция moveTo() устанавливает абсолютную целевую позицию (каждая позиция определяется заданным количеством шагов). Для получения дополнительной информации ознакомьтесь с документацией библиотеки.

Настройка 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();

Инициализируйте WebSocket-коммуникацию.

initWebSocket();

Вызовите функцию initFS() для инициализации файловой системы.

initFS();

И установите максимальную скорость и ускорение шагового двигателя.

stepper.setMaxSpeed(1000);
stepper.setAcceleration(100);

Обработка запросов

Затем обработайте веб-сервер. Когда вы получаете запрос по корневому (/) 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().

В loop() — это то место, где двигатель перемещается по одному шагу за раз с помощью функции run(). Эта функция заставляет двигатель двигаться до тех пор, пока он не достигнет желаемой позиции (установленной функцией move()).

stepper.run();

Кроме того, есть условие if для проверки, нужно ли уведомлять клиентов о том, что двигатель остановился. Функция distanceToGo() возвращает количество оставшихся шагов до достижения желаемой позиции. Так что когда она возвращает 0, это означает, что двигатель остановился. Мы также проверяем, что переменная notifyStop равна true (это означает, что двигатель ранее вращался).

if (stepper.distanceToGo() == 0 && notifyStop == true){
  direction = "stop";
  notifyClients(direction);
  notifyStop = false;
}

Когда эти условия выполнены, мы уведомляем клиентов о том, что двигатель остановился.

direction = "stop";
notifyClients(direction);

Наконец, установите переменную notifyStop в false. Она станет true снова только тогда, когда плата получит запрос на перемещение двигателя.

Загрузка кода и файлов

После вставки сетевых учётных данных сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку с именем data.

Arduino IDE — открыть папку скетча для создания папки data

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

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

Кнопка загрузки Arduino 2.0

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

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

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

Загрузка образа файловой системы ESP8266

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

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

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

Веб-сервер ESP8266 NodeMCU — шаговый двигатель WebSocket — двигатель вращается

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

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

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

Веб-сервер ESP8266 NodeMCU — шаговый двигатель WebSocket — двигатель остановлен

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

Посмотрите видео ниже для живой демонстрации.

Заключение

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

Если вы хотите узнать больше о HTML, CSS, JavaScript и протоколах связи клиент-сервер для создания веб-серверов ESP32 и ESP8266 с нуля, как мы это сделали в этом руководстве, обязательно ознакомьтесь с нашей электронной книгой:

Ознакомьтесь со следующими ресурсами, чтобы узнать больше о плате ESP8266:

Мы надеемся, что это руководство было для вас полезным.

Спасибо за чтение.