
Расскажу, как я собрал прототип газонокосилки, которой можно управлять с телефона. Она понадобилась для моего, совсем немаленького дачного участка (почти полгектара). Толкать косилку впереди себя или даже ходить сзади, держа агрегат за ручку, показалось мне жутко неудобным занятием. Поэтому я решил сделать что-то, наподобие радиоуправляемой машинки. А поскольку с пультами и джойстиками возиться тоже не хотелось, то написал Android-приложение и скетч для управления косилкой по WiFi с телефона.
Не игрушка...
«Машинка» получилась совсем не игрушечной и отнюдь не простой. Ходовая часть у неё — электрическая, на четырёх небольших мотор-колёсах, мощностью по 250 ватт каждое, и работает она от батареи 36 В. Но для того чтобы косить траву на больших площадях и в тяжелых условиях, электрический привод — слабоват. Четыре шестидюймовых мотор-колеса отлично справляются с движением — косилка маневренна, легко поворачивает и уверенно едет. Но вращать нож на участке в полгектара одним электромотором — задача куда сложнее. Либо нужен очень мощный (и тяжёлый) аккумулятор, либо — частые остановки на подзарядку. Поэтому в качестве рабочего органа я использовал бензиновый двигатель — Honda, 2.5 л. с., от старенькой газонокосилки, которую купил по случаю.

Сначала надо было определиться с размерами будущей машины. Очевидно, что устройство, которое должно выполнять работу комбайна, размером с пылесос не сделаешь. Двигатель, колёса, батареи: всё это надо было куда-то девать. Ширина скашивания травы в 48 см — уже немало. В итоге получился агрегат 1500*900 мм (с колёсами). Он помещается в автомобиль, но только в те авто, где для увеличения глубины багажника можно сложить заднее сиденье, например, в Duster или Tiguan. Поворачивается машинка по «танковому» принципу — противовращением колёс правого и левого борта. Так лучше обеспечивается поворот на месте и точность позиционирования. Скорость — не более 5-6 км/час.

Все металлоконструкции сделаны «на коленке», при помощи «сварки-болгарки», владению которыми я обучился у себя же на даче. Управляющий блок и всю остальную электронику я убрал в разнокалиберные распределительные коробки. Определённые проблемы вызвала регулировка среза травы по высоте. Для этого нужно поднимать и опускать двигатель, вместе с ножом. Я сварил стальной короб с кронштейнам�� и предусмотрел на нём и на раме крепления для электрического актуатора, который двигает короб и работает от батареи. Аккумулятор я хотел собрать самостоятельно но потом, в целях экономии времени купил готовый, для электросамоката (36V 10200 mAh).

Дальше потребовались мотор-колеса и контроллеры к ним. Прочёл в интернетах, что для таких задач отлично подходят старые гироскутеры с одноплатными контроллерами на процессорах STM32F103RCT6 или GD32F103RCT6. Купил сразу два, заплатил 40 долларов за две штуки. Повезло: контроллеры оказались подходящими. Для прошивки использовал утилиту STM32 ST-LINK Utility. Прошивка контроллера есть на Github. «Танцы с бубнами», конечно, были но по большей части из-за моей невнимательности. Все этапы работы подробно описаны Эммануэлем Феру.

Косилка, как сервер
С android-приложением я справился быстро, так как периодически пишу программы на Android, а вот со скетчем для контроллера на процессоре ESP32, пришлось повозиться. Некоторое время заняла «стыковка» всех трёх контроллеров — гироскутерных для двух осей и ESP32. Прошивка контроллеров гироскутеров предп��лагает возможность управления ими через UART. Главное, — разобраться с командами и заставить колёса вращаться.

Приложение с простым интерфейсом, напоминающим обычные кнопки джойстика (полной аутентичности, конечно, не будет), используется в качестве пульта (клиента). Пульт передаёт числа, которые затем можно отследить в мониторе порта, если компьютер с Arduino IDE подключить к ESP32. Мне такая схема упростила отладку. Модуль контроллера «поднимает» Wi‑Fi SoftAP, запускает сервер, который «слушает» команды управления. А мы — подключаем телефон к WiFi нашей косилки и управляем контроллерами колёс через ESP32, по Serial1 и Serial2 .
#define START_FRAME 0xABCD typedef struct { uint16_t start; int16_t leftWheel; int16_t rightWheel; uint16_t checksum; } SerialCommand; SerialCommand CommandRear, CommandFront; void SendToRear(int16_t uLeft, int16_t uRight) { CommandRear.start = START_FRAME; CommandRear.leftWheel = uLeft; CommandRear.rightWheel = uRight; CommandRear.checksum = (uint16_t)CommandRear.start ^ (uint16_t)CommandRear.leftWheel ^ (uint16_t)CommandRear.rightWheel; Serial1.write((uint8_t *)&CommandRear, sizeof(CommandRear)); } void SendToFront(int16_t uLeft, int16_t uRight) { CommandFront.start = START_FRAME; CommandFront.leftWheel = uLeft; CommandFront.rightWheel = uRight; CommandFront.checksum = (uint16_t)CommandFront.start ^ (uint16_t)CommandFront.leftWheel ^ (uint16_t)CommandFront.rightWheel; Serial2.write((uint8_t *)&CommandFront, sizeof(CommandFront)); }
В качестве транспортного протокола я выбрал UDP, который обеспечивает минимальную задержку отправки команд (не запрашивает подтверждений получения пакетов). Потерянный пакет не страшен, если следующий появится через 50-100 мс. При этом, безопасность «закрывается» полной остановкой движения по тайм-ауту.
const unsigned long DISCONNECT_TIMEOUT = 5000; unsigned long lastPacketTime = 0; void loop() { unsigned long currentTime = millis(); int packetSize = udp.parsePacket(); if (packetSize) { int len = udp.read(incomingPacket, 255); if (len > 0) { incomingPacket[len] = 0; lastPacketTime = currentTime; int command = atoi(incomingPacket); // ... разбор command и вычисление currentLeftWheel/currentRightWheel ... SendToRear(currentLeftWheel, currentRightWheel); SendToFront(currentLeftWheel, currentRightWheel); } } // если нет команд — стоп if (currentTime - lastPacketTime > DISCONNECT_TIMEOUT) { currentLeftWheel = 0; currentRightWheel = 0; SendToRear(0, 0); SendToFront(0, 0); } }
Нужно было регулировать и работу бензинового двигателя. В обычной косилке это делает оператор или встроенный механический регулятор, но он — инертный, и в высокой траве обороты перегруженного мотора могут упасть так быстро, что он — заглохнет. Поэтому я добавил датчик Холла, установив его прямо возле маховика бензинового двигателя, к которому приклеен магнит для возбуждения тока в первичной катушке зажигания. Датчик Холла считает обороты, а ESP32 сравнивает их с номиналом и если обороты падают, контроллер отдаёт сервоприводу команду приоткрыть воздушную заслонку карбюратора (угол поворота сервопривода я определил эмпирическим путём).
### ISR Холла #define HALL_SENSOR_PIN 4 volatile unsigned long pulseCount = 0; volatile unsigned long lastPulseMicros = 0; const unsigned long DEBOUNCE_US = 5000; void IRAM_ATTR countPulse() { unsigned long now = micros(); if (now - lastPulseMicros >= DEBOUNCE_US) { pulseCount += 1; lastPulseMicros = now; } }
### расчёт RPM раз в 500 мс const unsigned long CALCULATION_INTERVAL = 500; // ms const float pulsesPerRev = 1.0f; volatile float lastRpm = 0.0f; static unsigned long lastCalculationTime = 0; void loop() { unsigned long currentTime = millis(); if (currentTime - lastCalculationTime >= CALCULATION_INTERVAL) { noInterrupts(); unsigned long count = pulseCount; pulseCount = 0; interrupts(); float rpm = (count / (CALCULATION_INTERVAL / 1000.0f)) * 60.0f; rpm /= pulsesPerRev; noInterrupts(); lastRpm = rpm; interrupts(); lastCalculationTime = currentTime; } // ... }
### регулятор газа по порогам const int NORMAL_RPM_MIN = 2800; const int NORMAL_RPM_MAX = 3800; int targetServoPosition = 0; void regulateEngineSpeed(float currentRPM) { if (currentRPM < NORMAL_RPM_MIN - 200) { targetServoPosition += 5; targetServoPosition = constrain(targetServoPosition, 0, 80); } else if (currentRPM > NORMAL_RPM_MAX + 200) { targetServoPosition -= 3; targetServoPosition = constrain(targetServoPosition, 0, 80); } }

Что планирую сделать ещё в механике и телеметрии?
Косилка работает, хотя косить зимой особо нечего, и основные испытания агрегата и проверка его на прочность пройдут летом. В электронной части, в скетче и приложении, планирую добавить несложную телеметрию (контроль напряжения батареи, остатка топлива, пройденного расстояния и проч.). Датчиков уровня топлива на небольших бензиновых двигателях общего назначения обычно не ставят, но можно запросто «приколхозить» такой датчик прямо в бак.

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

Делаем автономный «комбайн»
Но самая важная и интересная опция, которую я планирую добавить, заключается в модернизации электронной части и программного обеспечения: я собираюсь сделать машину автономной. Для позиционирования планирую использовать трилатерацию — метод определения местоположения путём построения системы смежных треугольников, в которых измеряются длины их сторон. То есть мы работаем с расстояниями (не путайте с триангуляцией в которой измеряются углы треугольников по отношению к известным точкам).

Для решения такой задачи понадобятся четыре модуля BU01 с процессором DW1000, работающие по технологии Ultra-Wideband (UWB) и стандарту IEEE 802.15.4a/z. Три модуля с питанием от дешевых "зарядников" на 5 вольт с понижающими стабилизаторами (некоторые модули, из тех, что есть в продаже, рассчитаны на питание 3.3 вольта), необходимо будет разместить по краям участка, они будут выполнять роль анкеров. Один — надо установить на косилке. Он будет работать, в качестве тега и определять расстояния до остальных трех по методу TDoA (по разнице во времени передачи сигнала между анкорами), а с ESP-32 мы свяжем его по SPI. Точность позиционирования, заявленная производителем модулей — до 10 см. Но даже если будет 30 — это уже хорошо. Скорость передачи данных — до 6.8 Мбит/сек, расстояние – до 40 метров. Если участок больше, можно поставить больше анкеров (модуль поддерживает до 64). Прошиваются модули с помощью обычного USB-UART преобразователя логических уровней.

Навигация: фильтруем и компенсируем
Для планирования пути косилки по участку используем алгоритм А*. Для повышения точности используем фильтр Калмана, поскольку координаты выдаются с погрешностью +- 10 - 20 см. Наша система не потребует хранения данных — мы будем обрабатывать их по мере поступления. Рабочая зона делится на ячейки 50*50 (массив areaMap [GRID_SIZE][GRID_SIZE], где GRID_SIZE равен 50). Ширина ножа косилки 50 см, но для карты покрытия лучше возьмём ячейку (CELL_SIZE) 20–25 см, чтобы обеспечить полное перекрытие проходов и компенсировать ошибки UWB/курса. В процессе эксплуатации CELL_SIZE отработаем на практике.
#define GRID_SIZE 50 const double CELL_SIZE = 0.2; // 20 см GridCell areaMap[GRID_SIZE][GRID_SIZE];
Каждая ячейка хранит флаг, координаты центра, данные для алгоритма. При каждом обновлении позиции система помечает текущую ячейку как посещённую.
void updateMap() { int x = (int)round(currentPos.x / CELL_SIZE); int y = (int)round(currentPos.y / CELL_SIZE); if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { areaMap[x][y].visited = true; } }
Полный алгоритм нам не нужен, так как нужно искать следующую непосещённую ячейку, а не финишную точку.
void pathPlanner() { for (int x = 0; x < GRID_SIZE; x++) { for (int y = 0; y < GRID_SIZE; y++) { if (!areaMap[x][y].visited) { targetPos.x = x * CELL_SIZE + CELL_SIZE / 2; targetPos.y = y * CELL_SIZE + CELL_SIZE / 2; return; } } } targetPos = currentPos; // всё покрыто — остановка }
Одномерный фильтр Калмана позволяет более-менее эффективно бороться с ошибками позиционирования.
void kalmanFilter(Position *pos) { // Прогноз kalman.pX += kalman.q; kalman.pY += kalman.q; // Коррекция double kX = kalman.pX / (kalman.pX + kalman.r); double kY = kalman.pY / (kalman.pY + kalman.r); kalman.xEst += kX * (pos->x - kalman.xEst); kalman.yEst += kY * (pos->y - kalman.yEst); kalman.pX *= (1 - kX); kalman.pY *= (1 - kY); pos->x = kalman.xEst; pos->y = kalman.yEst; }
С помощью PID-регулятора с защитой от накопления ошибки, корректируем направление. Если погрешность превышает пять градусов - косилка останавливается и поворачивается на месте, компенсируя ошибку.
double headingError = targetHeading - currentPos.heading; // Нормализация [-180; +180] if (headingError > 180) headingError -= 360; if (headingError < -180) headingError += 360; // Анти-винт integral = constrain(integral + headingError, -MAX_INTEGRAL, MAX_INTEGRAL); double derivative = headingError - lastError; double pidOutput = Kp * headingError + Ki * integral + Kd * derivative;
Пока полной автономности не получается, поскольку программа и пара модулей — это полдела. Косилку, чтобы она длительное время работала в отсутствие хозяев, надо как-то заправлять и подзаряжать. Для этого необходимо построить «базовую станцию» с собственным программным обеспечением и механизмами. Хватит ли у меня на это терпения и времени — я не знаю.
Вывод
Можно ли построить автономную газонокосилку самому? Однозначно, можно. Создание робота с аналогичными функциями сегодня вполне доступно для любого энтузиаста. Как говорится, «было бы желание». К тому же, это интересно и здорово поднимает самооценку, да и в хозяйстве такая машина лишней не будет.
