Машинка, c робо-рукой, управляемая по wifi через телефон, с помощью Arduino+NodeMCU

Тема статьи: 
Электронные поделки и механизмы

Добрый день, друзья! Эта статья посвящена электронной поделке, которую давно хотелось реализовать — и вот дошли руки. Идея этой поделки проста — сделать маленькую четырехколесную машинку, на ней стоит робо-рука и их движениями можно управлять с телефона или планшета через wifi.

pic54.jpg

Компоненты, которые было решено использовать для этой задачи следующие:
1) корпус — пластиковые детали, напечатаные на 3D принтере+картон;

pic02.jpg

2) робо-рука — робот-манипулятор MeArm;

pic03.jpg

3) электроника — плата NodeMCU, плата ArduinoNano, желтые мотор-редукторы для Arduino, драйвер моторов на основе микросхемы L298N, шесть батареек типа AA на 1.2 В, кнопка включения-выключения.

pic04.jpg

Давайте все по-порядку.

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

pic05.jpg

Затем все необходимые пластиковые детали были напечатаны на 3D принтере.
Сначала из куска картона (пятислойного, профиль В), вырезаем прямоугольник, размером 140/180 мм.

pic06.jpg

Затем из напечатанных на 3D принтере деталей нужно собрать такую конструкцию:

pic07.jpg

То есть в каждое из 4х пластиковых колес вставляется диск, у которого с одной стороны - отверстие для винтика (диаметром 3мм), которым колесо будет прикручиваться к пластиковой детали-переходнику, с другой стороны - отверстие в форме шестигранника.
Пластиковые детали-переходники, в свою очередь имеют форму шестигранника с одной стороны, и форму цилиндра с другой, а также внутри отверстие диаметром 3мм. Концом-шестигранником деталь вставляется в диск колеса и с обратной стороны диска прикручивается винтиком.
Эти детали-переходники для передних и задних колес немного отличаются по форме и размерам. Задние колеса присоединяются к моторам, поэтому на том конце, который имеет форму цилиндра есть углубление по форме повторяющее вал мотора. То есть эта деталь нанизывается на вал с этого конца. И по размерам, вся эта деталь поменьше. Деталь для передних колес цилиндрической частью вставляется в подшипник (диаметр внешний — 22 мм, толщина 7 мм, диаметр внутреннего отверстия — 8 мм).
Подшипник вставлен в дополнительную пластиковую деталь, которая поддерживает его и прикрепляется к тележке.
Колеса крепятся к двум пластиковым деталям имеющим горизонтальные пластинки с отверстиями для винтиков, с помощью которых они прикручиваются справа и слева к куску картона.

pic08.jpg

От них вниз отходят вертикальными пластины, спроектированные так, чтобы к задним двум прикручивался мотор, а к двум передним пластиковая деталь, в которую вставляется подшипник передних колес.
Таким образом, размер всей тележки получается 140/180/75 мм.

pic09.jpg

pic10.jpg

Все эти детали печатаются и собираются вместе.

pic11.jpg

pic12.jpg

pic13.jpg

pic14.jpg

pic15.jpg

pic16.jpg

pic17.jpg

Моторы
Два задних колеса тележки присоединяются к моторам-редукторам для Arduino.

pic18.jpg

У них есть возможность реверсивного вращения, то есть и в одну и в другую сторону. Просто подключив моторы к плате Arduino или NodeMCU мы можем запрограммировать их на вращение либо в одну сторону, либо в другую, в зависимости от подключения контактов, но реверсивное вращение обеспечить не сможем. Причем NodeMCU выдает только 3.3В — этого не хватит, чтобы запустить моторы. От Arduino, выдающей 5В — моторы можно запустить, но лучше этого не делать, так как в моменты запуска и остановки моторов могут происходить броски тока, превышающие 5B.
Поэтому, для подключения моторов и управления ими нужен дополнительный модуль — драйвер, который называется H-мостом. С его помощью можно обеспечить реверсивное вращение и питание моторов. В проектах для Arduino часто используется драйвер для мотора на основе микросхемы L298N.

pic19.jpg

Вот схема ее контактов:

pic20.jpg

Что мы видим на этой плате:
1) Клемма мотора A (OUT1 и OUT2) — к этим клеммам подключается первый мотор-редуктор.
2) Клемма мотора В (OUT3 и OUT4) — к этим клеммам подключается второй мотор-редуктор.
3) Питание моторов (VSS) — вход для подключения внешнего источника питания (максимально допустимое напряжение +35 В), в нашем случае это блок батареек на 4.8 В.
4) Земля (GND) — общий провод, к которому подключается отрицательный полюс внешнего источника питания, а также нужно соединить его с GND контактом платы NodeMCU или Arduino, смотря какая из них выбрана для управления моторами. У нас управление моторов будет осуществляться через плату NodeMCU, а управление робо-рукой через Arduino.
5) Питание логики (Vs) (+5V). Через эту клемму осуществляется питание микросхемы L298N. То есть этот контакт нужно присоединить к контакту +5В платы Arduino. Но если в схеме используется плата NodeMCU, то она выдает только 3.3В — этого не хватает для питания логики L298N. В этом случае есть способ питания логики от стабилизатора напряжения, который располагается на плате драйвера. Он выдает 5В. Тогда клемма Vs никуда не подключается, а подается только ток на клемму VSS, но при этом должна быть установлена перемычка питания от стабилизатора, которая обозначена на картинке выше. Питание на плату NodeMCU при этом должно подаваться не более 12В от внешнего источника, подключив его «+» полюс к контакту VIN.
6) Управление мотором A (IN1, IN2) — контакты, управляющие направлениями вращения первого мотора. Подключаются к цифровым контактом управляющей платы (NodeMCU или Arduino). Напряжение на одном вращает мотор в одну сторону, на другом в другую.
7) Управление скоростью мотора А (ENA). Подключается к аналоговым контактам управляющей платы и скорость задается цифрой от 0 до 255.
8) Управление мотором В (IN3, IN4), управление скоростью мотора В (ENВ) — то же самое, но для второго мотора.

Схема подключения моторов через драйвер к плате NodeMCU:

pic21.jpg

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

pic22.jpg

pic23.jpg

Этот драйвер отличается от рассмотренного тем, что в нем отсутствует микросхема 78M05 (стабилизатор напряжения). Роль этого стабилизатора в нашей схеме выполняет плата ArduinoNano. Она подключается к драйверу (VIN и GND), тем самым запитывается энергией и 5В от Arduino подается на питание логики драйвера. В остальном то же самое.
Вот наша схема:

pic24.jpg

ArduinoNano
Платы NodeMCU и ArduinoNano предназначены для управления различными электронными схемами.
ArduinoNano построена на микроконтроллере ATmega328.

pic25.jpg

На ней есть 14 цифровых контактов и 8 аналоговых.
Программируется эта плата при помощи ArduinoIDE — среды разработки программ под Arduino, которую можно скачать с официального сайта и установить. Она содержит в себе список всех возможных видов плат Arduino, какие бывают, так что для программирования ArduinoNano не нужно устанавливать дополнительно никаких библиотек. Нужно только подключить плату к компьютеру через MiniUSB кабель, запустить среду разработки, найти в вписке плат «Инструменты → Плата → Arduino Nano».

pic26.jpg

Также нужно указать последовательный порт «Инструменты → Порт», корорый появится в списке после присоединения платы к компьютеру. И плата готова к программированию.
На данном этапе, в вышеприведенных схемах плата Arduino ничего не делает. В первой схеме с драйвером L298N она вообще отсутствует так как не нужна, в схеме с самодельным драйвером она выполняет роль стабилизатора напряжения и включена в схему, чтобы питать логику микросхемы L298N.
Предлагаю сначала запустить машинку и сделать так, чтобы ее движения управлялись с телефона, а вторым этапом, если все получится, приделать робо-руку. Робо рука будет управляться через плату Arduino, поэтому мы резервируем для нее место в схеме, но пока что она никакой код выполнять не будет. А плата NodeMCU будет управлять движением моторов, так что запрограммируем сейчас ее.

NodeMCU
Плата NodeMCU основана на микросхеме ESP8266MOD, содержащей встроенный Wifi модуль. Благодаря этому факту выбор и пал на нее. Она обеспечивает возможность посылать сигналы в нашу схему по Wifi через программу на телефоне или планшете.

pic27.jpg

Немного о Wifi подключениях...
Устройства, которые подключаются к сетям Wi-Fi, называются станциями (STA). Подключение к Wi-Fi обеспечивается точкой доступа (AP), которая выступает в качестве концентратора для одной или нескольких станций. Точка доступа вашей домашней сети на другом конце подключена к проводной сети. Она обычно интегрируется с маршрутизатором для обеспечения доступа из сети Wi-Fi к Интернету. Каждая точка доступа распознается по SSID (Service Set IDentifier), который по сути является именем сети, которую вы выбираете при подключении устройства (станции) к Wi-Fi.
Модули ESP8266 могут работать как станции, поэтому мы можем подключить их к сети Wi-Fi. Он также может работать как программная точка доступа (soft-AP) для создания собственной сети Wi-Fi. Когда модуль ESP8266 работает как мягкая точка доступа (программная точка доступа), мы можем подключить другие станции к модулю ESP8266.
Вот эту возможность мы и используем в программе. Наша плата NodeMCU при включении будет создавать точку доступа Wifi с возможностью подключения к ней станций (телефонов, планшетов, компьютеров, ноутбуков). Но количество подключений ограничим одним. Мы же не хотим, чтобы к роботу подключились три человека и одновременно начали вращать колесами?!
После создания точки доступа Wifi нужно с помощью специальной библиотеки кода создать веб-сервер на этой точке доступа и программным путем сгенерировать html страницу, отображающую кнопки управления моторами. Устройство-клиент подключается к данному Wifi, затем с этого устройства заходим на сервер по специальному IP адресу (по умолчанию это 192.168.4.1) и эта html страница c отображается. Нажатие на кнопки на этой странице передает информацию в программу, прошитую в NodeMCU и плата меняет состояние контактов, управляющих вращением моторов.
Чтобы запрограммировать плату NodeMCU, нужно установить дополнительную библиотеку — ESP8266WiFi. Она предоставляет широкий набор методов (функций) и свойств C++ для настройки и работы модуля ESP8266 в режиме станции и / или программной точки доступа. NodeMCU
появится в списке среды разработки ArduinoIDE только после установки этой библиотеки.
О подробностях установки можно узнать на сайте http://arduino.esp8266.com
На этом ресурсе дается ссылка на GitHub:
http://github.com/esp8266/Arduino
Там написано как осуществляется установка библиотеки при помощи Менеджера плат.
1. Нужно установить текущую интегрированную среду разработки Arduino версии 1.8.9 или выше. Текущая версия находится на сайте Arduino.
2. Запустить Arduino и открыть окно настроек «Файл → Настройки».
Ввести: «https://arduino.esp8266.com/stable/package_esp8266com_index.json» в поле URL-адреса диспетчера дополнительных плат.

pic28.png

3. Открыть Менеджер плат из меню «Инструменты → Плата».

pic29.png

В поле поиска нужно вбить esp8266, при этом в списке должен появиться этот модуль.

pic30.png

Затем нужно нажать кнопку «Установка». Какое-то время будет происходить установка библиотеки.
После этого можно подключать плату NodeMCU через MicroUSB к компьютеру, в среде программирования Arduino выбираем порт «Инструменты → Порт» и выбираем плату «NodeMCU 1.0 (ESP-12E Module)», которая появится в списке «Инструменты → Плата».

pic31.png

Плата готова к программированию.

Заливаем в нее такой код (к статье прилагается файл с кодом «sketch_RobokarForNodeMCU.ino»):

/*Программа для платы NodeMCU. К плате подключаются два мотор-редуктора, вращением которых управляют контакты D0-D3. 
NodeMCU создает программную точку доступа Wifi и запускает сервер, генерит html страницу на этом сервере, содержащую 
кнопки управления моторами. Через телефон или планшет происходит подключение по Wifi к плате и через эту страницу на сервере
управляются моторы*/

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>

//контакты, управляющие вращением моторов
#define pin_Moror1_front 16 //D0 NodeMCU
#define pin_Moror1_back 5 //D1
#define pin_Moror2_front 4 //D2
#define pin_Moror2_back 0 //D3

//название и пароль создаваемой программной точки доступа Wifi
const char* ssid = "ESP8266Net";
const char* password = "ESP12345";

//Переменная строка, указывающая какая кнопка нажата в данный момент
String CurentButtonActive = "stop";

// Устанавливаем свой веб сервер на порт 80
ESP8266WebServer server(80);

/* По-умолчанию сервер доступен по IP http://192.168.4.1 */
void setup() {
  delay(100);

  /*Устанавливаем параметры контактов - выходный и выключены*/
  pinMode(pin_Moror1_front, OUTPUT);
  pinMode(pin_Moror1_back, OUTPUT);
  pinMode(pin_Moror2_front, OUTPUT);
  pinMode(pin_Moror2_back, OUTPUT);
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, LOW);

  /*Отладочный код нужен, чтобы смотреть надписи, которые выводятся на порт ввода-вывода
  Открываем последовательное соединение через порт со скоростью передачи данных 115200 
  И выводим надпись. Раскомментировать при необходимости*/
  /*Serial.begin(115200);
  Serial.println();
  Serial.print("Configuring access point...");*/
  
  /* Создаем мягкую точку доступа Wifi.
  WiFi.softAP(ssid, password, channel, hidden, max_connection)
  ssid - строка, содержащая сетевоt имя Wifi (макс. 31 символ)
  password - необязательная символьная строка с паролем.
  channel - необязательный параметр для установки канала Wi-Fi, от 1 до 13. Канал по умолчанию = 1.
  hidden - необязательный параметр, если установлено значение true, будет скрывать SSID.
  max_connection - необязательный параметр для установки макс. одновременных подключенных станций, от 0 до 8. 
  По умолчанию 4. 
  После достижения максимального числа любая другая станция, 
которая хочет подключиться, будет вынуждена ждать, пока уже подключенная станция не отключится.
 */  WiFi.softAP(ssid, password,1,0,1);  /*Отладочный вывод информации на монитор порта*/  /*Serial.print("softAP IP address: ");  Serial.println(WiFi.softAPIP());*/  //Привязываем функции-обработчики полученных команд  server.on("/", onRestart);  server.on("/stop", onStop);  server.on("/front", onFront);  server.on("/back", onBack);  server.on("/left", onLeft);  server.on("/right", onRight);  //Запускаем сервер  server.begin();  /*Отладочный вывод информации на монитор порта*/  /*Serial.println("HTTP server started");*/ } void loop() {  //Прослушивание клиентов  server.handleClient(); } void onSendWebPage() {  server.send(200, "text/html", createWebPage());  delay(5); } void onRestart() {  digitalWrite(pin_Moror1_front, LOW);  digitalWrite(pin_Moror1_back, LOW);  digitalWrite(pin_Moror2_front, LOW);  digitalWrite(pin_Moror2_back, LOW);  CurentButtonActive = "stop";  onSendWebPage(); } void onStop() {  onRestart(); } void onFront() {    digitalWrite(pin_Moror1_front, HIGH);  digitalWrite(pin_Moror1_back, LOW);  digitalWrite(pin_Moror2_front, HIGH);  digitalWrite(pin_Moror2_back, LOW);  CurentButtonActive = "front";  onSendWebPage(); } void onBack() {  digitalWrite(pin_Moror1_front, LOW);  digitalWrite(pin_Moror1_back, HIGH);  digitalWrite(pin_Moror2_front, LOW);  digitalWrite(pin_Moror2_back, HIGH);  CurentButtonActive = "back";  onSendWebPage(); } void onLeft() {  digitalWrite(pin_Moror1_front, LOW);  digitalWrite(pin_Moror1_back, HIGH);  digitalWrite(pin_Moror2_front, HIGH);  digitalWrite(pin_Moror2_back, LOW);  CurentButtonActive = "left";  onSendWebPage(); } void onRight() {  digitalWrite(pin_Moror1_front, HIGH);  digitalWrite(pin_Moror1_back, LOW);  digitalWrite(pin_Moror2_front, LOW);  digitalWrite(pin_Moror2_back, HIGH);  CurentButtonActive = "right";  onSendWebPage(); } String createWebPage() {  //Создается html страница, которая будет открываться на установленном сервере  String WebPage = "";   // Отображаем страницу HTML   WebPage += "<!DOCTYPE html><html>";   WebPage += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";   WebPage += "<link rel=\"icon\" href=\"data:,\">";            // CSS для стиля кнопок on/off   WebPage += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}";   WebPage += ".button { background-color: #195B6A; border: none; color: white; padding: 10px 40px;";   WebPage += "text-decoration: none; font-size: 15px; margin: 2px; cursor: pointer;}";   WebPage += ".button2 {background-color: #77878A;}</style></head>";         // Заголовок для страницы   WebPage += " <body>";//<h1>Robocar</h1>   WebPage += "<div align=center><table><tr><td>";   WebPage += "</td><td>";   WebPage += "<p><a href=\"/front\"><button class=";   if(CurentButtonActive == "front")    WebPage += "\"button\"";   else    WebPage += "\"button button2\"";   WebPage += ">Front</button></a></p>";   WebPage += "</td><td>";   WebPage += "</td></tr><tr><td>";   WebPage += "<p><a href=\"/left\"><button class=";   if(CurentButtonActive == "left")    WebPage += "\"button\"";   else    WebPage += "\"button button2\"";   WebPage += ">Left</button></a></p>";   WebPage += "</td><td>";   WebPage += "<p><a href=\"/stop\"><button class=";   if(CurentButtonActive == "stop")    WebPage += "\"button\"";   else    WebPage += "\"button button2\"";   WebPage += ">Stop</button></a></p>";   WebPage += "</td><td>";   WebPage += "<p><a href=\"/right\"><button class=";   if(CurentButtonActive == "right")    WebPage += "\"button\"";   else    WebPage += "\"button button2\"";   WebPage += ">Right</button></a></p>";   WebPage += "</td></tr><tr><td>";   WebPage += "</td><td>";   WebPage += "<p><a href=\"/back\"><button class=";   if(CurentButtonActive == "back")    WebPage += "\"button\"";   else    WebPage += "\"button button2\"";   WebPage += ">Back</button></a></p>";   WebPage += "</td><td>";   WebPage += "</td></tr></table></div>";      WebPage += "</body></html>";   return WebPage; }

Коробка для электроники
После того, как программа зашита в NodeMCU, настало время соединить все детали в схему, описанную выше и разместить ее на тележке.
Для удобного размещения электроники я использовала такую макетную плату.

pic32.jpg

К ней припаяны разъемы, в которые втыкаются контакты плат Arduino и NodeMCU.

pic33.jpg

pic34.jpg

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

Вот вся наша электроника в собранном по схеме выше виде:

pic35.jpg

Теперь нужно как-то аккуратно и красиво разместить ее на тележке.
Для этого из куска картона делаем коробочку с нужными размерами:
140/110/60.

pic36.jpg

Вот что получается в результате.

pic37.jpg

Включаем механизм...
Берем мобильный телефон и находим в нем сеть Wifi «ESP8266Net». Коннектимся к ней, указав пароль «ESP12345».
Открываем в телефоне броузер и в строке адреса набираем номер IP: «192.168.4.1».
Открывается такая страница:

pic38.jpg

Если при нажатии на кнопки машинка реагирует и двигается в нужную сторону — все правильно и можно переходить к установке робо-руки.

Робот-манипулятор MeArm
Это довольно известный робот-манипулятор, который можно купить в магазинах робототехники, радиорынках, Алиэкспрессе и т. д.
Он представляет из себя конструктор из деталей, которые соединяются между собой винтиками по схеме. Им управляют четыре сервомотора «Tower Pro Mikro Servo 9G SG90».

pic39.jpg

Один мотор осуществляет круговое вращение, второй — движение вверх-вниз, третий — вперед-назад, четвёртый — открывает-закрывает клешню.
От каждого из них отходят три провода.
Красный — питание.
Коричневый — земля.
Желтый — сигнальный или информационный провод, по которому подаются команды на какой угол совершить оборот двигателю, а значит должы присоединяться к аналоговым контактам.

pic40.jpg

Питаются моторы от 5В.
Таким образом, теоретически можно их запитать от контакта Arduino +5V. Но на практике этого делать не рекомендуется, потому, что при включении и выключении Arduino могут быть резкие скачки напряжения, а также они наблюдаются если моторы дошли до максимального или минимального угла поворота, а программа подает сигнал вращаться. Чтобы избежать перегорания моторов подключим их к батарейке через стабилизатор.

pic41.jpg

Мы его сами спаяли. Это стабилизатор LM7805. У него 3 контакта. На первый подаем напряжение 7.2В от шести батареек, 2 — GND, 3 — выходной контакт, дающий 5 В, от которого питаем все 4 мотора роборуки. Между 1 и 2 контактом припаян конденсатор на 47мФ, между 2 и 3 на 100мФ.

pic42.jpg

Теперь нужно проверить работу всех моторов роборуки, прежде чем включать ее в схему с тележкой.
Для этого мы берем только плату Arduino, роборуку MeArm, блок батареек на 7.2 В и описанный выше стабилизатор.
Собираем с ними такую схему:

pic43.jpg

pic50.jpg

Заливаем в Arduino программу, по которой роборука по-очереди вращает все моторы. Сначала вращается в одну сторону, потом в другую, потом поднимается вверх, опускается вниз, потом выдвигается вперед-назад, открывает клешню и закрывает (к статье прилагается файл с кодом «sketch_MeArmServoTest.ino»):

/*Программа для робота-манипулятора MeArm. 
Получает данные от четырёх аналоговых контактов платы Arduino. 
Каждый из этих контактов задает вращение сервомоторов, управляющих роборукой.*/

//подключаем библиотеку servo
#include <Servo.h>

/*определяем ключевые слова 
(INI — начальное положение мотора, 
MAX — максимальное положение мотора, 
MIN — минимальное положение мотора, 
CUR — текущее положение мотора), 
которые будут использоваться как индексы для доступа к цифрам, своим для каждого мотора*/ 
#define INI 0
#define MAX 1
#define MIN 2
#define CUR 3

/*константные глобальные переменные хранящие время задержки в мс*/
const int delay_move = 50;
const int delay_setup = 100;
const int motor_step = 1;

/*Структура, хранящая всю необходимую информацию о моторе*/
struct SERVO_MOTOR{
//объект мотора servo
  Servo servo_obj;
 /*массив целочисленных переменных, у которого 
 0й элемент   (соответствует INI) — это начальное положение мотора, 
 1й (MAX) — максимально возможное, 
 2й (MIN) — минимально возможное, 
 3й (CUR) — текущее положение.*/
  int positions_data[4] = {0,0,0,0};
};

//создаём четыре объекта структуры SERVO_MOTOR
SERVO_MOTOR servo_round, servo_up, servo_front, servo_hand;

/*функция InitServoObjects содержит начальную инициализацию всех четырёх моторов*/
void InitServoObjects()
{
   //Инициируем объект servo_round  
  servo_round.positions_data[INI] = 90;  //начальный угол поворота
  servo_round.positions_data[MAX] = 170; //максимальный угол поворота
  servo_round.positions_data[MIN] = 30; //минимальный угол поворот
  servo_round.positions_data[CUR] = servo_round.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_round.servo_obj.attach(A0); 
//устанавливаем мотор на начальный угол поворота
  servo_round.servo_obj.write(servo_round.positions_data[INI]); 
  //задержка
  delay(delay_setup);

  //Инициируем объект servo_up
  servo_up.positions_data[INI] = 110;  //начальный угол поворота
  servo_up.positions_data[MAX] = 160; //максимальный угол поворота
  servo_up.positions_data[MIN] = 80; //минимальный угол поворот
  servo_up.positions_data[CUR] = servo_round.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_up.servo_obj.attach(A1); 
//устанавливаем мотор на начальный угол поворота
  servo_up.servo_obj.write(servo_up.positions_data[INI]); 
//задержка
  delay(delay_setup);

  //Инициируем объект servo_front
  servo_front.positions_data[INI] = 90;  //начальный угол поворота
  servo_front.positions_data[MAX] = 160; //максимальный угол поворота
  servo_front.positions_data[MIN] = 90; //минимальный угол поворот
  servo_front.positions_data[CUR] = servo_round.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_front.servo_obj.attach(A2); 
//устанавливаем мотор на начальный угол поворота
  servo_front.servo_obj.write(servo_front.positions_data[INI]); 
//задержка
  delay(delay_setup);
  
 //Инициируем объект servo_hand
  servo_hand.positions_data[INI] = 40;  //начальный угол поворота
  servo_hand.positions_data[MAX] = 40; //максимальный угол поворота
  servo_hand.positions_data[MIN] = 0; //минимальный угол поворот
  servo_hand.positions_data[CUR] = servo_round.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D7
  servo_hand.servo_obj.attach(A3); 
//устанавливаем мотор на начальный угол поворота
  servo_hand.servo_obj.write(servo_hand.positions_data[INI]); 
//задержка
  delay(delay_setup);
}

/*стандартная функция с начальной инициализацией всех компонентов 
программы*/
void setup()
{
  //инициируем все объекты в отдельной функции
  InitServoObjects();
}

/*стандартная функция с реализацией программы, в которой все действия циклически повторяются*/
void loop(){

 for(int i = servo_round.positions_data[INI]; i <= servo_round.positions_data[MAX]; i++)
 {
   servo_round.servo_obj.write(i); 
   delay(delay_move);
 }

 for(int i = servo_round.positions_data[MAX]; i >= servo_round.positions_data[MIN]; i--)
 {
   servo_round.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_round.positions_data[MIN]; i <= servo_round.positions_data[INI]; i++)
 {
   servo_round.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_up.positions_data[INI]; i <= servo_up.positions_data[MAX]; i++)
 {
   servo_up.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_up.positions_data[MAX]; i >= servo_up.positions_data[MIN]; i--)
 {
   servo_up.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_up.positions_data[MIN]; i <= servo_up.positions_data[INI]; i++)
 {
   servo_up.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_front.positions_data[INI]; i <= servo_front.positions_data[MAX]; i++)
 {
   servo_front.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_front.positions_data[MAX]; i >= servo_front.positions_data[MIN]; i--)
 {
   servo_front.servo_obj.write(i);  
   delay(delay_move);
 }

 for(int i = servo_hand.positions_data[INI]; i >= servo_hand.positions_data[MIN]; i--)
 {
   servo_hand.servo_obj.write(i);  
   delay(delay_move);
 }

  for(int i = servo_hand.positions_data[MIN]; i <= servo_hand.positions_data[MAX]; i++)
 {
   servo_hand.servo_obj.write(i);  
   delay(delay_move);
 }
}

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

SPI интерфейс передачи данных
SPI (serial peripheral interface, последовательный периферийный интерфейс) — это протокол подключения к шине, передающей данные по типу «ведущий-ведомый». Он был разработан корпорацией Motorola. Шина содержит 4 провода для связи. По ней могут взаимодействовать одновременно только 1 ведущее (master) и 1 ведомое устройство (slave). Соответственно, устройства с поддержкой SPI могут работать в одном из двух режимов — Master mode и Slave mode. Главное устройство инициирует связь , генерирует последовательный счетчик времени для синхронной передачи данных и может работать с несколькими ведомыми устройствами, выбирая одно из них.
И NodeMCU и Arduino поддерживают этот интерфейс. У NodeMCU есть две шины SPI — которые называются SPI и HSPI. SPI используется процессором для доступа к флешу с прошивкой, HSPI может использоваться для других устройств.
Связь Arduino и NodeMCU через интерфейс SPI осуществляется через следующие четыре контакта:

Название контакта Описание Контакт ArduinoNano Контакт NodeMCU
MISO Master In — Slave Out.
Ведущее устройство получает данные, а ведомое передает через этот контакт
D12 D6
MOSI Master Out — Slave In.
Ведущее устройство передает данные, а ведомое получает через этот контакт
D11 D7
SCLK Последовательные часы.
Только ведущее устройство может генерировать эти часы для связи, которая используется ведомыми устройствами
D13 D5
CS Chip Select.
Через этот контакт ведущее устройство может выбрать ведомое и начать с ним связь
D10 D8

Для обеспечения связи по SPI интерфейсу соединяются соответствующие контакты NodeMCU и Arduino (D5-D13, D6 — D12, D7-D11, D8-D10, GND-GND).
Теперь, прежде чем собирать окончательную схему и заливать программы, нужно проверить как работает SPI связь.

Заливаем программу в NodeMCU (к статье прилагается файл с кодом «sketch_SPITestMaster.ino»):

/*Программа для проверки работы SPI интерфейса. NodeMCU (Master) - посылает сообщения Arduino (Slave) 
Arduino выводит сообщения на монитор порта*/
//подключаем библиотеку SPI

#include<SPI.h>

const int delay_messg = 1000;

void setup() {
 SPI.begin();  /* запускаем SPI */
}

void loop() {

 /*Оправляем один за другим несколько сообщений с задержкой в 1 сек*/

 onSendStop();
 delay(delay_messg);  

 OnSendRound1();
 delay(delay_messg);

 OnSendRound2();
 delay(delay_messg);

 OnSendUp();
 delay(delay_messg);

 OnSendDown();
 delay(delay_messg);
 
 OnSendFront();
 delay(delay_messg);

 OnSendBack();
 delay(delay_messg);
 
 OnSendOpen();
 delay(delay_messg);

 OnSendClose();
 delay(delay_messg);
}

void onSendStop()
{
  char buff[]="stp\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendRound1()
{
  char buff[]="rd1\n";
  for(int i=0; i<sizeof buff; i++)  
  SPI.transfer(buff[i]);
}

void OnSendRound2()
{
  char buff[]="rd2\n";
  for(int i=0; i<sizeof buff; i++)  
  SPI.transfer(buff[i]);
}

void OnSendUp()
{
  char buff[]="up\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendDown()
{
  char buff[]="dwn\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendFront()
{
  char buff[]="frt\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendBack()
{
  char buff[]="bck\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendOpen()
{
  char buff[]="opn\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

void OnSendClose()
{
  char buff[]="cls\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
}

Заливаем программу в ArduinoUNO (к статье прилагается файл с кодом «sketch_SPITestSlave.ino»):

/*Программа для проверки работы SPI интерфейса. NodeMCU (Master) - посылает сообщения Arduino (Slave) 
Arduino выводит сообщения на монитор порта*/
//подключаем библиотеку SPI
#include <SPI.h>

//создаем массив символов для получения сообщения

char buff [5];

//текущий, записываемый индекс массива buff
volatile byte index;

//флаг, означающий, что сообщение получено
volatile bool receivedone;  /* use reception complete flag */

//строка, хранящая полученную команду
String commandSPI = "";

void setup (void)
{
  //Открываем последовательное соединение через порт со скоростью передачи данных 9600 
  Serial.begin (9600);
  SPCR |= bit(SPE);         /* Включаем SPI */
  pinMode(MISO, OUTPUT);    /* Устанавливаем MISO пин как выходной - OUTPUT */
  index = 0;
  receivedone = false;
  SPI.attachInterrupt();    /* добавляем прерывание по приходу сигнала по SPI*/
}

void loop (void)
{
  if (receivedone)          /* Если сообщение получено */
  {
    buff[index] = 0;
     commandSPI = buff; //присваиваем строке сообщение

  if(commandSPI == "stp")
  {
     Serial.println("stop");
  }
  else if(commandSPI == "rd1")
  {
    Serial.println("round1");
  }
  else if(commandSPI == "rd2")
  {
   Serial.println("round2");
  }
  else if(commandSPI == "up")
  {
    Serial.println("up");
  }
  else if(commandSPI == "dwn")
  {
    Serial.println("down");
  }
  else if(commandSPI == "frt")
  {
   Serial.println("front");
  }
  else if(commandSPI == "bck")
  {
  Serial.println("back");
  }
  else if(commandSPI == "opn")
  {
   Serial.println("open");
  }
  else if(commandSPI == "cls")
  {
   Serial.println("close");
  }
    
    index = 0;
    receivedone = false;
  }
}

// Функция-обработчик прерывания
ISR (SPI_STC_vect)
{
 uint8_t oldsrg = SREG;
  cli();
  char c = SPDR;
  if (index <sizeof buff)
  {
    /* Проверяем, получен ли символ перехода на новую строку, который оканчивает сообщение*/
    if (c == '\n'){     
     receivedone = true;
    }
    else
      buff [index++] = c;
  }
  SREG = oldsrg;
}

Собираем схему:

pic46.jpg

Затем подключаем обе платы по мини и микро USB к компьютеру.

pic47.jpg

Открываем Монитор порта для Arduino.
На нем должны отображаться команды, передаваемые с платы NodeMCU через SPI шину каждую секунду.

pic48.jpg

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

Окончательная схема
Окончательная схема робомашинки с самодельным драйвером моторов выглядит так:

pic49.jpg

Та же схема, но с драйвером L298N который можно купить на рынке:

pic50.jpg

Программа для Arduino (к статье прилагается файл с кодом «sketch_RobocarMeArm_ForArduino.ino»):

/*Программа для робота-манипулятора MeArm. 
Получает данные от четырёх аналоговых контактов платы NodeMCU. 
Каждый из этих контактов задает вращение сервомоторов, управляющих роборукой.*/

//подключаем библиотеку servo
#include <Servo.h>
#include <SPI.h>

/*определяем ключевые слова 
(INI — начальное положение мотора, 
MAX — максимальное положение мотора, 
MIN — минимальное положение мотора, 
CUR — текущее положение мотора), 
которые будут использоваться как индексы для доступа к цифрам, своим для каждого мотора*/ 
#define INI 0
#define MAX 1
#define MIN 2
#define CUR 3

/*константные глобальные переменные хранящие время задержки в мс*/
const int delay_move = 10;
const int delay_setup = 100;
const int motor_step = 1;

char buff [5];
volatile byte index;
volatile bool receivedone;  /* use reception complete flag */

String commandSPI = "";

/*Структура, хранящая всю необходимую информацию о моторе*/
struct SERVO_MOTOR{
//объект мотора servo
  Servo servo_obj;
 /*массив целочисленных переменных, у которого 
 0й элемент   (соответствует INI) — это начальное положение мотора, 
 1й (MAX) — максимально возможное, 
 2й (MIN) — минимально возможное, 
 3й (STP) — единицы соответствующие шагу в 1 градус, 
 4й (CUR) — текущее положение.*/
  int positions_data[5] = {0,0,0,0};
};

//создаём четыре объекта структуры SERVO_MOTOR
SERVO_MOTOR servo_round, servo_up, servo_front, servo_hand;

/*функция InitServoObjects содержит начальную инициализацию всех четырёх моторов*/
void InitServoObjects()
{
   //Инициируем объект servo_round  
  servo_round.positions_data[INI] = 90;  //начальный угол поворота
  servo_round.positions_data[MAX] = 180; //максимальный угол поворота
  servo_round.positions_data[MIN] = 0; //минимальный угол поворот
  servo_round.positions_data[CUR] = servo_round.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_round.servo_obj.attach(A0); 
//устанавливаем мотор на начальный угол поворота
  servo_round.servo_obj.write(servo_round.positions_data[INI]); 
  //задержка
  delay(delay_setup);

  //Инициируем объект servo_up
  servo_up.positions_data[INI] = 100;  //начальный угол поворота
  servo_up.positions_data[MAX] = 160; //максимальный угол поворота
  servo_up.positions_data[MIN] = 60; //минимальный угол поворот
  servo_up.positions_data[CUR] = servo_up.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_up.servo_obj.attach(A1); 
//устанавливаем мотор на начальный угол поворота
  servo_up.servo_obj.write(servo_up.positions_data[INI]); 
//задержка
  delay(delay_setup);

  //Инициируем объект servo_front
  servo_front.positions_data[INI] = 90;  //начальный угол поворота
  servo_front.positions_data[MAX] = 160; //максимальный угол поворота
  servo_front.positions_data[MIN] = 90; //минимальный угол поворот
  servo_front.positions_data[CUR] = servo_front.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino
  servo_front.servo_obj.attach(A2); 
//устанавливаем мотор на начальный угол поворота
  servo_front.servo_obj.write(servo_front.positions_data[INI]); 
//задержка
  delay(delay_setup);
  
 //Инициируем объект servo_hand
  servo_hand.positions_data[INI] = 40;  //начальный угол поворота
  servo_hand.positions_data[MAX] = 40; //максимальный угол поворота
  servo_hand.positions_data[MIN] = 0; //минимальный угол поворот
  servo_hand.positions_data[CUR] = servo_hand.positions_data[INI]; //текущий угол поворота
//назначаем объекту класса servo контакт Arduino D7
  servo_hand.servo_obj.attach(A3); 
//устанавливаем мотор на начальный угол поворота
  servo_hand.servo_obj.write(servo_hand.positions_data[INI]); 
//задержка
  delay(delay_setup);
}

/*стандартная функция с начальной инициализацией всех компонентов 
программы*/
void setup()
{
//инициируем все объекты в отдельной функции
  InitServoObjects();

  SPCR |= bit(SPE);         /* Enable SPI */
  pinMode(MISO, OUTPUT);    /* Make MISO pin as OUTPUT */
  index = 0;
  receivedone = false;
  SPI.attachInterrupt();    /* Attach SPI interrupt */
}

/*стандартная функция с реализацией программы, в которой все действия циклически повторяются*/
void loop(){

  if (receivedone)          
  {
    buff[index] = 0;
    
    commandSPI = buff;    
    index = 0;
    receivedone = false;
  }

  if(commandSPI == "stp")
  {
    
  }
  else if(commandSPI == "rd1")
  {
    if( (servo_round.positions_data[CUR] + motor_step) <= servo_round.positions_data[MAX]  )
    {
      servo_round.positions_data[CUR] = servo_round.positions_data[CUR] + motor_step;
      servo_round.servo_obj.write(servo_round.positions_data[CUR]);
    }
    else
      commandSPI = "stp";
  }
  else if(commandSPI == "rd2")
  {
   if( (servo_round.positions_data[CUR] - motor_step) >= servo_round.positions_data[MIN]  )
   {
     servo_round.positions_data[CUR] = servo_round.positions_data[CUR] - motor_step;
     servo_round.servo_obj.write(servo_round.positions_data[CUR]);
   }
   else
      commandSPI = "stp";
  }
  else if(commandSPI == "up")
  {
    if( (servo_up.positions_data[CUR] + motor_step) <= servo_up.positions_data[MAX]  )
    {
      servo_up.positions_data[CUR] = servo_up.positions_data[CUR] + motor_step;
      servo_up.servo_obj.write(servo_up.positions_data[CUR]);
    }
    else
      commandSPI = "stp";
  }
  else if(commandSPI == "dwn")
  {
    if( (servo_up.positions_data[CUR] - motor_step) >= servo_up.positions_data[MIN]  )
    {
      servo_up.positions_data[CUR] = servo_up.positions_data[CUR] - motor_step;
      servo_up.servo_obj.write(servo_up.positions_data[CUR]);
    }    
    else
      commandSPI = "stp";
  }
  else if(commandSPI == "frt")
  {
    if( (servo_front.positions_data[CUR] + motor_step) <= servo_front.positions_data[MAX]  )
   {
     servo_front.positions_data[CUR] = servo_front.positions_data[CUR] + motor_step;
     servo_front.servo_obj.write(servo_front.positions_data[CUR]);
   }
   else
      commandSPI = "stp";
  }
  else if(commandSPI == "bck")
  {
   if( (servo_front.positions_data[CUR] - motor_step) >= servo_front.positions_data[MIN]  )
   {
     servo_front.positions_data[CUR] = servo_front.positions_data[CUR] - motor_step;
     servo_front.servo_obj.write(servo_front.positions_data[CUR]);
   }
   else
      commandSPI = "stp";
  }
  else if(commandSPI == "opn")
  {
    if( (servo_hand.positions_data[CUR] - motor_step) >= servo_hand.positions_data[MIN]  )
   {
     servo_hand.positions_data[CUR] = servo_hand.positions_data[CUR] - motor_step;
     servo_hand.servo_obj.write(servo_hand.positions_data[CUR]);
   }
   else
      commandSPI = "stp";  
  }
  else if(commandSPI == "cls")
  {
     if( (servo_hand.positions_data[CUR] + motor_step) <= servo_hand.positions_data[MAX]  )
     {
       servo_hand.positions_data[CUR] = servo_hand.positions_data[CUR] + motor_step;
       servo_hand.servo_obj.write(servo_hand.positions_data[CUR]);
     }
     else
        commandSPI = "stp";
  }

  delay(delay_move);
}

// Обработчик прерывания по SPI
ISR (SPI_STC_vect)
{
  uint8_t oldsrg = SREG;
  cli();
  char c = SPDR;
  if (index <sizeof buff)
  {
    if (c == '\n'){     /* Check for newline character as end of msg */
     receivedone = true;
    }
    else
      buff [index++] = c;
  }
  SREG = oldsrg;
}

Программа для NodeMCU (к статье прилагается файл с кодом «sketch_RobocarMeArm_ForNodeMCU.ino»):

/*Программа для робота-машинки. Вращается машинка благодаря двум моторам-редукторам, управляемым этой программой для NodeMCU
(контакты D0-D3).
Еще программа передает команды по SPI интерфейсу плате Arduino для управления четырьмя моторами робота-руки MeArm.
NodeMCU создает программную точку доступа Wifi и запускает сервер, генерит html страницу на этом сервере, содержащую 
кнопки управления моторами. Через телефон или планшет происходит подключение по Wifi к плате и через эту страницу на сервере
управляются моторы*/

/*Подключаем библиотеки для Wifi, сервера, SPI интерфейса*/
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include<SPI.h>

//контакты, управляющие вращением моторов тележки
#define pin_Moror1_front 16 //D0 NodeMCU
#define pin_Moror1_back 5 //D1
#define pin_Moror2_front 4 //D2
#define pin_Moror2_back 0 //D3

//название и пароль создаваемой программной точки доступа Wifi
const char* ssid = "ESP8266Net";
const char* password = "ESP12345";

//Переменные строки, указывающие какая кнопка нажата в данный момент
String CurentButtonActive = "stop";
String CurentArmButtonActive = "stop";

/* Устанавливаем свой веб сервер на порт 80*/
/* По-умолчанию сервер доступен по IP http://192.168.4.1 */
ESP8266WebServer server(80);

void setup() {

  /*Устанавливаем параметры контактов - выходный и выключены*/
  pinMode(pin_Moror1_front, OUTPUT);
  pinMode(pin_Moror1_back, OUTPUT);
  pinMode(pin_Moror2_front, OUTPUT);
  pinMode(pin_Moror2_back, OUTPUT); 
  
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, LOW);
   
 /* Создаем мягкую точку доступа Wifi.
  WiFi.softAP(ssid, password, channel, hidden, max_connection)
  ssid - строка, содержащая сетевоt имя Wifi (макс. 31 символ)
  password - необязательная символьная строка с паролем.
  channel - необязательный параметр для установки канала Wi-Fi, от 1 до 13. Канал по умолчанию = 1.
  hidden - необязательный параметр, если установлено значение true, будет скрывать SSID.
  max_connection - необязательный параметр для установки макс. одновременных подключенных станций, от 0 до 8. 
  По умолчанию 4. 
  После достижения максимального числа любая другая станция, 
которая хочет подключиться, будет вынуждена ждать, пока уже подключенная станция не отключится.
  */  
  WiFi.softAP(ssid, password,1,0,1);

  //Привязываем функции-обработчики полученных команд от колес
  server.on("/", onRestart);
  server.on("/stop", onStop);
  server.on("/front", onFront);
  server.on("/back", onBack); 
  server.on("/left", onLeft);
  server.on("/right", onRight);

  //Привязываем функции-обработчики полученных команд от роборуки
  server.on("/armstop", onArmStop);
  server.on("/armround1", onArmRound1);
  server.on("/armround2", onArmRound2);
  server.on("/armup", onArmUp);
  server.on("/armdwn", onArmDown);
  server.on("/armfront", onArmFront);
  server.on("/armback", onArmBack);
  server.on("/armopn", onArmOpen);
  server.on("/armcls", onArmClose);  

 //Запускаем сервер
  server.begin();

  //Запускаем SPI интерфейс
  SPI.begin();
}

void loop() {
  //Прослушивание клиентов
  server.handleClient();
}

void onSendWebPage() {
  /*Вызываем функцию, перерисовывающую html страницу*/
  server.send(200, "text/html", createWebPage());
  delay(5);
}

void onRestart() {
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, LOW);
  CurentButtonActive = "stop";
  CurentArmButtonActive = "stop";
  char buff[]="armstop\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  onSendWebPage();
}

void onStop() {
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, LOW);
  CurentButtonActive = "stop";
  onSendWebPage();
}

void onFront() {  
  digitalWrite(pin_Moror1_front, HIGH);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, HIGH);
  digitalWrite(pin_Moror2_back, LOW);
  CurentButtonActive = "front";
  onSendWebPage();
}

void onBack() {
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, HIGH);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, HIGH);
  CurentButtonActive = "back";
  onSendWebPage();
}

void onLeft() {
  digitalWrite(pin_Moror1_front, LOW);
  digitalWrite(pin_Moror1_back, HIGH);
  digitalWrite(pin_Moror2_front, HIGH);
  digitalWrite(pin_Moror2_back, LOW);
  CurentButtonActive = "left";
  onSendWebPage();
}

void onRight() {
  digitalWrite(pin_Moror1_front, HIGH);
  digitalWrite(pin_Moror1_back, LOW);
  digitalWrite(pin_Moror2_front, LOW);
  digitalWrite(pin_Moror2_back, HIGH);
  CurentButtonActive = "right";
  onSendWebPage();
}

void onArmStop() {
  char buff[]="stp\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "stop";
  onSendWebPage();
}

void onArmRound1() {
  char buff[]="rd1\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "round1";
  onSendWebPage();
}

void onArmRound2() {
  char buff[]="rd2\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "round2";
  onSendWebPage();
}

void onArmUp() {
  char buff[]="up\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "up";
  onSendWebPage();
}

void onArmDown() {
  char buff[]="dwn\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "down";
  onSendWebPage();
}

void onArmFront() {
  char buff[]="frt\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "front";
  onSendWebPage();
}

void onArmBack() {
  char buff[]="bck\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "back";
  onSendWebPage();
}

void onArmOpen() {
  char buff[]="opn\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "open";
  onSendWebPage();
}

void onArmClose() {
  char buff[]="cls\n";
  for(int i=0; i<sizeof buff; i++)  
   SPI.transfer(buff[i]);
  CurentArmButtonActive = "close";
  onSendWebPage();
}

String createWebPage()
{
  /*Создается html страница, которая будет открываться на установленном сервере*/
  String WebPage = "";
   // Отображаем страницу HTML
   WebPage += "<!DOCTYPE html><html>";
   WebPage += "<head><meta name='viewport' content='width=device-width, initial-scale=1'>";
              // CSS для стиля кнопок on/off 
   WebPage += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}";
   WebPage += ".button { background-color: #195B6A; border: none; color: white; width: 80px;  padding: 7px 0px;";
   WebPage += " text-decoration: none; font-size: 15px; margin: 2px; cursor: pointer;}";
   WebPage += ".button2 {background-color: #77878A;}</style>";
         // Заголовок для страницы
   WebPage += "</head><body><div align='center'>";
   WebPage += "<table align='center'  valign = 'top'><tr><td>";
   WebPage += "<table align='center'  valign = 'top'><tr><td>";
   WebPage += "<table align='center'  valign = 'top'><tr><td>";
   WebPage += "<a href='/armround1'><button class=";
   if(CurentArmButtonActive == "round1")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Round1</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/armround2'><button class=";
  if(CurentArmButtonActive == "round2")
    WebPage += "'button'";
  else
    WebPage += "'button button2'";
   WebPage += ">Round2</button></a>";
   WebPage += "</td><tr><td>";
   WebPage += "<a href='/armup'><button class=";
   if(CurentArmButtonActive == "up")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Up</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/armdwn'><button class=";
   if(CurentArmButtonActive == "down")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Down</button></a>";
   WebPage += "</td></tr></table>";
   WebPage += "</td></tr><tr><td align ='center'>";
   WebPage += "<a href='/armstop'><button class=";
   if(CurentArmButtonActive == "stop")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Stop</button></a>";
   WebPage += "</td></tr><tr><td>";
   WebPage += "<table   align='center'  valign = 'top'><tr><td>";   
   WebPage += "<a href='/armfront'><button class=";
   if(CurentArmButtonActive == "front")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Front</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/armback'><button class=";
   if(CurentArmButtonActive == "back")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Back</button></a>";
   WebPage += "</td><tr><td>";
   WebPage += "<a href='/armopn'><button class=";
   if(CurentArmButtonActive == "open")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Open</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/armcls'><button class=";
   if(CurentArmButtonActive == "close")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Close</button></a>";
   WebPage += "</td></tr></table>";
   WebPage += "</td></tr></table>";
   WebPage += "</td><td><table>";
   WebPage += "<tr><td></td><td>";
   WebPage += "<a href='/front'><button class=";
   if(CurentButtonActive == "front")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Front</button></a>";
   WebPage += "</td><td></td></tr><tr><td>";
   WebPage += "<a href='/left'><button class=";
   if(CurentButtonActive == "left")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Left</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/stop'><button class=";
   if(CurentButtonActive == "stop")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Stop</button></a>";
   WebPage += "</td><td>";
   WebPage += "<a href='/right'><button class=";
   if(CurentButtonActive == "right")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Right</button></a>";
   WebPage += "</td></tr><tr><td></td><td>";
   WebPage += "<a href='/back'><button class=";
   if(CurentButtonActive == "back")
    WebPage += "'button'";
   else
    WebPage += "'button button2'";
   WebPage += ">Back</button></a>";
   WebPage += "</td><td></td></tr></table>";
   WebPage += "</td></tr></table>";   
   WebPage += "</div></body></html>";
   return WebPage;
}

Я спаяла схему на макетной плате, соединив нужные контакты с обратной стороны.

pic52.jpg

pic53.jpg

Подключаем все по схеме, включаем робота, при этом роборука должна перейти в исходное положение.
Затем берем мобильный телефон, идем в поиск сетей Wifi. Находим сеть «ESP8266Net», подключаемся введя пароль «ESP12345».
Открываем броузер и в строке адреса вводим: «192.168.4.1».
Отобразится html страница:

pic54.jpg

Справа кнопки управляют колесами машины, слева — роборукой.
Робомашинка готова к использованию.

pic55.jpg

Приложения к статье: