- 2025-02-25
-
发表了主题帖:
ROS2基础:Gazebo、RViz2、RQt - ROS仿真三幻神
# ROS2基础:Gazebo、RViz2、RQt - ROS仿真三幻神
src:[GZB官文](https://docs.ros.org/en/jazzy/Tutorials/Advanced/Simulators/Gazebo/Simulation-Gazebo.html),[GZB官网](https://gazebosim.org/),[RViz2官文](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/RViz/RViz-Main.html),[RViz指南](https://www.ncnynl.com/ros2docs/cn/rviz2/index.html),[RQt官仓](https://github.com/ros-visualization/rqt),[URDF-ROS2](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/URDF/)
[TOC]
概览:GZB负责“机器人”与“物理环境”交互,产生数据,比如拍摄的“实景”图片,雷达反射,以虚拟模拟现实等等。RViz可以显示这些数据,只需要节点发布对应的话题数据,RViz就可以绑定并订阅与可视化这些数据,且开源插件众多,总有一款能满足奇奇怪怪的数据。RQt体量更MINI,可以可视化ROS日志,显示简单话题数据类型,可视化的发布话题、服务等,绘制数据曲线、节点关系可视化等等。
提示:之前都是直接`mkdir xxx/src`开始开发,但是涉及GZB仿真,可以使用官方推荐的[模板](https://gazebosim.org/docs/latest/ros_gz_project_template_guide/)进行目录管理。
[==========]
## GZB - 物理仿真平台
介绍:GZB是一款开源物理仿真环境,起源于2002年,在2009年工程师开发ROS和PR2过程中实现了在GZB中的仿真,自此GZB成为ROS社区中被使用最多的仿真器。后续经过不断发展改进甚至重新开发,目前`[2025.02.25-Jazzy]`的版本为:`GZB SIM`,目前使用的版本号为`Harmonic`。所以可以在配合ROS进行仿真之前,可以先过一遍GZB的[QS(Quick Start on Docs)](https://gazebosim.org/docs/latest/getstarted/),先体会一番GZB的基本功能。
介绍:使用`XACRO(URDF)`描述构建机器人模型,后将模型加载到GZB仿真环境中,控制机器人与环境交互,从而使各功能节点获得模拟的环境信息。其中,控制以及获取GZB仿真数据的方式是通过话题进行数据交换。
```shell
# 安装ROS-GZB功能包:
sudo apt install ros-${ROS_DISTRO}-ros-gz -y
# 安装GZB-HMC:
sudo apt install lsb-release wget gnupg -y
# 添加公钥
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
# 添加软件源
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
# 安装本体
sudo apt update
sudo apt install gz-harmonic -y
# 查看是否安装成功:
gz sim --version
```
### URDF
URDF(Unified Robot Description Format)-统一机器人描述格式,基于XML描述机械结构(外观、物理性质、甚至可以描述外部环境)。使用过Solidworks的机甲大师们对机械结构的建模应该很熟悉,只不过使用URDF就像把原来鼠标点点可以设置的变为了使用XML描述,零件之间的配合变为了``而已。同时,SW与URDF/XACRO可以使用SW官方插件:SW2URDF,同时Blender、FreeCAD等等建模软件也含有类似的插件或提供直接的导入导出功能。
一个URDF文件(.xml/.urdf)往往为以下格式:
```xml
```
其中,机械结构的描述分为:描述刚体的``和描述刚体之间关系/配合/连接的`joint`:
```xml
...
...
```
Link标签内部可以定义刚体的:
```urdf
```
可以看到,在SW中,通过基本形状绘制草图拉伸裁切所能达成的效果,使用URDF也可以达成!甚至包括其基本的配合:
```urdf
```
其中,常用的配合/关节类型有:(其余请STFW/QTFGPT)
| 配合类型 | 描述 | 在SW中类似于 |
|:--:|:--:|:--:|
| continuous | 旋转关节 | 旋转配合 |
| revolute | 角度限制的旋转关节 | 角度配合 |
| prismatic | 滑动关节 | 滑动配合 |
| planar | 平面关节 | - |
| floating | 浮动关节 | - |
| fixed | 固定关节 | 固定配合 |
在手动创建机器人URDF或XACRO建模的时候,可以参考一下“围攻”,或者简单使用SW绘制草图,并使用草图块进行简单的配合模拟。
### XACRO
XACRO(XML Macro)-可使用宏定义的XML扩展,人话来说就是一个XML模板引擎,可作为URDF的扩展,提供了更强大的功能,如宏定义、条件语句、数学运算和文件包含-include,最终会转换为URDF文件供ROS使用。比如小车四个轮子,URDF得把差不多相同的代码CV四份,但是XACRO可以定义一个零件宏,模块化一个轮子,类似于组件带入参数的方式/直接调用函数的形式生成四个轮子。
如果将一些简单的Link比作SW的零件,那么一个`.xml/.xacro`文件就可视为一个装配体,可以导入、装配零件和子装配体,其在URDF的基础上扩展了:
#### 常量及计算
```xacro
```
#### 宏定义
```xacro
...
...
```
#### 文件包含
```xml
```
#### 建模示例
```xml
...
...
...
...
...
...
...
...
...
...
```
XACRO转URDF:
> 实际上,XACRO本质就是一个扩展了宏定义的XML文件,所以使用支持处理这种XML的类库均支持将附带宏的XML转换为普通的XML,比如:TinyXML、pugixml、Py标准库xml等等,但是推荐还是使用ros2附带的xacro命令(用于将xacro宏展开为urdf后在命令行启动GZB)
```shell
# 可以使用节点形式,意味着可作为launch的Node/Include..LaunchFile来一键启动
ros2 run xacro xacro xxx.xacro > xxx.urdf
# 也可以直接使用命令行命令,当然,也意味着可以os.system(...)或者system(...):
xacro xxx.xacro > xxx.urdf
```
或者直接使用ROS2作为依赖捆绑安装的XACRO库加载之后作为Launch启动GZB的参数:
```python
import xacro
xacro.process_file/parse/process_doc...
```
```c++
#include
xacro::Xacro xacro;
xacro.init...
```
### GZB插件
如果有小伙伴学过Web开发,会发现上述的建模过程类似于页面开发,如果玩过TresJS的小伙伴更是熟悉的没边,如果玩过爬虫比如bs4/xtree或者油猴脚本的同志们,在自定义插件那里也能找到不少熟悉的感觉。没玩过也不影响,只是如果提前玩过这些会更容易理解罢了。
GZB是一个物理仿真平台,可以视为一个物理引擎,可以模拟产生物理交互数据。GZB提供了不少插件用于机器人模型仿真:激光雷达、扫描仪、摄像头等等。你可以使用SDF来搭建模拟的场景,也可以直接在可视化界面里面点点拖拖将工具栏的简易几何体放到场景里。而插件是绑定URDF与代码的桥梁,你可以在代码中直接编写ROS-Node进而将产生的数据通过各种消息传递机制传播,或者利用消息控制在GZB中仿真的模型。
另外,SDF的具体标签与URDF类似,就是多了场景光照,剩余的请STFW。打开GZB的方法:
```shell
# sdf文件也可以在打开GZB后点击insert进行加载
gazebo [] # 也可以直接点:gz sim
# 也可以使用ROS2节点方式启动:
ros2 launch gazebo_ros gazebo.launch.py world:=
# 其中提供了`gz`系列命令处理sdf文件:
gz sdf -p my_model > my_model.sdf # 导出
gz sdf --check my_model.sdf # 验证
gz sdf --format my_model.sdf # 格式化
...
```
#### 自定义插件
以前:先会使用现成的,后会自定义插件,即:自己DIY一个;现在:突然发现先讲自定义之后再说有哪些现成的更符合学习的逻辑,不然只一知半解怎么用,却不知道为什么要这样用!
首先,插件的使用方式就是,使用Plugin标签绑定执行文件,然后在里面配置参数:
```xml
# 看了下面就知道为啥刚体和关节的名字需要唯一了!
...
...
233
1.0
robo_leg1
robo_leg2
...
```
之后,xxx.so实际上由xxx.cpp或其他语言编写的插件编译而成,xxx.cpp为:
```C++
// 有哪些包直接看官方文档,里面全多了
#include
#include
#include
#include
namespace gazebo_plugins
{
class XXX : public gazebo::ModelPlugin
{
public:
XXX() : emm_(666) {} // 设置emm_默认值
void Load(gazebo::physics::ModelPtr model, sdf::ElementPtr sdf) override
{
// 这里学过Js/爬虫的可类比于:node = lxml.html.fromstring(HTML)
ros_node_ = gazebo_ros::Node::Get(sdf);
// 这里可类比于document.querySelector("div") / node.xpath("div")
if (sdf->HasElement("emm")) {
emm_ = sdf->Get("emm");
}
if (!sdf->HasElement("qwe")) {
RCLCPP_ERROR(ros_node_->get_logger(), " * where your ?");
}
qwe_ = sdf->Get("qwe");
// 当你获取到div的时候,可以改变其样式监听其event,这里也一样
left_wheel_joint_ = model->GetJoint("joint_name1");
right_wheel_joint_ = model->GetJoint("joint_name1");
if (!left_wheel_joint_ || !right_wheel_joint_) {
RCLCPP_ERROR(ros_node_->get_logger(), " * you must set two wheel!");
return;
}
// 比如获取其速度,或者受力情况,或者施加力、速度、扭矩等等,并在onUpdate的时候发布
...
}
...
GZ_REGISTER_MODEL_PLUGIN(XXX)
}
```
其中,更详细的插件定制,请移步:[CSDN-GZB控制器插件编写与配置](https://blog.csdn.net/lc1852109/article/details/126450496),[CSDN-传感器插件示例](https://blog.csdn.net/qq_38313901/article/details/119707745),[官方示例](https://github.com/gazebosim/gazebo-classic/tree/gazebo11/examples/plugins)。
其中RZB与ROS消息需要设置[Bridge](https://gazebosim.org/docs/latest/ros2_integration/#ros-gz-bridge)才能使用ROS节点接收GZB的消息,在这里可以看[官方示例](https://github.com/gazebosim/ros_gz_project_template/tree/main)。
吐槽:为啥不介绍完整?因为抄官方示例手累啊QwQ,反正代码都一样,CV后让DeepSeek加点注释解释一下不比牢博主讲的透彻QwQ。
#### 常用插件
> 细节请自行STFW!或者等老滚6、文明7、辐射5、魔兽争霸4全出了之后再在评论区逐一介绍!
| **插件类型** | **插件名称** | **功能** | **使用方式** |
|:---------------------|:--------------------------------------|:------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------|
| **运动控制插件** | 差速驱动插件 (Differential Drive) | 仿真两轮差速驱动机器人。 | 配置URDF,通过`cmd_vel`话题控制。 |
| | 滑动转向驱动插件 (Skid Steer Drive) | 仿真四轮滑动转向机器人。 | 配置URDF,通过`cmd_vel`话题控制。 |
| | 关节控制器插件 (Joint Controller) | 控制机器人关节(如机械臂)。 | 配置URDF,通过`joint_trajectory_controller`控制。 |
| | 力控制插件 (Force Control) | 仿真施加力或扭矩到关节。 | 通过ROS 2服务或话题施加力。 |
| **传感器仿真插件** | 激光雷达插件 (Laser Plugin) | 仿真2D/3D激光雷达。 | 配置URDF,通过`/scan`话题获取数据。 |
| | 摄像头插件 (Camera Plugin) | 仿真RGB或深度摄像头。 | 配置URDF,通过`/image_raw`和`/camera_info`话题获取数据。 |
| | 超声波传感器插件 (Ultrasonic Sensor) | 仿真超声波传感器。 | 配置URDF,通过`/ultrasonic_sensor`话题获取距离数据。 |
| | IMU插件 (IMU Plugin) | 仿真惯性测量单元。 | 配置URDF,通过`/imu`话题获取加速度、角速度和姿态数据。 |
| | GPS插件 (GPS Plugin) | 仿真全球定位系统。 | 配置URDF,通过`/gps`话题获取位置数据。 |
| | RGB-D相机插件 (RGB-D Camera Plugin) | 仿真深度相机。 | 配置URDF,通过`/depth/image_raw`和`/depth/points`话题获取数据。 |
| **环境交互插件** | 模型加载插件 (Spawn Entity Plugin) | 动态加载URDF/SDF模型。 | 通过`/spawn_entity`服务加载模型。 |
| | 模型删除插件 (Delete Entity Plugin) | 删除Gazebo中的模型。 | 通过`/delete_entity`服务删除模型。 |
| | 地面真值插件 (Ground Truth Plugin) | 提供模型的真实位姿。 | 配置URDF,通过`/ground_truth/pose`话题获取数据。 |
| | 接触传感器插件 (Contact Sensor Plugin)| 检测模型间的接触。 | 配置URDF,通过`/contact`话题获取接触信息。 |
| **物理与动力学插件** | 重力插件 (Gravity Plugin) | 设置或修改重力参数。 | 通过ROS 2服务或话题修改重力。 |
| | 风场插件 (Wind Plugin) | 仿真风场对模型的影响。 | 配置URDF,通过`/wind`话题控制风场参数。 |
| | 摩擦插件 (Friction Plugin) | 设置模型间的摩擦系数。 | 配置URDF或SDF文件。 |
| **其他实用插件** | 时间同步插件 (Clock Plugin) | 同步Gazebo和ROS 2的时间。 | 自动启用,通过`/clock`话题获取时间。 |
| | 日志记录插件 (Logging Plugin) | 记录仿真数据。 | 配置URDF,通过`/gazebo/log`话题获取日志。 |
| | 灯光控制插件 (Light Plugin) | 控制Gazebo中的灯光。 | 通过ROS 2服务或话题控制灯光参数。 |
[==========]
## RViz - 数据可视化
介绍:只需要按照RV发布对应的话题数据,就可以直接看到图形化结果。其中RV带有不少可视化插件,也可以自己DIY可视化插件丰衣足食。
```shell
ros2 run rviz2 rviz2
```
使用方式就是简单的:打开,点击左下角的Add,之后添加对应的想显示的数据类型:
比如上图,我们的老朋友:图片和TF坐标系。添加后点击左边小三角,在下拉列表找自己节点正在发布的图片话题名进行绑定就能显示了。
另外如果下次想保存此次的设置,可以保存配置,下次直接打开的时候使用`-d xxx.rviz`即可。
补充:[CSDN-ROS1插件开发](https://blog.csdn.net/weixin_45710350/article/details/126087148),[Jazzy插件开发官文](https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Pluginlib.html)
[==========]
## RQT - 简易可视化
介绍:RQt与RV2相比更加轻量化,使用的时候点击`Plugins`,使用需要使用的模块。
介绍:RQt的定位更类似于后端开发中的Postman或者前端的JsonServer,就是可视化然后调试,而非原始的编写Node/REPL后`log/print(f"我接收到了{xxx.msg}")`。
```shell
sudo apt install ros-${ROS_DISTRO}-rqt -y # ROS_DISTRO是ROS2环境的版本名,比如:jazzy
# 启动:rqt
```
比如:使用RQT的GUI发布话题,查看日志,查看当前的Log,或者使用GUI进行ros2 bag等等:
[==========]
- 2025-02-21
-
发表了主题帖:
ROS2基础:TF坐标系管理与可视化工具
本帖最后由 禾可1228 于 2025-2-22 03:26 编辑
# ROS2基础:TF坐标系管理与可视化工具
src:[ROS2新书测评活动](https://bbs.eeworld.com.cn/elecplay/content/8e835055),[TF2官方文档](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Tf2/Tf2-Main.html),[Gazebo官方文档](https://docs.ros.org/en/jazzy/Tutorials/Advanced/Simulators/Gazebo/Simulation-Gazebo.html),[GZB官网](https://gazebosim.org/),[RViz2官方文档](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/RViz/RViz-Main.html),[Rqt官方仓库](https://github.com/ros-visualization/rqt)
[TOCM]
牢骚:博主的习惯就是“能省就省”,所以出现过一次的全称,且后续上下文没有歧义项的时候,此名称第二次出现就是省略的简写模式,比如:`Gazebo -> GZB`,`WorkSpace -> WS`,`Package -> PKG`,`Python -> Py`,`官方文档 -> 官文`,`官方网站 -> 官网`,`官方Git仓库 -> 官仓`等等,但是仅仅局限于讲解/讲道理的文本区间,其余的比如具体代码等等则仍然采用真实名称,可放心CV。且除了及其基础的篇章或者使用方式差异较大的接口会特殊提及外,像`Py:str(xxx.abc)`/`Cpp:xxx->abc.c_str()`这种差异的代码一般只会使用最简单或者打字最少的语言单次描述。
牢骚:同义词会使用`XX/XX`这种形式,有助于阅读和理解和“偷换概念(bushi”,比如:`张量/向量`,`松弛操作/短路径数值迭代/Relaxtion`等等。
[========]
介绍:ROS2只是一个管理不同`节点-Node`及其数据传输的系统,面对基础的图像、音频、温湿度、蓝牙等等数据使用基础的消息传递机制如:`话题-Topic`即可实现。但是面对更复杂的场景,比如坐标系管理,代码新手使用基础的消息传递机制管理机器人的各项坐标就会变得比较困难。这时就可以使用官方推荐的`TF2`包来管理你的坐标系关系。
介绍:TF2会生成与维护一个坐标树,在这里所有的坐标系都是通过描述坐标之间的关系来构建的。比如:`"B坐标系位于A坐标系X=10处,C坐标系位于B坐标系Y=2处,D坐标系位于A坐标系X=-10处"`这句话就描述/构建了一个坐标树。当其中某个坐标系位置发生改变,但是基于此坐标系的其余坐标系的相对位置未发生改变,则只需要在缓存更新此坐标系的相对位置即可,不需要更新每一个坐标系的位置。比如B位于A的X=10处,即使A坐标系改变,B仍然位于A的X=10处,无需改变缓存内B相对于A的位置关系/广播。
介绍:一个三维世界的刚体,使用`位置/偏移-translation`和`角度-rotation`就能描述其`位姿(位置+姿态)-Pose`,其相对于旋转轴的偏转角度/欧拉角`(x y z)`常用四元数来表示`[w x y z]`,工具库可以直接使用`tf_transformations.quaternion_from_euler(x, y, z)`来获得四元数,同理也有相应的函数将四元数转换为欧拉角。
抽象:可以将TF坐标树当作一个绘制地图的绘图员,TF广播器就是一个雷达兵,雷达兵随时上报位置,TF监听器就是一个战略家,随时问绘图员:在XX时间的时候雷达兵在哪?
```mermaid
graph TD
A[A坐标系] -->|X=10| B[B坐标系]
B -->|Y=2| C[C坐标系]
A -->|X=-10| D[D坐标系]
# 万一不支持Mermaid示意图,会看到这些注释
# A (0,0)
# |
# |-- B (10,0)
# | |
# | `-- C (10,2)
# |
# `-- D (-10,0)
```
> 之后的坐标例子默认使用此关系,直到“海龟跟随示例”小节,旋转矩阵、四元数、增广变换矩阵等将不赘述!其中旋转角度将会直接使用四元数`[w x y z]`表示:
```math
q=[w,v]=[w,[x,y,z]]=w+xi+yj+zk
```
[========]
## 广播坐标使用
TF使用“广播”坐标来维护坐标树,每个坐标系拥有唯一的名称对应,节点若想获取自己某个时间点相对于某个坐标系的坐标可以使用坐标Buffer以及监听器进行坐标转化。思路类似于:
> C自身为原点的坐标系为`坐标系C`
> C要相对于`坐标系B`运动,C现在在B中的坐标偏移是(2, 3, 0),旋转偏移为(1, 0, 0, 0)
> 那么可以将此坐标变化关系广播出去,这样子其他节点就可以定位到`坐标系C`相对于其他坐标系的位置
> 比如获得`时间点1`下的`坐标系C`相对于`坐标系B`的偏移,或`坐标系C`相对`坐标系A`的偏移
> 由于得到的偏移有属性指向自己偏移是基于哪个坐标系的,所以可以直接转化为自己选定坐标系的偏移
>
> > 注意:TF只管根据广播的坐标系关系维护坐标系树,以便于其他节点随时通过监听器获取位置和姿态
> > 其中位置控制还得靠自己对应的功能模块!比如你即使广播B在A的X=233处,但是你实际上没有让海龟Move(X=233),那么你就仅仅是谎报坐标了而已!一般情况下,携带里程计的机器人模块会有里程计回调自动更新里程计坐标系相对于世界/地图坐标系的偏移。
---
TF2的坐标广播与获取/监听:
```python
from tf2_ros import TransformBroadcaster, StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped
from tf2_ros import TransformException, Buffer, TransformListener
"""
TransformStamped: 位姿信息+坐标系信息(源坐标系名+目标坐标系名)+时间戳,表O在时间N有位姿X
TransformBroadcaster: 动态广播器
StaticTransformBroadcaster: 静态广播器
对于静态广播相对于动态广播的区别:
- 类似于Python的Tuple相对于List:
- 如果数据固定不更新为啥不使用开销小且符合语义且安全的元组?
- 且使用方式一致,参数一致,效果(若数据不会变化的情况下)一致
TransformListener:监听所有的坐标系变化,并存于一个Buffer
下面是描述位置关系的例子:
"""
class Node1(Node):
def __init__(self, node_name):
super.__init__(node_name)
# 定义TF广播器,此广播器与当前对象生命周期绑定
self.tf_broadcaster = TransformBroadcaster(self)
# 绘制初始/上述的ABCD坐标系位置关系
self.tf_broadcaster.sendTransform(B) # BCD是什么之后讲
self.tf_broadcaster.sendTransform(C) # 这里为了梳理逻辑就先省略了
self.tf_broadcaster.sendTransform(D) # 就当BCD是TransformStamped就好
# 此时TF的坐标树已经更新了对应的位置,可以使用监听器获取他们的坐标关系:
# 创建监听器,监听器负责将监听到的变换存到Buffer
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
# 获取 `当前时间` 下的相对于 `坐标系B原点` 的 `坐标系A` 所在的位姿关系
now = rclpy.time.Time()
try:
trans = self.tf_buffer.lookup_transform(
"B", "A", now
)
except TransformException as ex:
self.get_logger().info(f'无法获取变换: {ex},可能是坐标系名称错误')
else:
self.get_logger().info(f'此时位姿关系为:{trans}')
# 现在可以设置一个位于坐标系A的(0 0 0)点,查看其在坐标系B中的位置:
from geometry_msgs.msg import PointStamped
from tf2_geometry_msgs import do_transform_point
point = PointStamped()
point.header.frame_id = 'A'
point.point.x = 0.0;point.point.y = 0.0;point.point.z = 0.0
self.get_logger().info(f'在坐标系B中,点的坐标为:{
do_transform_point(point, trans)
}')
---
# OK,主要内容讲完,那么来看看BCD都是什么,以B举例子:
B = TransformStamped()
B.header.stamp = self.get_clock().now().to_msg()
B.header.frame_id = 'A' # 定义B坐标系相对于A坐标系的偏移
B.child_frame_id = 'B' # 即:在A坐标系看,B位于:(10, 0, 0)-(0,0,0,0)
# 由于此时提到了坐标系A和B,那么TF就会记录此时含有两个坐标系A和B
B.transform.translation.x = 10.0
B.transform.translation.y = 0.0
B.transform.translation.z = 0.0
B = self.euler_to_quaternion(0, 0, pose.theta) # 将欧拉角转换为四元数
B.transform.rotation.x = q[0]
B.transform.rotation.y = q[1]
B.transform.rotation.z = q[2]
B.transform.rotation.w = q[3]
# 这里就是上面的广播坐标关系,在广播后,TF就会把两个坐标关系计入缓存/内存
self.tf_broadcaster.sendTransform(B)
# C和D同理,无非一个是CB关系,一个是DA关系,改改上面的数字即可……
```
[========]
## 海龟跟随示例
> 前情提要:你安装了ROS2,且通过`键盘上下左右控制`、`代码创建生产者节点发布话题到'//cmd_vel'`、`命令行发布话题使用-rate控制频率`、`ros2 bag`等等手段尝试了调教小海龟。所以上述内容不再赘述。
> 前情提要:你会创建工作空间、会编译以及启动自己的节点,会使用Launch批量启动节点。所以上述内容不再赘述。
> 前情提要:除了命令行的`-rate`外,你会使用`Node::create_timer`创建`定时器`来定时触发`hello`消息的发布。所以上述内容不再赘述。
> 前情提要:你会创建TF的`广播器`,当`海龟1`运动的时候,你能够创建一个节点订阅海龟的位置,从而使用TF广播器将海龟坐标广播/公布/更新到TF坐标树。所以上述内容不再赘述。
> 前情提要:你会创建TF的`监听器`,你可以使用定时器不断获取海龟1的位置。所以上述内容不再赘述。
所以现在,创建一个“海龟跟随示例”还缺什么?答:初中物理,计算距离和速度,当知道`海龟1`和`海龟2`当前的坐标,若想`海龟2`在N秒内到达`海龟1`的位置需要哪个方向需要多少速度,并作为`话题`发布到`'//cmd_vel'`!过于简单,所以上述内容不再赘述。
```shell
# 创建工作空间与包
mkdir -p heke_example/src
cd heke_example/src
# 创建包,安装对应依赖
ros2 pkg create --build-type ament_python turtle2node \
--dependencies rclpy geometry_msgs turtlesim tf2_ros tf2_geometry_msgs
# 新建并编辑turtle2node.py:
vim turtle2node/turtle2node.py
```
```shell
# 编辑之后,将此文件加入setup.py编译指导,后编译,后依次启动节点:
...
entry_points={
'console_scripts': [
'heke = turtle2node.turtle2node:main',
],
},
...
colcon build --packages-select turtle2node
```
至于源代码:
```Python
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist, PointStamped, TransformStamped
from turtlesim.msg import Pose
from tf2_ros import TransformException, Buffer, TransformListener, TransformBroadcaster
from tf2_geometry_msgs import do_transform_point
from turtlesim.srv import Spawn
import math
class TurtleTfFollower(Node):
def __init__(self):
super().__init__('turtle_tf_follower')
# 创建TF2缓冲区和监听器
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
# 创建TF广播器,用于发布turtle1和turtle2的TF变换
self.tf_broadcaster = TransformBroadcaster(self)
# 创建发布器,用于发布控制turtle2速度的消息
self.publisher = self.create_publisher(Twist, '/turtle2/cmd_vel', 10)
# 订阅turtle1和turtle2的位姿
self.turtle1_pose_sub = self.create_subscription(Pose, '/turtle1/pose', self.turtle1_pose_callback, 10)
self.turtle2_pose_sub = self.create_subscription(Pose, '/turtle2/pose', self.turtle2_pose_callback, 10)
# 初始化turtle1和turtle2的位姿
self.turtle1_pose = Pose()
self.turtle2_pose = Pose()
# 创建定时器,定期执行跟随逻辑
self.timer = self.create_timer(0.1, self.follow_turtle)
# 生成第二个海龟
self.spawn_turtle2()
def spawn_turtle2(self):
"""生成第二个海龟(turtle2)"""
self.get_logger().info('正在生成turtle2...')
# 创建客户端,调用/spawn服务生成第二个海龟
self.spawn_client = self.create_client(Spawn, '/spawn')
while not self.spawn_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('等待/spawn服务...')
# 设置生成海龟的请求参数
request = Spawn.Request()
request.x = 5.0 # 初始x坐标
request.y = 5.0 # 初始y坐标
request.theta = 0.0 # 初始角度
request.name = 'turtle2' # 海龟名称
# 异步调用服务
future = self.spawn_client.call_async(request)
future.add_done_callback(self.spawn_callback)
def spawn_callback(self, future):
"""生成海龟的回调函数"""
try:
response = future.result()
if response.name == 'turtle2':
self.get_logger().info('成功生成turtle2')
except Exception as e:
self.get_logger().error(f'生成turtle2失败: {e}')
def turtle1_pose_callback(self, msg):
"""turtle1位姿回调函数"""
self.turtle1_pose = msg
self.publish_tf('turtle1', self.turtle1_pose)
def turtle2_pose_callback(self, msg):
"""turtle2位姿回调函数"""
self.turtle2_pose = msg
self.publish_tf('turtle2', self.turtle2_pose)
def publish_tf(self, turtle_name, pose):
"""发布turtle的TF变换"""
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = turtle_name
t.transform.translation.x = pose.x
t.transform.translation.y = pose.y
t.transform.translation.z = 0.0
# 将欧拉角转换为四元数
q = self.euler_to_quaternion(0, 0, pose.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
# 发布TF变换
self.tf_broadcaster.sendTransform(t)
def euler_to_quaternion(self, roll, pitch, yaw):
"""将欧拉角转换为四元数"""
cy = math.cos(yaw * 0.5)
sy = math.sin(yaw * 0.5)
cp = math.cos(pitch * 0.5)
sp = math.sin(pitch * 0.5)
cr = math.cos(roll * 0.5)
sr = math.sin(roll * 0.5)
q = [0] * 4
q[0] = sr * cp * cy - cr * sp * sy
q[1] = cr * sp * cy + sr * cp * sy
q[2] = cr * cp * sy - sr * sp * cy
q[3] = cr * cp * cy + sr * sp * sy
return q
def follow_turtle(self):
"""实现turtle2跟随turtle1的逻辑"""
try:
# 获取从turtle2到turtle1的变换
transform = self.tf_buffer.lookup_transform(
'turtle2', # 目标坐标系
'turtle1', # 源坐标系
rclpy.time.Time(), # 获取最新的变换
timeout=rclpy.duration.Duration(seconds=1.0))
# 创建一个PointStamped消息,表示turtle1的位置
point = PointStamped()
point.header.frame_id = 'turtle1'
point.point.x = 0.0
point.point.y = 0.0
point.point.z = 0.0
# 将turtle1的位置转换到turtle2的坐标系中
transformed_point = do_transform_point(point, transform)
# 计算turtle2需要移动的速度
cmd_vel = Twist()
cmd_vel.linear.x = 0.5 * transformed_point.point.x # 线性速度
cmd_vel.angular.z = 4.0 * transformed_point.point.y # 角速度
# 发布速度命令
self.publisher.publish(cmd_vel)
except TransformException as ex:
self.get_logger().info(f'无法获取变换: {ex}')
def main(args=None):
rclpy.init(args=args)
node = TurtleTfFollower()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
```
之后的启动节点(可自行编写Launch文件):
```shell
# 海龟1:
ros2 run turtlesim turtlesim_node
# 海龟1的键盘控制
ros2 run turtlesim turtle_teleop_key
# 海龟2,别忘记启用ROS2工作空间/install/setup.bash的环境!
ros2 run turtle2node heke
```
效果展示:
查看坐标系关系:
TF2命令行工具:
```shell
ros2 run tf2_tools view_frames # 查看TF树,生成的.gv和.pdf自行找工具查看!
ros2 run tf2_ros tf2_echo # 查看坐标系关系
```
[========]
## 可视化坐标系
啊,受够了干巴巴的PDF坐标系指示,就没有直接一点纵览全局的坐标系展示方案?RViz:😀。
吐槽:本来想一次性讲完GRR(见开头src部分)三幻神的,果然还是放到下一篇比较好……
```shell
ros2 run rviz2 rviz2
```
弱弱提示:RViz可以点击左下角的Add按钮添加显示类型,添加两个坐标轴之后,设置引用的坐标系为对应的海龟即可。另外记得把全局坐标系设置为`world`!
之后在通过键盘控制第一只海归就能看到两个坐标轴追着跑了!当跑完还想下次一键展示,则可以保存配置为`.rviz`文件,下次启动的时候直接`ros2 run rviz2 rviz2 -d xxx.rviz`即可加载配置!
[========]
-
回复了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
补充:`ros2 pkg prefix --share xxx`是查找包`xxx`的安装路径
-
回复了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
补充:一定注意,若编译包之后运行节点报错找不到包,一定先确定自己source了install/setup.bash启用了当前工作空间的环境!
-
回复了主题帖:
ROS2 Jazzy 基于 Ubuntu24.04LTS 的安装指北
> 补充内容 (2025-2-21 17:44): 补充:如果采用虚拟机的Ubuntu,则可以使用Tmux来命令行分块打开多窗口来启动/调试多节点,如果是WSL,那么你只需要使用Windows自带的分屏分窗口即可,如果还是不方便,可以直接右键选项卡,点击拆分,外加F11全屏
的示例
-
回复了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
freebsder 发表于 2025-2-21 16:11
ROS2成熟了吗?前几年关注的时候,还说ROS2不太得行
哦对了,机器人操作系统还有一个近年来随着Rust推广的[DORA](https://dora-rs.ai/)[-RS](https://github.com/dora-rs),这位才是真正的“默默无闻”
-
回复了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
补充:包的依赖记得配置到`package.xml`!在命令行添加依赖在pkg create时使用`--dependencies`选项,或者直接手动在package.xml添加:<depend>依赖名</depend>。与配置maven差不多……
-
回复了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
freebsder 发表于 2025-2-21 16:11
ROS2成熟了吗?前几年关注的时候,还说ROS2不太得行
看需求用不就行了,“成熟”这一词没有定论,毕竟一直在发展,换个思路,Python现在[2025.02.21]都3.13了,他成熟了吗,不一定,未来可能会更改各种特性,还会有不少的功能加入或者删减
-
回复了主题帖:
ROS2节点通信机制
补充:ROS2采用Discovery自发现机制,允许节点自主寻找并建立稳定的通信连接
- 2025-02-20
-
发表了主题帖:
ROS2基础:节点、功能包、工作空间与其编译运行
# ROS2基础:节点、功能包、工作空间与其编译运行
src:[ROS2新书测评活动](https://bbs.eeworld.com.cn/elecplay/content/8e835055),[Launch官仓-Github](https://github.com/ros2/launch/tree/jazzy)
QS:需要下载编译器:`sudo apt install python3-colcon-common-extensions`
[TOCM]
介绍:本篇博客将会介绍ROS2创建/编译/运行一个节点必备的知识,包括使用Launch的节点批量启动
注意:命令行代码默认项目已经`source install/setup.bash`,执行的工作目录为工作空间`my_project`
## 从创建一个节点开始
ROS2将`节点-Node`视为机器人的细胞,每个节点都是一个`独立的可执行文件/功能部件`。比如对于一个摄像头节点,他的职能就是拍摄图片发布到`数据空间-DataBus`,之后,需要这些数据的节点,比如显示屏节点,或者实体分割节点会订阅这些数据进行显示或处理。在ROS2中,每个节点独占一个进程,且由于ROS2的节点管理是基于哈希表名称映射的,所以每个节点必须有一个唯一的名称。
### 工作空间
`工作空间-WorkSpace`,就类比一个`项目目录-ProjectDir`,或者一个工作台,或者一个工具箱。如果拿其他技术栈来比喻:
|ROS2|前端页面|后端服务|游戏开发|建模渲染|智能项目|
|:--:|:--:|:--:|:--:|:--:|:--:|
|工作空间|Project|Project|Project|Project|工作空间|
|功能包|View|App/Service|场景|渲染链|Notebook|
|节点|Component|Router/API|单个脚本材质贴图等|合成器节点|单模型|
即:节点可以独立存在,且可以独立运行,但是得借由多个节点组成的包才是一个完整的系统,一个包可以有一个或多个节点,一个工作空间涵盖了多个包以及其之下的节点。
创建一个工作空间就是新建一个存放此新工程文件的文件夹:
```shell
mkdir -p my_project/src # 之后的命令行操作默认在my_project目录
```
如果是新工程,则可以`colcon build --packages-select=false`初始化此`my_project`工作空间,初始化会创建空的`install`, `build`, `log`目录,并设置初始的`install/setup.bash`,加载此bash环境后,ros2命令将会额外检测此工作空间下定义的消息、节点等等。
```shell
source install/setup.bash
```
如果是旧工程,可以使用ROS2自带的rosdep命令自动安装src代码空间中各功能包的依赖库:
```shell
rosdep install --from-paths src --ignore-src -r -y
```
### 功能包
`一个功能包-Package`,一般是一个可独立运行的完整组件,内含有一个或者多个节点,比如移动控制、视觉感知、自主导航等等,将完整机器人分割为功能包使用了解耦的思想,功能包可以独立方向至其他机器人项目。
```bash
# 创建功能包
ros2 pkg create --build-type
build-type: ament_cmake | ament_python,根据适合/喜欢的语言进行选择
package-name: 功能包名,即src文件夹下将会新建什么名字的文件夹(功能包)
# 比如创建一个位于 my_project/src 下的使用Python风格API的 my_package:
ros2 pkg create --build-type ament_python my_package
```
一个功能包含有一个描述元信息的`package.xml`,包括包的版权、作者信息、开发日期、功能描述等等。另外还有一个本地化包含有的包构建配置文件,比如Cmake包含有的`CMakeLists.txt`,Python包含有的`setup.py`。其中使用方式与各语言通用包构建发布的配置一致。
### 节点
节点是一个单独的可执行文件,其中无论是C++还是Py,此文件将会启动一个`Ros::Node`对象,比如:
```Python
from rcipy.node import Node
if __name__ == '__main__':
# 一定注意:节点名称得保证唯一性
node = Node("node_name")
# 循环等待ROS退出
rclpy.spin(node)
# 退出后将会销毁节点实例,关闭ROS接口
node.destory_node()
rclpy.shutdown()
```
```C++
#include
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node("node_name");
rclcpp::spin(std::shared_ptr(node));
rclcpp::shutdown();
return 0;
}
```
但是,启动一个空节点没有太大的实际意义,所以一般启动都是实现特定功能的节点子类:
```python
class MyNode(rclpy.Node):
def __init__(self, name: str, ...):
super.__init__(name)
...
...
```
```cpp
class MyNode: public rclcpp::Node
{
public:
MyNode(const string &name, ...): Node(name)
{
...
}
...
}
```
### 编译运行
启动节点不是单纯的运行写好的节点文件,而是需要合理配置编译选项后,编译,通过ros命令来启动:
```shell
ros2 run
```
其中需要设置的编译选项:
```python3
## Python: setup.py
...
entry_points = { # 设置程序入口
'console_scripts': [
# 格式为:`节点启动名 = 相对于工作空间下的main所在位置-my_node.py`
"node_class_name = my_package.my_node:main"
# 此时启动此节点:ros2 run my_project node_class_name
],
}
...
```
```cmake
// Cpp: CMakeLists.txt
...
# 查找依赖的功能包 rclcpp 提供ROS2 C++的基础接口
find_package(rclcpp REQUIRED)
# 添加一个可执行文件,节点启动名字为`emm`,对应源码src/my_node.cpp
add_executable(emm src/my_node.cpp)
# 配置编译时需要链接的库,比如 rclcpp 库:
ament_target_dependencies(emm rclcpp)
# 将编译生成的可执行文件`emm`拷贝到`install/lib`:
install(
TARGETS
emm
DESTINATION lib/${PROJECT_NAME}
)
# 编译后将可以这样运行此节点:ros2 run my_project emm
...
```
> 编译操作:
```shell
colcon build # 默认编译,直接编译此工作目录下的所有包的所有节点
colcon build --packages-select # 只编译指定功能包
colcon build --packages-up-to # 编译指定功能包及其依赖
```
> 清除编译:
```shell
# 直接了当删除除了源码的目录即可
rm -rf install/ build/ log/
```
[==========]
## 到批量启动多个节点
前情提要:`节点是一个独立进程` =意味着=> `每启动一个节点都会占用一个shell`,除非nohup后台启动或者使用容器等等虚拟化手段启动。当你需要启动的节点大于5个时,你会发现tmux也会变得不好使,那么如何使用一个命令行启动一键多个节点?且可以不用每次启动都麻烦的配置每个节点的启动配置?哈哈,ROS人有自己的Makefile:`Launch`!
### Launch
Launch,多节点启动与配置脚本,官方仓库:[`Github`](https://github.com/ros2/launch/tree/jazzy),官方说明:[`design.ros2.org`](https://design.ros2.org/articles/roslaunch.html)。Launch启动文件使用Python进行描述,可以配置命令行输入的各项参数,并同时使用Python原有的编程功能。注意:Launch的功能是利用模板生成命令行命令,类似于模板引擎展开为界面代码一样,并不影响节点源码的行为!但是可以通过条件判断来在生成命令的同时更改命令行参数(类似于条件编译),或者使用循环批量生成大量的节点进行启动。
#### 基础使用示例
```python3
import os
from launch import LaunchDescription
# 注意:这里的Node是用来生成模板命令字符串的,而不是ros2用于启动的Node
from launch_ros.actions import Node
# 下面导入的这个方法用来查询功能包路径
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
return LaunchDescription([
# 启动第一个Python节点,相当于:ros2 run my_py_package1 my_node1
# 但是指定了别名:my_node1
Node(
package='my_py_package1',
executable='my_node1',
name='my_node1',
output='screen'
),
# 启动第二个Python节点
Node(
package='my_py_package1',
executable='my_node2',
name='my_node2',
output='screen'
),
# 使用循环生成3个节点
*[Node(
package='my_cpp_package',
executable='my_node3',
name=f'cpp_node{i}',
output='screen'
) for i in range(3)],
])
```
> **Launch的编译和启动:**
Launch文件在使用前需要移动至`install`,但是直接在此文件夹下创建launch.py文件,在清理编译文件时会一不小心全删掉,所以一般放在功能包-Package目录下的launch文件夹下,但是如果图省事直接放于功能包-Package根目录,则常习惯命名为`xxx.launch.py`来表示这是一个launch描述文件,之后配置相应的编译指导文件,将其作为资源文件直接拷贝到`install/share`即可。
```Python
## Python: setup.py
...
data_files = [
# data file将会在编译时被原样拷贝到lib
(
os.path.join('share', package_name, 'launch'),
glob(os.path.join('launch', '*.launch.py'))
), ...
]
...
```
```CMake
// Cpp: CMakeLists.txt
...
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)
...
```
```shell
# Launch命令行操作,在编译-build后:
ros2 launch # eg. ros2 launch my_project emm.launch.py
```
#### 常用描述方法
综上所述,一个Launch启动文件可以用以下模板描述:
```Python
def generate_launch_description():
return LaunchDescription([ ... ])
# 即:在文件中定义 `generate_launch_description` 函数,返回启动文件的描述类
```
其中常用的描述方法有:
```python
from launch import LaunchDescription
from launch.actions import (
GroupAction,
IncludeLaunchDescription,
)
from launch_ros.actions import (
Node,
PushRosNamespace,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
# 普通的加载此工作空间下节点示例:
node1 = Node(
package='my_py_package1',
executable='my_node1',
)
# 当某些节点已经被另外一个Launch定义的时候,可以直接加载此Launch定义的节点
others = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
# get_package_share_directory可以获取环境包所在的目录
get_package_share_directory("环境中的其他包的包名"),
# 习惯将launch文件放于launch之下,且名字为*.launch.py
"launch", "xxx.launch.py",
)
])
)
# 通常无法保证外部Launch不与当前的名称冲突,所以常常给外部Launch套一层命名空间:
other2 = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
get_package_share_directory("环境中的其他包的包名"),
"launch", "xxx.launch.py",
)
])
)
other2_with_ns = GroupAction(
actions=[
PushRosNamespace('namespace_233'),
other2,
]
)
# 最后返回集合后的描述(如果仅仅一个Node,直接返回Node也可)
return LaunchDescription([node1, others, other2_with_ns])
```
#### 常用参数配置
```python
Node(
package="节点所在的功能包",
executable="编译后的可执行文件名",
namespace="节点所在命名空间",
name="对节点进行重命名",
parameters=[...], # 加载参数 / 参数文件路径
# 资源重映射,比如将话题名为"/emm"映射到话题名为"/asd/qwe"
remappings=[("原资源路径", "映射资源路径"), ("/emm", "/asd/qwe"), ...],
arguments=['-d', '命令行参数'],
)
```
使用参数-Parameters:
```Python
# 参数指定的可以是yaml文件的路径,也可以是Launch使用DeclareLaunchArgument定义的参数:
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution
def generate_launch_description():
# 以初始的海龟的背景颜色参数为示例
background_r_arg = DeclareLaunchArgument(
'background_r', default_value=TextSubstitution(text='0')
)
"""假设剩余的background_g和background_b参数在config/emm.yaml文件:
/turtlesim2/sim:
ros__parameters:
background_g: 0
background_b: 0
"""
config_gb_yaml_path = os.path.join(
get_package_share_directory("my_project"),
'config', 'emm.yaml'
)
return LaunchDescription([
background_r_arg, # 定义的参数得加入列表才能使用
Node(
...,
parameters=[
config_gb_yaml_path, # yaml文件路径形式加载参数
{
# 使用之前Launch文件内定义的参数
'background_r': LaunchConfiguration('background_r')
},
]
),
])
```
- 2025-02-19
-
发表了主题帖:
ROS2节点通信机制
# ROS2节点通信机制
src:[ROS2新书测评活动](https://bbs.eeworld.com.cn/elecplay/content/8e835055),[官方文档](https://docs.ros.org/en/jazzy/Concepts/Basic/About-Nodes.html#nodes)
[TOC]
介绍:ROS2有三种常用的节点通信机制:`话题-topic`、`服务-service`、`动作-action`;其中通信的消息格式可以通过对应的`通信接口文件-interface`进行定义;ROS2的节点通信机制基于`DDS-数据分发服务`,将数据放于公共的`DataBus`,各取所需;不同节点的参数存于同一个公共大`字典`中,可以随时设置与读取。ROS2支持分布式通信,不同节点只需要在同一局域网下即可无需任何配置直接通信。如果需要隔离分组,可以设置`域-domain`进行分组。同一个域之下的节点才可以相互通信。
补充:ROS2的节点(代码)可以进行的操作也拥有对应的命令行命令,方便调试。
补充:ROS2基于DDS通信机制/标准进行各种节点通信,DDS可以配置`QoS-质量服务`来配置常用的通信策略。DDS虽然稳定,但是效率低,未来会逐渐支持`Zenoh`。
## 节点通信机制
一般机器人由多个节点构成,每一个节点都是一个独立功能部件。节点之间需要传输数据进行通信。比如一个简易的监控系统:Node1为一个摄像头节点,Node2是一个显示屏节点,常规思路就是Node1通过UDP连接Node2,但是当节点数量增加,在复杂网络环境可能会出现连接不稳定,信息延迟,且代码会逐渐难以维护。但是若Node1定义了一个名称:“蜜雪兵城”,每次有新数据都会发布到这里,那么其他子节点每次需要数据,到这里取即可,代码就没有那么难以维护了。
ROS1的节点通信机制基于TCP/UDP,且有一个中心节点-ROSMaster负责信息匹配和通信管理,由于依赖中心节点,当中心节点宕掉后,整个系统将无法工作。ROS2采用DDS作为通信中间层,实现了去中心化的通信架构,数据集中在DataBus(下文简称为`DB`),节点之间的分布式数据传输就像吃火锅一样,有人添菜,有人只夹自己感兴趣的菜即可。且DDS可以通过配置`质量服务策略-QoS`,进而调整通信行为细节,以实现更高的实时性。
### 话题
话题-Topic,类似于一个博客专栏。比如一个博主开启了一个博客专栏“ROS2机器人开发专区”,一些用户对此感兴趣且点击订阅,博主每次在此专栏发布博客,此博客平台都会有弹出消息提醒用户“你关注的专栏更新了!”。话题分为`发布者-Publisher`和`订阅者-subscription`,发布者发布消息到DB特定的话题名下,订阅者定义回调函数,在发布者发布消息后触发此回调函数。
#### 创建发布者
之后的代码都将采用:`Python-Cpp-Shell`这种形式,其中创建节点/编译节点/启动节点见[基础第一章测评](https://bbs.eeworld.com.cn/thread-1305824-1-1.html)
```python
from rclpy.node import Node
from std_msgs.msg import String
class MyPublisher(Node):
def __init__(self, name, *args):
# 启动时定义节点名称,在ROS2中靠哈希表管理节点,所以节点和名称一一对应,不要重复
super().__init__(name)
# 设置话题类型-String(之后在通信接口定义会详解),话题名,队列长度
self.publisher = self.create_publisher(String, "mi_xue_bing_cheng", 10)
def send_hello():
msg = String()
msg.data = "hello"
self.publisher.publish(msg)
# 这里的logger相当于print,就是输出debug信息
self.get_logger().info('Publishing: "%s"' % msg.data)
# 这里设置每 1s 发送一次 hello
self.timer = self.create_timer(1, send_hello)
def main(args=None):
# 节点名称就随意了,EmmPub吧
rclpy.init("EmmPub", args=args)
node = MyPublisher()
# 这里阻塞等待ROS2退出,当退出后执行下面的销毁节点释放资源
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
# 之后节点启动的代码都大同小异,所以只在这里出现一次,后续将不重复出现
main()
```
```cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MyPublisherNode : public rclcpp::Node
{
public:
MyPublisherNode(const string &name)
: Node(name), count_(0)
{
publisher_ = this->create_publisher("mi_xue_bing_cheng", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1), std::bind(&MyPublisherNode::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "hello";
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared("EmmPub"));
rclcpp::shutdown();
return 0;
}
```
```shell
ros2 topic pub /mi_xue_bing_cheng std_msgs/msg/String "data: 'hello'"
```
#### 创建订阅者
```python
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MySubscriber(Node):
def __init__(self, name):
super().__init__(name)
# 定义接受的类型、在哪个话题名下接收、接收到数据后的回调函数、队列长度
self.subscription = self.create_subscription(
String,
'mi_xue_bing_cheng',
self.listener_callback,
10)
self.subscription # 防止垃圾回收
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
```
```c++
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MySubscriberNode : public rclcpp::Node
{
public:
MySubscriberNode(const string &name)
: Node(name)
{
subscription_ = this->create_subscription(
"mi_xue_bing_cheng", 10,
std::bind(&MySubscriberNode::topic_callback, this, std::placeholders::_1));
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription::SharedPtr subscription_;
};
```
```shell
ros2 topic echo /mi_xue_bing_cheng std_msgs/msg/String
```
#### 话题相关命令
话题相关的命令行操作:
```shell
ros2 topic list # 查看话题列表
ros2 topic info # 查看话题信息
ros2 topic hz # 查看话题发布频率
ros2 topic bw # 查看话题传输宽带
ros2 topic echo # 查看话题数据
ros2 topic pub # 发布话题消息
```
### 服务
与网页类似,`客户端-Client`发起`请求-Request`,`服务端-Service`回复`响应-Responce`。这里是一个请求“hello”,返回“hello client!”的示例。其中消息类型`emm.srv`详见`通信接口定义`。
#### 创建服务端
```python
from .emm.srv import Emm
"""
# 这里Emm是srv文件编译后转换为的Py类,同理,C++那边是C++的类
# .srv文件开始定义请求类型
string req
---
# 仨横线之后定义响应类型,详见`通信接口定义`
string res
"""
class MyService(Node):
def __init__(self, name):
super().__init__(name)
# 类型,服务名,回调函数
self.srv = self.create_service(Emm, "hello", self.hello_callback)
def hello_callback(self, request, response):
if request.req == "hello":
response.res = "hello client!"
else:
response.res = f"error! you request is {request.req} no `hello`!"
return response
```
```c++
#include "./emm.hpp" // 这里直接被编译为hpp头文件了,与py不同!
...
// 可以发现:(服务名, 回调函数)
service_ = this->create_service("hello", helloCallback);
...
void helloCallback(const shared_ptr request, const shared_ptr response)
{
if (request.req == "hello") {
response->res = "hello client";
} ... 略
}
```
#### 创建客户端
```python
...init
# 创建客户端
self.client = self.create_client(Emm, "hello")
# 等待服务端启动成功
while not self.client.wait_for_service(timeout_sec=1.0):
pass # 或者直接log:waitting service start...
...
def send_request():
msg = Emm.Request()
msg.req = "hello"
self.future = self.client.call_async(msg)
# 循环等待收到响应或者ROS2退出
while rclpy.ok():
if self.future.done():
response = self.future.result()
print(f"{response.res}")
break
```
```c++
// 好多auto……是因为博客MD编辑器没有自动补全,一个个手打超级长的类型看着累,打着也累……
...
auto client = node->create_client("hello");
while (!client->wait_for_service(1)) {
; // 等待服务端启动成功
if (!rclcpp::ok()) {;} // 此时服务端启动未成功就退出了ros2,log错误信息即可
}
...
...
auto msg = make_shared();
msg.req = "hello";
auto result = client->async_send_request(msg);
if (
rclcpp::spin_until_future_complete(node, result)
==
rclcpp::FutureReturnCode::SUCCESS
) {
auto responce = result.get();
cout res # 将参数保存到yaml文件
ros2 param load # 将参数加载到节点
```
```python
self.declare_parameter('param_name', 'param_value') # 定义参数
self.get_parameter('param_name').get_parameter_value() # 查询参数
self.set_parameters([ # 设置多个参数
rclpy.parameter.Parameter(
'param_name', rclpy.Parameter.Type.STRING, 'new_value'
)
])
```
```c++
this->declare_paramter('param_name', 'param_value');
this->get_parameter('param_name').as_string();
this->set_parameters({
rclcpp::Parameter(
"param_name", "new_value"
)
});
```
## 质量服务策略
QoS是一种网络传输策略,应用程序指定需要的网络传输质量,QoS尽量实现这种质量要求,常用的策略如下:
```plantext
DEADLINE - 节点必须在每个截止时间内完成一次通信
HISTORY - 表示针对历史数据的缓存大小
RELIABILITY - 表示数据的通信模式:
BEST_EFFORT - 尽力传输模式,网络不好也要保证数据流畅,可能导致数据丢失
RELIABLE - 可信赖模式,保证数据完整性
DURABILITY - 针对晚加入的节点也会有一定历史数据发送过去,让节点快速适应系统
```
QoS的默认配置位于`/opt/ros/ros版本名/include/rmw/qos_profiles.h`,自己配置可以使用`/opt/ros/ros版本名/include/rmw/rmw/types.h::rmw_qos_profile_t`下的结构体(Py直接使用内置`rclpy.qos::QoSProfile`方法在节点初始化时配置)
命令行配置需要使用`--qos-*`来查看/修改以及启动的节点的QoS配置
## 其他内容补充
暂无,之后有会更新至此
- 2025-02-07
-
发表了主题帖:
ROS2 Jazzy 基于 Ubuntu24.04LTS 的安装指北
# ROS2 Jazzy 基于 Ubuntu24.04LTS 的安装指北
src:[ROS2新书测评活动](https://bbs.eeworld.com.cn/elecplay/content/8e835055),[ROS2官网](https://www.ros.org/),[ROS2下载](https://www.ros.org/blog/getting-started/#)[页面](https://docs.ros.org/en/jazzy/Installation.html)
[TOCM]
介绍:`ROS-Robot Operating System:机器人操作系统`,2007年起源于斯坦福大学`STAIR项目-Morgan Quigley`,2010年正式确定其软件系统名称ROS,并发布1.0版本。2012起,ROS社区每年举办一届`ROSCon-ROS开发者大会`,由于v1版本的设计局限性,初期经验不足,2017年年底ROS2作为一款适用于所有机器人的操作系统正式发布。其版本节奏跟随Ubuntu版本发行节奏:每两年推出一个LTS,每个版本支持5年。ROS2首个LTS(长期支持版本)是2022年的`Humble`,而Jazzy是2024年5月推出的LTS。
补充:在官网你可以看到各种各样的小乌龟,比如滑雪的Humble,跳机械舞的爵士龟Jazzy,没错,这些奇奇怪怪的“版本号”,就是每个版本的吉祥龟名字
## Ubuntu平台选择
ROS可以运行在任意的主流操作系统,很大一部分机械模拟的包也有相应的GUI支持,但是WIN环境的编程环境/命令行环境不是很方便,大多数ROS开发者更钟爱Linux的发行版Ubuntu。常见的Ubuntu平台有:`WSL-基于windows的linux子系统`、`运行在windows的VMWare`、`运行在任意操作系统的Docker`、`手动刷机为Ubuntu24.04LTS`……
由于WSL属于最方便的部署环境,以下就介绍如何在Windows10/11上启动WSL以及安装ROS2吧:
> **第一步:打开控制面板 - 按下 `Home-Win徽标键` 输入 `控制面板` 点击系统检索的第一项**
> **点击程序 -> 启用或关闭Windows功能**
> **启用 `Hyper-V/虚拟机平台` 与 `WSL/适用于Linux的Windows子系统` 后点击确定**
之后等待安装读条完毕后,就可以安装Linux发行版了,先介绍通过**命令行**安装的,后介绍**应用商店**安装的,比如使用:Win+R,输入CMD打开命令提示符界面使用以下命令进行安装:
```shell
wsl --list --online # 查看可安装的版本列表
wsl --install -d # 使用此命令进行安装,如:wsl --install -d Ubuntu-22.04
wsl --list --verbose # 查看已安装的虚拟机
```
使用**微软应用商店**安装示例:打开直接搜索Ubuntu,点击安装
P.S.微软应用商店和Github一样,国内常常连接不到,比如上面搜索结果的图就是借用的贴吧的,编者的应用商店刚刚连不上QwQ,直接下载第一个没有版本号的即可。没有版本号的会根据当前发布的版本动态更新,比如2024会自动更新为24.04LTS,2026会自动更新为26.04LTS等等。但是附加版本号的就没有这个特性。
安装需要耐心等待,若在安装完全完成之前直接取消,很有可能会残留一个不完全的发行版,即:打开后是`#root:sh`,apt等等工具都没有!这种情况需要点击卸载后重新安装!
> **安装完毕后,可以直接Win+R在CMD中输入Ubuntu打开,也可以直接点击快捷图标打开**
打开后界面:
## 安装ROS2
在src中,指定了 ROS2 Jazzy 的官方安装指南页面:[也可以点击这里查看](https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html)
> **由于按照上述步骤安装的Ubuntu24.04默认支持Utf-8,所以这一步可以跳过**
```shell
locale # check for UTF-8
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
locale # verify settings
```
> **启用 Universe 软件源 与 添加 ROS2 PubKey**
```shell
sudo apt install software-properties-common
sudo add-apt-repository universe
```
```shell
sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
```
```shell
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
```
> **安装开发者工具包与 ROS2 本体**
```shell
sudo apt update && sudo apt install ros-dev-tools
```
```shell
sudo apt update
sudo apt upgrade
sudo apt install ros-jazzy-desktop
sudo apt install ros-jazzy-ros-base
```
> **设置环境变量 `★`**
此时直接输入命令`ros2`会出现找不到ros2命令的情况,所以需要设置环境变量,让shell可以找到ros2:
```shell
source /opt/ros/jazzy/setup.bash # 仅仅此次打开shell有效
```
如果想长期默认有效,则需要使用vim编辑`~/.bashrc`文件,在文件末尾追加:
```shell
if [ -f /opt/ros/jazzy/setup.bash ]; then
source /opt/ros/jazzy/setup.bash
fi
```
## 运行ROS2示例
打开一个命令行界面,召唤小乌龟:
```shell
ros2 run turtlesim turtlesim_node
```
打开另一个命令行界面,使用ROS2的消息机制控制乌龟转圈:
```shell
ros2 topic pub --rate 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"
```
这里特别注意:`0.0`是fp类型,不要写为`0`这种int类型的,会报错;另外,`x: 2.0`不要写为`x:2.0`,会报错识别类型失败,中间的空格不能掉!
之后我们打开第三个命令行,录制此时的命令:
```shell
ros2 bag record /turtle1/cmd_vel
```
录制一段时间后`Ctrl+C`停止录制,`ls`当前文件夹,会发现多了一个`rosbag2_当前时间`的文件夹,在里面就是录制的乌龟动作。
此时我们停止第二个让乌龟转圈的命令行,在第三个命令行播放录制的动作,会发现乌龟会继续转一会儿:
```shell
ros2 bag play rosbag2_2025_02_07-17_57_17/rosbag2_2025_02_07-17_57_17_0.mcap
```
Over,此时你已经将ROS2初步安装成功了!最后,转载请注明出处!
补充内容 (2025-2-21 02:33):
补充:ROS2可以选择的系统层:Linux、Windows、MacOS、RTOS等等
补充内容 (2025-2-21 02:53):
补充:ROS2开发环境推荐的插件:
- VSCode:ROS,URDF,Msg Language Support
- JetBrains:[官方配置推荐](https://www.jetbrains.com/help/clion/ros2-tutorial.html)
补充内容 (2025-2-21 03:02):
补充:如果你使用的是WSL下的Ubuntu开发环境,可以在VSCode安装WSL插件,从而在Ubuntu命令行界面输入:`code .`来打开VSCode编辑器,JetBrains的IDEA则直接在Window中打开,在开始界面点击`远程开发->WSL`。
补充内容 (2025-2-21 03:03):
补充:如果使用的是云Ubuntu开发环境,或者任意需要SSH远程连接的开发环境,比如树莓派,则可以直接在对应的代码编辑器选择连接SSH选项,或者下载SSH插件进行开发。
补充内容 (2025-2-21 16:00):
补充:所测评的书籍官网[在这里!](https://book.guyuehome.com/ROS2_Book/)
补充内容 (2025-2-21 16:01):
连接更改:https://book.guyuehome.com/ROS2_Book/0.%E5%89%8D%E8%A8%80/
补充内容 (2025-2-21 17:40):
补充:在启动海龟仿真与海龟键盘控制节点后的海龟玩法:"g|b|v|c|d|e|r|t"来控制海龟旋转,每次旋转30°,"上下左右方向键"控制海龟移动和旋转90°,"f"撤销上一次旋转,"q/Ctrl+C"退出
补充内容 (2025-2-21 17:44):
补充:如果采用虚拟机的Ubuntu,则可以使用Tmux来命令行分块打开多窗口来启动/调试多节点,如果是WSL,那么你只需要使用Windows自带的分屏分窗口即可,如果还是不方便,可以直接右键选项卡,点击拆分,外加F11全屏
- 2025-01-20
-
回复了主题帖:
读书活动入围名单:《ROS2智能机器人开发实践》
个人信息无误,确认可以完成阅读分享计划