rqt custum pluginの作り方 〜単独実行を添えて〜

# rqt custum pluginの作り方 〜単独実行を添えて〜

ROSを使った開発をしていると、しばしば`rqt_***`などのGUIソフトウェアを利用することがある。

しかし、いろいろROSに慣れてくると自分の自分のための自分だけのGUIを作りたくなってくる。
これはそういった思いから`rqt`プラグインの使い方を調べたメモ書きである。

## 開発環境

- Ubuntu 18.04 LTS
- ROS melodic

## よくある`rqt`アプリの構成

`rqt`を使用したアプリは`rqt_gui`をメインウィンドウとしてプラグインを読み込むことでGUIを構成しています。
なので、自分のためのGUIを作る方法はプラグインを作成することにあります。

![rqt.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/d0369b58-46d0-ef0e-7f72-980cb9078fbe.png)

## `rqt plugin` の作り方

### `rqt`のインストール

兎にも角にも`rqt`が必要なのでインストールします。

```bash
$ sudo apt install ros-melodic-rqt-gui ros-melodic-rqt-gui-py 
# ほとんどの人はROSをインストールした段階でインストールされてる
```

### `rqt plugin`パッケージを作成する

プラグインを作成するためのパッケージを準備します。

```bash
$ mkdir -p catkin_ws/src
$ cd catkin_ws/src
# catkin-toolsを使用した場合
$ catkin create pkg my_rqt_plugin --catkin-deps rospy rqt_gui rqt_gui_py
# catkin_create_pkgを使用した場合
$ catkin_create_pkg my_rqt_plugin rospy rqt_gui rqt_gui_py
```

この時点で以下のようなフォルダ構成になっています

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        └── package.xml
```

### `manifest.xml`,`plugin.xml`を作成します

作成するプラグインの基本情報やフォルダ構成を記述します。

```xml
<!-- manifest.xml -->

<package>
  <description brief="my_rqt_plugin">
    my_rqt_plugin
  </description>
  <author> *** your account name *** </author>
  <license> *** licence *** </license>
  <review status="unreviewed" notes=""/>
  <url> *** package wiki url *** </url>
  <depend package="rospy"/>
  <depend package="rqt_gui"/>
  <depend package="rqt_gui_py"/>  
  <export>
　　 <!-- あとで作成するplugin.xmlのパスを指定する -->
    <rqt_gui plugin="${prefix}/plugin.xml"/>
  </export>
</package>
```

```xml
<!-- plugin.xml -->

<!-- プラグインスクリプトが格納されているパスを指定する -->
<library path="scripts">
  <!-- あとで作成するプラグインスクリプトをインポートする際のクラス名です -->
  <!-- 今は仮でこのように設定しておきます -->
  <class name="my_rqt_plugin" type="my_rqt_plugin.my_rqt_plugin.MyRqtPlugin" base_class_type="rqt_gui_py::Plugin">
    <description>
      my rqt plugin
    </description>
    <qtgui>
      <label>my_rqt_plugin</label>
      <icon type="theme">applications-other</icon>
      <statustip>my_rqt_plugin</statustip>
    </qtgui>
  </class>
</library>
```

この時点で以下のようなフォルダ構成になります。

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        ├── package.xml
        ├── manifest.xml  # <- ここに作成する
        └── plugin.xml    # <- ここに作成する
```

### プラグインスクリプトを作成する

`plugin.xml`に`scripts`フォルダ内に`my_rqt_plugin.my_rqt_plugin.MyRqtPlugin`なるプラグインを作成したと記述したので
以下のようなファイルを作成する。

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        ├── package.xml
        ├── manifest.xml
        └── plugin.xml
        └── scripts/                  # <- フォルダを作成
            └── my_rqt_plugin/        # <- フォルダを作成
                ├── __init__.py       # <- 空のファイルで良い
                └── my_rqt_plugin.py  # <- ファイルを作成する
```

### `UI`ファイルを作成する

ここでROSではあまり馴染みがないが`Qt Designer`で生成する`.ui`を作成します。

※Qt自体に馴染みがない方はこちらのチュートリアルとある程度、慣れておこう
[Qt をはじめよう！ 第13回: GUI デザイナを使おう](https://www.qt.io/ja-jp/blog/2010/08/19/qt-designer)

今回は`Qt Designer`を使用して以下のようなGUIを`my_rqt_gui.ui`として保存した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/ece295e6-979a-afbe-d411-d1ebe246b487.png)

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        ├── package.xml
        ├── manifest.xml
        ├── plugin.xml
        └── scripts/        
            └── my_rqt_plugin/
                ├── __init__.py
                ├── my_rqt_plugin.py
                └── my_rqt_gui.ui     # <- ファイルを作成する
```

### プラグインスクリプトを記述する

詳細はスクリプト上に記述します。

`.ui`ファイルを読み込み、親オブジェクトに追加するまでの処理を記述する。

```python
# my_rqt_plugin.py

#! /usr/bin/env python2
# -*- coding: utf-8 -*-

import roslib
roslib.load_manifest('my_rqt_plugin')
import os
import rospy

from qt_gui.plugin import Plugin
from python_qt_binding import loadUi
from python_qt_binding.QtWidgets import QWidget  # ROSバージョンによってインポート時のパスが違う


class MyRqtPlugin(Plugin):

  def __init__(self, context):

    # Pluginクラスを初期化する
    super(MyRqtPlugin, self).__init__(context)
    
    self.setObjectName('MyRqtPlugin')

    self._widget = QWidget()
    
    # .ui ファイルを取得する
    ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'my_rqt_gui.ui')
    # .ui ファイルを読み込み、_widget配下にする
    loadUi(ui_file, self._widget)

    self._widget.setObjectName('MyRqtPluginUi')

    # ウィンドウタイトルを設定する
    if context.serial_number() > 1:
      self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))

    # Widgetを親のオブジェクトに追加する
    context.add_widget(self._widget)    

    # Pluginとしてのインターフェースを記述する
    def shutdown_plugin(self):
      pass

    def save_settings(self, plugin_settings, instance_settings):
      pass

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

ここまででプラグインの作成方法は終了です。

### カスタムウィジェットの作り方

今回は`Qt Designer`のPromoteを使ってWidgetを格上げします
格上げしたいWidgetを選択し、右クリックからPromoteを選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/4d949b4e-db26-6314-9fb4-de6e1b97bd97.png)

ここで`Base class name`は元となるWidgetを選択します。今回は`QLCDNumber`です
`Promoted class name`は格上げ後のクラス名なので好きな名前を設定してください
最後に`Header file`ですがここは注意してください
Pythonで作る場合は`<package name>.<module name>`になります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/72efd374-c995-0dce-be79-f0d4f5459b42.png)

例えば、パッケージ名:`my_rqt_plugin`内に`lcdnumber.py`というモジュールを作成した場合、`my_rqt_plugin.lcdnumber`になります

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        ├── package.xml
        ├── manifest.xml
        ├── plugin.xml
        └── scripts/        
            └── my_rqt_plugin/
                ├── __init__.py
                ├── my_rqt_plugin.py
                ├── my_rqt_gui.ui
                └── lcdnumber.py     # <- ファイルを作成する
```

参考として、カスタムウィジェットのクラスの書き方を載せます。

```python
#! /usr/bin/env python2
# -*- coding: utf-8 -*-

import rospy
from std_msgs.msg import Int32
from python_qt_binding.QtWidgets import QLCDNumber

class LCDNumber(QLCDNumber):

  def __init__(self, parent=None):

    super(LCDNumber, self).__init__(parent)
```


### トピックとの連動方法

カスタムプラグイン内では`rospy.init_node`をする必要はありません。`rqt_gui`がやってくれます。
なので、通常のROSノードと同じように設定を行います

```python
import roslib
roslib.load_manifest('my_rqt_plugin')
import os
import rospy

from qt_gui.plugin import Plugin
from python_qt_binding import loadUi
from python_qt_binding.QtWidgets import QWidget
from std_msgs.msg import Int32

class MyRqtPlugin(Plugin):

  def __init__(self, context):

    # Pluginクラスを初期化する
    super(MyRqtPlugin, self).__init__(context)    
    self.setObjectName('MyRqtPlugin')
    self._widget = QWidget()
    
    # .ui ファイルを取得する
    ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'my_rqt_gui.ui')
    # .ui ファイルを読み込み、_widget配下にする    
    loadUi(ui_file, self._widget, {'LCDNumber': LCDNumber})

    self._widget.setObjectName('MyRqtPluginUi')
    self._widget.pushButton.clicked.connect(self.publish)

    # ウィンドウタイトルを設定する
    if context.serial_number() > 1:
      self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))

    # Widgetを親のオブジェクトに追加する
    context.add_widget(self._widget)

    # 配信設定
    self.__pub = rospy.Publisher('rqt_test_pub', Int32, queue_size=10)
    self.__sub = rospy.Subscriber('rqt_test_sub', Int32, self.callback)
  
  def callback(self, data):
    print(data)
```

### シグナル・スロットの記述方法

ここはPyQtと同じなのでサンプルのみ記述します

#### 既存のシグナルスロットを接続する

```python
# ボタンクリックにpublish関数を連動させる
self._widget.pushButton.clicked.connect(self.publish)
```

#### カスタムシグナル・スロットを記述する

```python
from python_qt_binding.QtCore import Signal, Slot

class MyRqtPlugin(Plugin):
  # シグナルオブジェクトを準備する
  addvalue = Signal(int)
  
  def __init__(self):
    # シグナルとスレッドを接続する
    self.addvalue.connect(self.update_value)
  
  def test(self):
    # シグナルを発行する
    self.addvalue.emit(1)

  # スロットを宣言する
  @Slot(int)
  def update_value(self, value):
    print(value)
```




## 自作プラグインの動作確認

上記までのプラグイン作成を終えた後、プラグインの動作確認を行う。
まずはパッケージをビルドし、環境変数を通す。

```
$ cd catkin_ws/
$ catkin build my_rqt_plugin
$ source devel/setup.bash
```

`rqt_gui`を起動し、自作プラグインが認識されていることを確認する

```bash
$ rosru rqt_gui rqt_gui
```

認識された場合は`Plugins`の中に現れます。
それを選択して画面上に現れればプラグイン作成完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/13d6e55e-60f4-fbac-2523-7e377e9f5e53.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/4a153b8d-8aad-14b6-be6e-fd8abc5cebfc.png)

## rqtプラグインの単独実行

ここまでで作成したプラグインですが、いちいち`rqt_gui`を起動してからプラグインを呼び出すのが面倒です。
他のGUIアプリのように`rosrun rqt_*** rqt_***`のように実行したいですよね

そこで一つファイルを作成します。
※名前は何でもいいです

```bash
catkin_ws/
└── src/
    └── my_rqt_plugin/
        ├── src/
        ├── CMakeLists.txt
        ├── package.xml
        ├── manifest.xml
        ├── plugin.xml
        └── scripts/        
            ├── my_rqt_plugin/
            │   ├── __init__.py
            │   ├── my_rqt_plugin.py
            │   ├── my_rqt_gui.ui
            │   └── lcdnumber.py
            └── my_rqt_gui        # <- ファイルを作成する
```

```python
# my_rqt_gui

#! /usr/bin/env python2
# -*- coding: utf-8 -*-

import sys

from rqt_gui.main import Main

# メインウィンドウを作成する
main = Main()
# メインウィンドウ内にプラグインをスタンドアロンで呼び出す
sys.exit(main.main(sys.argv, standalone='my_rqt_plugin.my_rqt_plugin.MyRqtPlugin'))
```

最後にこのファイルに実行権限を与えて実行します。
GUIが表示されるはずです。

```bash
chmod +x catkin_ws/src/my_rqt_plugin/scripts/my_rqt_gui
rosrun my_rqt_plugin my_rqt_gui
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171097/980d1d07-5676-0c3a-dcc3-056155174b8e.png)


## 参考

- [Create your rqt plugin package](http://wiki.ros.org/rqt/Tutorials/Create%20your%20new%20rqt%20plugin#To_run_your_plugin_directly)

- [ROSにおけるrqtを使用したGUIアプリの作成方法](https://myenigma.hatenablog.com/entry/20140330/1396182802)