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 time

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

        #等待服务启动->构建请求->异步发送请求不堵塞线程->future作为容器存放响应此时为空->当获取响应时自动调用回调函数然后将响应存入future
    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 time

class 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 = 1
        self.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 response

def 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 files
  set(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 files
  set(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').value
        self.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').value
        self.model= self.get_parameter('model').value
        self.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.value
            if paramter.name== 'model':
                self.model = paramter.value
         return 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 SetParametersResult

class 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').value
        self.model= self.get_parameter('model').value
        self.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.value
            if paramter.name== 'model':
                self.model = paramter.value
        return 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_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 response

def 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,ParameterValue

class 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 = parameters
        future = 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 = model
        param_value.type =ParameterType.PARAMETER_STRING
        param.value = param_value
        response = 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 Node

def 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 Node

def 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 提供动态参数。

posted @ 2025-10-02 22:38  突破铁皮  阅读(22)  评论(0)    收藏  举报