Привет!
Вам часто бывает скучно? Ну так, что просто уже не знаешь, чем себя занять. Я в такие моменты люблю бесцельно скроллить ленту, залипать на разных видео, а еще листать маркетплейсы в надежде увидеть что-то, за что можно было бы зацепиться.
В один из таких моментов во время очередного просмотра содержимого Яндекс Маркета я наткнулся на игрушечного робота-курьера. Он показался мне достаточно милым, так что я решил его купить.

Робот-доставщик
Модель выполнена в виде настоящего ровера 3 от Яндекса в масштабе 1/64. Инерционный механизм делает из него небольшую игрушку, работающую по принципу детских машинок, которые были, вероятнее всего, у каждого в прекрасные времена более голубого неба и самой зеленой травы.

Также внутри робота расположены светодиоды для подсветки его «глаз» и периметра контейнера, который, к слову, открывается. Работает все великолепие на 3 батарейках типа LR44.

В общем ровер милый, выполнен неплохо и тут можно было бы остановиться, но в моей голове промелькнула одна мысль...
А может сделать его управляемым?
Почему нужно довольствоваться только тем, что есть из коробки? Ведь было бы здорово модифицировать этот ровер так, чтобы им можно было управлять. С этого момента моя голова была занята тем, чтобы понять, чего конкретно я хочу от бедной игрушки.
На самом деле, все просто. Примерно так же, как из инерционной машинки сделать радиоуправляемую. Только зачем радиоуправление, если можно написать мобильное приложение и управлять ей с телефона?
В итоге решил делать следующее:
-
Управление ровером должно быть реализовано с помощью мобильного приложения на iOS, которое я напишу на Flutter (про мой первый опыт работы с этим фреймворком можно прочитать в статье про Smart Connect)
-
Беспроводная передача данных будет производиться с помощью BLE, потому что ранее я имел с ним дело (можете ознакомиться со статьями про SmartLight и SmartPulse) и я считаю его идеальным под подобные проекты (имхо)
-
Ровер должен ездить вперед, назад, поворачивать влево и вправо, а также включать подсветку по кнопке в мобильном приложении
Далее я решил разобрать робота, чтобы понять, каким объемом пространства внутри него я могу располагать.

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

Для управления моторчиками нужен драйвер. Естественно, так как мы имеем дело с небольшим корпусом игрушки, то все платы надо было подобрать так же миниатюрные. Сказано - сделано, и драйвер L298N mini уже был у меня. На него сразу можно было припаять два моторчика, чему я был несказанно рад.
Родных батареек ровера на два маленьких мотора с высоким крутящим моментом было бы недостаточно (я проверил это в том числе экспериментально), поэтому решил использовать аккумулятор на 350 mAh и зарядную плату к нему. По моей задумке, это все должно было располагаться на месте батарейного отсека ровера - в его голове. Там было достаточно места для размещения аккумулятора, а также можно было спокойно получить доступ к его зарядной плате через крышку отсека, чтобы в дальнейшем заряжать робота.
А что будет всем этим управлять? Для работы с BLE существует крайне удачная линейка ESP32 контроллеров с различными модификациями. Так как в этом проекте мы ограничиваемся сравнительно небольшим пространством для монтажа плат внутрь устройства, я выбрал ESP32-C3 SuperMini, которая по своим размерам не больше зарядной платы аккумулятора.

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

Прошивка робота
Перейдем к прошивке ровера, полная версия которой лежит у меня в репозитории на GitHub (всегда рад гостям).
По сути все, что нам нужно для реализации передачи данных на ровер, это поднять BLE-сервер и определить сервис и характеристику, в которую мы будем отправлять значения для включения и выключения моторчиков. Я писал код в Arduino IDE с использованием библиотек BLEDevice.h
, BLEUtils.h
и BLEServer.h
Инициализируем работу с BLE посредством следующего кода:
BLEDevice::init("Yandex Delivery Robot");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService* pMoveService = pServer->createService(BLEUUID((uint16_t)0x170D));
pMoveCharacteristic = pMoveService->createCharacteristic(BLEUUID((uint16_t)0x2A60), BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
pMoveCharacteristic->setCallbacks(new MoveCharacteristicCallbacks());
pMoveService->start();
BLEAdvertising* pAdvertising = pServer->getAdvertising();
pAdvertising->addServiceUUID(pMoveService->getUUID());
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Здесь мы определяем наименование устройства, а также присваиваем UUID нашему сервису и характеристике pMoveCharacteristic
Запись в характеристики производится с помощью MoveCharacteristicCallbacks
, в которой я реализовал switch-case конструкцию. Внизу прикреплен урезанный фрагмент.
class MoveCharacteristicCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pCharacteristic) {
std::string value = pCharacteristic->getValue();
if (value.length() > 0) {
switch (value[0]) {
case 0x01: // Поворот ровера влево
ledcWrite(CHANNEL_IN1, 255);
ledcWrite(CHANNEL_IN2, 0);
ledcWrite(CHANNEL_IN3, 255);
ledcWrite(CHANNEL_IN4, 0);
break;
case 0x02: // Поворот ровера вправо
ledcWrite(CHANNEL_IN1, 0);
ledcWrite(CHANNEL_IN2, 255);
ledcWrite(CHANNEL_IN3, 0);
ledcWrite(CHANNEL_IN4, 255);
break;
}
}
}
}
Мобильное приложение
Не будем далеко отходить от кода, поэтому перейдем к написанию мобильного приложения на Flutter. В репозитории на GitHub я выложил его полную версию, а тут расскажу про основные вещи.
Логика работы кода мобильного приложения также как и в случае с кодом прошивки построена на switch-case конструкции. Я решил расположить на странице управления ровером кнопки в виде стрелок направления (вперед, назад, влево, вправо) и кнопку включения/выключения подсветки в виде горящей лампочки. Когда пользователь нажимает на любую из кнопок направления, в BLE-характеристику подключенного устройства записывается соответствующее значение (к примеру, для поворота ровера влево - 0x01, для правого поворота - 0x02 и тд.).
Positioned(
bottom: 160,
left: 80,
child: controlButton(Icons.arrow_back, 0x01, "Left"),
),
Positioned(
bottom: 160,
right: 80,
child: controlButton(Icons.arrow_forward, 0x02, "Right"),
)
Как только пользователь отпускает кнопку, в характеристику записывается значение 0, посредством которого в прошивке ровера включается один из кейсов свитча с отключением всех моторов. Таким образом робот будет ехать, когда кнопка направления нажата, а как только ее отпустят, ровер остановится.
onPointerDown: (_) => sendCommand(command),
onPointerUp: (_) => sendCommand(0)
Подсветка включается при одинарном нажатии на кнопку с лампочкой. При повторном нажатии на нее, подсветка будет отключена. По сути это просто кнопка с запоминанием своего состояния.
Positioned(
bottom: 40,
right: 20,
child: FloatingActionButton(
onPressed: () {
setState(() {
toggleState = !toggleState;
sendCommand(toggleState ? 3 : 6);
});
},
child: Icon(
toggleState ? Icons.lightbulb_outline : Icons.lightbulb,
color: Color.fromRGBO(255, 53, 63, 1),
),
backgroundColor: Colors.white,
),
)
Чтобы работать с BLE, я подключил flutter_blue: ^0.8.0
, а сам процесс подключения к устройству построен на определении рассылки от конкретного BLE-сервера по его названию. Запись значений производится по UUID сервиса и характеристики ровера.
List<BluetoothService> services = await widget.device.discoverServices();
var targetService = services.firstWhere((service) =>
service.uuid == Guid('0000170D-0000-1000-8000-00805f9b34fb'));
var targetCharacteristic = targetService.characteristics.firstWhere(
(characteristic) =>
characteristic.uuid ==
Guid('00002A60-0000-1000-8000-00805f9b34fb'));
await targetCharacteristic.write(value);
После того, как я протестировал работоспособность приложения с микроконтроллером, мне захотелось как-то "довести до ума", чтобы как минимум приложение выглядело чуть более симпатично. На странице управления ровером были только четыре кнопки управления моторами и одна кнопка для подсветки. Это выглядело скучновато, поэтому мне захотелось добавить динамики. Решил изобразить в Adobe Illustrator модельки с направлением ровера.

Реализовал обновление картинок по нажатию на соответствующие кнопки управления ровером также через switch по отправляемому с кнопки значению.
setState(() {
switch (command) {
case 0x01:
currentImage = 'assets/Влево.png';
break;
case 0x02:
currentImage = 'assets/Вправо.png';
break;
case 0x05:
currentImage = 'assets/Вперед.png';
break;
case 0x04:
currentImage = 'assets/Назад.png';
break;
default:
break;
}
});
Далее подобрал для интерфейса оригинальный красный цвет Яндекса и нарисовал иконку приложения, чтобы завершить разработку мобильного приложения и переключиться на монтаж всей электроники внутрь ровера.

Собираем приложение в Xcode, устанавливаем его на мобильное устройство (в моем случае это iPhone 12 Pro Max) и запускаем его для тестирования его работоспособности. В итоге мобильное приложение получило следующий внешний вид:

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

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

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

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

В итоге не без проблем, но у меня получилось разместить все компоненты внутри ровера. Зарядка устройства производится со стороны нижней части корпуса. Это напоминает мне подключение Magic Mouse, но только я это сделал за неимением других вариантов, а не потому что не люблю людей. Можно было бы расположить зарядную плату как-то иначе, но я не хотел портить внешний вид ровера (днище не считается, потому что его не видно).

Поехали!
На этом я завершил модификацию игрушки. По началу у меня проскакивали мысли добавить в ровер еще что-нибудь, к примеру лазерный дальномер, чтобы попробовать воссоздать логику работы реального робота-курьера, но я отказался от этой идеи, потому что места для размещения дополнительных плат в нем уже не осталось. Пилить контейнер изнутри я не хочу ради этого, потому что тогда теряется сама суть такого робота (даже пускай и игрушечного).
В качестве демонстрации управления ровером я записал небольшое видео:
Большое спасибо всем, кто прочитал эту статью!
Я буду вам безумно благодарен, если вы поделитесь своим мнением о ней в комментариях. Если вам понравилось, можете почитать другие мои статьи, которые лежат здесь. Обещаю, что вы нескучно проведете время.
Если у вас есть идеи или предложения, можете написать в комментариях, какие еще игрушки или вещи можно было бы модифицировать, и рано или поздно я сделаю это)
P.S. По поводу данной статьи я писал в отдел по связям с общественностью Яндекса, чтобы избежать претензий с их стороны, если кто-то решит, что модификация их официальной игрушки - это что-то уничижительное и негативное для репутации компании. К сожалению, меня проигнорировали, поэтому я считаю нужным упомянуть это здесь.
Автор: Макс Князев