ROS2自定义接口消息、参数服务案例

接着前面的博客,我们继续深入,我们先聊自定义接口消息。

一. ROS2自定义接口消息概念介绍

回想一下前面博客,话题用的消息msg是一个String类型,服务接口类型用的是官方示例里自定义的AddTwoInts类型,动作接口类型是自定义的。实际上不管是话题,服务,动作都是可以自定义接口的。接口的作用是为了定义传输数据的结构,这样方便分布在不同位置的程序间,互相收发数据。接口 = 通信协议 = 数据格式约定。 没有接口,节点之间根本不知道对方发的是什么、怎么解析。

1. 话题、服务、动作接口(数据格式约定)的区别,自定义接口要满足的规则

通信方式 接口文件后缀 结构 用途
话题(Topic) .msg 一段数据 连续传输(传感器、速度)
服务(Service) .srv 两段数据(请求 + 响应) 一次性调用(查询、开关)
动作(Action) .action 三段数据(目标 + 结果 + 反馈) 长时间任务(导航、机械臂)

如下是举例:

话题接口.msg

复制代码
int32 X
int32 Y
int32 Z

服务接口.srv

复制代码
# 请求(Request)
int32 A
int32 B
int32 C
---
# 响应(Response)
int32 Sum
int32 Accumulate

动作接口.action

复制代码
# 目标(Goal)
float32 TargetX
float32 TargetY
---
# 结果(Result)
bool Success
---
# 反馈(Feedback)
float32 Progress

后面再自定义接口类型时,按照上面方式照搬即可:

---是用来区分多个部分

每个不分可支持任意多个变量

命名规则:

  1. 必须大写字母开头
  2. 不能有下划线 _
  3. 只能用字母、数字

不用管为什么要满足这些规则,毕竟是人家制定的游戏规则,咱们按照这个来做就行。

2.可直接使用的接口

可看到,在前面博客介绍话题时,发送的消息并没有去自定义接口。那是因为ROS2自带自带标准接口类型 ,或者提供的官方示例(随安装ROS2时自动安装上了),不用自己定义,直接拿来用即可。

1)自带标准数据类型

复制代码
int8    int16    int32    int64
uint8   uint16   uint32   uint64
float32 float64

bool    # 真/假
string  # 字符串

int32[]      # 整数数组
float64[]    # 浮点数数组
string[]     # 字符串数组

2)话题(Topic)自带常用高级接口

复制代码
几何消息(控制机器人)
Point        # 点 (x,y,z)
Pose         # 位姿 (位置+朝向)
Twist        # 速度 (线速度+角速度)
Quaternion   # 四元数
Vector3      # 三维向量

传感器消息(雷达、相机、IMU)
mage        # 图片
Imu          # 陀螺仪/加速度计
LaserScan    # 激光雷达
Joy          # 手柄
Range        # 超声波

导航消息
Odometry     # 里程计
Path         # 路径

状态 / 日志
Header       # 时间戳+坐标系
Empty        # 空消息
Bool         # 布尔
Int32        # 整数
Float32      # 浮点数
String       # 字符串

3)服务(Service)自带常用接口

复制代码
最常用官方服务
Empty        # 无请求、无响应
SetBool      # 请求:bool,响应:bool+string
Trigger      # 请求:空,响应:成功+消息

示例中服务
AddTwoInts
SetBool  
Trigger  
Empty

导航 / 控制服务
nav2_msgs
ros2_control

ROS2 的示例服务(含 AddTwoInts)都在 example_interfaces 包里;

系统自带官方服务在 std_srvs

小乌龟示例在 turtlesim

还有就是要手动安装的服务,可以通过第三方包直接安装,如Nav2 和 ros2_control

sudo apt update

sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup

sudo apt install ros-humble-ros2-control ros-humble-ros2-controllers

分别对应如下的路径:

/opt/ros/humble/share/example_interfaces/srv/

/opt/ros/humble/share/std_srvs/srv/

/opt/ros/humble/share/turtlesim/srv/

/opt/ros/humble/share/nav2_msgs/srv/

/opt/ros/humble/share/control_msgs/srv/

4)动作自带标准接口

复制代码
example_interfaces/action/Fibonacci(官方教程用)
turtlesim/action/RotateAbsolute(小乌龟示例)

其它是安装第三方库,能用的动作接口,如Nav2、MoveIt2带的,不属于基础 ROS2 默认安装。

3. 小乌龟示例介绍

turtlesim(小乌龟)是自带完整 "话题 + 服务 + 动作" 三合一的官方示例 ,接口全在 turtlesim 包里,安装 ROS2 桌面版就自带。

话题(Topic)- 持续数据流,主要示例了如下3个:

复制代码
/turtle1/cmd_vel → geometry_msgs/msg/Twist(速度指令,控制运动)
/turtle1/pose → turtlesim/msg/Pose(乌龟坐标 + 角度)
/turtle1/color_sensor → turtlesim/msg/Color(轨迹颜色)

服务(Service)- 请求 - 应答,主要示例了如下

复制代码
/clear → std_srvs/srv/Empty(清轨迹)
/reset → std_srvs/srv/Empty(重置)
/spawn → turtlesim/srv/Spawn(生成新乌龟)
/kill → turtlesim/srv/Kill(杀死乌龟)
/turtle1/set_pen → turtlesim/srv/SetPen(设置画笔)
/turtle1/teleport_absolute → turtlesim/srv/TeleportAbsolute(绝对瞬移)
/turtle1/teleport_relative → turtlesim/srv/TeleportRelative(相对瞬移)

动作(Action)- 带反馈 + 可取消(ROS2 重点),主要示例了如下

复制代码
/turtle1/rotate_absolute → turtlesim/action/RotateAbsolute(转到绝对角度,有进度反馈)

接口位置全在全在 /opt/ros/humble/share/turtlesim/

消息:/opt/ros/humble/share/turtlesim/msg/

服务: /opt/ros/humble/share/turtlesim/srv/

动作:/opt/ros/humble/share/turtlesim/action/

但依赖的公共接口(不在 turtlesim 里)

  • geometry_msgs/msg/Twist
  • std_srvs/srv/Empty
  • rcl_interfaces/...(参数服务)

那想一想为什么安装ROS2会自带安装小乌龟示例呢?

这和一些开源软件,商业软件一样,要方便一个没有接触过的人自己学习。一般都是通过提供sample示例 + 帮助文档方式让新手快速入门,并快速展示自己的肌肉和实力。那么小乌龟示例的作用也在此,其虽小,但五脏俱全。

1)让新手不用真实机器人,也能立刻跑通 ROS2,它有模拟机器人的功能,方便身边没有实物机器人的时候做一些验证

2)一次性演示:话题 + 服务 + 动作

3)还能用来测试你的 ROS2 环境是否正常

4)可以认为小乌龟 = ROS2 的 "Hello World,通过它能够快速入门。

二. ROS2自定义接口消息使用

1. 自己手写话题、服务、动作接口

为了演示,这边一次性创建所有类型接口,并且会创建多个各类型接口,一个是单变量,一个是多变量。

复制代码
ros2 pkg create --build-type ament_cmake my_custom_interfaces

然后在包目录下,去手动或者命令创建msg,srv,action文件夹,注意名字得就是它们,不要改名。这些名称是 ROS 社区约定俗成的标准目录名,不是强制语法,但全行业统一遵守,目的是规范结构、一眼看懂用途、工具自动识别。

然后在msg,srv,action中添加接口文件,注意首字母需大写,且不能有下划线如下:

Msgdemo1.msg如下:

复制代码
#名字
string name

#年龄
int32 age

Msgdemo2.msg如下:

复制代码
#爱好
string hobby

Srvdemo1.srv如下:

复制代码
# 请求 Request
int32 score       # 得分
float32 weights   # 体重

---

# 响应 Response
bool success
string message   #得分值和体重值

Srvdemo2.srv如下:

复制代码
# 请求 Request
string address

---

# 响应 Response
string message   #地址

Actiondemo1.action如下:

复制代码
# Goal 目标
int32 count
int32 x
int32 y

---

# Feedback 过程反馈
int32 index       #计算到第几次了
int32 feedback_c   #和
int32 feedback_accumulate #积


---

# Result 最终结果
float32 sum
float32 accumulate

Actiondemo2.action如下:

复制代码
# Goal 目标
int32 count
---

# Feedback 过程反馈
int32 index       #计算到第几次了

---

# Result 最终结果
float32 sum

接下来修改CMakeLists.txt

复制代码
cmake_minimum_required(VERSION 3.8)
project(my_custom_interfaces)

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)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)

# 生成接口
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Msgdemo1.msg"
  "msg/Msgdemo2.msg"
  "srv/Srvdemo1.srv"
  "srv/Srvdemo2.srv"
  "action/Actiondemo1.action"
  "action/Actiondemo2.action"
)

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

ament_package()

修改package.xml

复制代码
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_custom_interfaces</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="yahboom@168.com">jetson</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  
  <buildtool_depend>rosidl_default_generators</buildtool_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <depend>action_msgs</depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

接下来编译接口包

复制代码
colcon build --packages-select my_custom_interfaces

编译成功效果

如下是验证接口是否生成成功:

首先用interface show下接口

复制代码
ros2 interface show 消息类型
# 示例
ros2 interface show std_msgs/msg/String

注:ros2 interface list | grep 可以搜索包含关键字的接口。

这边用如下两句话看下上面定义的话题消息接口。

python 复制代码
ros2 interface show my_custom_interfaces/msg/Msgdemo1
ros2 interface show my_custom_interfaces/msg/Msgdemo2
ros2 interface show my_custom_interfaces/srv/Srvdemo1
ros2 interface show my_custom_interfaces/srv/Srvdemo2
ros2 interface show my_custom_interfaces/action/Actiondemo1
ros2 interface show my_custom_interfaces/action/Actiondemo2

能正常展示字段,说明接口编译完成。

2. 接下来就测试自己手写的这些接口吧

1)测试话题接口

下面开始代码调用接口示例(即在一个可执行程序中的节点对象中,去使用话题发布接收满足上面数据规则的接口消息)。这边博主继续复用之前的python包,新建一个topic_use_customer_interface_publisher_side.py文件,在里面实现话题的发布,代码如下:

python 复制代码
import rclpy
from rclpy.node import Node
from my_custom_interfaces.msg import Msgdemo1
from my_custom_interfaces.msg import Msgdemo2

from rclpy.executors import MultiThreadedExecutor

class TopicTalker1(Node):
    def __init__(self):
        super().__init__("topic_publisher_node1")
        self.pub = self.create_publisher(Msgdemo1, "/Msgdemo1_topic", 10)
        self.timer1 = self.create_timer(1.0, self.pub_cb1)
        self.timer2 = self.create_timer(1.0, self.pub_cb2)
        self.count = 0

    def pub_cb1(self):
        msg = Msgdemo1()
        msg.name = "topic_publisher_node1_timer1"
        msg.age = 5
        self.pub.publish(msg)
        self.get_logger().info("节点1_定时器1发布数据")

    def pub_cb2(self):
        msg = Msgdemo1()
        msg.name = "topic_publisher_node1_timer2"
        msg.age = 15
        self.pub.publish(msg)
        self.get_logger().info("节点1_定时器2发布数据")


class TopicTalker2(Node):
    def __init__(self):
        super().__init__("topic_publisher_node2")
        self.pub = self.create_publisher(Msgdemo2, "/Msgdemo2_topic", 10)
        self.timer = self.create_timer(1.0, self.pub_cb)

    def pub_cb(self):
        msg = Msgdemo2()
        msg.hobby = "/topic_publisher_node2"
        self.pub.publish(msg)
        self.get_logger().info("节点2发布数据")


def main(args=None):
    # 全局初始化一次ROS上下文
    rclpy.init(args=args)
    node1 = TopicTalker1()
    node2 = TopicTalker2()

    # 多线程,让两个节点同时跑
    executor = MultiThreadedExecutor()
    executor.add_node(node1)
    executor.add_node(node2)

    try:
        executor.spin()
    finally:
        executor.shutdown()
        node1.destroy_node()
        node2.destroy_node()
        rclpy.shutdown()

if __name__ == "__main__":
    main()

再创建一个名为topic_use_customer_interface_subscriber_side.py的文件,在里面实现话题的订阅

python 复制代码
# 导入相关的库
import rclpy
from rclpy.node import Node
from my_custom_interfaces.msg import Msgdemo1
from my_custom_interfaces.msg import Msgdemo2


class Topic_Sub(Node):
    def __init__(self, name):
        super().__init__(name)
        # self.create_subscription(
        #    msg_type,  # 1. 消息类型(必须)
        #    topic,  # 2. 话题名称(必须)
        #    callback,  # 3. 收到消息后的回调函数(必须)
        #    qos_profile,  # 4. 队列长度(必须)
        #    callback_group=None,  # 5. 回调组(可选)
        # )

        # 4个必须参数(你写代码必写)
        # msg_type 消息类型,如String、Image、LaserScan
        # topic 话题名字字符串,如"my_topic"
        # callback 收到消息后自动调用的函数
        # qos_profile队列长度,一般写10

        self.sub1 = self.create_subscription(Msgdemo1, "/Msgdemo1_topic", self.sub_callback1, 1)
        print("1")
        self.sub2 = self.create_subscription(Msgdemo2, "/Msgdemo2_topic", self.sub_callback2, 1)

    def sub_callback1(self, msg):
        # print(msg.data,flush=True)
        self.get_logger().info(f"{msg.name},{msg.age}")

    def sub_callback2(self, msg):
        # print(msg.data,flush=True)
        self.get_logger().info(f"{msg.hobby}")


def main():
    rclpy.init()
    sub_demo = Topic_Sub("topic_subscriber_node")  # 创建对象并进行初始化
    rclpy.spin(sub_demo)
    sub_demo.destroy_node()  # 销毁节点对象
    rclpy.shutdown()  # 关闭ROS2 Python接口

再继续编辑配置文件setup.py, 如下:

完毕后,编译功能包 colcon build

下来我们验证下是否设置成功,先运行含有发布话题的节点的可执行程序

python 复制代码
ros2 run pythonpackagedemo1 topic_use_customer_interface_publisher_side

再运行含有订阅话题的节点的可执行程序

python 复制代码
ros2 run pythonpackagedemo1 topic_use_customer_interface_subscriber_side

发布订阅效果如下:

我们再看看一些额外额信息,如节点、话题数据

命令发布话题数据

python 复制代码
ros2 topic pub -r 2 /Msgdemo1_topic my_custom_interfaces/msg/Msgdemo1 "{name: 'cmd_pub', age: 20}"

命令行订阅话题数据

python 复制代码
ros2 topic echo /Msgdemo1_topic

查看话题宽带和频率

python 复制代码
ros2 topic hz /Msgdemo1_topic
ros2 topic bw /Msgdemo1_topic

命令执行效果如下:

2)测试服务接口

下面开始代码调用接口示例(即在一个可执行程序中的节点对象中,去使用服务的服务端接收满足上面数据规则的接口消息目标,然后执行一些操作)。这边博主继续复用之前的python包,新建一个service_use_cusomer_interface_server_side.py文件,在里面实现话题的发布,代码如下:

python 复制代码
# 导入相关的库文件
import rclpy
from rclpy.node import Node
from my_custom_interfaces.srv import Srvdemo1
from my_custom_interfaces.srv import Srvdemo2


class Service_Server(Node):
    def __init__(self, name):
        super().__init__(name)
        # 作用是:注册一个服务,监听客户端的请求,并调用回调函数处理请求。
        self.srv1 = self.create_service(Srvdemo1, '/srv1', self.trans1)
        self.srv2 = self.create_service(Srvdemo2, '/srv2', self.trans2)

    def trans1(self, request, response):
        response.success = True
        response.message = f"I have connected the info: score->{request.score},weigts->{request.weights}"
        print(response.message)
        return response

    def trans2(self, request, response):
        response.message = f"I have got the info: address->{request.address}"
        print(response.message)
        return response

    # ROS2规定服务回调函数必须有两个参数:
    # request:客户端发来的请求数据(这里是两个整数a、b)
    # response:服务端返回给客户端的响应数据(这里是求和结果sum)
    # 函数最后必须return response。


# 话题 = 聊天、广播、持续发消息(单向)
# 服务 = 打电话、请求 - 响应、干完就挂(双向)

# 话题(Topic)= 大喇叭广播
# 老师拿着喇叭一直喊:"现在温度 25 度!现在速度 1m/s!"
# 学生们随便听,想听就听,不想听就不听
# 不用回复,老师只管喊

# 服务(Service)= 打电话
# 你打给朋友:"帮我算一下 3+5 等于几?"(请求)
# 朋友算完:"等于 8"(响应)
# 必须一问一答,打完电话就结束

# 特性	       话题 (Topic)	         服务 (Service)
# 通信模式	   发布 → 订阅(单向)	     请求 → 响应(双向)
# 是否需要回复   不需要	              必须等待回复
# 使用场景	   传感器数据、持续状态	      执行任务、计算、开关
# 频率	       高频(几十~几百 Hz)	  低频(偶尔调用)
# 连接方式	   多对多	              一对一
# 阻塞	       不阻塞	              会阻塞(等结果)

# 只要是持续不断、实时传输的数据,都用话题:
# 摄像头图像
# 激光雷达数据
# 机器人位置、速度
# 温度、气压
# 遥控器指令
# 特点:只管发,不管对方收没收到。

# 只要是需要执行一个动作、需要结果、需要确认的,都用服务:
# 让机器人移动到某个点
# 计算两个数的和
# 打开 / 关闭传感器
# 保存一张图片
# 重置系统
# 查询机器人当前状态
# 特点:我叫你做,你必须做完告诉我结果。

# 最关键的区别:有没有响应
# 数据流用话题,任务调用用服务!

def main():
    rclpy.init()
    server_demo = Service_Server("service_server_node")
    rclpy.spin(server_demo)
    server_demo.destroy_node()  # 销毁节点对象
    rclpy.shutdown()  # 关闭ROS2 Python接口

再创建一个名为service_use_customer_interface_client_side.py的文件,在里面实现话题的订阅

python 复制代码
import rclpy
from rclpy.node import Node
from my_custom_interfaces.srv import Srvdemo1
from my_custom_interfaces.srv import Srvdemo2


class ServiceClient(Node):
    def __init__(self, node_name):
        super().__init__(node_name)
        self.cli1 = self.create_client(Srvdemo1, "/srv1")
        self.cli2 = self.create_client(Srvdemo2, "/srv2")
        self.get_logger().info("客户端就绪,等待服务...")

    # 异步发起srv1请求(不阻塞)
    def send_req_srv1(self, score, weights):
        if not self.cli1.wait_for_service(timeout_sec=2.0):
            self.get_logger().error("/srv1 服务未上线")
            return None
        req = Srvdemo1.Request()
        req.score = score
        req.weights = weights
        future = self.cli1.call_async(req)
        return future

    # 异步发起srv2请求(不阻塞)
    def send_req_srv2(self, address):
        if not self.cli2.wait_for_service(timeout_sec=2.0):
            self.get_logger().error("/srv2 服务未上线")
            return None
        req = Srvdemo2.Request()
        req.address = address
        future = self.cli2.call_async(req)
        return future


def main():
    rclpy.init()
    client = ServiceClient("service_client_node")

    # 1. 一次性同时发起两个异步请求,不等待返回
    fut1 = client.send_req_srv1(score=95, weights=88.0)
    fut2 = client.send_req_srv2(address="Suzhou Wujiang")

    future_list = []
    if fut1 is not None:
        future_list.append(fut1)
    if fut2 is not None:
        future_list.append(fut2)

    # 2. 自旋等待所有请求全部完成
    while future_list:
        rclpy.spin_once(client)
        for f in future_list[:]:
            if f.done():
                res = f.result()
                if f == fut1:
                    client.get_logger().info(f"【srv1结果】success:{res.success}, msg:{res.message}")
                elif f == fut2:
                    client.get_logger().info(f"【srv2结果】msg:{res.message}")
                future_list.remove(f)

    client.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

再继续编辑配置文件setup.py, 如下:

完毕后,编译功能包

下来我们验证下是否设置成功,先运行含有服务端的节点的可执行程序

python 复制代码
ros2 run pythonpackagedemo1 service_use_cusomer_interface_server_side

再运行含有客户端可执行程序

python 复制代码
ros2 run pythonpackagedemo1 service_use_customer_interface_client_side

服务请求和响应执行效果如下:

我们再看看一些额外额信息,如节点、话题数据

命令行调用服务客户端(发起请求)

python 复制代码
ros2 service call /srv1 my_custom_interfaces/srv/Srvdemo1 "{score: 90, weights: 25.4}"

执行效果如下:

3)测试动作接口

博主这边就不去测试了,因为之前博客中有完整创建动作通讯接口、使用测试的介绍。

ROS2话题、服务、动作通讯-CSDN博客文章浏览阅读221次,点赞9次,收藏6次。介绍了如何在工作空间中,创建包,并在包中创建一个或多个可执行程序,程序里定义了单个或多个节点类对象,以便可执行程序运行起来的时候,类对象能够执行动作,干一些事情。那多个可执行程序运行起来时,他们之间靠什么来通讯呢,常规是走tcp/ip通讯、共享内存等其它,但在ROS2中,已定定义了一些现成的方式,封装好了三种标准通信方式:话题,服务,动作通讯。注:话题,服务,动作通讯其实是在填充节点对象的功能,后面的节点对象就不再像前面博客演示那样,只打印几句话而已。为什么要有动作,它和话题,服务的区别是什么?https://blog.csdn.net/jiugeshao/article/details/161753260?spm=1001.2014.3001.5502

二. 参数服务案例

类似编程中的全局变量,可以便于在多个程序中共享某些数据,参数是ROS机器人系统中的全局字典,可以运行多个节点中共享数据。参数的特性非常丰富,比如某一个节点共享了一个参数,其他节点都可以访问,如果某一个节点对参数进行了修改,其他节点也有办法立刻知道,从而获取最新的数值。

每个节点自带一组内置服务:get_parameters / set_parameters / list_parameters

  • 存储:键值对(bool/int/double/string/ 数组)
  • 生命周期:归属单个节点,节点销毁参数消失
  • 定位:节点运行配置、开关、标定常量、静态设置、低速微调

用话题、服务、动作可以实现参数服务同样的功能,但为了修改参数,往往要自定义消息接口,要编译,要创建服务端,客户端或者发布者,订阅者,其实没有必要这么繁琐,使用参数服务可以简化操作。如下是它们的横向对比。

方式 通信模型 典型频率 适用场景 优缺点
参数服务 同步 RPC(内置现成服务) 低频,Hz 级以内 配置、标定、启停开关、偶尔改配置 优点:零开发、命令行调试、yaml 批量配置; 缺点:慢、阻塞、不适合高频
话题 Topic 异步发布订阅 任意高频(10~1000Hz) 持续状态推送、实时指令、传感器数据、周期性更新变量 优点:实时、非阻塞、适合高频刷新; 缺点:要自定义消息、启停都要写收发代码,配置管理弱
自定义服务 Service 同步一问一答 RPC 中低频 需要 "下发 + 立即应答结果" 的单次请求 优点:可控应答、可靠性强;缺点:需要手动写服务端 + 客户端,繁琐,不适合单纯配置
动作 Action 异步 + 反馈 + 取消 + 结果 长时任务 耗时任务:导航、机械臂运动、抓取、移动规划 优点:带进度、可取消; 缺点:太重,用来改参数纯属过度设计

接下来我们在上面话题示例,稍微改写下,用上参数服务。

新建topic_use_param_publisher_side.py文件如下:

python 复制代码
import rclpy
from rclpy.node import Node
from my_custom_interfaces.msg import Msgdemo1
from my_custom_interfaces.msg import Msgdemo2

from rclpy.executors import MultiThreadedExecutor

class TopicTalker1(Node):
    def __init__(self):
        super().__init__("topic_publisher_node1")
        self.pub = self.create_publisher(Msgdemo1, "/Msgdemo1_topic", 10)
        self.timer1 = self.create_timer(1.0, self.pub_cb1)
        
        #声明参数
        self.declare_parameter('name', 'topic_publisher_node1_timer1')   
        self.declare_parameter('age', 5)  

        #修改自身参数
        self.set_parameters([
                rclpy.Parameter("name", "modifiedname"),
                rclpy.Parameter("age", 100)
                            ])

        #参数修改回调(外部改参自动触发)
        self.add_on_set_parameters_callback(self.param_cb)

    def param_cb(self, params):
        for p in params:
            self.get_logger().info(f"更新参数 {p.name} = {p.value}")

        return rcl_interfaces.msg.SetParametersResult(successful=True)

    def pub_cb1(self):
        name = self.get_parameter('name').get_parameter_value().string_value   # 从ROS2系统中读取参数的值 
        age = self.get_parameter('age').value   # 从ROS2系统中读取参数的值

        msg = Msgdemo1()
        msg.name = name
        msg.age = age
        self.pub.publish(msg)
        self.get_logger().info("节点1_定时器1发布数据")


def main(args=None):
    # 全局初始化一次ROS上下文
    rclpy.init(args=args)
    node1 = TopicTalker1()
    rclpy.spin(node1)   
    node1.destroy_node()


if __name__ == "__main__":
    main()

接下来配置setup.py,然后编译生成可执行程序,博主这边就不再赘述,上面都有。读者可自行尝试。至于在别的程序里如何访问这些参数,或者修改其值,这边也不再去逐一演示了。到此,自定义消息接口和参数服务地理解及使用方法,已经很清晰了,授人以鱼不如授人以渔。我们学习一样新东西,重点是大局把握住方法,理解它,搞清楚为什么要有它地出现。然后知道怎么去用即可,一些很西化的东西,实际用的时候再去测试或查找资料。

我们最后用命令行测试下上面的参数服务。

python 复制代码
ros2 run pythonpackagedemo1 topic_use_param_publisher_side

执行完毕后使用如下命令语句去监视

python 复制代码
ros2 topic echo /Msgdemo1_topic

可看到未修改参数前打印如下:

用如下命令语句去修改参数

python 复制代码
ros2 param set topic_publisher_node1 name jack

可看到,有各种成熟的反馈机制,使用也越来越得心应手了。如下是一些常用命令,效果如下。

我们再继续实验把保存的参数服务参数从本地读回来(读回来之前,已经手动把参数文件修改了下)

此篇到这儿就结束啦,我们后面在继续深入了解!

相关推荐
AI科技星1 小时前
数术工坊·第八卷 大道归一录・番外・下篇 零界封神・万法归元终章
网络·人工智能·算法·几何学·拓扑学
GZ_TOGOGO1 小时前
Spring AI Alibaba 格式化输出
java·人工智能·spring
Keller-Zhou1 小时前
门店巡检AI图像识别系统技术架构设计
人工智能
giszz1 小时前
【WorkBuddy专栏20】项目指令的深度解析——如何让AI真正理解你的意图
人工智能
小翰生信1 小时前
单细胞m6A研究迎来新利器:Scm6A数据库网站解析
大数据·人工智能·数据库开发·数据库架构·生信分析·atac-seq·染色质可及性分析
kishu_iOS&AI1 小时前
Python Redis客户端 AI应用开发完整指南
人工智能·redis·ai a
江畔柳前堤1 小时前
github实战指南05-Fork与开源协作
人工智能·线性代数·oracle·开源·github·word
大山佬1 小时前
工业故障听诊:单片机上的稀疏音频分类模型部署
人工智能