創建 ROS rqt 插件 topic service

本機系統:Ubuntu 16.04, ROS Luna

更新20191023:Ubutnu 18.04 LTS ROS Melodic

 

所有文件可在https://github.com/huyaoyu/rqt_my_plugin獲取。

 

關於ROS rqt custom plugin的官方文檔在

http://wiki.ros.org/rqt/Tutorials/Create%20your%20new%20rqt%20plugin

http://wiki.ros.org/rqt/Tutorials/Writing%20a%20Python%20Plugin

相關源碼在

https://github.com/lucasw/rqt_mypkg

本文在綜合上述信息的基礎上,進行了嘗試,這裏把過程留下。

更新20191023:在實例中增加了topic的publisher和subscriber,增加了service 的server和client。

 

創建新ROS package

  • 在catkin的workspace/src下,創建新的ROS package。
catkin_create_pkg rqt_my_plugin rospy rqt_gui rqt_gui_py
  • 修改rqt_my_plugin/package.xml文件

變更package名稱(尚未完善測試,這裏是爲了防止python import過程中出現命名衝突)

<package><name>元素從qrt_my_plugin修改爲my_plugin

 

在<package>element下增加如下內容

<export>
    <rqt_gui plugin="${prefix}/plugin.xml" />
</export>

爲了使用service,在<package>元素內增加

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

 

創建plugin.xml文件。plugin.xml文件同樣位於rqt_my_plugin文件夾下。

<library path="src">
  <class name="My plugin" type="plugin.my_module.MyPlugin" base_class_type="rqt_gui_py::Plugin">
    <description>
      A Python GUI plugin for test the functionalities.
    </description>
    <qtgui>
      <group>
        <label>Logging</label>
        <icon type="theme">folder</icon>
        <statustip>Plugins related to logging.</statustip>
      </group>
      <label>My plugin label</label>
      <icon type="theme">applications-other</icon>
      <statustip>A Python GUI plugin for test the functionalities.</statustip>
    </qtgui>
  </class>
</library>

其中,<class>的type屬性是後面要創建的python腳本內定義的類名。<group>element表示將該插件置於rqt的plugin目錄的哪一個子目錄下。

setup.py文件

在同樣位置創建setup.py文件,內容如下。

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup
  
d = generate_distutils_setup(
    packages=['plugin'],
    package_dir={'': 'src'},
)

setup(**d)

創建UI resource

在package的文件夾內創建resource文件夾,在該文件夾內放置Qt designer輸出的.ui文件。.ui文件實際上是xml文件,這裏使用的實例如下,命名爲MyPlugin.ui。

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Class name</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>My plugin window title</string>
  </property>
  <widget class="QPushButton" name="button_test">
   <property name="geometry">
    <rect>
     <x>120</x>
     <y>70</y>
     <width>98</width>
     <height>27</height>
    </rect>
   </property>
   <property name="text">
    <string>Click me!</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

在目前的UI上,僅有一個PushButton。

 

my_module.py

接下來創建my_module.py,該文件的文件名必須也plugin.xml文件中的library/class@type屬性描述一致。在package的src文件夾下創建文件夾plugin,然後在src/plugin內創建__init__.py文件。__init__.py文件內容爲空。接下來在src/plugin內創建my_module.py文件,內容如下。

import os
import rospy
import rospkg
from std_msgs.msg import String

from qt_gui.plugin import Plugin
from python_qt_binding import loadUi
from python_qt_binding.QtCore import Qt, Slot
from python_qt_binding.QtWidgets import QWidget

from my_plugin.srv import AddTwoInts, AddTwoIntsResponse

class MyPlugin(Plugin):

    def __init__(self, context):
        super(MyPlugin, self).__init__(context)
        # Give QObjects reasonable names
        self.setObjectName('MyPlugin')

        # Process standalone plugin command-line arguments
        from argparse import ArgumentParser
        parser = ArgumentParser()
        # Add argument(s) to the parser.
        parser.add_argument("-q", "--quiet", action="store_true",
                      dest="quiet",
                      help="Put plugin in silent mode")
        args, unknowns = parser.parse_known_args(context.argv())
        if not args.quiet:
            print 'arguments: ', args
            print 'unknowns: ', unknowns

        # Create QWidget
        self._widget = QWidget()
        # Get path to UI file which should be in the "resource" folder of this package
        ui_file = os.path.join(rospkg.RosPack().get_path('my_plugin'), 'resource', 'MyPlugin.ui')
        # Extend the widget with all attributes and children from UI file
        loadUi(ui_file, self._widget)
        # Give QObjects reasonable names
        self._widget.setObjectName('MyPluginUi')
        # Show _widget.windowTitle on left-top of each plugin (when 
        # it's set in _widget). This is useful when you open multiple 
        # plugins at once. Also if you open multiple instances of your 
        # plugin at once, these lines add number to make it easy to 
        # tell from pane to pane.
        if context.serial_number() > 1:
            self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
        
        # Push button.
        self._widget.button_test.clicked.connect(self.on_button_test_clicked)
        
        # Add widget to the user interface
        context.add_widget(self._widget)

        # Simple topic publisher from ROS tutorial.
        self.rosPub = rospy.Publisher('my_plugin_pub', String, queue_size=10)
        self.rosPubCount = 0

        # Simple topic subscriber.
        self.rosSub = rospy.Subscriber("my_plugin_sub", String, self.ros_string_handler)

        # Start simple ROS server.
        self.rosSrv = rospy.Service("add_two_ints", AddTwoInts, self.handle_add_two_ints)

    def ros_string_handler(self, data):
        rospy.loginfo(rospy.get_caller_id() + ": Subscriber receives %s", data.data)

    def handle_add_two_ints(self, req):
        rospy.loginfo( "Returning [%s + %s = %s]" % (req.a, req.b, (req.a + req.b)) )
        return AddTwoIntsResponse(req.a + req.b)

    @Slot()
    def on_button_test_clicked(self):
        rospy.loginfo("button_test gets clicked. Send request to itself.")

        self.rosPub.publish("rosPubCount = %d. " % (self.rosPubCount))
        self.rosPubCount += 1

        # Send service request.
        try:
            rospy.wait_for_service("add_two_ints", timeout=5)

            # Set the actual request.
            add_two_ints = rospy.ServiceProxy('add_two_ints', AddTwoInts)
            resp = add_two_ints(self.rosPubCount, 100)

            rospy.loginfo("add_two_ints responses with %d. " % ( resp.sum ))
        except rospy.ROSException as e:
            rospy.loginfo("Service add_two_ints unavailable for 5 seconds.")
        except rospy.ServiceException as e:
            rospy.logerr("Service request to add_two_ints failed.")

    def shutdown_plugin(self):
        # TODO unregister all publishers here
        self.rosSrv.shutdown()
        rospy.loginfo("rosSrv shutdown. ")

        self.rosSub.unregister()
        rospy.loginfo("rosSub unregistered. ")

        self.rosPub.unregister()
        rospy.loginfo("rosPub unregistered. ")

    def save_settings(self, plugin_settings, instance_settings):
        # TODO save intrinsic configuration, usually using:
        # instance_settings.set_value(k, v)
        pass

    def restore_settings(self, plugin_settings, instance_settings):
        # TODO restore intrinsic configuration, usually using:
        # v = instance_settings.value(k)
        pass

    #def trigger_configuration(self):
        # Comment in to signal that the plugin has a way to configure
        # This will enable a setting button (gear icon) in each dock widget title bar
        # Usually used to open a modal configuration dialog
        

這裏my_moduel.py中,注意__init__函數中loadUi( )的使用。loadUi( )執行過之後,self._widget對象會根據MyPlugin.ui文件的描述,自動創建新的成員變量,例如我們的按鈕將會成爲self._widget.button_test。在__init__( )函數中同樣註冊好buttion_test的onClicked事件的回調函數,或者通過

self._widget.button_test.clicked.connect(self.on_button_test_clicked)

實現。on_button_test_clicked( )函數即爲該回調函數。這裏@Slot修飾符其實可以不用。觀察on_button_test_clicked( )函數可知,當我們點擊button_test按鈕後,將會產生一組有關簡單loginfo, topic和service的示例操作 。

 

首先,程序向my_plugin_pub topic 發送了一個std_msgs/String類型的數據。之後,程序試圖連接add_two_ints service,若5秒鐘內連接成功則向該service發送請求。請求成功處理後會顯示response結果。若請求超時或者請求返回異常,會有對應的處理。 這裏所用到的topic publisher和service server都聲明在MyPlugin的__init__()函數中,分別是self.rosPub和self.rosSrv。所以本實例中,add_two_ints service的提供方是自己,這只是作爲例子而已,現實中server和client若都是一個node貌似沒有什麼用。

 

此外,作爲實例的一部分,MyPlugin類中聲明瞭一個topic subscriber,註冊用於監聽my_plugin_sub topic。可通過rostopic pub命令進行測試。

 

本實例中所用到的topic publisher, subscriber和 service server, client的邏輯都取自ROS的官方教程

ROS文檔中提到,rqt插件不需要用戶顯式調用init_node( ),但是在shut_down( )函數中需要unsubscribe所有已經訂閱過的topic, 註銷自身的publisher,釋放所有自身的timer。

 

創建scripts文件夾

根據ROS的推薦方案,在package文件夾下創建scripts文件夾,將啓動plugin的python腳本放入其中。腳本名爲my_plugin,注意該文件名與src文件夾中的源碼文件是同名的,但是省略了.py擴展名。通過chmod命令修改my_plugin的權限,將其變爲可執行文件。my_plugin的內容如下。

#!/usr/bin/env python

import sys

from plugin.my_module import MyPlugin
from rqt_gui.main import Main

plugin = 'my_plugin'
main = Main(filename=plugin)
sys.exit(main.main(standalone=plugin))

 

創建service

在<catkin_ws>/src/rqt_my_plugin/下創建srv目錄。在srv目錄內創建文本文件AddTwoInts.srv,其內容如下

int64 a
int64 b
---
int64 sum

 

配置CMakeLists.txt

修改文件最開始project()指令爲project(my_plugin)

取消catkin_python_setup( )行的註釋,取消註釋或增加如下內容

## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
  rospy
  rqt_gui
  rqt_gui_py
  std_msgs
  message_generation
)

# Generate services in the 'srv' folder
add_service_files(
  FILES
  AddTwoInts.srv
)

# Generate added messages and services with any dependencies listed here
generate_messages(
  DEPENDENCIES
  std_msgs  # Or other packages containing msgs
)

# Mark executable scripts (Python etc.) for installation
# in contrast to setup.py, you can choose the destination
install(PROGRAMS
  scripts/my_plugin
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

install(DIRECTORY
  resource
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

# Mark other files for installation (e.g. launch and bag files, etc.)
install(FILES
  plugin.xml
  # myfile2
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

 

catkin_make

使用

catkin_make --pkg my_plugin

編譯package。事實上我們並沒有編寫任何C++代碼,所以catkin_make並沒有實際的編譯連接過程發生,但由於聲明瞭service,所以會有相關的生成過程。catkin_make之後,可以通過

rossrv show my_plugin/AddTwoInts

來顯示service的配置,結果如圖所示

rossrv show my_plugin/AddTwoInts

 

測試

可以使用如下幾種方法進行測試

(1)rosrun my_plugin my_plugin

(2)rqt --standalone my_plugin

(3)先啓動rqt,然後用Plugins->Logging->My plugin label菜單在rqt的框架內添加新一個my_plugin實例。

 

(1)和(2)將看到相同的GUI,如下圖所示。

Hit the button

(注意:由於我使用了屏幕縮放,所以GUI的比例可能不協調。我加大了按鈕的尺寸來容納屏幕縮放時自動放大的字體。)

方法(3)將在rqt內顯示我們的my_plugin,可以自由移動my_plugin到rqt的docking place或者開啓多個my_plugin實例。

通過點擊 “Click me!” 按鈕,啓動button_test的回調函數,此時在terminal內將看到我們輸出的字符串和service的工作情況,如上圖所示。

 

在另外一個terminal中通過

rostopic pub /my_plugin_sub std_msgs/String "Test topic string." -r 1

命令以1Hz頻率向my_plugin_sub topic發送數據,可以看到plugin所在的terminal內的輸出

Plugin subscriber

 

注意,在回調函數中,不能對Qt的Widget進行操作,原因是rqt將在子線程中運行插件,每個插件實例都有自己的線程。可在回調函數中emit 一個Qt singal,從在slot內而實現對Widget的操作(尚未測試)。

 

更新20191023

  • 爲了減少python import的難度,避免出現命名重複,修改了部分對象的命名,在此處列出(注意:前文相關位置都已進行了修正,前文所列配置和源碼均爲最新狀態
  • 修改rqt_my_plugin/package.xml文件,變更package名稱。<package><name>元素從qrt_my_plugin修改爲my_plugin
  • 重命名<catkin_ws>/src/rqt_my_plugin/src/rqt_my_plugin 文件夾爲 <catkin_ws>/src/rqt_my_plugin/src/plugin
  • 重命名<catkin_ws>/src/rqt_my_plugin/scripts/rqt_my_plugin 文件爲<catkin_ws>/src/rqt_my_plugin/scripts/my_plugin
  • 修改plugin.xml文件中<library><class>元素的type屬性,從type="rqt_my_plugin.my_module.MyPlugin" 改爲 type="plugin.my_module.MyPlugin"
  • 修改setup.py文件,將

d = generate_distutils_setup(
    packages=['rqt_my_plugin'],
    package_dir={'': 'src'},
)

改爲

d = generate_distutils_setup(
    packages=['plugin'],
    package_dir={'': 'src'},
)

  • 修改<catkin_ws>/src/rqt_my_plugin/src/plugin/my_module.py 中

ui_file = os.path.join(rospkg.RosPack().get_path('rqt_my_plugin'), 'resource', 'MyPlugin.ui')

ui_file = os.path.join(rospkg.RosPack().get_path('my_plugin'), 'resource', 'MyPlugin.ui')

  • 修改<catkin_ws>/src/rqt_my_plugin/scripts/my_plugin文件內容

從from rqt_my_plugin.my_module import MyPlugin 變爲 from plugin.my_module import MyPlugin

從plugin = 'rqt_my_plugin' 變爲 plugin = 'my_plugin'

  • 修改CMakeLists.txt

修改project( )指令爲project(my_plugin)

修改

install(PROGRAMS

  scripts/rqt_my_plugin

  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}

)

install(PROGRAMS

  scripts/my_plugin

  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}

)

其他修改見於CMakeLists.txt文件。

  • 添加有關topic和service的實現。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章