最新文章
-
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、rqt
launch 在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智能机器人开发实践》笔记 -
ubuntu系统异常:无法进入登录桌面
概述 最近发现ubuntu系统24.04.2的版本用着用着休眠熄屏后,突然登录不了,然后重启也进入不了桌面,显示如下: 按Ctrl + Alt + F3也进入不了tty控制台。 解决办法 重启系统进入Advanced options for Ubuntu(recovery mode)模式,进去后选择进去后选 root shell。然后强制进入命令行启动。 systemctl set-default multi-user.target reboot 重启后重装 GNOME 相关包。 sudo apt install --reinstall ubuntu-desktop gdm3 gnome-shell 然后 sudo systemctl restart gdm3 如果能够进入桌面那就说明成功了,此前的问题是GNOME 桌面核心包不完整/损坏,重装后依赖被补齐所以恢复了。 因为刚才设置了每次启动都进入命令行,所以恢复默认启动进入桌面。 sudo systemctl set-default graphical.target sudo reboot 常见问题 (1)发现外接的显示器,鼠标显示老是跳动? 把内置显示器和外接的显示器缩放都改到一样200%,看起来有效果。 -
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就已经在系统中安装好了。 -
ubuntu apt 命令
介绍 apt是ubuntu/debian的包管理工具,主要用于安装、更新、卸载和管理系统软件,他是对apt-get和apt-cache的简化整合,输出更直观。 常用的apt命令 sudo apt update:从 /etc/apt/sources.list 和 /etc/apt/sources.list.d/ 指定的镜像源获取最新软件包列表。只更新索引信息,不会更新软件。 sudo apt upgrade:升级所有已安装软件到新版本,但不会移除/替换已有包。 sudo apt full-upgrade:更彻底,会移除或替换包以完成升级(系统大版本升级时常用) sudo apt install 包名:安装软件包 sudo apt remove 包名:卸载软件包但保留配置文件。 sudo apt purge 包名:卸载软件并删除配置文件。 apt search 关键字:搜索关键字的软件包。 apt show 包名:显示软件包的版本、依赖、来源等详细信息。 sudo apt autoremove:自动卸载系统中不再需要的依赖包。 sudo apt clean:清空 /var/cache/apt/archives/ 下的缓存 .deb 文件。 sudo apt autoclean:只删除无效的缓存文件(已经在源里不存在的旧版本)。 apt安装执行流程 以安装vim为例 sudo apt install vim (1)解析请求 apt 会先确认你输入的命令(如 install),以及指定的软件包名称。它会调用 libapt-pkg 库 来处理包依赖关系和软件源信息。 (2)查询软件源信息 apt读取本地缓存文件,/var/lib/apt/lists/ 下的索引文件(来自 /etc/apt/sources.list 和 /etc/apt/sources.list.d/)。如果缓存过期或你执行了 apt update,apt 会先通过 HTTP/HTTPS 向镜像源服务器拉取最新的软件包索引。 (3)依赖解析 apt 会分析你要安装的软件包需要哪些依赖(Dependencies)。依赖可能有必须依赖(必装),推荐依赖(默认安装),建议依赖(不自动安装,只提示)。apt通过SAT求解器选择最佳依赖组合。 (4)下载软件包 apt 确定要安装的 .deb 包后,从配置的镜像源下载对应的文件。下载位置:/var/cache/apt/archives/。apt 内部通过 Acquire 系统(支持 HTTP、HTTPS、FTP 等协议)拉取数据,并验证 GPG 签名 确保包没有被篡改。 (5)安装与配置 apt 把下载好的 .deb 包调用dpkg(Debian package manager) 处理。 dpkg -i xxx.deb dpkg 负责解压 .deb,把文件放到系统目录里(如 /usr/bin、/etc/ 等),并运行包的安装脚本(preinst、postinst、prerm、postrm)。apt 会在这个过程中自动处理依赖包的安装顺序。 (6)触发系统更新 完成安装后,apt 可能会调用:ldconfig 更新共享库缓存、update-initramfs 更新内核启动镜像、systemctl daemon-reload 重载 systemd 服务。 软件源 软件源是apt update/install时定义系统如何找到、解析和使用软件仓库的。 旧版 软件源的配置在如下地址。 /etc/apt/sources.list /etc/apt/sources.list.d/*.list 一条软件源的格式大致如下: deb http://mirrors.aliyun.com/ubuntu jammy main restricted universe multiverse deb:二进制包仓库(还有 deb-src 表示源码包)。 http://mirrors.aliyun.com/ubuntu:镜像源地址。 jammy:发行版代号(Ubuntu 22.04 LTS = jammy)。 main restricted universe multiverse:启用的组件。 新版 从 Ubuntu 22.04 LTS(部分更新后)和 23.04 起,apt 引入了一个新的配置格式,新格式(.sources YAML 格式): Types: deb URIs: http://archive.ubuntu.com/ubuntu Suites: jammy Components: main restricted universe multiverse 新格式的文件存放路径在 /etc/apt/sources.list.d/ubuntu.sources 内容如下: Types: deb URIs: http://cn.archive.ubuntu.com/ubuntu/ Suites: oracular oracular-updates oracular-backports Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg Types: deb URIs: http://security.ubuntu.com/ubuntu/ Suites: oracular-security Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 第一段为主源+更新+回溯、第二段为安全更新源。 add-apt-repository add-apt-repository是用于添加/删除 APT 软件源(包括 PPA 和官方仓库组件)。这个命令由 software-properties-common 包提供。其目的简化对软件包源 /etc/apt/sources.list 和 /etc/apt/sources.list.d/x.list 文件的修改。 要使用add-apt-repository命令,需要安装software-properties-common工具,该工具包含了add-apt-repository命令。 sudo apt install software-properties-common add-apt-repository的语法为: sudo add-apt-repository [选项] <repository> 常见 repository类型: - PPA(个人/团队维护):ppa:用户名/仓库名。 - 官方仓库组件:universe、multiverse、restricted - 完整仓库地址:deb http://url distro component PPA PPA (Personal Package Archive)是个人/团队维护的软件,作用是提供官方仓库里没有的软件,或者比官方更快更新的版本。命名规则:ppa:用户名/仓库名。如下: sudo add-apt-repository ppa:deadsnakes/ppa deadsnakes → 维护者的用户名 ppa → 这个维护者创建的一个仓库,里面放了多个 Python 新版本 功能:能让你在 Ubuntu 上安装 Python 3.12、3.13 等官方仓库还没有的版本。 PPA 就像是别人开的小摊位,卖的是他们自己编译打包的软件。你可以信任并从那买,而不必等官方超市上架。 使用步骤: 添加PPA:sudo add-apt-repository ppa:<用户名>/<仓库名>;sudo apt update 安装软件:sudo apt install <软件包名> 删除PPA:sudo add-apt-repository -r ppa:<用户名>/<仓库名>;sudo apt update 示例 1:安装新版 Python,Ubuntu 自带的 Python 可能偏旧,如果想安装较新的 Python(比如 Python 3.12) sudo add-apt-repository ppa:deadsnakes/ppa sudo apt update sudo apt install python3.12 示例 2:安装新版 FFmpeg,官方仓库里的 FFmpeg 版本可能很老,如果你需要更新: sudo add-apt-repository ppa:savoury1/ffmpeg4 sudo apt update sudo apt install ffmpeg 示例 3:移除 PPA 安装的软件并恢复官方版本,假如你安装了 PPA 版本的 VLC,后来想回退到 Ubuntu 官方版本: sudo add-apt-repository -r ppa:videolan/stable-daily sudo apt update sudo apt install ppa-purge sudo ppa-purge ppa:videolan/stable-daily 官方仓库 组件 维护者 软件许可 举例 默认启用? main Ubuntu 官方团队 100% 开源、免费 bash、apt、systemd ✅ 默认启用 universe 社区志愿者维护 开源,但官方不保证支持 vim、ffmpeg ❌ 默认关 restricted 官方维护,但包含闭源驱动 通常是硬件驱动 NVIDIA/AMD 驱动 ❌ 默认关 multiverse 可能受限(版权/专利) 不完全自由,可能有法律限制 MP3 解码器、某些编解码器 ❌ 默认关 sudo add-apt-repository universe sudo add-apt-repository restricted sudo add-apt-repository multiverse sudo apt update 这几条命令分别启用: universe → 更多社区软件(开源,但官方不负责) restricted → 闭源驱动(主要是显卡、WiFi 驱动) multiverse → 可能有版权问题的软件(比如 DVD 解码器、某些编解码器) 总结 apt update → 更新索引 apt upgrade/full-upgrade → 升级系统 apt install/remove/purge → 安装和卸载软件 apt search/show → 查询软件 apt autoremove/clean/autoclean → 清理 -
Isaac Sim 快速入门:三种工作流程示例
简介 如果是NVIDIA Isaac Sim的新用户,可以按照本文的两个示例来体验Isaac Sim。本文主要提供Isaac Sim基础使用教程、机器人基础教程。 在快速入门教程中,所有可通过 GUI 执行的操作同样也能用 Python 实现。您可以在 GUI 操作与 Python 脚本之间自由切换。您在 GUI 中创建的所有内容都能保存为 USD 文件的一部分。 例如,您可以通过图形界面创建世界,并为机器人添加所需动作。随后将整个 USD 文件导入独立的 Python 脚本中,根据需求系统性地修改属性。 基础使用教程 本教程涵盖 Isaac Sim 的基础操作,包括界面导航、场景对象添加、查看对象基本属性以及运行模拟等内容。 通过本教程,您将从空白场景开始,根据三种不同工作流程的选择,最终实现机器人运动控制。提供三种不同工作流程的目的是展示 Isaac Sim 可根据需求以多种方式灵活使用。 可以查看两种工作流中的脚本以了解它们的差异。通过对比分析,有助于掌握如何执行完全相同的任务: 扩展脚本可在Window > Examples > Robotics Examples中找到,然后单击浏览器右上角的**Open Script **按钮。 独立脚本位于 \<isaac-sim-root-dir>/standalone_examples/tutorials/ 文件夹内。 可以通过编辑扩展示例中的任意脚本来体验"hot-reloading"功能。保存文件后,无需关闭模拟器即可立即看到变更生效。 在官方教程中有3个标签页,三个标签页执行相同操作并达成相同结果。 GUI: 图形用户界面 Extensions:扩展功能 Standalone Python:独立Python环境 GUI方式 步骤1:启动Isaac Sim linux:cd ~/isaacsim && ./isaac-sim.selector.sh windows:双击isaac-sim.selector.bat 模拟器完全加载后,创建新场景: 从顶部菜单栏点击File > New。首次启动 Isaac Sim 时,可能需要 5-10 分钟完成初始化。 步骤2: 添加地平面 为场景添加地平面:从顶部菜单栏点击Create > Physics > Ground Plane。 步骤3:添加光源 可以为场景添加光源以照亮其中的物体。如果场景中有光源但没有物体反射光线,场景仍会显得昏暗。 在顶部菜单栏中,点击Create > Lights > Distant Light。 步骤4:添加视觉立方体 "视觉"立方体是指没有附加物理属性的立方体,例如没有质量、没有碰撞体积。这种立方体不会受重力影响下落,也不会与其他物体发生碰撞。 从顶部菜单栏中,依次点击Create > Shape > Cube. 在用户界面最左侧找到箭头图标并点击Play。运行模拟时立方体不会有任何动作。 步骤5:移动、旋转与缩放立方体 使用左侧工具栏上的各种操控工具来操作立方体。 按下"W"键或点击移动工具即可拖拽移动立方体。通过点击箭头并拖拽可单轴移动,点击彩色方块并拖拽可双轴移动,点击工具中心的圆点并拖拽则可三轴自由移动。 按下“E”键或点击旋转控制器来旋转立方体。 按下“R”键或点击缩放控制器来调整立方体大小。点击箭头并拖动可单维度缩放,点击彩色方块并拖动可双维度缩放,点击控制器中心的圆圈并拖动则可实现三维同步缩放。 按下“esc”键取消选中立方体。 对于“移动”和“旋转”操作,可选择基于局部坐标系或世界坐标系进行操作。长按控制器即可查看选项。 可以通过立方体的Property属性面板进行更精确的修改,只需在对应输入框中输入具体数值即可。点击输入框旁的蓝色方块可将数值重置为默认值。 步骤6:添加物理与碰撞属性 常见的物理属性包括质量和惯性矩阵,这些属性使物体能够在重力作用下下落。碰撞属性则决定了物体能否与其他物体发生碰撞。 物理和碰撞属性可以分别添加,因此你可以创建一个能与其他物体碰撞但不受重力影响的物体,或是受重力影响但不会与其他物体碰撞的物体。但在多数情况下,这两个属性会同时添加。 为立方体添加物理和碰撞属性: 在场景树中找到对象("/World/Cube")并高亮显示它。 从工作区右下角的Property属性面板中,点击"Add"按钮并在下拉菜单中选择Physics。这将显示可添加到对象的一系列属性选项。 选择Rigid Body with Colliders Preset“带碰撞器的刚体预设”可为对象同时添加物理和碰撞网格。 按下播放Play按钮,观察立方体在重力作用下坠落并与地平面发生碰撞。 教程结束,记得保存你的工作。 扩展功能方式 通过一个名为"脚本编辑器"的现有扩展模块来演示扩展工作流的特性。脚本编辑器允许用户通过 Python 与场景进行交互。主要使用与独立 Python 工作流相同的 Python API。当我们开始与模拟时间轴交互时,特别是下一个教程中,这两种工作流的区别将变得清晰。 步骤1:启动 启动一个新的 Isaac Sim 实例,转到顶部菜单栏并点击Window > Script Editor。 步骤2:添加地平面 要通过交互式 Python 添加地平面,请将以下代码片段复制粘贴到脚本编辑器中,然后点击底部的运行Run按钮执行。 from isaacsim.core.api.objects.ground_plane import GroundPlane GroundPlane(prim_path="/World/GroundPlane", z_position=0) 步骤3:添加光源 可以为场景添加光源以照亮其中的物体。如果场景中有光源但没有物体反射光线,场景仍会显得昏暗。 在脚本编辑器中新建一个标签页(Tab > Add Tab)。 在脚本编辑器中复制粘贴以下代码片段并运行,即可添加光源。 import omni.usd from pxr import Sdf, UsdLux stage = omni.usd.get_context().get_stage() distantLight = UsdLux.DistantLight.Define(stage, Sdf.Path("/DistantLight")) distantLight.CreateIntensityAttr(300) 步骤4:添加视觉立方体 在脚本编辑器中新建一个标签页 (Tab > Add Tab)。 在脚本编辑器中复制粘贴以下代码片段并运行,即可添加两个立方体。我们将保留其中一个作为纯视觉对象,同时为另一个添加物理和碰撞属性以便对比。 import numpy as np from isaacsim.core.api.objects import VisualCuboid VisualCuboid( prim_path="/visual_cube", name="visual_cube", position=np.array([0, 0.5, 0.5]), size=0.3, color=np.array([255, 255, 0]), ) VisualCuboid( prim_path="/test_cube", name="test_cube", position=np.array([0, -0.5, 0.5]), size=0.3, color=np.array([0, 255, 255]), ) Isaac Sim 核心 API 是对原生 USD 和物理引擎 API 的封装。您可以使用原生 USD API 添加一个视觉立方体(不含物理和颜色属性)。请注意原生 USD API 更为冗长,但能提供对每个属性的更精细控制。 from pxr import UsdPhysics, PhysxSchema, Gf, PhysicsSchemaTools, UsdGeom import omni # USD api for getting the stage stage = omni.usd.get_context().get_stage() # Adding a Cube path = "/visual_cube_usd" cubeGeom = UsdGeom.Cube.Define(stage, path) cubePrim = stage.GetPrimAtPath(path) size = 0.5 offset = Gf.Vec3f(1.5,-0.2,1.0) cubeGeom.CreateSizeAttr(size) if not cubePrim.HasAttribute("xformOp:translate"): UsdGeom.Xformable(cubePrim).AddTranslateOp().Set(offset) else: cubePrim.GetAttribute("xformOp:translate").Set(offset) 步骤5:添加物理与碰撞属性 在 Isaac Sim 核心 API 中,我们为常用对象编写了封装器,这些封装器附带所有物理和碰撞属性。您可以通过以下代码片段添加一个具有物理和碰撞属性的立方体。 import numpy as np from isaacsim.core.api.objects import DynamicCuboid DynamicCuboid( prim_path="/dynamic_cube", name="dynamic_cube", position=np.array([0, -1.0, 1.0]), scale=np.array([0.6, 0.5, 0.2]), size=1.0, color=np.array([255, 0, 0]), ) 另外,如果想修改现有对象使其具备物理和碰撞属性,可以使用以下代码片段。 from isaacsim.core.prims import RigidPrim RigidPrim("/test_cube") from isaacsim.core.prims import GeometryPrim prim = GeometryPrim("/test_cube") prim.apply_collision_apis() 点击播放Play按钮,观察立方体在重力作用下坠落并与地平面碰撞。 步骤6: 移动、旋转与缩放立方体 使用核心 API 移动物体: import numpy as np from isaacsim.core.prims import XFormPrim translate_offset = np.array([[1.5,1.2,1.0]]) orientation_offset = np.array([[0.7,0.7,0,1]]) # note this is in radians scale = np.array([[1,1.5,0.2]]) stage = omni.usd.get_context().get_stage() cube_in_coreapi = XFormPrim(prim_paths_expr="/test_cube") cube_in_coreapi.set_world_poses(translate_offset, orientation_offset) cube_in_coreapi.set_local_scales(scale) 使用原始 USD API 移动物体: from pxr import UsdGeom, Gf import omni.usd stage = omni.usd.get_context().get_stage() cube_prim = stage.GetPrimAtPath("/visual_cube_usd") translate_offset = Gf.Vec3f(1.5,-0.2,1.0) rotate_offset = Gf.Vec3f(90,-90,180) # note this is in degrees scale = Gf.Vec3f(1,1.5,0.2) # translation if not cube_prim.HasAttribute("xformOp:translate"): UsdGeom.Xformable(cube_prim).AddTranslateOp().Set(translate_offset) else: cube_prim.GetAttribute("xformOp:translate").Set(translate_offset) # rotation if not cube_prim.HasAttribute("xformOp:rotateXYZ"): # there are also "xformOp:orient" for quaternion rotation, as well as "xformOp:rotateX", "xformOp:rotateY", "xformOp:rotateZ" for individual axis rotation UsdGeom.Xformable(cube_prim).AddRotateXYZOp().Set(rotate_offset) else: cube_prim.GetAttribute("xformOp:rotateXYZ").Set(rotate_offset) # scale if not cube_prim.HasAttribute("xformOp:scale"): UsdGeom.Xformable(cube_prim).AddScaleOp().Set(scale) else: cube_prim.GetAttribute("xformOp:scale").Set(scale) 独立python环境方式 脚本位于standalone_examples/tutorials/getting_started.py,要运行该脚本,请打开终端,导航至 Isaac Sim 安装根目录,并执行以下命令: ./python.sh standalone_examples/tutorials/getting_started.py 机器人基础教程 本小节介绍如何将机器人添加到场景中、移动机器人以及检查机器人状态。在开始教程前,请确保已经完成了上一章节isaac sim基础使用教程。 GUI方式 步骤1:向场景中添加机器人 新建场景:通过File > New Stage.。 添加机器人:向场景添加机器人,从顶部菜单栏点击Create > Robots > Franka Emika Panda Arm。 步骤2:检查机器人 使用物理检查器查看机器人关节属性。 前往Tools > Physics > Physics Inspector.。右侧将打开一个窗口。 选择 Franka 进行检查。窗口默认会显示关节信息,例如上下限位及默认位置。 点击右上角的三横线图标可查看更多选项,例如关节刚度和阻尼系数。 可选)修改这些数值,观察舞台上机器人随参数变化的运动。修改成功后会出现绿色对勾标记。 点击绿色对勾按钮,将当前参数设为机器人新的默认值。 步骤3:控制机器人 基于图形界面的机器人控制器位于 Omniverse 可视化编程工具 OmniGraphs 中。OmniGraph 相关章节提供了更深入的教程指导。本教程将通过快捷工具生成控制图,然后在 OmniGraph 编辑器中查看该控制图。 通过菜单栏选择 Tools > Robotics > Omnigraph Controllers > Joint Position.来打开控制图生成器。。 在新弹出的关节位置控制器**Articulation Position Controller Inputs **输入窗口中,点击 Robot Prim 字段旁的添加"Add"按钮。 选择 Franka 作为目标对象。 点击确定生成图表。 要移动机器人的话按照下面步骤 在右上角的“舞台”选项卡中,选择Graph > Position_Controller.。 选择 JointCommandArray 节点。您可以通过在舞台树中选择该节点,或在图表编辑器中选择该节点来完成此操作。 在右下角的Property选项卡中,可以看到关节命令值。构造数组节点Construct Array Node下的输入Inputs项对应机器人上的关节,从基座关节开始。 点击+按住+拖动不同的数值字段,或输入不同的值,可以看到机械臂位置发生变化。 点击Play开始模拟。 要生成可视化的图标 打开图表编辑器窗口:**Window > Graph Editors > Action Graph. **。该编辑器窗口会在包含机器人的视口选项卡下方以新选项卡形式打开。 调出新打开的浏览器标签页。 点击位于图形编辑器窗口中央的Edit Action Graph选项。 从列表中选择唯一存在的图形。 选择一个数组并查看Stage和Property选项卡,以了解每个数组节点关联的值。 在图形中选择"关节控制器"Articulation Controller对象以查看其属性。 Extension方式 步骤1:向场景添加机器人 新建一个场景(File > New)。要将机器人添加到场景中,请将以下代码片段复制粘贴到脚本编辑器中并运行。 import carb from isaacsim.core.prims import Articulation from isaacsim.core.utils.stage import add_reference_to_stage from isaacsim.storage.native import get_assets_root_path import numpy as np assets_root_path = get_assets_root_path() if assets_root_path is None: carb.log_error("Could not find Isaac Sim assets folder") usd_path = assets_root_path + "/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd" prim_path = "/World/Arm" add_reference_to_stage(usd_path=usd_path, prim_path=prim_path) arm_handle = Articulation(prim_paths_expr=prim_path, name="Arm") arm_handle.set_world_poses(positions=np.array([[0, -1, 0]])) 步骤2:检查机器人 Isaac Sim 核心 API 提供了许多函数调用来获取机器人相关信息。以下是查询关节数量与名称、各类关节属性以及关节状态的示例代码。 在脚本编辑器中新建标签页,复制粘贴以下代码片段。该操作需在上一步添加机器人完成后执行(此时 arm_handle 已创建)。运行代码前需先点击播放Play按钮,这些命令需在物理引擎运行状态下才能生效。 # Get the number of joints num_joints = arm_handle.num_joints print("Number of joints: ", num_joints) # Get joint names joint_names = arm_handle.joint_names print("Joint names: ", joint_names) # Get joint limits joint_limits = arm_handle.get_dof_limits() print("Joint limits: ", joint_limits) # Get joint positions joint_positions = arm_handle.get_joint_positions() print("Joint positions: ", joint_positions) 请注意,点击"运行"时仅会打印一次状态信息,即使模拟正在持续运行。如需查看最新状态,需反复点击"运行"按钮。若希望在每个物理步长都打印信息,需要将这些命令插入到每个物理步长都会执行的回调函数中。在章节"工作流程"中详细讲解时间步进机制。 要将命令插入物理回调中,请在脚本编辑器的单独标签页中运行以下代码片段。 import asyncio from isaacsim.core.api.simulation_context import SimulationContext async def test(): def print_state(dt): joint_positions = arm_handle.get_joint_positions() print("Joint positions: ", joint_positions) simulation_context = SimulationContext() await simulation_context.initialize_simulation_context_async() await simulation_context.reset_async() simulation_context.add_physics_callback("printing_state", print_state) asyncio.ensure_future(test()) 按下播放键启动模拟,然后运行该代码片段。您将看到终端在每个物理步骤打印出的信息。 如果不再需要每个物理步骤都打印信息,可以通过运行以下代码片段来移除物理回调。 simulation_context = SimulationContext() simulation_context.remove_physics_callback("printing_state") 步骤3:控制机器人 在 Isaac Sim 中有多种控制机器人的方式。最底层是直接发送关节指令来设置位置、速度和力矩。以下是通过关节层级的 Articulation API 控制机器人的示例。 在脚本编辑器中新建一个标签页,复制粘贴以下代码片段。该代码需在前述添加机器人步骤完成后运行(此时 arm_handle 已建立)。运行代码片段前请先点击播放键Play。这些指令需在物理引擎运行状态下生效。我们将提供两个位置供您切换。若您已将上述打印状态代码片段添加至每个物理步骤,当机器人移动时您应能看到打印的关节编号发生变化。 # Set joint position randomly arm_handle.set_joint_positions([[-1.5, 0.0, 0.0, -1.5, 0.0, 1.5, 0.5, 0.04, 0.04]]) # Set all joints to 0 arm_handle.set_joint_positions([[0.0, 0.0, 0.0 , 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) 与前述 get_joint_positions 功能类似,此处的 set_joint_positions 仅在您点击"运行"时执行一次。若您希望在每个物理步骤发送指令,则需将这些命令插入到每个物理步骤都会执行的物理回调函数中。 独立Python方式 脚本位于 standalone_examples/tutorials/getting_started_robot.py 。要运行该脚本,请打开终端,导航至 Isaac Sim 安装的根目录,并执行以下命令: ./python.sh standalone_examples/tutorials/getting_started_robot.py 到此就完成了基本的操作了,接下来就是一系列的教程可以参考:《机器人设置教程系列》。