Делаем робота на ESP32 с использованием сервоприводов

Сегодня мы пройдемся по шагам создания робота на основе ESP32 и четырех сервоприводов.

Шаг 1. О проекте

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

Я экспериментировал с различными платами разработки ESP32 и недавно заказал T-Beam серии TTGO, которая поставляется с разъемом для батареи, что дает возможность добавить свой собственный 18650 Lipo аккумулятор. Это действительно облегчает задачу регулирования мощности при создании небольшого робота, так как на нем уже есть цепь аккумулятора и зарядного устройства.

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

Шаг 2. Комплектующие

Для создания робота на ESP32 мы должны собрать ряд деталей, которые перечислим ниже:

  • сервоприводы x 4,
  • колеса x 4,
  • лента из 5 неопикселей (по желанию),
  • ESP32 с аккумуляторной батареей или ESP32 с внешней батареей,
  • кусочек плексигласа, который можно разрезать и просверлить для формирования шасси,
  • макетная плата,
  • некоторые провода (также я использовал мини-JST-разъем в качестве разъема, но всё можно просто припаять),
  • штыревые разъёмы для сервоприводов (вы сможете просто подключить сервоприводы к разъему платы) x 4,
  • некоторые пластиковые опоры.

Шаг 3. Сборка шасси

Переходим к сборке шасси нашего ESP32 робота.

Я хотел сделать настоящее шасси, используя какой-нибудь плексиглас или пластик. Даже можно было бы использовать старую пластиковую коробку из под завтраков или еды на вынос. Я вырезал кусок плексигласа немного шире, чем плата ESP32 (см. фото выше), но примерно такой же длины и отметил, где хотел бы добавить 4 отверстия для крепления ESP32, используя опоры монтажной платы.

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

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

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

Шаг 4. Создаем свою макетную плату

Переходим к созданию своей макетной платы, которая имеет в английском два названия - stripboard или veroboard. Суть этой платы в том, чтобы собирать схемы методом пайки деталей. Плата имеет сетку отверстий 0.9 мм, расположенных с шагом 2.54 мм (0.1 дюйма). С одной стороны у платы есть прямолинейные, изолированные друг от друга полосы медной фольги, а с другой стороны монтируются детали и перемычки. Многим нравится такой метод сборки и монтажа, когда нужно собрать небольшое устройство с одной или двумя микросхемами (чипами).

Я хотел сделать небольшую плату, которая позволила бы мне подключить ESP32 к плате и легко её снять при необходимости. Поэтому сделал её, как показано на фото выше. Были добавлены несколько выводов для штыревых разъемов под сервоприводы, а также лента неопикселей. Также добавлены 2 маленьких jst-разъема, которые у меня были, чтобы можно было использовать их для питания от ESP32, а также для обеспечения серво соединений.

Я обрезал одну из медных дорожек на нижней стороне платы (см. фото выше), чтобы сигнальный вывод для каждого сервопривода был разным, затем я использовал небольшой соединитель для проводов, чтобы переместить его по проводу на одну дорожку, чтобы два jst-контакта соединялись с одной стороной или другой.

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

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

Шаг 5. Схема соединения

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

Шаг 6. Собираем всё вместе

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

Шаг 7. Код для ESP32

Код для нашей ESP32 можно скачать или скопировать ниже:

#include <SPI.h>
#include <WiFi.h>
#include <Wire.h> 
#include <Servo.h>
#include <Adafruit_NeoPixel.h>
#include "SSD1306.h" 
//#include "images.h"

#ifdef __AVR__
  #include <avr/power.h>
#endif
static const int servoPin = 25; //  works with TTGO
static const int servoPin2 = 33; // works with TTGO

#define PIN 14 // Neopixel works with TTGO
Adafruit_NeoPixel strip = Adafruit_NeoPixel(5, PIN, NEO_GRB + NEO_KHZ800);

SSD1306 display(0x3c, 21, 22);
Servo servo1;
Servo servo2;

//network credentials
const char* ssid     = "your ssid here";
const char* password = "your password here";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Decode HTTP GET value
String valueString = String(5);
int pos1 = 0;
int pos2 = 0;

void setup() {
  Serial.begin(115200);
  servo1.attach(servoPin);
  servo2.attach(servoPin2);
  
   //Display something on Oled if required
  display.init();
  display.flipScreenVertically();  
  //display.setFont(ArialMT_Plain_10);
  display.setFont(ArialMT_Plain_16);
  //display.setFont(ArialMT_Plain_24);
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);

  
  // This is for Trinket 5V 16MHz, you can remove these three lines if you are not using a Trinket
  #if defined (__AVR_ATtiny85__)
    if (F_CPU == 16000000) clock_prescale_set(clock_div_1);
  #endif
  // End of trinket special code
  
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'
  
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    display.drawString(5, 20, "STARTING..");
    delay(500);
    display.clear();
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  //Display IP address in Oled
  display.drawString(0, 0, "IP: ");
  display.drawString(20, 0,(WiFi.localIP().toString())); 
  display.setFont(ArialMT_Plain_24);
  display.drawString(5, 20, "READY");
  display.display();
  
  server.begin();
}

void loop(){
  colorWipe(strip.Color(0, 0, 255), 100); // Blue
  colorWipe(strip.Color(255, 0, 0), 100); // Red
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {                             // If a new client connects,
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            
            // Controls the motor pins according to the button pressed
            if (header.indexOf("GET /forward") >= 0) {
              Serial.println("Forward");
              servo1.write(170);
              servo2.write(10);
            }  else if (header.indexOf("GET /left") >= 0) {
              Serial.println("Left");
              servo1.write(90);
              servo2.write(10); 
            }  else if (header.indexOf("GET /stop") >= 0) {
              Serial.println("Stop");
              servo1.write(90);
              servo2.write(90);             
            } else if (header.indexOf("GET /right") >= 0) {
              Serial.println("Right");
              servo1.write(170);
              servo2.write(90);   
            } else if (header.indexOf("GET /reverse") >= 0) {
              Serial.println("Reverse");
              servo1.write(10);
              servo2.write(170);         
            }
            // Display the HTML web page
            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 to style the buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            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>");
            
            // Web Page        
            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>");
            
            //Request example: GET /?value=100& HTTP/1.1 - sets PWM duty cycle to 100% = 255
            if(header.indexOf("GET /?value=")>=0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              valueString = header.substring(pos1+1, pos2);
              //Set motor speed value
              if (valueString == "0") {
                 servo1.write(90);
                 servo2.write(90);  
              }
              else { 
                Serial.println(valueString);
              } 
            }         
            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}
// Fill the dots one after the other with a color
void colorWipe(uint32_t c, uint8_t wait) {
  //for(uint16_t i=0; i<strip.numPixels(); i++) {
  for(uint16_t i=1; i<4; i++) {
    strip.setPixelColor(i, c);
    strip.show();
    delay(wait);
  }
}

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

Шаг 8. Контроль и тестирование

Нам нужно было несколько простых элементов управления.  В интернете есть много хороших примеров того, как запустить веб-сервер, и отобразили элементы управления, чтобы вы могли заставить робота ездить. Я немного изменил примеры, чтобы использовать серводвигатели и добавил код для использования ленты неопикселей. Плюс сделал отображение на экране Oled IP-адреса к которому мне нужно подключиться, чтобы я мог управлять роботом.

2 декабря 2019 в 20:35 | Обновлено 20 февраля 2020 в 02:39 (редакция)
Опубликовано:
Уроки,

Добавить комментарий

Ваш E-mail не будет никому виден. Обязательные поля отмечены *