1. 개요
우분투 운영체제 환경 isaacsim 6.0.0에서 ros2를 사용하여 로봇 암(ur10e gripper)을 제어 하고자 한다.

2. 제어 UI 프로그램 (terminal #1)
첫번째 터미널에서 다음과 같이 입력하고 아래 코드를 입력한다.
> source /opt/ros/humble/setup.bash
> python external_ui_controller.py
# external_ui_controller.py
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
import tkinter as tk
class RobotUIController(Node):
def __init__(self):
super().__init__('robot_ui_controller')
# /joint_commands 라는 토픽으로 JointState 메시지를 발행
self.publisher_ = self.create_publisher(JointState, '/joint_commands', 10)
self.dof_names = [
'shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint',
'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint', 'finger_joint'
]
self.num_dofs = len(self.dof_names)
self.dof_targets = [0.0] * self.num_dofs
self.setup_ui()
def setup_ui(self):
self.root = tk.Tk()
self.root.title("ROS 2 Robot Joint Controller")
self.root.geometry("450x350")
self.labels = []
for i in range(self.num_dofs):
frame = tk.Frame(self.root)
frame.pack(pady=5)
# Joint Name
tk.Label(frame, text=f"[{i}] {self.dof_names[i]}", width=25, anchor='w').pack(side=tk.LEFT)
# Value Label
val_lbl = tk.Label(frame, text="0.00", width=5)
val_lbl.pack(side=tk.LEFT, padx=10)
self.labels.append(val_lbl)
# Buttons
tk.Button(frame, text="Up (+0.1)", command=lambda idx=i: self.update_joint(idx, 0.1)).pack(side=tk.LEFT,
padx=2)
tk.Button(frame, text="Down (-0.1)", command=lambda idx=i: self.update_joint(idx, -0.1)).pack(side=tk.LEFT,
padx=2)
def update_joint(self, idx, delta):
self.dof_targets[idx] += delta
# Limit 관절 가동 범위
if 0 <= idx <= 5:
self.dof_targets[idx] = max(-3.14, min(3.14, self.dof_targets[idx]))
elif idx == 6:
self.dof_targets[idx] = max(0.0, min(0.7, self.dof_targets[idx]))
self.dof_targets[idx] = round(self.dof_targets[idx], 2)
self.labels[idx].config(text=f"{self.dof_targets[idx]:.2f}")
# ROS 2 메시지 발행
self.publish_state()
def publish_state(self):
msg = JointState()
msg.name = self.dof_names
msg.position = self.dof_targets
self.publisher_.publish(msg)
self.get_logger().info(f'Published commands: {self.dof_targets}')
def main(args=None):
rclpy.init(args=args)
controller = RobotUIController()
# Tkinter 메인 루프와 ROS 2 스핀을 함께 처리
while rclpy.ok():
rclpy.spin_once(controller, timeout_sec=0.01)
controller.root.update()
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
다음과 같은 GUI환경에서 로봇을 제어하는 ROS2 메시지를 보낸다.

3. IsaacSim 프로그램( terminal #2)
> export ROS_DISTRO=humble
> export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
아래 명령어는 IsaacSim의 설치 위치에 따라 LD_LIBRARY_PATH를 올바르게 설정하여야 한다.
> export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
아래 명령어는 IsaacSim의 설치 위치에 따라 LD_LIBRARY_PATH를 올바르게 설정하여야 한다.
> export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/lee/isaacsim/exts/isaacsim.ros2.core/humble/lib
> ~/isaacsim/python.sh ros2based_robot_control.py #<-- 파이썬은 isaacsim/python.sh를 사용한다.
# ros2based_robot_control.py
import argparse
from isaacsim import SimulationApp
parser = argparse.ArgumentParser()
parser.add_argument("--test", action="store_true")
parser.add_argument("--headless", action="store_true")
args, _ = parser.parse_known_args()
# 1. 시뮬레이션 앱 생성 (순정 상태)
simulation_app = SimulationApp({"headless": args.headless, "hide_ui": False})
# 2. 최신 버전에 맞춘 ROS 2 Bridge 익스텐션 활성화
from isaacsim.core.experimental.utils.app import enable_extension
enable_extension("isaacsim.ros2.bridge") # <-- omni.isaac.ros2_bridge에서 변경
# 3. 익스텐션 적용 및 환경 변수 로드를 위한 시뮬레이터 틱
simulation_app.update()
# 4. 이제 정상적으로 rclpy 임포트 가능
import rclpy
from rclpy.node import Node
# 확인용 출력
print("ROS 2 Bridge 및 rclpy 로드 성공!")
from sensor_msgs.msg import JointState
# ----------------------------------------------------------------------
# Isaac Sim imports
# ----------------------------------------------------------------------
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
import omni.kit.app
from isaacsim.core.experimental.objects import DomeLight, GroundPlane
from isaacsim.core.experimental.prims import Articulation
from isaacsim.core.simulation_manager import SimulationManager
from isaacsim.core.utils.viewports import set_camera_view
from isaacsim.storage.native import get_assets_root_path_async
_HOLD_STEPS: int = 120
# ----------------------------------------------------------------------
# ROS2 Subscriber
# ----------------------------------------------------------------------
class RobotJointSubscriber(Node):
"""Receive JointState commands from ROS2"""
def __init__(self):
super().__init__("isaac_sim_robot_subscriber")
self.subscription = self.create_subscription(
JointState,
"/joint_commands",
self.listener_callback,
10,
)
self.latest_positions = None
self.get_logger().info(
"ROS2 Subscriber initialized. Listening to '/joint_commands'..."
)
def listener_callback(self, msg):
self.latest_positions = list(msg.position)
# ----------------------------------------------------------------------
# Scene Setup
# ----------------------------------------------------------------------
async def setup_scene() -> Articulation:
assets_root_path = await get_assets_root_path_async()
stage_utils.add_reference_to_stage(
usd_path=(
assets_root_path
+ "/Isaac/Samples/Rigging/Manipulator/"
"configure_manipulator/ur10e/ur/ur_gripper.usd"
),
path="/World/ur",
)
GroundPlane("/World/GroundPlane")
DomeLight("/World/DomeLight").set_intensities(1000)
await omni.kit.app.get_app().next_update_async()
set_camera_view(
eye=[1.5, 1.5, 1.0],
target=[0.0, 0.0, 0.5],
camera_prim_path="/OmniverseKit_Persp",
)
robot = Articulation("/World/ur")
await omni.kit.app.get_app().next_update_async()
return robot
# ----------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------
def main(args: argparse.Namespace, app: SimulationApp) -> None:
SimulationManager.setup_simulation(dt=1.0 / 60.0)
robot = app.run_coroutine(setup_scene())
app.update()
if args.headless:
print(
"Headless mode: simulation is paused. "
"Press Play in the livestream UI to begin."
)
while app.is_running() and not app_utils.is_playing():
app.update()
else:
app_utils.play()
app.update()
print(f"Robot DOFs: {robot.dof_names}")
# ------------------------------------------------------------------
# ROS2 Initialization
# ------------------------------------------------------------------
rclpy.init()
subscriber_node = RobotJointSubscriber()
num_dofs = 7
dof_indices = list(range(num_dofs))
frame_count = 0
# ------------------------------------------------------------------
# Main Simulation Loop
# ------------------------------------------------------------------
while app.is_running():
app.update()
# Process ROS2 callbacks
rclpy.spin_once(
subscriber_node,
timeout_sec=0.0,
)
# Apply incoming joint positions
if subscriber_node.latest_positions is not None:
target_positions = subscriber_node.latest_positions[:num_dofs]
robot.set_dof_position_targets(
target_positions,
dof_indices=dof_indices,
)
frame_count += 1
if args.test and frame_count >= (_HOLD_STEPS * 2):
break
# ------------------------------------------------------------------
# Cleanup
# ------------------------------------------------------------------
subscriber_node.destroy_node()
rclpy.shutdown()
# ----------------------------------------------------------------------
# Entry Point
# ----------------------------------------------------------------------
if __name__ == "__main__":
try:
main(args, simulation_app)
except KeyboardInterrupt:
print("\nExiting...")
except Exception:
import traceback
traceback.print_exc()
finally:
simulation_app.close()

Asset은 6.0.0 버전으로 다운로드를 받았으며 파일 용량이 커서 별도 외장하드에 저장하고 링크로 연결하였다.
독립적인 GUI 프로그램에서 로봇을 제어하는 ROS2 메시지를 isaacsim에 보내고 로봇을 제어할 수 있다.
반응형