2. 시뮬레이션
2.9런치 파일과 use_sim_time
1.7장에서 런치 파일의 기본 문법을 봤습니다. 이 장은 TIKI-MINI 패키지의 실제 런치 파일을 읽으며 그 문법이 어디에 쓰였는지 확인합니다. 모든 내용은 tiki_bringup, tiki_control, tiki_description, tiki_navigation의 런치 파일에서 가져왔습니다.
런치 파일 지도
| 파일 | 하는 일 | 포함하는 것 |
|---|---|---|
tiki_bringup/launch/tiki.launch.py | 로봇 기본 구동을 한 번에 시작 | description, control, rplidar |
tiki_description/launch/tiki_description.launch.py | URDF 처리, robot_state_publisher, 선택적으로 RViz | 없음 |
tiki_control/launch/tiki_control.launch.py | ros2_control_node와 컨트롤러 두 개 | 없음 |
tiki_bringup/launch/rplidar.launch.py | 라이다 노드 | 없음 |
tiki_navigation/launch/bringup.launch.py | Nav2 전체(위치추정, 내비게이션, 선택적 SLAM) | nav2_bringup의 런치 파일 3개 |
tiki_slam/launch/slam.launch.py도 있습니다. slam_toolbox의 async_slam_toolbox_node를 단독으로 띄우며 2.10장에서 다룹니다.
tiki.launch.py 읽기
이 파일은 런치 파일을 조립하는 방식의 전형입니다. 노드를 직접 만들지 않고 다른 런치 파일 세 개를 불러 순서만 정합니다. 핵심 부분만 줄였습니다.
include_tiki_description = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_tiki_description, 'launch', 'tiki_description.launch.py')),
launch_arguments={'rsp': 'True'}.items())
include_rplidar = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_tiki_bringup, 'launch', 'rplidar.launch.py')),
launch_arguments={"serial_port": '/dev/ttyUSB0'}.items(),
condition=IfCondition(rplidar))
tiki_control_timer = TimerAction(period=7.0, actions=[include_tiki_control])
rplidar_timer = TimerAction(period=3.0, actions=[include_rplidar])
각 도구가 하는 일은 이렇습니다.
IncludeLaunchDescription은 다른 런치 파일을 이 파일 안에 펼칩니다.launch_arguments는 불러온 파일이 선언한 인자에 값을 채워 줍니다. 파일은get_package_share_directory로 찾은 설치 경로에서 읽습니다.DeclareLaunchArgument는 명령줄에서이름:=값으로 받을 인자를 선언합니다. 이 파일의 인자는include_rplidar하나입니다.IfCondition은 값이 참일 때만 그 액션을 실행합니다.include_rplidar:=false를 주면 라이다 포함을 통째로 건너뜁니다.TimerAction은 지정한 시간(초)이 지난 뒤에 안의 액션을 실행합니다.
런치 파일을 읽을 때는 LaunchDescription([...]) 목록을 먼저 보세요. 이 목록에 들어간 액션만 실행됩니다. 이 파일은 description, control 타이머, 인자 선언, rplidar 타이머 순입니다. IfCondition은 타이머가 터지는 3초 뒤에 인자 값을 읽습니다.
인자 한눈에 보기
모든 런치 파일의 인자를 모았습니다.
| 런치 파일 | 인자 | 기본값 | 쓰는 곳 |
|---|---|---|---|
tiki.launch.py | include_rplidar | True | IfCondition으로 rplidar 포함 여부 결정 |
tiki_description.launch.py | rviz | false | RViz 노드 실행 조건 |
rsp | true | robot_state_publisher 실행 조건. tiki.launch.py가 True로 넘김 | |
rplidar.launch.py | channel_type | serial | 노드 파라미터 |
serial_port | /dev/ttyUSB0 | 노드 파라미터. tiki.launch.py가 같은 값을 넘김 | |
serial_baudrate | 460800 | 노드 파라미터 | |
frame_id | laser | 노드 파라미터 | |
inverted | false | 노드 파라미터 | |
angle_compensate | true | 노드 파라미터 | |
scan_mode | Standard | 노드 파라미터 | |
tiki_control.launch.py | (없음) | ||
slam.launch.py | slam_params_file | tiki_slam/config/slam.yaml | slam_toolbox 파라미터 파일 |
bringup.launch.py | namespace | 빈 문자열 | 최상위 네임스페이스 |
use_namespace | false | 네임스페이스 적용 여부 | |
slam | False | SLAM 모드와 지도 기반 위치추정 선택 | |
map | 기본값 없음 | 지도 YAML 경로(필수) | |
use_sim_time | false | Nav2 파라미터 재작성과 포함하는 런치에 전달 | |
params_file | tiki_navigation/params/nav2_params.yaml | Nav2 파라미터 원본 | |
autostart | true | Nav2 자동 시작 | |
use_composition | True | 컴포지션(한 프로세스에 노드 모음) 사용 | |
use_respawn | False | 노드 종료 시 재시작 | |
log_level | info | 로그 수준 |
tiki.launch.py가 rviz를 넘기지 않는다는 점을 놓치기 쉽습니다. 이 파일로 구동하면 RViz는 기본값(false) 그대로 꺼져 있습니다. 로봇 모델만 RViz로 보려면 description 런치 파일을 직접 실행합니다(맨 아래 명령 참고).
시작 순서
타임라인의 단계를 눌러 보세요. 각 시점까지 생기는 노드와 토픽이 오른쪽에 쌓입니다. 3초와 7초는 TimerAction의 period 값이고, 그 뒤 두 단계는 시간이 아니라 이벤트로 이어져서 걸리는 시간이 고정되어 있지 않습니다. 토픽 이름은 런치 파일에 직접 적혀 있지 않은 것이 많아 표준 동작을 근거로 적었고, 그런 곳에는 근거를 덧붙였습니다.
description 포함, 타이머 등록
tiki_bringup/launch/tiki.launch.py
tiki_description.launch.py가 rsp:=True로 포함됩니다. xacro는 런치 파일이 로드될 때 파이썬 코드로 처리되고 robot_state_publisher가 URDF 문자열을 파라미터로 받아 시작합니다. 같은 시점에 3초, 7초 TimerAction이 대기 상태로 들어갑니다.
참고: 토픽 이름은 robot_state_publisher의 표준 동작에서 가져왔고 이 저장소의 파일에는 없습니다.
이 단계까지 생기는 것
노드와 컨트롤러
- /robot_state_publisher
토픽
- /robot_description
- /tf_static
라이다는 3초 뒤에
TimerAction의 주석에 이유가 적혀 있습니다. robot_state_publisher가 준비되는 동안 센서 노드까지 한꺼번에 뜨면 부하가 몰리니 센서를 늦춘다고 합니다. 라이다는 URDF에 의존하지 않아서 순서가 필수는 아니고 부하 분산용입니다.
control은 7초 뒤에
control 쪽은 사정이 다릅니다. tiki_control.launch.py는 로봇 모델을 파일에서 읽지 않고 실행 중인 robot_state_publisher에게 물어서 받습니다.
robot_description = Command(['ros2 param get --hide-type /robot_state_publisher robot_description'])
control_node = Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[{'robot_description': ParameterValue(robot_description, value_type=str)},
controller_params_file],
...)
Command는 셸 명령을 실행해서 그 출력을 값으로 씁니다. 여기서는 터미널에서 직접 치는ros2 param get과 똑같은 명령입니다.--hide-type은 출력 앞의 타입 설명을 뺍니다.ParameterValue(..., value_type=str)은 이 값을 문자열로 다루라고 지정합니다. 지정하지 않으면 URDF XML 텍스트를 런치가 YAML처럼 해석하려 들 수 있습니다.parameters목록에는 딕셔너리와 YAML 파일 경로를 함께 넘깁니다.tiki_controllers.yaml의update_rate: 50같은 값이 여기로 들어갑니다.
Command가 평가되는 시점에 /robot_state_publisher 노드가 없으면 ros2 param get이 값을 얻지 못합니다. 그래서 control 포함을 7초 타이머에 넣어 description 시작 뒤로 미룹니다. 런치 파일은 의존을 직접 기다리지 않고 시간으로 흉내 냅니다.
spawner 두 개는 순서대로
ros2_control_node가 뜬 뒤 컨트롤러는 spawner가 로드하고 활성화합니다. 두 spawner를 동시에 띄우지 않고 이벤트로 이었습니다.
delay_diff_drive_controller_spawner_after_joint_state_broadcaster_spawner = RegisterEventHandler(
event_handler=OnProcessExit(
target_action=joint_state_broadcaster_spawner,
on_exit=[diff_drive_controller_spawner],
)
)
RegisterEventHandler는 이벤트 처리기를 등록합니다. OnProcessExit은 지정한 프로세스(joint_state_broadcaster spawner)가 끝나면 on_exit의 액션(diff_controller spawner)을 실행합니다. 이 파일의 LaunchDescription에는 diff_drive_controller_spawner 자체가 들어 있지 않고 이 핸들러가 대신 실행합니다. 상태 토픽을 먼저 내보낸 뒤 구동 컨트롤러를 올리는 순서입니다.
내비게이션 런치와 RewrittenYaml
tiki_navigation/launch/bringup.launch.py는 Nav2 인자를 받아 nav2_bringup의 런치 파일들에 나눠 줍니다. 눈여겨볼 부분은 파라미터 파일을 실행 때마다 새로 만드는 곳입니다.
param_substitutions = {
'use_sim_time': use_sim_time,
'yaml_filename': map_yaml_file}
configured_params = RewrittenYaml(
source_file=params_file,
root_key=namespace,
param_rewrites=param_substitutions,
convert_types=True)
nav2_params.yaml에는 use_sim_time: False가 15번 나오고 map_server의 yaml_filename은 빈 문자열입니다. RewrittenYaml은 원본을 읽어서 키 이름이 일치하는 값을 런치 인자로 바꾸고, 결과를 임시 파일로 저장해 그 경로를 돌려줍니다(nav2_common의 humble 소스에서 확인). 그래서 YAML 한 줄도 고치지 않고 use_sim_time:=true나 map:=/경로/map.yaml로 값을 바꿀 수 있습니다. convert_types=True는 "true" 문자열을 불리언으로 되돌립니다. root_key는 네임스페이스가 비어 있으면 파일을 감싸지 않습니다.
slam 인자에 따라 nav2_bringup의 slam_launch.py(slam:=True) 또는 localization_launch.py(그 반대)를 포함하고, 어느 쪽이든 navigation_launch.py를 포함합니다. 모두 use_sim_time을 넘겨 받습니다.
use_sim_time
모든 ROS2 노드는 시간을 씁니다. 타이머 주기, 메시지 header.stamp, TF 변환의 시각이 그렇습니다. 기본은 컴퓨터의 벽시계입니다. 노드의 use_sim_time 파라미터를 true로 하면 노드는 /clock 토픽으로 받은 시각을 현재 시각으로 씁니다.
이 기능이 필요한 이유는 시뮬레이터와 로그 재생입니다. Gazebo는 시뮬레이션 시간을 자기 속도로 흘려서 /clock으로 내보내고, ros2 bag play --clock도 기록 당시의 시각을 내보냅니다. 노드가 벽시계를 쓰면 메시지 시각과 노드 시각이 어긋나서 TF 조회가 "미래/과거 외삽" 같은 오류로 실패합니다.
한 시스템 안의 노드는 모두 같은 시계를 써야 합니다. 어떤 노드는 /clock, 어떤 노드는 벽시계를 쓰면 한쪽이 찍은 시각을 다른 쪽이 이해하지 못합니다.
TIKI-MINI 패키지에서는 시계 관련 값이 이렇게 흩어져 있습니다.
| 위치 | 상태 |
|---|---|
nav2_params.yaml | 모든 노드 use_sim_time: False |
bringup.launch.py | use_sim_time 인자(기본 false)를 선언하고 RewrittenYaml로 위 값을 덮어쓰며 포함하는 런치에도 전달 |
브라우저 에뮬레이터의 시간과는 다릅니다
이 매뉴얼의 코드 상자도 시간이 빨리 흐릅니다. 가상 시간이라서 spin()이 실제로 기다리지 않고 이벤트를 즉시 계산합니다. 하지만 이것은 use_sim_time과 같은 메커니즘이 아닙니다. 에뮬레이터는 /clock 토픽을 내보내지 않고 use_sim_time 파라미터도 읽지 않습니다. 타이머를 감는 속도를 코드 상자가 직접 정할 뿐입니다. 실제 ROS2에서 시뮬레이션 시간을 쓸 때는 노드마다 use_sim_time을 맞춰야 한다는 점을 따로 기억하세요.
명령어
런치 파일을 실제로 실행하는 명령입니다. 환경에 따라 가능한 것이 다릅니다.
# VM: 로봇 모델을 RViz로 확인 (하드웨어 없이 가능)
ros2 launch tiki_description tiki_description.launch.py rviz:=true
# 실기기(TIKI-MINI) 전용: 시리얼 포트를 열고 모터를 구동할 수 있습니다
ros2 launch tiki_bringup tiki.launch.py
# 실기기 전용: 라이다 없이 시작
ros2 launch tiki_bringup tiki.launch.py include_rplidar:=false
# 실행 중 확인 (VM 또는 실기기)
ros2 launch -s tiki_bringup tiki.launch.py # 받는 인자 목록
ros2 node list
ros2 control list_controllers
ros2 param get /robot_state_publisher use_sim_time
tiki.launch.py는 DiffDriveTiki가 고정 경로(/dev/ttyAMA1)의 시리얼 포트를 열어야 하므로 이 하드웨어가 없는 VM에서는 실행하지 않습니다. 로봇 컴퓨터에서 쓰는 런치 파일입니다.
한눈에 정리
tiki.launch.py는 노드를 직접 만들지 않고 description, rplidar, control 런치 파일을IncludeLaunchDescription으로 조립한다.DeclareLaunchArgument로 인자를 받고IfCondition으로 포함 여부를 정하며TimerAction으로 시작 시점을 미룬다. 인자는include_rplidar하나다.- 라이다는 3초(부하 분산), control은 7초 뒤에 시작한다. control은
robot_state_publisher에서ros2 param get으로robot_description을 받기 때문이다. - spawner 두 개는
RegisterEventHandler와OnProcessExit으로 이어joint_state_broadcaster가 먼저,diff_controller가 나중에 뜬다. Command는 셸 명령 출력을 값으로 쓰고ParameterValue(value_type=str)은 그 값을 문자열로 고정한다.RewrittenYaml은 Nav2 파라미터 파일의use_sim_time과yaml_filename을 런치 인자로 바꿔 임시 파일에 쓴다.use_sim_time은 노드가/clock을 시각으로 쓰게 하는 파라미터이고 시스템의 모든 노드가 같아야 한다.- 브라우저 에뮬레이터의 가상 시간은
use_sim_time과 다른 메커니즘이다.
확인 문제
Q1. tiki.launch.py에서 include_rplidar:=false를 주면 어떤 노드가 뜨지 않고, 어떤 파일의 어떤 도구가 그 일을 하나요?
rplidar_node가 뜨지 않습니다. tiki.launch.py의 include_rplidar 인자를 IfCondition이 읽어 rplidar 포함을 건너뜁니다. description과 control은 영향을 받지 않습니다.
Q2. tiki_control.launch.py만 먼저 실행했더니 ros2_control_node에 robot_description이 들어오지 않습니다. 원인과 해결은?
control 런치는 ros2 param get으로 /robot_state_publisher에서 robot_description을 받아 오는데 그 노드가 아직 없기 때문입니다. tiki_description.launch.py를 먼저 실행해 robot_state_publisher를 띄우고, 파라미터가 나오는지(ros2 param get /robot_state_publisher robot_description) 확인한 뒤 control 런치를 실행합니다. tiki.launch.py는 이 순서를 7초 타이머로 맞춥니다.
다음 장
2.10장에서는 tiki_slam의 slam.launch.py와 slam.yaml을 읽습니다. 이 장에서 본 slam_params_file 인자가 어떻게 쓰이는지, 지도 작성 파라미터 값이 무엇을 뜻하는지 확인합니다.