继"ROS2 Jazzy + C++ 实战路线------基础学习1"继续学习
程序整体架构:
┌──────────────────┐
│ my_first_node │
│ │
│ Publisher │
└────────┬─────────┘
│
│ std_msgs/msg/String
↓
/hello
│
│ std_msgs/msg/String
↓
┌────────┴─────────┐
│ hello_subscriber │
│ │
│ Subscription │
└──────────────────┘
注意:
Publisher:
create_publisher<std_msgs::msg::String>(
"/hello",
10
);
Subscriber:
create_subscription<std_msgs::msg::String>(
"/hello",
10,
...
);
它们能通信,需要最基本的两个条件:
Topic 名称一样:
/hello
消息类型一样:
std_msgs/msg/String
这一点非常重要。
例如 Publisher 发:
/hello
std_msgs/msg/String
Subscriber 却订阅:
/abc
那就收不到。
或者 Topic 都叫:
/hello
但消息类型不同,也不能正常配对,这是理解 ROS2 非常重要的一步,看看 Publisher 代码,里面有没有:
#include "hello_subscriber.hpp"
没有。
有没有:
HelloSubscriber IP
没有。
有没有:
调用 HelloSubscriber::topic_callback()
也没有。
Subscriber 也不知道 Publisher 的类名。
所以:
Publisher
实际上只知道:
我要往 /hello 发 String
Subscriber 只知道:
我要从 /hello 收 String
它们完全不需要:
直接认识对方
这就是:解耦
1. 什么叫"解耦"?
比如传统程序可能是:
A程序
↓
直接调用
↓
B程序
于是 B 改了:
函数名
接口
地址
实现
A 也可能需要跟着改。
ROS2 Topic 更像:
A
↓
把数据放到 /hello
B
↓
从 /hello 拿数据
A 不关心:
到底有没有 B
B 是谁
B 是 C++ 还是 Python
B 做什么
B 也不关心 A 的内部实现。
2. ROS2 怎么让它们发现彼此?
你可能会问:
我没告诉 Publisher Subscriber 的 IP,它们怎么找到对方?
ROS2 底层通常通过 DDS 完成发现和通信,先理解:
ROS2 / DDS
负责帮助节点发现彼此
Publisher:
"谁订阅 /hello?"
Subscriber:
"我订阅 /hello。"
然后底层建立通信,所以应用代码里面,你不需要手动写:
192.168.1.100
192.168.1.101
这种固定连接逻辑。
不过以后跨机器通信时,我们会学习:
ROS_DOMAIN_ID
DDS Discovery
防火墙
同网段
多网卡
QoS
这些是真机项目会遇到的问题。
现在先不展开。
Type
↓
这个 Topic 传什么数据
Publisher count: 1
↓
有一个人在发
Subscription count: 1
↓
有一个人在收
一个 Publisher 给多个 Subscriber
比如:
/mocap/human_pose
│
┌─────────┼──────────┐
↓ ↓ ↓
GMR Monitor Recorder
动捕节点只发布一次。
然后:
GMR
拿去做重定向。
Monitor
统计频率和掉帧。
Recorder
记录原始数据。Publisher 不需要为三个 Subscriber 分别写三套发送逻辑。
自定义 msg
MocapFrame
│
├── frame_id
├── source
└── joint_positions
这就是自定义 ROS2 Message,我们不把接口塞进 my_first_pkg,这里开始教你一个比较工程化的习惯:
接口单独放一个 Package。
我们创建:
my_interfaces
以后结构就是:
ros2_ws/
└── src/
├── my_first_pkg/
│
│ ├── Publisher
│ └── Subscriber
│
└── my_interfaces/
└── msg/
└── MocapFrame.msg
这也是 ROS2 官方自定义接口教程采用的做法:单独建立一个 ament_cmake 接口包,然后其他 C++ 或 Python Package 使用它。(GitHub)
以后你的真实工程,我们更可能叫:
mocap_msgs
而不是:
my_interfaces
现在为了学习先用简单名字。
3. 创建接口 Package
执行:
cd ~/ros2_ws/src
然后:
ros2 pkg create \
--build-type ament_cmake \
--license Apache-2.0 \
my_interfaces
进去:
cd my_interfaces
创建:
mkdir msg
现在:
my_interfaces/
│
├── CMakeLists.txt
├── package.xml
│
└── msg/
ROS2 规定 .msg 文件放在 Package 的 msg/ 目录中。(GitHub)
4. 创建你的第一个接口
执行:
nano msg/MocapFrame.msg
写:
uint32 frame_id
string source
float64[] joint_positions
保存,这就是一个真正的 ROS2 接口。
配置:修改 CMakeLists.txt
CMakeLists.txt
package.xml
nano ~/ros2_ws/src/my_interfaces/CMakeLists.txt
可以先简化成下面这样:
cmake_minimum_required(VERSION 3.8)
project(my_interfaces)
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MocapFrame.msg"
)
ament_package()
最关键的是:
find_package(rosidl_default_generators REQUIRED)
以及:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MocapFrame.msg"
)
5. rosidl_generate_interfaces 是干什么的?
这一句非常关键。
rosidl_generate_interfaces(...)
大白话:
ROS2,请根据我的
.msg文件生成可以给程序使用的代码。
过程:
MocapFrame.msg
↓
rosidl
↓
自动生成代码
↓
C++可以使用
Python也可以使用
ROS2 官方教程也是通过 rosidl_default_generators 和 rosidl_generate_interfaces() 完成这个过程。(GitHub)
你可以把:
rosidl
暂时理解为:
ROS2 的接口代码生成系统。
不用再深入。下一步 修改 package.xml,打开:
nano ~/ros2_ws/src/my_interfaces/package.xml
在 <package> 里面加入:
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
所以里面会有类似:
<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
这三行各自是什么意思?
先大概记:
rosidl_default_generators
↓
编译时生成接口代码
rosidl_default_runtime
↓
程序运行时使用接口
rosidl_interface_packages
↓
告诉ROS2:
"我是一个接口Package"
这些也是官方 Jazzy 自定义接口配置要求。(GitHub), 编译接口
回到 Workspace:
cd ~/ros2_ws
先:
source /opt/ros/jazzy/setup.bash
然后:
colcon build --packages-select my_interfaces
成功以后:
source install/setup.bash
检查接口有没有生成成功
这是今天非常重要的一条命令:
ros2 interface show my_interfaces/msg/MocapFrame
如果成功,你应该看到:
uint32 frame_id
string source
float64[] joint_positions
看到它,就说明:
你的第一个 ROS2 自定义接口已经创建成功。
官方教程也使用 ros2 interface show <package>/msg/<Message> 来验证接口是否成功生成。(GitHub)
6.接口服务
服务接口:只定义一个 .srv 文件,服务接口定义在包的 srv/ 目录下,一个 .srv 文件里同时写请求 和响应 ,中间用 --- 分隔。
例如 AddTwoInts.srv:
srv
int64 a
int64 b
---
int64 sum
-
---上面是请求(Request) -
---下面是响应(Response)
构建后,ROS 2 会为这一个 .srv 文件生成两个 C++ 结构体:
cpp
AddTwoInts::Request
AddTwoInts::Response
但它们来自同一个接口定义,不是两个独立的文件。
客户端发送请求时用 AddTwoInts::Request,服务端返回时用 AddTwoInts::Response。
服务端和客户端使用的服务类型都是:AddTwoInts
所以服务接口是一个通用定义,包含请求和响应两部分。
发布/订阅:只定义一个 .msg 文件
发布/订阅的消息接口定义在 msg/ 目录下,一个 .msg 文件只描述一种消息结构。
例如 String.msg:
msg
string data
发布者发布 std_msgs::msg::String,订阅者也订阅 std_msgs::msg::String。
两边用的是同一个消息类型,不是分别定义不同的。
所以发布/订阅也是定义一个通用消息类型 ,发布者和订阅者共享它。
关键点:
-
服务接口不是"给发布和订阅分别定义不一样的",因为服务里根本没有发布和订阅。
-
服务接口是一个
.srv文件,里面同时包含请求和响应。 -
客户端和服务端共享这个服务类型,只是各自使用其中的 Request 或 Response 部分。
-
发布/订阅则是一个
.msg文件,发布者和订阅者共享同一个消息类型。
7. 一个非常重要的概念:依赖顺序
现在有:
my_interfaces
↓
先生成 MocapFrame
my_first_pkg
↓
才能使用 MocapFrame
所以不能反过来。
好在:
colcon build
会分析 Package 依赖。
知道:
my_first_pkg
依赖
my_interfaces
所以它会先编:
my_interfaces
再编:
my_first_pkg
这也是 ROS2 Package 化的重要意义。
8. Topic 和 Interface 不要混
这是现在非常容易混的地方。
比如:
/mocap_frame
是:
Topic 名字。
而:
my_interfaces/msg/MocapFrame
是:
消息类型。
可以理解成:
/mocap_frame
= 快递路线
MocapFrame
= 快递箱规格
Publisher 和 Subscriber 想通信,必须:
Topic 对得上
+
Message Type 对得上
9. 现在回头看"接口"到底是什么
到这里应该能真正理解:
MocapFrame.msg
不是一个 Node。
不是 Topic。
不是 Publisher。
它只是:
Node 之间约定好的数据格式。
例如:
uint32 frame_id
string source
float64[] joint_positions
意思就是:
大家以后传动捕数据,都按照这个格式来。
这就是接口约定。
MocapFrame.msg
↓
rosidl_generate_interfaces
↓
生成 C++ 消息类型
↓
my_interfaces::msg::MocapFrame
↓
Publisher
↓
/mocap_frame
↓
Subscriber
如果这张图理解了,你已经把 ROS2 自定义 msg 的主流程学会了。
Workspace
↓
Package
↓
Node
↓
Topic
↓
Publisher / Subscriber
↓
msg 自定义接口
这已经够你理解 ROS2 最基本的数据流了。
接下来我们快速把 ROS2 核心框架补齐。
Service、Action、Parameter 的整体区别
你先记这一张表:
| 机制 | 用来干什么 | 例子 |
|---|---|---|
| Topic | 持续传数据 | IMU、动捕、关节状态 |
| Service | 请求一次,回答一次 | 重置、启动、查询状态 |
| Action | 执行一个需要时间的任务 | 机械臂移动、导航到目标点 |
| Parameter | 配置节点 | IP、端口、频率、模型路径 |
机器人里大多数功能,基本就是这四种。
1. Topic:持续数据流
你已经学过:
mocap_driver
↓
/mocap/human_pose
↓
gmr_retarget
特点是:
一直发
一直收
例如:
50 Hz 动捕
100 Hz IMU
500 Hz 关节状态
所以:
传感器数据、状态数据、控制目标,优先想到 Topic。
2. Service:问一次,答一次
结构:
Client
↓ 请求
Service
↓ 响应
Server
比如:
请求:
启动动捕
响应:
success = true
或者:
请求:
清零编码器
响应:
清零完成
所以 Service 特别适合:
start
stop
reset
enable
disable
get_status
这种操作。
你可以直接记:
Topic 是"持续广播",Service 是"问答"。
3. .srv 接口
前面你学了:
.msg
Service 对应:
.srv
例如:
bool enable
---
bool success
string message
中间:
---
把它分成:
请求
---
响应
所以 .srv 本质也是接口定义。
4. Action:耗时任务
为什么已经有 Service,还需要 Action?
比如:
让机器人走到 5 米外
可能需要:
10 秒
20 秒
你肯定不希望:
发请求
↓
卡着等20秒
↓
最后才返回
所以 ROS2 有:
Action
Action 允许:
发送目标
↓
执行中
↓
不断反馈进度
↓
可以取消
↓
最终返回结果
比如:
目标:
机械臂移动到位置A
反馈:
20%
40%
70%
90%
结果:
成功
所以一句话:
Service 适合快速操作,Action 适合耗时任务。
5. .action 接口
Action 接口一般三部分:
Goal
---
Result
---
Feedback
比如:
目标位置
---
是否成功
---
当前进度
先知道这个结构就够了。
现在完全不需要写 Action Server 代码。
6. Parameter:节点配置
Parameter 对你的项目非常重要,例如你之前的动捕程序可能会写死:
port = 7014;
不推荐。
更好的方式:
source_type = axis
primary_port = 7014
backup_port = 7015
queue_capacity = 100
通过 Parameter 配置。
这样换设备时:
Axis
↓
Xsens
↓
PNLink
不需要重新改代码、重新编译。
只改配置。
这也是 ROS2 工程化很重要的一点。
7. YAML 又是什么?
ROS2 Parameter 通常会配合:
.yaml
例如:
mocap_driver:
ros__parameters:
source_type: "axis"
primary_port: 7014
backup_port: 7015
publish_rate: 50.0
然后 Node 启动时加载。
所以未来你的系统会变成:
代码
+
YAML配置
而不是:
代码里写死所有参数
这点非常重要。
8. Launch:一次启动整个系统
如果有:
mocap_driver
gmr_retarget
robot_output
rviz
总不能每次开四个终端。
所以 ROS2 有:
Launch
一个 Launch 文件可以一次启动:
mocap_driver_node
+
gmr_retarget_node
+
robot_output_node
+
RViz
以后你最终可能只运行:
ros2 launch humanoid_bringup mocap_system.launch.py
整个系统就起来了。
所以:
Launch = 系统启动脚本。
9. rosbag2:ROS2 的"录像机"
这个对你以后调试掉帧、动捕特别有用。
rosbag2 可以录:
Topic 数据
例如:
ros2 bag record /mocap/human_pose
录下来以后,即使 Axis 不在现场,也能:
回放数据
↓
重新跑 GMR
↓
重新分析掉帧
所以你可以理解:
rosbag2 = ROS2 数据录像和回放。
这对机器人调试非常重要。
10. 现在把 ROS2 核心框架串起来
你先掌握这一张图:
ROS2 系统
Launch
↓
┌─────────── Node ───────────┐
│ │
│ Topic │
│ 持续数据 │
│ │
│ Service │
│ 请求 / 响应 │
│ │
│ Action │
│ 长任务 │
│ │
│ Parameter │
│ 配置 │
└─────────────────────────────┘
↓
msg / srv / action
↓
Interface
↓
rosbag2
记录 / 回放
目前这一层你先掌握到 "知道什么时候用什么" 就够了。