1. 목표
실습 환경은 아래와 같습니다.
- TurtleBot3 Burger
실제 USB 스피커가 연결된 장비입니다.
여기에서gTTS와mpg123을 실행합니다. - 원격 PC
사용자가 직접 조작하는 GUI 프로그램을 실행합니다.
여기에서는 음성을 직접 출력하지 않고/audio/command토픽만 발행합니다.
즉, 원격 PC의 GUI는 명령만 보내고, TurtleBot3 Burger가 실제 음성을 출력합니다.
2. 시스템 구조
전체 구조는 다음과 같습니다.
원격 PC
└── robot_audio_gui
└── audio_gui_publisher.py
|
| /audio/command 토픽 발행
v
TurtleBot3 Burger
└── robot_audio_output
└── audio_output_node.py
|
| gTTS로 MP3 생성
| mpg123로 MP3 재생
v
USB 스피커
3. AudioCommand 메시지
먼저 음성 명령 메시지를 사용합니다.
파일 위치는 다음과 같습니다.
~/turtlebot3_ws/src/robot_audio_interfaces/msg/AudioCommand.msg
내용은 다음과 같습니다.
# Audio command type
uint8 TYPE_TTS=0
uint8 TYPE_EFFECT=1
uint8 TYPE_TTS_AND_EFFECT=2
uint8 TYPE_STOP=3
uint8 type
# TTS text
string text
# Effect sound id
# examples:
# start, waypoint, goal, warning, error
string sound_id
# 0.0 ~ 1.0
float32 volume
# repeat count
uint8 repeat
메시지 필드의 의미는 다음과 같습니다.
| 필드 | 의미 |
|---|---|
type | 명령 종류 |
text | gTTS로 출력할 문장 |
sound_id | 출력할 효과음 이름 |
volume | 볼륨 값, 0.0 ~ 1.0 |
repeat | 반복 횟수 |
type 값은 다음과 같습니다.
| type | 의미 |
|---|---|
0 | TTS 음성만 출력 |
1 | 효과음만 출력 |
2 | 효과음 출력 후 TTS 출력 |
3 | 현재 재생 중인 음성 정지 |
4. robot_audio_interfaces 패키지 생성
TurtleBot3와 원격 PC 양쪽에 같은 메시지 패키지가 모두 설치된 상태입니다.
메시지가 정상 생성되었는지 확인합니다.
ros2 interface show robot_audio_interfaces/msg/AudioCommand


5. 원격 PC용 PyQt GUI 패키지 생성
이 패키지는 원격 PC에서 실행합니다.
원격 PC에도 ROS 2 Humble과 robot_audio_interfaces 패키지가 있어야 합니다.
cd ~/pyqt_ws/src
ros2 pkg create robot_audio_gui --build-type ament_python --dependencies rclpy robot_audio_interfaces

원격 PC의 GUI는 gTTS를 실행하지 않습니다.
원격 PC의 GUI는 mpg123도 실행하지 않습니다.
원격 PC의 GUI는 USB 스피커도 제어하지 않습니다.
오직 /audio/command 토픽만 발행합니다.
6. 원격 PC용 audio_gui_publisher.py
파일 위치는 다음과 같습니다.
touch ~/pyqt_ws/src/robot_audio_gui/robot_audio_gui/audio_gui_publisher.py

전체 코드는 다음과 같습니다.
#!/usr/bin/env python3
import sys
import rclpy
from rclpy.node import Node
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QTextEdit,
QPushButton,
QLabel,
QVBoxLayout,
QHBoxLayout,
QComboBox,
QDoubleSpinBox,
QSpinBox,
QMessageBox,
)
from robot_audio_interfaces.msg import AudioCommand
class AudioCommandPublisher(Node):
def __init__(self):
super().__init__('audio_gui_publisher_node')
self.declare_parameter('topic_name', '/audio/command')
self.topic_name = self.get_parameter('topic_name').value
self.publisher = self.create_publisher(
AudioCommand,
self.topic_name,
10
)
self.get_logger().info(f'Audio GUI publisher started')
self.get_logger().info(f'Publish topic: {self.topic_name}')
def publish_command(
self,
command_type,
text='',
sound_id='',
volume=1.0,
repeat=1
):
msg = AudioCommand()
msg.type = command_type
msg.text = text
msg.sound_id = sound_id
msg.volume = float(volume)
msg.repeat = int(repeat)
self.publisher.publish(msg)
self.get_logger().info(
f'Published AudioCommand '
f'type={msg.type}, text="{msg.text}", '
f'sound_id="{msg.sound_id}", volume={msg.volume}, repeat={msg.repeat}'
)
class AudioGuiWindow(QWidget):
def __init__(self, ros_node: AudioCommandPublisher):
super().__init__()
self.ros_node = ros_node
self.setWindowTitle('TurtleBot3 Audio Command GUI')
self.resize(560, 420)
self.title_label = QLabel('TurtleBot3 원격 음성 출력 GUI')
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText(
'TurtleBot3 USB 스피커로 출력할 문장을 입력하세요.'
)
self.sound_combo = QComboBox()
self.sound_combo.addItem('효과음 없음', '')
self.sound_combo.addItem('start', 'start')
self.sound_combo.addItem('waypoint', 'waypoint')
self.sound_combo.addItem('goal', 'goal')
self.sound_combo.addItem('warning', 'warning')
self.sound_combo.addItem('error', 'error')
self.volume_spin = QDoubleSpinBox()
self.volume_spin.setRange(0.0, 1.0)
self.volume_spin.setSingleStep(0.1)
self.volume_spin.setValue(1.0)
self.repeat_spin = QSpinBox()
self.repeat_spin.setRange(1, 10)
self.repeat_spin.setValue(1)
self.tts_button = QPushButton('TTS 출력')
self.effect_button = QPushButton('효과음 출력')
self.tts_effect_button = QPushButton('효과음 + TTS 출력')
self.stop_button = QPushButton('재생 정지')
self.clear_button = QPushButton('내용 지우기')
self.status_label = QLabel('대기 중')
sound_layout = QHBoxLayout()
sound_layout.addWidget(QLabel('효과음:'))
sound_layout.addWidget(self.sound_combo)
option_layout = QHBoxLayout()
option_layout.addWidget(QLabel('볼륨:'))
option_layout.addWidget(self.volume_spin)
option_layout.addWidget(QLabel('반복:'))
option_layout.addWidget(self.repeat_spin)
button_layout_1 = QHBoxLayout()
button_layout_1.addWidget(self.tts_button)
button_layout_1.addWidget(self.effect_button)
button_layout_2 = QHBoxLayout()
button_layout_2.addWidget(self.tts_effect_button)
button_layout_2.addWidget(self.stop_button)
button_layout_2.addWidget(self.clear_button)
main_layout = QVBoxLayout()
main_layout.addWidget(self.title_label)
main_layout.addWidget(self.text_edit)
main_layout.addLayout(sound_layout)
main_layout.addLayout(option_layout)
main_layout.addLayout(button_layout_1)
main_layout.addLayout(button_layout_2)
main_layout.addWidget(self.status_label)
self.setLayout(main_layout)
self.tts_button.clicked.connect(self.publish_tts)
self.effect_button.clicked.connect(self.publish_effect)
self.tts_effect_button.clicked.connect(self.publish_tts_and_effect)
self.stop_button.clicked.connect(self.publish_stop)
self.clear_button.clicked.connect(self.clear_text)
self.spin_timer = QTimer(self)
self.spin_timer.timeout.connect(self.spin_ros_once)
self.spin_timer.start(20)
def spin_ros_once(self):
rclpy.spin_once(self.ros_node, timeout_sec=0.0)
def get_text(self):
return self.text_edit.toPlainText().strip()
def get_sound_id(self):
return self.sound_combo.currentData()
def get_volume(self):
return self.volume_spin.value()
def get_repeat(self):
return self.repeat_spin.value()
def publish_tts(self):
text = self.get_text()
if not text:
QMessageBox.warning(
self,
'입력 오류',
'TTS로 출력할 문장을 입력하세요.'
)
return
self.ros_node.publish_command(
command_type=AudioCommand.TYPE_TTS,
text=text,
sound_id='',
volume=self.get_volume(),
repeat=self.get_repeat()
)
self.status_label.setText('TTS 명령 발행 완료')
def publish_effect(self):
sound_id = self.get_sound_id()
if not sound_id:
QMessageBox.warning(
self,
'선택 오류',
'출력할 효과음을 선택하세요.'
)
return
self.ros_node.publish_command(
command_type=AudioCommand.TYPE_EFFECT,
text='',
sound_id=sound_id,
volume=self.get_volume(),
repeat=self.get_repeat()
)
self.status_label.setText(f'효과음 명령 발행 완료: {sound_id}')
def publish_tts_and_effect(self):
text = self.get_text()
sound_id = self.get_sound_id()
if not text:
QMessageBox.warning(
self,
'입력 오류',
'TTS로 출력할 문장을 입력하세요.'
)
return
if not sound_id:
QMessageBox.warning(
self,
'선택 오류',
'먼저 출력할 효과음을 선택하세요.'
)
return
self.ros_node.publish_command(
command_type=AudioCommand.TYPE_TTS_AND_EFFECT,
text=text,
sound_id=sound_id,
volume=self.get_volume(),
repeat=self.get_repeat()
)
self.status_label.setText('효과음 + TTS 명령 발행 완료')
def publish_stop(self):
self.ros_node.publish_command(
command_type=AudioCommand.TYPE_STOP,
text='',
sound_id='',
volume=1.0,
repeat=1
)
self.status_label.setText('재생 정지 명령 발행 완료')
def clear_text(self):
self.text_edit.clear()
self.status_label.setText('내용 삭제 완료')
def closeEvent(self, event):
self.spin_timer.stop()
event.accept()
def main(args=None):
rclpy.init(args=args)
ros_node = AudioCommandPublisher()
app = QApplication(sys.argv)
window = AudioGuiWindow(ros_node)
window.show()
exit_code = app.exec_()
ros_node.destroy_node()
rclpy.shutdown()
sys.exit(exit_code)
if __name__ == '__main__':
main()

7. 원격 PC GUI 코드 설명
원격 PC GUI에서 중요한 부분은 다음입니다.
self.publisher = self.create_publisher(
AudioCommand,
self.topic_name,
10
)
이 Publisher가 /audio/command 토픽을 발행합니다.
TTS 버튼을 누르면 다음 메시지를 발행합니다.
msg.type = AudioCommand.TYPE_TTS
msg.text = text
msg.sound_id = ''
msg.volume = volume
msg.repeat = repeat
효과음 버튼을 누르면 다음 메시지를 발행합니다.
msg.type = AudioCommand.TYPE_EFFECT
msg.text = ''
msg.sound_id = sound_id
msg.volume = volume
msg.repeat = repeat
효과음과 TTS를 함께 출력하려면 다음 메시지를 발행합니다.
msg.type = AudioCommand.TYPE_TTS_AND_EFFECT
msg.text = text
msg.sound_id = sound_id
msg.volume = volume
msg.repeat = repeat
정지 버튼을 누르면 다음 메시지를 발행합니다.
msg.type = AudioCommand.TYPE_STOP
msg.text = ''
msg.sound_id = ''
msg.volume = 1.0
msg.repeat = 1
즉, 원격 PC GUI는 실제 음성을 만들지 않습니다.
명령 메시지만 만들고 발행합니다.
8. robot_audio_gui/package.xml
<?xml version="1.0"?>
<package format="3">
<name>robot_audio_gui</name>
<version>0.0.1</version>
<description>PyQt GUI publisher for robot audio command topic</description>
<maintainer email="user@example.com">user</maintainer>
<license>Apache-2.0</license>
<exec_depend>rclpy</exec_depend>
<exec_depend>robot_audio_interfaces</exec_depend>
<exec_depend>python3-pyqt5</exec_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>

9. robot_audio_gui/setup.py
from setuptools import setup
package_name = 'robot_audio_gui'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
data_files=[
(
'share/ament_index/resource_index/packages',
['resource/' + package_name]
),
(
'share/' + package_name,
['package.xml']
),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='user',
maintainer_email='user@example.com',
description='PyQt GUI publisher for robot audio command topic',
license='Apache-2.0',
entry_points={
'console_scripts': [
'audio_gui_publisher = robot_audio_gui.audio_gui_publisher:main',
],
},
)

10. 원격 PC에서 GUI 패키지 빌드
원격 PC에서 빌드합니다.
cd ~/pyqt_ws
colcon build --packages-select robot_audio_gui
source install/setup.bash
11. 실행
실행 순서는 중요합니다.
먼저 TurtleBot3 Burger에서 음성 출력 노드를 실행합니다.
cd ~/turtlebot3_ws
source install/setup.bash
ros2 run robot_audio_output audio_output_node

그 다음 원격 PC에서 GUI를 실행합니다.
cd ~/turtlebot3_ws
source install/setup.bash
source ~/turtlebot3_ws/install/setup.bash
ros2 run robot_audio_gui audio_gui_publisher

GUI 창이 뜨면 문장을 입력하고 TTS 출력 버튼을 누릅니다.
정상이라면 TurtleBot3 Burger에 연결된 USB 스피커에서 음성이 출력됩니다.


