Создание собственного сценария без использования robohead_controller
В этом разделе мы создадим изменённую версию команды «Улыбнись», реализованную без использования оболочки robohead_controller. Такой подход позволяет более гибко управлять устройством, вызывая сервисы напрямую из своего скрипта.
Описание задачи
Наша цель — написать скрипт, который выполнит следующие действия:
- Выведет изображение
smile.pngна дисплей Робоголовы. - Проиграет аудио-файл
smile.mp3через динамики. - На протяжении 3 секунд покачает ушами вперёд-назад с частотой 2 Гц.
- С помощью шейного сустава поднимет и повернёт голову в сторону.
- После завершения всех действий вернёт голову и уши в исходное положение.
Такой сценарий позволит познакомиться с низкоуровневым взаимодействием с драйверами Робоголовы, минуя обёртку robohead_controller.
Шаг 1. Подготовка
- Создайте в домашней папке (
/home/pi) папкуscriptsперейдите в неё
cd ~/
mkdir scripts
-
Скопируйте в папку два медиа-файла:
-
В этой же папке создайте файл скрипта
main.py:
#!/usr/bin/env python3
"""
smile_node.py — демонстрация управления медиа и сервоприводами Робоголовы
ROS2 Jazzy | Python 3.10+
"""
from robohead_interfaces.srv import PlayMedia, SimpleCommand, Move
from rclpy.node import Node
from rclpy.time import Duration
import rclpy
import os
import time
import sys
class SmileNode(Node):
def __init__(self):
super().__init__("smile_node")
# Пути к файлам
self.script_path = os.path.dirname(os.path.abspath(__file__))
# Клиенты сервисов
self.srv_play_media = self.create_client(PlayMedia, "/robohead/media_driver/play_media")
self.srv_set_volume = self.create_client(SimpleCommand, "/robohead/media_driver/set_volume")
self.srv_neck_set_angle = self.create_client(Move, "/robohead/neck_driver/neck_set_angle")
self.srv_ears_set_angle = self.create_client(Move, "/robohead/ears_driver/ears_set_angle")
self.srv_ears_is_idle = self.create_client(SimpleCommand, "/robohead/ears_driver/is_idle")
# Ожидание доступности всех сервисов
services = [
("play_media", self.srv_play_media),
("set_volume", self.srv_set_volume),
("neck_set_angle", self.srv_neck_set_angle),
("ears_set_angle", self.srv_ears_set_angle),
("ears_driver/is_idle", self.srv_ears_is_idle),
]
for name, srv in services:
if not srv.wait_for_service(timeout_sec=10.0):
self.get_logger().error(f"Service '{name}' not available")
sys.exit(1)
self.get_logger().info("All services available")
def call_service_sync(self, client, request, timeout_sec=5.0):
"""Вспомогательный метод для синхронного вызова сервиса"""
future = client.call_async(request)
start = self.get_clock().now()
while rclpy.ok() and not future.done():
if (self.get_clock().now() - start) > Duration(seconds=timeout_sec):
self.get_logger().error("Service call timeout")
return None
rclpy.spin_once(self, timeout_sec=0.1)
return future.result()
def run(self):
# 1. Установка громкости
self.get_logger().info("Setting volume...")
req_vol = SimpleCommand.Request()
req_vol.data = 60
self.call_service_sync(self.srv_set_volume, req_vol)
# 2. Воспроизведение медиа
self.get_logger().info("Playing media...")
req_media = PlayMedia.Request()
req_media.path_to_video_file = os.path.join(self.script_path, 'smile.png')
req_media.path_to_audio_file = os.path.join(self.script_path, 'smile.mp3')
req_media.loop = False
self.call_service_sync(self.srv_play_media, req_media)
# 3. Поворот головы (шейный сустав)
self.get_logger().info("Moving neck...")
req_neck = Move.Request()
req_neck.angle_a = 30 # наклон вперёд/вверх
req_neck.angle_b = -30 # поворот в сторону
req_neck.duration = 1.0
self.call_service_sync(self.srv_neck_set_angle, req_neck)
# 4. Качание ушами в течение 3 секунд (2 Гц = туда-обратно за 0.5 с)
self.get_logger().info("Wiggling ears for 3 seconds...")
direction = 1
start_time = self.get_clock().now()
cycle_duration = 0.5 # 2 Гц: полный цикл туда-обратно
while (self.get_clock().now() - start_time) < Duration(seconds=3.0):
req_ears = Move.Request()
req_ears.angle_a = 90 * direction
req_ears.angle_b = -90 * direction
req_ears.duration = 0.5 # половина цикла
self.call_service_sync(self.srv_ears_set_angle, req_ears)
while self.call_service_sync(self.srv_ears_is_idle, SimpleCommand.Request()).data != True:
time.sleep(0.01) # 100 Гц
# Обрабатываем колбэки ROS2
rclpy.spin_once(self, timeout_sec=0.01)
# Переключаем направление и ждём половину периода
direction *= -1
self.get_logger().info("Sequence completed!")
# Возвращаемся в стартовую позицию
req_media = PlayMedia.Request()
req_media.path_to_video_file = "__STOP__"
req_media.path_to_audio_file = "__STOP__"
req_media.loop = False
self.call_service_sync(self.srv_play_media, req_media)
req = Move.Request()
req.angle_a = 0
req.angle_b = 0
req.duration = 1.0
self.call_service_sync(self.srv_neck_set_angle, req)
self.call_service_sync(self.srv_ears_set_angle, req)
def main():
rclpy.init()
node = SmileNode()
try:
node.run()
except KeyboardInterrupt:
node.get_logger().info("Interrupted by user")
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()
if __name__ == "__main__":
main()
Всё что ниже обновить (Шаг 2 и Шаг 3) в соответсвии с кодом выше
Шаг 2. Структура и содержание скрипта main.py
В файле main.py мы будем обращаться напрямую к сервисам пакетов Робоголовы: дисплею, динамикам, ушам и шее. Ниже приводём подробный разбор кода.
import os
import rospy
# Импортируем типы запросов и ответов для сервисов дисплея
from display_driver.srv import PlayMedia, PlayMediaRequest
# Импортируем типы запросов и ответов для сервиса управления ушами
from ears_driver.srv import EarsSetAngle, EarsSetAngleRequest
# Импортируем типы запросов и ответов для сервиса управления шеей
from neck_driver.srv import NeckSetAngle, NeckSetAngleRequest
# Импортируем типы запросов и ответов для управления динамиками
from speakers_driver.srv import PlayAudio, PlayAudioRequest, SetVolume, SetVolumeRequest
# Инициализируем ROS-ноду с названием "smile_node"
rospy.init_node("smile_node")
script_path = os.path.dirname(os.path.abspath(__file__)) + '/'
print('Инициализация узла завершена')
- Инициализация ROS-ноды
Мы вызываемrospy.init_node("smile_node"), чтобы зарегистрировать новый узел в ROS. Переменнаяscript_pathсодержит полный путь к папке со скриптом, чтобы затем корректно формировать пути к медиа-файлам.
# Ожидаем, пока все нужные сервисы будут доступны
rospy.wait_for_service('/robohead_controller/display_driver/PlayMedia')
rospy.wait_for_service('/robohead_controller/neck_driver/NeckSetAngle')
rospy.wait_for_service('/robohead_controller/ears_driver/EarsSetAngle')
rospy.wait_for_service('/robohead_controller/speakers_driver/PlayAudio')
rospy.wait_for_service('/robohead_controller/speakers_driver/SetVolume')
print('Все сервисы доступны')
- Ожидание сервисов
Перед тем как отправлять запросы, необходимо убедиться, что сервисы на стороне пакетов устройств уже запущены и готовы принять команды. Мы дожидаемся следующих сервисов:/robohead_controller/display_driver/PlayMedia— воспроизведение изображений./robohead_controller/neck_driver/NeckSetAngle— управление углами шеи./robohead_controller/ears_driver/EarsSetAngle— управление углами ушей./robohead_controller/speakers_driver/PlayAudio— воспроизведение аудио./robohead_controller/speakers_driver/SetVolume— установка громкости.
# Создаём объекты-посредники (ServiceProxy) для каждого сервиса
service_display = rospy.ServiceProxy(
'/robohead_controller/display_driver/PlayMedia', PlayMedia
)
service_neck = rospy.ServiceProxy(
'/robohead_controller/neck_driver/NeckSetAngle', NeckSetAngle
)
service_ears = rospy.ServiceProxy(
'/robohead_controller/ears_driver/EarsSetAngle', EarsSetAngle
)
service_play_audio = rospy.ServiceProxy(
'/robohead_controller/speakers_driver/PlayAudio', PlayAudio
)
service_set_volume = rospy.ServiceProxy(
'/robohead_controller/speakers_driver/SetVolume', SetVolume
)
print('ServiceProxy для всех сервисов создан')
print('Запуск сценария "Улыбнись"')
- Создание объектов ServiceProxy
При помощиrospy.ServiceProxyмы создаём вызовы к сервисам, используя их имена и типы сообщений.
Шаг 3. Пошаговое выполнение сценария
3.1. Показ изображения на дисплее
# Заполняем запрос для воспроизведения изображения на экране
msg = PlayMediaRequest()
msg.path_to_file = script_path + 'smile.png'
msg.is_blocking = 0
msg.is_cycled = 0
service_display(msg)
print('Изображение отображено')
3.2. Поворот шеи и наклон головы
# Подготовим запрос для изменения угла шеи
neck_msg = NeckSetAngleRequest()
neck_msg.horizontal_angle = 30 # поворот вправо на 30°
neck_msg.vertical_angle = 15 # поднятие вверх на 15°
neck_msg.is_blocking = 0
neck_msg.duration = 1
service_neck(neck_msg)
print('Шея повернута: горизонтальный угол 30°, вертикальный угол 15°')
horizontal_angle— угол поворота шеи в горизонтальной плоскости (градусы).vertical_angle— угол наклона шеи в вертикальной плоскости (градусы).duration— время, за которое будет выполнено движение (в секундах).
3.3. Установка громкости и воспроизведение звука
# Устанавливаем громкость на 60% из диапазона 0..100
volume_msg = SetVolumeRequest()
volume_msg.volume = 60
service_set_volume(volume_msg)
print('Установлена громкость 60%')
# Воспроизводим аудио-файл
audio_msg = PlayAudioRequest()
audio_msg.path_to_file = script_path + 'smile.mp3'
audio_msg.is_cycled = 0
audio_msg.is_blocking = 0
service_play_audio(audio_msg)
print('Аудио "smile.mp3" запущено')
volume_msg.volume— значение громкости от 0 до 100.audio_msg.path_to_file— путь к mp3-файлу.
3.4. Анимация ушей
Для создания эффекта покачивания ушей мы будем попеременно устанавливать углы на 45° вперёд и назад с шагом 0.5 секунды в течение 3 секунд.
# Начальное время для отсчёта трёх секунд
start_time = rospy.get_time()
# Начальное направление движения ушей: 1 — вперёд, -1 — назад
direction = 1
ears_msg = EarsSetAngleRequest()
# Пока не прошло 3 секунды, дергаем ушами
while (rospy.get_time() - start_time) < 3:
ears_msg.left_ear_angle = direction * 45 # угол для левого уха
ears_msg.right_ear_angle = -direction * 45 # угол для правого уха
service_ears(ears_msg)
direction *= -1
rospy.sleep(0.5)
print('Анимация ушей (3 секунды) завершена')
ears_msg.left_ear_angleиears_msg.right_ear_angle— углы поворота левого и правого уха (в градусах).- Мы меняем направление (
direction *= -1) каждые 0.5 секунды.
3.5. Завершение сценария: возврат в исходное положение
После того, как изображение и звук уже проиграны, а анимация с ушами завершена, необходимо убрать картинку с дисплея и вернуть голову и уши в нулевое положение.
# Отключаем отображение изображения (отправляем пустой вызов)
service_display()
# Возвращаем уши в 0° (нейтральное положение)
service_ears(0, 0)
# Возвращаем шею в 0° и устанавливаем длительность движения 1 секунду
service_neck(0, 0, 1, 1)
print('Сценарий "Улыбнись" завершён. Все механизмы возвращены в исходное положение.')
Шаг 4. Запуск сервисов и выполнение скрипта
-
Остановка системного сервиса
robohead.service.sudo systemctl stop robohead.service -
Запуск зависимостей (драйверов).
ros2 launch robohead_controller dependencies.launch.pyЭта команда запустит все необходимые драйверы (display_driver, ears_driver, neck_driver, speakers_driver, ...).
-
Запуск нашего скрипта.
В том же терминале, где расположен файлmain.py, выполните:python3 ~/scripts/main.pyСкрипт автоматически подключится к запущенным сервисам и выполнит описанный сценарий.
-
Повторный запуск.
После первого запуска скрипта можно многократно перезапускать его командой:python3 ~/scripts/main.pyПосле перезагрузки Робоголовы, повторите шаг 1 (остановку сервиса), затем шаг 2 (запуск зависимостей).