当前位置: 首页 > news >正文

ROS2之服务

ROS2 的服务(Service)

  • 概念
    服务是一种 请求-响应(request-response)通信机制,由 客户端(Client)服务端(Server) 两部分组成。

  • 通信模式

    • 客户端 发送一个请求(Request)。

    • 服务端 处理请求,并返回响应(Response)。

    • 一问一答,适合短时操作。

  • 特点

    • 一对一通信,不像话题那样是一对多。

    • 请求和响应的数据类型是 固定定义好的 .srv 文件(里面有请求字段和响应字段)。

    • 适合执行需要确认结果的操作。

  • 典型应用场景

    • 查询机器人位置。

    • 控制执行某个单次动作(例如开关灯、拍照)。

    • 获取传感器的单次读数。

服务的本质是通过两个交互的话题实现的,当你创建一个服务时,系统会自动生成两个隐藏的话题:

  1. 请求话题(Request Topic)

    • 客户端(Client)通过这个话题向服务端(Server)发送请求消息。

  2. 响应话题(Response Topic)

    • 服务端(Server)通过这个话题把处理结果返回给客户端。

一个服务 = 请求话题 + 响应话题

一般来说客户端节点会向请求话题发布消息,然后从响应话题得到消息,同理服务端向响应话题发布消息,从请求话题得到消息,二者的对于不同的话题交互都是单向的,保证了一个负责请求,一个负责响应。

image

基于服务的参数通信

参数通信

  • 在 ROS2 中,参数(Parameter) 是节点的配置项,用来控制节点的行为。
    例如:

    • turtlesim_node 的背景颜色

    • 激光雷达的刷新频率

    • 控制器的 PID 系数

  • 参数通信 指的是:节点之间通过 服务机制(Service)设置、修改、查询参数

ROS2 参数通信的特点

  1. 一对一的服务通信

    • 参数操作是请求-响应模式(类似打电话)。

    • 客户端(如 ros2 param 命令)发送请求,服务端(节点)返回结果。

  2. 内置服务接口
    ROS2 每个支持参数的节点都会自动提供一些参数相关的服务:

    • /node_name/get_parameters 获取参数值

    • /node_name/set_parameters 设置参数值

    • /node_name/list_parameters 列出参数

    • /node_name/describe_parameters 获取参数描述(类型、范围等)

  3. 接口类型定义
    所有参数相关的服务接口都在 rcl_interfaces 包中定义,比如:

    • rcl_interfaces/srv/GetParameters

    • rcl_interfaces/srv/SetParameters

通过参数通信调节设置小海龟的背景颜色,在 ROS2 中,也可以通过 YAML 配置文件提前写好参数,然后在启动节点时加载,让节点自动带着这些参数运行。

image

话题消息接口为功能包下的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 是一种命名方式,规则是 每个单词的首字母大写且不使用下划线或空格,例如:GetFaceInfoPersonMsg

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()

 image

image

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()

先启动海龟模拟强,再启动客户端,然后启动服务端

image

参数设置

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 命令查看,不过需要先启动节点。

image

我编写客户端代码,调用其自带的参数设置服务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()

 image

 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> &parameters)-> 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'),])

image

同时可以在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 三大核心组件

  1. LaunchDescription(启动描述)

    • 作用:是整个 Launch 文件的入口,用来组织和返回所有要执行的动作。

    • 特点:所有节点、参数、事件处理器等都需要被封装到 LaunchDescription([...]) 中。

  2. Action(动作)

    • 作用:定义实际要做的事情,比如启动节点(Node)、声明参数(DeclareLaunchArgument)、设置环境变量(SetEnvironmentVariable)等。

    • 特点:是 launch 文件中真正会被执行的“任务单元”。

  3. Substitution(替换/变量)

    • 作用:在运行时动态生成或替换值,比如包路径(FindPackageShare)、命令行参数(LaunchConfiguration)、环境变量(EnvironmentVariable)等。

    • 特点:为 Action 提供灵活的参数输入。

LaunchDescription 管理 → Action 执行 → Substitution 提供动态参数。

http://www.hskmm.com/?act=detail&tid=23192

相关文章:

  • macOS上优雅运行Docker容器
  • 题解:CF1770H Koxia, Mahiru and Winter Festival
  • HarmonyOS之LocalStorage - 详解
  • Spring Boot Logback:实现定时任务日志与业务日志隔离 - Higurashi
  • 网络流 最小割 Dinic算法
  • 15.VLANIF(2025年9月30日) - 教程
  • 树莓派搭建NAS之一:安装系统
  • 新手Markdown学习
  • 马云归来,“新零售”不死 - 指南
  • RNN
  • 10.2笔记
  • Shell / Bash 学习
  • 【Linux 架构探幽:从入门到内核・系统编程开篇】基础指令与权限精讲,筑牢框架制作根基
  • 使用 Dart 进行验证码识别
  • 用 Rust 进行验证码识别
  • teset3
  • Java并发编程(5)
  • 定时任务详解
  • 华为wlan无线配置 - 教程
  • PINN训练新思路:把初始条件和边界约束嵌入网络架构,解决多目标优化难题
  • 可持久化数据结构
  • 2025.10.2——1黄
  • 图的匹配
  • Tarjan 算法
  • Mondriaans Dream题解
  • 网络流 最大流 Dinic 算法
  • 2025.10.2 测试
  • 数学章节总结
  • 软件工程_作业一
  • CF VP 记录