Qt Designer로 TurtleBot3 Burger 제어용 PyQt UI 만들기 #2

11. ROS 2 Node 클래스 추가하기

이제 Python 코드에 ROS 2 노드를 추가합니다.

먼저 필요한 ROS 2 모듈을 import합니다.

Python 파일 상단에 다음을 추가합니다.

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist

그리고 MainWindow 클래스 위에 ROS 2 노드 클래스를 추가합니다.

class TurtleBot3GuiNode(Node):
    def __init__(self, namespace=''):
        super().__init__('turtlebot3_burger_gui')

        namespace = namespace.strip()

        # 사용자가 "empty", "none", "/" 등을 넣으면 namespace 없음으로 처리
        if namespace.lower() in ['', 'empty', 'none', '/']:
            namespace = ''

        # 앞뒤 slash 정리
        namespace = namespace.strip('/')

        if namespace:
            cmd_topic = f'/{namespace}/cmd_vel'
        else:
            cmd_topic = '/cmd_vel'

        self.cmd_pub = self.create_publisher(
            Twist,
            cmd_topic,
            10
        )

    def publish_cmd(self, linear, angular):
        msg = Twist()
        msg.linear.x = float(linear)
        msg.angular.z = float(angular)
        self.cmd_pub.publish(msg)

이 클래스는 ROS 2 노드입니다.

핵심은 다음입니다.

self.cmd_pub = self.create_publisher(Twist, self.topic('/cmd_vel'), 10)

이 코드가 /cmd_vel 토픽 publisher를 만듭니다.

Namespace를 입력하면 /tb3/cmd_vel 같은 토픽으로 바꿔서 발행할 수 있습니다.

12. Connect 버튼으로 ROS 2 노드 생성하기

이제 MainWindow에서 ROS 2 노드를 생성합니다.

__init__()에 다음 변수를 추가합니다.

self.node = None

전체 구조는 다음처럼 됩니다.

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        ui_path = Path(__file__).with_name('turtlebot3_burger_gui.ui')
        uic.loadUi(str(ui_path), self)

        self.node = None

        self.connect_signals()

connect_ros() 함수를 다음처럼 수정합니다.

def connect_ros(self):
    if self.node:
        print('ROS 2 is already connected')
        return

    domain_id = self.domain_lineEdit.text().strip()
    os.environ['ROS_DOMAIN_ID'] = domain_id if domain_id else '30'

    if not rclpy.ok():
        rclpy.init(args=None)

    self.node = TurtleBot3GuiNode(self.namespace_lineEdit.text())

    self.ros_status_lineEdit.setText('Connected')

    print('ROS 2 connected')

disconnect_ros() 함수도 수정합니다.

def disconnect_ros(self):
    if self.node:
        self.node.destroy_node()
        self.node = None

    self.ros_status_lineEdit.setText('Disconnected')

    print('ROS 2 disconnected')

이제 send_velocity() 함수를 실제 ROS 2 발행 함수로 변경합니다.

def send_velocity(self, linear, angular):
    if not self.node:
        print('Connect ROS 2 first')
        return

    self.node.publish_cmd(linear, angular)
    self.cmd_lineEdit.setText(f'lin={linear:.2f}, ang={angular:.2f}')

    print(f'/cmd_vel published: linear={linear:.2f}, angular={angular:.2f}')

여기까지 작성하면 버튼을 누를 때 /cmd_vel 메시지를 발행할 수 있습니다.

하지만 아직 중요한 문제가 하나 있습니다.

ROS 2는 rclpy.spin_once()를 계속 호출해야 callback과 내부 처리가 정상적으로 돌아갑니다.

PyQt는 자체 이벤트 루프를 사용하고, ROS 2도 자체 spin 처리가 필요합니다.

그래서 PyQt에서는 QTimer를 사용해서 주기적으로 rclpy.spin_once()를 호출하는 구조를 사용합니다.

13. QTimer로 ROS 2 spin 처리하기

상단 import에 QTimer를 추가합니다.

from PyQt5.QtCore import QTimer

__init__()에 타이머를 추가합니다.

self.ros_timer = QTimer(self)
self.ros_timer.timeout.connect(self.spin_ros_once)

connect_ros()에서 ROS 2 연결 후 타이머를 시작합니다.

self.ros_timer.start(20)

disconnect_ros()에서 타이머를 멈춥니다.

self.ros_timer.stop()

그리고 spin_ros_once() 함수를 추가합니다.

def spin_ros_once(self):
    if self.node:
        rclpy.spin_once(self.node, timeout_sec=0.0)

정리하면 관련 함수는 다음과 같습니다.

def connect_ros(self):
    if self.node:
        print('ROS 2 is already connected')
        return

    domain_id = self.domain_lineEdit.text().strip()
    os.environ['ROS_DOMAIN_ID'] = domain_id if domain_id else '30'

    if not rclpy.ok():
        rclpy.init(args=None)

    self.node = TurtleBot3GuiNode(self.namespace_lineEdit.text())

    self.ros_status_lineEdit.setText('Connected')
    self.ros_timer.start(20)

    print('ROS 2 connected')

def disconnect_ros(self):
    if self.node:
        self.node.destroy_node()
        self.node = None

    self.ros_timer.stop()
    self.ros_status_lineEdit.setText('Disconnected')

    print('ROS 2 disconnected')

def spin_ros_once(self):
    if self.node:
        rclpy.spin_once(self.node, timeout_sec=0.0)

이제 TurtleBot3 Burger가 켜져 있고 bringup이 실행 중이면 GUI에서 /cmd_vel을 발행할 수 있습니다.

테스트 전에 터미널에서 bringup을 실행합니다.

source /opt/ros/humble/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_bringup robot.launch.py

다른 터미널에서 GUI를 실행합니다.

source /opt/ros/humble/setup.bash
export TURTLEBOT3_MODEL=burger
python3 turtlebot3_burger_gui_node.py

GUI에서 순서대로 실행합니다.

  1. Connect 클릭
  2. Forward 클릭
  3. 로봇이 전진하는지 확인
  4. STOP 클릭

실제 로봇으로 실습할 때는 반드시 바퀴가 바닥에서 뜬 상태로 먼저 테스트하는 것이 안전합니다.

14. Nav2 Goal 영역 만들기

이번에는 Nav2 목표 좌표를 입력하는 영역을 만듭니다.

Qt Designer에서 Group Box를 추가합니다.

objectName: nav_groupBox
title: Nav2 Goal
geometry:
  x: 400
  y: 190
  width: 360
  height: 260

목표 X 입력 Label을 추가합니다.

class: QLabel
objectName: goal_x_label
text: X
geometry:
  x: 20
  y: 35
  width: 50
  height: 25

목표 X 입력 SpinBox를 추가합니다.

class: QDoubleSpinBox
objectName: goal_x_spinBox
minimum: -20.00
maximum: 20.00
decimals: 2
geometry:
  x: 80
  y: 35
  width: 90
  height: 25

목표 Y 입력 Label을 추가합니다.

class: QLabel
objectName: goal_y_label
text: Y
geometry:
  x: 190
  y: 35
  width: 50
  height: 25

목표 Y 입력 SpinBox를 추가합니다.

class: QDoubleSpinBox
objectName: goal_y_spinBox
minimum: -20.00
maximum: 20.00
decimals: 2
geometry:
  x: 250
  y: 35
  width: 90
  height: 25

Yaw 입력 Label을 추가합니다.

class: QLabel
objectName: goal_yaw_label
text: Yaw
geometry:
  x: 20
  y: 75
  width: 50
  height: 25

Yaw 입력 SpinBox를 추가합니다.

class: QDoubleSpinBox
objectName: goal_yaw_spinBox
minimum: -3.14
maximum: 3.14
decimals: 2
singleStep: 0.10
geometry:
  x: 80
  y: 75
  width: 90
  height: 25

Preset ComboBox를 추가합니다.

class: QComboBox
objectName: preset_goal_CB
geometry:
  x: 20
  y: 115
  width: 150
  height: 30

ComboBox에는 다음 항목을 추가합니다.

Home
Point A
Point B

Designer에서 ComboBox 항목을 추가하는 방법은 다음과 같습니다.

  1. preset_goal_CB 클릭
  2. 콤보박승[서 마우스 오른쪽을 클릭하면 나타나는 메뉴에서 Edit Items를 선택합니다.
  3. ‘+’ 버튼 클릭
  4. Home, Point A, Point B 추가
  5. OK 클릭

Preset 불러오기 버튼을 추가합니다.

class: QPushButton
objectName: load_preset_PB
text: Load Preset
geometry:
  x: 190
  y: 115
  width: 150
  height: 30

Goal 전송 버튼을 추가합니다.

class: QPushButton
objectName: send_goal_PB
text: Send Goal
geometry:
  x: 20
  y: 170
  width: 150
  height: 35

Goal 취소 버튼을 추가합니다.

class: QPushButton
objectName: cancel_goal_PB
text: Cancel Goal
geometry:
  x: 190
  y: 170
  width: 150
  height: 35

Initial Pose 버튼을 추가합니다.

class: QPushButton
objectName: initial_pose_PB
text: Set Initial Pose
geometry:
  x: 20
  y: 215
  width: 320
  height: 35

저장합니다.

turtlebot3_burger_gui.ui

15. Nav2 Goal 컴포넌트 사용하기

먼저 Preset 버튼부터 연결합니다.

connect_signals()에 다음을 추가합니다.

self.load_preset_PB.clicked.connect(self.load_preset_goal)

함수를 추가합니다.

def load_preset_goal(self):
    idx = self.preset_goal_CB.currentIndex()

    presets = [
        (0.0, 0.0, 0.0),
        (1.0, 0.0, 0.0),
        (0.0, 1.0, 1.57)
    ]

    x, y, yaw = presets[idx]

    self.goal_x_spinBox.setValue(x)
    self.goal_y_spinBox.setValue(y)
    self.goal_yaw_spinBox.setValue(yaw)

    print(f'Preset loaded: x={x}, y={y}, yaw={yaw}')

실행해서 확인합니다.

python3 turtlebot3_burger_gui_node.py

확인 순서는 다음입니다.

  1. ComboBox에서 Point A 선택
  2. Load Preset 클릭
  3. X 값이 1.00으로 바뀌는지 확인
  4. ComboBox에서 Point B 선택
  5. Load Preset 클릭
  6. Y 값이 1.00, Yaw 값이 1.57로 바뀌는지 확인

이 단계에서는 Nav2로 goal을 보내지 않습니다.

먼저 UI 입력값이 Python 코드로 잘 들어오는지 확인하는 단계입니다.

16. Odometry View 영역 만들기

이번에는 /odom 데이터를 화면에 표시합니다.

Qt Designer에서 Group Box를 추가합니다.

objectName: odom_groupBox
title: Odometry View
geometry:
  x: 780
  y: 190
  width: 380
  height: 260

Label 3개를 추가합니다.

class: QLabel
objectName: odom_x_label
text: X
geometry:
  x: 25
  y: 40
  width: 40
  height: 25

class: QLabel
objectName: odom_y_label
text: Y
geometry:
  x: 25
  y: 95
  width: 40
  height: 25

class: QLabel
objectName: odom_yaw_label
text: Yaw
geometry:
  x: 25
  y: 150
  width: 40
  height: 25

LCD Number 3개를 추가합니다.

class: QLCDNumber
objectName: odom_x_lcd
digitCount: 7
geometry:
  x: 80
  y: 35
  width: 130
  height: 40

class: QLCDNumber
objectName: odom_y_lcd
digitCount: 7
geometry:
  x: 80
  y: 90
  width: 130
  height: 40

class: QLCDNumber
objectName: odom_yaw_lcd
digitCount: 7
geometry:
  x: 80
  y: 145
  width: 130
  height: 40

초기화 버튼을 추가합니다.

class: QPushButton
objectName: reset_odom_view_PB
text: Reset View
geometry:
  x: 230
  y: 200
  width: 120
  height: 35

저장합니다.

turtlebot3_burger_gui.ui

17. /odom 구독 코드 작성하기

상단 import에 Odometry 메시지를 추가합니다.

from nav_msgs.msg import Odometry

Yaw 계산을 위해 math도 추가합니다.

import math

Quaternion을 yaw로 바꾸는 함수를 추가합니다.

def quaternion_to_yaw(q):
    siny_cosp = 2.0 * (q.w * q.z + q.x * q.y)
    cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
    return math.atan2(siny_cosp, cosy_cosp)

TurtleBot3GuiNode 클래스의 __init__()/odom subscriber를 추가합니다.

if namespace:
    odom_topic = f'/{namespace}/odom'
else:
     odom_topic = '/odom'

self.odom_sub = self.create_subscription(
    Odometry,
    odom_topic,
    self.odom_callback,
    10
)

self.last_odom = None

callback 함수를 추가합니다.

def odom_callback(self, msg):
    self.last_odom = msg

이제 GUI에서 주기적으로 화면을 갱신해야 합니다.

MainWindow.__init__()에 UI 갱신 타이머를 추가합니다.

self.ui_timer = QTimer(self)
self.ui_timer.timeout.connect(self.refresh_robot_status)
self.ui_timer.start(200)

refresh_robot_status() 함수를 추가합니다.

def refresh_robot_status(self):
    if not self.node:
        return

    odom = self.node.last_odom

    if odom:
        p = odom.pose.pose.position
        yaw = quaternion_to_yaw(odom.pose.pose.orientation)

        self.odom_x_lcd.display(f'{p.x:.2f}')
        self.odom_y_lcd.display(f'{p.y:.2f}')
        self.odom_yaw_lcd.display(f'{yaw:.2f}')

초기화 버튼도 연결합니다.

connect_signals()에 다음을 추가합니다.

self.reset_odom_view_PB.clicked.connect(self.reset_odom_display)

그리고 함수를 추가합니다.

def reset_odom_display(self):
    self.odom_x_lcd.display('0.00')
    self.odom_y_lcd.display('0.00')
    self.odom_yaw_lcd.display('0.00')
    print('Odometry display reset. Robot odom frame is not reset.')

여기서 중요한 점이 있습니다.

Reset View 버튼은 화면 표시만 0으로 바꾸는 버튼입니다. 로봇의 실제 /odom 좌표계를 리셋하는 기능은 아닙니다.

18. Initial Pose 발행하기

Nav2를 사용할 때는 로봇의 초기 위치를 /initialpose로 알려줘야 합니다.

RViz2에서 2D Pose Estimate를 누르는 것과 같은 역할을 GUI 버튼으로 만들겠습니다.

상단 import에 메시지를 추가합니다.

from geometry_msgs.msg import PoseWithCovarianceStamped

Yaw를 Quaternion으로 바꾸는 함수를 추가합니다.

def yaw_to_quaternion(yaw):
    half = yaw * 0.5

    qx = 0.0
    qy = 0.0
    qz = math.sin(half)
    qw = math.cos(half)

    return qx, qy, qz, qw

TurtleBot3GuiNode.__init__()에 publisher를 추가합니다.

if namespace:
    initpos_topic = f'/{namespace}/initialpose'
else:
    initpos_topic = '/initialpose'

self.initial_pose_pub = self.create_publisher(
    PoseWithCovarianceStamped,
    initpos_topic,
    10
)

TurtleBot3GuiNode 클래스에 initial pose 발행 함수를 추가합니다.

def publish_initial_pose(self, x, y, yaw):
    msg = PoseWithCovarianceStamped()

    msg.header.frame_id = 'map'
    msg.header.stamp = self.get_clock().now().to_msg()

    msg.pose.pose.position.x = float(x)
    msg.pose.pose.position.y = float(y)

    qx, qy, qz, qw = yaw_to_quaternion(yaw)

    msg.pose.pose.orientation.x = qx
    msg.pose.pose.orientation.y = qy
    msg.pose.pose.orientation.z = qz
    msg.pose.pose.orientation.w = qw

    msg.pose.covariance[0] = 0.25
    msg.pose.covariance[7] = 0.25
    msg.pose.covariance[35] = 0.0685

    self.initial_pose_pub.publish(msg)

connect_signals()에 버튼 연결을 추가합니다.

self.initial_pose_PB.clicked.connect(self.set_initial_pose)

MainWindow 클래스에 함수를 추가합니다.

def set_initial_pose(self):
    if not self.node:
        print('Connect ROS 2 first')
        return

    self.node.publish_initial_pose(
        self.goal_x_spinBox.value(),
        self.goal_y_spinBox.value(),
        self.goal_yaw_spinBox.value()
    )

    print('Initial pose published to /initialpose')

테스트 순서는 다음입니다.

  1. TurtleBot3 bringup 실행
  2. Nav2 실행
  3. RViz2 실행
  4. GUI 실행
  5. Connect 클릭
  6. X, Y, Yaw 입력
  7. Set Initial Pose 클릭
  8. RViz2에서 로봇 위치가 반영되는지 확인

19. Nav2 Goal 전송하기

이제 Nav2 action server로 목표 좌표를 보내겠습니다.

상단 import에 다음을 추가합니다.

from rclpy.action import ActionClient
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped

TurtleBot3GuiNode.__init__()에 ActionClient를 추가합니다.

if namespace:
    navpos_topic = f'/{namespace}/navigate_to_pose'
else:
    navpos_topic = '/navigate_to_pose'

self.nav_client = ActionClient(
    self,
    NavigateToPose,
    navpos_topic
)

self.goal_handle = None

TurtleBot3GuiNode 클래스에 goal 전송 함수를 추가합니다.

def send_goal(self, x, y, yaw):
    goal_msg = NavigateToPose.Goal()

    goal_msg.pose = PoseStamped()
    goal_msg.pose.header.frame_id = 'map'
    goal_msg.pose.header.stamp = self.get_clock().now().to_msg()

    goal_msg.pose.pose.position.x = float(x)
    goal_msg.pose.pose.position.y = float(y)

    qx, qy, qz, qw = yaw_to_quaternion(yaw)

    goal_msg.pose.pose.orientation.x = qx
    goal_msg.pose.pose.orientation.y = qy
    goal_msg.pose.pose.orientation.z = qz
    goal_msg.pose.pose.orientation.w = qw

    if not self.nav_client.wait_for_server(timeout_sec=1.0):
        return False, 'Nav2 action server is not available'

    future = self.nav_client.send_goal_async(goal_msg)
    future.add_done_callback(self._goal_response_callback)

    return True, f'Goal sent: x={x:.2f}, y={y:.2f}, yaw={yaw:.2f}'

Goal 응답 callback을 추가합니다.

def _goal_response_callback(self, future):
    self.goal_handle = future.result()

    if self.goal_handle and self.goal_handle.accepted:
        self.get_logger().info('Goal accepted')
    else:
        self.get_logger().warn('Goal rejected')

Goal 취소 함수도 추가합니다.

def cancel_goal(self):
    if self.goal_handle:
        self.goal_handle.cancel_goal_async()
        return True

    return False

이제 connect_signals()에 버튼 연결을 추가합니다.

self.send_goal_PB.clicked.connect(self.send_nav_goal)
self.cancel_goal_PB.clicked.connect(self.cancel_nav_goal)

MainWindow 클래스에 함수를 추가합니다.

def send_nav_goal(self):
    if not self.node:
        print('Connect ROS 2 first')
        return

    ok, text = self.node.send_goal(
        self.goal_x_spinBox.value(),
        self.goal_y_spinBox.value(),
        self.goal_yaw_spinBox.value()
    )

    print(text)

Goal 취소 함수도 추가합니다.

def cancel_nav_goal(self):
    if not self.node:
        print('Connect ROS 2 first')
        return

    if self.node.cancel_goal():
        print('Goal cancel requested')
    else:
        print('No active goal handle')

테스트 순서는 다음과 같습니다.

  1. TurtleBot3 bringup 실행
  2. SLAM 또는 Navigation2 실행
  3. 지도와 localization 상태 확인
  4. GUI 실행
  5. Connect 클릭
  6. 목표 X, Y, Yaw 입력
  7. Send Goal 클릭
  8. TurtleBot3 Burger가 목표 지점으로 이동하는지 확인

Nav2 goal이 동작하려면 단순히 GUI만 켜져 있으면 안 됩니다.

반드시 Nav2가 실행 중이어야 하고, 지도 또는 localization 상태가 준비되어 있어야 합니다.

20. LaserScan 최소 거리 표시하기

이번에는 /scan 데이터를 구독해서 가장 가까운 장애물 거리를 표시합니다.

상단 import에 LaserScan을 추가합니다.

from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import LaserScan

TurtleBot3GuiNode.__init__()에 subscriber를 추가합니다.

qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=10
)

if namespace:
    scan_topic = f'/{namespace}/scan'
else:
    scan_topic = '/scan'

self.scan_sub = self.create_subscription(
    LaserScan,
    scan_topic,
    self.scan_callback,
    qos_profile
)

self.last_scan_min = None

callback 함수를 추가합니다.

def scan_callback(self, msg):
    values = [
        v for v in msg.ranges
        if math.isfinite(v) and v > 0.0
    ]

    self.last_scan_min = min(values) if values else None

refresh_robot_status() 함수에 scan 표시 코드를 추가합니다.

if self.node.last_scan_min is not None:
    self.scan_lineEdit.setText(f'{self.node.last_scan_min:.2f}')

최종 refresh_robot_status() 함수는 다음과 같습니다.

def refresh_robot_status(self):
    if not self.node:
        return

    odom = self.node.last_odom

    if odom:
        p = odom.pose.pose.position
        yaw = quaternion_to_yaw(odom.pose.pose.orientation)

        self.odom_x_lcd.display(f'{p.x:.2f}')
        self.odom_y_lcd.display(f'{p.y:.2f}')
        self.odom_yaw_lcd.display(f'{yaw:.2f}')

    if self.node.last_scan_min is not None:
        self.scan_lineEdit.setText(f'{self.node.last_scan_min:.2f}')

테스트 순서는 다음과 같습니다.

  1. TurtleBot3 bringup 실행
  2. GUI 실행
  3. Connect 클릭
  4. LiDAR 앞에 물체를 가까이 가져가기
  5. Min Scan(m) 값이 변하는지 확인

21. Log 영역 만들기

지금까지는 상태를 터미널에 출력했습니다.

이제 GUI 안에 로그 창을 만들어서 학생들이 버튼을 누를 때마다 화면에서 결과를 확인할 수 있게 하겠습니다.

Qt Designer에서 Group Box를 추가합니다.

objectName: log_groupBox
title: Log
geometry:
  x: 20
  y: 460
  width: 1140
  height: 250

그 안에 QListWidget을 추가합니다.

class: QListWidget
objectName: log_listWidget
geometry:
  x: 20
  y: 35
  width: 1100
  height: 200

저장합니다.

turtlebot3_burger_gui.ui

22. print 대신 GUI 로그 사용하기

MainWindow 클래스에 로그 함수를 추가합니다.

def log(self, text):
    self.log_listWidget.addItem(text)
    self.log_listWidget.scrollToBottom()

이제 기존 print()self.log()로 바꿉니다.

예를 들어 connect_ros()는 다음처럼 바꿉니다.

def connect_ros(self):
    if self.node:
        self.log('ROS 2 is already connected')
        return

    domain_id = self.domain_lineEdit.text().strip()
    os.environ['ROS_DOMAIN_ID'] = domain_id if domain_id else '30'

    if not rclpy.ok():
        rclpy.init(args=None)

    self.node = TurtleBot3GuiNode(self.namespace_lineEdit.text())

    self.ros_status_lineEdit.setText('Connected')
    self.ros_timer.start(20)

    self.log('ROS 2 connected')

send_velocity()도 다음처럼 바꿉니다.

def send_velocity(self, linear, angular):
    if not self.node:
        self.log('Connect ROS 2 first')
        return

    self.node.publish_cmd(linear, angular)
    self.cmd_lineEdit.setText(f'lin={linear:.2f}, ang={angular:.2f}')

    self.log(f'/cmd_vel published: linear={linear:.2f}, angular={angular:.2f}')

__init__() 마지막에 시작 로그도 추가합니다.

self.log('GUI started. Press Connect after ROS 2 environment is sourced.')

이제 GUI를 실행하면 아래쪽 로그 창에 동작 결과가 표시됩니다.

23. 종료 처리 추가하기

GUI를 닫을 때는 로봇이 계속 움직이면 안 됩니다.

그래서 창을 닫기 전에 정지 명령을 보내고, 실행 중인 launch 프로세스를 종료하고, ROS 2 노드를 정리합니다.

MainWindow 클래스에 closeEvent()를 추가합니다.

def closeEvent(self, event):
    if self.node:
        self.send_velocity(0.0, 0.0)

    self.stop_processes()
    self.disconnect_ros()

    if rclpy.ok():
        rclpy.shutdown()

    event.accept()

이 코드는 창을 닫을 때 자동으로 호출됩니다.

실제 로봇에서는 종료 시 정지 명령이 매우 중요합니다.

GUI 프로그램이 꺼졌는데 로봇이 마지막 속도 명령을 유지하면 위험한 상황이 생길 수 있습니다.

Leave a Comment