IgnitionGazebo講座02 GUIプラグインを作成する

# 環境

この記事は以下の環境で動いています。

| 項目 |	値 |
|:--|:--|
| CPU | Core i5-3470  |
| Ubuntu | 22.04 |
| GPU| GTX 1050 Ti |
| GPU driver | 535.86.05  |
| ROS2 | Humble |
| Ignition Gazebo| Fortress |

# 概要

IgnitionではPluginを使用することで、機能を拡張することが出来ます。今回はGUIプラグインを作成・使用をします。
Ignition GazeboのGUIはGazebo Classicから大きく変更されていて、画面上のほぞすべての表示要素をプラグインで制御できるようになっています。

# 予備知識

## QtQuick

QtQuickとはQtの新たなUIフレームワークです。QML(Qt Meta-Object Language)というファイルを使ってUIを実装します。追加のパッケージが必要なため以下のコマンドでインストールします。

```shell:extrasのインストール
sudo apt install qml-module-qtquick-extras
```

# GUIプラグインの作成

## QML

```qml:ignition_plugin_lecture/src/switch_panel/SwitchPanel.qml
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import QtQuick.Extras 1.4
import "qrc:/qml"

Rectangle 
{
  Layout.minimumWidth: 400
  Layout.minimumHeight: 110

  Row {
    spacing: 5
    
    Button {
      text: "Forward"
      onPressed: { SwitchPanel.OnForwardButton(); }
    }

    Button {
      text: "Stop"
      onPressed: { SwitchPanel.OnStopButton(); }
    }

    Button {
      text: "Reverse"
      onPressed: { SwitchPanel.OnReverseButton(); }
    }
  }
}
```

* qmlファイルを用いてUIを設計します。
* トップには`Rectangle`要素を、その下に`Row`要素を置きます。
    * `Layout.minimumWidth`/`Layout.minimumHeight`は設定しないとGUIが正しく表示されないことがあります。

* `Row`はQtでいうLayoutに近いもので配下の要素を水平に並べます。
* `Button`は押しボタンの要素です。
    * `onPressed`以下にはボタンを押した時に実行される処理を書きます。この処理は後のccファイルに書きます。

## c++コード

```c++:ignition_plugin_lecture/src/switch_panel/SwitchPanel.hpp
#include <ignition/gui/qt.h>
#include <ignition/transport/Node.hh>
#include <ignition/gui/Plugin.hh>

namespace iginition_plugin_lecture
{

class SwitchPanel : public ignition::gui::Plugin
{
  Q_OBJECT

public:
  SwitchPanel();
  virtual ~SwitchPanel();
  void LoadConfig(const tinyxml2::XMLElement * _pluginElem) override;

protected slots:
  void OnForwardButton(void);
  void OnStopButton(void);
  void OnReverseButton(void);

private:
  void CreateIgnitionIf(void);

private:
  ignition::transport::Node node_;
  ignition::transport::Node::Publisher speed_pub_;
  float forward_speed_{1.0f};
  float reverse_speed_{1.0f};
};

}  // namespace iginition_plugin_lecture
```

```c++:ignition_plugin_lecture/src/switch_panel/SwitchPanel.cpp
#include "SwitchPanel.hpp"

#include <ignition/msgs/float.pb.h>
#include <ignition/plugin/Register.hh>

namespace iginition_plugin_lecture
{

SwitchPanel::SwitchPanel() : Plugin()
{
  CreateIgnitionIf();
}

SwitchPanel::~SwitchPanel()
{
}

void SwitchPanel::LoadConfig(const tinyxml2::XMLElement * _pluginElem)
{
  if (!_pluginElem) {
    return;
  }

  auto forward_speed_elem = _pluginElem->FirstChildElement("forward_speed");
  if (nullptr != forward_speed_elem)
  {
    forward_speed_ = forward_speed_elem->FloatText();
  }

  auto reverse_speed_elem = _pluginElem->FirstChildElement("reverse_speed");
  if (nullptr != reverse_speed_elem)
  {
    reverse_speed_ = reverse_speed_elem->FloatText();
  }
}

void SwitchPanel::OnForwardButton(void) {
  ignition::msgs::Float float_msg;
  float_msg.set_data(forward_speed_);
  speed_pub_.Publish(float_msg);
}

void SwitchPanel::OnStopButton(void) {
  ignition::msgs::Float float_msg;
  float_msg.set_data(0.0f);
  speed_pub_.Publish(float_msg);
}

void SwitchPanel::OnReverseButton(void) {
  ignition::msgs::Float float_msg;
  float_msg.set_data(reverse_speed_);
  speed_pub_.Publish(float_msg);
}

void SwitchPanel::CreateIgnitionIf(void){
  this->speed_pub_ = this->node_.Advertise<ignition::msgs::Float>("target_speed");
}

}

// Register this plugin
IGNITION_ADD_PLUGIN(iginition_plugin_lecture::SwitchPanel, ignition::gui::Plugin)
```

* `ignition::gui::Plugin`クラスを継承したクラスを作成します。
* 起動時に`LoadConfig()`が呼ばれます。SDFファイルで子要素で書いた値が読めます。
* `OnForwardButton()`、`OnStopButton()`、`OnReverseButton()`UIで作ったボタンの押した時の処理slots関数です。

## ビルド系
qrcはリソースファイルで実行に必要なファイルを列挙するファイルです。のちのCMakeで指定します。画像ファイルなどを使うときはこれも記載に追加します。

```text:ignition_plugin_lecture/src/switch_panel/SwitchPanel.qrc
<!DOCTYPE RCC><RCC version="1.0">
  <qresource prefix="SwitchPanel/">
    <file>SwitchPanel.qml</file>
  </qresource>
</RCC>
```

CmakeではQt特有の記述が入ります。

```cmake:ignition_plugin_lecture/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(ignition_plugin_lecture)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

set(CMAKE_AUTOMOC ON)

find_package(ament_cmake REQUIRED)
find_package(ignition-plugin1 REQUIRED COMPONENTS register)
find_package(ignition-gazebo6 REQUIRED)

include_directories(SYSTEM
  ${IGNITION-COMMON_INCLUDE_DIRS}
  ${IGNITION-GUI_INCLUDE_DIRS}
)

# gui plugin
qt5_add_resources(resources_rcc src/switch_panel/SwitchPanel.qrc)
add_library(SwitchPanel SHARED
  src/switch_panel/SwitchPanel.cpp
  ${resources_rcc}
)
target_link_libraries(SwitchPanel
  ${IGNITION-COMMON_LIBRARY_DIRS}
  ${IGNITION-GUI_LIBRARIES}
)

# system plugin
add_library(RotateAxis SHARED
  src/rotate_axis/rotate_axis.cpp
)
target_link_libraries(RotateAxis
  ${IGNITION-PLUGIN_LIBRARIES}
  ${IGNITION-GAZEBO_LIBRARIES}
)

install(
  TARGETS SwitchPanel RotateAxis
  LIBRARY DESTINATION lib
)

ament_environment_hooks("${CMAKE_CURRENT_SOURCE_DIR}/hooks/${PROJECT_NAME}.dsv.in")

ament_package()
```

* `set(CMAKE_AUTOMOC ON)`を書いてQt用のMOCを有効にします。
* `qt5_add_resources()`でqrcファイルを指定します。qmlファイルや画像を取り込んだc++ファイルが生成されます。
* ターゲット名とQMLファイル名は一緒にします。
* `ament_environment_hooks()`で環境変数を設定します。

# GUIプラグインの使用

```xml:ignition_lecture/worlds/gui_example1.sdf
<?xml version="1.0" ?>

<sdf version="1.6">
  <world name="visualize_lidar_world">
    
    <gui fullscreen="0">
      <plugin filename="GzScene3D" name="3D View">
        <camera_pose>10 0 6 0 0.5 3.14</camera_pose>
        <ignition-gui>
          <title>3D View</title>
          <property type="bool" key="showTitleBar">false</property>
          <property type="string" key="state">docked</property>
        </ignition-gui>
      </plugin>

      <plugin filename="SwitchPanel" name="iginition_plugin_lecture::SwitchPanel">
        <forward_speed>0.3</forward_speed>
        <reverse_speed>-0.1</reverse_speed>
        <ignition-gui>
          <title>SwitchPanel</title>
          <property type="bool" key="showTitleBar">true</property>
          <property type="string" key="state">docked</property>
          <property key="resizable" type="bool">true</property>
        </ignition-gui>
      </plugin>

      <plugin filename="Teleop" name="ignition::gui::plugins::Teleop">
        <topic>/cmd_vel</topic>
        <ignition-gui>
          <property type="bool" key="showTitleBar">true</property>
          <property type="string" key="state">docked</property>
          <property key="resizable" type="bool">true</property>
        </ignition-gui>
      </plugin>
    </gui>

    <physics name="1ms" type="ignored">
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1.0</real_time_factor>
    </physics>
    <plugin
      filename="libignition-gazebo-physics-system.so"
      name="ignition::gazebo::systems::Physics">
    </plugin>
    <plugin
      filename="libignition-gazebo-sensors-system.so"
      name="ignition::gazebo::systems::Sensors">
      <render_engine>ogre2</render_engine>
    </plugin>
    <plugin
      filename="libignition-gazebo-scene-broadcaster-system.so"
      name="ignition::gazebo::systems::SceneBroadcaster">
    </plugin>

    <light name="sun" type="directional">
      <cast_shadows>true</cast_shadows>
      <pose>0 0 10 0 0 0</pose>
      <diffuse>0.8 0.8 0.8 1</diffuse>
      <specular>0.2 0.2 0.2 1</specular>
      <attenuation>
        <range>1000</range>
        <constant>0.9</constant>
        <linear>0.01</linear>
        <quadratic>0.001</quadratic>
      </attenuation>
      <direction>-0.5 0.1 -0.9</direction>
    </light>

  </world>
</sdf>
```

* `<gui>`タグの中でGUIの設定を書きます。このタグがない場合はignitionのデフォルトのGUIが適用されます。
    * `<ignition-gui>`以下ではパネルの設定を指定します。
        * `<title>`パネルのタイトルの文字を指定します。
        * 他はパネルのサイズの設定です。

## ビルド&実行

```shell:ビルド
source /opt/ros/humble/setup.bash
cd ros2_ws
colcon build
```

```shell:実行
source ~/ros2_ws/install/setup.bash
ros2 launch ignition_lecture sim.launch.py
ros2 launch ignition_lecture gazebo.launch.py world:=gui_example1.sdf
```

```shell:ignトピックのecho
ign topic -e -t target_speed
```

![gui_plugin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/9ee8871b-ff29-9df6-58c7-49908f3ba257.gif)


# 参考
[ignition pluginパス](https://gazebosim.org/api/gazebo/6.0/resources.html)
[gui pluginリファレンス](https://gazebosim.org/api/gui/7.0/classignition_1_1gui_1_1Plugin.html)

# 目次ページへのリンク
[ROS2講座の目次へのリンク](https://qiita.com/srs/items/5f0d765343ca78f524ed)