Привет, Хабр!

Меня зовут Степан Бурмистров, я преподаю робототехнику уже более 9 лет и за это время видел, как менялись технологии, подходы и инструменты для обучения. Одним из самых мощных инструментов для разработки интеллектуальных роботов является ROS (Robot Operating System), но долгое время он считался фреймворком исключительно для профессионалов. Пришло время это изменить!

Как зародилась идея курса

Примерно год назад на ROS MeetUp (https://habr.com/ru/articles/809613/) мы с моим учеником Ждановым Степаном (@ret77876) представили систему удаленного управления манипулятором на ROS2. Параллельно была опубликована статья, в которой буквально за несколько шагов можно было установить и запустить ROS2. Доклад вызвал оживленную дискуссию, из которой родилось стратегическое решение — создать доступный курс по ROS2 для школьников.

Совместно с Алексеем Бурковым(@AmigoRRR) мы разработали программу курса и начали активную работу над его созданием.

Курс БЕСПЛАТНЫЙ - открытый для всех: https://stepik.org/course/221157
Ведь наша цель - сделать Российское образование в области робототехники - лучшим!

В настоящее время некоторые разделы курса еще в процессе создания, однако многое сделано уже сейчас:

  • Подготовлен вводный раздел, который снимает страх перед «неизвестным Linux». В нем разбираются установка на виртуальную машину, работа с WSL, установка на компьютер, описание способой настройки сети и базовые команды Linux.

  • Созданы модули по основным способам коммуникации в ROS2, включая примеры запуска демо и реализацию собственных решений.

  • Разработан "базовый" ROS2-робот, который станет основой для освоения всех ключевых технологий и концепций.

Сегодня я хочу поделиться с вами подробностями о курсе и рассказать, как ROS2 может стать доступным даже для начинающих инженеров! ?

Что такое этот ваш ROS?

ROS (Robot Operating System) — это не совсем операционная система, как может показаться из названия, а мощная программная среда для разработки робототехнических систем. Она включает в себя набор инструментов, библиотек и фреймворков, которые помогают инженерам и разработчикам создавать сложные, многомодульные роботы.

С момента появления ROS в 2007 году, он стал стандартом в мире робототехники. Однако долгое время он оставался инструментом для университетских лабораторий и крупных промышленных проектов. Основная проблема — сложность освоения, особенно для новичков.

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

Основы ROS2 Jazzy

Разделы курса, которые описывают, как установить ROS2 и настроить все его основные способы связи

  • Установка и настройка ROS2

  • Топик, паблишер, подписчик. Первые ноды

  • Создание создание собственной ноды управления turtle

  • А также сервис, экшн, создание лаунч файлов для запуска больших проектов

ROS2 робот

Далее, подробно расскажу о роботе, который был разработан для это курса.

Репозиторий робота: https://github.com/stepanburmistrov/ROS2_robotV1

Встречайте:

Задачи на разработку робота были следующие:

  • Простота изготовления: корпус робота полностью выполнен из фанеры, что позволяет вырезать его на лазерном станке за несколько (ну ладно, десятков) минут

  • Возможность изменения: платформы робота содержат множество отверстий, к которым без труда крепятся любые электронные компоненты, датчики и другое "навесное" оборудование

  • Относительно невысокая цена: компоненты робота выбирались по принципу надежности, функциональности, но при этом не слишком высокими по цене.

  • Возможность интеграции различных способов связи и протоколов между микроконтроллером и ROS2. Как использование MicroRos, так и собственные протоколы обмена с одноплатным компьютером

3D-модель

Все 3D-модели деталей робота, а также файлы DXF для резки на лазерном станке доступны в репозитории робота: https://github.com/stepanburmistrov/ROS2_robotV1/tree/main/3D\

Схема и выбор компонентов

При разработке учебного робота для курса ROS2 мы уделили особое внимание выбору компонентов. Основные критерии — доступность, надежность и простота интеграции с ROS2. На изображении представлена полная электрическая схема робота.

Основные компоненты робота:

  1. Микроконтроллер ESP32 Wroom 32

    ESP32 WROOM32
    ESP32 WROOM32
    • правляет моторами, энкодерами, сервоприводами и светодиодной лентой.

    • Связь с Raspberry Pi осуществляется через USB/UART.

  2. Моторы с редуктором и энкодерами

    JGB37. 200RPM. 6V
    JGB37. 200RPM. 6V
    • Два колеса с независимым управлением.

    • Энкодеры позволяют получать обратную связь о скорости и положении.

  3. Драйвер моторов (L298N / ZK-5AD)

    • Управляет скоростью и направлением вращения колес.

    • Использует ШИМ-сигналы от ESP32.

  4. IMU (ICM-20948)

    • Датчик инерциальных измерений для ориентации робота в пространстве.

    • Подключение: I2C.

  5. Cервоприводы с обратной связью FeeTech STS3215()

    • Применяются для звеньев манипулятора и захвата.

    • Управляются через последовательный интерфейс.

  6. Аккумуляторный блок 8.4V и регулятор напряжения XL4016

    • Питает всю систему через стабилизаторы напряжения (5V).

  7. Одноплатный компьютер (Raspberry Pi 4 / Orange Pi 5)

    • Используется для запуска ROS2, обработки данных с камеры и лидара, а также для взаимодействия с микроконтроллером.

    • Подключение: USB (камера, лидар, ESP32).

  8. Лидар (RPLIDAR C1 / YDLIDAR)

    • Позволяет строить карту окружающего пространства.

    • Подключение: USB или UART.

  9. Дополнительные сенсоры

    • Датчики расстояния, датчики линии и другие модули, расширяющие функциональность робота.

Работа с компонентами

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

Также весь необходимый код есть в репозитории:
https://github.com/stepanburmistrov/ROS2_robotV1

Приведу пример:

Правильная работа с моторами и энкодерами позволяет двигаться роботу ровно, а также производить расчет текущего местоположения.

Для это применяется алгоритм PID-регулятора, достаточно известный, чтобы описывать тут его логику. Однако подбор коэффициентов этого алгоритма может превратиться в длительное развлечение для того, кто будет настраивать робота.

Для удобного подбора коэффициентов предлагается следующий алгоритм:

  • Реализуем протокол обмена данными по UART c парсингом значений:

............

void processCommand(String command) {
  command.trim();
  if (command.startsWith("SET_COEFF")) {
    float newKp, newKi, newKd, newKff;
    if (parseSetCoeff(command, &newKp, &newKi, &newKd, &newKff)) {
      pidKp = newKp;
      pidKi = newKi;
      pidKd = newKd;
      pidKff = newKff;
      Serial.println("OK: Coefficients updated");
    } else {
      Serial.println("ERROR: Invalid coefficients");
    }
  } else {
    Serial.println("ERROR: Unknown command");
  }
}

...........

bool parseSetCoeff(const String& command, float* Kp, float* Ki, float* Kd, float* Kff) {
  int index1 = command.indexOf(' ');
  if(index1 == -1) return false;
  int index2 = command.indexOf(' ', index1 + 1);
  if(index2 == -1) return false;
  int index3 = command.indexOf(' ', index2 + 1);
  if(index3 == -1) return false;
  int index4 = command.indexOf(' ', index3 + 1);
  if(index4 == -1) return false;

  *Kp = command.substring(index1 + 1, index2).toFloat();
  *Ki = command.substring(index2 + 1, index3).toFloat();
  *Kd = command.substring(index3 + 1, index4).toFloat();
  *Kff = command.substring(index4 + 1).toFloat();
  return true;
}

...........
  • Пишем скрипт на Python, который позволит визуально оценивать текущую и целевую скорость вращения колес и настраивать коэффициенты "на лету", сразу же наблюдая результат.

import tkinter as tk
from tkinter import ttk
import serial
import threading
import time
import re
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Класс для чтения данных с serial-порта в отдельном потоке
class SerialReader(threading.Thread):
    def __init__(self, serial_port, baudrate, callback):
        super().__init__()
        self.serial_port = serial_port
        self.baudrate = baudrate
        self.callback = callback
        self._stop_event = threading.Event()
        try:
            self.ser = serial.Serial(serial_port, baudrate, timeout=1)
        except Exception as e:
            print("Error opening serial port:", e)
            self.ser = None

    def run(self):
        if not self.ser:
            return
        while not self._stop_event.is_set():
            try:
                line = self.ser.readline().decode("utf-8", errors="ignore").strip()
                if line:
                    self.callback(line)
            except Exception as e:
                print("Error reading from serial:", e)
            time.sleep(0.01)

    def stop(self):
        self._stop_event.set()
        if self.ser:
            self.ser.close()

# Главное приложение на Tkinter
class App:
    def __init__(self, root):
        self.root = root
        self.root.title("PID Tuner and Speed Monitor")
        
        # Настройки serial (измените при необходимости)
        self.serial_port = "COM5"  # или "/dev/ttyUSB0"
        self.baudrate = 115200
        self.serial_thread = None
        
        self.create_widgets()
        
        # Настройка Matplotlib для графика
        self.figure = Figure(figsize=(12,4), dpi=100)
        self.ax = self.figure.add_subplot(111)
        self.ax.set_title("Wheel Speed Response")
        self.ax.set_xlabel("Time (s)")
        self.ax.set_ylabel("Speed (mm/s)")
        
        self.canvas = FigureCanvasTkAgg(self.figure, master=self.root)
        self.canvas.get_tk_widget().grid(row=7, column=0, columnspan=4, pady=10)
        
        # Списки для накопления данных графика
        self.time_data = []
        self.measured_left_data = []
        self.target_left_data = []
        self.measured_right_data = []
        self.target_right_data = []
        self.start_time = time.time()
        
        self.start_serial()
        self.update_plot()
    
    def create_widgets(self):
        # Ползунки для задания коэффициентов PID
        
        # Kp
        tk.Label(self.root, text="Kp:").grid(row=0, column=0, sticky="e")
        self.kp_scale = tk.Scale(self.root, from_=0, to=10, resolution=0.1,
                                 orient=tk.HORIZONTAL, length=200,
                                 command=self.update_coefficients_from_sliders)
        self.kp_scale.set(2.0)
        self.kp_scale.grid(row=0, column=1)
        
        # Ki
        tk.Label(self.root, text="Ki:").grid(row=0, column=2, sticky="e")
        self.ki_scale = tk.Scale(self.root, from_=0, to=10, resolution=0.1,
                                 orient=tk.HORIZONTAL, length=200,
                                 command=self.update_coefficients_from_sliders)
        self.ki_scale.set(2.5)
        self.ki_scale.grid(row=0, column=3)
        
        # Kd
        tk.Label(self.root, text="Kd:").grid(row=1, column=0, sticky="e")
        self.kd_scale = tk.Scale(self.root, from_=0, to=10, resolution=0.01,
                                 orient=tk.HORIZONTAL, length=200,
                                 command=self.update_coefficients_from_sliders)
        self.kd_scale.set(0.0)
        self.kd_scale.grid(row=1, column=1)
        
        # Kff
        tk.Label(self.root, text="Kff:").grid(row=1, column=2, sticky="e")
        self.kff_scale = tk.Scale(self.root, from_=0, to=1, resolution=0.05,
                                  orient=tk.HORIZONTAL, length=200,
                                  command=self.update_coefficients_from_sliders)
        self.kff_scale.set(0.3)
        self.kff_scale.grid(row=1, column=3)
        
        # Ползунки для задания целевых скоростей колес
        tk.Label(self.root, text="Target Speed Left (mm/s):").grid(row=2, column=0, columnspan=2, sticky="e")
        self.target_left_scale = tk.Scale(self.root, from_=-500, to=500, resolution=1,
                                          orient=tk.HORIZONTAL, length=200,
                                          command=self.update_speed_from_sliders)
        self.target_left_scale.set(0)
        self.target_left_scale.grid(row=2, column=2)
        
        tk.Label(self.root, text="Target Speed Right (mm/s):").grid(row=3, column=0, columnspan=2, sticky="e")
        self.target_right_scale = tk.Scale(self.root, from_=-500, to=500, resolution=1,
                                           orient=tk.HORIZONTAL, length=200,
                                           command=self.update_speed_from_sliders)
        self.target_right_scale.set(0)
        self.target_right_scale.grid(row=3, column=2)
        
        # Текстовое поле для вывода статуса и отладочной информации
        self.status_text = tk.Text(self.root, height=5, width=60)
        self.status_text.grid(row=5, column=0, columnspan=4, pady=5)
    
    def start_serial(self):
        self.serial_thread = SerialReader(self.serial_port, self.baudrate, self.handle_serial_line)
        self.serial_thread.start()
    
    def handle_serial_line(self, line):
        # Вывод полученной строки в текстовом поле
        self.status_text.insert(tk.END, line + "\n")
        self.status_text.see(tk.END)
        # Пробуем распарсить строку с данными скорости.
        # Ожидаемый формат:
        # "POS X=... Y=... Th=... ENC L=... R=... SPD L=123.45 mm/s R=67.89 mm/s Target L=100.00 mm/s R=100.00 mm/s"
        pattern = r"SPD L=([\d\.\-]+) mm/s R=([\d\.\-]+) mm/s.*Target L=([\d\.\-]+) mm/s R=([\d\.\-]+) mm/s"
        match = re.search(pattern, line)
        if match:
            try:
                measured_left = float(match.group(1))
                measured_right = float(match.group(2))
                target_left = float(match.group(3))
                target_right = float(match.group(4))
                current_time = time.time() - self.start_time
                self.time_data.append(current_time)
                self.measured_left_data.append(measured_left)
                self.target_left_data.append(target_left)
                self.measured_right_data.append(measured_right)
                self.target_right_data.append(target_right)
            except Exception as e:
                print("Error parsing speed data:", e)
    
    def update_coefficients_from_sliders(self, value):
        # Формирование команды вида "SET_COEFF Kp Ki Kd Kff" и отправка по serial
        cmd = "SET_COEFF {} {} {} {}\n".format(
            self.kp_scale.get(),
            self.ki_scale.get(),
            self.kd_scale.get(),
            self.kff_scale.get()
        )
        if self.serial_thread and self.serial_thread.ser:
            try:
                self.serial_thread.ser.write(cmd.encode("utf-8"))
                self.status_text.insert(tk.END, "Sent: " + cmd)
                self.status_text.see(tk.END)
            except Exception as e:
                print("Error sending coefficients:", e)
    
    def update_speed_from_sliders(self, value):
        # Формирование команды вида "SET_WHEELS_SPEED leftSpeed rightSpeed" и отправка по serial
        cmd = "SET_WHEELS_SPEED {} {}\n".format(
            self.target_left_scale.get(),
            self.target_right_scale.get()
        )
        if self.serial_thread and self.serial_thread.ser:
            try:
                self.serial_thread.ser.write(cmd.encode("utf-8"))
                self.status_text.insert(tk.END, "Sent: " + cmd)
                self.status_text.see(tk.END)
            except Exception as e:
                print("Error sending wheel speed:", e)
    
    def update_plot(self):
        # Обновление графика с накопленными данными
        self.ax.clear()
        self.ax.set_title("Wheel Speed Response")
        self.ax.set_xlabel("Time (s)")
        self.ax.set_ylabel("Speed (mm/s)")
        self.ax.plot(self.time_data, self.measured_left_data, label="Measured Left")
        self.ax.plot(self.time_data, self.target_left_data, label="Target Left", linestyle="--")
        self.ax.plot(self.time_data, self.measured_right_data, label="Measured Right")
        self.ax.plot(self.time_data, self.target_right_data, label="Target Right", linestyle="--")
        self.ax.legend()
        
        # Определяем окно по 20 секунд
        if self.time_data:
            current_time = self.time_data[-1]
            if current_time < 20:
                self.ax.set_xlim(0, 20)
            else:
                self.ax.set_xlim(current_time - 20, current_time)
        
        self.canvas.draw()
        self.root.after(1000, self.update_plot)
    
    def on_closing(self):
        if self.serial_thread:
            self.serial_thread.stop()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()
  • Получаем удобный инструмент для настройки параметров

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

Этот учебный робот станет отличной платформой для освоения ROS2 и создания собственных проектов в области образовательной и исследовательской робототехники! ?

В настоящее время еще ведется работа по написанию этого курса, уже многое сделано, но еще больше впереди!

Да, и самое главное:

Хакатон по ROS2-роботам

Приглашаем учителей, студентов и школьников, готовых собрать собственного робота на хакатон "Cборка и программирование ROS2-робота" который пройдет в первой части ROS MeetUp 4-6 апреля 2025. Я буду одним из ведущих этого хакатона, и помогу начинающим и со сборкой, и с написанием кода для первого запуска.

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

Хакатон мы разделим на 2 трека:

  • Начинающие - участники, которые собирают своего первого робота. Вам очень важно пройти все опубликованные разделы курса, закупить компоненты и освоить тот максимум, на который вы готовы

  • Продвинутые - участники, у которых уже есть робот, работающий на ROS2, и вы готовы за время хакатона оформить документацию, сформировать репозитории с кодом, схемами, чертежами и инструкциями

https://ros-event.timepad.ru/event/3197948/

Данный курс является отправной точкой, с которой стартует ваша разработка, которую вы дооформите за время хакатона.

Комплектующие вы покупаете сами и приносите с собой, на месте можно паять, печатать на 3d принтере, резать на лазерном резаке, собирать робота.

Возможно некоторые комплектующие будут предоставлены, об этом читайте в ROS чате.

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

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

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

Регламент хакатона можно прочесть в документе.

Хакатон проводиться по инициативе МинЦифры, результаты будут использованы в программе «Кода будущего» по робототехнике.

До встречи на хакатоне!

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


  1. DashBerlin
    12.02.2025 18:26

    А у малинки, сколько достаточно памяти?


    1. Stepan_Burmistrov Автор
      12.02.2025 18:26

      Смотря что там запускать. ROS и какие-то базовые ноды для обмена данными я на 1-2 Гб запускал.

      А так-то, оперативки много не бывает:)


    1. YourgenAP
      12.02.2025 18:26

      Из личного опыта, 4гб хорошо себя чувствует на любых задачах даже с лидарами на 10-20 герц с облаком 1024 на 512 точек и вычислениями в реальном времени. 2гб можно взять для стандартных методов навигации и абсолютно не беспокоиться


  1. OldFashionedEngineer
    12.02.2025 18:26

    Хороший рекламный проспект получился