rqt 全家桶:节点图、话题监控与实时调参

《rqt 全家桶:节点图、话题监控与实时调参》

rqt 是 ROS 2 中最实用的一组图形化调试工具。它不能代替命令行,但能把分散在节点、话题和参数服务中的运行状态集中展示出来:rqt_graph 用于确认“谁在和谁通信”,rqt_topic 用于查看“正在传什么”,rqt_plot 用于观察“数值怎样随时间变化”,rqt_reconfigure 用于在节点运行期间修改参数。

本文先用 turtlesim 完成零代码的最小实验,再从零创建一个可运行的温控模拟包。最终会得到两个 ROS 2 节点和三个业务话题,并能在不重启节点的情况下修改目标温度、振幅、加热增益和启停状态,立即从消息与曲线中看到结果。

1. 版本、系统与前置条件

1.1 本文采用的环境

本文以以下组合为主线:

项目 本文环境
操作系统 Ubuntu 24.04 LTS
ROS 版本 ROS 2 Jazzy Jalisco
语言 Python 3、rclpy
图形环境 X11 或 Wayland 桌面

如果使用其他 ROS 2 发行版,需要把命令中的 jazzy 替换为对应发行版名称。ROS 1 中的 dynamic_reconfigure 与 ROS 2 参数机制不同,本文只讲 ROS 2。

1.2 安装所需软件

sudo apt update
sudo apt install -y \
  ros-jazzy-desktop \
  ros-jazzy-rqt \
  ros-jazzy-rqt-common-plugins \
  python3-colcon-common-extensions \
  python3-rosdep
source /opt/ros/jazzy/setup.bash

检查环境与四个核心插件:

printenv ROS_DISTRO
ros2 pkg executables rqt_graph
ros2 pkg executables rqt_topic
ros2 pkg executables rqt_plot
ros2 pkg executables rqt_reconfigure

新终端都要加载 ROS 环境;构建工作空间后还要加载工作空间环境:

source /opt/ros/jazzy/setup.bash
source ~/rqt_lab_ws/install/setup.bash

若设置了 ROS_DOMAIN_ID,所有终端必须一致。

2. rqt 观察的基本模型

ROS 2 系统可抽象为四层:

层次 主要问题 工具
计算图 有哪些节点,谁连着谁 rqt_graph
消息流 话题类型、频率、带宽、字段值 rqt_topic、ros2 topic
运行配置 节点有哪些参数,能否运行时修改 rqt_reconfigure、ros2 param
时间序列 数值是否震荡、漂移、饱和 rqt_plot

rqt_graph 显示的是 ROS 计算图中的端点关系,出现连线不代表数据一定持续到达。rqt_topic 会主动订阅话题并反序列化消息,因此能显示当前值和接收速率,但会增加负载。rqt_plot 只适合绘制数值叶子字段,例如 Float64.data 或 Temperature.temperature。rqt_reconfigure 通过参数服务读取并修改节点参数,不会把值写回 YAML 文件。

一个实用顺序是:先看图,再看话题类型与频率,然后画曲线,最后调参数并观察响应。

3. 启动 rqt 与识别插件

独立启动

ros2 run rqt_graph rqt_graph
ros2 run rqt_topic rqt_topic
ros2 run rqt_plot rqt_plot
ros2 run rqt_reconfigure rqt_reconfigure

也可以直接启动统一外壳:

rqt

然后从 Plugins 菜单加载图、话题、绘图和参数插件。

插件没有出现时

先确认包可见:

ros2 pkg prefix rqt_graph
ros2 pkg prefix rqt_topic
ros2 pkg prefix rqt_plot
ros2 pkg prefix rqt_reconfigure

如果菜单里没有,尝试:

rqt --force-discover

还不行就重新加载 ROS 环境,检查 ROS_DISTRO 和 AMENT_PREFIX_PATH。

4. 零代码最小示例:观察 turtlesim

这个示例用来确认四件事:节点连接、消息查看、二维曲线绘制、运行时改参数。

启动两个业务节点

终端 1:

source /opt/ros/jazzy/setup.bash
ros2 run turtlesim turtlesim_node

终端 2:

source /opt/ros/jazzy/setup.bash
ros2 run turtlesim turtle_teleop_key

用方向键驱动海龟移动。

用 rqt_graph 看连接

终端 3:

source /opt/ros/jazzy/setup.bash
ros2 run rqt_graph rqt_graph

应看到 /teleop_turtle 通过 /turtle1/cmd_vel 向 /turtlesim 发送速度命令,/turtlesim 还会发布 /turtle1/pose。

命令行核对:

ros2 node list
ros2 topic info /turtle1/cmd_vel --verbose

用 rqt_topic 看消息

终端 4:

source /opt/ros/jazzy/setup.bash
ros2 run rqt_topic rqt_topic

启用 /turtle1/pose,移动海龟时 x、y、theta、linear_velocity、angular_velocity 会更新。

命令行观察:

ros2 topic type /turtle1/pose
ros2 interface show turtlesim/msg/Pose
ros2 topic hz /turtle1/pose

用 rqt_plot 画位置曲线

source /opt/ros/jazzy/setup.bash
ros2 run rqt_plot rqt_plot /turtle1/pose/x /turtle1/pose/y

横向移动主要改变 x,纵向移动主要改变 y。

用 rqt_reconfigure 改参数

source /opt/ros/jazzy/setup.bash
ros2 run rqt_reconfigure rqt_reconfigure

选择 /turtlesim,修改 background_r、background_g、background_b,背景色会变化。

ros2 param list /turtlesim
ros2 param describe /turtlesim background_r

5. 节点图:从“看见连线”到定位通信问题

rqt_graph 中,节点通常显示为方框,话题位于节点之间,箭头方向代表消息流:

发布节点 -> 话题 -> 订阅节点

一个话题可以有多个发布者和多个订阅者。命名空间会成为节点名和相对话题名的前缀。

当期望的连线不存在时,推荐顺序:

ros2 node list
ros2 node info /目标节点
ros2 topic list -t
ros2 topic info /目标话题 --verbose

常见问题:

  • 节点不在图中:进程退出、环境不同、发现域不同
  • 节点存在但话题名不对:相对名或命名空间错误
  • 两端都存在但没数据:QoS 不兼容或回调没执行
  • 图中出现额外订阅者:rqt_topic 或 rqt_plot 正在观察

图形筛选只改变显示,不会停止节点。先用图建立直觉,再用命令行确认事实。

6. 话题监控:类型、频率与当前值

先确认类型:

ros2 topic list -t
ros2 topic type /turtle1/pose
ros2 interface show turtlesim/msg/Pose

三个命令不要混淆:

ros2 topic hz /目标话题
ros2 topic bw /目标话题
ros2 topic echo /目标话题 --once
  • hz 看频率
  • bw 看带宽
  • echo --once 看一个样本

对高频或大消息,rqt_topic 会增加负担。观察者也是订阅者,打开多个插件会增加端点数量。

如果界面不更新,先用 ros2 topic echo 验证;若传感器使用 best_effort,可尝试:

ros2 topic echo /目标话题 --qos-reliability best_effort

7. 实时曲线:选择字段并正确解释

rqt_plot 的路径形式是:

/话题名/嵌套字段/数值叶子字段

例如:

/lab/target_temperature/data
/lab/temperature/temperature
/lab/heater_power/data

可以一次传入多条曲线:

ros2 run rqt_plot rqt_plot \
  /lab/target_temperature/data \
  /lab/temperature/temperature \
  /lab/heater_power/data

曲线能帮助判断:

  • 目标变化后,测量值是否朝正确方向移动
  • 输出是否饱和
  • 系统是否震荡
  • 发布周期是否断裂

但 rqt_plot 主要用于趋势判断,不适合严格的时间同步分析。需要离线分析时用 rosbag。

8. 实时调参:ROS 2 参数服务与约束

ROS 2 中,普通节点默认会提供参数服务。rqt_reconfigure 通过这些服务查看并修改参数。

参数除了名称和值,还可以带描述和范围。节点应在参数回调中校验关键约束,因为外部客户端不只有 GUI,还可能是命令行或自定义程序。

参数、话题和服务的边界:

  • 参数:低频配置,如增益、阈值、开关
  • 话题:连续数据流,如传感器数据和控制输出
  • 服务:一次请求一次响应

YAML 只提供启动时初值。运行时修改参数不会回写原文件。

9. 创建温控观察工程

这一节创建全文唯一一套主线工程:

  • setpoint_source 周期性发布目标温度
  • thermal_plant 订阅目标并模拟加热与散热
  • 再发布测量温度和加热器功率

创建目录

mkdir -p ~/rqt_lab_ws/src/rqt_demo/{rqt_demo,resource,launch,config}
cd ~/rqt_lab_ws/src/rqt_demo
touch resource/rqt_demo
touch rqt_demo/__init__.py

目录结构:

~/rqt_lab_ws/
└── src/
    └── rqt_demo/
        ├── config/demo.yaml
        ├── launch/rqt_lab.launch.py
        ├── resource/rqt_demo
        ├── rqt_demo/__init__.py
        ├── rqt_demo/setpoint_source.py
        ├── rqt_demo/thermal_plant.py
        ├── package.xml
        ├── setup.cfg
        └── setup.py

package.xml

<?xml version="1.0"?>
<package format="3">
  <name>rqt_demo</name>
  <version>0.1.0</version>
  <description>Observable thermal control demo for rqt tools.</description>
  <maintainer email="learner@example.com">learner</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_python</buildtool_depend>
  <exec_depend>ament_index_python</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>
  <exec_depend>rcl_interfaces</exec_depend>
  <exec_depend>rclpy</exec_depend>
  <exec_depend>sensor_msgs</exec_depend>
  <exec_depend>std_msgs</exec_depend>

  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

setup.py

from glob import glob
import os

from setuptools import find_packages, setup

package_name = 'rqt_demo'

setup(
    name=package_name,
    version='0.1.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        (
            'share/ament_index/resource_index/packages',
            ['resource/' + package_name],
        ),
        ('share/' + package_name, ['package.xml']),
        (
            os.path.join('share', package_name, 'launch'),
            glob('launch/*.launch.py'),
        ),
        (
            os.path.join('share', package_name, 'config'),
            glob('config/*.yaml'),
        ),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='learner',
    maintainer_email='learner@example.com',
    description='Observable thermal control demo for rqt tools.',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'setpoint_source = rqt_demo.setpoint_source:main',
            'thermal_plant = rqt_demo.thermal_plant:main',
        ],
    },
)

setup.cfg

[develop]
script_dir=$base/lib/rqt_demo

[install]
install_scripts=$base/lib/rqt_demo

setpoint_source.py

import math

import rclpy
from rcl_interfaces.msg import (
    FloatingPointRange,
    ParameterDescriptor,
    SetParametersResult,
)
from rclpy.node import Node
from std_msgs.msg import Float64


def double_descriptor(description, minimum, maximum, step=0.0):
    return ParameterDescriptor(
        description=description,
        floating_point_range=[
            FloatingPointRange(
                from_value=minimum,
                to_value=maximum,
                step=step,
            )
        ],
    )


class SetpointSource(Node):

    def __init__(self):
        super().__init__('setpoint_source')

        self.declare_parameter('base_target', 24.0, double_descriptor('Center value of the target temperature.', 15.0, 35.0, 0.5))
        self.declare_parameter('amplitude', 2.0, double_descriptor('Sine wave amplitude.', 0.0, 8.0, 0.1))
        self.declare_parameter('period_sec', 20.0, double_descriptor('Sine wave period.', 2.0, 120.0, 1.0))

        self.publisher = self.create_publisher(Float64, 'target_temperature', 10)
        self.start_time = self.get_clock().now()
        self.add_on_set_parameters_callback(self.validate_parameters)
        self.timer = self.create_timer(0.2, self.on_timer)

    def validate_parameters(self, parameters):
        limits = {
            'base_target': (15.0, 35.0),
            'amplitude': (0.0, 8.0),
            'period_sec': (2.0, 120.0),
        }
        for parameter in parameters:
            if parameter.name not in limits:
                continue
            minimum, maximum = limits[parameter.name]
            if not minimum <= float(parameter.value) <= maximum:
                return SetParametersResult(successful=False, reason=f'{parameter.name} must be between {minimum} and {maximum}')
        return SetParametersResult(successful=True)

    def on_timer(self):
        elapsed = (self.get_clock().now() - self.start_time).nanoseconds / 1e9
        base_target = self.get_parameter('base_target').value
        amplitude = self.get_parameter('amplitude').value
        period_sec = self.get_parameter('period_sec').value

        target = base_target + amplitude * math.sin(2.0 * math.pi * elapsed / period_sec)
        msg = Float64()
        msg.data = float(target)
        self.publisher.publish(msg)


def main(args=None):
    rclpy.init(args=args)
    node = SetpointSource()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

thermal_plant.py

import random

import rclpy
from rcl_interfaces.msg import (
    FloatingPointRange,
    ParameterDescriptor,
    SetParametersResult,
)
from rclpy.node import Node
from sensor_msgs.msg import Temperature
from std_msgs.msg import Float64


def double_descriptor(description, minimum, maximum, step=0.0):
    return ParameterDescriptor(
        description=description,
        floating_point_range=[
            FloatingPointRange(
                from_value=minimum,
                to_value=maximum,
                step=step,
            )
        ],
    )


class ThermalPlant(Node):

    def __init__(self):
        super().__init__('thermal_plant')

        self.declare_parameter('ambient_temperature', 20.0, double_descriptor('Ambient temperature.', 0.0, 40.0, 0.5))
        self.declare_parameter('heating_gain', 0.45, double_descriptor('Error-to-power gain.', 0.0, 1.0, 0.01))
        self.declare_parameter('heater_strength', 1.8, double_descriptor('Heating rate.', 0.1, 5.0, 0.1))
        self.declare_parameter('cooling_rate', 0.08, double_descriptor('Cooling coefficient.', 0.0, 0.5, 0.01))
        self.declare_parameter('sensor_noise', 0.03, double_descriptor('Measurement noise.', 0.0, 1.0, 0.01))
        self.declare_parameter('enabled', True, ParameterDescriptor(description='Enable or disable the heater.'))

        self.temperature = self.get_parameter('ambient_temperature').value
        self.target_temperature = 24.0
        self.target_subscription = self.create_subscription(Float64, 'target_temperature', self.on_target, 10)
        self.temperature_publisher = self.create_publisher(Temperature, 'temperature', 10)
        self.power_publisher = self.create_publisher(Float64, 'heater_power', 10)
        self.add_on_set_parameters_callback(self.validate_parameters)
        self.timer = self.create_timer(0.2, self.on_timer)

    def validate_parameters(self, parameters):
        limits = {
            'ambient_temperature': (0.0, 40.0),
            'heating_gain': (0.0, 1.0),
            'heater_strength': (0.1, 5.0),
            'cooling_rate': (0.0, 0.5),
            'sensor_noise': (0.0, 1.0),
        }
        for parameter in parameters:
            if parameter.name not in limits:
                continue
            minimum, maximum = limits[parameter.name]
            if not minimum <= float(parameter.value) <= maximum:
                return SetParametersResult(successful=False, reason=f'{parameter.name} must be between {minimum} and {maximum}')
        return SetParametersResult(successful=True)

    def on_target(self, message):
        self.target_temperature = message.data

    def on_timer(self):
        dt = 0.2
        ambient = self.get_parameter('ambient_temperature').value
        heating_gain = self.get_parameter('heating_gain').value
        heater_strength = self.get_parameter('heater_strength').value
        cooling_rate = self.get_parameter('cooling_rate').value
        sensor_noise = self.get_parameter('sensor_noise').value
        enabled = self.get_parameter('enabled').value

        error = self.target_temperature - self.temperature
        power = max(0.0, min(1.0, heating_gain * error)) if enabled else 0.0

        heating = heater_strength * power
        cooling = cooling_rate * (self.temperature - ambient)
        self.temperature += (heating - cooling) * dt

        measured = self.temperature + random.gauss(0.0, sensor_noise)

        tmsg = Temperature()
        tmsg.header.stamp = self.get_clock().now().to_msg()
        tmsg.header.frame_id = 'room'
        tmsg.temperature = float(measured)
        tmsg.variance = float(sensor_noise ** 2)
        self.temperature_publisher.publish(tmsg)

        pmsg = Float64()
        pmsg.data = float(power)
        self.power_publisher.publish(pmsg)


def main(args=None):
    rclpy.init(args=args)
    node = ThermalPlant()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

demo.yaml

/lab/setpoint_source:
  ros__parameters:
    base_target: 24.0
    amplitude: 2.0
    period_sec: 20.0

/lab/thermal_plant:
  ros__parameters:
    ambient_temperature: 20.0
    heating_gain: 0.45
    heater_strength: 1.8
    cooling_rate: 0.08
    sensor_noise: 0.03
    enabled: true

rqt_lab.launch.py

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    package_share = get_package_share_directory('rqt_demo')
    config_file = os.path.join(package_share, 'config', 'demo.yaml')

    return LaunchDescription([
        Node(
            package='rqt_demo',
            executable='setpoint_source',
            namespace='lab',
            name='setpoint_source',
            output='screen',
            parameters=[config_file],
        ),
        Node(
            package='rqt_demo',
            executable='thermal_plant',
            namespace='lab',
            name='thermal_plant',
            output='screen',
            parameters=[config_file],
        ),
    ])

10. 从零构建、运行和观察完整示例

安装依赖并构建

cd ~/rqt_lab_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install --packages-select rqt_demo
source install/setup.bash

检查安装结果:

ros2 pkg executables rqt_demo
ros2 pkg prefix rqt_demo
ls install/rqt_demo/share/rqt_demo/config
ls install/rqt_demo/share/rqt_demo/launch

启动业务节点

终端 1:

source /opt/ros/jazzy/setup.bash
source ~/rqt_lab_ws/install/setup.bash
ros2 launch rqt_demo rqt_lab.launch.py

应看到目标值和温度随时间变化。

建立基准事实

终端 2:

source /opt/ros/jazzy/setup.bash
source ~/rqt_lab_ws/install/setup.bash
ros2 node list
ros2 topic list -t | grep '^/lab/'

应看到:

/lab/setpoint_source
/lab/thermal_plant
/lab/heater_power [std_msgs/msg/Float64]
/lab/target_temperature [std_msgs/msg/Float64]
/lab/temperature [sensor_msgs/msg/Temperature]

继续检查频率和样本:

ros2 topic hz /lab/temperature
ros2 topic echo /lab/temperature --once
ros2 param get /lab/thermal_plant heating_gain
ros2 param get /lab/setpoint_source amplitude

用 rqt_graph 确认结构

source /opt/ros/jazzy/setup.bash
source ~/rqt_lab_ws/install/setup.bash
ros2 run rqt_graph rqt_graph

主链路应为:

/lab/setpoint_source -> /lab/target_temperature -> /lab/thermal_plant

/lab/thermal_plant 还会发布 /lab/temperature 和 /lab/heater_power。

用 rqt_topic 检查三个消息流

ros2 run rqt_topic rqt_topic

分别启用 /lab/target_temperature、/lab/temperature 和 /lab/heater_power。

用 rqt_plot 对齐目标、温度和功率

ros2 run rqt_plot rqt_plot \
  /lab/target_temperature/data \
  /lab/temperature/temperature \
  /lab/heater_power/data

若功率不明显,可单独绘制:

ros2 run rqt_plot rqt_plot /lab/heater_power/data

用 rqt_reconfigure 实时改变行为

ros2 run rqt_reconfigure rqt_reconfigure

先改 /lab/setpoint_source:

  1. amplitude = 0.0,目标变成水平线
  2. base_target = 28.0,目标立即跳变
  3. period_sec = 8.0,再把 amplitude = 2.0

再改 /lab/thermal_plant:

  1. heating_gain = 0.10,升温变慢
  2. heating_gain = 0.80,升温更快
  3. 关闭 enabled,功率变为 0,温度回落
  4. 再打开 enabled

命令行验证:

ros2 param get /lab/setpoint_source base_target
ros2 param get /lab/thermal_plant heating_gain
ros2 param get /lab/thermal_plant enabled

测试非法值:

ros2 param set /lab/thermal_plant heating_gain 2.0

应返回失败,参数值保持不变。

11. 常用命令与字段路径速查

节点与话题

目的 命令
列出节点 ros2 node list
查看节点端点 ros2 node info /lab/thermal_plant
列出话题和类型 ros2 topic list -t
查看端点与 QoS ros2 topic info TOPIC --verbose
查看一个样本 ros2 topic echo TOPIC --once
统计频率 ros2 topic hz TOPIC
查看接口 ros2 interface show TYPE

参数

目的 命令
列出节点参数 ros2 param list NODE
读取参数 ros2 param get NODE NAME
查看描述与范围 ros2 param describe NODE NAME
修改参数 ros2 param set NODE NAME VALUE
导出当前参数 ros2 param dump NODE

三条曲线路径

含义 话题类型 rqt_plot 路径
目标温度 std_msgs/msg/Float64 /lab/target_temperature/data
测量温度 sensor_msgs/msg/Temperature /lab/temperature/temperature
加热功率 std_msgs/msg/Float64 /lab/heater_power/data

12. 常见问题与排错

rqt 窗口打开但没有节点

printenv ROS_DISTRO
printenv ROS_DOMAIN_ID
ros2 node list

如果 ros2 node list 为空,检查业务进程是否还在运行。必要时重启 CLI 守护进程:

ros2 daemon stop
ros2 daemon start

ros2 launch 找不到包或启动文件

cd ~/rqt_lab_ws
source /opt/ros/jazzy/setup.bash
colcon build --symlink-install --packages-select rqt_demo
source install/setup.bash
ros2 pkg prefix rqt_demo

如果启动文件缺失,检查 setup.py 的 data_files 是否包含 launch/*.launch.py。

节点存在但 /lab 话题没有连接

ros2 node info /lab/setpoint_source
ros2 node info /lab/thermal_plant
ros2 topic info /lab/target_temperature --verbose

rqt_plot 没有曲线

先确认话题有数据:

ros2 topic echo /lab/temperature --once
ros2 interface show sensor_msgs/msg/Temperature

然后输入正确字段路径:

/lab/temperature/temperature

rqt_reconfigure 看不到业务节点

ros2 param list /lab/thermal_plant
ros2 service list | grep '/lab/thermal_plant/.*parameters'

13. 使用建议

建议始终按这个顺序排查:

  1. ros2 node list 和 rqt_graph
  2. ros2 topic list -t
  3. rqt_topic 或 ros2 topic echo
  4. rqt_plot
  5. rqt_reconfigure

每次只改一个参数,记录修改前后的变化。调试时优先选择语义清晰、单位明确的数值话题。实时观察和离线分析要分开:rqt_topic 和 rqt_plot 适合当前状态,rosbag 适合复盘。

14. 总结

rqt_graph、rqt_topic、rqt_plot 和 rqt_reconfigure 分别覆盖结构、消息、趋势和配置四个角度。可靠的调试方式不是只打开一个窗口,而是先确认图关系,再检查话题类型与样本,随后对齐关键曲线,最后只改一个参数并观察因果变化。

本文的温控示例提供了一条完整可复现链路:/lab/setpoint_source 发布目标温度,/lab/thermal_plant 计算并发布测量温度与加热功率;三个数值字段可以实时绘制,节点参数可以在运行中修改,非法范围会被节点拒绝。

15. 参考资料

1 个赞