ESP32:Примеры/Машинка-робот с дистанционным WiFi-управлением

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Машинка-робот с дистанционным WiFi-управлением

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

В этом проекте затрагивается несколько тем, о которых уже рассказывалось в других руководствах по ESP32 на нашем сайте. Поэтому для лучшего понимания того, что будет написано ниже, советуем с ними ознакомиться (если еще не ознакомились). Вот они:

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

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

WiFi

Робот будет управляться через WiFi при помощи ESP32. Мы создадим веб-интерфейс, с помощью которого можно будет управлять роботом, и доступ к этому интерфейсу можно будет получить с любого устройства (ПК, ноутбука, планшета и т.д.), подключенного к вашей локальной сети.

Веб-интерфейс для управления роботом

Веб-интерфейс будет состоять из 6 компонентов: 5 кнопок вверху (вперед, назад, влево, вправо и стоп) и одного ползунка внизу (для изменения скорости движения, на нем можно выбрать скорость 0%, 25%, 50%, 75% и 100%).

Примечание

О том, как создавать кнопки, читайте в руководстве «Веб-сервер на базе ESP32: управление выходными контактами», а о создании ползунка – в руководстве «Веб-сервер на базе ESP32: удаленное управление сервоприводом».

Шасси

Мы будем использовать набор, показанный на картинке ниже. Он называется Smart Robot Chassis Kit. Его можно купить в большинстве онлайн-магазинов. Он стоит около 10 долларов и очень легко собирается. Подойдет и любой другой набор, но в нем также должны быть два DC-мотора.

Драйвер моторов L298N

Есть много разных способов для управления DC-моторами (электромоторами постоянного тока). Мы же воспользуемся драйвером моторов L298N, т.к. он позволяет значительно облегчить управление скоростью и направлением двух DC-моторов.

Питание

Моторам требуется много электроэнергии, поэтому нам также понадобится внешний источник питания. Это значит, что нам будут нужны два разных источника питания – один для DC-моторов, и один для ESP32. Плату ESP32 мы будем питать от пауэрбанка (вроде тех, что используются для зарядки смартфонов).

Моторы будут питаться от четырех 1.5-вольтовых АА-батареек. Подойдут, впрочем, и перезаряжаемые батарейки, и любой другой подходящий источник питания.

Справочная информация

Краткое введение в драйвер моторов L298N

Как уже говорилось выше, есть много разных способов для управления DC-моторами. Наш метод подходит для большинства моторов, используемых в любительских проектах и требующих 6-12 вольт. Мы воспользуемся драйвером моторов L298N, который может управлять нагрузкой до 3 ампер при 35 вольтах, что как раз подходит для нашего проекта. Он также позволяет одновременно управлять двумя DC-моторами, что идеально для создания робота на колесах.

Распиновка драйвера моторов L298N

Давайте взглянем на контакты драйвера моторов L298N и разберемся, как он работает.

Драйвер моторов L298N оснащен двумя клеммниками, которые находятся по разные стороны платы. Левый клеммник состоит из портов OUT1 и OUT2, а правый – из портов OUT3 и OUT4.

  • OUT1 – плюсовой порт для DC-мотора А;
  • OUT2 – минусовой порт для DC-мотора А;
  • OUT3 – плюсовой порт для DC-мотора B;
  • OUT4 – минусовой порт для DC-мотора B;

Внизу находится еще один клеммник, но уже на 3 порта: +12V, GND и +5V. Порт +12V используется для питания моторов. Порт +5V используется для питания чипа L298N. Но если поставить перемычку (на изображении выше она помечена оранжевым цветом), чип будет питаться от питания для DC-моторов. В этом случае подавать 5-вольтовое питание на порт +5V не нужно.

Примечание

Если напряжение питания превышает 12 вольт, перемычку нужно снять и подать 5-вольтовое питание на порт +5V.

Важно отметить, что хотя порт называется +12V, для нашего проекта (и с поставленной перемычкой) подойдет напряжение в диапазоне между 6 и 12 вольтами. Мы будем питать проект от четырех 1.5-вольтовых АА-батареек общим напряжением примерно 6 вольт.

Итого:

  • +12V: Это порт, к которому нужно подключить питание;
  • GND: Это порт для заземления;
  • +5V: Если перемычка снята, на этот порт нужно подать 5-вольтовое питание. Если перемычка стоит, этот порт будет источником 5-вольтового питания;
  • Перемычка: Если перемычка на месте, чип питается от питания моторов. Если перемычка снята, на порт +5V нужно подать 5-вольтовое питание. Если напряжение питания превышает 12 вольт, перемычку нужно снять.

В правом нижнем углу платы располагается 4 входных контакта, а по бокам от них – еще 2 контакта. 4 входных контакта предназначены для управления направлением вращения DC-моторов, а 2 контакта по бокам – для управления скоростью каждого мотора.

  • IN1 – входной контакт 1 для мотора А;
  • IN2 – входной контакт 2 для мотора А;
  • IN3 – входной контакт 1 для мотора B;
  • IN4 – входной контакт 2 для мотора B;
  • ENA – контакт для управления скоростью мотора A;
  • ENB – контакт для управления скоростью мотора B;

Рядом с контактами ENA и ENB находится по одному контакту +5V, и по умолчанию на них стоят перемычки. Чтобы управлять скоростью моторов, эти перемычки нужно снять.

Управление DC-моторами с помощью L298N

Теперь, когда мы познакомились с драйвером моторов L298N, давайте разберемся, как использовать его для управления DC-моторами.

Контакты ENA и ENB

Контакты ENA и ENB (для управления скоростью моторов) используются как переключатели между режимами «вкл» и «выкл», но также могут принимать более вариативные ШИМ-значения.

Например:

  • Если подать на контакт ENA значение «HIGH», это разрешит управление мотором А на максимальной скорости;
  • Если подать на контакт ENA значение «LOW», это выключит мотор А;
  • Если подать ШИМ-сигнал на контакт ENA, это позволит управлять скоростью мотора. Скорость мотора пропорциональна коэффициенту заполнения ШИМ. Но помните, что если задать маленький коэффициент заполнения, моторы, возможно, просто не будут вращаться, а лишь постоянно жужжать.
Сигнал, подаваемый на ENA или ENB Как ведет себя мотор
HIGH Мотор работает на максимальной скорости
LOW Мотор не работает
ШИМ Мотор работает (скорость пропорциональна коэффициенту заполнения)
Входные контакты

Входные контакты предназначены для управления направлением вращения моторов. Контакты IN1 и IN2 управляют мотором А, а контакты IN3 и IN4мотором B.

Если подать на контакт IN1 значение LOW, а на IN2 – значение HIGH, мотор A будет крутиться вперед. Если инвертировать значения (на IN1 подать HIGH, а на IN2LOW), мотор A будет крутиться назад. Мотор B управляется аналогичным образом.

Таким образом, если вы хотите, чтобы робот ехал вперед, оба мотора должны крутиться вперед, а чтобы он ехал назад – оба мотора должны крутиться назад.

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

НАПРАВЛЕНИЕ INPUT 1 INPUT 2 INPUT 3 INPUT 4
Вперед 0 1 0 1
Назад 1 0 1 0
Вправо 0 1 0 0
Влево 0 0 0 1
Стоп 0 0 0 0

Как работает этот код

Мы подробно рассказывали о коде для создания веб-сервера на базе ESP32 в других руководствах. Поэтому здесь мы затронем лишь те фрагменты, что касаются нашего проекта.

Задаем настройки для подключения к WiFi

Начинаем с того, что задаем в двух переменных ниже SSID и пароль для локальной WiFi-сети, к которой нужно подключить ESP32.

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

Создаем переменные для контактов драйвера моторов L298N

Далее создаем переменные для входных контактов (IN1, IN2, IN3, IN4), а также для контактов управления скоростью вращения моторов (ENA и ENB) драйвера моторов L298N.

// мотор 1:
int motor1Pin1 = 27;
int motor1Pin2 = 26;
int enable1Pin = 14;

// мотор 2:
int motor2Pin1 = 33;
int motor2Pin2 = 25;
int enable2Pin = 32;

Задаем свойства ШИМ

Если мы хотим управлять скоростью вращения моторов, нам нужно будет отправлять ШИМ-сигнал на контакты ENA и ENB. Следовательно, нам нужно создать переменные для свойств ШИМ-сигнала вроде частоты, канала ШИМ, разрешения и коэффициента заполнения.

// переменные для свойств широтно-импульсной модуляции (ШИМ):
const int freq = 30000;
const int pwmChannel = 0;
const int resolution = 8;
int dutyCycle = 0;
Примечание

Более подробно об использовании ШИМ вместе с ESP32 читайте в руководстве«Изменение яркости светодиода при помощи ШИМ».

setup()

В блоке setup() делаем выходными контакты для моторов.

// переключаем контакты моторов в режим «OUTPUT»:
pinMode(motor1Pin1, OUTPUT);
pinMode(motor1Pin2, OUTPUT);
pinMode(motor2Pin1, OUTPUT);
pinMode(motor2Pin2, OUTPUT);

Затем настраиваем ШИМ-канал при помощи заданных ранее свойств.

ledcSetup(pwmChannel, freq, resolution);

В следующих двух строчках подключаем этот ШИМ-канал к контактам ENA и ENB. Таким образом, на контакты ENA и ENB будет идти один и тот же ШИМ-сигнал.

ledcAttachPin(enable1Pin, pwmChannel); 
ledcAttachPin(enable2Pin, pwmChannel);

Наконец, при помощи функции ledcWrite() генерируем ШИМ-сигнал с заранее заданным коэффициентом заполнения.

ledcWrite(pwmChannel, dutyCycle);

Код ниже подключает ESP32 к локальной WiFi-сети и печатает в монитор порта ее IP-адрес.

// подключаемся к WiFi-сети при помощи заданных выше SSID и пароля:
Serial.print("Connecting to ");  //  "Подключаемся к "
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
// печатаем в мониторе порта
// локальный IP-адрес и запускаем веб-сервер:
Serial.println("");
Serial.println("WiFi connected.");  //  "Подключились к WiFi-сети."
Serial.println("IP address: ");  //  "IP-адрес: "
Serial.println(WiFi.localIP());
server.begin();

loop()

В блоке loop() ESP32 всегда будет прослушивать входящих клиентов и при получении запроса сохранять входящие данные.

WiFiClient client = server.available();  // Запускаем прослушку 
                                         // входящих клиентов.
  if (client) {                          // Если подключился 
                                         // новый клиент,
  Serial.println("New Client.");         // печатаем в монитор порта  
                                         // сообщение об этом.
  String currentLine = "";               // Создаем строку
                                         // для хранения данных,
                                         // пришедших от клиента.
  while (client.connected()) {           // Запускаем цикл while(), 
                                         // который будет работать,
                                         // пока клиент подключен.
    if (client.available()) {            // Если у клиента
                                         // есть байты, которые
                                         // можно прочесть,
      char c = client.read();            // считываем байт 
      Serial.write(c);                   // и печатаем его 
                                         // в мониторе порта.
      header += c;
      if (c == '\n') {                   // Если полученный байт – 
                                         // это символ новой строки.
        // Если мы получили два символа новой строки подряд,
        // то это значит, что текущая строка пуста.
        // Это конец HTTP-запроса клиента, поэтому отправляем ответ:
        if (currentLine.length() == 0) {
          // HTTP-заголовки всегда начинаются с кода ответа
          // (например, с «HTTP/1.1 200 OK»),
          // а также с информации о типе контента,
          // чтобы клиент знал, что получает.
          // После этого пишем пустую строчку:
          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html");
          client.println("Connection: close");
                     //  "Соединение: отключено"
          client.println();

Управляем роботом

В следующем фрагменте задано несколько операторов if() и else(), с помощью которых проверяется, какая команда отправляется роботу – «вперед», «назад», «влево», «вправо» или «стоп». Затем контактам моторов отправляются значения, соответствующие выбранной команде.

Например, чтобы повернуть робота влево, всем контактам нужно отправить значение «LOW» (кроме «motor2Pin2», которому нужно отправить «HIGH»).

// Этот код отвечает за управление контактами моторов
// согласно тому, какие нажаты кнопки на веб-странице:
if (header.indexOf("GET /forward") >= 0) {
  Serial.println("Forward");  //  "Вперед"
  digitalWrite(motor1Pin1, LOW);
  digitalWrite(motor1Pin2, HIGH); 
  digitalWrite(motor2Pin1, LOW);
  digitalWrite(motor2Pin2, HIGH);
}  else if (header.indexOf("GET /left") >= 0) {
  Serial.println("Left");  //  "Влево"
  digitalWrite(motor1Pin1, LOW); 
  digitalWrite(motor1Pin2, LOW); 
  digitalWrite(motor2Pin1, LOW);
  digitalWrite(motor2Pin2, HIGH);
}  else if (header.indexOf("GET /stop") >= 0) {
  Serial.println("Stop");  //  "Стоп"
  digitalWrite(motor1Pin1, LOW); 
  digitalWrite(motor1Pin2, LOW); 
  digitalWrite(motor2Pin1, LOW);
  digitalWrite(motor2Pin2, LOW);             
} else if (header.indexOf("GET /right") >= 0) {
  Serial.println("Right");  //  "Вправо"
  digitalWrite(motor1Pin1, LOW); 
  digitalWrite(motor1Pin2, HIGH); 
  digitalWrite(motor2Pin1, LOW);
  digitalWrite(motor2Pin2, LOW);    
} else if (header.indexOf("GET /reverse") >= 0) {
  Serial.println("Reverse");  //  "Назад"
  digitalWrite(motor1Pin1, HIGH);
  digitalWrite(motor1Pin2, LOW); 
  digitalWrite(motor2Pin1, HIGH);
  digitalWrite(motor2Pin2, LOW);          
}

Как видите, каждая команда – предназначенная для того, чтобы переместить робота в каком-либо направлении или остановить его – представляет собой уникальную комбинацию сигналов «HIGH» и «LOW». Более подробно об этом рассказывалось выше, в разделе «Входные контакты».

Показываем веб-страницу

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

// Показываем веб-страницу:
client.println("<!DOCTYPE HTML><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// При помощи CSS задаем стиль кнопок.
// Попробуйте поэкспериментировать
// с атрибутами «background-color» и «font-size»,
// чтобы стилизовать кнопки согласно своим предпочтениям: 
client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
client.println(".button { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-color: #4CAF50;");
client.println("border: none; color: white; padding: 12px 28px; text-decoration: none; font-size: 26px; margin: 1px; cursor: pointer;}");
client.println(".button2 {background-color: #555555;}</style>");
client.println("<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script></head>");
            
// веб-страница:        
client.println("<p><button class=\"button\" onclick=\"moveForward()\">FORWARD</button></p>");
client.println("<div style=\"clear: both;\"><p><button class=\"button\" onclick=\"moveLeft()\">LEFT </button>");
client.println("<button class=\"button button2\" onclick=\"stopRobot()\">STOP</button>");
client.println("<button class=\"button\" onclick=\"moveRight()\">RIGHT</button></p></div>");
client.println("<p><button class=\"button\" onclick=\"moveReverse()\">REVERSE</button></p>");
client.println("<p>Motor Speed: <span id=\"motorSpeed\"></span></p>");          
client.println("<input type=\"range\" min=\"0\" max=\"100\" step=\"25\" id=\"motorSlider\" onchange=\"motorSpeed(this.value)\" value=\"" + valueString + "\"/>");
            
client.println("<script>$.ajaxSetup({timeout:1000});");
client.println("function moveForward() { $.get(\"/forward\"); {Connection: close};}");
client.println("function moveLeft() { $.get(\"/left\"); {Connection: close};}");
client.println("function stopRobot() {$.get(\"/stop\"); {Connection: close};}");
client.println("function moveRight() { $.get(\"/right\"); {Connection: close};}");
client.println("function moveReverse() { $.get(\"/reverse\"); {Connection: close};}");
client.println("var slider = document.getElementById(\"motorSlider\");");
client.println("var motorP = document.getElementById(\"motorSpeed\"); motorP.innerHTML = slider.value;");
client.println("slider.oninput = function() { slider.value = this.value; motorP.innerHTML = this.value; }");
client.println("function motorSpeed(pos) { $.get(\"/?value=\" + pos + \"&\"); {Connection: close};}</script>");
client.println("</html>");
Примечание

О создании веб-страниц для веб-сервера подробно рассказывается в руководстве «Веб-сервер на базе ESP32: управление выходными контактами».

Управляем скоростью мотора

Во фрагменте кода ниже мы считываем значение ползунка и, исходя из него, меняем скорость мотора. Такой же метод используется в руководстве «Веб-сервер на базе ESP32: удаленное управление сервоприводом».

// Пример HTTP-запроса: «GET /?value=100& HTTP/1.1»;
// Он задает коэффициент заполнения ШИМ на 100% (255):
if(header.indexOf("GET /?value=")>=0) {
  pos1 = header.indexOf('=');
  pos2 = header.indexOf('&');
  valueString = header.substring(pos1+1, pos2);
  // Задаем скорость мотора:
  if (valueString == "0") {
    ledcWrite(pwmChannel, 0);
    digitalWrite(motor1Pin1, LOW); 
    digitalWrite(motor1Pin2, LOW); 
    digitalWrite(motor2Pin1, LOW);
    digitalWrite(motor2Pin2, LOW);   
  }
  else { 
    dutyCycle = map(valueString.toInt(), 25, 100, 200, 255);
    ledcWrite(pwmChannel, dutyCycle);
    Serial.println(valueString);
  } 
}

В переменную «valueString» сохраняется текущее значение ползунка.

Если ползунок стоит на значении «0», моторы не будут работать. То есть мы задаем ШИМ-каналу коэффициент заполнения «0» и отправляем всем контактам моторов значения «LOW».

if (valueString == "0") {
 ledcWrite(pwmChannel, 0);
 digitalWrite(motor1Pin1, LOW);
 digitalWrite(motor1Pin2, LOW);
 digitalWrite(motor2Pin1, LOW);
 digitalWrite(motor2Pin2, LOW);
}

Если на ползунке стоит ненулевое значение, то моторы начнут вращаться. Расчет коэффициента заполнения на основе значения ползунка осуществляется при помощи Arduino-функции map(). В ней начальным значением коэффициента заполнения задано «200», т.к. если задать меньше, робот просто не будет двигаться (моторы лишь будут издавать странное жужжание).

else {
 dutyCycle = map(valueString.toInt(), 25, 100, 200, 255);
 ledcWrite(pwmChannel, dutyCycle);
 Serial.println(valueString);
}

После этого мы задаем коэффициент заполнения для ШИМ-канала, с его помощью и управляя скоростью моторов.

Тестируем веб-сервер

Теперь давайте протестируем веб-сервер. Убедитесь, что вставили в код SSID и пароль для своей WiFi-сети. Также проверьте, правильные ли в IDE Arduino выбраны COM-порт и плата. После этого жмите на кнопку «Загрузка». Когда загрузка завершится, откройте монитор порта на скорости 115200 бод.

Чтобы узнать IP-адрес ESP32, нажмите на ней на кнопку EN.

Отключите ESP32 от компьютера и запитайте от пауэрбанка.

Убедитесь, что все четыре АА-батарейки стоят на месте, а движковый переключатель стоит в положении «вкл».

Откройте браузер и впишите в адресную строку IP-адрес ESP32, чтобы получить доступ к веб-серверу. Этот веб-сервер можно открыть на любом устройстве (ПК, ноутбуке, смартфоне и т.д.), подключенном к вашей локальной сети, и с его помощью управлять роботом.

Теперь, если все настроено правильно, в браузере должен появиться веб-интерфейс с кнопками и ползунком, с помощью которого можно будет управлять машинкой-роботом.

Внимание!

Если моторы крутятся в неправильном направлении, нужно просто поменять местами провода, подключенные к моторам. Например, провода, подключенные к контактам OUT1 и OUT2 или OUT3 и OUT4. Это должно решить проблему.

Поздравляем! Вы своими руками сделали робота с дистанционным WiFi-управлением! Он может перемещаться вперед, назад, влево и вправо. Вы также можете остановить его, нажав на кнопку «STOP». Более того, вы даже можете менять скорость движения робота при помощи ползунка.

Робот работает превосходно и мгновенно отвечает на команды.

Итого

Ну что, нравится робот? Советуем не останавливаться на достигнутом и попробовать оснастить машинку новыми фичами – например, так:

  • Подключите к ней RGB-светодиод и запрограммируйте ESP32 так, чтобы его цвет менялся в зависимости от направления, в котором едет машинка;
  • Подключите к ней ультразвуковой датчик, чтобы она останавливалась, увидев препятствие.

На наш взгляд, это хороший проект для того, чтобы практиковать на нем концепты, о которых рассказывалось в других руководствах по ESP32 на нашем сайте.

Необходимое оборудование

Схема

Примечание

На этой схеме изображена 36-контактная версия платы ESP32 DEVKIT DOIT V1, поэтому если у вас какая-то другая модель, обязательно сверьтесь с ее распиновкой.

Начинаем с подключения ESP32 к драйверу моторов. Для размещения ESP32 и построения цепи можно воспользоваться либо небольшой контактной, либо печатной макетной платой. В таблице ниже показано, с помощью каких контактов ESP32 и драйвер моторов L298N необходимо подключить друг к другу.

Драйвер моторов L298N ESP32
IN1 GPIO27
IN2 GPIO26
ENA GPIO14
IN3 GPIO33
IN4 GPIO25
ENB GPIO32

После этого подключите оба мотора к клеммникам драйвера моторов L298N. В целях ослабления скачков напряжения мы рекомендуем припаять к плюсовому и минусовому контактам каждого мотора керамический конденсатор на 100 нФ (как показано на схеме выше).

Кроме того, можно припаять движковый переключатель к красному проводу, идущему от батареек. Благодаря ему вы сможете включать/отключать питание, идущее к моторам и драйверу моторов.

Наконец, запитайте моторы при помощи четырех АА-батареек, подключив их к портам питания драйвера моторов. Так как наш робот должен быть портативным, ESP32 будет питаться от пауэрбанка. Прикрепите его к шасси, например, при помощи ленты-липучки.

Код

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

// загружаем библиотеку для WiFi:
#include <WiFi.h>

// вставляем ниже SSID и пароль для своей WiFi-сети:
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// создаем объект сервера и задаем ему порт «80»:
WiFiServer server(80);

// переменная для хранения HTTP-запроса:
String header;

// мотор 1:
int motor1Pin1 = 27; 
int motor1Pin2 = 26; 
int enable1Pin = 14; 

// мотор 2:
int motor2Pin1 = 33; 
int motor2Pin2 = 25; 
int enable2Pin = 32;

// переменные для свойств широтно-импульсной модуляции (ШИМ):
const int freq = 30000;
const int pwmChannel = 0;
const int resolution = 8;
int dutyCycle = 0;

// переменные для расшифровки HTTP-запроса GET:
String valueString = String(5);
int pos1 = 0;
int pos2 = 0;

void setup() {
  Serial.begin(115200);
  
  // переключаем контакты моторов в режим «OUTPUT»:
  pinMode(motor1Pin1, OUTPUT);
  pinMode(motor1Pin2, OUTPUT);
  pinMode(motor2Pin1, OUTPUT);
  pinMode(motor2Pin2, OUTPUT);

  // задаем настройки ШИМ-канала:
  ledcSetup(pwmChannel, freq, resolution);
  
  // подключаем ШИМ-канал 0 к контактам ENA и ENB,
  // т.е. к GPIO-контактам для управления скоростью вращения моторов:
  ledcAttachPin(enable1Pin, pwmChannel);
  ledcAttachPin(enable2Pin, pwmChannel);

  // подаем на контакты ENA и ENB 
  // ШИМ-сигнал с коэффициентом заполнения «0»:
  ledcWrite(pwmChannel, dutyCycle);
  
  // подключаемся к WiFi-сети при помощи заданных выше SSID и пароля:
  Serial.print("Connecting to ");  //  "Подключаемся к "
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // печатаем в мониторе порта
  // локальный IP-адрес и запускаем веб-сервер:
  Serial.println("");
  Serial.println("WiFi connected.");  //  "Подключились к WiFi-сети."
  Serial.println("IP address: ");  //  "IP-адрес: "
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();  // Запускаем прослушку 
                                           // входящих клиентов.

  if (client) {                            // Если подключился 
                                           // новый клиент,
    Serial.println("New Client.");         // печатаем в монитор порта  
                                           // сообщение об этом.
    String currentLine = "";               // Создаем строку
                                           // для хранения данных,
                                           // пришедших от клиента.
    while (client.connected()) {           // Запускаем цикл while(), 
                                           // который будет работать,
                                           // пока клиент подключен.
      if (client.available()) {            // Если у клиента
                                           // есть байты, которые
                                           // можно прочесть,
        char c = client.read();            // считываем байт 
        Serial.write(c);                   // и печатаем его 
                                           // в мониторе порта.
        header += c;
        if (c == '\n') {                   // Если полученный байт – 
                                           // это символ новой строки.
          // Если мы получили два символа новой строки подряд,
          // то это значит, что текущая строка пуста.
          // Это конец HTTP-запроса клиента, поэтому отправляем ответ:
          if (currentLine.length() == 0) {
            // HTTP-заголовки всегда начинаются с кода ответа
            // (например, с «HTTP/1.1 200 OK»),
            // а также с информации о типе контента,
            // чтобы клиент знал, что получает.
            // После этого пишем пустую строчку:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
                       //  "Соединение: отключено"
            client.println();
            
            // Этот код отвечает за управление контактами моторов
            // согласно тому, какие нажаты кнопки на веб-странице:
            if (header.indexOf("GET /forward") >= 0) {
              Serial.println("Forward");  //  "Вперед"
              digitalWrite(motor1Pin1, LOW);
              digitalWrite(motor1Pin2, HIGH); 
              digitalWrite(motor2Pin1, LOW);
              digitalWrite(motor2Pin2, HIGH);
            }  else if (header.indexOf("GET /left") >= 0) {
              Serial.println("Left");  //  "Влево"
              digitalWrite(motor1Pin1, LOW); 
              digitalWrite(motor1Pin2, LOW); 
              digitalWrite(motor2Pin1, LOW);
              digitalWrite(motor2Pin2, HIGH);
            }  else if (header.indexOf("GET /stop") >= 0) {
              Serial.println("Stop");  //  "Стоп"
              digitalWrite(motor1Pin1, LOW); 
              digitalWrite(motor1Pin2, LOW); 
              digitalWrite(motor2Pin1, LOW);
              digitalWrite(motor2Pin2, LOW);             
            } else if (header.indexOf("GET /right") >= 0) {
              Serial.println("Right");  //  "Вправо"
              digitalWrite(motor1Pin1, LOW); 
              digitalWrite(motor1Pin2, HIGH); 
              digitalWrite(motor2Pin1, LOW);
              digitalWrite(motor2Pin2, LOW);    
            } else if (header.indexOf("GET /reverse") >= 0) {
              Serial.println("Reverse");  //  "Назад"
              digitalWrite(motor1Pin1, HIGH);
              digitalWrite(motor1Pin2, LOW); 
              digitalWrite(motor2Pin1, HIGH);
              digitalWrite(motor2Pin2, LOW);          
            }
            // Показываем веб-страницу:
            client.println("<!DOCTYPE HTML><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // При помощи CSS задаем стиль кнопок.
            // Попробуйте поэкспериментировать
            // с атрибутами «background-color» и «font-size»,
            // чтобы стилизовать кнопки согласно своим предпочтениям: 
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-color: #4CAF50;");
            client.println("border: none; color: white; padding: 12px 28px; text-decoration: none; font-size: 26px; margin: 1px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}</style>");
            client.println("<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script></head>");
            
            // веб-страница:        
            client.println("<p><button class=\"button\" onclick=\"moveForward()\">FORWARD</button></p>");
            client.println("<div style=\"clear: both;\"><p><button class=\"button\" onclick=\"moveLeft()\">LEFT </button>");
            client.println("<button class=\"button button2\" onclick=\"stopRobot()\">STOP</button>");
            client.println("<button class=\"button\" onclick=\"moveRight()\">RIGHT</button></p></div>");
            client.println("<p><button class=\"button\" onclick=\"moveReverse()\">REVERSE</button></p>");
            client.println("<p>Motor Speed: <span id=\"motorSpeed\"></span></p>");          
            client.println("<input type=\"range\" min=\"0\" max=\"100\" step=\"25\" id=\"motorSlider\" onchange=\"motorSpeed(this.value)\" value=\"" + valueString + "\"/>");
            
            client.println("<script>$.ajaxSetup({timeout:1000});");
            client.println("function moveForward() { $.get(\"/forward\"); {Connection: close};}");
            client.println("function moveLeft() { $.get(\"/left\"); {Connection: close};}");
            client.println("function stopRobot() {$.get(\"/stop\"); {Connection: close};}");
            client.println("function moveRight() { $.get(\"/right\"); {Connection: close};}");
            client.println("function moveReverse() { $.get(\"/reverse\"); {Connection: close};}");
            client.println("var slider = document.getElementById(\"motorSlider\");");
            client.println("var motorP = document.getElementById(\"motorSpeed\"); motorP.innerHTML = slider.value;");
            client.println("slider.oninput = function() { slider.value = this.value; motorP.innerHTML = this.value; }");
            client.println("function motorSpeed(pos) { $.get(\"/?value=\" + pos + \"&\"); {Connection: close};}</script>");
           
            client.println("</html>");
            
            // Пример HTTP-запроса: «GET /?value=100& HTTP/1.1»;
            // Он задает коэффициент заполнения ШИМ на 100% (255):
            if(header.indexOf("GET /?value=")>=0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              valueString = header.substring(pos1+1, pos2);
              // Задаем скорость мотора:
              if (valueString == "0") {
                ledcWrite(pwmChannel, 0);
                digitalWrite(motor1Pin1, LOW); 
                digitalWrite(motor1Pin2, LOW); 
                digitalWrite(motor2Pin1, LOW);
                digitalWrite(motor2Pin2, LOW);   
              }
              else { 
                dutyCycle = map(valueString.toInt(), 25, 100, 200, 255);
                ledcWrite(pwmChannel, dutyCycle);
                Serial.println(valueString);
              } 
            }         
            // HTTP-ответ заканчивается еще одной пустой строкой:
            client.println();
            // Выходим из цикла while():
            break;
          } else {  // Если получили символ новой строки,
                    // то очищаем переменную «currentLine»:
            currentLine = "";
          }
        } else if (c != '\r') {  // Если получили что-либо,
                                 // кроме символа возврата каретки...
          currentLine += c;      // ...добавляем эти данные
                                 // в конец переменной «currentLine»
        }
      }
    }
    // Очищаем переменную «header»:
    header = "";
    // Отключаем соединение:
    client.stop();
    Serial.println("Client disconnected.");  // "Клиент отключен."
    Serial.println("");
  }
}

См.также

Внешние ссылки