Расскажу, как я собрал прототип газонокосилки, которой можно управлять с телефона. Она понадобилась для моего, совсем немаленького дачного участка (почти полгектара). Толкать косилку впереди себя или даже ходить сзади, держа агрегат за ручку, показалось мне жутко неудобным занятием. Поэтому я решил сделать что-то, наподобие радиоуправляемой машинки. А поскольку с пультами и джойстиками возиться тоже не хотелось, то написал 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;
Пока полной автономности не получается, поскольку программа и пара модулей — это полдела. Косилку, чтобы она длительное время работала в отсутствие хозяев, надо как-то заправлять и подзаряжать. Для этого необходимо построить «базовую станцию» с собственным программным обеспечением и механизмами. Хватит ли у меня на это терпения и времени — я не знаю.
Вывод
Можно ли построить автономную газонокосилку самому? Однозначно, можно. Создание робота с аналогичными функциями сегодня вполне доступно для любого энтузиаста. Как говорится, «было бы желание». К тому же, это интересно и здорово поднимает самооценку, да и в хозяйстве такая машина лишней не будет.
Автор: Petro38
