Help us understand the problem. What is going on with this article?

オリジナル Rviz Plugin をつくってみる 2. ROS1で .ui ファイルを使ってオリジナルパネルをつくる

はじめに

本記事は,下記記事のシリーズ記事です.
関連記事等気になる方は是非下記記事から飛んでください!

https://qiita.com/RyodoTanaka/items/eadfb81bd52404dabdb4

では早速rviz::Panelを作ってみよう!と思います.
と言っても,ソースコードでパネル配置やらをゴリゴリ書く方法は既に多くの紹介があります.
本シリーズの最初に記事のリストがあるので,そちらをご覧ください.

本記事では,「GUIからパネルのデザインを作って,Rviz Pluginにする」ということに主眼を置きたいと思います.
まずは ROS1 環境(melodic, noetic)で動作するものを作ろうと思います.
ソースコードは下記にて公開しています(Apache2.0)

環境については,DockerでROSの環境ベースを作っているので,興味のある方は是非使ってみてください...!

(注意)今回の記事は長いです.が,必要最低限のことしか書かないように努めています...!

動作説明

というわけで早速動作してる様子です.
ソースコードはこちらです.(noetic-develのURLです.)

rviz-panel.gif

コード解説

.uiファイルをつくる

タイトルにもなっている.uiファイルを作ります.
このファイルはパネルのデザインを記述したファイルで,qtcreatorで直感的に配置を作ることができます.
qtcreatoraptで入れれるので,入ってない方は入れてみてください.

$ sudo apt install qtcreator

何か既に出来上がっている.uiファイルを開いてみましょう.
今回は,rviz_plugin_examples/src/ui/dial_panel.uiを開きます.
すると下記のようなWindowが出るはずです.

$ cd <rviz_plugin_examples>/src/ui
$ qtcreator dial_panel.ui

Screenshot from 2020-12-16 20-31-21.png

このツールのいじり方は割愛しますが,おそらく説明が無くても大体わかるんじゃないかと思います.
qtcreator .ui 作り方等でググるとたくさん記事が出てくるので,気になる方はそちらをどうぞ!

要はここでWindow内の要素を配置していきます.
一点気をつけるポイントは,あとで出てくるんですが,各要素の名前がSLOT(コールバック関数)とつなげる際に必要になります.
今は一旦そんなもんかということで流してください.
そして,こいつが MoveItのパネルの設計を定義している正体です.
確かにこの方法なら複雑なパネルも作れますよね...!

あんまり長くなるのも嫌なので次に行きます!

dial_panel.h

#pragma once

#ifndef Q_MOC_RUN
#include <ros/ros.h>
#endif

#include <rviz/panel.h>

namespace Ui {
class DialUI;
}

namespace rviz_plugin_examples
{
class DialPanel: public rviz::Panel
{
  Q_OBJECT
 public:
  DialPanel(QWidget* parent = nullptr);
  ~DialPanel() override;

  void onInitialize() override;
  void onEnable();
  void onDisable();

private Q_SLOTS:
  void dialValueChanged(int value);
  void lineEditChanged();
  void buttonClicked();

protected:
  Ui::DialUI* ui_;
  int value_{0};
  std::string topic_name_{"dial"};

  ros::NodeHandle nh_;
  ros::Publisher pub_;
};
} // end namespace rviz_plugin_examples

namespace Ui {
class DialUI;
}

先程.uiファイルで作ったウインドウはコンパイル時に自動でクラスとヘッダファイルができます.
なので,ここでは宣言だけしておく必要があります.
namespaceUiで固定.
DialUI.uiファイルのQWidtetの所で好きな名前を付けれます.今回はDialUIです.

namespace rviz_plugin_examples
{
class DialPanel: public rviz::Panel
{
  Q_OBJECT
 public:
  DialPanel(QWidget* parent = nullptr);
  ~DialPanel() override;

  void onInitialize() override;
  void onEnable();
  void onDisable();

今回作るプラグインは,rviz::Panelを継承します.
QtのWidgetは入れ子にもできるので親の QWidgetを引数に取っています.
onInitialize()等は読んだままの関数です.
windowの 初期化時,起動時,終了時 の挙動を記述します.

private Q_SLOTS:
  void dialValueChanged(int value);
  void lineEditChanged();
  void buttonClicked();

前記事で紹介していた SLOT,平たく言うとコールバック関数です.
中身を好きに設計できます.

protected:
  Ui::DialUI* ui_;
  int value_{0};
  std::string topic_name_{"dial"};

  ros::NodeHandle nh_;
  ros::Publisher pub_;
};
} // end namespace rviz_plugin_examples

あとは,ROSのPublishに必要なものと,前方宣言していたUi::DialUIのポインタ宣言です.
UI::DiaiLUIの実体は,後で出てきますが,クラスのコンストラクタで確保します.

dial_panel.cpp

#include <rviz_plugin_examples/dial_panel.h>
#include <pluginlib/class_list_macros.h>
#include <std_msgs/Float64.h>

#include "ui_dial_panel.h"

namespace rviz_plugin_examples
{
DialPanel::DialPanel(QWidget* parent) : Panel(parent),  ui_(new Ui::DialUI())
{
  ui_->setupUi(this);
}

DialPanel::~DialPanel() = default;

void DialPanel::onInitialize()
{
  connect(ui_->dial, SIGNAL(valueChanged(int)), this , SLOT(dialValueChanged(int)));
  connect(ui_->pushButton, SIGNAL(clicked()), this, SLOT(buttonClicked()));

  ui_->line_edit->setPlaceholderText("Input topic name (Default : dial)");

  connect(ui_->line_edit,  SIGNAL(textChanged(const QString &)), this, SLOT(lineEditChanged()));

  pub_ = nh_.advertise<std_msgs::Float64>("dial", 1);
  parentWidget()->setVisible(true);
}

void DialPanel::onEnable()
{
  show();
  parentWidget()->show();
}

void DialPanel::onDisable()
{
  hide();
  parentWidget()->hide();
}

void DialPanel::lineEditChanged()
{
  std::string old_topic_name = topic_name_;
  if(ui_->line_edit->text().isEmpty())
    topic_name_ = "dial";
  else
    topic_name_ = ui_->line_edit->text().toStdString();

  ROS_INFO("You set the topic name : %s", topic_name_.c_str());

  if(old_topic_name != topic_name_)
    pub_ = nh_.advertise<std_msgs::Float64>(topic_name_, 1);
}

void DialPanel::dialValueChanged(int value)
{
  ui_->lcd->display(value);
  value_ = value;
  ROS_INFO("You set the value : %d", value_);
}

void DialPanel::buttonClicked()
{
  std_msgs::Float64 msg;
  msg.data = static_cast<double>(value_);
  pub_.publish(msg);
  ROS_INFO("You pushed the button.");
}

}  // namespace rviz_plugin_examples

PLUGINLIB_EXPORT_CLASS(rviz_plugin_examples::DialPanel, rviz::Panel )
#include <rviz_plugin_examples/dial_panel.h>
#include <pluginlib/class_list_macros.h>
#include <std_msgs/Float64.h>

#include "ui_dial_panel.h"

今回はダイアルをRviz上で回し,その値を任意のTopic名でPublishボタンを押したらPublishするという機能を実装します.
値は,std_msgs::Float64なので,これを include しています.
pluginlib/class_list_macros.hは,ROSのpluginlibを利用するために必要です.
これは他のプログラム(ここではRviz)から,ライブラリとしてプログラムを呼び出すための仕組みで,Rviz Pluginでなくても使える仕組みです.
ここでは詳しくは触れませんが,下記本(私も一部関わっております)の11章に詳しく載っています.(森田さんが担当された箇所です)

最後のui_dial_panel.hはコンパイル時に自動生成されるヘッダです.
前項の.uiファイル,ここではdial_panel.uiの拡張子の前に接頭文字ui_がついたヘッダui_dial_panel.hが生成されます.
もちろん中身を利用するのでIncludeしています.

DialPanel::DialPanel(QWidget* parent) : Panel(parent),  ui_(new Ui::DialUI())
{
  ui_->setupUi(this);
}

DialPanel::~DialPanel() = default;

コンストラクタとデストラクタです.
前述の通り,ここでUi::DialUI()クラスの実態確保(new)とセットアップを行っています.
デストラクタはデフォルトコンストラクタです.

void DialPanel::onInitialize()
{
  connect(ui_->dial, SIGNAL(valueChanged(int)), this , SLOT(dialValueChanged(int)));
  connect(ui_->pushButton, SIGNAL(clicked()), this, SLOT(buttonClicked()));

  ui_->line_edit->setPlaceholderText("Input topic name (Default : dial)");

  connect(ui_->line_edit,  SIGNAL(textChanged(const QString &)), this, SLOT(lineEditChanged()));

  pub_ = nh_.advertise<std_msgs::Float64>("dial", 1);
  parentWidget()->setVisible(true);
}

ウィンドウの初期化処理です.
とうとうここでWindow内の要素SLOTSIGNALでつなげます.
つなげる際にはconnect()関数を使います.
ここで書いている書き方はQt4で用いられている書き方です.
この書き方はQt5でも使用できる上,わかりやすいのでこれを使っています.
中身は日本語で書くと,

connect(送信元, 送信SIGNAL,受信元, SLOT)

です.
きちんと型を知りたい方や,Qt5の書き方(Fanctorベースの書き方)を知りたいという方は下記記事がおすすめです.

例えば1つ目のconnect()関数を見てみると

 connect(ui_->dial, SIGNAL(valueChanged(int)), this , SLOT(dialValueChanged(int)));

となっています.
送信元は先程qtcreatorで作ったダイアルのオブジェクト(QObjectといいます)です.
QObjectの名前(オブジェクト名)は下図のように,qtcreator上で入力したものと合わせる必要があります.
逆に言うと,好きな名前に変更できます.
rviz-dial-ui.png

SLOT(コールバック関数)は自身のクラスにialValueChanged(int)で登録しているので,これを指定しています.
同じ要領で,Publishボタンと,トピック名を入れるLinEditorも実装されています.

その後,

  pub_ = nh_.advertise<std_msgs::Float64>("dial", 1);

ROSのパブリッシャーの設定(初期はdialというトピック名で Publish することにしています)を行い,

  parentWidget()->setVisible(true);

このウィンドウ(パネル)が追加された時,表示された状態になるように設定しています.

void DialPanel::onEnable()
{
  show();
  parentWidget()->show();
}

void DialPanel::onDisable()
{
  hide();
  parentWidget()->hide();
}

この部分は特に多くの説明は必要ないかと思います.
要は,

  • Enableになったときに,見えるようにしてね
  • Disableにしたときは隠してね

という処理を行っているだけです.

void DialPanel::lineEditChanged()
{
  std::string old_topic_name = topic_name_;
  if(ui_->line_edit->text().isEmpty())
    topic_name_ = "dial";
  else
    topic_name_ = ui_->line_edit->text().toStdString();

  ROS_INFO("You set the topic name : %s", topic_name_.c_str());

  if(old_topic_name != topic_name_)
    pub_ = nh_.advertise<std_msgs::Float64>(topic_name_, 1);
}

この部分はトピック名を変更した時の処理が書かれています.
空の場合は "dial" に,それ以外は前のトピック名と異なればPublisherを再設定するようになっています.
また,この処理に入ったことがわかるように ROS_INFO()を使ってメッセージを表示させています.(これはなくても良い)

void DialPanel::dialValueChanged(int value)
{
  ui_->lcd->display(value);
  value_ = value;
  ROS_INFO("You set the value : %d", value_);
}

この部分はダイアルの値が変わった時の処理が書かれています.
値が変わったら,ダイアルの隣の LCD 表示の数字を変更します.
具体的にはqtcreator上でオブジェクト名をlcdとした LCD 表示を受け取った値に書き換えています.
その後,ローカル変数value_に値を格納し,ROS_INFO()でメッセージ表示をしています.
メッセージ表示は前と同様,あってもなくても良いです.

void DialPanel::buttonClicked()
{
  std_msgs::Float64 msg;
  msg.data = static_cast<double>(value_);
  pub_.publish(msg);
  ROS_INFO("You pushed the button.");
}

この部分はPublishボタンが押されたときの処理が書かれています.
ローカル変数value_の値をPublishしているただけです.
またこれまでと同様,あってもなくても良いのですが,メッセージ表示をしています.

PLUGINLIB_EXPORT_CLASS(rviz_plugin_examples::DialPanel, rviz::Panel )

とうとう最後の部分です!
この部分で,前述のpluginlibに本クラスを登録しています.
これがないとpluginlibから本クラスのライブラリが探せないので必ず記述してください.

さぁ,やっとこれでソースコードの設定は終了です.
全体の折返し地点ですね...!
今回の記事は長いですががんばりましょう...!

iconを追加

必ず必要なわけではないですが,せっかくなんで見た目をかっちょ良くしたいですよね...!
ということで小休憩的にiconの設定方法です.
ここでのiconというのは,Rviz上に表示されるときのアイコンのことです.
ルールとして,下記があります.

  • icons/classesディレクトリ下に,クラス名(ここではDialPanel)と同じ名前のPNG画像を保存する.
  • PNG画像は 16x16 px

今回は,FontoAwesome系の無料画像を適当に引っ張ってきたものを利用しています.

では,ラストスパート頑張っていきましょう...!

plugin_description.xml

<library path="librviz_plugin_examples">
  <!-- Dial Panel -->
  <class name="rviz_plugin_examples/DialPanel" type="rviz_plugin_examples::DialPanel" base_class_type="rviz::Panel">
    <description>
      Dial Panel plugin.
    </description>
  </class>
</library>

pluginlibに渡す設定ファイルです.

<library path="librviz_plugin_examples">

ここで設定する名前は,ライブラリ(リンカ)の名前です.
今回のプログラムは,コンパイルすると

<catkin_ws>/devel/lib/librviz_plugin_examples.so

となって出来上がります.
このときの.soを除いた名前を設定する必要があります.

 <class name="rviz_plugin_examples/DialPanel" type="rviz_plugin_examples::DialPanel" base_class_type="rviz::Panel">

ここで設定するnameは,dial_panel.cppの最後に記載した名前と一致させる必要があります.
今回は,rviz_plugin_examples::DialPanelと登録していたので,rviz_plugin_examples/DialPanelとしています.
要は::/に変えておけばOKです.
typeはクラス名そのものを記載します.今回は,rviz_plugin_examples::DialPanelです.
base_class_typeは,継承元のクラス名を記載します.今回は,rviz::Panelです.

以上で必要最低限の設定ができました.
<description>タグには,Rviz上で選択した際に表示される説明文を書いておいてください.空でもOKです.

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(rviz_plugin_examples)

###################
# compile options #
###################
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++17 -DEIGEN_RUNTIME_NO_MALLOC")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -fno-asm")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")

#############
## Library ##
#############
## Eigen ##
find_package(Eigen3 REQUIRED)
include_directories(${EIGEN3_INCLUDE_DIR})
link_directories(${EIGEN3_LIBRARY_DIRS})

# Qt settings #
find_package(Qt5 ${rviz_QT_VERSION} REQUIRED Core Widgets)
macro(qt_wrap_ui)
  qt5_wrap_ui(${ARGN})
endmacro()
include_directories(${Qt5Core_INCLUDE_DIRS})
include_directories(${Qt5Widgets_INCLUDE_DIRS})
add_definitions(-DQT_NO_KEYWORDS)
# cmake settings #
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

# catkin #
set(DEPEND_ROS_PKGS
  class_loader
  pluginlib
  roscpp
  rviz
  std_msgs
  )
find_package(catkin REQUIRED
  COMPONENTS
  ${DEPEND_ROS_PKGS}
  )

catkin_package(
  INCLUDE_DIRS
  include
  LIBRARIES
  ${PROJECT_NAME}
  CATKIN_DEPENDS
  ${DEPEND_ROS_PKGS}
  DEPENDS
  EIGEN3
  )

#########
# Build #
#########
## add include directory ##
include_directories(
  include
  ${catkin_INCLUDE_DIRS}
  )
## add library path ##
link_directories(${catkin_LIBRARY_DIRS})

# source codes #
set(HEADERS
  include/rviz_plugin_examples/dial_panel.h
  )
qt5_wrap_ui(UIC_FILES
  src/ui/dial_panel.ui
  )
set(SOURCE_FILES
  src/dial_panel.cpp
  )

add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${HEADERS} ${UIC_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION "${${PROJECT_NAME}_VERSION}")
target_include_directories(${PROJECT_NAME} PRIVATE "${OGRE_PREFIX_DIR}/include")

###########
# Install #
###########
install(FILES plugin_description.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
install(DIRECTORY icons DESTINATION share/${PROJECT_NAME})
install(DIRECTORY include/ DESTINATION include)
install(TARGETS ${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin)
cmake_minimum_required(VERSION 3.5)
project(rviz_plugin_examples)

###################
# compile options #
###################
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++17 -DEIGEN_RUNTIME_NO_MALLOC")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -fno-asm")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")

#############
## Library ##
#############
## Eigen ##
find_package(Eigen3 REQUIRED)
include_directories(${EIGEN3_INCLUDE_DIR})
link_directories(${EIGEN3_LIBRARY_DIRS})

おなじみのプロジェクトの設定から,C++のコンパイルオプションの設定,Eigenのライブラリインポートの設定です.
Eigen3は使ってないんですが,いつも設定値に入れるので消し忘れてました :flushed: 見逃してください... :innocent:

# Qt settings #
find_package(Qt5 ${rviz_QT_VERSION} REQUIRED Core Widgets)
macro(qt_wrap_ui)
  qt5_wrap_ui(${ARGN})
endmacro()
include_directories(${Qt5Core_INCLUDE_DIRS})
include_directories(${Qt5Widgets_INCLUDE_DIRS})
add_definitions(-DQT_NO_KEYWORDS)
# cmake settings #
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

Qt用の設定です.
後で.uiファイルからライブラリを作り出すためのコマンドqt5_wrap_ui()のラッピングマクロqt_wrap_ui()も定義しています.
今回は QtCoreQtWidgetを使うので,これらのinclude_directoriesやらパッケージ設定等を行っています.

catkin_package(
  INCLUDE_DIRS
  include
  LIBRARIES
  ${PROJECT_NAME}
  CATKIN_DEPENDS
  ${DEPEND_ROS_PKGS}
  DEPENDS
  EIGEN3
  )

#########
# Build #
#########
## add include directory ##
include_directories(
  include
  ${catkin_INCLUDE_DIRS}
  )
## add library path ##
link_directories(${catkin_LIBRARY_DIRS})

このあたりは ROSの普通のCMakeLists.txtの書き方と変わらないと思いますので詳しい解説は割愛します.

# source codes #
set(HEADERS
  include/rviz_plugin_examples/dial_panel.h
  )
qt5_wrap_ui(UIC_FILES
  src/ui/dial_panel.ui
  )
set(SOURCE_FILES
  src/dial_panel.cpp
  )

add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${HEADERS} ${UIC_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION "${${PROJECT_NAME}_VERSION}")
target_include_directories(${PROJECT_NAME} PRIVATE "${OGRE_PREFIX_DIR}/include")

ここも基本的には普通の方法と変わらないのですが,唯一 .uiファイルからライブラリを生成するための処理が書かれています.
出来上がったライブラリは,add_library()部分で追加されています.

###########
# Install #
###########
install(FILES plugin_description.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
install(DIRECTORY icons DESTINATION share/${PROJECT_NAME})
install(DIRECTORY include/ DESTINATION include)
install(TARGETS ${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin)

最後にインストール処理です.

install(FILES plugin_description.xml
  DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

この部分で,先程記述したpluginlib用の設定ファイルplugin_description.xmlをインストールし,

install(DIRECTORY icons DESTINATION share/${PROJECT_NAME})
install(DIRECTORY include/ DESTINATION include)
install(TARGETS ${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin)

この部分でアイコン,includeディレクトリ以下すべて,作成されるライブラリ(今回はlibrviz_plugin_examples.so)がインストールされます.

package.xml

<package format="2">
  <name>rviz_plugin_examples</name>
  <version>1.0.0</version>
  <description>Rviz plugin examples</description>

  <author email="groadpg@gmail.com">RyodoTanaka</author>
  <maintainer email="groadpg@gmail.com">RyodoTanaka</maintainer>

  <license>Appache2.0</license>

  <buildtool_depend>catkin</buildtool_depend>
  <buildtool_depend>pkg-config</buildtool_depend>

  <depend>class_loader</depend>
  <depend>pluginlib</depend>
  <depend>roscpp</depend>
  <depend>std_msgs</depend>
  <depend>rviz</depend>
  <depend>eigen</depend>
  <depend>qtbase5-dev</depend>

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

</package>

package.xmlは通常とほとんど何も変わらないので詳しい説明は割愛します.
ただし,pluginlibの設定値を Rviz のプラグイン設定に渡す必要があり,下記記述部分がこれを行っている部分です.

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

おわりに

以上で必要なファイルの説明はすべて完了しました!
なるべく簡素に駆け足で解説しているので,不足部分や間違いがあると思います.
その場合はぜひコメント欄にてご指摘いただけると嬉しいです.
よろしくおねがいします!

というわけで次は,これをROS2(foxy)で実装するにはどうすんの...? という話です.
次記事 -> https://qiita.com/RyodoTanaka/items/4ca117672ad171472578

RyodoTanaka
ロボットのエンジニアになりたい大学院生です. クラリネット吹くのが好きです.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away