rqt_guiでプラグインに引数をわたす仕組みを調べる

# はじめに

[rqt_topic](http://wiki.ros.org/rqt_topic)の[ソース](https://github.com/ros-visualization/rqt_topic/blob/master/src/rqt_topic/topic_widget.py#L63)を眺めている時に`selected_topics`がコンストラクタで設定できるように実装されていますが、rqt_gui経由では設定できなかったため、今回その仕組みを調べました。

# 前提条件

| 環境  | バージョン |
|:--:|:--:|
| 開発機  | Ubuntu 16.04.6 LTS |
| ROS  | Kinetic Kame |

# rqt_guiで引数を設定する

[ドキュメント](http://wiki.ros.org/rqt_gui)には詳細がないため、まずヘルプを参照します。

```
$ python /opt/ros/kinetic/lib/rqt_gui/rqt_gui -h
usage: rqt_gui [-b BINDING] [--clear-config] [-f] [--force-discover] [-h] [-l]
               [-ht] [-p PERSPECTIVE] [--perspective-file PERSPECTIVE_FILE]
               [--reload-import] [-s PLUGIN] [-t] [-v]
               [--args [PLUGIN_ARGS [PLUGIN_ARGS ...]]] [--list-perspectives]
               [--list-plugins] [--command-pid PID]
               [--command-start-plugin PLUGIN]
               [--command-switch-perspective PERSPECTIVE]

Options for GUI instance:
  -b BINDING, --qt-binding BINDING
                        choose Qt bindings to be used [pyqt|pyside]
  --clear-config        clear the configuration (including all perspectives
                        and plugin settings)
  -f, --freeze-layout   freeze the layout of the GUI (prevent rearranging
                        widgets, disable undock/redock)
  --force-discover      force a rediscover of plugins
  -h, --help            show this help message and exit
  -l, --lock-perspective
                        lock the GUI to the used perspective (hide menu bar
                        and close buttons of plugins)
  -ht, --hide-title     hide the title label, the icon, and the help button
                        (combine with -l and -f to eliminate the entire title
                        bar and reclaim the space)
  -p PERSPECTIVE, --perspective PERSPECTIVE
                        start with this named perspective
  --perspective-file PERSPECTIVE_FILE
                        start with a perspective loaded from a file
  --reload-import       reload every imported module
  -s PLUGIN, --standalone PLUGIN
                        start only this plugin (implies -l). To pass arguments
                        to the plugin use --args
  -t, --on-top          set window mode to always on top
  -v, --verbose         output qDebug messages
  --args [PLUGIN_ARGS [PLUGIN_ARGS ...]]
                        arbitrary arguments which are passes to the plugin
                        (only with -s, --command-start-plugin or --embed-
                        plugin). It must be the last option since it collects
                        all following options.

Options to query information without starting a GUI instance:
  These options can be used to query information about valid arguments for
  various options.

  --list-perspectives   list available perspectives
  --list-plugins        list available plugins

Options to operate on a running GUI instance:
  These options can be used to perform actions on a running GUI instance.

  --command-pid PID     pid of the GUI instance to operate on, defaults to
                        oldest running GUI instance
  --command-start-plugin PLUGIN
                        start plugin
  --command-switch-perspective PERSPECTIVE
                        switch perspective

Special options for embedding widgets from separate processes:
  These options should never be used on the CLI but only from the GUI code
  itself.
```

rqt_guiでプラグインに渡す引数は、`rqt_gui -s {プラグイン名} --args {引数}`となるようです。ただし注意点として`It must be the last option since it collects all following options.`とあるように、このオプションは、全てのそれ以降の引数を対象としているため、最後の引数とする必要があります。

## rqt_topicの場合

[rqt_topic](http://wiki.ros.org/rqt_topic)に[selected_topics](https://github.com/ros-visualization/rqt_topic/blob/master/src/rqt_topic/topic_widget.py#L63)を設定します。

```
$ python /opt/ros/kinetic/lib/rqt_gui/rqt_gui -s rqt_topic --args /rosout
```

しかし選択したトピック以外にも表示がされてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559128/669ad195-5266-c509-2425-51740bd8d4f2.png)

## rqt_plotの場合

[rqt_plot](http://wiki.ros.org/rqt_plot)に対象のトピックを設定します。

```
$ python /opt/ros/kinetic/lib/rqt_gui/rqt_gui -s rqt_plot --args /my_controller/cmd_vel/linear
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559128/8ec93c91-9451-93b0-b579-f2c34e2d0b59.png)

このプラグインでは、意図した引数として設定できていることが確認できます。そのため、引数の設定方法は間違っていないようです。

# rqt_guiの仕組みを調べる

[rqt_gui](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L93)を実行すると[コンストラクタ](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L46)が呼ばれ、[super](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L49)に渡ります。これは、`from qt_gui.main import Main as Base`で定義されており、[Baseのコンストラクタ](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L42)が呼ばれます。初期化が終わると[main.main()](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L94)から[super(Main, self).main](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L62)が呼ばれ、[main](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L215)が実行されます。mainを実行するとargvは、[arguments](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L221)に格納されます。

[main.py#L224](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L224)でstandaloneでないときの処理分岐がありますが、今回は[standalone=None](https://github.com/ros-visualization/rqt/blob/kinetic-devel/rqt_gui/src/rqt_gui/main.py#L62)のためTrueになり、[plugin_args](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L228)が設定され、最終的に[self._options.plugin_args](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L242)に設定されます。

コマンドラインから`-s`オプションで設定したプラグインパラメータは、standaloneがfalseの時に[main.py#L112](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L112)で`standalone_plugin`に設定され、[plugin](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L580)に格納され、[plugin_manager](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/main.py#L619)によってロードされます。

`load_plugin`では、argvという変数になり、内部の[self._load_plugin_load](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_manager.py#L194)を呼び出します。今回は、` direct handler for in-process plugins`になるため[PluginHandlerDirect](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_manager.py#L239)で`handler`を設定します。PluginHandlerDirectは、[PluginHandler](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L39)を継承しているため、[super(PluginHandlerDirect, self)](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L44)でargvが設定され、[self._argv](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler.py#L65)に格納されます。最後にhandlerで[load](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_manager.py#L253)が実行されます。

loadが実行されると`self._context`に[PluginContext](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L51)が生成され、[self._handler](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_context.py#L45)として設定されるため、[argv](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_context.py#L65)でhundlerの`argv`を参照することができます。[PluginHandler](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L52)のロードが行われると、[self._load](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler.py#L102)が呼ばれますが、これは`NotImplementedError`からわかる通りhandlerによってオーバライドされるものです。実装は、[plugin_handler_direct.py#L54](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L54)となり、`self._context`（先ほど設定した[PluginContext](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L51)）がロードに使用されていることが確認できました。

# プラグイン側の実装の確認

rqt_guiでcontextという形でプラグイン側に渡されることがわかりました。それでは、前述でパラメータの設定の確認ができたrqt_plotの実装を確認してみます。

## rqt_plotの引数について

コンストラクタの[context](https://github.com/ros-visualization/rqt_plot/blob/master/src/rqt_plot/plot.py#L48)が、前述の[PluginContext](https://github.com/ros-visualization/qt_gui_core/blob/kinetic-devel/qt_gui/src/qt_gui/plugin_handler_direct.py#L51)で設定したものであり、[parse_args](https://github.com/ros-visualization/rqt_plot/blob/master/src/rqt_plot/plot.py#L54)によって`argv`がパースされます。[parse_args](https://github.com/ros-visualization/rqt_plot/blob/master/src/rqt_plot/plot.py#L70)は、[Plot.add_arguments](https://github.com/ros-visualization/rqt_plot/blob/master/src/rqt_plot/plot.py#L108)となり、`rqt_gui`で設定した引数がプラグインまで実際にわたされていることが確認できました。

## rqt_topicの引数について

では、引数を設定しても反映されなかったrqt_topicについて確認してみます。[context](https://github.com/ros-visualization/rqt_topic/blob/master/src/rqt_topic/topic.py#L40)は、同様ですが、実装を確認すると`context`から`argv`を呼び出している部分がありません。同様にこのプラグインにはargparseの実装がありません。つまり、コンストラクタには`selected_topics`を設定すればできるように実装されているだけでインターフェースが存在しませんでした。これが、今回引数を設定しても反映されない原因でした。

# おまけ

今回の調査では、rqt_guiを経由した非スタンドアロンで起動しましたが、スタンドアロンもそれぞれ用意されています。コマンドとして登録されているものは、下記のみですが、

```
$ which rqt_plot
/opt/ros/kinetic/bin/rqt_plot
$ ls /opt/ros/kinetic/bin/rqt* | grep rqt
-rwxr-xr-x 1 root root 255 6月 8 2019 /opt/ros/kinetic/bin/rqt
-rwxr-xr-x 1 root root 229 6月 8 2019 /opt/ros/kinetic/bin/rqt_bag
-rwxr-xr-x 1 root root 148 6月 8 2019 /opt/ros/kinetic/bin/rqt_console
-rwxr-xr-x 1 root root 156 6月 8 2019 /opt/ros/kinetic/bin/rqt_dep
-rwxr-xr-x 1 root root 149 6月 8 2019 /opt/ros/kinetic/bin/rqt_graph
-rwxr-xr-x 1 root root 162 6月 8 2019 /opt/ros/kinetic/bin/rqt_logger_level
-rwxr-xr-x 1 root root 236 6月 8 2019 /opt/ros/kinetic/bin/rqt_plot
-rwxr-xr-x 1 root root 243 6月 8 2019 /opt/ros/kinetic/bin/rqt_shell
```

standaloneの実行は、

```
$ ls /opt/ros/kinetic/lib/rqt* | grep -v / | grep -v '^\s*$'
rqt_bag
rqt_console
rqt_controller_manager
rqt_dep
rqt_graph
rqt_gui
rqt_joint_trajectory_controller
rqt_launch
rqt_logger_level
rqt_moveit
rqt_msg
rqt_nav_view
rqt_plot
rqt_pose_view
rqt_publisher
rqt_py_console
rqt_reconfigure
rqt_robot_monitor
rqt_robot_steering
rqt_runtime_monitor
rqt_rviz
rqt_service_caller
rqt_shell
rqt_srv
rqt_tf_tree
rqt_top
rqt_topic
rqt_web
```
から実行することができます。これらの違いは、[rqt_topic/setup.py](https://github.com/ros-visualization/rqt_topic/blob/master/setup.py)と[rqt_plot/setup.py](https://github.com/ros-visualization/rqt_plot/blob/master/setup.py)からなるもので、`generate_distutils_setup`に`scripts`という引数を渡すかどうかの違いです。これは、[setup_dot_py.html#using-package-xml-in-setup-py](http://docs.ros.org/kinetic/api/catkin/html/user_guide/setup_dot_py.html#using-package-xml-in-setup-py)の

> ROS Users should generally not use the scripts argument, as in ROS, executables should be executed using rosrun rather than being installed to the global bin folder.

よりわかる通り`scripts`引数を使うとROS Global Binにインストールされます。これは、catkinで設定される[CATKIN_GLOBAL_BIN_DESTINATION](http://docs.ros.org/kinetic/api/catkin/html/user_guide/variables.html#CATKIN_GLOBAL_BIN_DESTINATION)のことです。setup.pyを使用した[catkin_python_setup](https://github.com/ros-visualization/rqt_plot/blob/master/CMakeLists.txt#L6)があった場合、インストールの挙動がCMakeLists.txtだけで完結しないので注意が必要です。

話を戻すと実装は、[rqt_topic](https://github.com/ros-visualization/rqt_topic/blob/master/scripts/rqt_topic)や[rqt_plot](https://github.com/ros-visualization/rqt_plot/blob/master/scripts/rqt_plot)で確認でき、これまでのソースの確認ですでにわかる通り、`standalone=plugin`でスタンドアローンで起動し、`plugin_argument_provider=Plot.add_arguments`でプラグインのパーサを設定していることがわかります。

# まとめ

* rqt_guiプラグインでの引数の扱い方を確認できた
* プラグインで引数を扱うためにはcontextからargvを参照すると共にargparseの実装が必要である
* インストールは、CMakeLists.txtで完結せずsetup.pyで追加の設定が可能である