ROS2 Jazzy + C++ 实战路线——基础学习2

继"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_generatorsrosidl_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
    记录 / 回放

目前这一层你先掌握到 "知道什么时候用什么" 就够了。

相关推荐
江苏久众新视1 小时前
SOP-AI视觉检测实战:从“专人专用”到“产线普适”的工程落地分享
人工智能·视觉检测
科技研学社1 小时前
自动化缝制升级:支撑产业海外转移,降低建厂用工门槛
人工智能·自动化
m0_462605221 小时前
大模型实战营week7
人工智能
adinnet20261 小时前
Neo4j 凭什么叫原生图数据库?Cypher 查询能做什么
大数据·人工智能
tellmewhoisi1 小时前
机器学习:集成学习2(GBDT算法)
机器学习
小猿君1 小时前
Claude 入口砍到只剩一个,GPT-6 已经能自己连干 24 小时
人工智能·chatgpt·agi
goujunwe1 小时前
从效果衰减到稳定增长:GEO 常态化运维实战指南
人工智能
AR-26710-1 小时前
Linux Day7——建组/用户、umask、Python脚本
linux·python
朗锐智科_机器视觉1 小时前
光电传感器:机器视觉的“触发与到位“
人工智能·机器视觉·传感器·工控机·图像采集卡