ROS系统
			- 
					
						  ROS2 建图与导航:Slam_toolbox、Nav2实践地图构建 在 ROS2 中,地图构建常用 SLAM(Simultaneous Localization and Mapping) 技术。其大概流程是: 传感器采集数据:可使用激光雷达(2D/3D LiDAR)或相机(VSLAM) 里程计信息:来自机器人轮式里程计/IMU SLAM算法处理:融合传感器数据+里程计,估计机器人位姿同时构建环境栅格地图。 地图表示:最终生成map.pgm(图像)+map.yaml(参数配置) SLAM简单理解就是让机器人在一个陌生的环境能够自主定位和地图构建,常用的ROS2建图工具有: slam_toolbox:ROS2官方推荐,支持在线建图(机器人移动时建图)、离线建图(先存bag后生成地图)、增量式建图(可保存后编辑地图)。 cartographer_ros:google出品,实时性能好,但配置较复杂。 RTAB-Map:可用于视觉/激光SLAM,支持3D。 slam_toolbox 先来简单看看slam_toolbox的流程,其输入为/scan(激光雷达点云)、/odom(里程计)、/tf(坐标变换)三个话题;经过处理后输出/map(占据栅格地图nav_msgs/OccupancyGrid格式)、/tf(发布map->odom的变换用于定位)。 (1)安装依赖包 sudo apt install ros-${ROS_DISTRO}-slam-toolbox sudo apt install ros-${ROS_DISTRO}-turtlebot3* 上面的命令第一条命令安装slam_toolbox算法库,其支持在线建图/离线建图/增量式建图,包含的内容有online_async_launch.py(在线异步建图)、online_sync_launch.py(在线同步建图)、offline_launch.py(离线建图),提供的节点有async_slam_toolbox_node、sync_slam_toolbox_node。 第二条命令是安装TurtleBot3 全套支持包,主要是提供机器人模型+仿真环境+驱动+可视化,主要包含的子包有turtlebot3_description(URDF机器人模型)、turtlebot3_gazebo(Gazebo 仿真环境)、turtlebot3_bringup(真机启动配置)、turtlebot3_teleop(键盘遥控程序)、turtlebot3_cartographer(Cartographer 建图的配置)、turtlebot3_navigation2(Nav2 配置文件) 使用turtlebot3要添加环境变量。 echo "export TURTLEBOT3_MODEL=burger" >> ~/.bashrc source ~/.bashrc (2)启动Gazebo仿真环境 ros2 launch turtlebot3_gazebo turtlebot3_house.launch.py 启动之后就看到上图的环境了。启动后创建了以下节点 laumy@ThinkBook-14-G7-IAH:~$ ros2 node list /robot_state_publisher /ros_gz_bridge /robot_state_publisher:根据 URDF 模型发布机器人各个 link 的 TF 变换。输入为/joint_states,输出为/tf, /tf_static。 /ros_gz_bridge:桥接 Gazebo (Ignition/Harmonic) 与 ROS2。将 Gazebo 插件产生的数据转成 ROS2 话题,比如 /odom、/scan、/imu。也把 ROS2 的 /cmd_vel 控制命令转发到 Gazebo 控制插件。 以下话题 laumy@ThinkBook-14-G7-IAH:~$ ros2 topic list /clock /cmd_vel /imu /joint_states /odom /parameter_events /robot_description /rosout /scan /tf /tf_static /clock:仿真时间(来自 Gazebo) /cmd_vel : 速度控制输入(你发指令控制机器人时用) /imu : IMU 传感器数据(加速度、角速度) /joint_states : 关节状态(机器人轮子转角/速度) /odom : 里程计数据(机器人相对位姿估计) /scan : 激光雷达数据(2D 激光点云) /tf, /tf_static : 坐标变换(map/odom/base_link/laser 等) /robot_description : 机器人模型(URDF 内容) /rosout, /parameter_events : 系统日志和参数事件(ROS2 通用) 总体的数据流如下图 (3)运行slam_toolbox建图 ros2 launch slam_toolbox online_async_launch.py use_sim_time:=True online_async_launch.py 表示使用异步建图(适合实时建图),use_sim_time:=True 使用 Gazebo 的仿真时间。 运行上面命令已经开始在建图了。 (4)RViz可视化 ros2 run rviz2 rviz2 在RViz中进行设置 Fixed Frame:设置为map 添加插件:Map(话题/map)、LaserScan(话题/scan) (5)控制机器人移动 ros2 run turtlebot3_teleop teleop_keyboard 用键盘方向键让机器人在环境里转一圈,地图会逐渐补全。 Control Your TurtleBot3! --------------------------- Moving around: w a s d x 这个节点会根据键盘的输入发布消息到 /cmd_vel [geometry_msgs/msg/Twist] (6)地图保存 ros2 run nav2_map_server map_saver_cli 会把内存中的/map话题保存生成map.pgm和map.yaml两个文件,以便后续的导航Nav2使用。生成在当前执行命令的目录下。 laumy@ThinkBook-14-G7-IAH:~/map$ tree . ├── map_1758272082.pgm └── map_1758272082.yaml 1 directory, 2 files Cartographer 待补充 RTAB 待补充 自主导航 目前导航使用比较多的是Nav2,框架如下。 其主要流程可以分为4大部分 感知输入:输入激光雷达/scan、里程计/odom、TF(map->odom->base_link)、地图/map。 定位:使用AMCL(Adaptive Monte Carlo Localization),在已有的地图中定位机器人位置,发布map->odom的TF。 规划:全局规划器 (planner_server)从当前位置到目标点,规划一条全局路径(比如 A*、Dijkstra、NavFn)。局部规划器 (controller_server):根据实时传感器数据,计算机器人下一步的速度指令 /cmd_vel(如 DWB controller)。 执行调度:行为树 (bt_navigator)管理整个任务流程定位 → 规划 → 控制 → 到达目标。恢复器 (recoveries_server)遇到障碍或失败时执行恢复动作(原地旋转、清理代价地图)。 安装 sudo apt update sudo apt install ros-${ROS_DISTRO}-navigation2 sudo apt install ros-${ROS_DISTRO}-nav2-bringup 启动导航 先启动仿真环境,这里使用world地图不用house了。 ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py 再启动导航,需要指定地图。 ros2 launch turtlebot3_navigation2 navigation2.launch.py use_sim_time:=True map:=/home/laumy/map/map_1758275883.yaml 设定导航 (1)设定机器人位置 在 RViz 工具栏选择 2D Pose Estimate 工具。在地图上点击机器人所在位置,拖动鼠标设定朝向。RViz 会往 /initialpose 话题发布一条消息,AMCL 接收到后,就会生成 map → odom 的 TF。 (2)设定目标位置 这样机器人就会自动移动到目标点。
- 
					
						  ROS2机器感知:相机、雷达、IMU相机 相机安装 sudo apt install ros-jazzy-usb-cam 可视化 run usb_cam usb_cam_node_exe --ros-args -p video_device:=/dev/video2 ros2 run rqt_image_vew rqt_image_view 上面的-p指定具体的摄像头,可以用v4l2-ctl --list-devices列出。 参数 查看当前相机的发布话题 laumy@ThinkBook-14-G7-IAH:~/dev_ws$ ros2 topic info -v /image_raw Type: sensor_msgs/msg/Image Publisher count: 1 Node name: usb_cam Node namespace: / Topic type: sensor_msgs/msg/Image Topic type hash: RIHS01_d31d41a9a4c4bc8eae9be757b0beed306564f7526c88ea6a4588fb9582527d47 Endpoint type: PUBLISHER GID: 01.0f.e7.13.47.a9.02.80.00.00.00.00.00.00.18.03 QoS profile: Reliability: RELIABLE History (Depth): UNKNOWN Durability: VOLATILE Lifespan: Infinite Deadline: Infinite Liveliness: AUTOMATIC Liveliness lease duration: Infinite Subscription count: 1 Node name: rqt_gui_cpp_node_43605 Node namespace: / Topic type: sensor_msgs/msg/Image Topic type hash: RIHS01_d31d41a9a4c4bc8eae9be757b0beed306564f7526c88ea6a4588fb9582527d47 Endpoint type: SUBSCRIPTION GID: 01.0f.e7.13.55.aa.5f.b0.00.00.00.00.00.00.14.04 QoS profile: Reliability: BEST_EFFORT History (Depth): UNKNOWN Durability: VOLATILE Lifespan: Infinite Deadline: Infinite Liveliness: AUTOMATIC Liveliness lease duration: Infinite 查看话题下的消息类型 laumy@ThinkBook-14-G7-IAH:~/dev_ws$ ros2 interface show sensor_msgs/msg/Image # This message contains an uncompressed image # (0, 0) is at top-left corner of image std_msgs/Header header # Header timestamp should be acquisition time of image builtin_interfaces/Time stamp int32 sec uint32 nanosec string frame_id # Header frame_id should be optical frame of camera # origin of frame should be optical center of cameara # +x should point to the right in the image # +y should point down in the image # +z should point into to plane of the image # If the frame_id here and the frame_id of the CameraInfo # message associated with the image conflict # the behavior is undefined uint32 height # image height, that is, number of rows uint32 width # image width, that is, number of columns # The legal values for encoding are in file include/sensor_msgs/image_encodings.hpp # If you want to standardize a new string format, join # ros-users@lists.ros.org and send an email proposing a new encoding. string encoding # Encoding of pixels -- channel meaning, ordering, size # taken from the list of strings in include/sensor_msgs/image_encodings.hpp uint8 is_bigendian # is this data bigendian? uint32 step # Full row length in bytes uint8[] data # actual matrix data, size is (step * rows) 三维相机 略 雷达 常见的雷达类型,单线激光雷达、多线激光雷达。 消息格式 可用下面命令查看雷达的消息类型 ros2 interface show sensor_msgs/msg/LaserScan 打印如下: # Single scan from a planar laser range-finder # # If you have another ranging device with different behavior (e.g. a sonar # array), please find or create a different message, since applications # will make fairly laser-specific assumptions about this data std_msgs/Header header # timestamp in the header is the acquisition time of builtin_interfaces/Time stamp int32 sec uint32 nanosec string frame_id # the first ray in the scan. # # in frame frame_id, angles are measured around # the positive Z axis (counterclockwise, if Z is up) # with zero angle being forward along the x axis float32 angle_min # start angle of the scan [rad] float32 angle_max # end angle of the scan [rad] float32 angle_increment # angular distance between measurements [rad] float32 time_increment # time between measurements [seconds] - if your scanner # is moving, this will be used in interpolating position # of 3d points float32 scan_time # time between scans [seconds] float32 range_min # minimum range value [m] float32 range_max # maximum range value [m] float32[] ranges # range data [m] # (Note: values < range_min or > range_max should be discarded) float32[] intensities # intensity data [device-specific units]. If your # device does not provide intensities, please leave # the array empty. 开箱示例 开箱即用的示例 #安装 sudo apt install ros-jazzy-turtlebot3* -y #启动Gazebo仿真 export TURTLEBOT3_MODEL=burger ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py #打开RViz2 ros2 launch turtlebot3_bringup rviz2.launch.py 如果要让车动起来的话 export TURTLEBOT3_MODEL=burger ros2 run turtlebot3_teleop teleop_keyboard 键盘的 W A S D 控制机器人移动,RViz2 里 /scan 会随着车动而更新。 IMU 在 ROS2 里,IMU 指的是 惯性测量单元(Inertial Measurement Unit),是一类传感器的统称。它在机器人系统中非常常见,比如无人车、无人机、移动机器人几乎都需要 IMU。 IMU包含加速度计、陀螺仪、磁力计(可选)。 消息格式 IMU的数据格式如下: laumy@ThinkBook-14-G7-IAH:~/dev_ws$ ros2 interface show sensor_msgs/msg/Imu # This is a message to hold data from an IMU (Inertial Measurement Unit) # # Accelerations should be in m/s^2 (not in g's), and rotational velocity should be in rad/sec # # If the covariance of the measurement is known, it should be filled in (if all you know is the # variance of each measurement, e.g. from the datasheet, just put those along the diagonal) # A covariance matrix of all zeros will be interpreted as "covariance unknown", and to use the # data a covariance will have to be assumed or gotten from some other source # # If you have no estimate for one of the data elements (e.g. your IMU doesn't produce an # orientation estimate), please set element 0 of the associated covariance matrix to -1 # If you are interpreting this message, please check for a value of -1 in the first element of each # covariance matrix, and disregard the associated estimate. std_msgs/Header header builtin_interfaces/Time stamp int32 sec uint32 nanosec string frame_id geometry_msgs/Quaternion orientation #姿态,x,y,z的角度,用 四元数 表示姿态(避免欧拉角万向节锁)。一般看这个就行了。 float64 x 0 float64 y 0 float64 z 0 float64 w 1 float64[9] orientation_covariance # Row major about x, y, z axes geometry_msgs/Vector3 angular_velocity #角速度,绕 x/y/z 轴的角速度 float64 x float64 y float64 z float64[9] angular_velocity_covariance # Row major about x, y, z axes geometry_msgs/Vector3 linear_acceleration #线性加速度,三个分量:沿 x/y/z 轴的加速度 float64 x float64 y float64 z float64[9] linear_acceleration_covariance # Row major x, y z 开箱示例 下面是一个开箱即用的示例。Gazebo 里已经有 IMU 传感器插件,可以直接加载。运行下面命令: ros2 launch ros_gz_sim_demos imu.launch.py 如果要用rviz2来显示的话,需要安装imu插件 sudo apt install ros-jazzy-rviz-imu-plugin 可以在仿真器中选择工具Translate(十字箭头图标)、Rotate(圆弧箭头图标)调整位置和角度,可以看到右边RViz中的也会跟着变化。 也可以使用下面命令来实时打印 ros2 topic echo /imu
- 
					
						  ROS2机器人仿真:urdf模型、gaezbo仿真机器URDF模型 URDF 的全称是 Unified Robot Description Format(统一机器人描述格式)。它是 ROS / ROS2 系统里专门用来描述机器人结构和属性的一种 XML 格式文件。简单来说,URDF 就是“机器人说明书”,告诉 ROS:这个机器人长什么样、由哪些零件组成、它们之间怎么连接。 URDF核心作用是建模机器人、仿真可视化、运动学与动力学计算。一个带轮式的机器人URDF示例: <robot name="simple_bot"> <!-- 车体 --> <link name="base_link"> <visual> <geometry> <box size="0.5 0.3 0.1"/> </geometry> </visual> </link> <!-- 左轮 --> <link name="left_wheel"/> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel"/> <origin xyz="0.2 0.15 0"/> <axis xyz="0 1 0"/> </joint> <!-- 右轮 --> <link name="right_wheel"/> <joint name="right_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="right_wheel"/> <origin xyz="0.2 -0.15 0"/> <axis xyz="0 1 0"/> </joint> </robot> 创建一个URDF (1)创建一个包 ros2 pkg create --build-type ament_python learn_urdf --dependencies rclpy robot_state_publisher joint_state_publisher_gui rviz2 (2)新建urdf和launch目录 主要用于存放URDF和launch文件。 cd learn_urdf mkdir urdf launch (3)编写urdf文件 进入urdf目录,创建simple_bot.urdf模型文件。 <?xml version="1.0"?> <robot name="simple_bot"> <!-- 车体 --> <link name="base_link"> <visual> <geometry> <box size="0.5 0.3 0.1"/> <!-- 长0.5m 宽0.3m 高0.1m --> </geometry> <material name="gray"> <color rgba="0.7 0.7 0.7 1.0"/> </material> </visual> </link> <!-- 左轮 --> <link name="left_wheel"> <visual> <geometry> <cylinder radius="0.05" length="0.02"/> <!-- 半径0.05m 厚度0.02m --> </geometry> <!-- 旋转90度,让圆柱变成“轮子”横放 --> <origin xyz="0 0 0" rpy="1.5708 0 0"/> <material name="black"> <color rgba="0 0 0 1.0"/> </material> </visual> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel"/> <origin xyz="0.2 0.15 0"/> <!-- 相对于车体的位置 --> <axis xyz="0 0 1"/> <!-- 绕Z轴旋转 --> </joint> <!-- 右轮 --> <link name="right_wheel"> <visual> <geometry> <cylinder radius="0.05" length="0.02"/> </geometry> <origin xyz="0 0 0" rpy="1.5708 0 0"/> <material name="black"> <color rgba="0 0 0 1.0"/> </material> </visual> </link> <joint name="right_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="right_wheel"/> <origin xyz="0.2 -0.15 0"/> <axis xyz="0 0 1"/> <!-- 改成绕 Z 轴旋转 --> </joint> </robot> (4)创建launch文件 进入launch目录,编写display.launch.py文件 from launch import LaunchDescription from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory import os def generate_launch_description(): # 找到 URDF 文件路径 urdf_file = os.path.join( get_package_share_directory('learn_urdf'), 'urdf', 'simple_bot.urdf' ) # 读取 URDF 文件 with open(urdf_file, 'r') as infp: robot_desc = infp.read() # robot_state_publisher 节点 rsp_node = Node( package='robot_state_publisher', executable='robot_state_publisher', output='screen', parameters=[{'robot_description': robot_desc}] ) # joint_state_publisher_gui 节点(带GUI,可以动关节) jsp_node = Node( package='joint_state_publisher_gui', executable='joint_state_publisher_gui', name='joint_state_publisher_gui', output='screen' ) # rviz2 节点 rviz_node = Node( package='rviz2', executable='rviz2', name='rviz2', output='screen' ) return LaunchDescription([rsp_node, jsp_node, rviz_node]) (5)配置setup.py from setuptools import setup import os from glob import glob ...... data_files=[ # 让 ROS2 知道这个包的安装位置 ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), # 安装 launch 文件 (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')), # 安装 urdf 文件 (os.path.join('share', package_name, 'urdf'), glob('urdf/*.urdf')), ], ...... 找到 setup.py,把里面的 data_files 部分改成上面那样,主要加入导入的模块。 (6)编译运行 cd ~/dev_ws colcon build source install/setup.bash ros2 launch learn_urdf display.launch.py 执行上面命令后,就可以看到运行了rviz2,然后配置一下rviz2即可显示模型。 最后的效果就是这样,可以通过joint state publish调整轮子方向。 XACRO模型优化 XACRO是XML Macros,它是ROS 中对 URDF 的扩展,可以理解为 “带变量和函数的 URDF”。普通 URDF 是纯 XML,很死板,写起来很长;XACRO 在 URDF 的基础上加了变量、宏、表达式计算、条件/循环。 如下示例一个普通的URDF <link name="leg1"> <visual><geometry><cylinder radius="0.05" length="0.3"/></geometry></visual> </link> <link name="leg2"> <visual><geometry><cylinder radius="0.05" length="0.3"/></geometry></visual> </link> 有很多重复,如果用XACRO <xacro:macro name="leg" params="name"> <link name="${name}"> <visual> <geometry> <cylinder radius="0.05" length="0.3"/> </geometry> </visual> </link> </xacro:macro> <xacro:leg name="leg1"/> <xacro:leg name="leg2"/> 机器模型动起来 如果要让创建的模型能够动起来,那就要在Gazebo加载模型,然后通过/cmd_vel控制就可以让机器动起来了。 Gazebo是如何加载机器的模型了? 首先在xxx.launch.py中将机器的描述文件.xacro展开为标准的.urdf XML内容。然后会通过 robot_state_publisher 节点把这个 URDF 存到 ROS 参数服务器里/robot_description。 import xacro robot_desc = xacro.process_file('simple_bot.urdf.xacro').toxml() 其次,Gazebo并不能直接解析 URDF,它使用的是 SDF (Simulation Description Format)。需要通过spawn_entity.py 读取 URDF → 转成 SDF。因此在launch文件中有看到spawn_entity.py。 ros2 run gazebo_ros spawn_entity.py -topic robot_description -entity simple_bot 最后Gazebo 根据 SDF 构建物理模型,比如SDF的link、joint等等。 运动控制 这里直接就不在演示完整创建过程了,直接使用示例程序。 ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py ros2 run teleop_twist_keyboard teleop_twist_keyboard 执行上面两个命令,就可以在控制终端i/j/k/l在仿真环境中控制机器人移动了。 相机仿真 可以给机器装上相机 ros2 launch learning_gazebo_harmonic load_mbot_camera_into_gazebo_harmonic.launch.py ros2 run rviz2 rviz2 雷达仿真 ros2 launch learning_gazebo_harmonic load_mbot_lidar_into_gazebo_harmonic.launch.py ros2 run rviz2 rviz2 附录:本文主要来自《ROS2智能机器人开发实践》笔记
- 
					
						  ROS2常用开发工具:launch、TF、Gazebo、rosbag、rqtlaunch 在ROS2中,lauch是一个程序启动器,它的作用是一条命令可以启动多个节点,加载参数文件(YAML),重映射话题/服务/动作名称,条件启动(仿真/真机切换),嵌套调用其他launch文件。 lauch文件支持3种格式 格式 后缀 特点 推荐度 Python .launch.py 功能最强,可写逻辑、条件,官方推荐 ⭐⭐⭐⭐⭐ XML .launch.xml 类似 ROS1,结构清晰,但灵活性差 ⭐⭐ YAML .launch.yaml 简洁配置,功能最弱 ⭐ 但在ROS2中目前更多的使用.launch.py。这里我们重点介绍此种方式。 launch文件结构 最小的结构示例 from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node(package='pkg_name', executable='exe_name', name='node_name') ]) 创建launch文件需要导入launch和launch_ros模块,入口函数为generate_launch_description(),该函数中定义了要启动的节点,通过LaunchDescription来描述节点如包、执行程序、名称。 使用步骤 (1)创建一个包 cd ~/dev_ws/src ros2 pkg create --build-type ament_python launch_demo (2)新建launch目录 一般launch文件都会有个单独的目录,因此新建目录用于存放launch文件。 cd launch_demo mkdir launch (3)编写lauch文件 demo.launch.py from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node(package='learn_topic_cpp', executable='topic_helloworld_pub', name='pub', output='screen'), Node(package='learn_topic_cpp', executable='topic_helloworld_sub', name='sub', output='screen') ]) (4)修改setup.py 注册launch文件,主要是将.launch文件要拷贝到install/share/xxx/launch目录下。 from setuptools import find_packages, setup import os from glob import glob ...... data_files=[ (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')), ] ..... 需要导入setuptools、os、glob这几个模块。 (5)编译&环境刷新 cd ~/dev_ws colcon build source install/setup.bash (6)运行测试 ros2 launch launch_demo demo.launch.py 进阶功能 (1)加载参数 config/params.yaml my_node: ros__parameters: use_sim_time: true max_speed: 2.0 launch 文件: Node( package='my_pkg', executable='my_node', parameters=['config/params.yaml'] ) (2)命令行参数 from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration DeclareLaunchArgument('use_sim_time', default_value='false') Node( package='my_pkg', executable='my_node', parameters=[{'use_sim_time': LaunchConfiguration('use_sim_time')}] ) 在运行的时候可以传递参数 ros2 launch my_pkg demo.launch.py use_sim_time:=true (3)条件启动 from launch.conditions import IfCondition Node( package='gazebo_ros', executable='gzserver', condition=IfCondition(LaunchConfiguration('use_gazebo')) ) 只有当条件use_gazebo满足时才会启动,如运行。 ros2 launch my_pkg demo.launch.py use_gazebo:=true (4)话题重映射 Node( package='my_pkg', executable='controller', remappings=[('/cmd_vel', '/robot1/cmd_vel')] ) 可以将节点中的话题进行重映射,如把节点内部用到的 /cmd_vel 改名为 /robot1/cmd_vel,在实际场景中多机器人系统可以避免冲突。 TF TF是transform的缩写,其作用主要是维护并查询机器人系统中各个坐标系之间的关系(包括位置+姿态)。其主要的核心功能如下: 管理多个坐标系的父子关系(各个坐标系之间形成一颗TF树) 提供任意两个坐标系之间的变换(平移+旋转) 试试广播、查询坐标关系。 可以理解为TF就像是一个全局的时空字典,每个坐标系(Frame)都是字典里的key,而变化关系是key之间的link。 常见的坐标系有 map:世界地图坐标系,一般是静态的,当机器人定位完成后,map是全局的参考。 odom:里程计坐标系(相对),会随着时间推移会漂移。 base_link:机器人本地中心,是所有传感器和控制指令的基准。 camera_link:摄像头坐标系。 laser_link:雷达坐标系。 典型的TF树 map → odom → base_link → camera_link → laser_link 每个坐标都有一个父坐标系(除了根map),变换是单项定义的。孙子的坐标系的位姿等于“父坐标系的位姿+子坐标系的相对位姿”。如上要知道map与camera_link的关系。则T_map_camera = T_map_odom × T_odom_base × T_base_camera 运行命令,查询 turtle2 坐标系相对于 turtle1 坐标系的变换关系。 ros2 run tf2_ros tf2_echo turtle2 turtle1 打印数据如下: At time 1758102120.301409314 - Translation: [3.679, -0.208, 0.000] - Rotation: in Quaternion [0.000, 0.000, 0.003, 1.000] - Rotation: in RPY (radian) [0.000, -0.000, 0.006] - Rotation: in RPY (degree) [0.000, -0.000, 0.364] - Matrix: 1.000 -0.006 0.000 3.679 0.006 1.000 0.000 -0.208 0.000 0.000 1.000 0.000 0.000 0.000 0.000 1.000 Translation:平移,子坐标系的原点,相对于父坐标系的位置是x = 3.679 m,y = -0.208 m,z = 0 m。 Quaternion:旋转,单位四元数。 RPY :欧拉角。 Matrix:变换矩阵。 也可以使用rviz2来直观感受一下 广播与查询 举例一个场景机器人本体 base_link → 传感器 camera_link。 (1)广播父子坐标 import rclpy from rclpy.node import Node from geometry_msgs.msg import TransformStamped from tf2_ros import TransformBroadcaster import tf_transformations # 四元数工具 class FrameBroadcaster(Node): def __init__(self): super().__init__('frame_broadcaster') self.broadcaster = TransformBroadcaster(self) self.timer = self.create_timer(0.1, self.broadcast_tf) # 10Hz 发布 def broadcast_tf(self): t = TransformStamped() t.header.stamp = self.get_clock().now().to_msg() t.header.frame_id = 'base_link' # 父坐标系 t.child_frame_id = 'camera_link' # 子坐标系 # 平移: 相机在机器人前方 0.2m,高 0.5m t.transform.translation.x = 0.2 t.transform.translation.y = 0.0 t.transform.translation.z = 0.5 # 旋转: 无旋转 q = tf_transformations.quaternion_from_euler(0, 0, 0) t.transform.rotation.x = q[0] t.transform.rotation.y = q[1] t.transform.rotation.z = q[2] t.transform.rotation.w = q[3] self.broadcaster.sendTransform(t) def main(): rclpy.init() node = FrameBroadcaster() rclpy.spin(node) if __name__ == '__main__': main() 调用TransformBroadcaster用于广播坐标变换父->子。 (2)查询坐标 import rclpy from rclpy.node import Node from tf2_ros import Buffer, TransformListener class FrameListener(Node): def __init__(self): super().__init__('frame_listener') self.tf_buffer = Buffer() self.listener = TransformListener(self.tf_buffer, self) self.timer = self.create_timer(1.0, self.lookup_tf) def lookup_tf(self): try: # 查询 base_link 在 map 下的变换 trans = self.tf_buffer.lookup_transform( 'map', # 父坐标系 'base_link', # 子坐标系 rclpy.time.Time()) t = trans.transform.translation r = trans.transform.rotation self.get_logger().info( f"位置: x={t.x:.2f}, y={t.y:.2f}, z={t.z:.2f}; " f"四元数: [{r.x:.3f}, {r.y:.3f}, {r.z:.3f}, {r.w:.3f}]" ) except Exception as e: self.get_logger().warn(f"查询失败: {e}") def main(): rclpy.init() node = FrameListener() rclpy.spin(node) if __name__ == '__main__': main() 调用TransformListener + Buffer查询坐标,打印如下: 位置: x=3.68, y=-0.21, z=0.00; 四元数: [0.000, 0.000, 0.003, 1.000] 也可以用命令行: ros2 run tf2_ros tf2_echo base_link camera_link Gazebo 安装 sudo apt-get update sudo apt-get install lsb-release gnupg sudo curl https://packages.osrfoundation.org/gazebo.gpg --output /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] https://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null sudo apt-get update sudo apt install gz-harmonic sudo apt install ros-jazzy-ros-gz 详情参考Binary Installation on Ubuntu。 安装完成后,启动下面命令正常启动就说明安装好了。 gz sim 机器仿真示例 启动一个机器仿真环境 ros2 launch ros_gz_sim_demos diff_drive.launch.py 启动一个键盘控制节点 ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=model/vehicle_blue/cmd_vel 使用i/j/,/l四个按键可以控制机器人前后左右动作。 传感器示例 ros2 launch ros_gz_sim_demos rgbd_camera_bridge.launch.py 仿真摄像头,运行上面命令后会打开Gazebo仿真界面和RViz上位机。可以在Gazebo中直接看到RGBD相机仿真后发布的图像数据。 RViz RViz 全称 ROS Visualization,是 ROS(Robot Operating System)里一个非常重要的三维可视化工具。它的作用主要是帮助开发者 直观地查看机器人系统中的数据和状态。 RViz的作用主要有坐标系可视化、传感器数据展示、机器人模型显示、路径与轨迹可视化、调试与验证等等。 RViz的核心功能模块主要有Displays、Views、Tools、Topics订阅。 RViz只负责可视化(接受数据显示)不会进行物理仿真,而Gazebo是物理仿真器,可以模拟机器人运动、环境交互。常用的做法是Gazebo负责仿真将数据发布到ROS 然后通过RViz显示数据。 RViz已经集成到完整版的ROS中,一般不需要额外单独安装。 ros2 run rviz2 rviz2 示例:tf数据可视化 分别启动两个终端,运行海龟跟随运动。 ros2 launch learning_tf turtle_following_demo.launch.py ros2 run turtlesim turtle_teleop_key 接着运行rviz2 ros2 run rviz2 rviz2 添加TF 坐标系显示 示例:图像数据可视化 ros2 run usb_cam usb_cam_node_exe 执行启动相机驱动,如果找不到命令就先安装sudo apt install ros-jazzy-usb-cam 按照上面的方式添加image,如果报错 需要把topic订阅改为/image_raw rosbag rosbag 是 ROS(Robot Operating System)中的一种数据记录与回放工具,用来保存和重放机器人运行时产生的各种消息数据。简单来说,它就像“黑匣子”,能把机器人运行时的传感器、话题消息、控制指令等全部记录下来,方便后期调试和复现实验。 记录数据 先启动"老演员" ros2 run turtlesim turtlesim_node ros2 run turtlesim turtle_teleop_key 然后创建一个文件夹用于存放录制数据 mkdir ~/bagfiles cd ~/bagfiles/ 列出当前有哪些话题 ros2 topic list -v Published topics: * /parameter_events [rcl_interfaces/msg/ParameterEvent] 3 publishers * /rosout [rcl_interfaces/msg/Log] 3 publishers * /turtle1/cmd_vel [geometry_msgs/msg/Twist] 1 publisher * /turtle1/color_sensor [turtlesim/msg/Color] 1 publisher * /turtle1/pose [turtlesim/msg/Pose] 1 publisher Subscribed topics: * /parameter_events [rcl_interfaces/msg/ParameterEvent] 3 subscribers * /turtle1/cmd_vel [geometry_msgs/msg/Twist] 1 subscriber 接下来就记录/turtle1/cmd_vel话题。 ros2 bag record /turtle1/cmd_vel 这样只要在键盘终端控制海龟不断移动,就可以记录下数据了。停止记录Ctrl+C。 laumy@ThinkBook-14-G7-IAH:~/bagfiles$ tree . └── rosbag2_2025_09_18-12_52_52 ├── metadata.yaml └── rosbag2_2025_09_18-12_52_52_0.mcap 2 directories, 2 files 回放数据 可以使用下面命令查看数据文件信息。 laumy@ThinkBook-14-G7-IAH:~/bagfiles$ ros2 bag info rosbag2_2025_09_18-13_01_53/ Files: rosbag2_2025_09_18-13_01_53_0.mcap Bag size: 29.8 KiB Storage id: mcap ROS Distro: jazzy Duration: 26.796084773s Start: Sep 18 2025 13:01:58.126984611 (1758171718.126984611) End: Sep 18 2025 13:02:24.923069384 (1758171744.923069384) Messages: 251 Topic information: Topic: /turtle1/cmd_vel | Type: geometry_msgs/msg/Twist | Count: 251 | Serialization Format: cdr Service: 0 Service information: 回放命令: ros2 bag play rosbag2_2025_09_18-13_01_53/ 这样海龟就会复制刚才键盘执行的运动路径。 rqt rqt 是 ROS 官方提供的一个基于 Qt 的 GUI 框架,本质上是一个 插件管理和可视化平台。 它的设计理念是:ROS 系统是分布式的,节点、话题、服务、参数等很多,调试和监控光靠命令行不直观,因此需要一个统一的 图形化工具箱 来观察和操作。 使用如下命令来安装 sudo apt install ros-jazzy-rqt 安装完成之后,执行 rqt rqt默认是没有选择任何插件。要添加插件,需要从插件菜单中选择项目。 日志显示 日志显示有两种打开方式一种是执行rqt后,在Plugins->Logging->Console-打开,另外一种是执行下面命令 ros2 run rqt_console rqt_console 图像显示 在启动之前先运行usb ros2 run usb_cam usb_cam_node_exe UI界面启动"Plugins->Visualization>Image View"或者通过命令的方式: ros2 run rqt_image_view rqt_image_view 接着添加订阅的话题 发布话题/服务数据 不仅可以在命令行中发布话题或服务,也可以通过rqt工具发布。 先启动turtlesim ros2 run turtlesim turtlesim_node 然后使用rqt的"Plugin->Topics->MessagePublish"。 绘制数据曲线 rqt还可以绘制数据曲线,将需要显示的xy坐标使用曲线描述出来,便于体现机器人的速度、位置等信息随时间变化的趋势。 "Plugins->Visualization>plot" 然后依次添加topic,就可以记录轨迹了。 节点可视化 打开“introspection->Node Graph”可以看到系统中所有的节点关系。 附录:本文来自《ROS2智能机器人开发实践》笔记
- 
					
						  ROS2节点通信:话题、服务、动作三剑客+参数简介 在ROS系统每个节点可以理解为一个进程,更准确的说法它是一个包含了特定功能的独立执行单元。节点在ROS2中通常是一个可执行文件,负责执行特定的任务,如控制机器人、传感器数据处理等。每个节点可以通过ROS2的通信机制与其他节点进行数据交换。 在具体的工程应用中,需要多个节点进行配合完成一个具体的产品。那么多个节点直接如何进行通信了,ROS系统提供了话题、服务、动作这几种方式,当然除了上面3中之外还有一种参数的方式,这种方式是一种共享的全局变量,让各个节点可以进行查询/设置参数以达到信息传输的目的。 话题 搞过网络MQTT的对ROS2中的话题通信理解就简单了,可以说ROS2中的话题通信与MQTT是一模一样的。话题通信分为发布者和订阅者。发布者通过话题(topic)发布消息,订阅者通过订阅该话题后可接受到该消息。话题的通信是异步通信的方式,结构上是多对多的关系。 python示例 先创建一个包 cd ~/dev_ws/src/ ros2 pkg create --build-type ament_python learning_topic_python 创建一个发布者节点程序topic_hello_pub.py import rclpy from rclpy.node import Node from std_msgs.msg import String class PublisherNode(Node): def __init__(self, name): super().__init__(name) self.pub = self.create_publisher(String, "hello", 10) self.timer = self.create_timer(1.0, self.publish_message) def publish_message(self): msg = String() msg.data = "Hello, ROS2!" self.pub.publish(msg) self.get_logger().info("Published: %s" % msg.data) def main(args=None): rclpy.init(args=args) node = PublisherNode("publisher_node") rclpy.spin(node) node.destroy_node() rclpy.shutdown() 创建订阅者节点程序topic_hello_sub.py import rclpy from rclpy.node import Node from std_msgs.msg import String class SubscriberNode(Node): def __init__(self, name): super().__init__(name) self.sub = self.create_subscription(String, "hello", self.callback, 10) def callback(self, msg): self.get_logger().info("Received: %s" % msg.data) def main(args=None): rclpy.init(args=args) node = SubscriberNode("subscriber_node") rclpy.spin(node) node.destroy_node() rclpy.shutdown() 程序运行发布者 ros2 run learning_topic_python topic_hello_pub 程序运行订阅者 ros2 run learning_topic_python topic_hello_sub 核心点就是发布调用self.create_publisher,订阅调用self.create_subscription。 C++示例 创建一个包 ros2 pkg create --build-type ament_cmake learn_topic_cpp 编写发布者节点 #include <chrono> #include <functional> #include <memory> #include <string> #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" using namespace std::chrono_literals; class PublisherNode : public rclcpp::Node { public: PublisherNode() : Node("topic_hello") { publisher_ = this->create_publisher<std_msgs::msg::String>("hello", 10); timer_ = this->create_wall_timer(1s, std::bind(&PublisherNode::timer_callback, this)); } private: void timer_callback() { auto msg = std_msgs::msg::String(); msg.data = "Hello, ROS2!"; publisher_->publish(msg); RCLCPP_INFO(this->get_logger(), "Published: %s", msg.data.c_str()); } rclcpp::TimerBase::SharedPtr timer_; rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<PublisherNode>()); rclcpp::shutdown(); return 0; } 编写订阅者节点 #include <memory> #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" using namespace std::chrono_literals; using std::placeholders::_1; class SubscriberNode : public rclcpp::Node { public: SubscriberNode() : Node("topic_hello") { subscription_ = this->create_subscription<std_msgs::msg::String>("hello", 10, std::bind(&SubscriberNode::topic_callback, this, _1)); RCLCPP_INFO(this->get_logger(), "SubscriberNode initialized"); } 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<std_msgs::msg::String>::SharedPtr subscription_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<SubscriberNode>()); rclcpp::shutdown(); return 0; } 运行测试 到这里话题的python和cpp示例都实践了,话题主要使用的是异步的方式。 服务 在 ROS 2 中,服务(Service)通信是一种 请求-响应模式,允许客户端发送请求到服务端,服务端处理请求并返回响应。这种方式通常用于需要同步交互的情况,如远程调用、控制请求或计算任务等。这种通信方式与HTTP的通信方式类似,只有一个服务器,由客户端发起get,然后服务端响应数据。 python示例 创建一个包 ros2 pkg create --build-type ament_python learn_srv_python 创建一个add_server.py节点 import sys import rclpy from rclpy.node import Node from learning_interface.srv import AddTwoInts class adderServer(Node): def __init__(self, name): super().__init__(name) self.srv = self.create_service(AddTwoInts, 'add_two_int', self.adder_callback) def adder_callback(self, request, response): response.sum = request.a + request.b self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) return response def main(args=None): rclpy.init(args=args) node = adderServer("Service_adder_server") rclpy.spin(node) rclpy.shutdown() 再创建一个add_client.py节点 from socket import timeout import sys import rclpy from rclpy.node import Node from learning_interface.srv import AddTwoInts class adderClient(Node): def __init__(self, name): super().__init__(name) self.client = self.create_client(AddTwoInts, 'add_two_int') while not self.client.wait_for_service(timeout_sec=1.0): self.get_logger().info('server not available, waiting again...') self.request = AddTwoInts.Request() def send_request(self): self.request.a = int(sys.argv[1]) self.request.b = int(sys.argv[2]) self.future = self.client.call_async(self.request) def main(args=None): rclpy.init(args=args) node = adderClient("Service_adder_client") node.send_request() while rclpy.ok(): rclpy.spin_once(node) if node.future.done(): try: response = node.future.result() except Exception as e: node.get_logger().info( 'Service call failed %r' %(e,)) else: node.get_logger().info( 'Result of add_two_ints: for %d + %d = %d' % (node.request.a, node.request.b, response.sum)) break node.destroy_node() rclpy.shutdown() 运行结果如下 示例演示了客户端请求两个数给服务器计算求和,服务器收到客户端的数据后,计算后返回结果。 在创建client和server的时候,要能够匹配上需要有共同的地址如create_service参数为"add_two_int"和create_client的参数地址一样。在服务端需要注册一个回调函数adder_callback,当收到客户端的数据时调用回调函数返回结果。 C++示例 创建一个C++的包 ros2 pkg create --build-type ament_cmake learn_srv_cpp 创建add_server.cpp节点代码 #include "rclcpp/rclcpp.hpp" #include "learning_interface/srv/add_two_ints.hpp" #include <memory> void adderServer(const std::shared_ptr<learning_interface::srv::AddTwoInts::Request> request, std::shared_ptr<learning_interface::srv::AddTwoInts::Response> response) { response->sum = request->a + request->b; RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld b: %ld", request->a, request->b); RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", response->sum); } int main(int argc, char **argv) { rclcpp::init(argc, argv); std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("service_adder_server"); rclcpp::Service<learning_interface::srv::AddTwoInts>::SharedPtr service = node->create_service<learning_interface::srv::AddTwoInts>("add_two_int", &adderServer); RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints."); rclcpp::spin(node); rclcpp::shutdown(); } 创建一个add_client.cpp节点代码 #include "rclcpp/rclcpp.hpp" #include "learning_interface/srv/add_two_ints.hpp" #include <memory> #include <chrono> #include <cstdlib> using namespace std::chrono_literals; int main(int argc, char **argv) { rclcpp::init(argc, argv); if (argc != 3) { RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: service_adder_client X Y"); return 1; } std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("service_adder_client"); rclcpp::Client<learning_interface::srv::AddTwoInts>::SharedPtr client = node->create_client<learning_interface::srv::AddTwoInts>("add_two_int"); auto request = std::make_shared<learning_interface::srv::AddTwoInts::Request>(); request->a = atoll(argv[1]); request->b = atoll(argv[2]); while (!client->wait_for_service(1s)) { if (!rclcpp::ok()) { RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting."); return 0; } RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again..."); } auto result = client->async_send_request(request); // Wait for the result if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) { RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Result of add_two_ints: for %ld + %ld = %ld", request->a, request->b, result.get()->sum); } else { RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints"); } rclcpp::shutdown(); return 0; } 结果运行如下: 总结一下服务的通信方式可以理解为跟http的类似,基于请求和响应的方式进行,只有一个服务器,但是可以有多个客户端是一个一对多的通信结构。数据的通信跟话题相比其是同步传输,而话题是异步传输。 动作 ROS 2 的 Action 是一种用于实现异步、长时间运行的任务的通信机制,常用于那些需要反馈或结果的任务。相比 ROS 2 的 Service(同步请求-响应模式),Action 允许客户端请求任务并在任务执行过程中接收反馈,最终获取任务的结果。它通常适用于需要较长时间处理的任务,如机器人运动控制、路径规划、抓取任务等。 具体的通信流程如下: 可以分为3个阶段 阶段1:客户端异步的方式发送目标。 阶段2:服务端定期反馈实时结果。 阶段3:服务端反馈最终的结果。 python示例 创建一个包 ros2 pkg create --build-type ament_python learn_action_python 创建一个服务端节点程序action_server.py import time import rclpy # ROS2 Python接口库 from rclpy.node import Node # ROS2 节点类 from rclpy.action import ActionServer # ROS2 动作服务器类 from learning_interface.action import MoveCircle # 自定义的圆周运动接口 class MoveCircleActionServer(Node): def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self._action_server = ActionServer( # 创建动作服务器(接口类型、动作名、回调函数) self, MoveCircle, 'move_circle', self.execute_callback) def execute_callback(self, goal_handle): # 执行收到动作目标之后的处理函数 self.get_logger().info('Moving circle...') feedback_msg = MoveCircle.Feedback() # 创建一个动作反馈信息的消息 for i in range(0, 360, 30): # 从0到360度,执行圆周运动,并周期反馈信息 feedback_msg.state = i # 创建反馈信息,表示当前执行到的角度 self.get_logger().info('Publishing feedback: %d' % feedback_msg.state) goal_handle.publish_feedback(feedback_msg) # 发布反馈信息 time.sleep(0.5) self.get_logger().info('Goal succeeded') goal_handle.succeed() # 动作执行成功 result = MoveCircle.Result() # 创建结果消息 result.finish = True return result # 反馈最终动作执行的结果 def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = MoveCircleActionServer("action_move_server") # 创建ROS2节点对象并进行初始化 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口 创建一个客户端阶段程序action_client.py import rclpy # ROS2 Python接口库 from rclpy.node import Node # ROS2 节点类 from rclpy.action import ActionClient # ROS2 动作客户端类 from learning_interface.action import MoveCircle # 自定义的圆周运动接口 class MoveCircleActionClient(Node): def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self._action_client = ActionClient( # 创建动作客户端(接口类型、动作名) self, MoveCircle, 'move_circle') def send_goal(self, enable): # 创建一个发送动作目标的函数 self.get_logger().info('Waiting for action server...') self._action_client.wait_for_server() # 等待动作的服务器端启动 goal_msg = MoveCircle.Goal() # 创建一个动作目标的消息 goal_msg.enable = enable # 设置动作目标为使能,希望机器人开始运动 self.get_logger().info('Sending goal request...') self._send_goal_future = self._action_client.send_goal_async( # 异步方式发送动作的目标 goal_msg, # 动作目标 feedback_callback=self.feedback_callback) # 处理周期反馈消息的回调函数 self._send_goal_future.add_done_callback(self.goal_response_callback) # 设置一个服务器收到目标之后反馈时的回调函数 def goal_response_callback(self, future): # 创建一个服务器收到目标之后反馈时的回调函数 goal_handle = future.result() # 接收动作的结果 if not goal_handle.accepted: # 如果动作被拒绝执行 self.get_logger().info('Goal rejected :(') return self.get_logger().info('Goal accepted :)') # 动作被顺利执行 self._get_result_future = goal_handle.get_result_async() # 异步获取动作最终执行的结果反馈 self._get_result_future.add_done_callback(self.get_result_callback) # 设置一个收到最终结果的回调函数 def get_result_callback(self, future): # 创建一个收到最终结果的回调函数 result = future.result().result # 读取动作执行的结果 self.get_logger().info('Result: {%d}' % result.finish) # 日志输出执行结果 def feedback_callback(self, feedback_msg): # 创建处理周期反馈消息的回调函数 feedback = feedback_msg.feedback # 读取反馈的数据 self.get_logger().info('Received feedback: {%d}' % feedback.state) def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = MoveCircleActionClient("action_move_client") # 创建ROS2节点对象并进行初始化 node.send_goal(True) # 发送动作目标 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口 运行 总结一下流程: 客户端 服务端 | | |-- 发送目标 ----------->| | |-- execute_callback() 被调用 | | (开始执行动作) | | |<-- 目标接受/拒绝 ------|-- 立即响应目标请求 | | |-- goal_response_callback 被调用 | | | |-- 开始循环执行 | | (0°, 30°, 60°...) | | |<-- 反馈消息 ----------|-- publish_feedback(0°) | | |-- feedback_callback 被调用 | | |<-- 反馈消息 ----------|-- publish_feedback(30°) | | |-- feedback_callback 被调用 | | |<-- 反馈消息 ----------|-- publish_feedback(60°) | | |-- feedback_callback 被调用 | | | ... (继续循环) ... | | | |<-- 反馈消息 ----------|-- publish_feedback(330°) | | |-- feedback_callback 被调用 | | | |-- 动作执行完成 | |-- goal_handle.succeed() | |-- 返回结果 | | |<-- 最终结果 ----------|-- 发送结果消息 | | |-- get_result_callback 被调用 | | |-- 动作完成 -----------| 客户端一共注册了3个回调: goal_response_callback:服务器第一次接收到目标请求后的回调函数,调用self._send_goal_future.add_done_callback设置。 feedback_callback:调用send_goal_async时注册,这个回调是服务器实时反馈的回调函数。 get_result_callback:服务器最后返回的结果回调。调用self._get_result_future.add_done_callback注册。 服务端就仅仅只有execute_callback一个回调,即对应客户端的feedback回调,另外两个都是隐式的。 C++示例 创建一个包 ros2 pkg create --build-type ament_cmake learn_action_cpp 创建客户端节点程序action_client.cpp #include <iostream> #include "rclcpp/rclcpp.hpp" // ROS2 C++接口库 #include "rclcpp_action/rclcpp_action.hpp" // ROS2 动作类 #include "learning_interface/action/move_circle.hpp" // 自定义的圆周运动接口 using namespace std; class MoveCircleActionClient : public rclcpp::Node { public: // 定义一个自定义的动作接口类,便于后续使用 using CustomAction = learning_interface::action::MoveCircle; // 定义一个处理动作请求、取消、执行的客户端类 using GoalHandle = rclcpp_action::ClientGoalHandle<CustomAction>; explicit MoveCircleActionClient(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions()) : Node("action_move_client", node_options) // ROS2节点父类初始化 { this->client_ptr_ = rclcpp_action::create_client<CustomAction>( // 创建动作客户端(接口类型、动作名) this->get_node_base_interface(), this->get_node_graph_interface(), this->get_node_logging_interface(), this->get_node_waitables_interface(), "move_circle"); } // 创建一个发送动作目标的函数 void send_goal(bool enable) { // 检查动作服务器是否可以使用 if (!this->client_ptr_->wait_for_action_server(std::chrono::seconds(10))) { RCLCPP_ERROR(this->get_logger(), "Client: Action server not available after waiting"); rclcpp::shutdown(); return; } // 绑定动作请求、取消、执行的回调函数 auto send_goal_options = rclcpp_action::Client<CustomAction>::SendGoalOptions(); using namespace std::placeholders; send_goal_options.goal_response_callback = std::bind(&MoveCircleActionClient::goal_response_callback, this, _1); send_goal_options.feedback_callback = std::bind(&MoveCircleActionClient::feedback_callback, this, _1, _2); send_goal_options.result_callback = std::bind(&MoveCircleActionClient::result_callback, this, _1); // 创建一个动作目标的消息 auto goal_msg = CustomAction::Goal(); goal_msg.enable = enable; // 异步方式发送动作的目标 RCLCPP_INFO(this->get_logger(), "Client: Sending goal"); this->client_ptr_->async_send_goal(goal_msg, send_goal_options); } private: rclcpp_action::Client<CustomAction>::SharedPtr client_ptr_; // 创建一个服务器收到目标之后反馈时的回调函数 void goal_response_callback(GoalHandle::SharedPtr goal_message) { if (!goal_message) { RCLCPP_ERROR(this->get_logger(), "Client: Goal was rejected by server"); rclcpp::shutdown(); // Shut down client node } else { RCLCPP_INFO(this->get_logger(), "Client: Goal accepted by server, waiting for result"); } } // 创建处理周期反馈消息的回调函数 void feedback_callback( GoalHandle::SharedPtr, const std::shared_ptr<const CustomAction::Feedback> feedback_message) { std::stringstream ss; ss << "Client: Received feedback: "<< feedback_message->state; RCLCPP_INFO(this->get_logger(), "%s", ss.str().c_str()); } // 创建一个收到最终结果的回调函数 void result_callback(const GoalHandle::WrappedResult & result_message) { switch (result_message.code) { case rclcpp_action::ResultCode::SUCCEEDED: break; case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(this->get_logger(), "Client: Goal was aborted"); rclcpp::shutdown(); // 关闭客户端节点 return; case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(this->get_logger(), "Client: Goal was canceled"); rclcpp::shutdown(); // 关闭客户端节点 return; default: RCLCPP_ERROR(this->get_logger(), "Client: Unknown result code"); rclcpp::shutdown(); // 关闭客户端节点 return; } RCLCPP_INFO(this->get_logger(), "Client: Result received: %s", (result_message.result->finish ? "true" : "false")); rclcpp::shutdown(); // 关闭客户端节点 } }; // ROS2节点主入口main函数 int main(int argc, char * argv[]) { // ROS2 C++接口初始化 rclcpp::init(argc, argv); // 创建一个客户端指针 auto action_client = std::make_shared<MoveCircleActionClient>(); // 发送动作目标 action_client->send_goal(true); // 创建ROS2节点对象并进行初始化 rclcpp::spin(action_client); // 关闭ROS2 C++接口 rclcpp::shutdown(); return 0; } 创建服务端节点程序action_server.cpp #include <iostream> #include "rclcpp/rclcpp.hpp" // ROS2 C++接口库 #include "rclcpp_action/rclcpp_action.hpp" // ROS2 动作类 #include "learning_interface/action/move_circle.hpp" // 自定义的圆周运动接口 using namespace std; class MoveCircleActionServer : public rclcpp::Node { public: // 定义一个自定义的动作接口类,便于后续使用 using CustomAction = learning_interface::action::MoveCircle; // 定义一个处理动作请求、取消、执行的服务器端 using GoalHandle = rclcpp_action::ServerGoalHandle<CustomAction>; explicit MoveCircleActionServer(const rclcpp::NodeOptions & action_server_options = rclcpp::NodeOptions()) : Node("action_move_server", action_server_options) // ROS2节点父类初始化 { using namespace std::placeholders; this->action_server_ = rclcpp_action::create_server<CustomAction>( // 创建动作服务器(接口类型、动作名、回调函数) this->get_node_base_interface(), this->get_node_clock_interface(), this->get_node_logging_interface(), this->get_node_waitables_interface(), "move_circle", std::bind(&MoveCircleActionServer::handle_goal, this, _1, _2), std::bind(&MoveCircleActionServer::handle_cancel, this, _1), std::bind(&MoveCircleActionServer::handle_accepted, this, _1)); } private: rclcpp_action::Server<CustomAction>::SharedPtr action_server_; // 动作服务器 // 响应动作目标的请求 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const CustomAction::Goal> goal_request) { RCLCPP_INFO(this->get_logger(), "Server: Received goal request: %d", goal_request->enable); (void)uuid; // 如请求为enable则接受运动请求,否则就拒绝 if (goal_request->enable) { return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; } else { return rclcpp_action::GoalResponse::REJECT; } } // 响应动作取消的请求 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptr<GoalHandle> goal_handle_canceled_) { RCLCPP_INFO(this->get_logger(), "Server: Received request to cancel action"); (void) goal_handle_canceled_; return rclcpp_action::CancelResponse::ACCEPT; } // 处理动作接受后具体执行的过程 void handle_accepted(const std::shared_ptr<GoalHandle> goal_handle_accepted_) { using namespace std::placeholders; // 在线程中执行动作过程 std::thread{std::bind(&MoveCircleActionServer::execute, this, _1), goal_handle_accepted_}.detach(); } void execute(const std::shared_ptr<GoalHandle> goal_handle_) { const auto requested_goal = goal_handle_->get_goal(); // 动作目标 auto feedback = std::make_shared<CustomAction::Feedback>(); // 动作反馈 auto result = std::make_shared<CustomAction::Result>(); // 动作结果 RCLCPP_INFO(this->get_logger(), "Server: Executing goal"); rclcpp::Rate loop_rate(1); // 动作执行的过程 for (int i = 0; (i < 361) && rclcpp::ok(); i=i+30) { // 检查是否取消动作 if (goal_handle_->is_canceling()) { result->finish = false; goal_handle_->canceled(result); RCLCPP_INFO(this->get_logger(), "Server: Goal canceled"); return; } // 更新反馈状态 feedback->state = i; // 发布反馈状态 goal_handle_->publish_feedback(feedback); RCLCPP_INFO(this->get_logger(), "Server: Publish feedback"); loop_rate.sleep(); } // 动作执行完成 if (rclcpp::ok()) { result->finish = true; goal_handle_->succeed(result); RCLCPP_INFO(this->get_logger(), "Server: Goal succeeded"); } } }; // ROS2节点主入口main函数 int main(int argc, char * argv[]) { // ROS2 C++接口初始化 rclcpp::init(argc, argv); // 创建ROS2节点对象并进行初始化 rclcpp::spin(std::make_shared<MoveCircleActionServer>()); // 关闭ROS2 C++接口 rclcpp::shutdown(); return 0; } 客户端同样也是注册了3个回调: 目标的回调:goal_response_callback 反馈的回调:feedback_callback 结果的回调:result_callback 运行代码 通信接口 在 ROS 2 中,通信接口(Communication Interface)是指节点之间用于交换信息的机制和定义。这些接口可以通过不同的通信方式(如发布/订阅、服务、动作等)实现,且每种方式都有其特定的通信协议和数据结构。 在ROS 2系统中提供了多种通信接口,每种接口都有不同的用途和行为。常见的通信接口类型包括: 话题(Topic):使用.msg文件格式定义消息类型。 服务(Service):使用.srv文件格式定义消息类型 动作(Action):使用.action文件格式定义消息类型。 对于通信接口有一些查明命令 查询通信接口:ros2 interface list 查询某个通信接口定义:ros2 interface show <接口名> 查询某个功能包中的通信接口定义:ros2 interface package <包名> 如下示例 laumy@ThinkBook-14-G7-IAH:~/dev_ws$ ros2 interface package learn_action_cpp laumy@ThinkBook-14-G7-IAH:~/dev_ws$ ros2 interface show learning_interface/srv/AddTwoInts int64 a # 第一个加数 int64 b # 第二个加数 --- int64 sum # 求和结果 下面举例创建通信接口的示例,为了方便我们直接在一个包里面创建3个不同类型的通信接口,分别对应.msg,.action,.srv。步骤如下: (1)创建一个包 ros2 pkg create my_interfaces --build-type ament_cmake (2)创建文件下存放.msg、.srv、.action文件 mkdir msg srv action (3)定义.msg文件 在 msg 文件夹中创建一个 .msg 文件。比如创建一个 MyMessage.msg 文件,定义消息格式: int32 id string name float64 value (4)定义.srv文件 在 srv 文件夹中创建一个 .srv 文件。比如创建一个 AddTwoInts.srv 文件,定义服务请求和响应格式: int64 a int64 b --- int64 sum (5)定义.action文件 在 action 文件夹中创建一个 .action 文件。比如创建一个 MoveTo.action 文件,定义动作的目标、结果和反馈格式: # Goal float64 x float64 y --- # Result bool success string message --- # Feedback float64 distance_remaining (6)修改CMakeList.txt 添加依赖:在 find_package 部分,添加对 ROS 2 生成接口工具的依赖 find_package(rosidl_default_generators REQUIRED) 添加接口文件的生成:使用 rosidl_generate_interfaces 命令生成消息、服务和动作的代码。 rosidl_generate_interfaces(${PROJECT_NAME} "msg/MyMessage.msg" "srv/AddTwoInts.srv" "action/MoveTo.action" ) 安装生成的接口文件: install(DIRECTORY msg srv action DESTINATION share/${PROJECT_NAME} ) (7)修改packge.xml文件 在 package.xml 中,添加对 rosidl_default_runtime 和 rclpy 等依赖的声明,确保能够生成并使用这些接口。 <depend>rosidl_default_runtime</depend> <depend>rclpy</depend> <depend>example_interfaces</depend> (8)编译 colcon build (9)使用定义的接口 话题通信 from my_interfaces.msg import MyMessage 服务通信 from my_interfaces.srv import AddTwoInts 动作通信 from my_interfaces.action import MoveTo 参数 在ROS2系统中,想要获取一些硬件设备如摄像头的分辨率等怎么表示了,这就用到了ROS系统中的参数。在 ROS 2 中,系统参数(System Parameters)用于存储和配置节点的运行时设置。通过参数,用户可以灵活地调整节点的行为,而无需修改源代码。ROS 2 的参数机制非常强大,支持动态更改参数值,并且能够在节点启动时通过命令行或 YAML 配置文件进行预设。 参数可以认为是多个节点可以互相访问的全局变量。 定义参数 (1)创建一个ROS2 包 参数的定义可以包含在其他功能的包中,也可以单独的定义一个包。 ros2 pkg create my_param_pkg --build-type ament_python cd my_param_pkg (2)声明参数 在 ROS 2 中,节点需要在初始化时声明参数。你可以为每个参数指定一个默认值和类型。 import rclpy from rclpy.node import Node class ParamExampleNode(Node): def __init__(self): super().__init__('param_example_node') # 声明参数,并为其设定默认值 self.declare_parameter('param_name', 'default_value') self.declare_parameter('param_int', 10) self.declare_parameter('param_bool', True) def main(args=None): rclpy.init(args=args) node = ParamExampleNode() rclpy.spin(node) rclpy.shutdown() if __name__ == '__main__': main() 使用 declare_parameter() 方法声明一个参数。 查询与设置 代码的方式查询与设置,调用get_parameter和set_parameters进行获取和设置参数 获取参数 param_value = self.get_parameter('param_name').get_parameter_value().string_value param_int_value = self.get_parameter('param_int').get_parameter_value().integer_value param_bool_value = self.get_parameter('param_bool').get_parameter_value().bool_value 设置参数 self.set_parameters([Parameter('param_name', value='new_value')]) 也可以通过YAML文件的方式设置 param_example_node: ros__parameters: param_name: "new_value" param_int: 20 param_bool: false 通过 YAML 文件来为节点提供参数配置,这对于启动时设置多个参数非常有用。使用 --params-file 参数启动节点并加载该配置文件: ros2 run my_param_pkg my_node --params-file params.yaml 还可以通过命令行的方式设置和查询。 列出节点参数 ros2 param list /param_example_node 获取参数的值 ros2 param get /param_example_node param_name 设置参数的值 ros2 param set /param_example_node param_name "new_value" 还可以通过注册参数回调来处理参数的动态更新。当参数值发生变化时,回调函数会被调用。可以通过 add_on_set_parameters_callback() 注册这个回调。 def parameter_callback(params): for param in params: self.get_logger().info(f"Parameter '{param.name}' has been updated to {param.value}") return rclpy.parameter.SetParametersResult(successful=True) self.add_on_set_parameters_callback(parameter_callback) DDS DDS(Data Distribution Service)是 ROS 2 的核心通信协议,话题、服务和动作都是基于底层的DDS实现。DDS 是一个中间件标准,它定义了数据交换、通信模式和可靠性保证的方式。ROS 2 采用 DDS 来实现节点之间的通信,提供了高效、灵活的实时数据传输机制。 DDS 提供了一些强大的特性,包括高可用性、灵活的 QoS(Quality of Service)配置、低延迟和高吞吐量。它是面向数据的,适合用于需要数据流的场景,如机器人、物联网、自动驾驶等领域。 ROS2系统中对DDS实现做了解耦,可以有不同的 DDS 实现(例如,Fast DDS、Cyclone DDS、RTI Connext DDS)提供了不同的功能和特性,但它们遵循相同的 DDS 标准。 DDS通信结构中,只有在一个domain域中才能通信,可以理解在一个网段中,所以了要能够使应用互相通信,需要将各个应用绑定到一个domain中。 DDS中还有另外一个特性就是服务特性QoS,可以设置通信的传输质量,优先级等。 在 ROS 2 中,可以通过 rclcpp 或 rclpy 中的接口来设置 QoS,下面简单举例。例如,设置话题的 QoS: from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy from std_msgs.msg import String qos_profile = QoSProfile( reliability=QoSReliabilityPolicy.RELIABLE, history=QoSHistoryPolicy.KEEP_LAST, depth=10 ) publisher = node.create_publisher(String, 'topic_name', qos_profile) 本文关于ROS2的通信记录到此,具体在根据实践中查询吧。 附录:本文来自《ROS2智能机器人开发实践》笔记
- 
					
						  ROS2实践:创建一个包和节点准备 在ROS2中什么是包,什么是节点? 包(Package) 是 ROS 2 的基本构建单元,包含了代码、资源、配置和依赖,组织着机器人的各个功能模块。这个packge可以理解为openwrt的package,主要用于怎么构建、编译代码,存放程序的配置等等。 节点(Node) 是 ROS 2 中执行的具体单元,它是一个程序,可以完成传感器读取、控制计算、数据处理等任务,并通过话题、服务或动作与其他节点进行通信。可以理解为openwrt中package中实际的程序,一个节点是一个进程。 一个包中可以有多个节点,节点与包是包含的关系。 安装前面的文章安装好ROS2的开发环境。 安装colcon编译工具、rosdep工具。 sudo apt install python3-colcon-ros 创建一个工作空间 mkdir -p ~/dev_ws/src python的方式 创建一个包 cd ~/dev_ws/src/ ros2 pkg create --build-type ament_cmake learning_pkg_python 创建完成后,会产生如下文件 . ├── learning_pkg_python │ ├── learning_pkg_python │ │ └── __init__.py │ ├── package.xml │ ├── resource │ │ └── learning_pkg_python │ ├── setup.cfg │ ├── setup.py │ └── test │ ├── test_copyright.py │ ├── test_flake8.py │ └── test_pep257.py 创建节点程序 在~/dev_ws/src/learning_pkg_python/learning_pkg_python目录下创建一个node_helloworld_class.py文件,实现代码如下: #! /usr/bin/env python3 # -*- coding: utf-8 -*- import rclpy from rclpy.node import Node import time class HelloworldNode(Node): def __init__(self, name): super().__init__(name) while rclpy.ok(): self.get_logger().info("Hello World") time.sleep(0.5) def main(args=None): rclpy.init(args=args) node = HelloworldNode("node_helloworld_class") rclpy.spin(node) node.destroy_node() rclpy.shutdown() 注意node_helloworld_class.py文件要和默认的init.py在一个目录下,保持同级。 设置程序入口 entry_points={ 'console_scripts': [ 'node_helloworld_class = learning_pkg_python.node_helloworld_class:main', ], }, 设置python的程序入口,让ROS2系统知道python程序的入口。 console_scripts::这告诉Python包管理器(如pip)要创建哪些命令行可执行脚本。 node_helloworld_class:这是终端中ros2 run后面直接运行的命令名称。 learning_pkg_python.node_helloworld_class:main:这是Python模块的路径(包名.模块名) 编译 cd ~/dev_ws/ colcon build 编译的时候,要切换到工程的根目录下进行编译。因为编译会在当前的根目录下生成build、install、log,而工程下会有很多包,因此建议都切换到工程的根目录下进行编译。 执行 ros2 run learning_pkg_python node_helloworld_class CPP方式 创建一个包 执行创建包的命令 ros2 pkg create --build-type ament_cmake learning_pkg_cpp 下面是生成的包的目录结构 . ├── learning_pkg_cpp │ ├── CMakeLists.txt │ ├── include │ │ └── learning_pkg_cpp │ ├── package.xml │ └── src 创建节点程序 在learning_pkg_cpp/src目录下创建一个node_helloworld_class.cpp文件,编写代码。 #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" class HelloWorldClass : public rclcpp::Node { public: HelloWorldClass():Node("hello_world_class") { while(rclcpp::ok()) { RCLCPP_INFO(this->get_logger(), "Hello World"); sleep(1); } } }; int main(int argc, char *argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<HelloWorldClass>()); rclcpp::shutdown(); return 0; } 设置编译选项 python是不用编译的,但是C++程序需要编译因此需要更新CmakeLists.txt。在文件中添加一下内容。 find_package(rclcpp REQUIRED) # 查找依赖的功能包rclcpp,提供ROS2 C++的基础接口 add_executable(node_helloworld_class src/node_helloworld_class.cpp) # 添加一个可执行文件名称为node_helloworld_class,源码路径为src/node_helloworld_class.cpp ament_target_dependencies(node_helloworld_class rclcpp) #编译时链接rclcpp的依赖库 install(TARGETS node_helloworld_class DESTINATION lib/${PROJECT_NAME} ) #将可执行文件拷贝到install目录下。 编译执行 # 编译 cd ~/dev_ws/ colcon build # 运行 source install/setup.bash #先执行一下环境变量,可以补全新的命令。 常用命令 创建包 创建一个功能包的命令 ros2 pkg create --build-type <build-type> <packge_name> pkg:调用功能包相关功能的子命令。 create:创建功能包的子命令。 build-type:表示新创建的功能包是C++还是Python,如果使用C++或者C,这里是"ament_cmake";如果使用python,这里就是"ament_python"。 packege_name:新建功能包的名字。 示例 cd ~/dev_ws/src ros2 pkg create --build-type ament_cmake learning_pkg_cpp ros2 pkg create --build-type ament_python learning_pkg_python 注意执行上面命令时,就会在当前目录下进行创建包,因此为了工程目录的结构,要切换到指定的目录进行创建。 运行 ros2 run <package_name> <executable_name> [arguments]
- 
					
						  ROS2安装:ubuntu 24.04.2上实践前言 本文在unbuntu 24.10系统上搭建ROS2系统。 No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.2 LTS Release: 24.04 Codename: noble 设置编码格式 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 添加ROS apt仓库 启用 universe 仓库 sudo apt install software-properties-common sudo add-apt-repository universe 添加 ROS 的 GPG key 和 repository sudo apt update && sudo apt install curl gnupg2 lsb-release sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 将仓库添加到列表中 echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null 安装ROS 2 下载安装ROS2桌面版软件。 sudo apt update sudo apt upgrade sudo apt install ros-jazzy-desktop 设置环境变量 执行上面的安装命令,没有报错就已经成功安装好ROS2了,默认在/opt路径下。因为要常用到ROS2相关的命令,因此可以添加一下环境变量。 echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc source ~/.bashrc 运行测试用例 在一个终端启动如下命令,表示一个数据的发布者节点发送消息。 ros2 run demo_nodes_cpp talker [INFO] [1757919638.619982839] [talker]: Publishing: 'Hello World: 1' [INFO] [1757919639.619919625] [talker]: Publishing: 'Hello World: 2' [INFO] [1757919640.619874774] [talker]: Publishing: 'Hello World: 3' [INFO] [1757919641.619899411] [talker]: Publishing: 'Hello World: 4' [INFO] [1757919642.619969273] [talker]: Publishing: 'Hello World: 5' [INFO] [1757919643.619894999] [talker]: Publishing: 'Hello World: 6' [INFO] [1757919644.619798092] [talker]: Publishing: 'Hello World: 7' [INFO] [1757919645.619868044] [talker]: Publishing: 'Hello World: 8' [INFO] [1757919646.619927174] [talker]: Publishing: 'Hello World: 9' 在另外一个终端执行一下命令,可以订阅此消息 ros2 run demo_nodes_py listener [INFO] [1757919722.633895350] [listener]: I heard: [Hello World: 85] [INFO] [1757919723.620902142] [listener]: I heard: [Hello World: 86] [INFO] [1757919724.621171224] [listener]: I heard: [Hello World: 87] [INFO] [1757919725.621231371] [listener]: I heard: [Hello World: 88] [INFO] [1757919726.620940203] [listener]: I heard: [Hello World: 89] 如果listener能够正确接收到消息,那么ROS2就已经在系统中安装好了。
- 
					
						  什么是ROS:机器人操作系统快览随记ROS的秘诀 梳理出有什么节点,每个节点的输入和输出是什么? 环境搭建 catkin vscode ROS插件、bracket pair colorizer 2 terminator sudo apt install terminator CRTL+ALT+T启动 CRTL+SHIFT+E左右分屏 CRTL+SHIFT+O上下分屏 CRTL+SHIFT+W关闭窗口 程序 基础单元node 节点的容器packege,以packege安装节点,packege里面有多个节点。节点不能脱离packege存在。 编写节点步骤 订阅与发布 多个节点直接的信息交互 跟MQTT机制类似。 消息类型,bool、byte、float.......可上官网查看https://index.ros.org 发布者 步骤: 1. 确定话题名称和消息类型 2. 包含消息类型的头文件。 3. 通过NodeHandler大管家发布一个话题获得消息发送对象。 4. 生成要发送的消息包并进行发送数据复制。 5. 调用消息发送对象publish函数发布消息。 常用工具: rostopic list 列出当前系统所有活跃的话题 rostopic echo <主题名称> 显示话题中发送的消息包内容 rostopic hz 查看话题发布频率 订阅者 步骤: 1. 确定话题名称和消息类型。 2. 包含ros.h和消息类型对应头文件。 3. 通过NodeHandler订阅一个话题并设置一个消息接收回调。 4. 定义一个回调函数。 5. main函数中执行ros::spinOnce(),在while循环中可以响应接收消息。 常用工具:rqt_graph 查看ROS中节点,通讯关系。 launch 可以支持一下启动多个节点,使用xml来描述。 使用launch文件,可以通过roslaunch指令一次启动多个节点。 在lauch文件中,为节点添加output="screen"属性,可以让节点信息输出终端。 在launch文件中,为节点添加launch-prefix="gnome-terminal -e"属性,可以让节点单独运行在一个独立终端中。 可以通过launch文件来分析工程代码。 python方式 使用python的方式,只需要开始的时候编译一次,同步一下环境就行,后续就不用编译了,因为python是脚本,不需要编译。 机器的运动控制 线性方向:X,Y,Z坐标值。 角度控制:X,Y,Z方向旋转角度。 使用wbr_simulation,要控制机器人就发送消息就行了,消息类型就是线性方向+角度 要控制对主题/cmd_vel发布指令即可。 RViz 激光雷达原理,发射红外,接收红外,通过光速与时间计算障碍物距离,360°旋转。 Rviz是观测传感器数据,显示到仿真界面中。 激光雷达 激光雷达的数据包格式 float32[] ranges #一个数组,每个角度对应的测试距离。共360 float32[] ranges # 每个角度的信号强度,强度越高对上上面的测试距离越可信 如何获取到雷达测距数据? 使用wbr_simulation,订阅/scan话题即可获取到雷达数据。激光雷达每扫描一圈,就会调用一次注册订阅的话题回调函数。 如何实现运动避障的效果? 订阅/scan获取雷达测试数据,对/cmd_vel主题进行发布控制。当发现测距比较近的时候,就发布控制指令进行调整。 IMU 用于测量空间姿态,也就是陀螺仪。 消息包格式 imu/data_raw( sensor_msgs/Imu):加速度输出的矢量加速度、陀螺仪输出的旋转角速度 imu/data(sensor_msgs/imu):对矢量加速度和旋转角速度融合后的4元素姿态描述。最后可以通过这4元素计算出欧拉角度。 imu/mag(sensor_msgs/MagneticField):磁强计输出数据。 ros中怎么获取数据?订阅/imu/data主题得到4元素,然后通过FT计算欧拉角,得到相对X,Y,Z的旋转角度。 航向锁定的实验 获取到朝向角后,然后通过/cmd_vel发布主题进行控制。 ROS消息包 标准消息包:std_msgs,包含数值类型、数组类型、结构体类型。 常规消息包:common_msgs,包括(sensor_msgs)传感器消息包、(geometry_msgs)几何消息包、(nav_msgs)导航消息包等等。 自定义消息包:根据基本的消息包类型来构建消息。需要按照ros规则来创建一个.msg的文件, rosmsg show xxx显示。 栅格地图:map_server 获取/map主题,获取栅格坐标格式为nav_msgs/OccupancyGird,可以和RVIZ进行联动显示。 SLAM simultaneous localization and mapping 同时定位与地图构建 如何建图原理? 先确定一个参考位置,然后进行移动,每移动一个位置获得一个图,这个图可以是用视觉识别的物体、雷达探测障碍物等方式,然后通过每个位置获得的图进行拼接合并,以此获取到全局的地图。 在ros系统中如何获取地图 激光雷电 发布/scan-> Slam节点获取处理 发布/map->RViz显示 hector_mapping开源的slam算法 TF TransForm描述两个坐标系的空间关系,坐标变化关系。简单理解就是机器人相对参考坐标的位置关系 由TF发布节点,通过/tf主题来发布。 结合里程计的 gmaping建图算法。 导航 详细的官方图 规划器 先使用目标点生成一个导航路线,然后按照导航路线走,在过程中遇到障碍物则进行避障。在move_base中提供了不同风格的规划器拥有规划路线如Dijkstra算法、A*算法。 定位算法 接着需要知道机器人的具体位置,知道具体的位置才能跟着导航路线走,这就需要定位节点如AMCL算法。 代价地图 代价地图,因为导航路线规划有时候的最短路径没有考虑机器人的尺寸,可能会导致机器人沿着墙边走而导致卡住,代价地图就是把障碍物线设置一个虚拟的安全距离(也可以说是把障碍物膨胀变大些)。全局代价地图和全局代价地图。 恢复行为 机器在运动过程中遇到了障碍物导致不能行动,会进入应急机制重新进行规划路线。 局部规划器 DWA、TEB、WpbhLocalPlanner等等。 ACTION action是节点通信的另外一种方式,与订阅/发布不同而是可以双向传输,传输的双方分别是client和Server。client向server发送信息后,server可以持续不断的通知返回信息。 如:可以通过action接口来调用move_base设置导航的功能。client发送导航的目的地,然后server按照目的地运动,client阻塞等待server的回复,当server到达目的时返回结果。 航点的目的地,不用口算、目测;具体的导航目的地可以用插件获取,如waterplus_map_tools。可以将导航插件写到launch。 ROS相机 /image_raw主题:相机原始数据 /image_color:相机的彩色图像 /image_color_rect:畸变校正后的彩色图像。 /camera_info:相机相关的参数。 获取到图像后可以调用opencv处理图像,如果要做目标跟随,可以找出目标的坐标,然后跟随运动。