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