[Robotics] 2. Robot Motion Tool 정리

김성윤(Jack)·6일 전

Robotics

목록 보기
2/6

0. 전체 내용 요약과 키워드

이 문서는 로봇의 구조를 정의하고, 그 구조를 실제 움직임으로 연결하는 과정을 정리한다. URDF/Xacro는 link, joint, visual, collision, inertial 정보를 통해 로봇의 몸과 운동학적 tree를 만든다. RViz는 이 모델과 TF, joint state, planning 결과를 시각화하고, Gazebo와 ros2_control은 물리 시뮬레이션과 controller 실행을 연결한다.

MoveIt 부분은 로봇 모델을 바탕으로 planning group, end-effector, planning scene, collision object, trajectory planning, controller execution이 어떻게 이어지는지 설명한다. 실습 코드는 /joint_states, /collision_object, STT topic, MoveItPy planning pipeline, gripper 제어, pick-and-place sequence를 통해 “모델이 보인다”에서 “안전하게 계획하고 실행한다”로 넘어가는 원리를 보여 준다.

키워드: URDF, Xacro, link, joint, visual, collision, inertial, TF, RViz, Gazebo, ros2_control, controller, joint_states, MoveIt, planning group, end-effector, planning scene, collision object, trajectory, OMPL, Pilz, pick-and-place, gripper, STT, Doosan robot

1. URDF와 Xacro

1.1. URDF와 Xacro 기본 구조

1.1.1. 기본 개념

URDF는 로봇의 구조를 XML 형식으로 표현하는 파일이다. URDF는 로봇의 링크와 조인트, 시각 형상, 충돌 형상, 관성 정보를 정의한다. Xacro는 URDF를 더 효율적으로 작성하기 위한 매크로 언어다. 반복되는 구조를 줄이고 parameter를 사용해 모델을 관리하기 쉽게 만든다.

1.1.2. Link와 Joint

URDF에서 link는 로봇의 강체 부분을 의미하고, joint는 link와 link를 연결하는 관절을 의미한다. Joint에는 fixed, revolute, continuous, prismatic 같은 종류가 있다. Revolute joint는 제한된 범위 안에서 회전하고, prismatic joint는 직선 방향으로 이동한다.

Joint에는 parent link와 child link가 있으며, origin과 axis, limit 정보를 가진다. 이 관계가 로봇의 운동학적 구조를 만든다.

1.1.3. Visual, Collision, Inertial

URDF에서 visual, collision, inertial은 서로 다른 목적을 가진다. Visual은 RViz나 시뮬레이터에서 사람이 보는 모양이다. Collision은 충돌 검사에 사용하는 형상이다. Inertial은 물리 시뮬레이션에서 질량과 관성 계산에 사용된다.

이 세 정보를 구분하지 않고 visual만 잘 만들면 로봇이 화면에는 보이지만 planning이나 simulation에서는 문제가 생길 수 있다.

1.1.4. MoveIt과 Gazebo에서의 의미

MoveIt은 링크, 조인트, joint limit, collision geometry를 이용해 planning scene과 collision checking을 구성한다. Gazebo는 collision과 inertial 정보를 이용해 물리 시뮬레이션을 수행한다. RViz는 visual과 TF를 바탕으로 로봇 상태를 시각화한다.

하나의 URDF가 여러 도구의 공통 기반이 되는 것이다.

1.1.5. Xacro의 필요성

로봇 모델이 커질수록 같은 형태의 링크나 joint가 반복된다. 매번 XML을 복사하면 관리가 어렵다. Xacro를 사용하면 길이, 질량, 색상, joint limit 같은 값을 변수로 두고 재사용할 수 있다.

1.1.6. 실습에서 봐야 하는 부분

URDF/Xacro 실습에서는 링크 이름과 조인트 이름을 단순히 훑는 것이 아니라, parent-child 관계가 어떻게 로봇 tree를 만드는지 봐야 한다. Base link에서 시작해 각 link가 joint로 연결되고, end-effector까지 이어지는 구조가 로봇의 운동학적 체인이 된다.

또한 origin과 axis가 중요하다. Origin은 child link가 parent link 기준 어디에 놓이는지 정의하고, axis는 revolute/prismatic joint가 어느 방향으로 움직이는지 정의한다. Axis가 틀리면 로봇 관절이 의도와 다른 방향으로 움직인다.

1.1.7. MoveIt/Gazebo 오류와의 연결

URDF 오류는 뒤 단계에서 다른 문제처럼 나타날 수 있다. Collision geometry가 너무 크면 MoveIt planning이 계속 실패할 수 있고, inertial 정보가 부정확하면 Gazebo에서 로봇이 불안정하게 움직일 수 있다. Joint limit이 잘못되면 planning이 비현실적인 자세를 만들 수 있다.

따라서 URDF/Xacro는 단순 모델 파일이 아니라 로봇 시스템 전체의 공통 계약이다. 이 계약이 정확해야 RViz 시각화, Gazebo 물리 시뮬레이션, MoveIt motion planning이 모두 정상적으로 이어진다.

1.1.8. URDF/Xacro 실습 정리

  • 실습에서 확인할 점

URDF/Xacro 실습에서 이해해야 하는 핵심은 로봇 모델링이 단순 3D 모델링이 아니라는 점이다. 로봇의 운동학, 충돌 검사, 물리 시뮬레이션, 시각화가 모두 모델 파일에 의존한다.

  • URDF는 로봇의 시각 모델만이 아니다

ROS/6차시/my_robot.urdf.xacro, move_urdf/src/urdf_r2d2/urdf/r2d2.urdf.xacro, gazebo_ws/src/simple_arm_description/urdf/simple_arm.urdf.xacro는 URDF/Xacro가 로봇의 외형을 그리는 파일에 그치지 않는다는 점을 보여 준다. URDF에는 link, joint, parent-child 관계, joint axis, limit, visual, collision, inertial 정보가 들어간다. 이 정보는 RViz 시각화, TF tree, Gazebo physics, MoveIt collision checking, controller joint name 연결에 모두 사용된다.

  • Link와 Joint를 읽는 법

URDF를 읽을 때는 link 목록을 먼저 외우는 것보다 tree 구조를 봐야 한다. 어떤 link가 base이고, 어떤 joint가 parent link와 child link를 연결하며, joint type이 fixed인지 revolute인지 continuous인지 확인한다. Revolute joint라면 axis와 limit이 중요하다. Axis가 틀리면 로봇이 의도와 다른 방향으로 회전하고, limit이 틀리면 MoveIt과 Gazebo가 허용 범위를 다르게 이해할 수 있다.

  • Xacro를 쓰는 이유

Xacro는 반복되는 URDF 구조를 macro와 property로 줄이기 위해 쓴다. simple_arm_core.urdf.xacro, simple_arm_gazebo.ros2_control.xacro, simple_arm_moveit.urdf.xacro처럼 기능별 xacro를 나누면 같은 로봇의 기본 구조, Gazebo 제어 설정, MoveIt용 설정을 조합할 수 있다. 로봇이 커질수록 link와 joint를 순수 URDF로만 작성하면 중복이 많아지고 수정 실수가 늘어난다.

  • Gazebo와 MoveIt용 모델이 갈라지는 지점

Gazebo에서는 mass, inertia, collision, ros2_control plugin 같은 물리와 제어 정보가 중요하다. MoveIt에서는 planning group, collision geometry, end-effector link, joint limit, kinematics 설정이 중요하다. 같은 로봇 모델을 기반으로 하더라도 시뮬레이션과 planning이 요구하는 정보가 조금 다르기 때문에 xacro 파일을 기능별로 나누어 조립한다.

  • Doosan과 OnRobot 모델의 의미

doosan-robot2/urdf/m0609_isaac_sim.urdf, m0609.urdf, m0609.white.urdf, m0609.blue.urdf는 같은 M0609 로봇이라도 사용 환경에 따라 모델 파일이 달라질 수 있음을 보여 준다. Isaac Sim용 URDF는 USD 변환과 simulation import를 고려해야 하고, 색상이나 mesh 경로가 다른 모델은 시각화 목적이 다를 수 있다.

OnRobot RG2 관련 xacro는 그리퍼가 로봇 arm과 별도 장치이지만 end-effector로 조립되어야 한다는 점을 보여 준다. Gripper 모델에는 손가락 link, gripper joint, collision geometry, tool frame이 들어가며, pick-and-place에서는 이 frame이 실제 grasp pose와 직접 연결된다.

  • 실습에서 확인해야 할 것

URDF/Xacro 실습을 볼 때는 “모델이 보인다”에서 멈추면 안 된다. RViz에서 TF tree가 끊기지 않는지, Gazebo에서 물리적으로 안정적인지, MoveIt에서 collision checking이 합리적으로 되는지, controller 설정의 joint name과 URDF joint name이 일치하는지 확인해야 한다. 로봇 모델 파일은 이후 모든 실습의 기준 좌표와 구조를 정하는 출발점이다.

  • 한 줄 정리

URDF/Xacro는 로봇의 몸을 ROS2, MoveIt, Gazebo, RViz가 함께 이해할 수 있는 형식으로 정의한다.

1.2. RViz

1.2.1. 기본 개념

RViz는 ROS2 시스템에서 로봇 상태, TF, sensor data, planning 결과를 시각화하는 도구다. RViz는 물리 시뮬레이터가 아니라 시각화 도구다. 이 차이를 이해해야 Gazebo와 RViz의 역할을 구분할 수 있다.

1.2.2. 시각화 대상

RViz에서 자주 확인하는 것은 robot model, TF tree, joint state, point cloud, camera image, marker, planning scene, trajectory다. 로봇 모델이 제대로 보이지 않으면 URDF나 TF 문제일 수 있다. 센서 데이터가 엉뚱한 위치에 나타나면 frame 설정이 잘못되었을 가능성이 높다.

MoveIt planning 결과를 RViz에서 보면 계획된 경로가 장애물을 피하는지, end-effector가 목표 pose로 가는지 확인할 수 있다.

1.2.3. 디버깅 도구로서의 의미

RViz는 디버깅 도구로도 중요하다. 로봇이 움직이지 않을 때 controller 문제인지, planning 문제인지, TF 문제인지, joint state 문제인지 구분하려면 시각화가 필요하다.

TF frame이 끊겨 있으면 perception 결과가 표시되지 않을 수 있고, joint state가 publish되지 않으면 로봇 모델이 움직이지 않는다.

1.2.4. MoveIt에서의 RViz

MoveIt을 사용할 때 RViz는 planning 결과를 확인하는 중심 도구가 된다. 사용자는 RViz에서 목표 pose를 지정하고, planning된 trajectory를 시각적으로 확인할 수 있다. 이때 보이는 경로는 단순 애니메이션이 아니라 MoveIt이 planning scene과 collision checking을 바탕으로 계산한 결과다.

RViz에서 로봇이 장애물을 통과하는 것처럼 보인다면 planning scene이나 collision object 설정을 확인해야 한다. End-effector 목표가 이상한 방향을 향한다면 TF나 pose orientation 문제가 있을 수 있다. RobotModel이 움직이지 않는다면 joint state가 publish되고 있는지 확인해야 한다.

1.2.5. Perception 결과 시각화

RViz는 CV 결과를 로봇 시스템 안에서 확인하는 데도 사용할 수 있다. Detection 결과는 marker나 bounding box로 표시할 수 있고, segmentation이나 depth 기반 obstacle은 point cloud나 marker array로 표현할 수 있다. 이렇게 하면 perception node가 publish한 결과가 실제 로봇 좌표계에서 어디에 놓이는지 확인할 수 있다.

따라서 RViz는 단순히 “로봇을 보는 화면”이 아니라 ROS2 topic, TF, robot state, planning result, perception output이 서로 맞는지 확인하는 통합 디버깅 도구다.

1.2.6. RViz 실습 정리

  • 실습에서 확인할 점

RViz 실습에서 중요한 것은 화면을 보는 것이 아니라 “무엇을 시각화하고 있는지”를 아는 것이다. RobotModel display는 URDF와 joint state를 기반으로 하고, TF display는 frame 관계를 보여 준다. Marker는 detection 결과나 목표 위치를 표시할 수 있다. MoveIt plugin은 planning scene과 trajectory를 시각화한다.

  • Display launch와 Gazebo launch의 차이

simple_arm_description/launch/display.launch.py 또는 display_launch.py는 robot description을 RViz에 띄우고 joint state와 TF를 통해 로봇 모델이 어떻게 보이는지 확인하는 실행 파일이다. 반면 gazebo_spawn.launch.py는 Gazebo 물리 시뮬레이션에 로봇을 spawn한다. RViz는 물리를 계산하지 않고, ROS topic과 TF를 시각화한다.

  • RViz에서 RobotModel이 보이려면 필요한 것

RobotModel display는 robot_description에 들어간 URDF와 /joint_states를 함께 사용한다. URDF만 있으면 fixed pose의 모델은 보일 수 있지만, joint state가 없으면 현재 관절 위치가 반영되지 않는다. TF display는 frame 사이 변환이 끊기지 않았는지 보여 준다. 따라서 RViz에서 모델이 이상하게 보이면 URDF, joint state broadcaster, robot_state_publisher, TF frame 이름을 함께 확인해야 한다.

  • JointState publish와 spin_once 코드

move_urdf/src/urdf_r2d2/urdf_r2d2/state_publisher.py는 RViz에서 움직이는 로봇 모델이 어떻게 만들어지는지 보여 준다. 핵심은 /joint_states에 해당하는 JointState message를 publish하고, 동시에 TF transform을 broadcast하는 것이다.

self.joint_pub = self.create_publisher(JointState, 'joint_states', qos_profile)
self.broadcaster = TransformBroadcaster(self, qos=qos_profile)
while rclpy.ok():
    rclpy.spin_once(self)

    now = self.get_clock().now()
    joint_state.header.stamp = now.to_msg()
    joint_state.name = ['swivel', 'tilt', 'periscope']
    joint_state.position = [swivel, tilt, height]

    self.joint_pub.publish(joint_state)
    self.broadcaster.sendTransform(odom_trans)
    loop_rate.sleep()

여기서 joint_state.name은 URDF에 정의된 joint 이름과 맞아야 하고, joint_state.position은 각 joint의 현재 값을 담는다. RViz의 RobotModel은 이 값을 받아 관절 자세를 갱신한다. rclpy.spin_once(self)는 반복문 안에서 한 번씩 callback 처리를 허용하는 코드다. 이 예제에는 subscription callback이 많지 않더라도, ROS2 node가 timer, parameter, shutdown signal 같은 event를 처리할 수 있게 만드는 실행 지점이라는 의미가 있다.

  • MoveIt RViz plugin이 보여 주는 것

simple_arm_moveit/launch/moveit_rviz.launch.py, demo.launch.py는 MoveIt 설정과 RViz를 함께 띄우는 구조다. MoveIt RViz plugin은 planning group, goal state, planning scene, collision object, planned trajectory를 시각화한다. 여기서 보이는 trajectory는 실제 실행 결과가 아니라 planner가 계산한 경로다. Gazebo나 실제 로봇에서 움직임을 확인하려면 controller 실행까지 연결되어야 한다.

  • RViz는 디버깅 도구다

RViz 실습에서 중요한 것은 화면이 예쁜지보다 어떤 topic과 frame을 보고 있는지다. Detection result는 marker나 image overlay로 볼 수 있고, depth point는 point cloud로 볼 수 있으며, MoveIt 목표 pose와 collision object는 planning scene으로 볼 수 있다. 로봇-CV 통합에서 RViz는 perception 결과가 robot frame 기준으로 맞게 변환되었는지 확인하는 핵심 디버깅 도구다.

  • 한 줄 정리

RViz는 ROS2 시스템의 상태와 좌표계, planning 결과를 눈으로 확인하게 해 주는 시각화 도구다.

1.3. Gazebo와 ros2_control

1.3.1. Gazebo

  • 기본 개념

Gazebo는 로봇을 실제 하드웨어 없이 물리 환경 안에서 실험할 수 있게 해 주는 시뮬레이터다. Gazebo를 사용하면 로봇 모델, 지면, 물체, 중력, 충돌, 마찰, 센서, controller를 함께 구성하고 실제 로봇에 적용하기 전에 알고리즘을 검증할 수 있다.

  • RViz와의 차이

Gazebo를 단순한 3D 뷰어로 생각하면 핵심을 놓친다. RViz는 로봇 상태와 센서 데이터를 시각화하는 데 강하지만, 물리 시뮬레이션을 직접 수행하는 도구는 아니다. Gazebo는 로봇이 물리 환경에서 어떻게 움직이는지 계산한다.

따라서 Gazebo에서는 collision geometry, mass, inertia, friction, controller 설정이 모두 중요하다.

  • 실행 흐름

Gazebo에서 로봇을 움직이려면 URDF 모델만으로는 부족하다. 먼저 URDF/Xacro로 로봇 모델을 정의하고, Gazebo에서 모델을 spawn한다. 그 다음 controller manager와 controller를 실행하고, ROS2 topic이나 action으로 명령을 보내 joint가 움직이는지 확인한다.

이 흐름이 맞아야 “시뮬레이션 안에서 로봇이 제어된다”고 말할 수 있다.

  • Gazebo에서 확인해야 하는 요소

Gazebo 실습에서는 로봇이 화면에 보이는지만 확인하면 부족하다. 먼저 로봇 모델이 정상적으로 spawn되는지 확인해야 한다. 링크와 조인트가 URDF에서 의도한 구조대로 배치되어야 하고, joint limit이 실제 움직임과 맞아야 한다. 다음으로 물리 속성이 중요하다. Mass, inertia, collision geometry가 부정확하면 로봇이 비현실적으로 흔들리거나, 충돌 판정이 이상해지거나, controller 명령을 줘도 예상과 다른 움직임이 나온다.

또한 Gazebo에서는 controller와의 연결을 봐야 한다. 로봇 모델이 있어도 controller가 joint command를 전달하지 못하면 움직이지 않는다. ros2_control 설정, controller manager, joint trajectory controller, command/state interface가 맞물려야 한다. 따라서 Gazebo 실습은 “시뮬레이터 실행”이 아니라 모델, 물리, controller, ROS2 interface가 함께 맞는지 검증하는 과정이다.

  • MoveIt과의 관계

MoveIt은 경로를 계획하고, Gazebo는 그 경로가 물리 환경 안에서 어떻게 실행되는지 확인하게 한다. MoveIt에서 planning이 성공해도 Gazebo에서 controller가 제대로 설정되지 않으면 로봇은 움직이지 않을 수 있다. 반대로 Gazebo에서 로봇이 움직여도 collision geometry나 planning scene이 부정확하면 실제 planning 품질은 떨어질 수 있다.

따라서 Gazebo와 MoveIt은 역할이 다르지만 분리해서 볼 수 없다. MoveIt은 “어떻게 움직일지”를 계산하고, Gazebo는 “그 움직임이 시뮬레이션 환경에서 실제로 실행되는지”를 확인한다.

Gazebo 실습 정리

  • 실습에서 확인할 점

Gazebo 실습은 실제 로봇 제어 전에 문제를 발견하는 데 유용하다. Joint limit이 잘못되어 로봇이 비현실적으로 움직이거나, collision shape가 실제보다 커서 planning이 실패하거나, inertia가 부정확해 물리 움직임이 이상해질 수 있다.

Gazebo는 단순 결과 확인용 화면이 아니라, 로봇 모델과 controller, 물리 조건이 맞는지 검증하는 실험 환경이다.

  • Gazebo에 로봇을 spawn한다는 뜻

gazebo_ws/src/simple_arm_description/launch/gazebo_spawn.launch.py와 강의 코드의 gazebo_spawn_launch.py는 URDF/Xacro로 정의한 로봇을 Gazebo simulation world에 올리는 흐름을 보여 준다. 여기서 spawn은 단순히 화면에 3D 모델을 띄우는 것이 아니라, Gazebo가 로봇의 link, joint, collision, inertia, controller interface를 simulation entity로 인식하게 만드는 과정이다.

로봇이 Gazebo에 보이더라도 모델이 올바르다고 볼 수는 없다. 링크 parent-child 관계가 맞아야 하고, joint axis와 limit이 맞아야 하며, collision geometry와 inertia가 물리적으로 말이 되어야 한다. 시뮬레이터에서 로봇이 흔들리거나 쓰러지거나 명령과 다르게 움직이면 대개 URDF/Xacro의 물리 속성, ros2_control 설정, controller 연결 중 하나를 확인해야 한다.

  • Simple Arm 실습 코드의 흐름

simple_arm_description package에는 simple_arm.urdf.xacro, simple_arm_gazebo.ros2_control.xacro, controllers.yaml, gazebo_spawn.launch.py, display.launch.py가 함께 있다. 이 조합은 로봇 모델, Gazebo plugin/control interface, controller 설정, 실행 launch를 분리해서 관리하는 구조다.

trajectory_publisher.py/joint_trajectory_controller/joint_trajectory topic으로 JointTrajectory를 한 번 publish한다. 메시지에는 joint_namesJointTrajectoryPoint가 들어가며, point에는 목표 joint position과 time_from_start가 들어간다. 즉 Gazebo의 로봇을 움직이는 명령은 “각 joint가 언제 어느 위치에 도달해야 하는가”라는 trajectory 형태로 들어간다.

state_monitor.py, joint_monitor.py/joint_states나 controller state를 읽어 현재 로봇 상태를 확인한다. auto_stop.pyjoint3 위치가 threshold를 넘으면 /effort_controller/commands로 0 torque 명령을 보내고 종료한다. 이 코드는 Gazebo 실습이 단순 움직임 확인이 아니라 state feedback을 읽고 안전 조건을 걸어 제어 흐름을 닫는 과정임을 보여 준다.

  • Gazebo와 RViz의 역할 차이

display.launch.py는 주로 RViz에서 robot model과 joint state를 시각화하는 데 쓰이고, gazebo_spawn.launch.py는 Gazebo 물리 환경에 로봇을 올리는 데 쓰인다. RViz에서 보이는 것은 “로봇 상태를 시각화한 결과”이고, Gazebo에서 움직이는 것은 “물리와 controller가 연결된 simulation 결과”다. 따라서 RViz에서 모델이 정상이라고 해서 Gazebo 제어가 정상이라는 뜻은 아니다.

  • MoveIt과 함께 쓸 때 확인할 것

gazebo_ws/src/simple_arm_moveit/launch/gazebo_moveit.launch.py는 Gazebo와 MoveIt을 함께 띄우는 구조다. 이때 MoveIt은 trajectory를 계획하고, ros2_control controller가 trajectory를 받아 Gazebo robot joint를 움직인다. 따라서 joint name, controller name, action interface, /joint_states, TF, robot description이 모두 같은 모델을 가리켜야 한다. 하나라도 어긋나면 planning은 되는데 실행이 안 되거나, Gazebo에서 움직임이 이상하게 보일 수 있다.

  • 한 줄 정리

Gazebo는 로봇 모델과 제어 명령이 물리 환경 안에서 실제처럼 동작하는지 확인하는 시뮬레이터다.

ros2_control

  • 기본 개념

ros2_control은 ROS2에서 로봇의 하드웨어 인터페이스와 controller를 표준화하는 프레임워크다. 실제 로봇이든 Gazebo 안의 시뮬레이션 로봇이든, controller가 command를 보내고 state를 읽는 구조를 일관되게 만들기 위해 사용한다.

  • 제어 구조

로봇 제어는 크게 세 층으로 볼 수 있다. 첫째, 상위 노드는 목표 위치나 속도 같은 명령을 만든다. 둘째, controller는 그 명령을 joint command로 변환하거나 trajectory를 따라가게 한다. 셋째, hardware interface는 실제 actuator나 시뮬레이터와 데이터를 주고받는다.

ros2_control은 이 층 사이의 인터페이스를 정리한다.

  • Command Interface와 State Interface

Command interface는 controller가 로봇에 보내는 명령이다. Position command, velocity command, effort command가 있을 수 있다. State interface는 로봇에서 읽어 오는 상태다. Joint position, velocity, effort가 대표적이다.

Controller는 state를 읽고 command를 보낸다. 이 구조를 명확히 해야 실제 하드웨어와 시뮬레이터를 같은 방식으로 다룰 수 있다.

  • Gazebo와의 연결

Gazebo와 연결할 때는 시뮬레이션 로봇이 실제 하드웨어처럼 동작하도록 plugin과 ros2_control 설정이 필요하다. Controller manager는 여러 controller를 로드하고 활성화한다.

Joint trajectory controller를 사용하면 MoveIt에서 생성한 trajectory를 받아 로봇 joint가 따라가도록 만들 수 있다.

  • Controller Manager

Controller manager는 controller를 로드하고, 설정하고, 활성화하는 역할을 한다. 로봇 시스템에서는 여러 controller가 동시에 존재할 수 있다. 예를 들어 joint state broadcaster는 현재 joint 상태를 publish하고, joint trajectory controller는 목표 trajectory를 받아 joint를 움직인다.

Controller가 로드되어 있다고 해서 항상 동작하는 것은 아니다. Configure와 activate 단계가 필요할 수 있고, controller가 요구하는 interface가 robot hardware description과 맞아야 한다. Command interface와 state interface가 맞지 않으면 controller가 활성화되지 않거나 명령을 보낼 수 없다.

  • MoveIt과의 관계

MoveIt은 trajectory를 계획하지만, 실제 joint에 명령을 보내는 계층은 controller다. 따라서 MoveIt 실습에서 planning은 성공하는데 로봇이 움직이지 않는다면 controller 설정을 확인해야 한다. Joint name이 맞는지, controller type이 맞는지, action interface가 연결되어 있는지, joint state가 publish되는지 확인해야 한다.

이 점을 이해하면 로봇 제어 흐름이 더 분명해진다. MoveIt은 경로를 만들고, ros2_control은 그 경로를 로봇 하드웨어나 Gazebo 시뮬레이션에 전달하는 실행 계층을 담당한다.

ros2_control 실습 정리

  • 실습에서 확인할 점

ros2_control을 이해하면 “MoveIt이 경로를 계획했다”와 “로봇이 실제로 움직였다” 사이에 controller 계층이 있다는 점이 보인다. Planning은 목표 trajectory를 만들지만, 그 trajectory를 실행하는 것은 controller다.

  • Controller 설정은 모델과 실행을 이어 준다

simple_arm_description/config/controllers.yamlsimple_arm_moveit/config/ros2_controllers.yaml, moveit_controllers.yaml은 ros2_control이 왜 필요한지 보여 준다. URDF/Xacro는 로봇의 구조를 설명하지만, 어떤 controller가 어떤 joint를 어떤 interface로 제어할지는 별도 설정이 필요하다. 이 설정이 있어야 trajectory command가 실제 joint command로 연결된다.

simple_arm_gazebo.ros2_control.xacro는 Gazebo simulation에서 사용할 control interface를 로봇 모델에 붙이는 역할을 한다. 즉 Gazebo 속 로봇이 joint를 가진 물리 모델로만 존재하는 것이 아니라, controller manager가 접근할 수 있는 hardware interface를 가진 대상으로 등록된다.

  • Joint trajectory controller가 하는 일

trajectory_publisher.py/joint_trajectory_controller/joint_trajectoryJointTrajectory 메시지를 보낸다. 메시지의 핵심은 joint_namespoints다. joint_names가 controller 설정의 joint 이름과 맞지 않으면 controller는 목표를 해석할 수 없다. points에는 위치와 도달 시간이 들어가므로, controller는 단순 위치 명령이 아니라 시간 조건을 가진 trajectory를 따라가야 한다.

강의 코드의 send_waypoint.pywaypoint_action_follower.py는 더 명확하게 FollowJointTrajectory action을 사용한다. send_waypoint.py는 먼저 controller state topic에서 현재 joint name과 현재 position을 읽고, 그 값을 시작점으로 넣은 뒤 target position을 다음 point로 넣어 action goal을 만든다. 이렇게 현재 상태를 시작점으로 넣는 이유는 controller가 갑자기 불연속 목표를 받지 않게 하기 위해서다.

  • State feedback이 필요한 이유

joint_monitor.py, state_monitor.py, controller_state_monitor.py는 controller가 명령을 받았는지뿐 아니라 실제 상태가 어떻게 변하는지 확인하는 코드다. 로봇 제어에서는 publish가 성공했다고 제어가 성공한 것이 아니다. /joint_states나 controller state를 봐야 실제 joint position, velocity, error를 확인할 수 있다.

auto_stop.pyjoint3 위치가 기준값을 넘으면 effort command를 0으로 보내는 안전 예제다. 이 코드는 feedback을 읽지 않는 제어가 왜 위험한지 보여 준다. 실제 로봇에서는 limit, collision, emergency stop, torque/current monitoring 같은 안전 조건이 반드시 state feedback과 연결되어야 한다.

  • MoveIt과 연결될 때의 의미

MoveIt은 경로를 계획하지만 실제 실행은 controller가 담당한다. simple_arm_moveit의 controller 설정과 Gazebo의 ros2_control 설정이 맞아야 MoveIt에서 생성한 trajectory가 /follow_joint_trajectory action을 통해 controller로 들어가고, controller가 Gazebo 또는 실제 robot joint를 움직인다. 따라서 MoveIt 실행 문제를 볼 때는 planner만 볼 것이 아니라 controller manager, joint state broadcaster, joint trajectory controller, action server 이름을 함께 확인해야 한다.

  • 한 줄 정리

ros2_control은 로봇의 명령과 상태를 controller와 hardware interface로 연결하는 제어 프레임워크다.

ros2_control 핵심 코드 인용

URDF/Xacro와 Gazebo 제어의 연결은 ros2_control block에서 가장 분명하게 드러난다.

<ros2_control name="${name}" type="system">
  <hardware>
    <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  </hardware>
  <joint name="joint1_z">
    <command_interface name="position"/>
    <state_interface name="position"/>
    <state_interface name="velocity"/>
  </joint>
</ros2_control>

joint1_z는 로봇 모델의 joint 이름과 일치해야 한다. command_interface는 controller가 어떤 명령을 보낼 수 있는지, state_interface는 controller가 어떤 상태를 읽을 수 있는지 정한다. Gazebo plugin은 시뮬레이션 속 joint를 ros2_control hardware처럼 다룰 수 있게 만든다.

Controller YAML은 어떤 controller가 어떤 joint를 제어할지 정한다.

controller_manager:
  ros__parameters:
    update_rate: 100
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster
    arm_controller:
      type: joint_trajectory_controller/JointTrajectoryController

arm_controller:
  ros__parameters:
    joints: [joint1_z, joint1_y, joint2, joint3]
    command_interfaces: [position]
    state_interfaces: [position, velocity]

URDF/Xacro가 로봇의 몸과 관절 구조를 말한다면, controller 설정은 그 관절을 어떤 controller가 어떤 interface로 움직일지 말한다. MoveIt에서 planning이 성공해도 joint 이름, controller 이름, action interface가 맞지 않으면 trajectory는 실행되지 않는다. 그래서 RViz에서 모델이 보이는 것, Gazebo에서 물리가 도는 것, controller가 trajectory를 받는 것은 각각 따로 확인해야 한다.

2. MoveIt

2.1. MoveIt 기본 구조와 핵심 코드

2.1.1. 학습 목적

MoveIt은 로봇 팔의 motion planning을 위한 프레임워크다. 로봇 팔을 움직인다는 것은 단순히 목표 좌표를 주고 관절을 돌리는 일이 아니다. 로봇의 링크와 조인트 구조, 관절 제한, 현재 자세, 장애물, 자기 충돌, end-effector 목표 pose, controller 실행 가능성을 모두 고려해야 한다.

MoveIt은 이 복잡한 과정을 하나의 planning pipeline으로 묶어 준다. 따라서 MoveIt을 배운다는 것은 “로봇팔을 움직이는 명령어”를 배우는 것이 아니라, 목표 pose가 실제 실행 가능한 trajectory가 되기까지 어떤 단계가 필요한지 이해하는 것이다.

2.1.2. MoveIt의 기본 구성

MoveIt은 로봇 모델을 기반으로 동작한다. 이 모델은 URDF/Xacro에서 정의된 링크, 조인트, joint limit, collision geometry를 사용한다. MoveIt Setup Assistant나 설정 패키지를 통해 planning group, end-effector, virtual joint, controller, kinematics solver 등을 정의한다.

Planning group은 함께 움직일 joint 묶음이다. 예를 들어 manipulator arm과 gripper를 분리할 수 있다. End-effector는 목표 pose를 지정할 기준이 되는 링크다. 로봇 팔을 특정 위치로 보내려면 end-effector 기준 pose를 설정하고, MoveIt이 그 pose를 만족하는 joint trajectory를 찾는다.

2.1.3. Motion Planning

Motion planning은 시작 상태에서 목표 상태까지 충돌 없이 이동할 수 있는 경로를 찾는 과정이다. 이 과정은 단순 직선 보간이 아니다. 로봇 팔은 여러 관절을 가진 고차원 configuration space에서 움직인다. 어떤 목표 pose는 관절 구조상 도달할 수 없고, 도달 가능하더라도 중간 경로에서 장애물이나 자기 몸과 충돌할 수 있다.

MoveIt은 planning scene을 사용해 로봇과 주변 환경을 모델링한다. Planning scene 안에는 현재 로봇 상태, 장애물, collision object, attached object가 포함된다. Planner는 이 정보를 바탕으로 충돌 없는 경로를 찾는다.

2.1.4. Collision Checking

Collision checking은 MoveIt의 핵심이다. 로봇이 움직이는 동안 링크끼리 충돌하지 않는지, 주변 물체와 부딪히지 않는지 검사한다. URDF의 collision geometry가 부정확하면 collision checking도 부정확해진다. 따라서 MoveIt 실습은 URDF/Xacro 실습과 분리될 수 없다.

충돌 검사는 단순히 장애물 회피만 의미하지 않는다. 로봇 팔은 자기 링크끼리 부딪힐 수도 있다. 또한 gripper가 물체를 잡은 후에는 그 물체가 attached object가 되어 로봇과 함께 움직인다고 봐야 한다. 이때 planning scene이 바뀌고, 충돌 검사도 바뀐다.

2.1.5. Trajectory Generation과 Execution

Planner가 찾은 경로는 실제 controller가 실행할 수 있는 trajectory로 변환되어야 한다. 단순한 위치의 나열만으로는 부족하다. 각 joint가 언제 어느 위치에 있어야 하는지, 속도와 가속도 제한을 만족하는지 고려해야 한다.

Trajectory execution 단계에서는 controller가 이 trajectory를 받아 로봇을 움직인다. 여기서 ros2_control과 joint trajectory controller가 연결된다. MoveIt이 planning을 성공했더라도 controller 설정이 맞지 않으면 실제 로봇이나 Gazebo 로봇은 움직이지 않을 수 있다.

2.1.6. Gazebo와 RViz와의 연결

MoveIt 실습은 보통 RViz에서 planning 결과를 확인하고, Gazebo에서 물리 실행을 검증하는 흐름으로 이어진다. RViz는 planning scene과 trajectory를 시각화해 목표 pose와 경로가 적절한지 확인하게 해 준다. Gazebo는 로봇 모델과 controller가 물리 환경에서 실제로 동작하는지 검증한다.

MoveIt과 Gazebo를 함께 사용할 때는 robot model, controller, joint state, TF가 모두 맞아야 한다. 하나라도 어긋나면 planning은 되는데 실행이 안 되거나, RViz에서는 보이는데 Gazebo에서는 움직임이 이상한 문제가 생긴다.

2.1.7. Python으로 MoveIt 제어

수업 흐름에서 MoveIt은 설정과 시각화뿐 아니라 Python script 제어로 확장된다. Python으로 MoveIt을 제어한다는 것은 목표 pose를 코드로 지정하고, planning을 호출하고, trajectory 실행을 요청하는 흐름을 이해하는 것이다.

이 단계에서는 MoveIt이 GUI 도구가 아니라 ROS2 시스템 안에서 호출 가능한 planning component라는 점이 중요하다. 즉 perception node가 물체 위치를 계산하면, Python node가 그 위치를 목표 pose로 만들고 MoveIt planning을 요청할 수 있다.

2.1.8. Waypoint와 IK 해의 문제

MoveIt Python 제어 강의에서는 여러 waypoint를 순서대로 실행하는 구조가 등장한다. Waypoint는 로봇이 지나가야 할 중간 목표 pose다. 단순히 pose 목록을 만들고 차례대로 planning하면 로봇이 자연스럽게 움직일 것처럼 보이지만, 실제로는 관절이 크게 뒤집히거나 waypoint 사이 이동이 매끄럽지 않을 수 있다.

이 현상은 같은 end-effector pose를 만드는 관절 해가 여러 개 존재하기 때문에 발생한다. 이를 IK 해가 여러 개 존재한다고 한다. OMPL 같은 planner가 각 waypoint마다 그 순간 가능한 관절 자세 중 하나를 선택하면, 이전 waypoint에서의 관절 상태와 연속성이 깨질 수 있다. 결과적으로 end-effector 위치는 맞아도 관절이 불필요하게 크게 회전할 수 있다.

따라서 waypoint planning에서는 목표 pose만 보는 것이 아니라 관절 공간에서의 연속성, 경로의 부드러움, 이전 자세와의 변화량도 함께 봐야 한다. 이 내용은 MoveIt이 단순 pose solver가 아니라 planner pipeline을 통해 경로 품질을 다루는 시스템이라는 점을 보여 준다.

2.1.9. Planner Pipeline과 Pilz

Planner pipeline은 MoveIt에서 어떤 계열의 planner를 사용하고, planning 요청을 어떤 방식으로 처리할지 정의하는 전체 체계다. Pipeline은 큰 틀이고, planner는 그 안에서 실제 경로를 계산하는 알고리즘이다.

OMPL은 sampling 기반 planning에 강하고 복잡한 환경에서 충돌 없는 경로를 찾는 데 사용된다. 하지만 산업용 로봇에서 직선 이동, 원호 이동, point-to-point 이동처럼 예측 가능한 motion primitive가 필요한 경우 Pilz planner가 적합할 수 있다. Pilz는 LIN, CIRC, PTP 같은 산업 로봇식 motion command를 제공한다.

이 차이를 이해하면 planner 선택이 단순 옵션이 아니라 작업 성격에 따른 설계 결정이라는 점이 보인다. 장애물 회피가 복잡한 환경에서는 sampling planner가 필요할 수 있고, 공정 자동화처럼 반복적이고 정형화된 움직임에서는 Pilz의 직관적인 motion primitive가 더 적합할 수 있다.

2.1.10. 안전 workspace와 clamp

Python MoveIt 제어 강의에는 목표 좌표가 안전 범위를 벗어나면 제한 범위 안으로 잘라 넣는 clamp 구조가 등장한다. 이는 로봇이 물리적으로 위험한 공간으로 움직이지 않도록 방어하는 코드다.

목표 pose를 외부 입력이나 perception 결과에서 받을 때는 값이 항상 안전하다고 가정할 수 없다. 검출 오류, 좌표 변환 오류, 사용자 입력 실수로 로봇이 작업 공간 밖이나 충돌 위험 영역으로 이동하려 할 수 있다. 따라서 planning 전에 목표 위치를 안전 workspace로 제한하는 과정이 필요하다.

2.1.11. 로봇/CV 연결

MoveIt은 CV perception 결과와 로봇 control 사이의 다리 역할을 한다. YOLO가 물체의 bounding box를 찾거나 segmentation이 물체 영역을 찾으면, 이 결과는 depth와 TF 변환을 거쳐 로봇 기준 목표 pose가 될 수 있다. 또는 인식된 장애물을 planning scene의 collision object로 추가할 수 있다.

따라서 MoveIt은 perception 결과를 실제 로봇 행동으로 바꾸는 핵심 단계다. CV가 “무엇이 어디에 있는가”를 알려 주면, MoveIt은 “로봇이 어떻게 움직여야 하는가”를 계산한다.

2.1.12. MoveIt 실습 정리

  • 실습에서 이해할 점

MoveIt 과제와 실습을 통해 확인한 핵심은 로봇팔 제어가 여러 계층의 연결이라는 점이다. URDF/Xacro는 로봇의 몸을 정의한다. TF는 좌표계를 연결한다. MoveIt은 planning scene과 collision checking으로 경로를 만든다. ros2_control과 controller는 trajectory를 실행한다. RViz는 이 과정을 시각화하고, Gazebo는 물리 환경에서 검증한다.

MoveIt은 “로봇을 목표점으로 보내는 기능”이 아니라, 로봇이 안전하게 움직일 수 있는지 계산하고 실행 가능한 trajectory로 만드는 motion planning 시스템이다.

  • MoveItPy 코드의 기본 실행 흐름

dsr_practicemp_basic.py, mp_waypoint.py, mp_waypoint_pilz.py, mp_waypoint_pilz_lin.py는 MoveIt을 Python에서 어떻게 호출하는지 보여 준다. 공통 흐름은 MoveItPy 인스턴스를 만들고, get_planning_component("manipulator")로 planning group을 가져온 뒤, 현재 상태를 시작 상태로 설정하고, 목표 joint state나 end-effector pose를 goal로 설정하고, plan()을 호출한 다음 robot.execute()로 trajectory를 실행하는 구조다.

이 흐름에서 GROUP_NAME, BASE_FRAME, EE_LINK는 단순 상수가 아니다. GROUP_NAME은 SRDF에서 정의한 planning group과 일치해야 하고, BASE_FRAME은 목표 pose를 해석할 기준 좌표계이며, EE_LINK는 목표 pose를 맞출 end-effector link다. 이 셋이 URDF/SRDF와 맞지 않으면 planner는 목표를 제대로 해석할 수 없다.

  • 안전 작업영역 clamp가 들어가는 이유

mp_waypoint.py 계열 코드에는 SAFE_X_MIN, SAFE_Y_MIN, SAFE_Y_MAX, SAFE_Z_MINclamp_to_safe_workspace()가 반복해서 등장한다. 이 함수는 목표 pose가 작업 가능한 범위를 벗어나면 x, y, z를 안전 범위 안으로 잘라 넣는다.

이 처리가 필요한 이유는 perception 결과나 사용자 입력이 항상 안전하다고 볼 수 없기 때문이다. 카메라 좌표 변환이 틀리거나, 음성 명령이 잘못 해석되거나, waypoint 값을 잘못 넣으면 로봇이 바닥이나 자기 몸체, 작업대 밖으로 움직이려 할 수 있다. Clamp는 planning을 시작하기 전 목표 자체를 제한해 위험한 목표가 planner로 들어가는 것을 막는다.

  • Waypoint planning에서 IK 문제가 드러나는 이유

mp_waypoint.py는 여러 pose waypoint를 순서대로 넣어 planning하고 실행한다. 겉으로는 end-effector가 점들을 따라가면 되는 문제처럼 보이지만, 실제로는 각 pose마다 여러 inverse kinematics 해가 존재할 수 있다. 같은 tool pose라도 팔꿈치 방향이나 wrist posture가 달라질 수 있기 때문이다.

OMPL 기반 planner가 각 waypoint에서 가능한 해 중 하나를 선택하면, 이전 waypoint의 joint posture와 다음 waypoint의 joint posture가 매끄럽게 이어진다는 보장이 없다. 그래서 end-effector 위치는 맞지만 joint가 갑자기 크게 돌아가거나, waypoint 사이 움직임이 산업용 로봇 동작처럼 예측 가능하지 않을 수 있다. 이 문제가 강의에서 waypoint와 IK를 따로 다룬 이유다.

  • OMPL과 Pilz를 나누어 쓰는 이유

mp_waypoint_pilz.py, mp_waypoint_pilz_lin.pyPlanRequestParameters를 사용해 planning pipeline과 planner id를 명시한다. HOME 이동에는 omplRRTConnect를 쓰고, waypoint 이동에는 pilz_industrial_motion_plannerPTP 또는 LIN을 사용한다.

OMPL은 복잡한 configuration space에서 충돌 없는 경로를 찾는 데 강하지만, 산업용 로봇에서 기대하는 직선 이동이나 point-to-point primitive를 항상 보장하는 방식은 아니다. Pilz는 PTP, LIN, CIRC 같은 산업용 motion primitive를 제공하므로 공정 동작처럼 예측 가능한 이동을 만들 때 적합하다. LIN은 tool center point가 직선에 가깝게 움직이게 하고, PTP는 joint 또는 point-to-point 이동에 초점을 둔다.

  • Collision object를 추가한다는 뜻

collision_obstacle.pymoveit_msgs/CollisionObject/collision_object topic으로 publish해 planning scene에 box 장애물을 추가한다. 코드에서는 SolidPrimitive.BOX로 크기를 정하고, Pose로 위치를 정한 뒤 CollisionObject.ADD operation을 설정한다.

여기서 중요한 점은 장애물을 단순 시각화 marker로 그리는 것이 아니라 planner가 실제로 피해야 할 collision object로 등록한다는 것이다. Planning scene에 들어간 장애물은 이후 경로 계산에서 충돌 검사 대상이 된다. 따라서 장애물의 frame_id, 크기, 위치가 정확해야 한다. frame이 틀리면 장애물이 엉뚱한 곳에 놓이고, 크기가 틀리면 안전 여유가 과하거나 부족해진다.

  • FollowJointTrajectory action과 MoveIt 실행

강의 코드의 send_waypoint.py, waypoint_action_follower.py는 controller와 통신할 때 control_msgs/action/FollowJointTrajectory action을 사용한다. send_waypoint.py는 먼저 /arm_controller/state 같은 controller state topic에서 현재 joint 이름과 위치를 읽고, 현재 위치를 trajectory의 첫 point로 넣은 뒤 target position을 다음 point로 넣는다.

이 구조는 controller 실행이 단순 publish가 아니라 action goal이라는 점을 보여 준다. Goal에는 전체 trajectory가 들어가고, controller는 이를 실행한 뒤 result status를 돌려준다. waypoint_action_follower.py는 여러 waypoint를 시간 간격으로 구성해 action server에 보내고, 실행이 끝나면 hold time 뒤 반복한다. MoveIt에서 생성된 trajectory도 결국 controller가 이해하는 trajectory/action 형태로 내려가야 실제 로봇이나 Gazebo가 움직인다.

  • Pick and Place 코드에서 MoveIt이 맡는 부분

pick_and_place.py, gear_assembly.py는 MoveIt이 작업 전체를 혼자 해결하지 않는다는 점을 보여 준다. MoveIt은 pick pose, approach pose, retreat pose, place pose로 이동하는 motion planning과 execution을 담당한다. 반면 gripper 개폐는 onrobot.py의 RG2 제어 코드가 담당한다.

Pick sequence는 보통 위에서 접근하고, pick 높이까지 내려가고, gripper를 닫고, 다시 위로 올라가는 순서다. Place sequence는 place 위치 위로 이동하고, 내려가고, gripper를 열고, 후퇴한다. 이 순서를 나누는 이유는 물체와 충돌하지 않고 안정적으로 접근하기 위해서다. 단일 목표 pose로 바로 이동하면 주변 물체나 작업대와 충돌할 수 있고, grasp 전후의 안전 높이를 보장하기 어렵다.

  • 실습 코드에서 MoveIt을 읽는 기준

MoveIt 실습 코드는 plan() 한 줄을 보는 것이 아니라 계획 이전과 이후를 함께 봐야 한다. 이전에는 URDF/SRDF, planning group, frame, goal pose, 안전 범위, planning scene이 있고, 이후에는 trajectory execution, controller action, gripper 동작, Gazebo/RViz 검증이 있다. 이 전체 흐름을 연결해야 MoveIt이 “목표 pose를 안전한 로봇 동작으로 바꾸는 계층”이라는 의미가 분명해진다.

  • 한 줄 정리

MoveIt은 로봇 모델, planning scene, collision checking, trajectory generation, controller execution을 연결해 목표 pose를 안전한 로봇 움직임으로 바꾸는 motion planning 프레임워크다.

2.1.13. MoveIt 실습 코드 흐름

MoveItPy 코드: planning pipeline과 planner 선택

MoveItPy 실습의 기본 흐름은 robot 객체, planning component, planning parameter, goal, plan, execute가 순서대로 연결되는 구조다.

robot = MoveItPy(node_name="moveit_py")
arm = robot.get_planning_component(GROUP_NAME)

home_params = PlanRequestParameters(robot)
home_params.planning_pipeline = "ompl"
home_params.planner_id = "RRTConnect"

pilz_params = PlanRequestParameters(robot)
pilz_params.planning_pipeline = "pilz_industrial_motion_planner"
pilz_params.planner_id = "LIN"

GROUP_NAME은 SRDF의 planning group과 맞아야 하고, planning_pipelineplanner_id는 어떤 방식으로 경로를 만들지 정한다. OMPL은 충돌 없는 경로 탐색에 강하고, Pilz의 LIN은 tool center point가 직선에 가깝게 움직이는 산업용 motion primitive에 가깝다.

planning_component.set_start_state_to_current_state()
planning_component.set_goal_state(pose_stamped_msg=pose_goal, pose_link=EE_LINK)
plan_result = planning_component.plan(parameters=plan_parameters)
robot.execute(group_name=GROUP_NAME, robot_trajectory=plan_result.trajectory, blocking=True)

이 코드는 MoveIt의 네 단계를 그대로 보여 준다. 현재 robot state를 시작점으로 잡고, end-effector pose를 goal로 주고, planner가 trajectory를 만든 뒤, controller 실행 계층으로 trajectory를 넘긴다. Planning과 execution이 분리되어 있기 때문에 plan이 성공해도 controller 설정이 틀리면 실제 로봇은 움직이지 않는다.

Planning Scene 코드: collision object 등록

Planning scene에 장애물을 넣는 코드는 시각화가 아니라 collision checking 대상을 등록하는 작업이다.

pub = node.create_publisher(CollisionObject, "/collision_object", qos)

primitive = SolidPrimitive()
primitive.type = SolidPrimitive.BOX
primitive.dimensions = list(size_xyz)

box.primitives.append(primitive)
box.primitive_poses.append(pose)
box.operation = CollisionObject.ADD
pub.publish(box)

SolidPrimitive.BOXdimensions는 장애물의 모양과 크기, pose는 위치, CollisionObject.ADD는 planning scene에 추가한다는 명령이다. 이 object가 정확해야 MoveIt이 실제 장애물을 피해 경로를 계획한다. YOLO나 segmentation으로 얻은 물체 위치를 collision object로 넣을 때도 frame과 크기가 틀리면 planner가 잘못된 세계를 기준으로 판단한다.

Planning scene 파이프라인: 장애물 정보를 어디로 날리고 누가 쓰는가

collision_obstacle.py의 장애물 등록 흐름은 collision_obstacle node -> /collision_object topic -> MoveIt planning scene -> collision checking -> trajectory planning으로 이어진다. 이때 publisher는 단순히 화면에 박스를 그리는 것이 아니라, MoveIt이 경로를 계산할 때 피해야 할 세계 모델을 보낸다.

qos = QoSProfile(
    history=HistoryPolicy.KEEP_LAST,
    depth=1,
    reliability=ReliabilityPolicy.RELIABLE,
    durability=DurabilityPolicy.TRANSIENT_LOCAL,
)
pub = node.create_publisher(CollisionObject, "/collision_object", qos)
box = CollisionObject()
box.id = object_id
box.header.frame_id = frame_id

primitive = SolidPrimitive()
primitive.type = SolidPrimitive.BOX
primitive.dimensions = list(size_xyz)

pose = Pose()
pose.position.x, pose.position.y, pose.position.z = pos_xyz
pose.orientation.w = 1.0

box.primitives.append(primitive)
box.primitive_poses.append(pose)
box.operation = CollisionObject.ADD

pub.publish(box)

여기서 frame_id는 장애물 위치가 어떤 좌표계를 기준으로 해석되는지 정한다. 예를 들어 base_link 기준으로 보낸 box는 robot base에서 본 장애물이다. Perception 결과를 camera frame에서 얻었다면, 먼저 TF나 hand-eye calibration으로 base frame으로 변환한 뒤 CollisionObject를 만들어야 한다. 그렇지 않으면 MoveIt은 실제 장애물이 아닌 엉뚱한 위치의 장애물을 기준으로 경로를 피한다.

QoS를 TRANSIENT_LOCAL로 둔 것도 파이프라인 관점에서 중요하다. /collision_object를 publish한 순간 MoveIt 쪽 subscriber가 늦게 붙으면 일반 volatile topic에서는 메시지를 놓칠 수 있다. 하지만 collision object는 “한순간의 로그”가 아니라 planning scene 상태에 가깝다. 그래서 마지막 장애물 메시지를 보존해 late subscriber도 받을 수 있게 하는 설정이 필요하다.

Pick and Place 코드: 접근, 집기, 후퇴의 순서

Pick-and-place 코드는 motion planning과 gripper 제어가 분리된다는 점을 보여 준다.

gripper = RG(GRIPPER_NAME, TOOLCHARGER_IP, TOOLCHARGER_PORT)
gripper.move_gripper(GRIPPER_OPEN_WIDTH, GRIPPER_FORCE)

pose_goal.pose.position.z = pos["z"] + APPROACH_OFFSET
plan_and_execute(robot, arm, logger, pose_goal=pose_goal, plan_parameters=pilz_params)

pose_goal.pose.position.z = pos["z"]
plan_and_execute(robot, arm, logger, pose_goal=pose_goal, plan_parameters=pilz_params)

gripper.move_gripper(GRIPPER_CLOSE_WIDTH, GRIPPER_FORCE)

MoveIt은 approach pose와 grasp pose까지의 robot arm trajectory를 만들고 실행한다. Gripper는 별도 TCP/Modbus 계층으로 열린다. 접근 높이를 둔 뒤 내려가서 잡는 이유는 목표 pose 하나로 바로 이동할 때 생길 수 있는 작업대 충돌, 물체 측면 충돌, 불안정한 grasp를 줄이기 위해서다.

STT 코드: 음성을 제한된 로봇 명령으로 바꾸는 구조

STT 제어는 음성을 바로 robot motion으로 넣지 않고 topic과 parser를 거쳐 제한된 명령으로 바꾼다.

self._pub = self.create_publisher(String, "/stt_result", 10)
text = recognizer.recognize_google(audio, language=self._lang)
msg = String()
msg.data = text
self._pub.publish(msg)
normalized = text.lower().replace(" ", "")
for kw, cmd in KEYWORD_MAP.items():
    if kw in normalized:
        return cmd

첫 번째 코드는 음성을 ROS2 message로 바꾸는 입력 계층이고, 두 번째 코드는 자유로운 문장을 제한된 robot command로 줄이는 안전 계층이다. 이 분리가 있어야 음성 인식 오류가 곧바로 위험한 pose 목표가 되지 않는다.

2.2. Planning Scene과 Collision

2.2.1. Planning Scene과 Collision Checking 상세

  • 학습 목적

MoveIt은 실제 세계를 직접 보는 것이 아니라, 내부에 구성된 planning scene을 기준으로 경로를 계획한다. 실제 환경에 장애물이 있어도 planning scene에 반영되지 않으면 MoveIt은 그 장애물을 모른다. 반대로 실제로는 없는 장애물이 planning scene에 남아 있으면 MoveIt은 불필요하게 경로를 피하거나 planning에 실패할 수 있다.

따라서 planning scene은 단순 배경 정보가 아니라 MoveIt이 의사결정을 내리는 세계 모델이다. 로봇이 안전하게 움직이려면 이 세계 모델이 실제 환경과 최대한 일치해야 한다.

  • 기본 개념

Planning scene은 MoveIt이 motion planning을 수행하기 위해 사용하는 가상 세계다. 여기에는 로봇의 현재 상태, 주변 장애물, collision object, attached object, joint state, frame 정보가 포함된다. 로봇이 안전하게 움직이려면 planning scene이 실제 환경을 충분히 반영해야 한다.

  • Collision Checking

Collision checking은 로봇이 움직이는 동안 자기 자신이나 주변 물체와 충돌하지 않는지 확인하는 과정이다. 로봇 팔은 여러 링크로 이루어져 있기 때문에 자기 충돌도 고려해야 한다. 팔꿈치 링크가 몸체와 부딪히거나, gripper가 팔의 다른 링크와 충돌할 수 있다.

외부 장애물과의 충돌도 planning scene에 있는 collision object를 통해 검사한다.

충돌 검사는 planning의 안전성을 결정한다. 목표 pose가 도달 가능하더라도 중간 경로에서 장애물과 충돌하면 실행할 수 없는 경로다. 또한 목표 지점 주변에서 gripper가 물체나 테이블과 부딪힐 수 있다. 따라서 motion planning은 단순히 시작점과 목표점을 잇는 문제가 아니라, 전체 이동 과정에서 충돌이 없는지 확인하는 문제다.

  • Collision Geometry

Collision geometry는 visual geometry와 다를 수 있다. Visual geometry는 사람이 보기 좋은 자세한 모델일 수 있지만, collision geometry는 계산을 위해 단순화된 박스, 실린더, sphere, convex mesh로 표현할 수 있다.

너무 복잡한 collision geometry는 계산 비용을 늘리고, 너무 부정확한 geometry는 잘못된 planning 결과를 낳는다.

  • Perception과의 연결

Planning scene이 중요한 이유는 perception 결과와 직접 연결될 수 있기 때문이다. 카메라나 depth sensor로 장애물을 인식하면, 그 결과를 planning scene에 collision object로 추가할 수 있다. 그러면 MoveIt은 그 장애물을 피하는 경로를 계획한다.

예를 들어 YOLO나 segmentation으로 작업 대상 주변의 물체를 인식하고, depth 정보를 이용해 3D 위치를 추정하면, 그 물체를 planning scene에 추가할 수 있다. 이때 인식 결과가 부정확하면 collision object 위치도 틀어지고, MoveIt은 잘못된 세계를 기준으로 경로를 계획한다.

따라서 perception과 planning scene 사이에는 좌표 변환과 신뢰도 검증이 필요하다. 카메라 frame에서 얻은 물체 위치를 robot base frame으로 변환하고, 물체 크기와 shape를 적절한 collision geometry로 표현해야 한다.

Planning Scene과 Collision 실습 정리

  • 이해해야 할 점

Planning scene은 눈에 보이는 배경이 아니라 MoveIt이 실제로 알고 있는 세계다. 실제 환경에 장애물이 있어도 planning scene에 없으면 MoveIt은 그 장애물을 피하지 않는다. 반대로 실제로는 없는 장애물이 planning scene에 남아 있으면 MoveIt은 불필요하게 경로를 피하거나 planning에 실패할 수 있다.

Collision checking은 로봇 시스템에서 안전과 직결된다. 실습에서는 planning이 성공했는지뿐 아니라, 왜 특정 경로가 거부되었는지, 어떤 collision object가 영향을 주었는지 확인하는 습관이 필요하다.

  • CollisionObject 메시지의 구조

dsr_practice/dsr_practice/collision_obstacle.pymoveit_msgs/msg/CollisionObject를 만들어 planning scene에 box 장애물을 추가한다. 메시지에는 id, header.frame_id, primitive shape, primitive pose, operation이 들어간다. SolidPrimitive.BOXdimensions는 장애물의 크기를 정하고, Pose는 장애물이 기준 frame에서 어디에 있는지 정한다.

  • frame_id가 중요한 이유

Collision object의 위치는 항상 frame_id 기준으로 해석된다. 코드에서 BASE_FRAME = "base_link"를 쓰면 box 위치는 robot base 기준 좌표다. 만약 camera frame에서 얻은 좌표를 변환하지 않고 base frame 장애물로 넣으면 장애물이 실제와 다른 곳에 생긴다. 그러면 planner가 엉뚱한 곳을 피하거나 실제 장애물을 무시하게 된다.

  • QoS를 TRANSIENT_LOCAL로 두는 이유

장애물 publisher는 ReliabilityPolicy.RELIABLE, DurabilityPolicy.TRANSIENT_LOCAL, depth 1 QoS를 사용한다. Planning scene을 구독하는 쪽이 약간 늦게 붙더라도 마지막 collision object 메시지를 받을 수 있게 하기 위한 설정이다. 장애물 정보는 순간 로그가 아니라 planning scene 상태에 가까우므로, late subscriber가 놓치면 안 된다.

  • 경유 waypoint가 생기는 이유

collision_obstacle.py는 기본 waypoint 사이에 detour point를 넣어 장애물을 피하는 경로를 만든다. 이 실습에서 중요한 것은 장애물을 추가하면 planner가 알아서 항상 이상적인 경로를 만든다고 믿는 것이 아니다. 장애물 위치, planner 종류, planning time, 목표 pose, 안전 높이에 따라 planning 성공 여부와 경로 품질이 달라진다. 필요하면 사람이 작업 sequence를 안전한 중간 목표로 나누어야 한다.

  • Pick-and-place와 연결되는 의미

Pick-and-place에서 물체를 집은 뒤에는 그 물체가 robot에 붙은 attached object처럼 취급될 수 있다. 이때 planning scene은 단순히 주변 장애물 목록이 아니라, 로봇이 현재 무엇을 들고 있는지까지 반영해야 한다. 그래서 collision 관리는 MoveIt의 부가 기능이 아니라 안전한 manipulation을 위한 핵심 상태 관리다.

  • 한 줄 정리

Planning scene은 MoveIt이 알고 있는 세계이고, collision checking은 그 세계 안에서 안전한 경로를 찾기 위한 검사 과정이다.

2.3. 두산 로봇, Pick and Place, STT

2.3.1. 두산 로봇과 실제 로봇 제어 기반

  • 왜 두산 로봇 기초를 따로 정리하는가

ROS2, MoveIt, Gazebo를 이해해도 실제 로봇을 다룰 때는 로봇 제조사에서 제공하는 구조와 제어 방식, 안전 모드, 네트워크 연결, bringup 절차를 알아야 한다. 두산 로봇 자료는 이론적인 ROS2 시스템이 실제 협동로봇과 만날 때 어떤 준비가 필요한지 보여 준다.

두산 로봇 수업 흐름은 단순히 로봇 팔을 움직이는 것에서 끝나지 않는다. 실제 하드웨어와 연결하려면 로봇 모델, 제어 모드, IP/host 설정, bringup launch, 로봇 상태 확인, 직접 교시 모드와 원격 제어 모드 전환 같은 절차가 함께 필요하다.

  • 실제 로봇과 시뮬레이션의 차이

시뮬레이션에서는 로봇 모델과 controller가 맞으면 비교적 쉽게 움직임을 확인할 수 있다. 하지만 실제 로봇은 안전 상태, 모드 전환, 네트워크 연결, 비상정지 상태, 충돌 감지, 속도 제한 같은 조건을 만족해야 한다.

따라서 실제 로봇 제어에서는 명령을 보내기 전에 로봇이 어떤 모드에 있는지, 원격 명령을 받을 수 있는지, bringup이 정상적으로 되었는지 확인해야 한다. 이 구조를 이해하지 못하면 MoveIt이나 ROS2 node가 정상이어도 실제 로봇이 움직이지 않을 수 있다.

  • Bringup의 의미

Bringup은 로봇 하드웨어나 시뮬레이션 로봇을 ROS2 시스템 안으로 올리는 과정이다. 이 과정에서 robot description, controller, driver, joint state, TF, RViz 등이 함께 실행될 수 있다. 두산 로봇 bringup은 실제 m0609 같은 모델을 ROS2에서 제어 가능한 대상으로 만들기 위한 시작점이다.

Bringup launch에는 mode, model, host 같은 인자가 들어갈 수 있다. mode는 real/simulation 여부, model은 사용할 로봇 모델, host는 실제 로봇 제어기의 IP 주소와 연결된다. 이 설정은 단순 옵션이 아니라 로봇 시스템이 어떤 대상과 통신할지 결정한다.

  • 로봇 모드와 서비스 호출

실제 로봇에서는 직접 교시 모드와 원격 제어 모드가 구분된다. 수업 자료에서는 로봇 모드 전환을 service call로 수행하는 흐름이 등장한다. 이 구조는 ROS2 service가 실제 로봇 상태 변경에도 사용될 수 있음을 보여 준다.

로봇 모드를 service로 바꾼다는 것은, 로봇 제어도 결국 ROS2 interface를 통해 추상화될 수 있다는 뜻이다. 단, 실제 하드웨어에서는 안전 상태와 권한이 함께 작동하므로 service call이 성공했다고 해서 무조건 로봇을 움직일 수 있는 것은 아니다.

  • MoveIt과 두산 로봇의 연결

MoveIt에서 계획한 trajectory를 실제 두산 로봇에 실행하려면 로봇 description, planning group, controller, driver 연결이 모두 맞아야 한다. 시뮬레이션에서 성공한 planning을 실제 로봇에 적용할 때는 속도, 가속도, workspace limit, 충돌 위험을 더 보수적으로 봐야 한다.

두산 로봇 자료는 MoveIt이 단순 시뮬레이션 도구가 아니라 실제 협동로봇 제어로 이어질 수 있다는 점을 보여 준다. 하지만 그 연결에는 bringup, mode 설정, controller, safety 확인이 함께 필요하다.

Fallback 코드: 실행 환경이 달라져도 디버그 정보를 잃지 않는 구조

두산 bringup 계층의 dsr_bringup2/utils.py에는 package가 source 실행인지 install 실행인지에 따라 git root를 찾는 fallback 코드가 들어 있다.

if git_root is None:
    git_root = find_git_root_for_package_symlink_install(here)
if git_root is None:
    for parent in here.parents:
        if parent.name == "install":
            ws_root = parent.parent
            git_root = find_any_git_in_src(ws_root)
            break

if git_root is None:
    return {
        "commit": "unknown",
        "branch": "unknown",
        "user": "unknown",
        "email": "unknown",
    }

이 코드는 로봇 제어 알고리즘 자체는 아니지만, 실습과 디버깅에서 중요하다. ROS2 package는 source tree에서 실행될 수도 있고, colcon build 뒤 install space에서 실행될 수도 있다. 실행 위치가 달라져도 commit, branch 같은 정보를 최대한 찾고, 끝내 찾지 못하면 "unknown"으로 안전하게 떨어진다. 즉 fallback은 “아무 값이나 넣는 임시 처리”가 아니라, 실행 환경 차이 때문에 node가 깨지지 않도록 하고 디버그 정보를 가능한 범위에서 보존하는 구조다.

설정 파일 읽기에서도 같은 방식이 쓰인다.

if not os.path.exists(yaml_path):
    print(f"[dsr_controller2] YAML file not found: {yaml_path}")
    return 100

try:
    ...
    return update_rate
except Exception as e:
    print(f"[dsr_controller2] Failed to read YAML: {e}")
    return 100

update_rate YAML을 읽지 못해도 기본값 100으로 돌아가게 한 것은 controller 설정을 읽는 과정에서 예외가 전체 bringup 실패로 번지는 것을 줄이기 위한 방어 코드다. 실제 로봇 시스템에서는 file path, package share 경로, install 방식이 자주 달라지므로 이런 fallback이 있어야 실습 환경 차이를 견딜 수 있다.

  • 한 줄 정리

두산 로봇 기초는 ROS2와 MoveIt으로 만든 제어 흐름을 실제 협동로봇에 연결하기 위한 하드웨어 bringup, 모드 전환, 안전 조건, controller 연결을 다룬다.

Pick and Place 작업 sequence

  • 학습 목적

Pick and place는 로봇 manipulation의 대표 작업이다. 로봇이 물체를 인식하고, 접근하고, gripper로 잡고, 목표 위치로 옮긴 뒤 놓는 전체 흐름을 포함한다. 이 작업은 perception, planning, control, gripper, collision checking이 모두 연결되어야 성립한다.

수업 후반의 Isaac Sim과 MoveIt 자료는 pick and place를 통해 로봇팔 시스템의 전체 연결 구조를 보여 준다.

  • 작업 단계

Pick and place는 보통 다음 단계로 나뉜다.

  1. 물체 위치를 파악한다.
  2. Pre-grasp pose를 정한다.
  3. End-effector가 물체에 접근한다.
  4. Gripper를 닫아 물체를 잡는다.
  5. 물체를 들어 올린다.
  6. 목표 위치로 이동한다.
  7. Gripper를 열어 물체를 놓는다.
  8. 로봇을 안전한 자세로 되돌린다.

각 단계는 단순 위치 이동이 아니라 collision checking과 trajectory execution을 포함한다.

  • Gripper 장착과 URDF 변환

Isaac Sim pick-and-place 자료에서는 M0609 로봇에 OnRobot RG2 gripper를 장착하는 흐름이 등장한다. Xacro 파일을 URDF로 변환하고, mesh와 urdf 폴더를 정리한 뒤 Isaac Sim에서 import한다.

Gripper를 로봇에 붙이려면 단순히 모델을 화면에 배치하는 것으로는 부족하다. Gripper base link와 robot end-effector link의 연결 관계가 맞아야 하고, 물리적으로 함께 움직이도록 assembly나 joint 관계가 설정되어야 한다.

  • RMPFlow와 Controller

Isaac Sim에서는 manipulator 제어에 RMPFlow 같은 controller 설정이 사용될 수 있다. RMPFlow는 로봇의 motion policy를 계산해 end-effector 목표를 따라가도록 만드는 방식이다. 이때 robot description, joint limit, collision sphere, controller yaml 설정이 함께 사용된다.

Pick-and-place script에서는 물체 위치와 목표 위치를 task로 정의하고, controller가 매 step마다 로봇 action을 계산한다. 따라서 코드는 단순 순차 명령이 아니라 world, task, controller, robot, gripper가 반복 step 안에서 상호작용하는 구조다.

  • Perception과의 연결

초기 실습에서는 물체 위치를 코드로 정해둘 수 있지만, 실제 로봇에서는 CV perception이 물체 위치를 제공해야 한다. YOLO나 segmentation으로 물체를 찾고, depth와 TF를 통해 3D pose를 계산하면 pick target이 된다.

따라서 pick and place는 CV와 로봇 제어가 만나는 대표 예시다. 인식 결과가 부정확하면 접근 pose가 틀어지고, collision scene이 부정확하면 이동 중 충돌이 발생할 수 있다.

Pick and Place 실습 정리

  • Pick and Place는 하나의 pose 이동이 아니다

dsr_practice/dsr_practice/pick_and_place.pygear_assembly.py는 pick-and-place가 단순히 pick pose와 place pose 두 점을 잇는 문제가 아님을 보여 준다. 실제 sequence는 home 이동, approach pose 이동, pick 높이 하강, gripper close, retreat, place approach, place 하강, gripper open, retreat로 나뉜다.

이렇게 나누는 이유는 충돌과 grasp 안정성 때문이다. 물체 바로 옆으로 직선 이동하면 작업대나 주변 물체와 부딪힐 수 있고, 위에서 접근하지 않으면 gripper finger가 물체를 밀어낼 수 있다. Approach offset은 물체 위 안전 높이에서 자세를 맞춘 뒤 수직으로 내려가게 해 grasp 실패 가능성을 줄인다.

  • MoveIt과 gripper의 책임 분리

Pick-and-place 코드에서 MoveItPy는 robot arm trajectory를 계획하고 실행한다. 반면 OnRobot RG2 gripper는 onrobot.pyRG class가 Modbus/TCP 방식으로 제어한다. 코드에는 GRIPPER_OPEN_WIDTH, GRIPPER_CLOSE_WIDTH, GRIPPER_FORCE 같은 값이 따로 있으며, pick 순간에는 gripper를 닫고 place 순간에는 연다.

이 분리가 중요한 이유는 arm motion planning과 end-effector actuation이 서로 다른 문제이기 때문이다. MoveIt은 gripper가 어느 위치로 이동해야 하는지 계산하지만, 실제 손가락을 몇 mm 열고 어떤 힘으로 닫을지는 gripper driver가 처리한다. 둘을 task sequence에서 정확한 순서로 묶어야 실제 조립 작업이 된다.

  • Pilz를 사용하는 이유

Pick-and-place 코드에서는 PlanRequestParameters로 HOME 이동에는 OMPL/RRTConnect를 사용하고, 작업 pose 이동에는 Pilz PTP를 사용한다. 반복 공정에서는 “어딘가 충돌 없이 가기”보다 “예측 가능한 방식으로 접근하고 후퇴하기”가 더 중요할 때가 많다. 그래서 산업용 motion primitive를 제공하는 Pilz가 pick/place 구간에 적합하다.

  • Gear assembly로 확장되는 구조

gear_assembly.py는 gear pick pose와 place pose를 task dictionary로 정의하고, 같은 planning helper와 gripper 제어를 사용해 조립 sequence를 수행한다. 여기서 task dictionary는 perception 결과로 대체될 수 있는 부분이다. 초기 실습에서는 pose가 코드에 고정되어 있지만, 실제 시스템에서는 YOLO/segmentation/depth/TF를 거쳐 계산된 pose가 이 자리에 들어간다.

  • CV와 연결되는 지점

Pick target을 사람이 코드에 넣으면 로봇은 정해진 위치의 물체만 집을 수 있다. CV가 연결되면 image에서 물체를 찾고, depth로 3D 위치를 계산하고, hand-eye calibration과 TF로 robot base 기준 pose를 만든다. 그 pose가 MoveIt의 pick pose가 되고, gripper 동작과 결합해 실제 pick-and-place가 된다. 따라서 이 문서는 MoveIt, depth camera calibration, object detection, segmentation과 함께 읽어야 한다.

  • 한 줄 정리

Pick and place는 물체 인식, 접근 pose, gripper 제어, motion planning, trajectory execution이 연결되어야 완성되는 로봇 manipulation 작업이다.

STT 로봇 연동 구조

  • 왜 STT를 로봇과 연결하는가

STT는 Speech-to-Text의 약자로, 사람의 음성을 텍스트로 변환하는 기술이다. 로봇 제어에 STT를 연결하면 사용자가 키보드나 코드 대신 음성 명령으로 로봇에게 작업을 지시할 수 있다. 이는 인간-로봇 상호작용의 기본 형태 중 하나다.

수업 흐름에서는 STT node가 음성을 인식해 텍스트 topic을 publish하고, 다른 node가 이 텍스트를 subscribe해 로봇 명령으로 변환하는 구조를 다룬다. 이 구조는 ROS2 topic이 센서 데이터뿐 아니라 사용자 입력 이벤트에도 사용될 수 있음을 보여 준다.

  • STT Node 구조

STT node는 마이크 입력을 받고, Google Web Speech API 같은 음성 인식 엔진을 통해 텍스트로 변환한다. 변환된 텍스트는 /stt_result 같은 topic으로 publish된다. 이 topic을 다른 node가 subscribe하면 음성 명령을 로봇 제어 로직으로 연결할 수 있다.

STT node는 언어 설정, 마이크 장치, 소음 임계값, energy threshold 같은 parameter를 가져야 한다. 환경 소음이 크면 음성 감지가 잘못될 수 있으므로 시작 시 주변 소음을 측정해 threshold를 보정하는 구조가 사용된다.

  • 텍스트를 로봇 명령으로 바꾸는 방식

음성 인식 결과는 사람이 말한 자연어 문장이다. 로봇은 이 문장을 그대로 이해하지 못하므로, 명령어로 정규화하는 단계가 필요하다. 예를 들어 “왼쪽으로 가”, “left”, “왼쪽” 같은 표현을 모두 left 명령으로 매핑할 수 있다.

이 과정에서는 띄어쓰기 제거, 대소문자 통일, 키워드 검색 같은 전처리가 사용된다. 인식 결과가 완벽하지 않을 수 있으므로, 명령 매핑은 가능한 입력 변형을 견딜 수 있게 설계해야 한다.

  • MoveIt과 STT Pick and Place

STT 결과를 로봇 팔 제어와 연결하면 음성 명령으로 home, left, right 같은 pose 이동을 수행할 수 있다. 더 나아가 pick and place 작업도 음성 명령으로 시작할 수 있다.

이 구조에서는 STT node가 perception이나 planning을 직접 수행하지 않는다. STT node는 사용자 의도를 텍스트로 바꾸고, robot control node가 그 텍스트를 명령으로 해석해 MoveIt planning이나 controller 실행을 호출한다. 역할이 분리되어야 음성 인식과 로봇 제어를 독립적으로 수정할 수 있다.

STT 로봇 제어 실습 정리

  • STT node는 음성을 topic으로 바꾼다

dsr_practice/dsr_practice/stt_node.py는 microphone 입력을 받아 Google Web Speech API로 텍스트를 인식하고, 결과를 /stt_result topic에 std_msgs/String으로 publish한다. Parameter로 language, device_index, energy_threshold, pause_threshold, phrase_time_limit, dynamic_energy, ambient_duration를 선언한다.

이 parameter들이 필요한 이유는 음성 인식이 실행 환경에 민감하기 때문이다. 마이크 장치 번호가 다를 수 있고, 주변 소음에 따라 energy threshold가 달라질 수 있으며, 발화가 끝났다고 판단하는 pause threshold도 상황에 맞게 조절해야 한다. STT node는 로봇 제어를 직접 하지 않고, “음성 입력을 ROS2 text topic으로 변환하는 책임”만 가진다.

  • Robot control node는 text를 명령으로 해석한다

stt_robot_control.py/stt_result를 subscribe하고, 들어온 문자열을 정규화한 뒤 keyword map으로 home, left, right, forward, backward, up, down, stop 같은 명령으로 바꾼다. 이 node는 MoveItPy를 사용해 현재 end-effector pose를 기준으로 5cm offset 이동을 만들거나, home joint constraint로 복귀 동작을 계획한다.

self.create_subscription(String, "/stt_result", self._stt_cb, 10)

def _stt_cb(self, msg: String):
    cmd = _text_to_cmd(msg.data)
    if cmd is None:
        self.get_logger().debug(f"매핑 없음: '{msg.data}'")
        return

    if cmd == "stop":
        ...
    else:
        self._cmd_q.put(cmd)

이 subscription callback은 음성 텍스트를 곧바로 motion으로 실행하지 않고, 먼저 제한된 명령으로 변환한 뒤 queue에 넣는다. cmd is None이면 실행하지 않고 돌아가므로, 인식된 문장이 keyword map에 없을 때 로봇이 임의로 움직이지 않는다.

executor = MultiThreadedExecutor()
executor.add_node(node)

try:
    executor.spin()
except KeyboardInterrupt:
    pass
finally:
    node.destroy_node()
    robot.shutdown()
    rclpy.shutdown()

STT 제어 node도 executor.spin()이 있어야 /stt_result subscription callback이 계속 처리된다. MoveIt planning과 음성 callback, worker thread가 함께 있으므로 실행 루프를 명확히 두어야 node가 음성 입력을 기다리고, 들어온 명령을 queue에 넣고, robot motion으로 이어 갈 수 있다.

여기서 중요한 점은 STT 결과를 바로 로봇 명령으로 쓰지 않는다는 것이다. 음성 텍스트는 오인식될 수 있으므로 keyword map, valid command set, stop 처리, 안전 workspace clamp가 필요하다. 특히 이동 명령은 SAFE_X_MIN, SAFE_Y_MIN, SAFE_Y_MAX, SAFE_Z_MIN으로 제한되어 위험한 목표가 MoveIt에 들어가지 않도록 한다.

  • 음성 명령과 MoveIt planning의 연결

stt_robot_control.py는 음성 명령을 해석한 뒤 MoveIt planning component에 goal을 넣고 planning/execution을 수행한다. left/right/up/down 같은 명령은 절대 좌표 목표라기보다 현재 tool pose에서 offset을 더하는 jog command에 가깝다. 따라서 이 코드는 자연어 전체를 이해하는 시스템이 아니라, 제한된 명령어를 안전한 robot motion command로 변환하는 interface다.

  • STT Pick and Place의 의미

stt_pick_and_place.py와 관련 launch 파일은 음성 인식 결과를 pick-and-place sequence의 trigger로 확장한다. 이 구조에서는 STT node가 perception이나 motion planning을 대신하지 않는다. STT는 사용자 의도를 text event로 바꾸고, task node가 그 event를 받아 MoveIt, gripper, safety logic을 조합한다.

  • 실습에서 확인해야 할 것

STT 로봇 제어 실습은 음성 인식 정확도만 보는 과제가 아니다. 음성이 ROS2 topic으로 들어오고, command parser가 제한된 명령으로 해석하며, MoveIt이 안전 범위 안에서 계획하고, launch가 STT node와 robot control node를 함께 실행하는 전체 구조를 봐야 한다. 이렇게 나누어야 음성 UI가 로봇 제어의 위험한 직접 입력이 아니라 검증된 task trigger가 된다.

  • 한 줄 정리

STT-로봇 연동은 음성 입력을 ROS2 topic으로 전달하고, 텍스트 명령을 로봇 pose 이동이나 pick-and-place 작업으로 변환하는 인간-로봇 상호작용 구조다.

profile
AI 공부합니다

0개의 댓글