[Project#] 시리즈는 프로젝트(Trash Throwing Simulation with Deep Reinforcement Learning) 제작 과정을 설명한다. 본 포스팅은 RL Environment 코드 분석에 대한 내용이다.

모든 내용은 이전 글과 이어진다. 이전 글을 오류 없이 모두 진행했다고 가정하고 글을 작성하였다. (이전 글 LINK)

1. my_environment.launch.py

  • launch 파일은 여러 노드를 한 번에 실행시키기 위한 파일이다. ROS 1에서는 XML 기반이었지만 ROS 2에서는 Python 방식을 추가하였다.
  • my_environment.launch.py 파일은 my_environment_pkg 패키지 안에 있는 런치 파일이며, 강화 학습을 적용하기 위한 환경을 생성한다.
  • 전체 코드의 일부분을 가져와서 설명하는 방식으로 작성하였다.

1.1. 전체 코드

import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.actions import ExecuteProcess

def generate_launch_description():

	my_sphere_files       = get_package_share_directory('my_sphere_pkg')
	my_doosan_robot_files = get_package_share_directory('my_doosan_pkg')
	my_environmets_files  = get_package_share_directory('my_environment_pkg')
 
	# Start doosan robot and controller
	doosan_robot = IncludeLaunchDescription(PythonLaunchDescriptionSource(my_doosan_robot_files + '/launch/my_doosan_controller.launch.py')) 
	
	# Start sphere mark
	sphere_mark  = IncludeLaunchDescription(PythonLaunchDescriptionSource(my_sphere_files + '/launch/my_sphere.launch.py')) 

	# Start Rviz
	rviz_file = my_environmets_files + "/rviz/my_rviz_env.rviz"
	rviz_node = Node( package='rviz2',
									  executable='rviz2',
									  name='rviz2',
									  output='log',
									  arguments=['-d', rviz_file])

	# Start Gazebo   
	world_file_name = 'my_world.world'
	world = os.path.join(get_package_share_directory('my_environment_pkg'), 'worlds', world_file_name)
	gazebo_node = ExecuteProcess(cmd=['gazebo', '--verbose', world,'-s', 'libgazebo_ros_factory.so'], output='screen')

	# Node
	ld = LaunchDescription()

	ld.add_action (doosan_robot)
	ld.add_action (sphere_mark)
	ld.add_action (gazebo_node)
	ld.add_action (rviz_node)

	return ld

1.2. 모듈 가져오기

import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.actions import ExecuteProcess
  • os: Operation System의 약자로, 운영체제에서 제공하는 여러 기능을 파이썬에서 수행할 수 있게 해준다. 런치 파일에선 주로 파일 경로를 확인하기 위해 사용한다.
  • LaunchDescription: 어떤 노드를 실행시킬지 기술해둔 Description을 작성한다. 다만 특정한 규칙으로 실행시킨 Node에 대한 옵션을 지정해주어야 하며, 여러 Node들의 실행이 필요할 수 있기에, 기본적으로 List의 구조를 가진다.
  • Node: 노드 클래스를 의미한다.
  • get_package_share_directory: ROS2에서 지정된 패키지의 share 디렉토리에 대한 경로를 얻기 위해 사용되는 함수이다. 이 디렉토리에는 일반적으로 구성 파일, 시작 파일 및 데이터 파일과 같은 여러 패키지 간에 공유되는 실행 불가능한 파일이 포함되어 있다.
  • IncludeLaunchDescription: 다른 launch 파일 안에 launch 파일을 포함하는 데 사용된다.
  • PythonLaunchDescriptionSource: Python launch 파일의 위치를 지정하는 데 사용된다.
  • ExecuteProcess **: 프로세스 하나를 실행시킨다는 구분의 개념으로 생각하면 좋다.

1.3. generate_launch_description 함수

  • ros2 launch 커멘드를 통해 launch file을 실행시키면, 이 이름을 가진 함수를 찾아들어갑니다. 그래서 모든 launch file에는 빠지지 않고 제일 먼저 등장하는 함수이다.
  1. share 폴더 경로 얻기

     my_sphere_files       = get_package_share_directory('my_sphere_pkg')
     my_doosan_robot_files = get_package_share_directory('my_doosan_pkg')
     my_environmets_files  = get_package_share_directory('my_environment_pkg')
    
    • 빌드 시 install 폴더 안에 각 패키지에 대한 폴더가 생기며, 그 안에 share 폴더가 있다. get_package_share_directory 함수는 인자로 받은 패키지의 폴더 경로를 가져온다.
    • 각 변수의 정확한 값(경로)은 아래와 같다.
     > my_sphere_files: /home/dndqodqks/ros2_ws/install/my_sphere_pkg/share/my_sphere_pkg
     > my_doosan_robot_files: /home/dndqodqks/ros2_ws/install/my_doosan_pkg/share/my_doosan_pkg
     > my_environmets_files: /home/dndqodqks/ros2_ws/install/my_environment_pkg/share/my_environment_pkg
    
  2. 런치 파일 가져오기

     # Start doosan robot and controller
     doosan_robot = IncludeLaunchDescription(PythonLaunchDescriptionSource(my_doosan_robot_files + '/launch/my_doosan_controller.launch.py'))
        
     # Start sphere mark
     sphere_mark  = IncludeLaunchDescription(PythonLaunchDescriptionSource(my_sphere_files + '/launch/my_sphere.launch.py'))
    
    • PythonLaunchDescriptionSource와 IncludeLaunchDescription 함수를 활용하여 런치 파일에 대한 객체를 선언할 수 있다.
  3. Rviz 노드 생성하기

     # Start Rviz
     rviz_file = my_environmets_files + "/rviz/my_rviz_env.rviz"
     rviz_node = Node( package='rviz2',
     								  executable='rviz2',
     								  name='rviz2',
     								  output='log',
     								  arguments=['-d', rviz_file])
    
    • rviz 파일의 주소를 저장한 후 rviz 노드를 생성한다.
    • Node 객체의 각 파라미터의 의미는 아래와 같다.
      • package: 실행시킬 Node가 포함된 package를 선택한다.
      • executable: 노드가 시작될 때 실행될 실행 파일의 이름을 지정한다.
      • name: 이 매개변수는 시작할 노드의 이름을 지정한다. ROS2 시스템의 각 노드는 고유한 이름을 가져야 한다. 이 경우 노드의 이름도 rviz2이다.
      • output: 이 매개변수는 노드가 시작될 때 콘솔에 표시될 출력 유형을 지정한다. 이 매개변수의 옵션은 log(로그 메시지만 표시), screen(모든 출력을 콘솔에 표시) 및 both(로그 메시지와 출력을 모두 콘솔에 표시)이다. 이 경우 출력 유형은 log입니다.
      • arguments: 런치 파일이 노드로 시작될 때 런치 파일에 전달되어야 하는 추가 인수를 지정한다. 이 경우 -d 인수는 rviz2 구성 파일의 경로와 함께 rviz2 런치 파일에 전달되어 rviz2에 3D 시각화를 표시하는 방법을 알려준다.
  4. 가제보 노드 생성하기

     # Start Gazebo   
     world_file_name = 'my_world.world'
     world = os.path.join(get_package_share_directory('my_environment_pkg'), 'worlds', world_file_name)
     gazebo_node = ExecuteProcess(cmd=['gazebo', '--verbose', world,'-s', 'libgazebo_ros_factory.so'], output='screen')
    
    • os.path.join 함수를 활용하여 .world 파일의 주소를 생성한다.
    • .world 파일의 주소는 아래와 같다.

        /home/dndqodqks/ros2_ws/install/my_environment_pkg/share/my_environment_pkg/worlds/my_world.world
      
    • ExecuteProces를 사용하여 gazebo_node를 생성한다. ExecuteProces는 터미널창에 입력하는 명령어라고 생각하면 편하다. 각각의 내용은 다음과 같다.
      • --verbose: 상세 출력을 활성화한다.
      • world: .world 파일의 경로를 의미하며, 해당 파일을 바탕으로 시뮬레이터의 배경을 생성한다.
      • -s: 로드 되어야 하는 플러그인을 지정한다. 위 코드에서는 libgazebo_ros_factory.so가 필요한 플러그인인다.
      • output: gazebo 명령의 출력이 터미널에 표시되도록 한다.
  5. LaunchDescription 객체 반환

     # Node
     ld = LaunchDescription()
        
     ld.add_action (doosan_robot)
     ld.add_action (sphere_mark)
     ld.add_action (gazebo_node)
     ld.add_action (rviz_node)
        
     return ld
    
    • LaunchDescription 객체를 선언하고 add_action 함수를 활용하여 실행하고자 하는 노드를 추가한다. 이후 객체를 리턴하여 추가된 노드를 실행한다.
    • LaunchDescription 객체에 실행하고자 하는 노드를 추가하는 다른 방법도 있다. LaunchDescription을 선언할 때 인자로 노드를 넣어주는 것이다. 이때 리스트의 형태로 입력해야 한다. 예시는 아래와 같다. 어떤 방식이 ROS2에 더 어울리는지 모르겠다.

        return LaunchDescription([doosan_robot, sphere_mark, gazebo_node, rviz_node])
      

2. my_doosan_controller.launch.py

  • my_doosan_controller.launch.py은 my_doosan_pkg 안에 있는 런치 파일이다.

2.1. 전체 코드

import os
from launch_ros.actions import Node
from launch import LaunchDescription
from launch.substitutions import Command
from launch.actions import ExecuteProcess
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():

	#robot model to option m1013 or a0912 
	
	robot_model = 'a0912'
	#robot_model = 'm1013'

	xacro_file = get_package_share_directory('my_doosan_pkg') + '/description'+'/xacro/'+ robot_model +'.urdf.xacro'
	
	# Robot State Publisher 
	robot_state_publisher = Node(package    ='robot_state_publisher',
								 executable ='robot_state_publisher',
								 name       ='robot_state_publisher',
								 output     ='both',
								 parameters =[{'robot_description': Command(['xacro', ' ', xacro_file])           
								}])

	# Spawn the robot in Gazebo
	spawn_entity_robot = Node(package     ='gazebo_ros', 
							  executable  ='spawn_entity.py', 
							  arguments   = ['-entity', 'my_doosan_robot', '-topic', 'robot_description'],
							  output      ='screen')

	# load and START the controllers in launch file	
	load_joint_state_broadcaster = ExecuteProcess(
										cmd=['ros2', 'control', 'load_controller', '--set-state', 'start','joint_state_broadcaster'],
										output='screen')
	
	load_joint_trajectory_controller = ExecuteProcess( 
										cmd=['ros2', 'control', 'load_controller', '--set-state', 'start', 'joint_trajectory_controller'], 
										output='screen')

	return LaunchDescription([robot_state_publisher, spawn_entity_robot, load_joint_state_broadcaster, load_joint_trajectory_controller  ])

2.2. 모듈 가져오기

import os
from launch_ros.actions import Node
from launch import LaunchDescription
from launch.substitutions import Command
from launch.actions import ExecuteProcess
from ament_index_python.packages import get_package_share_directory
  • 위에서 설명한 모듈은 설명하지 않는다.
  • substitutions 모듈은 launch 패키지에 의해 시작된 노드에 대한 명령줄 인수를 동적으로 생성하는 데 사용할 수 있는 일련의 클래스 및 함수를 제공한다.
  • Command 클래스는 명령을 실행하고 그 출력을 대체 값으로 사용할 수 있는 대체 유형이다.
  • 정확히 무엇을 의미하는지 이해하지 못하였다.

2.3. generate_launch_description 함수

  1. xacro_file 경로 저장하기

     robot_model = 'a0912'
     #robot_model = 'm1013'
        
     xacro_file = get_package_share_directory('my_doosan_pkg') + '/description'+'/xacro/'+ robot_model +'.urdf.xacro'
    
    • description/xacro 폴더 내부에 있는 a0912 모델의 xacro 파일의 경로를 저장한다.
    • xacro 파일에는 로봇의 형태 및 마찰력 등이 기술되어 있다.
  2. robot state publisher 노드 생성하기

     # Robot State Publisher 
     robot_state_publisher = Node(package    ='robot_state_publisher',
     							 executable ='robot_state_publisher',
     							 name       ='robot_state_publisher',
     							 output     ='both',
     							 parameters =[{'robot_description': Command(['xacro', ' ', xacro_file])           
     							}])
    
    • robot_state_publisher 패키지의 robot_state_publisher 노드를 실행하며, screen과 log 둘 다 출력한다.
    • 파라미터로 딕셔너리 형태의 데이터를 입력하고 있다 . 여기서 Command 함수는 어떤 역할을 하는지 모르겠다.
  3. spawn entity robot 노드 생성하기

     # Spawn the robot in Gazebo
     spawn_entity_robot = Node(package     ='gazebo_ros', 
     						  executable  ='spawn_entity.py', 
     						  arguments   = ['-entity', 'my_doosan_robot', '-topic', 'robot_description'],
     						  output      ='screen')
    
    • “spawn entity robot”은 로봇 모델을 시뮬레이션 환경에 생성하는 것을 의미한다. “spawn”은 “생성하다”라는 의미이고, “entity”는 “개체” 또는 “모델”을 의미한다. 따라서 “spawn entity robot”은 “로봇 모델을 시뮬레이션 환경에 생성하다”라는 뜻이다.
    • gazebo_ros 패키지의 spawn_entity.py 파일을 실행하며, 터미널에 정보를 출력한다. 이때 2개의 인자를 arguments로 보낸다. 첫 번째는 ‘-entity my_doosan_robot’이다. 이 인수는 시뮬레이션에서 스폰될 로봇 모델의 이름을 지정한다. 이 경우 이름은 my_doosan_robot으로 설정된다. 두 번째는 ‘-topic robot_description’이다. 이 인수는 로봇 설명이 게시될 ROS topic을 지정한다. 이 경우 topic 이름은 robot_description으로 설정됩니다.

    ❗arguments VS parameters

    • arguments는 실행될 때 노드의 실행 파일에 전달되는 명령줄 인수이다. 이는 일반적으로 노드에 대한 런타임 구성을 제공하거나 노드가 올바르게 작동하는 데 필요한 추가 정보를 지정하는 데 사용된다.
    • parameters는 노드에 대해 설정되고 런타임 시 노드에서 액세스할 수 있는 구성 값이다. parameters는 일반적으로 시작 파일에 설정되며 노드를 다시 컴파일하지 않고도 변경할 수 있다. 센서의 IP 주소 또는 로봇의 최대 속도와 같이 노드의 수명 동안 일정하게 유지되는 구성을 제공하는 데 사용할 수 있다.

    요약하면 argumentsparameters 모두 노드에 대한 구성 값을 제공하지만 arguments는 노드가 시작될 때 명령줄에 전달되며 노드가 시작될 때마다 변경할 수 있으며, parameters는 시작 파일에 있으며 노드를 다시 시작하지 않고도 변경할 수 있다.

  4. load joint state broadcaster 프로세스 생성하기

     # load and START the controllers in launch file	
     load_joint_state_broadcaster = ExecuteProcess(
     									cmd=['ros2', 'control', 'load_controller', '--set-state', 'start','joint_state_broadcaster'],
     									output='screen')
    
    • ros2 control 도구를 사용하여 컨트롤러를 로드하고 시작하는 명령을 실행하는 load_joint_state_broadcaster라는 ExecuteProcess 객체를 생성한다.
    • 실행되는 명령은 ros2 control load_controller --set-state start joint_state_broadcaster이다. 이 명령은 joint_state_broadcaster라는 컨트롤러를 로드하고 해당 상태를 start로 설정한다.
    • output 매개변수는 'screen'으로 설정되며, 이는 프로세스에서 생성된 모든 출력이 콘솔에 표시됨을 의미한다.
  5. load joint trajectory controller 프로세스 생성하기

     load_joint_trajectory_controller = ExecuteProcess( 
     									cmd=['ros2', 'control', 'load_controller', '--set-state', 'start', 'joint_trajectory_controller'], 
     									output='screen')
    
    • ros2 control 도구를 사용하여 컨트롤러를 로드하고 시작하는 명령을 실행하는 데 사용되는 load_joint_trajectory_controller라는 ExecuteProcess 객체를 생성한다.
    • 실행 중인 명령은 ros2 control load_controller --set-state start joint_trajectory_controller이다. 이 명령은 joint_trajectory_controller라는 컨트롤러를 로드하고 상태를 start로 설정한다.
    • output 매개변수는 'screen'으로 설정되며, 이는 프로세스에서 생성된 모든 출력이 콘솔에 표시됨을 의미한다.
  6. LaunchDescription 객체 반환

     return LaunchDescription([robot_state_publisher, spawn_entity_robot, load_joint_state_broadcaster, load_joint_trajectory_controller  ])
    
    • 위에서 생성한 노드와 프로세스를 LaunchDescription 객체로 반환한다.

3. my_sphere.launch.py

  • my_sphere_pkg 패키지 안에 있는 my_sphere.launch.py 파일이다.

3.1. 전체 코드

import os
from launch_ros.actions import Node
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():

    pkg_dir = get_package_share_directory('my_sphere_pkg') 

    # SDF
    sdf_file_name = 'sdf/sphere_goal/model.sdf'
    sdf = os.path.join(pkg_dir, 'models', sdf_file_name)
    
    spawn_entity = Node(package='gazebo_ros', 
                        executable='spawn_entity.py', 
                        arguments=['-entity', 'my_sphere', '-file', sdf, '-x','0.5', '-y','0.5', '-z','1'], 
                        output='screen')

    # Nodes
    # node_mark --> coordinate_node.py --> reads the position of the sphere in Gazebo and publishes the Marker Topic 
    
    node_mark = Node(package ='my_sphere_pkg', executable ='reader_mark_node', output ='screen')
    
    return LaunchDescription([spawn_entity, node_mark])

3.2. Import

import os
from launch_ros.actions import Node
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration
from ament_index_python.packages import get_package_share_directory
  • LaunchConfiguration: 런치 파일의 다른 부분에서 대체 항목으로 사용할 수 있는 시작 시간 구성 값을 지정하는 방법을 제공한다. 예를 들어, LaunchConfiguration을 사용하여 네트워크 엔드포인트의 IP 주소 또는 포트 번호, 구성 파일 경로 또는 기타 시작 시간 매개변수를 지정할 수 있다.

3.3. generate_launch_description 함수

  1. share directory 경로 저장

     pkg_dir = get_package_share_directory('my_sphere_pkg')
    
    • my_sphere_pkg 패키지의 share 폴더 경로를 저장한다.
  2. SDF 파일 경로 저장

     # SDF
     sdf_file_name = 'sdf/sphere_goal/model.sdf'
     sdf = os.path.join(pkg_dir, 'models', sdf_file_name)
    
    • sdf 파일의 경로를 저장하고, os.path.join 함수를 활용하여 절대 경로를 저장한다.
  3. Gazebo에 구체 생성

     spawn_entity = Node(package='gazebo_ros', 
                           executable='spawn_entity.py', 
                           arguments=['-entity', 'my_sphere', '-file', sdf, '-x','0.5', '-y','0.5', '-z','1'], 
                           output='screen')
    
    • gazebo_ros 패키지의 spawn_entity.py 파일을 실행한다.
    • Gazebo 시뮬레이션 환경의 위치(0.5, 0.5, 1)에서 이름이 my_sphere인 entity(객체)를 생성합니다. entity에 대한 모델 정의는 sdf 변수로 지정된다.
  4. node_mark 노드 생성

     node_mark = Node(package ='my_sphere_pkg', executable ='reader_mark_node', output ='screen')
    
    • my_sphere_pkg 패키지의 reader_mark_node 노드를 실행한다.
  5. LaunchDescription 객체 반환

     return LaunchDescription([spawn_entity, node_mark])
    
    • 각 노드를 LaunchDescription 객체로 반환한다.

4. 결과

Untitled

5. 참고 문헌