ROS-RQT学习笔记: 编写一个自己的RQT插件(上)

1234
0
2020年6月16日 10时04分

全文目录

  • 什么是RQT?
  • 一个RQT Plugin的工程目录
  • 各文件详细说明
  • 程序运行逻辑
  • 编写RQT插件的具体操作步骤

 

1 什么是RQT?

RQT是ROS中一个基于QT的GUI开发框架, 在这个框架内可以搭载许多GUI小应用, 这些小应用也被叫做rqt_plugin. 利用这个框架就可以在同一个界面内组合多个小应用, 然后打造属于自己的个性化界面,因此非常方便. 另外, 基于RQT框架编写GUI程序, 可以使用统一的API, 关于订阅话题, 发布话题等等都会比较方便, 最重要的是, 这样写出来的GUI小应用不仅可以独立运行还可以依托RQT运行, 非常方便. 因此, 想要基于ROS写图形化界面就可以考虑学习使用RQT.

 

目前,ROS中所提供的可用RQT Plugin(小应用), 包括两类: rqt_common_plugins以及rqt_robot_plugins

参考链接:RQT插件介绍

 

rqt_common_plugins: ROS后端工具

 

  • Launch: 用于编辑\储存\启动Launch文件
  • Dynamic Reconfigure: 用于编辑动态参数
  • Node Graph: 用于显示ROS的Computation graph
  • 等等

 

rqt_robot_plugins: 与机器人交互的工具

 

  • RViz: 这个就不用多说了
  • Diagnostic Monitor: 显示原始诊断数据的简单图形用户界面工具
  • 等等

 

2 一个RQT Plugin的工程目录

一个RQT插件的文件夹结构如下:

 

- rqt_mypkg (mypkg是你自己定义的插件名称)
- - resource
	- > mypkg.ui
- - src
	- rqt_mypkg
		- >  __init__.py
		- >  mypkg.py
		- >  mypkg_widget.py
		- >  ***.py
		- >  ...
- - scripts
	- > rqt_mypkg (也是python文件,但后缀名没有加.py)
- > CMakeLists.txt
- > package.xml
- > plugin.xml
- > setup.py

 

3 各文件详细说明

3.1 package.xml

这个文件是生成ROS包时自动产生的, 主要用来定义软件包的依赖关系, 包括对编译工具的依赖, 对编译, 运行以及测试时需要用到的别的软件包的依赖关系.

 

在这里, 我们只需对默认生成的文件进行简单的修改:

 

找到<export>标签, 在里面增加一行: <rqt_gui plugin="${prefix}/plugin.xml"/>

 

实例如下:

 

   1 <package>
   2   :
   3   <!-- all the existing tags -->
   4   <export>
   5     <rqt_gui plugin="${prefix}/plugin.xml"/>
   6   </export>
   7   :
   8 </package>

 

3.2 plugin.xml

这个文件很重要, 文件中定义了一些参数, 这些参数会在插件运行时被调用. 也就是说, 这个文件不仅仅是给别人看的, 也参与到运行中. (一般, 我们都认为package.xml在运行时不会被调用, 但这里显然不是)

 

文件实例:

 

   1 <library path="src">
   2   <class name="My Plugin" type="rqt_mypkg.my_module.MyPlugin" base_class_type="rqt_gui_py::Plugin">
   3     <description>
   4       An example Python GUI plugin to create a great user interface.
   5     </description>
   6     <qtgui>
   7       <!-- optional grouping...
   8       <group>
   9         <label>Group</label>
  10       </group>
  11       <group>
  12         <label>Subgroup</label>
  13       </group>
  14       -->
  15       <label>My first Python Plugin</label>
  16       <icon type="theme">system-help</icon>
  17       <statustip>Great user interface to provide real value.</statustip>
  18     </qtgui>
  19   </class>
  20 </library>

 

参数说明:

 

  • name: 定义包的名字, 这个没有什么特殊作用, 只要不能冲突, 可以自己随便取
  • type: 格式必须为package.module.class, package是src文件夹下的子文件夹的名字, 根据我们上文第二部分可知, 是rqt_mypkg(但这个文件夹名字是可以自由定义的). module是这个src/rqt_mypkg文件夹下核心的那个python文件的名字, 即mypkg, class是mypkg.py这个文件中定义的那个类的名字.
  • base_class_type: 这个参数不需要改动
  • description: 插件的介绍
  • qtgui: 这个标签底下的所有参数都是用来描述在rqt_gui软件界面的状态栏中如何显示我们创建的Plugin的名称和图标
  • qtgui->group: 将我们创建的Plugin分组
  • qtgui->label: Plugin的名称
  • qtgui->icon: Plugin的图标
  • qtgui->statustip: 鼠标悬浮在Plugin图标上时, 出现的提示内容

 

3.3 setup.py

我们运行插件的时候需要导入一些写好的模块, 如果PYTHONPATH的运行路径没有包括这些模块的路径, 那么运行就会报错. 所以我们需要将这些模块给移动(安装)到PYTHONPATH的路径中去, 这时候就需要setup.py这个文件.

 

实例如下:

 

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup
d = generate_distutils_setup(
    packages=['rqt_mypkg'], # 这里指的是src文件夹下的那个文件夹
    package_dir={'': 'src'},
    scripts=['scripts/rqt_mypkg']
)
setup(**d)

 

实际上这一段代码要做的就是把src/rqt_mypkg里面写好的模块, 安装到devel/lib/python2.7/dist-packages/rqt_mypkg文件夹下, 这样我们只要 source devel/setup.bash之后, 就可以运行

 

3.4 CMakeLists.txt

这也是生成ROS时, 系统默认生成的文件, 这里需要改的位置有两个:

 

  1. 取消catkin_python_setup()这句话的注释, 这一句话是配合刚才的setup.py这个文件的, 只有取消注释这句话以后, 当运行catkin_make命令时, 才会运行setup.py这个文件
  2. 修改:

 

install(PROGRAMS scripts/rqt_mypkg
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
install(DIRECTORY
  resource
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
install(FILES
  plugin.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

 

通过上述两步操作以后, 才可以通过: rosrun rqt_mypkg rqt_mypkg来运行我们创建的Plugin.

 

3.5 resource -> mypkg.ui

这个是Qt中用于描述界面组件布局的文件, 在RQT插件的工程中被放在resource文件夹下. 这个文件可以通过Qt Creator的图形化界面来制作.

 

下面是一个示例:

 

   1 <?xml version="1.0" encoding="UTF-8"?>
   2 <ui version="4.0">
   3  <class>Form</class>
   4  <widget class="QWidget" name="Form">
   5   <property name="geometry">
   6    <rect>
   7     <x>0</x>
   8     <y>0</y>
   9     <width>400</width>
  10     <height>300</height>
  11    </rect>
  12   </property>
  13   <property name="windowTitle">
  14    <string>Form</string>
  15   </property>
  16   <widget class="QPushButton" name="Test">
  17    <property name="geometry">
  18     <rect>
  19      <x>120</x>
  20      <y>70</y>
  21      <width>98</width>
  22      <height>27</height>
  23     </rect>
  24    </property>
  25    <property name="text">
  26     <string>Original Name</string>
  27    </property>
  28   </widget>
  29  </widget>
  30  <resources/>
  31  <connections/>
  32 </ui

 

这个文件是用xml语言写的, 但是并不需要我们直接去写代码, 可以用Qt Creator设计并保存.

 

需要注意一点,上述代码的第4行<widget class="QWidget" name="Form">, 顶层的widget必须是QWidget. 有的时候用Qt Creator设计出来的界面, 顶层的widget默认是QMainWindow, 那么就需要在代码里把QMainWindow改成QWidget.

 

3.6 scripts -> rqt_mypkg

这个文件就是Plugin的主入口, 我们通过调用这个文件从而开始运行Plugin.

 

实例如下:

 

   1 #!/usr/bin/env python
   2 
   3 import sys
   4 
   5 from rqt_mypkg.my_module import MyPlugin
   6 from rqt_gui.main import Main
   7 
   8 plugin = 'rqt_mypkg'
   9 main = Main(filename=plugin)
  10 sys.exit(main.main(standalone=plugin))

 

乍一看, 感觉怎么都不可能运行这个程序就能进入到我们的Plugin的界面, 毕竟我们写好的界面程序都放在src/rqt_mypkg下面.

 

实际上, 这里的Main是rqt框架下的一个类, 我们这里告诉它包含我们的plugin这个包的名字是’rqt_mypkg’, 然后它会自动去’rqt_mypkg’这个包下面找到plugin.xml文件, 然后根据这两个文件中定义的参数来运行和调用我们写好的界面程序.

 

所以, 这个程序除了plugin = 'rqt_mypkg'这句话需要修改包的名称以外, 其余地方都不需要改动. 但是, 前提是你必须写对了plugin.xml这个文件.

 

3.7 src->rqt_mypkg->init.py

这个文件是用来标识这个文件夹下是一个函数模块, 可供导入调用. 这个文件中不需要写任何内容.

 

3.8 src->rqt_mypkg->mypkg.py

这个文件定义了我们自己的Plugin类, 这个类继承于Plugin类.

 

实例如下:

 

   1 import os
   2 import rospy
   3 import rospkg
   4 
   5 from qt_gui.plugin import Plugin
   6 from python_qt_binding import loadUi
   7 from python_qt_binding.QtWidgets import QWidget
   8 
   9 class MyPlugin(Plugin):
  10 
  11     def __init__(self, context):
  12         super(MyPlugin, self).__init__(context)
  13         # Give QObjects reasonable names
  14         self.setObjectName('MyPlugin')
  15 
  16         # Process standalone plugin command-line arguments
  17         from argparse import ArgumentParser
  18         parser = ArgumentParser()
  19         # Add argument(s) to the parser.
  20         parser.add_argument("-q", "--quiet", action="store_true",
  21                       dest="quiet",
  22                       help="Put plugin in silent mode")
  23         args, unknowns = parser.parse_known_args(context.argv())
  24         if not args.quiet:
  25             print 'arguments: ', args
  26             print 'unknowns: ', unknowns
  27 
  28         # Create QWidget
  29         self._widget = QWidget()
  30         # Get path to UI file which should be in the "resource" folder of this package
  31         ui_file = os.path.join(rospkg.RosPack().get_path('rqt_mypkg'), 'resource', 'MyPlugin.ui')
  32         # Extend the widget with all attributes and children from UI file
  33         loadUi(ui_file, self._widget)
  34         # Give QObjects reasonable names
  35         self._widget.setObjectName('MyPluginUi')
  36         # Show _widget.windowTitle on left-top of each plugin (when 
  37         # it's set in _widget). This is useful when you open multiple 
  38         # plugins at once. Also if you open multiple instances of your 
  39         # plugin at once, these lines add number to make it easy to 
  40         # tell from pane to pane.
  41         if context.serial_number() > 1:
  42             self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
  43         # Add widget to the user interface
  44         context.add_widget(self._widget)
  45 
  46     def shutdown_plugin(self):
  47         # TODO unregister all publishers here
  48         pass
  49 
  50     def save_settings(self, plugin_settings, instance_settings):
  51         # TODO save intrinsic configuration, usually using:
  52         # instance_settings.set_value(k, v)
  53         pass
  54 
  55     def restore_settings(self, plugin_settings, instance_settings):
  56         # TODO restore intrinsic configuration, usually using:
  57         # v = instance_settings.value(k)
  58         pass
  59 
  60     #def trigger_configuration(self):
  61         # Comment in to signal that the plugin has a way to configure
  62         # This will enable a setting button (gear icon) in each dock widget title bar
  63         # Usually used to open a modal configuration dialog

 

说明:

  1. 注意这个类的名字, 它的名字要和plugin.xml的type属性的定义一致, 否则会报错.
  2. 这个类继承于Plugin类, 所有的RQT插件都是继承于这个类的子类.
  3. RQT的编程中, 虽然所有Qt中的部件都是由python_qt_binding导入的, 但实际上所有的用法都与PyQt完全一模一样, 编写程序时只需要对照PyQt的文档编写即可.
  4. 程序的第29,31和33行是读取ui文件, 注意写RQT插件需要事先定义好ui文件, 而不能之间在程序里编写界面. 另外, 31行用于查找ui文件路径的代码, 之所以写得这么复杂, 也是为了用户可以在任何路径下运行这个文件都不会产生找不到ui文件的错误.
  5. 程序的第41-44行不能删除, 这是用于协调多个Plugin运行在同一个RQT GUI界面的情况. 另外, 第44行是把当前的插件添加到RQT GUI的界面中.
  6. 函数shutdown_plugin, save_settings, restore_settings都是原Plugin类定义好的函数, 可以根据需要自行重载.

 

3.9 src->rqt_mypkg->mypkg_widget.py

有的时候, 把所有代码都写在同一个类里并不方便, 会让一个类显得特别臃肿. 因此, 可以把有关GUI界面的代码分离出来, 写另一个类中.

 

实例如下:

 

mypkg.py

 

#!/usr/bin/env python
import os
import rospy
import rospkg
from qt_gui.plugin import Plugin
from MyPlugin import MyWidget

class MyPlugin(Plugin):
    def __init__(self, context):
        super(MyPlugin, self).__init__(context)
        self.setObjectName('MyPlugin')
        self._widget = MyWidget() # 定义GUI Widget类
        if context.serial_number() > 1:
            self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
        context.add_widget(self._widget) # Add widget to the user interface

    def shutdown_plugin(self):
        self._widget.close_plugin() # 当关闭插件是, 调用这个函数, 通常是注销一些回调函数
        pass

    def save_settings(self, plugin_settings, instance_settings):
        pass
        
    def restore_settings(self, plugin_settings, instance_settings):
        pass

 

mypkg_widget.py

 

from __future__ import division
# Qt
from python_qt_binding import loadUi
from python_qt_binding.QtCore import Qt, QTimer, Signal, Slot
from python_qt_binding.QtGui import QImage, QPixmap
from python_qt_binding.QtWidgets import QHeaderView, QMenu, QTreeWidgetItem, QWidget
# ROS
import roslib
import roslib.message
import roslib.names
import rospkg
import rospy
import rostopic
# Others
import os

class MyWidget(QWidget):

    def __init__(self):
        super(MyWidget,self).__init__()
        # read UI file
        rp = rospkg.RosPack()
        ui_file = os.path.join(rp.get_path('rqt_mypkg'), 'resource', 'MyPlugin.ui')
        loadUi(ui_file, self)
        self.setObjectName('MyPluginUi')
        # connect widget with slot function
        self.open_pushButton.clicked.connect(self.open_button_slot)
 
    @Slot() # 按钮的回调函数
    def open_button_slot(self):
        topic_type, real_topic, fields = rostopic.get_topic_type("your_topic_name")        
        data_class = roslib.message.get_message_class(topic_type)
        # 订阅了一个话题
        self.mysub = rospy.Subscriber(real_topic, data_class, self.my_callback)
	
	# 话题的回调函数
    def my_callback(self, msg):
       balabala
	
	# 关闭插件时, 注销订阅的话题
    def close_plugin(self):
       try:
            self.mysub.unregister()
        except AttributeError as e:
            rospy.logerr("Subscriber doesn't open.")

 

说明:

 

在RQT的程序当中, 实现话题发布, 订阅或者是服务端, 客户端功能时, 都和普通的写rospy的程序一样, 唯一不同的点在于, 不需要init_node().

发表评论

后才能评论