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

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


Часть 1. Механика и радиоуправление
Часть 2. Определение высоты травы
Часть 3. Сегментация травы нейросетью
Часть 4. Карта газона на визуальных маркерах

Механическая часть

В наше время сделать из обычной газонокосилки радиоуправляемую стало очень просто. Для этого достаточно купить б/у гироскутер за 2000 рублей и микроконтроллер ESP8266 (или ESP32) за 150 рублей.

Перепрошив родную плату гироскутера (Внимание! перепрошивке поддаются не все платы, а только довольно старые модели) с помощью инструкции на сайте, мы получим возможность управлять каждым колесом с помощью микроконтроллера ESP8266 по UART, а в качестве пульта радиоуправления будем использовать обычный телефон, который будет подключаться к ESP8266 по Wifi.

Есть также вариант управлять моторами без перепрошивки основной платы, но он требует программной доработки, см. ниже последний вариант под спойлером "Подробнее о перепрошивке платы".

Прошивка платы гироскутера

Не все платы поддаются перепрошивке. Плата должна выглядеть как на сайте https://github.com/EmanuelFeru/hoverboard-firmware-hack-FOC и иметь микропроцессор STM32F103RCT6 или GD32F103RCT6 (это китайский клон STM32).

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

Прошивку с сайта https://github.com/EmanuelFeru/hoverboard-firmware-hack-FOC необходимо сначала настроить для работы через UART. Потому что там есть несколько способов управления моторами - через джойстик, I2C и т.д.

Откройте файл platformio.ini и раскомментируйте в нем строку

default_envs = VARIANT_USART

По умолчанию управление делается через длинный провод. Но чтобы уменьшить помехи, давайте переделаем управление на короткий провод. Для этого откройте файл Inc/config.h и в разделе #ifdef VARIANT_USART раскомментируйте строку

#define CONTROL_SERIAL_USART3  0

И закомментируйте строку

//#define CONTROL_SERIAL_USART2  0

В принципе, можно включить (когда строка есть) или выключить (когда строка закомментирована) обратную связь по второму TX проводу в шлейфе. По нему могут приходить текущие обороты моторов, текущий заряд аккумулятора и так далее. За это отвечают строки #define FEEDBACK_SERIAL_USART2 (длинный провод) и #define FEEDBACK_SERIAL_USART3 (короткий провод).

Строки //#define SIDEBOARD_SERIAL_USART2 0 и //#define SIDEBOARD_SERIAL_USART3 0 оставьте все время закомментированными, это другой вариант UART команд, он сейчас не нужен.

Итоговый вариант для управления по короткому проводу должен выглядеть так:

#ifdef VARIANT_USART
  // #define SIDEBOARD_SERIAL_USART2 0
  // #define CONTROL_SERIAL_USART2  0
  // #define FEEDBACK_SERIAL_USART2

  // #define SIDEBOARD_SERIAL_USART3 0
  #define CONTROL_SERIAL_USART3  0
  #define FEEDBACK_SERIAL_USART3

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

Для этого в файле Inc/config.h найдите строку #define USART3_BAUD в разделе ### UART SETIINGS #### (их там всего две, можно просто заменить обе) и замените ее с 115200 на 9600:

#define USART3_BAUD           9600

И последнее, о чем стоит сказать. По умолчанию моторы управляются напряжением в режиме VOLTAGE MODE. Но для газонокосилки больше подходит режим SPEED MODE. В этом случае газонокосилка будет сама стараться поддерживать скорость движения пропорционально уровню газа на пульте управления.

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

Но в режиме SPEED MODE есть и минусы. Во-первых, неизвестно насколько хорошо встроенные PID коэффициенты будут поддерживать скорость именно на вашей косилке. Во-вторых, если газонокосилка упрется в препятствие, то ток вырастет до максимальных 15 А на каждое колесо, что будет сильно греться и сажать аккумулятор.

В общем, SPEED MODE должен подходить для газонокосилки в теории лучше (она будет ехать ровнее), но можно оставить и режим по умолчанию как более безопасный. Если решите попробовать этот режим, то в файле Inc/config.h найдите строчку

#define CTRL_MOD_REQ    VLT_MODE

И замените ее на:

#define CTRL_MOD_REQ    SPD_MODE

Дальше следуйте инструкции по перепрошивке на сайте.

Подробнее о перепрошивке платы

Есть несколько вариантов как залить прошивку на плату. И есть также способ как управлять моторами вообще без прошивки (см. в самом конце).

С помощью ST-Link

Проще всего прошить плату с помощью программатора ST-Link, например такого:

ST-Link V2 mini
ST-Link V2 mini

И плагина PlatformIO для VSCode. Тогда заливка прошивки в плату будет делаться нажатием одной кнопки.

На плате гироскутера есть выводы SWD для программирования, к ним можно подпаять проводки или DuPont штырьки, а можно и просто прижать штырьки от программатора рукой на время прошивки.

Существует два способа подвести питание +3.3V к микропроцессору STM32 во время прошивки.

Первый, рекомендуемый на сайте, это использовать штатный аккумулятор гироскутера. Который через серию имеющихся на плате преобразователей 36V -> 12V -> 5V -> 3.3V питает STM32. Для этого нужно подключить аккумулятор, подать питание на плату нажатием кнопки питания, и только после этого подключать ST-Link V2 к USB от компьютера.

ВНИМАНИЕ! Не подключайте в этом случае выход +3.3V от программатора к пину +3.3V на SWD разъеме! При одновременной подаче +3.3V от программатора и внешнего питания от аккумулятора, есть большая вероятность сжечь STM32.

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

Но если вам страшно подключать 36V к USB компьютера (и правильно делаете! иногда транзисторы пробивает и эти 36 V попадают на всю плату), то более безопасным является запитать STM32 напрямую от +3.3V программатора ST-Link.

Для этого полностью отсоедините аккумулятор и подключите +3.3V от программатора к +3.3V пину SWD.

Кнопку питания в этом случае держать нажатой не нужно.

ВНИМАНИЕ! В этом случае нужно обязательно разрядить конденсаторы на плате, прежде чем подключать программатор. На них довольно долго держатся 36-42 V, и это будет аналогично как одновременно подключить +3.3V от программатора и внешнее питание, что приведет к выходу из строя STM32. Для разрядки конденсаторов замкните аккумуляторные провода питания на 10 секунд (лучше через небольшое сопротивление). И проверьте потом мультиметром, что на них остаточное напряжение осталось меньше 1 В.

В любом случае, при первой прошивке вам придется снять блокировку с чипа. На сайте есть инструкция, но если коротко, это делается в программе ST-Link Utility. Надо подключиться к чипу кнопкой Connect to target, в меню Target выбрать Option Bytes... и там в ниспадающем списке в Read Out Protection выбрать Disabled.

С помощью UART

Если у вас нет программатора ST-Link V2 или вы не хотите его покупать (он стоил раньше порядка 500 руб, а как сейчас, с учетом мирового дефицита чипов STM32, неизвестно), залить прошивку можно также через UART интерфейс с помощью программы Flash Loader Demonstrator.

На STM32F103RB такая прошивка делается через UART1 (в даташите на чип GD32F103RCT6 он обозначен как UART0) на пинах Tx = PA9 и Rx = PA10.

К сожалению, для входа в этот режим на пин BOOT0 надо подать +3.3V, а этот пин на плате разведен так, что идет сразу на землю. Поэтому придется перерезать дорожку на плате и делать там кнопку. Но там очень неудобно подлезть и без микроскопа это сложно сделать. Поэтому настоятельно рекомендуется заливать прошивку через SWD разъем с помощью программатора ST-Link.

С помощью ESP8266

В принципе, протокол SWD является открытым и довольно простым. По сути, там нужно в один регистр записать номер ячейки в памяти, а в другой какой поместить туда байт. И таким образом залить всю прошивку. Поэтому можно использовать плату ESP8266 в качестве программатора вместо ST-Link.

Вот здесь описан протокол SWD и как заливать через него прошивку: https://www.silabs.com/documents/public/application-notes/an0062.pdf

К сожалению, мне не удалось найти готовую реализацию программатора на базе ESP8266 именно для перепрошики STM32. Но вот здесь можно взять основные функции для доступа к чипу через SWD, чтения и записи ячеек FLASH памяти: https://github.com/scanlime/esp8266-arm-swd

Без перепрошивки

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

К сожалению, в гироскутерах используется нестандартный 9-bit UART, да еще и на нестандартной скорости 26315 baud. Аппаратный UART на ESP8266 не поддерживает 9-bit режим, а программного для Espruino (об этом позже) и Micropython с таким режимом нет. Вроде кто-то пытался переделать ардуиновскую 9-bit библиотеку для ESP8266 на базе Arduino Core, но кажется безуспешно.

Поэтому подменять сигналы с гироскопов придется с помощью Arduino Nano и софтверной библиотеки SoftwareSerial9.

Обратите внимание, что короткий провод (USART3, правый мотор) толерантен к +5V, но длинный (USART2, левый мотор) нет! Подключаться к нему с помощью Arduino Nano нужно обязательно через конвертор уровней с 5V до 3.3V. Или использовать делитель напряжение на резисторах.

Однако с подменой сигналов с гироскопов есть одна большая проблема.

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

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

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

Для заинтересовавшихся этим способом управления моторами, используйте поиск в гугле по ключевым словам "reverse engineering hoverboard". Также оставлю эти ссылки:

http://www.alvaroferran.com/blog/reverse-engineering-a-hoverboard
https://hackaday.com/2016/06/10/reverse-engineering-hoverboard-motor-drive/
http://drewspewsmuse.blogspot.com/2016/06/how-i-hacked-self-balancing-scooter.html
https://github.com/addibble/HoverboardController/blob/master/controller/controller.ino

Собственно, скетч для Arduino Nano для управления моторами гироскутера без перепрошивки, имитируя штатные сигналы с боковых гироскопов, получается очень простым.

Подключите пин D10 к TX у короткого провода (это белый или зеленый провод, но всегда ближний к черному с землей), а D12 к TX у длинного провода (тоже ближний к черному провод в шлейфе, но здесь обязательно через конвертор уровней с +5V до +3.3V). И общую землю к черному проводу на шлейфе. Саму Arduino Nano можно запитать от красного провода на шлейфе, на нем +12..+14V. Но это верхний предел для Arduino, может сгореть или сильно греться. Лучше запитайте ее от внешнего USB разъема или двух 18650 аккумуляторов.

А к пинам A0 и A1 подключите обычный аналоговый джойстик с aliexpress. После этого сможете джойстиком управлять моторами. Но будьте готовы, что после некоторого порога они выйдут на полные обороты.

Весь скетч для Arduino Nano с установленной библиотекой https://github.com/addibble/SoftwareSerial9 приведен под спойлером. Он отличается от большинства найденных в интернете тем, что в конце команды добавлены два байта 128. Без этого конкретно мой гироскутер отказывался работать.

Hoverboard.ino
#include <SoftwareSerial9.h>

SoftwareSerial9 driverOne(9, 10);   // Rx, Tx
SoftwareSerial9 driverTwo(11, 12);  // Rx, Tx

void setup() {
  driverOne.begin(26315);
  driverTwo.begin(26315);
}

char c = ' ';
signed int val1 = 0;
signed int val2 = 0;

void loop() {

  // Read Joystick
  int sensorValue1 = analogRead(A0);
  int sensorValue2 = analogRead(A1);

  // Convert the analog reading (which goes from 0 - 1023) to a voltage (0 - 5V):
  int val1 = map(sensorValue1, 0, 1023, -250, 250);
  int val2 = map(sensorValue2, 0, 1023, -250, 250);// map the vlaues to new values

  //*******************************************
  //**********motor1 controlling***************
  driverOne.write9(256);
  driverOne.write9(val1 & 0xFF);
  driverOne.write9((val1 >> 8) & 0xFF);
  driverOne.write9(val1 & 0xFF);
  driverOne.write9((val1 >> 8) & 0xFF);
  driverOne.write9(85);
  driverOne.write9(128);
  driverOne.write9(128);
  delayMicroseconds(300);
  //**********motor2 controlling***************
  driverTwo.write9(256);
  driverTwo.write9(val2 & 0xFF);
  driverTwo.write9((val2 >> 8) & 0xFF);
  driverTwo.write9(val2 & 0xFF);
  driverTwo.write9((val2 >> 8) & 0xFF);
  driverTwo.write9(85);
  driverTwo.write9(128);
  driverTwo.write9(128);
  delayMicroseconds(300);
}

Аккумулятор

Плата гироскутера успешно работает с напряжением до 50В (аккумуляторы 12S, возможно придется указать новое значение в прошивке), но штатный аккумулятор в гироскутерах в зависимости от уровня заряда имеет напряжение 36-42В (10S).

Обычно б/у гироскутеры имеют убитую или полуживую батарею. К счастью, ее легко починить. Встроенная зарядка на аккумуляторе имеет защиту от перенапряжения и слишком низкого напряжения на каждом из 10 блоков по две банки 18650 (всего там 20 аккумуляторов 18650), из которых состоят аккумуляторы гироскутеров. Но практически не имеет балансировки. Точнее, она есть, но очень слабая - током порядка 40 мА, что очень мало. При дисбалансе между банками 5% и выше, встроенная балансировка перестает работать, что приводит к тому, что одна или две банки уходят в ноль и аккумулятор перестает работать.

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

Аккумуляторы 18650 соединены в сборке с помощью точечной сварки. Но они также прекрасно паяются, если сделать небольшой шаблон, открывающий только центральную часть торца, и делать все это быстро. Инструкцию посмотреть можно, например, тут: https://www.youtube.com/watch?v=iI8M1PO59fY

Колеса

Обычно на газонокосилках есть оси, на которые крепятся колеса. На эти оси можно закрепить деревянный брусок 40х40 мм или аналогичный алюминиевый профиль, купленный в ближайшем Оби или Леруа Мерлен. А уже к бруску, с помощью штатного металлического зажима с разобранного корпуса гироскутера, закрепить мотор-колеса четырьмя болтами М8.

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

Общая идея должна быть видна на следующей фотографии

Впереди надо поставить одно по центру или два по бокам самоцентрирующихся колеса. Я выбрал вариант с двумя, но это не принципиально, с одним тоже хорошо работает.

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

И еще ближе...

Конкретно на моей газонокосилке подходят цельнолитые колеса от гироскутеров диаметром 8 дюймов, если крепить их снизу к деревянному бруску 40х40 мм. Либо надувные 10/11 дюймов (на фото выше), если крепить их сверху.

Учтите, что одной пары 8-дюймовых колес может не хватить на вашей газонокосилке, если она тяжелая, а трава высокая. Тогда придется делать косилку полноприводной, используя 4 колеса от двух 8" гироскутеров, и две их родные платы. Полноприводная версия ездит заметно лучше (я пробовал, но потом случайно сжег одну плату и поэтому вернулся к двухколесной версии). Для синхронной работы двух плат достаточно соединить их проводом с общей землей и посылать команды параллельно по двум UART, по одному на каждую плату.

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

Если же использовать 10.5 или 11 дюймовые надувные колеса, то одна пара мотор-колес с самоцентрирующимися колесиками спереди нормально тянет даже по высокой траве. Дело не только в диаметре, но и в ширине протектора, поэтому старайтесь брать колеса от "внедорожного" гиросутера.

Крепление платы

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

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

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

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

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

Радиоуправление

Мозгом нашей газонокосилки, который будет управлять моторами и выполнять простые функции автопилота, будет недорогой микроконтроллер ESP8266. А точнее, популярная на алиэкспресс плата NodeMCU V3 (цена около 150 руб). И она же будет wifi радиоканалом для пульта управления.

Так как интерфейс пульта управления на экране смартфона будет написан на HTML, то давайте и всю программу управления для ESP8266 напишем на более привычном и знакомом многим javascript, а не на стандартном Си.

Для этого существует прекрасный проект Espruino. Необходимо залить один раз на ESP8266 специальную прошивку, а потом можно будет писать скетчи для нее на обычном javascript. И заливать через удобный Espruino IDE по проводам через COM порт или по воздуху через Wifi.

Для любителей языка python есть аналогичный проект Micropython. А для языка Си на ESP8266 классический Arduino Core. Аналогичные модификации есть и для ESP32, если вы используете ее.

Установка Espruino

На ESP8266 устанавливать Espruino надо согласно инструкциям на сайте https://www.espruino.com/EspruinoESP8266

Для этого нужно скачать архив espruino_2v10.zip (ссылка вверху страницы) с http://www.espruino.com/Download, распаковать его, зайти в папку espruino_2v10_esp8266_4mb и запустить из нее сначала команду для очистки старого flash

esptool.py --port COM3 --baud 115200 erase_flash

А потом команду для прошивки самой Espruino

esptool.py --port COM3 --baud 115200 write_flash --flash_freq 80m --flash_mode qio --flash_size 32m 0x0000 "boot_v1.6.bin" 0x1000 espruino_esp8266_user1.bin 0x3FC000 esp_init_data_default.bin 0x3FE000 blank.bin

Здесь COM3 - это ваш определившийся COM порт при подключении ESP8266 к USB, а esptool - необходимая утилита для прошивки.

Для установки утилиты esptool, если у вас еще не установлен Python, то установите его с сайта https://www.python.org, а после в командной строке запустите команду

pip install esptool

Примечание: есть еще какая-то версия Espruino в виде combined прошивки, но ее установить у меня не получилось. Поэтому просто запускайте команду выше с такими параметрами, подставив в нее только номер своего COM порта.

Заливка прошивок

Для заливки javascript прошивок в ESP8266 понадобится Espruino IDE. Можно воспользоваться онлайн версией на https://www.espruino.com/ide/

Либо скачать оффлайн версию с http://www.espruino.com/Web+IDE

В обоих случаях в настройках Espruino IDE на вкладке Communications надо поставить Baud Rate = 115200, иначе в левой панели печатать нельзя будет, и скрипты заливаться не будут.

Чтобы заливать в ESP8266 свои программы по воздуху через wifi, нужно в настройках Espruino IDE на вкладке Communications в разделе Connect over TCP Address указать IP адрес вашей ESP8266. Если вы будете подключаться к ней как к точке доступа, то указывайте там всегда 192.168.4.1

А если настроите ESP8266, чтобы она подключалась к вашему существующему wifi роутеру, то тот IP, который ваша сеть назначит для ESP8266. Например, 192.168.1.104

Можно указать несколько IP адресов через точку с запятой: 192.168.1.104;192.168.4.1

Настройка Wifi

Осталось настроить Wifi на ESP8266.

Подключите NodeMCU V3 к USB, запустите Espruino IDE, нажмите кнопку Connect в левом верхнем углу и выберите номер COM порта, на котором у вас определилась ESP8266.

В левой части будет обычная javascript консоль, куда можно вводить код, который будет выполняться на ESP8266.

Чтобы настроить ESP8266 как точку доступа (к которой потом на улице будет подключаться смартфон), введите в этой консоли команды:

var wifi = require("Wifi");
wifi.disconnect();
wifi.startAP("ESP8266", {password:"esp12345", authMode:"wpa_wpa2"}); 
wifi.save();

Здесь в wifi.startAP() задается имя будущей Wifi сети, которая будет создаваться при включении микроконтроллера, и пароль для подключения к ней с телефона. А wifi.save() сохраняет настройки, чтобы они вступали в силу после перезагрузки ESP8266.

Все, дальше можете писать свои javascript программы и загружать их в ESP8266 через Espruino IDE. После заливки программы напишите в консоли save(), чтобы она сохранилась в памяти и начинала выполняться при каждом включении ESP8266 (не пишите эту команду в теле самого скрипта!)

И последнее, что стоит отметить, в Espruino при включении ESP8266 javascript скрипты начинают выполняться не с самого начала с первой строчки, как обычно делается во всех скриптах. А с функции onInit(). Это аналог window.onload() в браузерном javascript.

Пульт радиоуправления

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

После заливки в ESP8266 скрипта, который мы сейчас напишем, при подаче питания на ESP8266, будет появляться новая wifi сеть с именем "ESP8266" (или какое вы задали имя сети выше при настройке). Подключитесь к ней с телефона с паролем, который указали при настройке выше, а после откройте в браузере адрес http://192.168.4.1. На экране телефона появится интерфейс пульта управления.

Слайдеры радиоуправления газонокосилкой
Слайдеры радиоуправления газонокосилкой

Подключение

На NodeMCU V3 можно подавать питание по USB разъему от повербанка, либо через пин VIN.

ВНИМАНИЕ! на пин VIN нельзя подавать напряжение больше 8..9 В, иначе ESP8266 сгорит. Поэтому ее нельзя подключать к красному проводу в шлейфе короткого провода, так как там 12..14 В. С более низких напряжений брать с платы гироскутера +3.3V тоже нельзя, так как они не потянут по току. ESP8266 в пике может потреблять до 400 мА. Поэтому лучше запитать ESP8266 от внешнего аккумулятора или использовать какой-то понижающий преобразователь с 14В до 7 В.

Аппаратный UART на NodeMCU висит на пине D4, поэтому к белому или зеленому проводу на шлейфе с входом RX для UART (это всегда ближний провод к черному с землей), надо всегда подключать D4 с ESP8266. И GND к черному проводу на шлейфе.

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

Сначала подключайте аккумулятор, потом нажимайте кнопку питания для включения платы гироскутера. Она начнет издавать по три коротких звука, что нет UART сигнала. После этого подавайте питание на ESP8266. Повторяющиеся три звука перестанут издаваться, значит все ок.

Выключать в обратном порядке. Возможно, это необязательно, так как непонятно - приходящие с ESP8266 +3.3V по UART подают питание на весь чип STM32 или нет? Если да, то есть риск спалить STM32 при подаче на него внешнего питания от силового аккумулятора. Хотя на практике вроде этого не происходит, но до выяснения этого вопроса лучше не рисковать.

Программа

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

Телефон будет каждые 20..40 мс слать на ESP8266 команды вида /M-1000,1000

Первое число в диапазоне -1000...1000 будет означать газ обоих моторов назад/вперед, а второе, тоже в диапазоне -1000...1000, отвечать за повороты влево/вправо.

Соответственно, команда /M0,0 будет означать что газонокосилка должна стоять на месте.

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

Приступим к написанию кода! (да, я понимаю что все уже устали, осталось немного)

Сначала несколько настраиваемых параметров

wheel_4 = false;   // 2 платы?
pin_4 = NodeMCU.D2;// пин для UART второй платы
TIMEOUT = 300;     // таймаут связи, мс

Здесь можно задать wheel_4 = true для четырехмоторной версии. И пин pin_4 для второй платы (первая всегда будет на аппаратном UART на пине D4).

И несколько нужных переменных

speed = 0;  // газ
steer = 0;  // повороты

var server;
serial2 = null;
time = Date.now();
time_last = null;	// время последней команды

// для бинарных данных по UART
buffer = new ArrayBuffer(8);	// 8 байт в одной UART команде
view = new DataView(buffer);	// нужно для записи uint16_t и int16_t в buffer

Дальше функция onInit(), которая запускается при старте ESP8266. В ней мы инициируем один или два UART (первый аппаратный как основной, а второй софтверный для четырехколесной версии). В коде ниже используется скорость UART 9600, подразумевая что вы поставили такую при прошивке платы. Если же вы ее не меняли, то укажите здесь 115200.

Также в onInit() мы запускаем проверку по таймеру каждые 300 мс таймаута связи. И отправляем на моторы первую команду стоять на месте. И запускаем HTTP сервер для приема команд с телефона.

Здесь можно было бы использовать websocket'ы в качестве канала связи. У вебсокета задержка меньше, примерно 18-20 мс между принимаемыми командами. Но смысла в этом нет, потому что и обычные HTTP GET запросы тоже приходят с частотой примерно 20..25 мс (50 раз в секунду), чего вполне достаточно для плавного управления.

function onInit() {
	// эта функция запускается при старте

	// UART на D4 (аппаратный Tx)
	Serial2.setup(9600, {tx:NodeMCU.D4});

	// софтверный UART для второй платы
	if (wheel_4) {
		serial2 = new Serial();
		serial2.setup(9600,{tx:pin_4}); 
		}

	send_motors();

	// timeout wifi
	setInterval(check_timeout,TIMEOUT);

	// создаем HTTP сервер
	server = require("http").createServer(onPageRequest).listen(80);
	}

Функция проверки таймаута связи. Если связи давно нет, то останавливаем газонокосилку.

function check_timeout(){
	if (Date.now() - time_last > TIMEOUT) {
		// стоп
		speed = 0;
		steer = 0;	
		send_motors();
		}
	}

Дальше код HTTP сервера, который принимает команды от телефона

function onPageRequest(req, res) {
	var msg = req.url;
	res.writeHead(200, {'Content-Type': 'text/html', 'Access-Control-Allow-Origin': '*'});
	
	// время прихода последней команды
	time_last = Date.now(); 

	// парсим строку

	if (msg =="/") {
		res.end(WEB);
		return;

	} else if (msg[1]=="M") {
		// /M-1000,1000

		var ind = msg.indexOf(',',2);
		var sp = parseInt(msg.substring(2,ind));
		var st = parseInt(msg.substring(ind+1, msg.length));

		// газ моторов (шлем в UART всегда)
		if (sp >= -1000 && sp <= 1000 && st >= -1000 && st <= 1000) {
			speed = sp;
			steer = st;
			send_motors();
			}
		} 
	res.end('OK');
	}

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

Если пришла команда вида /M-1000,1000 (то есть: speed,steer), то вырезаем числа из строки, проверяем на всякий случай, что они находятся в диапазоне -1000...1000, и отправляем команду по UART на плату гироскутера.

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

Здесь интересная особенность, что надо отправлять клиенту HTTP заголовок

res.writeHead(200, {'Content-Type': 'text/html', 'Access-Control-Allow-Origin': '*'});

Чтобы Chrome не ругался на настройки безопасности (это потом понадобится, когда будем парсить ответ от сервера в следующих частях).

Ну и наконец функция, которая отправляет бинарные данные по UART на плату гироскутера (или на две, если у вас полноприводная газонокосилка)

function send_motors() {
	// отправка текущего газа на моторы

	view.setUint16(0, 0xABCD, true); // uint16_t start = 0xABCD, стартовые 2 байта сообщения, true - все little endian
	view.setInt16(2, steer, true);   // int16_t steer=-1000..1000
	view.setInt16(4, speed, true);   // int16_t speed=-1000..1000
	view.setUint16(6, 0xABCD ^ steer ^ speed, true);  // uint16_t checksum = start ^ steer ^ speed

	Serial2.write(buffer);	// write, чтобы отправить бинарные данные
	if (wheel_4) serial2.write(buffer);		// вторая плата
	}

Здесь используются целых две хитроумные штуки, чтобы записать из javascript бинарные данные. ArrayBuffer (переменная buffer) содержит 8 байт команды для UART команды. А DataView (переменная view) записывает в ArrayBuffer двухбайтовые представления чисел uint16_t и int16_t. Причем в little endian формате, благодаря флагу true.

Формат команды для UART определяется в прошивке платы гироскутера. Сначала в ней всегда должны идти два байта 0xABCD, потом повороты steer в диапазоне -1000...1000, потом скорость speed тоже -1000...1000, а потом идут два байта контрольной суммы как XOR от всех предыдущих чисел.

HTML интерфейс

Ну и наконец, осталось написать HTML интерфейс странички, которая будет загружаться с ESP8266. Мы его будем хранить в переменной WEB и отдавать клиенту при первом подключении.

Здесь важно помнить, что размер строки с HTML кодом не должен превышать 4 кб, иначе Espruino упадет.

Чтобы ESP8266 не упала из-за множества параллельных запросов (все таки это слабенький чип и он не предназначен для большой нагрузки), запросы будем слать строго последовательно. То есть, отправили один HTTP GET запрос, подождали ответ от ESP8266 (или таймаут), после отправили следующий и так далее.

Это позволит сделать работу радиоуправления надежной. При этом HTTP таймауты в браузерах очень долгие, по несколько секунд. Для газонокосилки это не страшно, так как через 300 миллисекунд (одна треть секунды) отсутствия связи она сама остановится. Но вот ждать потом с пультом несколько секунд без управления неприятно и неудобно.

В HTML5 есть специальный механизм добавить таймаут в функцию fetch() через signal и AbortController(). Странно, что таймаут не включен в стандарт самой fetch, ну да ладно. Выглядит это примерно так:

send_interval = 15; // ms

async function send(msg) {
  const controller = new AbortController();
  const signal = controller.signal;
  setTimeout(() => controller.abort(), 2000);

  try {
    const response = await fetch("http://192.168.4.1/"+msg, {signal});
    if (!response.ok) throw new Error('error');
    const data = await response.text();
    if (data!='OK') info.innerText = data;
    if (info.style.color != "green") info.style.color = "green";
  } catch (error) {
    info.style.color = "red";
  }
  if (msg[0] == 'M') setTimeout(send_motors, send_interval);
  }

Здесь установлен таймаут для fetch() как 2000 мс в setTimeout(). Дальше обработка всех возможных ошибок (здесь все неочевидно, потому что сама fetch воспринимает как ошибку ответы кроме статуса 200, поэтому ошибку самой связи приходится обрабатывать отдельно). Заодно меняем цвет значка в левом верхнем углу на зеленый если есть связь и на красный, если связи нет.

Причем в настольном Chromе этот механизм таймаута через signal у меня не работает. Но на телефоне все работает как надо.

И в конце, если команда начиналась на /M, то отправляем команду снова через send_interval миллисекунд. Небольшая пауза в send_interval = 15 полезна, чтобы меньше нагружать ESP8266. В итоге частота посылаемых команд будет не 20 мс, а 35 мс, но это приемлемо. Хотя можно поставить send_interval = 0, это тоже вполне нормально работает.

Управление газонокосилкой будет делаться слайдерами с измененным стилем:

.slider {-webkit-appearance: none; background: #82E0AA; outline: none; position: absolute; height:30%;}

Причем вертикальный слайдер сделаем из обычного путем добавления ему в CSS стиль rotate(-90deg);

Делать управление слайдерами на полный диапазон -1000...1000 не стоит, потому что газонокосилка будет слишком быстро ездить (под 15 км/час). Поэтому сделаем два дополнительных слайдера, ограничивающих верхний диапазон для основных.

При любом изменении слайдеров при событии oninput будем пересчитывать текущие газ и повороты с учетом ограничения по верхнему пределу:

function update(){
  speed = Math.round((speed_s.value*1 +1000)*(speed_m.value*2)/2000 - speed_m.value);
  steer = Math.round((steer_s.value*1 +1000)*(steer_m.value*2)/2000 - steer_m.value);
  }

При этом при отпускании слайдеров по событиям onmouseup и ontouchend будем ставить их в нейтральное положение.

Еще из интересного добавлено

<meta name="viewport" content="width=200, initial-scale=1">

Чтобы элементы интерфейса на мобильном экране выглядели достаточно крупными.

Над основным полем со слайдерами надо использовать CSS свойство touch-action:none, чтобы страница не сдвигалась, когда по ней водишь пальцами. Но ее все еще можно сдвинуть в поле штатного скроллбара в правой части экрана. Сделать полный экран у меня не получилось, найденные способы почему-то не заработали на мобильном Chrome.

В остальном интерфейс сделан на position:absolute; у элементов. Еще пришлось ширину элементов рассчитывать с учетом ширины экрана в вертикальной и горизонтальной ориентации, примерно так:

width:min(90%, 80vmin);

Это конечно грубо, можно было бы написать как-то лучше, но для для такой простой программы и так сойдет.

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

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

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

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

HTML код страницы в скрипте для ESP8266 заключен между скобками WEB=`...`, так туда можно копировать весь текст, что удобно отлаживать в обычном браузере.

Итоговый код программы

Объединив все вместе, получается следующий javascript код (убран под спойлер)

Исходный код для ESP8266

wheel_4 = false;		// 2 платы?
pin_4 = NodeMCU.D2;     // пин для UART второй платы
TIMEOUT = 300;			// таймаут связи, мс

speed = 0;  // газ
steer = 0;  // повороты

var server;
serial2 = null;
time = Date.now();
time_last = null;	// время последней команды

// для бинарных данных по UART
buffer = new ArrayBuffer(8);	// 8 байт в одной UART команде
view = new DataView(buffer);	// нужно для записи uint16_t и int16_t в buffer

function onInit() {
	// эта функция запускается при старте

	// UART на D4 (аппаратный Tx)
	Serial2.setup(9600, {tx:NodeMCU.D4});

	// софтверный UART для второй платы
	if (wheel_4) {
		serial2 = new Serial();
		serial2.setup(9600,{tx:pin_4}); 
		}

	send_motors();

	// timeout wifi
	setInterval(check_timeout,TIMEOUT);

	// создаем HTTP сервер
	server = require("http").createServer(onPageRequest).listen(80);

	}

function check_timeout(){
	if (Date.now() - time_last > TIMEOUT) {
		// стоп
		speed = 0;
		steer = 0;	
		send_motors();
		}
	}


function onPageRequest(req, res) {
	var msg = req.url;
	res.writeHead(200, {'Content-Type': 'text/html', 'Access-Control-Allow-Origin': '*'});
	
	// время прихода последней команды
	time_last = Date.now(); 

	// парсим строку

	if (msg =="/") {
		res.end(WEB);
		return;

	} else if (msg[1]=="M") {
		// /M-1000,1000

		var ind = msg.indexOf(',',2);
		var sp = parseInt(msg.substring(2,ind));
		var st = parseInt(msg.substring(ind+1, msg.length));

		// газ моторов (шлем в UART всегда)
		if (sp >= -1000 && sp <= 1000 && st >= -1000 && st <= 1000) {
			speed = sp;
			steer = st;
			send_motors();
			}
		} 
	res.end('OK');
	}


function send_motors() {
	// отправка текущего газа на моторы

	view.setUint16(0, 0xABCD, true); // uint16_t start = 0xABCD, стартовые 2 байта сообщения, true - все little endian
	view.setInt16(2, steer, true);   // int16_t steer=-1000..1000
	view.setInt16(4, speed, true);   // int16_t speed=-1000..1000
	view.setUint16(6, 0xABCD ^ steer ^ speed, true);  // uint16_t checksum = start ^ steer ^ speed

	Serial2.write(buffer);	// write, чтобы отправить бинарные данные
	if (wheel_4) serial2.write(buffer);		// вторая плата
	}



//WEB = `Server start...`;
WEB = `
<html>
<meta charset="UTF-8" />
<meta name="viewport" content="width=200, initial-scale=1">
<style>
.slider {-webkit-appearance: none; background: #82E0AA; outline: none; position: absolute; height:30%;}
</style>
<span id="info" style="color:red;">&nbsp;⬤</span>
<button onclick="div_opt.style.display=='none'?div_opt.style.display='block':div_opt.style.display='none';">☰</button>
<div id='div_opt' style="display:none">
<input id='speed_m' type="range" min="0" max="1000" value ="500"> <input id='steer_m' type="range" min="0" max="1000" value ="400">
<br>move <input id='i_move' type=text size=1 value=220>
</div>
<div style="height:calc(100vmin - 32px);touch-action:none; position:relative;">
<input type="range" id="speed_s" min="-1000" max="1000" value ="0" class="slider" style="transform: translateX(-32%) rotate(-90deg); top:32%; width:min(90%, 80vmin);" oninput="update(this)" onmouseup="reset(this)" ontouchend="reset(this)">
<input type="range" id="steer_s" min="-1000" max="1000" value ="0" class="slider" style="top:25%; left:40%; width: 55%;" oninput="update(this)" onmouseup="reset(this)" ontouchend="reset(this)">
<button style="top:75%;left:min(35%, 35vmin);height:15%;width:20%; position:absolute; user-select:none;" onmousedown="move(true)" onmouseup="move(false)" ontouchstart="move(true)" ontouchend="move(false)" ontouchcancel="move(false)">Move</button>
</div>
<script>

send_interval = 15; // ms

speed = 0; 
steer = 0;

is_move = false;       

setTimeout(send_motors,1000);

async function send(msg) {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 2000);

try {
  const response = await fetch("http://192.168.4.1/"+msg, {signal});
  if (!response.ok) throw new Error('error');
  const data = await response.text();
  if (data!='OK') info.innerText = data;
  if (info.style.color != "green") info.style.color = "green";
} catch (error) {
  info.style.color = "red";
}
if (msg[0] == 'M') setTimeout(send_motors, send_interval);
}

function send_motors() {
if (is_move) send('M'+i_move.value+","+steer); 
else send('M'+speed+","+steer); 
}

function reset(el) {
el.value = 0;
update(); 
}

function update(){
speed = Math.round((speed_s.value*1 +1000)*(speed_m.value*2)/2000 - speed_m.value);
steer = Math.round((steer_s.value*1 +1000)*(steer_m.value*2)/2000 - steer_m.value);
}

function move(v){
is_move=v;
}

</script>
`;

Залейте этот скрипт в ESP8266 через Espruino IDE и введите в консоли в левой части экрана

save()

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

Результат

В итоге у вас должно получиться примерно так

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

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

Комментарии (35)