Python launch 파일 안에서 LaunchArgument 값을 꺼내려면: LaunchConfiguration이 실행 시점에 문자열로 이어 붙여진다는 점으로 ‘2’와 ‘5’가 7이 아니라 25가 되는 경우 계산하기
DeclareLaunchArgument('a', default_value='2')로 인자를 선언하고, 바로 아래에서 int(LaunchConfiguration('a'))를 쓰면 TypeError가 난다. 그래서 인자 두 개를 리스트에 나란히 넣어 노드 파라미터로 넘겼더니, 2와 5를 더한 7을 기대한 자리에 25가 들어간다. Python launch 파일 안에서 LaunchArgument 값을 꺼내려면 먼저 이 두 현상이 같은 원인에서 나온다는 점을 알아야 한다. LaunchConfiguration은 값이 아니라 “실행할 때 값을 찾아 문자열로 돌려주겠다”는 자리표시자이고, 자리표시자 여러 개는 더해지지 않고 붙여 쓰인다.
이 규칙만 알면 '2'와 '5'가 어디서 문자열 '25'가 되고 어디서 정수 25가 되는지 손으로 계산할 수 있다. 그 계산을 ROS 2 Jazzy 컨테이너에서 ros2 launch를 기본값과 명령줄 값으로 실행한 결과와 맞춰 봤고, 2026년 10월 15일의 5회 실행은 소스 코드로 세운 예측과 모두 같았다. 로봇이나 시뮬레이터 없이 launch 프레임워크만 돌린 결과다. 이를 근거로 덧셈은 PythonExpression, 실행 여부 분기는 IfCondition, 일반 Python 로직은 OpaqueFunction에 맡기는 기준까지 정리한다. ros2 launch 패키지 파일.launch.py 이름:=값으로 인자를 넘겨 본 경험이 있고 Python에서 '2' + '5'가 '25'라는 것을 알면 충분하다.
generate_launch_description()이 도는 동안 인자 값은 아직 없다
런치 파일(launch file)은 여러 노드와 설정을 한 번에 띄우는 스크립트다. Python 런치 파일의 generate_launch_description()은 노드를 직접 실행하지 않는다. 무엇을 어떤 순서로 실행할지 적은 계획 목록, 곧 런치 디스크립션(launch description)을 만들어 돌려줄 뿐이다. 함수가 return하는 순간에는 아직 아무것도 실행되지 않았다.
명령줄의 a:=3 같은 값은 그 다음 단계에서 들어간다. 계획 목록에 적힌 실행 단계 하나하나를 액션(action)이라고 하는데, ros2 launch는 사용자의 런치 파일을 다른 런치 파일을 포함하는 액션인 IncludeLaunchDescription으로 감싸고 명령줄 인자를 거기에 넘긴다(ros2launch api.py L167-L177). 이 포함 액션은 런치 파일이 돌려준 계획 앞에 인자마다 SetLaunchConfiguration 액션을 붙인다. 그래서 명령줄 값은 계획 속 어떤 액션보다 먼저 런치 컨텍스트(launch context, 실행 중 상태를 담는 객체)의 launch_configurations 사전에 기록된다(include_launch_description.py L237-L243).
DeclareLaunchArgument는 자기 차례가 왔을 때 그 이름의 값이 아직 없으면 기본값을 채우고, 이미 있으면 덮어쓰지 않는다(declare_launch_argument.py L203-L215). 어느 쪽으로 들어왔든 사전의 값은 문자열이다. 이후 액션들은 각자 들고 있는 자리표시자를 이 사전에서 찾아 채운다.
이 자리표시자를 ROS 2는 치환(substitution)이라고 부른다. 공식 튜토리얼은 substitution을 런치 디스크립션을 실행하는 동안에만 평가되는 변수라고 정의한다(Using substitutions, L42, Jazzy 판 튜토리얼 페이지에도 같은 문장이 있다). LaunchConfiguration('a')는 그 substitution 중 하나다. 이 객체의 perform(context)가 컨텍스트에서 'a'를 찾아 문자열로 돌려주고, 값도 기본값도 없으면 SubstitutionFailure를 낸다(launch_configuration.py L87-L102).
그러니 generate_launch_description() 안에서 int(LaunchConfiguration('a'))를 부르는 것은 아직 채워지지 않은 빈칸을 숫자로 바꾸라는 요청이다. LaunchConfiguration('a') + LaunchConfiguration('b')도 같은 이유로 Python 수준에서 실패한다. 두 객체를 더하는 방법이 클래스에 정의돼 있지 않기 때문이다.
“나중에 채울 빈칸”이라는 비유는 시점을 이해하는 데까지만 맞다. 이 빈칸은 언제나 문자열로 채워진다. 그리고 빈칸 여러 개를 한 리스트에 놓으면 덧셈이 아니라 붙여쓰기로 합쳐진다. 25는 이 두 번째 규칙에서 나온다.

손으로 계산해 보는 ‘2’와 ‘5’의 세 갈래
substitution과 문자열이 섞인 리스트를 문자열 하나로 만드는 함수의 본체는 한 줄이다.
return ''.join([context.perform_substitution(sub) for sub in subs])
Jazzy 브랜치의 perform_substitutions_impl.py L24-L26에 있는 코드다. 각 substitution을 문자열로 바꾼 뒤 구분자 없이 이어 붙인다. launch 설계 문서도 “substitution의 iterable은 문자열 하나로 연결된다”고 적는다(architecture.rst L126-L137). 참고로 Rolling 튜토리얼에는 이 규칙을 알려 주는 팁 문단(L209-L210)이 있지만 Jazzy 튜토리얼에는 그 문단이 없다. Jazzy 사용자가 튜토리얼만 읽고는 놓치기 쉬운 지점이다.
이 한 줄을 출발점으로 a='2', b='5', idx='2'일 때의 결과를 계산하면 다음과 같다.
1단계, 연결. [a, b]는 '2' + '5'로 이어져 문자열 '25'가 된다. ['robot_', a, b]라면 'robot_25'다. 토픽 이름이나 파일 경로를 만들 때 바라는 동작이 바로 이것이다. 공식 예제의 [LaunchConfiguration('file', default='example_substitutions'), '_launch', '.py']도 21자, 7자, 3자 조각이 이어진 31자 문자열 example_substitutions_launch.py 하나로 풀린다.
2단계, PythonExpression이면 eval. PythonExpression은 받은 리스트를 먼저 같은 방식으로 연결한 다음 그 문자열을 Python 식으로 평가(eval, 문자열을 Python 코드로 실행하는 것)하고 결과를 다시 str()로 돌려준다(python_expression.py L106-L120). 그래서 연산자까지 문자열로 넣어야 한다.
PythonExpression([a, ' + ', b])→ 연결 결과'2 + 5'→ eval →'7'PythonExpression([a, b])→ 연결 결과'25'→ eval →'25'PythonExpression([idx, ' * 1.5'])→'2 * 1.5'→ eval →'3.0'
두 번째 줄이 함정이다. PythonExpression으로 감쌌으니 계산될 거라고 생각하기 쉽지만, eval은 이미 붙어 버린 '25'를 받는다.
3단계, 노드 파라미터면 YAML. Node(parameters=[{'offset': [a, b]}])처럼 파라미터 값이 substitution 리스트이면 launch_ros는 연결한 문자열을 yaml.safe_load로 읽고, 결과가 bool·int·float·str이면 그 타입을 그대로 쓴다(evaluate_parameters.py L69-L89). YAML(설정 파일에 흔히 쓰는 데이터 표기 형식)에서 25는 정수다. 노드는 오류 없이 정수 25를 받는다. 문자열 단계에서는 눈에 띄던 실수가 여기서 그럴듯한 숫자로 바뀌어 조용히 들어간다.
엄밀히 말하면 launch가 '2'와 '5'로 25를 “계산”하는 일은 없다. launch가 만드는 것은 문자열 '25'이고, 그것을 숫자로 읽는 쪽은 YAML이나 사람이다. 그 숫자의 크기를 실감하기 위해 한 자리 숫자 문자열 두 개를 이어 십진수로 읽으면 2×10+5=25, 의도한 덧셈은 2+5=7, 차이는 18이라는 교육용 산술을 따로 검산해 두었다(검산 예제 example.py, 검산 기록 verification.json, Python 3.12.3에서 종료 코드 0). 이 검산은 산술만 확인한 것이고 launch를 실행한 기록이 아니다. launch 실행은 다음 절에서 따로 확인한다.
측정을 보기 전에 직접 예측해 볼 것
같은 launch 파일을 a:=3 b:=4 r:=199로 실행한다고 하자. 다음 세 질문의 답을 먼저 적어 두면 아래 결과를 읽기 쉽다.
- 리스트
[a, b]로 만든 로그와 노드 파라미터offset_concat은 각각 무엇이 되는가? PythonExpression([a, ' + ', b])의 값은 기본값 실행과 달라지는가?IfCondition(PythonExpression([r, ' == 200', ' and ', u]))가 붙은 액션은 실행되는가?
1번은 연결 규칙대로 문자열 '34', 노드에서는 YAML을 거쳐 정수 34다. 2번은 '3 + 4'를 eval하므로 '7'이다. 기본값 2와 5의 합도 7이라서, 두 실행에서 합은 같고 연결 결과만 25에서 34로 바뀐다. 이 우연 덕분에 “인자를 바꿨는데 왜 값이 그대로지?”와 “왜 값이 엉뚱하게 바뀌지?”를 한 번에 구분할 수 있다. 3번은 '199 == 200 and True'가 'False'가 되므로 건너뛴다.
ROS 2 Jazzy 컨테이너에서 다섯 번 실행한 결과
아래 결과는 이 블로그가 실험에 쓰는 측정용 실행기(harness, 이하 하네스)로 얻었다. 같은 스크립트 experiment.py를 네트워크를 끊은 컨테이너에서 5회 실행하고, 회차마다 종료 코드·소요 시간·출력을 results.json에 남긴다. 이미지·버전 같은 실행 환경은 같은 파일에 한 번만 기록한다.
스크립트는 launch API를 직접 부르는 부분(A부)과 실제 ros2 launch를 두 번 실행하는 부분(B부)으로 나뉜다. B부의 launch 파일은 인자 다섯 개를 선언하고, LogInfo로 연결·PythonExpression·OpaqueFunction 결과를 찍고, 파라미터 확인용 rclpy 노드를 띄운다. 이 노드는 실제로 받은 파라미터의 값과 ROS 타입을 PROBE 줄로 출력한다. 각 결과는 실행 전에 소스 코드를 근거로 적어 둔 예측표와 비교했다.
측정 환경은 하네스가 기록한 그대로다. 이미지 ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca(ros:jazzy-ros-base), ROS 배포판 jazzy, Python 3.12.3, rclpy 7.1.12-1noble.20260902.053513, RMW(ROS가 통신 미들웨어를 갈아 끼우는 층, ROS middleware)는 기본값 rmw_fastrtps_cpp, 네트워크 없음, 1 CPU·1 GiB 제한이다. 같은 스크립트를 5회 실행했고 5회 모두 종료 코드 0이었으며, 아래 값은 5회 모두 같았다.
| 확인한 것 | 기본값 예측 | 기본값 실측 | a:=3 b:=4 r:=199 예측 |
실측 |
|---|---|---|---|---|
LogInfo 안의 [a, b] |
25 | 25 | 34 | 34 |
PythonExpression([a, b]) |
25 | 25 | 34 | 34 |
PythonExpression([a, ' + ', b]) |
7 | 7 | 7 | 7 |
OpaqueFunction 안의 int(a) + int(b) |
7 | 7 | 7 | 7 |
IfCondition(PythonExpression([r, ' == 200', ' and ', u])) |
실행 | 실행(if_branch=ran) |
건너뜀 | 건너뜀(줄 없음) |
노드 파라미터 offset_concat: [a, b] |
25, INTEGER | 25, INTEGER | 34, INTEGER | 34, INTEGER |
노드 파라미터 offset_sum: PythonExpression([a, ' + ', b]) |
7, INTEGER | 7, INTEGER | 7, INTEGER | 7, INTEGER |
노드 파라미터 scale: PythonExpression([idx, ' * 1.5']) |
3.0, DOUBLE | 3.0, DOUBLE | 3.0, DOUBLE | 3.0, DOUBLE |
노드 파라미터 offset_str: ParameterValue([a, b], value_type=str) |
’25’, STRING | ’25’, STRING | ’34’, STRING | ’34’, STRING |
기본값 실행(launch /tmp/args_demo.launch.py)의 1회차 원본 출력 일부다.
[INFO] [launch.user]: OUT concat=25
[INFO] [launch.user]: OUT pyexpr_no_operator=25
[INFO] [launch.user]: OUT pyexpr_sum=7
[INFO] [launch.user]: OUT opaque_types=str,str
[INFO] [launch.user]: OUT opaque_sum=7
[INFO] [launch.user]: OUT if_branch=ran
[python3-1] PROBE {"offset_concat": {"value": 25, "ros_type": "INTEGER", "py_type": "int"}, "offset_sum": {"value": 7, "ros_type": "INTEGER", "py_type": "int"}, "scale": {"value": 3.0, "ros_type": "DOUBLE", "py_type": "float"}, "offset_str": {"value": "25", "ros_type": "STRING", "py_type": "str"}}
a:=3 b:=4 r:=199 실행에서는 if_branch 줄이 아예 찍히지 않았다.
[INFO] [launch.user]: OUT concat=34
[INFO] [launch.user]: OUT pyexpr_no_operator=34
[INFO] [launch.user]: OUT pyexpr_sum=7
[INFO] [launch.user]: OUT opaque_types=str,str
[INFO] [launch.user]: OUT opaque_sum=7
두 실행 모두 generate_launch_description() 안에서 찍은 줄은 PLAN_TIME type=LaunchConfiguration is_str=False였다. 계획을 만드는 동안 a는 문자열 대신 LaunchConfiguration 객체로만 존재한다는 뜻이다.
A부에서 오류로 끝난 호출은 1회차 stdout의 case와 error 필드만 옮기면 다음과 같다. InvalidConditionExpressionError 문구 끝의 객체 메모리 주소는 실행마다 달라지므로 …로 줄였다.
plan_int TypeError: int() argument must be a string, a bytes-like object or a real number, not 'LaunchConfiguration'
plan_add TypeError: unsupported operand type(s) for +: 'LaunchConfiguration' and 'LaunchConfiguration'
plan_perform_empty_ctx SubstitutionFailure: launch configuration 'a' does not exist
pyexpr_string_no_quotes NameError: name 'robot1' is not defined
if_raw_200 InvalidConditionExpressionError: invalid condition expression, expected one of [true, 1, false, 0] but got '200', expanded from '<…>'
if_raw_yes InvalidConditionExpressionError: invalid condition expression, expected one of [true, 1, false, 0] but got 'yes', expanded from '<…>'
같은 A부에서 IfCondition(LaunchConfiguration('r'))는 r='1'과 r=' TRUE '일 때 True, 따옴표로 감싼 PythonExpression(["'", name, "' == 'robot1'"])은 'True'를 돌려줬다.
재현 스크립트 experiment.py, 실행별 원본 출력이 든 results.json, 실행 전 예측을 적은 plan.md는 실험 디렉터리에 공개돼 있다. 파일로 바로 열려면 experiment.py, results.json, plan.md를 보면 된다.
결과는 예측표와 한 칸도 다르지 않았다. 소스 코드를 읽고 세운 세 갈래 계산이 실제 ros2 launch에서도 그대로 성립한다는 확인이다.
눈여겨볼 줄은 PROBE다. 노드는 offset_concat을 INTEGER 25로 받았고, launch 실행은 종료 코드 0으로 끝났다. 반면 offset_str처럼 ParameterValue(..., value_type=str)로 타입을 고정하면 YAML의 자동 타입 결정을 거치지 않아 문자열 '25'가 그대로 간다. 파라미터가 문자열이어야 하는 경우(예: 숫자처럼 생긴 ID)라면 이 방법이 맞다.
다만 이 표는 정식 5회 실행에서 얻은 것이고, 그 전에 시험 실행에서 막힌 일이 하나 있었다. 처음에는 파라미터 확인 스크립트를 /tmp에 쓰고 직접 실행하게 했는데, 하네스가 /tmp를 실행 권한 없는(noexec) tmpfs(메모리 위의 임시 파일시스템)로 붙여서 PermissionError: [Errno 13] Permission denied가 났다. 그런데 노드 프로세스가 시작도 못 했는데 ros2 launch의 종료 코드는 0이었다. 그래서 Node(executable='python3', arguments=['/tmp/param_probe.py'], ...)로 바꿔 python3가 스크립트를 읽게 했고, 노드가 직접 찍은 PROBE 줄이 있을 때만 파라미터 결과로 인정했다. 적어도 이 조건에서는 ros2 launch가 0으로 끝났다는 사실만으로 노드가 파라미터를 받았다고 판단할 수 없었다.
직접 재현하려면 Docker가 있는 빈 디렉터리에서 스크립트를 받고, 하네스와 같은 고정 이미지로 실행한다. 컨테이너는 네트워크 없이 돌고, 스크립트가 컨테이너 안 /tmp에 launch 파일과 노드를 써서 실행한다.
curl -fsSLO https://raw.githubusercontent.com/sungpyo9053/blog/25e02feb602d7a21f5b6f35a3efaa6477c4c4f44/experiments/ros2-python-launch-argument-value-substitution-string-concat/experiment.py
docker run --rm --network none --cpus 1 --memory 1g -v "$PWD":/work ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca bash -c "source /opt/ros/jazzy/setup.bash && python3 /work/experiment.py"
출력은 {"part": ..., "case": ...} 형식의 JSON 줄이다. ros2_launch_default 줄의 logs에 "concat": "25", "pyexpr_sum": "7"이 있고 node_params가 null이 아니면 위 표와 같은 결과를 얻은 것이다. node_params가 null이면 노드가 시작하지 못한 경우이므로 같은 줄의 process_lines를 먼저 본다.
계산을 어디에 맡길지 고르는 기준
결과를 놓고 보면 선택지는 “값이 필요한 계산을 실행 시점의 어느 장치에 맡기느냐”로 정리된다. generate_launch_description() 안의 일반 Python 코드는 후보가 아니다. 그 시점에는 값이 없다.
| 하고 싶은 일 | 쓸 것 | 이유 |
|---|---|---|
| 인자를 다른 문자열과 붙이기(토픽 이름, 파일 경로) | substitution 리스트 그대로 | 연결이 기본 동작이다 |
| 인자로 산술한 값을 파라미터·인자에 넣기 | PythonExpression에 연산자까지 넣기 |
연결 뒤 eval하고 결과는 문자열이다 |
| 인자로 액션 실행 여부 정하기 | IfCondition(PythonExpression([...])) |
조건은 true/1/false/0만 받는다 |
| 파일 읽기, 반복문으로 노드 여러 개 만들기 같은 일반 로직 | OpaqueFunction 안에서 perform(context) |
실행 시점에 진짜 문자열을 받는다 |
| 숫자처럼 생긴 파라미터를 문자열로 유지 | ParameterValue(..., value_type=str) |
YAML 자동 타입 결정을 막는다 |
산술: 연산자도 문자열 조각이다
PythonExpression에서 연산자를 빠뜨리면 PythonExpression([a, b])처럼 25가 그대로 나온다. 위 측정에서 pyexpr_no_operator가 기본값 실행에서 25, 덮어쓴 실행에서 34였던 것이 그 증거다. 연산자 앞뒤 공백까지 포함해 ' + '처럼 별도 조각으로 넣는 습관을 들이면 연결 결과를 머릿속에서 바로 읽을 수 있다.
문자열 인자를 비교할 때도 같은 원리가 작동한다. PythonExpression([name, " == 'robot1'"])은 robot1 == 'robot1'이라는 Python 소스가 되고, eval은 따옴표 없는 robot1을 변수 이름으로 읽어 NameError를 낸다. 연결된 문자열이 그대로 Python 코드가 된다는 점만 기억하면, ["'", name, "' == 'robot1'"]처럼 따옴표를 조각으로 넣어야 하는 이유가 따로 외울 규칙이 아니게 된다. 같은 이유로 명령줄 인자 문자열이 Python 식으로 평가된다는 점도 알아 둘 만하다.
조건: IfCondition은 Python의 참·거짓 규칙을 따르지 않는다
IfCondition은 연결된 문자열을 앞뒤 공백을 지우고 소문자로 바꾼 뒤 true·1이면 참, false·0이면 거짓으로 보고, 그 밖의 문자열은 오류로 처리한다(evaluate_condition_expression_impl.py L25-L48). Python이라면 비어 있지 않은 문자열이 모두 참이지만 여기서는 아니다. 측정에서도 ' TRUE '는 통과했고 '200'과 'yes'는 InvalidConditionExpressionError였다.
비교가 필요하면 공식 예제처럼 PythonExpression이 비교를 끝내게 한다(example_substitutions_launch.py L55-L60). 비교 결과 True가 str()을 거쳐 'True'가 되고, IfCondition이 이를 소문자 'true'로 받아들인다.
확인 질문 하나: use_sim 인자를 IfCondition(LaunchConfiguration('use_sim'))에 바로 넣고 사용자가 use_sim:=yes라고 입력하면 어떻게 될까? launch가 InvalidConditionExpressionError로 멈춘다. 'yes'는 네 문자열 중 어느 것도 아니기 때문이다. 위 if_raw_yes 줄이 바로 이 경우다.
일반 Python 로직: OpaqueFunction 안에서 꺼낸다
인자 값으로 파일을 열거나, 개수만큼 노드를 만들거나, 여러 분기를 Python 문법으로 쓰고 싶다면 OpaqueFunction을 쓴다. 실행 시점에 function(context, *args, **kwargs)를 호출하는 액션이라서(opaque_function.py L32-L75), 함수 안에서는 perform(context)로 진짜 문자열을 받을 수 있다. 측정에 쓴 launch 파일의 해당 부분을 그대로 옮긴다.
def opaque(context):
a = LaunchConfiguration('a').perform(context)
b = LaunchConfiguration('b').perform(context)
return [LogInfo(msg=f'OUT opaque_types={type(a).__name__},{type(b).__name__}'),
LogInfo(msg=f'OUT opaque_sum={int(a) + int(b)}')]
generate_launch_description()이 돌려주는 리스트 안에는 OpaqueFunction(function=opaque),로 넣었다. 꺼낸 값이 str,str로 찍힌 것처럼 여기서도 값은 문자열이고, 숫자 계산은 int()로 직접 바꾼 뒤에 한다. 함수는 launch 엔티티의 리스트(또는 None)를 돌려줘야 한다. 질문 게시판의 한 답변처럼 Node 하나만 돌려주면 이 시그니처와 맞지 않는다.
OpaqueFunction이 가장 자유롭지만, 그 안에서 만들 액션은 함수가 실행되기 전까지 코드를 읽어야만 알 수 있다. 문자열 연결로 끝나는 일이라면 리스트, 한 줄 산술이나 비교라면 PythonExpression으로 충분하고, OpaqueFunction은 Python 문법이 정말 필요할 때 꺼내는 편이 읽는 사람에게 친절하다. 명령줄 인자를 sys.argv에서 직접 파싱하는 우회도 질문 게시판에 보이지만 launch의 공식 API가 아니므로 여기서는 다루지 않는다.
이 결과를 다른 환경에 옮길 때
측정 범위는 ROS 2 Jazzy의 launch 프레임워크가 문자열과 타입을 어떻게 평가하는지까지다. 이미지에 설치된 패키지는 ros-jazzy-launch 3.4.11과 ros-jazzy-launch-ros 0.26.12였고, 본문에 링크한 소스 파일은 GitHub jazzy 브랜치 고정 커밋의 같은 파일과 바이트 단위로 같았다. declare_launch_argument.py 하나만 독스트링의 XML 예시 두 줄이 달랐고, 인용한 execute 부분은 같았다(2026년 10월 15일 확인). Humble이나 Rolling에서 같은 결과가 나오는지는 대조하지 않았다. XML·YAML 런치 파일의 $(var), $(eval)도 같은 substitution을 쓰지만 이 글에서 실행하지는 않았다.
노드는 파라미터를 받아 출력하는 확인용 스크립트였고, 시뮬레이터나 실물 로봇은 쓰지 않았다. 그래도 실물에 옮길 때 이 결과가 뜻하는 바는 분명하다. 위치 오프셋이나 속도 한계 같은 숫자 파라미터를 launch 인자로 만든 경우, 리스트 연결 실수는 오류 없이, 정상처럼 보이는 다른 숫자로 노드에 들어간다. 로봇을 움직이기 전에 ros2 param get <노드 이름> <파라미터 이름>으로 실제 값과 타입을 확인하고, 하드웨어 비상 정지 수단을 손이 닿는 곳에 둔 채 저속으로 시작하는 기본 절차를 지킨다. 이것은 일반적인 확인 순서일 뿐이고 각 로봇의 안전 절차를 대신하지 않는다. 잘못 들어간 오프셋이 좌표 변환에서 어떤 위치 오차로 드러나는지는 tf 프레임 오프셋 방향 오류 계산에서 이어서 볼 수 있다.
자기 launch 파일을 점검할 때는 숫자로 쓰려는 LaunchConfiguration이 리스트 안에서 다른 조각과 붙어 있지 않은지부터 본다. 붙어 있다면 그 리스트가 연결로 끝나는지, PythonExpression에서 eval되는지, 노드 파라미터에서 YAML로 읽히는지를 차례로 따라가면 결과 값과 타입을 손으로 계산할 수 있다. 실행 시점과 계획 시점을 turtlesim 예제로 더 보고 싶다면 ROS 2 Jazzy 공식 Using substitutions 튜토리얼이 다음 단계로 적당하다.
출처와 확인일
모두 2026년 10월 15일에 확인했다. 소스 링크는 Jazzy 브랜치의 고정 커밋이다.
- ROS 2 문서, Using substitutions(Rolling 고정 커밋): substitution 평가 시점과 리스트 연결 팁
- ros2/launch include_launch_description.py와 declare_launch_argument.py: 명령줄 값이 먼저 기록되고 기본값은 비어 있을 때만 채워지는 순서
- ros2/launch perform_substitutions_impl.py:
''.join연결 - ros2/launch python_expression.py: 연결 뒤 eval,
str()반환 - ros2/launch_ros evaluate_parameters.py: 노드 파라미터의 YAML 타입 결정
- HuntLab 교육용 산술 검산 example.py와 verification.json: 산술 검산이며 launch 실행 기록이 아니다
이 글에는 제휴·협찬이 없다.