
В предыдущей статье я показал, как настроить GPIO одноплатника на примере Orange Pi Zero H+. Я привел команды для проверки GPIO и написал скрипт gpio_setup.sh для добавления необходимых прав на GPIO для пользователя. Также разработал класс LedLineGpio для управления светодиодами и настроил задержку при отправке команд. Кроме того, я изменил механизм их отправки так, чтобы команда не дублировалась при удержании кнопки.
Оглавление
Введение
Веб-камера — глаза робота. Пишу веб-приложение на FastAPI для управления DIY-проектом
Часть 4. Управление моторами через L298N // вы здесь
Инфраструктура и развёртывание проекта
В четвёртой статье я расскажу, как управлять моторами через популярный драйвер двигателей L298N. Также покажу, как подключить этот драйвер к одноплатнику Orange Pi Zero H+. Будет представлен программный код для управления моторами через GPIO, а также код самих команд управления роботом для бэкенд-приложения на FastAPI.
Статья будет полезна любителям DIY-проектов и веб-разработчикам, интересующимся фреймворком FastAPI.
Драйвер двигателей L298N
Драйвер двигателей — это электронное устройство, которое позволяет микроконтроллеру управлять скоростью и направлением вращения электродвигателя, подавая на него соответствующие сигналы.
Изначально я планировал использовать российский драйвер ЛМ2-130, который применял в статье, где собирал гусеничную платформу на Arduino. Однако на практике столкнулся с ограничением моей платы Orange Pi Zero H+: при работе с ЛМ2-130 она позволяет управлять только одним мотором.
Причина в том, что у Orange Pi Zero H+ есть лишь один PWM-пин, необходимый для регулирования скорости. Управление скоростью через PWM поддерживается во всех драйверах моторов, но у ЛМ2-130 он также отвечает за остановку двигателя.
Как управлять мотором на ЛМ2-130 (на базе микросхемы L293D)?
Пины IN Right и IN Left — подача 0 (LOW/False) или 1 (HIGH/True) для смены направления вращения мотора;
Пины PWM-Right и PWM-Left — значение от 0 до 255 для регулирования мощности вращения.
Таким образом, без второго PWM-пина невозможно корректно останавливать второй мотор — а для гусеничной платформы с двумя двигателями это критично. К тому же, если на моей плате есть только один PWM-вывод, не исключено, что на других одноплатниках может возникнуть та же проблема.
Поэтому мне потребовался драйвер, который умеет отключать моторы без использования PWM. В итоге я выбрал более простой и популярный L298N — он позволяет управлять моторами без необходимости выделять под это отдельные PWM-пины.

Управление моторами на L298N:
Пины ENA и ENB:
— Значения от 0 до 255 — регулировка мощности (для этого нужно снять перемычку);
— Если оставить перемычку на месте, моторы будут вращаться на максимальной скорости.Пины IN1 и IN2 — управление направлением вращения левого мотора:
— IN1 = HIGH и IN2 = LOW — мотор вращается в одну сторону;
— IN1 = LOW и IN2 = HIGH — мотор вращается в другую сторону;
— N1 = LOW и IN2 = LOW — мотор останавливается.Пины IN3 и IN4 — управление направлением вращения правого мотора (по аналогии с левым).
Таким образом, для каждого направления используется свой управляющий пин, а при подаче LOW на оба пина мотор останавливается без применения PWM. Теперь подключим драйвер к одноплатнику.
Подключение L298N к Orange Pi Zero H+:
IN1 -> Pa11;
IN2 -> Pa12;
IN3 -> Pa18;
IN4 -> Pa19;
+5V вход -> +5V;
GND -> GND.
Драйвер я запитал напрямую от одноплатника — исключительно для демонстрации на тестовом стенде. Плата не способна отдать через пин 5 В ток, достаточный для раскрытия всего потенциала DC-моторов и самого драйвера. Поэтому моторы не раскручиваются на полную, но это даже удобно: стенд не будет слишком быстро «уезжать» со стола.
Кроме того, мой небольшой powerbank выдаёт лишь 1 A на выходе, чего тоже недостаточно. В дальнейшем я планирую заменить его на блок питания минимум на 2 A. С более мощным источником питания система станет стабильнее, а платформа будет быстрее двигаться и получит лучшую тягу.

На фото — тестовый стенд с двумя LEGO-моторами. Подключение выполнено через самодельный адаптер LEGO → Dupont мама, собранный буквально на коленке. Такая конструкция позволяет проверить правильность работы моторов без подключения громоздкого гусеничного шасси.
Замена статического ip на динамический
Статический IP-адрес на первый взгляд кажется удобным решением. Устройству задаётся фиксированный адрес, по которому можно подключаться без необходимости проверять его актуальность в настройках роутера.
Однако у такого подхода есть подводный камень: статический IP может пересечься с динамическим адресом другого устройства. Именно в такой ситуации я недавно оказался: IP моего ПК неожиданно совпал со статическим IP одноплатника, который я включил позже.
Что делать в подобных случаях? Можно перезапустить ПК или вручную переназначить ему IP, но это неудобно и отнимает время. Поэтому я решил отказаться от статического адреса на одноплатнике и вместо этого задать ему имя (доменоподобный идентификатор), связанный с его динамическим IP. Это имя будет видно всем устройствам локальной сети.
В поиске решения я наткнулся на avahi-daemon — простую и элегантную службу. Она реализует протокол mDNS/DNS-SD (мультикаст DNS), который позволяет устройствам обнаруживать друг друга в локальной сети без централизованного DNS-сервера. С его помощью можно автоматически находить сервисы (например, принтеры, медиа-серверы, SSH-хосты) по адресу вида hostname.local.
Устанавливаю avahi-daemon:
sudo apt install avahi-daemon
Задаю имя хоста для одноплатника:
sudo hostnamectl set-hostname mydevice
Разбор команды:
hostnamectl — утилита управления именем хоста (systemd);
set-hostname — подкоманда для установки нового имени;
mydevice — имя хоста, которое сохранится в системе.
Перезапуск службы avahi-daemon для применения настроек:
sudo systemctl restart avahi-daemon
Теперь к одноплатнику можно обращаться по адресу mydevice.local с любого устройства в сети. При этом IP останется динамическим, а конфликтов с адресами больше не возникнет.
При необходимости avahi-daemon можно установить и на другие Linux-устройства, чтобы обращаться к ним по аналогичной схеме.
Если используете avahi-daemon в приложении, не забудьте заменить в конфигурации IP-адрес устройства на его имя вида yourdevice.local.
Код робота
Начну с классов, отвечающих за управление GPIO-линиями, которые находятся в файле gpio_control.py.
Я слегка модифицировал класс LedLineGpio, сделав его универсальным. Теперь параметры линии GPIO передаются в конструктор, а не захардкожены.
Новый импорт:
from settings import settings
Обновлённый код класса LedLineGpio:
class LedLineGpio: """Класс для управления линией gpio для LED""" def __init__(self, line: int, gpio_path: str, consumer: str) -> None: self.line = line self._request = request_lines( path=gpio_path, consumer=consumer, config={ self.line: LineSettings( direction=Direction.OUTPUT, output_value=Value.INACTIVE ) } ) # Другой код
Разбор обновлённого кода:
line: int — номер линии (пина) GPIO, с которой будет работать класс. Это индекс внутри контроллера GPIO;
gpio_path: str — путь к устройству GPIO в системе (обычно что-то вроде
/dev/gpiochip0), указывает, через какой чип/контроллер вести работу;consumer: str — строковое имя потребителя (идентификатор клиента), которое передаётся драйверу GPIO для отладки и отслеживания того, кто захватил линию.
Следующий шаг — написать универсальный класс для управления линиями GPIO моторов. Он позволит работать с любым количеством двигателей, создавая отдельный экземпляр класса для каждого мотора.
Класс MotorDCLineGpio:
class MotorDCLineGpio: """Класс для управления линией GPIO для DC мотора""" def __init__(self, line_in1: int, line_in2: int, gpio_path: str, consumer: str) -> None: self.line_in1 = line_in1 self._request_in1 = request_lines( path=gpio_path, consumer=consumer, config={ self.line_in1: LineSettings( direction=Direction.OUTPUT, output_value=Value.INACTIVE ) } ) self.line_in2 = line_in2 self._request_in2 = request_lines( path=gpio_path, consumer=consumer, config={ self.line_in2: LineSettings( direction=Direction.OUTPUT, output_value=Value.INACTIVE ) } ) def forward_motor(self) -> None: """Метод для вращения мотора вперёд""" self._request_in1.set_value(self.line_in1, Value.ACTIVE) self._request_in2.set_value(self.line_in2, Value.INACTIVE) def backward_motor(self) -> None: """Метод для вращения мотора назад""" self._request_in1.set_value(self.line_in1, Value.INACTIVE) self._request_in2.set_value(self.line_in2, Value.ACTIVE) def stop_motor(self) -> None: """Метод для остановки мотора""" self._request_in1.set_value(self.line_in1, Value.INACTIVE) self._request_in2.set_value(self.line_in2, Value.INACTIVE) def close(self) -> None: """Метод для освобождения ресурса""" self._request_in1.release() self._request_in2.release()
Разбор кода:
self.line_in1 и self.line_in2 — номера GPIO-линий (пинов) на одноплатнике;
self._request_in1 и self._request_in2 — объекты, управляющие GPIO-линиями, соответствующими пинам направления;
forward_motor() — вращение мотора вперёд;
backward_motor() — вращение мотора назад;
stop_motor() — остановка мотора;
close() — освобождение ресурсов.
Принцип работы аналогичен управлению LED: линии GPIO активируются и деактивируются через метод set_value(). Отличие в том, что для вращения мотора одна линия должна быть в состоянии Value.ACTIVE, а вторая — Value.INACTIVE. Для остановки же обе линии переводятся в Value.INACTIVE.
После написания этого класса его уже можно использовать в сервисе робота. Но есть несколько неудобств:
Нужно создавать два экземпляра класса для каждого мотора и передавать в них параметры линий;
Для управления одним мотором приходится вызывать сразу два метода;
Такой способ не масштабируется — он неудобен для платформ с четырьмя и более моторами.
Чтобы решить эти проблемы, я написал класс-«интерфейс» для управления роботом. Он берёт на себя создание экземпляров классов для моторов, настройку GPIO и вызов нескольких методов внутри одного. В результате достаточно создать всего один экземпляр этого интерфейса в сервисе робота — и управление будет готово.
Класс RobotControl:
class RobotControl: """Класс для управления роботом""" def __init__(self) -> None: self._left_motor = MotorDCLineGpio( line_in1=settings.gpio_lines.left_motor_line_in1, line_in2=settings.gpio_lines.left_motor_line_in2, gpio_path=settings.gpio_lines.gpio_path, consumer=settings.gpio_lines.left_motor_consumer ) self._right_motor = MotorDCLineGpio( line_in1=settings.gpio_lines.right_motor_line_in1, line_in2=settings.gpio_lines.right_motor_line_in2, gpio_path=settings.gpio_lines.gpio_path, consumer=settings.gpio_lines.right_motor_consumer ) self.stop() def forward(self) -> None: """Движение робота вперёд""" self._left_motor.forward_motor() self._right_motor.forward_motor() def backward(self) -> None: """Движение робота назад""" self._left_motor.backward_motor() self._right_motor.backward_motor() def left(self) -> None: """Поворот робота налево""" self._right_motor.forward_motor() self._left_motor.backward_motor() def right(self) -> None: """Поворот робота направо""" self._right_motor.backward_motor() self._left_motor.forward_motor() def stop(self) -> None: """Остановка робота""" self._left_motor.stop_motor() self._right_motor.stop_motor() def close(self) -> None: """Освобождение ресурса""" self._left_motor.close() self._right_motor.close()
Разбор кода:
self._left_motor и self._right_motor — атрибуты класса с экземплярами классов для двух моторов, в которые передаются настройки для GIPIO;
self.stop() в конструкторе — гарантирует, что все линии моторов будут выключены и при включении робота двигатели не начнут вращаться из-за случайно активированных линий.
Теперь управлять двумя моторами стало гораздо удобнее. В дальнейшем я планирую сделать класс более универсальным, чтобы можно было работать как с двухмоторными, так и с четырёхмоторными платформами.
Я создал образец .env-файла, по которому любой сможет сформировать свой конфигурационный файл с настройками.
Содержимое .env.example:
# Настройки для websocket WEBSOCKET_HOST=хост WEBSOCKET_PORT=порт # Команды FORWARD=вперёд BACKWARD=назад LEFT=влево RIGHT=вправо STOP=стоп # Настройки для линий GPIO GPIO_PATH=путь до gpio LED_LINE=номер линии LED_CONSUMER=идентификатор клиента LEFT_MOTOR_LINE_IN1=номер линии для левого (вращение вперёд) LEFT_MOTOR_LINE_IN2=номер линии для левого (вращение назад) LEFT_MOTOR_CONSUMER=идентификатор клиента для левого RIGHT_MOTOR_LINE_IN1=номер линии для правого (вращение вперёд) RIGHT_MOTOR_LINE_IN2=номер линии для правого (вращение назад) RIGHT_MOTOR_CONSUMER=идентификатор клиента для правого
Каждая переменная снабжена описанием. В этом примере появились новые параметры для управления линиями моторов, а также переменная для команды.
Теперь я обновляю settings.py для использования новых переменных окружения.
Обновлённый код класса CommandsRobot:
class CommandsRobot(ModelConfig): """Класс с командами для робота""" # Другой код stop: str def is_command(self, command: str) -> bool: """Есть ли команда в командах""" return command in self.model_dump().values()
Разбор нового кода:
stop: str — атрибут класс для хранения команды stop;
is_command() — метод, проверяющий наличие команды в списке доступных:
— self.model_dump().values() — собирает атрибуты класса в словарь и возвращает их значения;
— return command in self.model_dump().values() — вернёт True, если команда найдена, иначе False.
Далее пишу класс GpioLines, наследуемый от ModelConfig, который будет хранить переменные окружения, относящиеся к линиям GPIO.
Класс GpioLines:
class GpioLines(ModelConfig): """Класс для линий GPIO""" gpio_path: str led_line: int led_consumer: str left_motor_line_in1: int left_motor_line_in2: int left_motor_consumer: str right_motor_line_in1: int right_motor_line_in2: int right_motor_consumer: str
Чтобы удобно получать доступ к этим настройкам, создаю экземпляр GpioLines внутри класса Settings:
Обновлённый код класса Settings:
class Settings(ModelConfig): """Класс для данных конфига""" # Другой код gpio_lines: GpioLines = GpioLines()
Прежде чем реализовывать новую логику в сервисе робота, я написал класс FormResponse в файле response_data.py для формирования ответов робота веб-приложению. Идея: брать команду из запроса фронтенда и возвращать структуру ответа со статусом, сообщением и кодом ошибки (если возникнет).
Стоит помнить, что WebSocket — это не HTTP. У него (см. RFC 6455) есть собственные transport-level коды: например, 1006 означает потерю соединения.
Для обмена данными между фронтендом и бэкендом этого достаточно. Но для связи сервиса робота и бэкенда может потребоваться более детализированная обратная связь.
Когда transport-level кодов недостаточно?
Команда не найдена (аналог 404);
Команда не выполнена (например, из-за ошибки);
Команда успешно выполнена (аналог 200);
Нужен дебаг взаимодействия между сервисом робота и бэкендом.
В таких случаях удобно использовать application-level коды (200/404/500) и структуру ответа, похожую на REST. Transport-level коды описывают уровень протокола (WebSocket), а application-level — уровень приложения.
Для этого я создаю класс FormResponse, который наследуется от Enum. В Python Enum используется для описания фиксированных наборов констант. В моём случае он хранит кортежи со статус-кодами и сообщениями, что делает ответы читаемыми и структурированными.
Импорт:
from enum import Enum
Класс FromResponse:
class FormResponse(Enum): """Класс для формирования ответа""" NOT_FOUND_COMMAND = (404, 'Unknown command!') OK_COMMAND = (200, 'Command executed') SERVER_ERR = (500, 'Internal Server Error!') @property def response(self) -> dict[str, int | str]: """Словарь для формирования ответа серверу""" return { 'status': self.value[0], 'message': self.value[1] } def get_response_err(self, name_err: str, message_err: str) -> dict[str, int | str]: """Метод для формирования ответа серверу с ошибкой""" return { 'status': self.value[0], 'name_error': name_err, 'message': message_err }
Разбор кода:
FormResponse — перечисление (
Enum) для стандартных ответов сервера;NOT_FOUND_COMMAND — статус 404, когда команда неизвестна;
OK_COMMAND — статус 200, команда успешно выполнена;
SERVER_ERR — статус 500, внутренняя ошибка сервера;
response — свойство, возвращающее словарь с ключами
statusиmessageдля базового ответа;get_response_err — метод для формирования словаря с ошибкой, включающего
status,name_errorиmessage.
Теперь всю новую логику я собираю в сервисе робота, которая находится в файле robot_pi_service.py.
Новые импорты:
from socket import gethostbyname from json import loads, dumps, JSONDecodeError from gpio_control import RobotControl from response_data import FormResponse
Разбор новых импортов:
gethostbyname из socket — возвращает IP по имени хоста (
mydevice.local);dumps из json — функция для сериализации Python-объектов в JSON-строку;
RobotControl из gpio_control — класс для управления роботом через GPIO;
FormResponse из response_data — Enum с методами для формирования стандартных ответов сервера.
Обновлённый код функции start()
async def start() -> None: """Асинхронная функция запуска вебсокета и бесконечного цикла""" # Другой код async with serve( handler=robot_control_gpio, host=gethostbyname(settings.websocket_host), port=settings.websocket_port ): # Другой код
Разбор обновлённого кода:
gethostbyname(settings.websocket_host) — получает IP по имени хоста. Если передан обычный IP, возвращается он же.
Теперь WebSocket-сервис может использовать имя хоста вроде mydevice.local, которое я задал одноплатнику через hostnamectl.
Следующий шаг — подключение новой логики управления моторами в функции robot_control_gpio().
Обновлённая функция robot_control_gpio():
async def robot_control_gpio(websocket: WebSocketServerProtocol) -> None: """ Асинхронная функция для управлением gpio робота (через websocket) """ try: robot_control = RobotControl() commands_status = FormResponse while True: try: command = await asyncio.wait_for(websocket.recv(), timeout=30.0) data = loads(command) command_name = data.get('command') except asyncio.TimeoutError as err: print('Таймаут ожидания команды от клиента') continue # продолжение цикла, чтобы не закрывать соединение except JSONDecodeError: print('Ошибка при декодировании JSON!') continue match command_name: case settings.commands_robot.forward: robot_control.forward() case settings.commands_robot.backward: robot_control.backward() case settings.commands_robot.left: robot_control.left() case settings.commands_robot.right: robot_control.right() case settings.commands_robot.stop: robot_control.stop() case _: data.update(commands_status.NOT_FOUND_COMMAND.response) await websocket.send(message=dumps(data)) if settings.commands_robot.is_command(command=command_name): data.update(commands_status.OK_COMMAND.response) await websocket.send(message=dumps(data)) except exceptions.ConnectionClosed: pass except (exceptions.ConnectionClosedOK, exceptions.InvalidMessage, exceptions.InvalidState) as err: message_err = f'{err.__class__.__name__}: {err}' print(message_err) data.update( commands_status.SERVER_ERR.get_response_err( name_err=err.__class__.__name__, message_err=err ) ) await websocket.send(message=dumps(data)) finally: robot_control.close()
Разбор обновлённого кода:
robot_control = RobotControl() — создаёт объект для управления роботом через GPIO;
commands_status = FormResponse — подключает Enum с шаблонами стандартных ответов сервера;
command_name = data.get('command') — получает название команды из JSON, пришедшего от клиента;
match command_name: — выбирает действие робота в зависимости от команды;
robot_control.forward() и другие команды — выполняют движение робота: вперёд, назад, влево, вправо;
robot_control.stop() — останавливает движение робота;
case _: — обработка неизвестной команды:
— data.update(commands_status.NOT_FOUND_COMMAND.response) — добавляет в ответ статус 404 и сообщение об ошибке;
— await websocket.send(message=dumps(data)) — отправляет ответ клиенту через WebSocket.if settings.commands_robot.is_command(command=command_name): — проверяет, является ли команда допустимой.
— data.update(commands_status.OK_COMMAND.response) — добавляет в ответ статус 200 и сообщение об успешном выполнении.data.update в блоке с ошибками — добавляет информацию о серверной ошибке.
— commands_status.SERVER_ERR.get_response_err — формирует структуру ответа с названием и описанием ошибки.robot_control.close() — завершает работу с GPIO, освобождая ресурсы.
Код для обработки команд стал более лаконичным и удобным. Теперь команды приходят в формате: {"command": "forward"} вместо прежнего вида:{"forward": true}. Остановка моторов выполняется через команду "stop". Логика программы избавлена от громоздких конструкций if...else, что делает код более чистым и понятным.
Ссылка на итоговый open-source проект robot-pi-service.
Код веб-приложения
Настало время обновить код веб-приложения, чтобы оно стало совместимо с новой логикой робота.
Обновлённый код для отправки команд из command.js:
// Другой код const commandStop = {"command": "stop"}; // Назначение обработчиков событий для кнопки "Вперёд" const forwardButton = document.getElementById("forward-button"); forwardButton.addEventListener("mousedown", () => startSendingCommand({"command": "forward"})); // Начало отправки команды forwardButton.addEventListener("mouseup", () => { sendCommand(JSON.stringify(commandStop)); stopSendingCommand(); }); // отправка команды forwardOff и остановка отправки при отпускании кнопки forwardButton.addEventListener("mouseleave", stopSendingCommand); // Остановка отправки, если курсор уходит с кнопки // Другой код
Разбор обновлённого кода:
const commandStop = {"command": "stop"}; — константа для команды
stop;startSendingCommand({"command": "forward"}) — фронтенд теперь отправляет команду в формате, который ожидает сервис робота (для остальных команд внесены аналогичные изменения);
sendCommand(JSON.stringify(commandStop)) — при отпускании кнопки отправляется команда
stop(аналогично для других команд).
Теперь, когда структура команд обновлена, можно переходить к изменениям на бэкенде веб-приложения.
Добавляю недостающие переменные в .env.example:
# Урлы STREAM_URL=адрес видеопотока WEBSOCKET_URL=адрес вебсокета клиента WEBSOCKET_URL_ROBOT=адрес вебсокета робота # Команды для робота FORWARD=вперёд BACKWARD=назад LEFT=влево RIGHT=вправо STOP=стоп
Теперь добавляю новую команду stop в класс CommandsRobot, который находится в файле settings.py.
Обновлённый класс CommandsRobot:
stop: str
Теперь все команды робота, включая остановку, имеют единый формат и могут использоваться фронтендом и бэкендом согласованно.
Завершающим шрихом изменяю код в эндпоинте для обработки команд websocket_endpoint() из файла views.py, чтобы он поддерживал новую структуру команд.
Обновлённый код эндпоинта websocket_endpoint():
@router.websocket('/ws') async def websocket_endpoint(websocket: WebSocket) -> None: """Эндпоинт для обработки команд фронтенда""" # Установка содединения по веб-сокету await websocket.accept() try: previous_command = None while True: # Получение команды от клиента (с веб-сокета) response = await websocket.receive_text() data = loads(response) command = data.get('command') if previous_command != command: previous_command = command valid_commands = settings.commands_robot.get_list_commands() if command in valid_commands: # оптравка команды роботу robot_answer = await command_to_robot(command=response) if robot_answer: # отправка ответа робота на вебсокет фронтенда await websocket.send_text(f'Получена команда: {command}, ответ робота: {robot_answer}') except WebSocketDisconnect: print('WebSocket отключен') except (WebSocketException, exceptions.InvalidMessage) as err: print(f'{err.__class__.__name__}: {err}') except JSONDecodeError: print('Ошибка при декодировании JSON')
Разбор обновлённого кода:
command = data.get('command') — получение команды теперь осуществляется по ключу
command. Нет необходимости использовать старую конструкциюname_command = next(iter(data), None);if command in valid_commands: — проверка команды выполняется через новую переменную
commandвместо старойname_command.
Теперь логика обработки команд стала короче, чище и полностью соответствует обновлённой структуре приложения.
Ссылка на итоговый open-source проект web-robot-control.
Управление моторами через веб-приложение
Перехожу к самой интересной части — проверке работы приложения вручную. Мне было важно оценить, насколько отзывчиво ведут себя моторы, а также как камера передаёт изображение при управлении. Главное — чтобы не было сильных рывков и зависаний видео при движении.
Запуск сервиса робота:
make run
Запуск веб-приложения:
poetry run start_app
Я записал короткое видео демонстрации работы управления двумя моторами через веб-приложение.
Как видно на видео, моторы реагируют быстро и точно на все команды, а камера передаёт изображение с достаточной плавностью для управления в реальном времени. Из-за небольшого расстояния приходится быстро отпускать кнопку, что иногда вызывает лёгкие рывки.
Заключение и планы на будущее
Основная часть функционала по управлению гусеничным шасси завершена. Удалось подключить к одноплатнику два мотора через драйвер L298N, а также управлять ими через веб-приложение в режиме реального времени.
В следующей статье планируется установить всю электронику на гусеничное шасси и провести тестирование на полосе препятствий, чтобы продемонстрировать работу платформы в действии.
Кроме того, на данный момент класс LedLineGpio остался неиспользованным. В предыдущей статье он применялся для управления LED-линией. В дальнейшем планируется добавить светодиод в качестве индикатора: он будет сигнализировать о готовности робота к работе и о факте соединения по WebSocket. При этом в веб-приложении появится возможность управлять индикатором.
Если у вас есть идеи по развитию проекта, поделитесь ими в комментариях — буду рад услышать ваши предложения!
Автор статьи @Arduinum
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
