ROS2 的服务(Service)
-
概念:
服务是一种 请求-响应(request-response)通信机制,由 客户端(Client) 和 服务端(Server) 两部分组成。 -
通信模式:
-
客户端 发送一个请求(Request)。
-
服务端 处理请求,并返回响应(Response)。
-
一问一答,适合短时操作。
-
-
特点:
-
一对一通信,不像话题那样是一对多。
-
请求和响应的数据类型是 固定定义好的
.srv
文件(里面有请求字段和响应字段)。 -
适合执行需要确认结果的操作。
-
-
典型应用场景:
-
查询机器人位置。
-
控制执行某个单次动作(例如开关灯、拍照)。
-
获取传感器的单次读数。
-
服务的本质是通过两个交互的话题实现的,当你创建一个服务时,系统会自动生成两个隐藏的话题:
-
请求话题(Request Topic)
-
客户端(Client)通过这个话题向服务端(Server)发送请求消息。
-
-
响应话题(Response Topic)
-
服务端(Server)通过这个话题把处理结果返回给客户端。
-
一个服务 = 请求话题 + 响应话题。
一般来说客户端节点会向请求话题发布消息,然后从响应话题得到消息,同理服务端向响应话题发布消息,从请求话题得到消息,二者的对于不同的话题交互都是单向的,保证了一个负责请求,一个负责响应。
基于服务的参数通信
参数通信
-
在 ROS2 中,参数(Parameter) 是节点的配置项,用来控制节点的行为。
例如:-
turtlesim_node
的背景颜色 -
激光雷达的刷新频率
-
控制器的 PID 系数
-
-
参数通信 指的是:节点之间通过 服务机制(Service) 来 设置、修改、查询参数。
ROS2 参数通信的特点
-
一对一的服务通信
-
参数操作是请求-响应模式(类似打电话)。
-
客户端(如
ros2 param
命令)发送请求,服务端(节点)返回结果。
-
-
内置服务接口
ROS2 每个支持参数的节点都会自动提供一些参数相关的服务:-
/node_name/get_parameters
获取参数值 -
/node_name/set_parameters
设置参数值 -
/node_name/list_parameters
列出参数 -
/node_name/describe_parameters
获取参数描述(类型、范围等)
-
-
接口类型定义
所有参数相关的服务接口都在rcl_interfaces
包中定义,比如:-
rcl_interfaces/srv/GetParameters
-
rcl_interfaces/srv/SetParameters
-
通过参数通信调节设置小海龟的背景颜色,在 ROS2 中,也可以通过 YAML 配置文件提前写好参数,然后在启动节点时加载,让节点自动带着这些参数运行。
话题消息接口为功能包下的msg下的.msg文件,而服务消息接口为srv下的.srv文件
服务接口一般形式靠---分割
# 请求部分(Request)
<请求字段类型> <请求字段名称>
<请求字段类型> <请求字段名称>
...---
# 响应部分(Response)
<响应字段类型> <响应字段名称>
<响应字段类型> <响应字段名称>
...
ROS2 接口命名规范对照表
类型 | 正确示例 ✅ | 错误示例 ❌ | 说明 |
---|---|---|---|
包名 (package) | face_interfaces |
FaceInterfaces , face-interfaces , faceInterfaces |
必须小写,下划线分隔,不允许大写或 - |
消息文件 (.msg) | Person.msg , FaceData.msg , RobotStatus.msg |
person.msg , face_data.msg , Face_data.msg |
文件名 PascalCase,首字母大写,不能用下划线 |
服务文件 (.srv) | GetFaceInfo.srv , AddTwoInts.srv |
get_face_info.srv , add_two_ints.srv |
文件名 PascalCase,系统会生成 _Request 和 _Response |
动作文件 (.action) | Navigate.action , TrackFace.action |
track_face.action , Navigate_Action.action |
文件名 PascalCase,系统会生成 _Goal 、_Result 、_Feedback |
消息/服务/动作字段 (内部) | string name , int32 age , geometry_msgs/Point position |
string Name , int Age , float positionX |
字段名必须 小写+下划线,类型用 ROS 标准类型 |
生成的类型名 (自动生成) | GetFaceInfo_Request , GetFaceInfo_Response Navigate_Goal , Navigate_Result , Navigate_Feedback |
get_face_info_Request , face_interfaces_Request |
自动生成的类型名也遵循 PascalCase,错误一般来自文件命名不规范 |
PascalCase:PascalCase 是一种命名方式,规则是 每个单词的首字母大写且不使用下划线或空格,例如:GetFaceInfo
、PersonMsg
。
Python实现人脸检测服务
该人脸检测服务由客户端上传人脸图片,发送给服务端,客户端启动之后会等待服务端的启动才能进行正常的服务,否则处于休眠状态,防止占用过多cpu,然服务端响应回人脸个数,用时和每个框的四个点的位置信息给客户端进行展示
首先我们需要创建一个工作控件service_example同时创建src文件夹用于存放功能包
创建服务接口,用于客户端和服务端交互数据,所需依赖除了解析消息接口的依赖还需要sensor_msgs用于传递人脸图片,注意消息接口的功能包需要用到C++的CMakeLists.txt文件进行构建
# 人脸图像
sensor_msgs/Image image
---
# 人脸个数
int16 number
# 识别耗时
float32 use_time
# 人脸框的四个点
int32[] top
int32[] right
int32[] bottom
int32[] left
在CMakeLists.txt文件加上对应的转换函数
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/FaceInterfaces.srv"
DEPENDENCIES sensor_msgs
)
构建得到相应的服务接口
创建客户服务端功能包,里面创建两个节点,一个客户端节点,一个服务端节点
import rclpy
from rclpy.node import Node
from face_interfaces.srv import FaceInterfaces
import face_recognition
import cv2
from ament_index_python.packages import get_package_share_directory
import os
from cv_bridge import CvBridge
import timeclass FaceDetectClientNode(Node):def __init__(self):super().__init__('face_detect_client_node')# 用于 OpenCV 图像 ↔ ROS 图像消息的转换,因为ROS接口里面的图像类型和OpenCV的图像类型并不相同self.bridge = CvBridge()self.client = self.create_client(FaceInterfaces,'face_detect')self.get_logger().info('face detect client start')# 获取照片的绝对路径self.face_image_path = os.path.join(get_package_share_directory('demo_python_service'),'resource/face.jpg')self.image = cv2.imread(self.face_image_path)def send_request(self):# 每搁一秒询问服务死否启动,否则等待while self.client.wait_for_service(timeout_sec = 1.0) is False:self.get_logger().info('wait service start')# 获取服务接口的请求数据结构request = FaceInterfaces.Request()# 图像结构转换request.image =self.bridge.cv2_to_imgmsg(self.image)# 异步发送请求,返回一个未来的响应,不能直接用它的值,但可以给它加回调函数,当结果返回时就会触发future=self.client.call_async(request)# 回调函数,响应时触发future.add_done_callback(self.result_callback)def result_callback(self,future):response =future.result()self.get_logger().info(f'face number :{response.number},use time : {response.use_time}')self.show_response(response)def show_response(self,response):# 为每个人脸描绘出边框for i in range(response.number):top=response.top[i]right =response.right[i]bottom= response.bottom[i]left=response.left[i]cv2.rectangle(self.image,(left,top),(right,bottom),(255,0,0),4)cv2.imshow('Face Detecte Result',self.image)cv2.waitKey(0)def main():rclpy.init()node = FaceDetectClientNode()node.send_request()rclpy.spin(node)rclpy.shutdown()
import rclpy
from rclpy.node import Node
from face_interfaces.srv import FaceInterfaces
import face_recognition
import cv2
from ament_index_python.packages import get_package_share_directory
import os
from cv_bridge import CvBridge
import timeclass FaceDetectServiceNode(Node):def __init__(self):super().__init__('face_detect_service_node')self.service = self.create_service(FaceInterfaces,'face_detect',self.face_detect_callback)self.bridge = CvBridge()# 人脸检测模型的参数self.number_of_times_to_upsampl = 1self.model = 'hog'self.face_image_path = os.path.join(get_package_share_directory('demo_python_service'),'resource/face.jpg')self.get_logger().info('face detect service start')def face_detect_callback(self,request,response):# 判断客户端给的图像是否为空if request.image.data:cv_image= self.bridge.imgmsg_to_cv2(request.image)self.get_logger().warn('image is null,use defacult')else:cv_image = cv2.imread(self.face_image_path)self.get_logger().info('image is finishing')# 记录开始检测的时间start_time = time.time()face_locations = face_recognition.face_locations(cv_image,number_of_times_to_upsample=self.number_of_times_to_upsampl,model=self.model)# 计算时间差response.use_time = time.time() - start_time# 数据赋值response.number = len(face_locations)for top,right,bottom,left in face_locations:response.top.append(top)response.right.append(right)response.bottom.append(bottom)response.left.append(left)return responsedef main():rclpy.init()node = FaceDetectServiceNode()rclpy.spin(node)rclpy.shutdown()
C++实现海龟巡逻服务
客户端随机生成一些目标点,让海龟去巡逻,服务端接收到这些目标点控制海龟去这些目标点,我们直接使用之前学习话题的代码进行控制
服务接口结构
float32 target_x
float32 target_y
---
int8 SUCESS=1
int8 FAIL=0
int8 result #结果,SUCESS/│FAIL取其一
创建服务端和客户端
#include <rclcpp/rclcpp.hpp>
#include <turtlesim/msg/pose.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <partol_interfaces/srv/partol.hpp>
using namespace std;
using namespace rclcpp;
using Partol = partol_interfaces::srv::Partol;class TurtleClientNode:public Node
{private:Client<Partol>::SharedPtr partol_client;TimerBase::SharedPtr timer;public:explicit TurtleClientNode(const string& node_name):Node(node_name){partol_client = this->create_client<Partol>("partol");// 随机数种子srand(time(NULL));//创建一个 10 秒周期的定时器,每次定时器触发时,先检查服务是否可用,如果服务端还没启动,循环等待,如果整个 ROS2 系统关闭了,就报错退出timer = this->create_wall_timer(10s,[&]()->void{while(!this->partol_client->wait_for_service(1s)){if(!rclcpp::ok()){RCLCPP_ERROR(this->get_logger(),"wait service,rclcpp is over");}RCLCPP_INFO(this->get_logger(),"wait service.........");}// 随机生成目标地点坐标auto request = std::make_shared<Partol::Request>();request->set__target_x(rand()%15);request->set__target_y(rand()%15);RCLCPP_INFO(this->get_logger(),"create target point(%f,%f)",request->target_x,request->target_y);// 异步请求,这样不需要一直等待响应而什么都不干,可以在等待响应的过程中进行其他操作this->partol_client->async_send_request(request,[&](Client<Partol>::SharedFuture future)->void{auto response = future.get();if(response->result == Partol::Response::SUCESS){RCLCPP_INFO(this->get_logger(),"turtle is success");}else{RCLCPP_ERROR(this->get_logger(),"turtle is fail");}});});}};int main(int argc,char** argv){init(argc,argv);auto node = make_shared<TurtleClientNode>("turtle_client");RCLCPP_INFO(node->get_logger(),"turtle_client start");spin(node);shutdown();return 0;
}
#include <rclcpp/rclcpp.hpp>
#include <turtlesim/msg/pose.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <partol_interfaces/srv/partol.hpp>
using namespace std;
using namespace rclcpp;
using Partol = partol_interfaces::srv::Partol;class TurtleServiceNode:public Node
{private:Service<Partol>::SharedPtr service;Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher;Subscription<turtlesim::msg::Pose>::SharedPtr subsciber;double target_x {1.0};double target_y {1.0};double k {1.0};double max_speed {3.0};public:explicit TurtleServiceNode(const string& node_name):Node(node_name){service = this->create_service<Partol>("partol",[&](const Partol::Request::SharedPtr request,Partol::Response::SharedPtr response) -> void{if((request->target_x>0 && request->target_x<12.0f)&&(request->target_y>0 && request->target_y<12.0f)){this->target_x = request->target_x;this->target_y = request->target_y;response->result = Partol::Response::SUCESS;}else{response->result = Partol::Response::FAIL;}});publisher = this->create_publisher<geometry_msgs::msg::Twist>("/turtle1/cmd_vel", 10);subsciber = this->create_subscription<turtlesim::msg::Pose>("/turtle1/pose", 10,bind(&TurtleServiceNode::pose_callback,this,placeholders::_1));}void pose_callback(const turtlesim::msg::Pose::SharedPtr pose){auto current_x = pose->x;auto current_y = pose->y;RCLCPP_INFO(this->get_logger(),"x=%f y=%f \n",current_x,current_y);auto distance = sqrt((target_x-current_x)*(target_x-current_x)+(target_y-current_y)*(target_y-current_y));auto angle = atan2((target_y-current_y),(target_x-current_x)) - pose->theta;auto msg = geometry_msgs::msg::Twist();if(distance>0.1){if(fabs(angle)>0.2){msg.angular.z = fabs(angle);}else{msg.linear.x = k*distance;}}if(msg.linear.x > max_speed){msg.linear.x = max_speed;}publisher->publish(msg);}};int main(int argc,char** argv){init(argc,argv);auto node = make_shared<TurtleServiceNode>("turtle_service");RCLCPP_INFO(node->get_logger(),"turtle_service start");spin(node);shutdown();return 0;
}
cmake_minimum_required(VERSION 3.8)
project(demo_cpp_service)if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")add_compile_options(-Wall -Wextra -Wpedantic)
endif()# find dependencies
find_package(ament_cmake REQUIRED)
find_package(partol_interfaces REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(turtlesim REQUIRED)
add_executable(turtleService src/turtle_service.cpp)
add_executable(turtleClient src/turtle_client.cpp)
ament_target_dependencies(turtleService partol_interfaces rclcpp geometry_msgs turtlesim)
ament_target_dependencies(turtleClient partol_interfaces rclcpp geometry_msgs turtlesim)if(BUILD_TESTING)find_package(ament_lint_auto REQUIRED)# the following line skips the linter which checks for copyrights# comment the line when a copyright and license is added to all source filesset(ament_cmake_copyright_FOUND TRUE)# the following line skips cpplint (only works in a git repo)# comment the line when this package is in a git repo and when# a copyright and license is added to all source filesset(ament_cmake_cpplint_FOUND TRUE)ament_lint_auto_find_test_dependencies()
endif()install(TARGETS turtleService turtleClient
DESTINATION lib/${PROJECT_NAME}
)ament_package()
先启动海龟模拟强,再启动客户端,然后启动服务端
参数设置
Python的人脸检测将模型的参数由ros的参数机制进行设置,受用命令行中的--ros-args -p 参数名:=参数值
self.declare_parameter('number_of_times_to_upsampl',1)self.declare_parameter('model','hog')self.number_of_times_to_upsampl = self.get_parameter('number_of_times_to_upsampl').valueself.model= self.get_parameter('model').value
订阅参数更新实现,更改服务端代码
def __init__(self):super().__init__('face_detect_service_node')self.service = self.create_service(FaceInterfaces,'face_detect',self.face_detect_callback)self.bridge = CvBridge()self.declare_parameter('number_of_times_to_upsampl',1)self.declare_parameter('model','hog')self.number_of_times_to_upsampl = self.get_parameter('number_of_times_to_upsampl').valueself.model= self.get_parameter('model').valueself.face_image_path = os.path.join(get_package_share_directory('demo_python_service'),'resource/face.jpg')self.add_on_set_parameters_callback(self.parameters_callback)self.get_logger().info('face detect service start')def parameters_callback(self,paramters):for paramter in paramters:self.get_logger().info(f"{paramter.name}->{paramter.value}")if paramter.name =='number_of_times_to_upsample':self.number_of_times_to_upsample = paramter.valueif paramter.name== 'model':self.model = paramter.valuereturn SetParametersResult('success')
在 ROS2 中,节点只要用到参数机制(比如声明参数),就会自动生成一系列 参数相关的服务,可以通过 ros2 service list 命令查看,不过需要先启动节点。
我编写客户端代码,调用其自带的参数设置服务set_Parameters进行参数的设置,其服务接口为rcl_interfaces/srv/SetParameters,可以看到其消息接口本质上是一个复合接口,除了基本的数据结构还依赖其他的消息接口, 因此在构建的时候需要加上其他消息接口的依赖,注意消息接口中调用其他消息接口只需要包名/接口名
这种形式。
import rclpy
from rclpy.node import Node
from face_interfaces.srv import FaceInterfaces
import face_recognition
import cv2
from ament_index_python.packages import get_package_share_directory
import os
from cv_bridge import CvBridge
import time
from rcl_interfaces.msg import SetParametersResultclass FaceDetectServiceNode(Node):def __init__(self):super().__init__('face_detect_service_node')self.service = self.create_service(FaceInterfaces,'face_detect',self.face_detect_callback)self.bridge = CvBridge()self.declare_parameter('number_of_times_to_upsampl',1)self.declare_parameter('model','hog')self.number_of_times_to_upsampl = self.get_parameter('number_of_times_to_upsampl').valueself.model= self.get_parameter('model').valueself.face_image_path = os.path.join(get_package_share_directory('demo_python_service'),'resource/face.jpg')self.add_on_set_parameters_callback(self.parameters_callback)self.get_logger().info('face detect service start')# 获取客户端传来的参数并进行设置def parameters_callback(self,paramters):for paramter in paramters:self.get_logger().info(f"{paramter.name}->{paramter.value}")if paramter.name =='number_of_times_to_upsample':self.number_of_times_to_upsample = paramter.valueif paramter.name== 'model':self.model = paramter.valuereturn SetParametersResult(successful=True)def face_detect_callback(self,request,response):if request.image.data:cv_image= self.bridge.imgmsg_to_cv2(request.image)self.get_logger().warn('image is null,use defacult')else:cv_image = cv2.imread(self.face_image_path)self.get_logger().info('image is finishing')start_time = time.time()face_locations = face_recognition.face_locations(cv_image,number_of_times_to_upsample=self.number_of_times_to_upsampl,model=self.model)response.use_time = time.time() - start_timeresponse.number = len(face_locations)for top,right,bottom,left in face_locations:response.top.append(top)response.right.append(right)response.bottom.append(bottom)response.left.append(left)return responsedef main():rclpy.init()node = FaceDetectServiceNode()rclpy.spin(node)rclpy.shutdown()
import rclpy
from rclpy.node import Node
from face_interfaces.srv import FaceInterfaces
import face_recognition
import cv2
from ament_index_python.packages import get_package_share_directory
import os
from cv_bridge import CvBridge
import time
from rcl_interfaces.srv import SetParameters
from rcl_interfaces.msg import Parameter,ParameterType,ParameterValueclass FaceDetectClientNode(Node):def __init__(self):super().__init__('face_detect_client_node')self.bridge = CvBridge()self.client = self.create_client(FaceInterfaces,'face_detect')self.get_logger().info('face detect client start')self.face_image_path = os.path.join(get_package_share_directory('demo_python_service'),'resource/face.jpg')self.image = cv2.imread(self.face_image_path)print(type(self.image))print(self.image is None)# 设置模型的客户端def call_set_parameters(self,parameters):update_param_client = self.create_client(SetParameters,'/face_detect_service_node/set_parameters')while update_param_client.wait_for_service(timeout_sec=1.0) is False:self.get_logger().info('wait parameters service!')request = SetParameters.Request()request.parameters = parametersfuture = update_param_client.call_async(request)rclpy.spin_until_future_complete(self,future)response = future.result()return response# 更新模型的参数def update_detect_model(self,model='hog'):param= Parameter()param.name ='model'param_value = ParameterValue()param_value.string_value = modelparam_value.type =ParameterType.PARAMETER_STRINGparam.value = param_valueresponse = self.call_set_parameters([param])for result in response.results:self.get_logger().info(f"result:{result.successful}{result.reason}")def send_request(self):while self.client.wait_for_service(timeout_sec = 1.0) is False:self.get_logger().info('wait service start')request = FaceInterfaces.Request()request.image =self.bridge.cv2_to_imgmsg(self.image)future=self.client.call_async(request)future.add_done_callback(self.result_callback)def result_callback(self,future):response =future.result()self.get_logger().info(f'face number :{response.number},use time : {response.use_time}')self.show_response(response)def show_response(self,response):for i in range(response.number):top=response.top[i]right =response.right[i]bottom= response.bottom[i]left=response.left[i]cv2.rectangle(self.image,(left,top),(right,bottom),(255,0,0),4)# cv2.imshow('Face Detecte Result',self.image)# cv2.waitKey(0)def main():rclpy.init()node = FaceDetectClientNode()node.update_detect_model('hog')node.send_request()node.update_detect_model('cnn')node.send_request()rclpy.spin(node)rclpy.shutdown()
C++使用参数节点
使用参数机制设置小海龟的比例系数和最大速度,代码都是差不多的就不过多写了
this->declare_parameter("k",k);this->declare_parameter("max_speed",max_speed);this->get_parameter("k",k);this->get_parameter("max_speed",max_speed);this->param_callback_handle = this->add_on_set_parameters_callback([&](const vector<Parameter> ¶meters)-> rcl_interfaces::msg::SetParametersResult{rcl_interfaces::msg::SetParametersResult result;result.successful=true;for (const auto & parameter : parameters){RCLCPP_INFO(this->get_logger(),"update param %s=%f",parameter.get_name().c_str(),parameter.as_double());if(parameter.get_name()=="k")k = parameter.as_double();if(parameter.get_name()=="max_speed")max_speed = parameter.as_double();}return result;});
ROS2 Launch
Launch 是 ROS2 的启动管理系统,用来一次性启动和配置多个节点及其参数,一般使用的是python的语言
Node(package='turtlesim', # 节点所在功能包executable='turtlesim_node', # 要运行的可执行文件name='sim', # 节点名称(相当于 ros2 run ... --ros-args -r __node:=sim)namespace='my_ns', # 节点命名空间output='screen', # 日志输出方式:screen (终端显示) / log (保存到日志文件)emulate_tty=True, # 保证输出有颜色(常用于 Docker/终端)parameters=[ # 节点参数{'background_r': 255, 'background_g': 255, 'background_b': 255}, '/path/to/config.yaml'],remappings=[ # 话题重映射('/turtle1/cmd_vel', '/cmd_vel'),('/turtle1/pose', '/pose')],arguments=['--ros-args', '--log-level', 'INFO'], # 额外命令行参数ros_arguments=['--remap', '__ns:=/robot1'], # 专门的 ROS 参数(新版本推荐)respawn=True, # 节点崩溃时是否自动重启respawn_delay=2.0, # 重启延迟时间(秒)prefix='gdb -ex run --args', # 前缀命令,比如用 gdb/valgrind 启动节点cwd='ros_home', # 设置工作目录env={'MY_ENV_VAR': 'value'} # 传递环境变量
)
我们使用launch同时启动客户端和服务端两个节点
from launch import LaunchDescription
from launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([Node(package='demo_python_service', # 功能包名executable='faceDetectServiceNode', # 服务端入口name='face_detect_service_node',output='screen'),Node(package='demo_python_service',executable='faceDetectClientNode', # 客户端入口name='face_detect_client_node',output='screen'),])
同时可以在launch中声明参数,但如果需要给节点的话需要一个转换
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([# 声明参数DeclareLaunchArgument('model',default_value='hog',description='Face detect model: hog or cnn'),DeclareLaunchArgument('upsample',default_value='1',description='Number of times to upsample'),# 启动服务端Node(package='demo_python_service',executable='faceDetectServiceNode',name='face_service',parameters=[{'model': LaunchConfiguration('model'),'number_of_times_to_upsample': LaunchConfiguration('upsample')}]),# 启动客户端Node(package='demo_python_service',executable='faceDetectClientNode',name='face_client')])
launch 三大核心组件
-
LaunchDescription(启动描述)
-
作用:是整个 Launch 文件的入口,用来组织和返回所有要执行的动作。
-
特点:所有节点、参数、事件处理器等都需要被封装到
LaunchDescription([...])
中。
-
-
Action(动作)
-
作用:定义实际要做的事情,比如启动节点(
Node
)、声明参数(DeclareLaunchArgument
)、设置环境变量(SetEnvironmentVariable
)等。 -
特点:是 launch 文件中真正会被执行的“任务单元”。
-
-
Substitution(替换/变量)
-
作用:在运行时动态生成或替换值,比如包路径(
FindPackageShare
)、命令行参数(LaunchConfiguration
)、环境变量(EnvironmentVariable
)等。 -
特点:为 Action 提供灵活的参数输入。
-
LaunchDescription 管理 → Action 执行 → Substitution 提供动态参数。