はじめに
本記事は,下記記事のシリーズ記事です.
関連記事等気になる方は是非下記記事から飛んでください!
では早速rviz::Panel
を作ってみよう!と思います.
と言っても,ソースコードでパネル配置やらをゴリゴリ書く方法は既に多くの紹介があります.
本シリーズの最初に記事のリストがあるので,そちらをご覧ください.
本記事では,**「GUIからパネルのデザインを作って,Rviz Pluginにする」**ということに主眼を置きたいと思います.
まずは ROS1 環境(melodic, noetic)で動作するものを作ろうと思います.
ソースコードは下記にて公開しています(Apache2.0)
- melodic : https://github.com/RyodoTanaka/rviz_plugin_examples/tree/melodic-devel
- noetic : https://github.com/RyodoTanaka/rviz_plugin_examples/tree/noetic-devel
環境については,DockerでROSの環境ベースを作っているので,興味のある方は是非使ってみてください...!
(注意)今回の記事は長いです.が,必要最低限のことしか書かないように努めています...!
動作説明
というわけで早速動作してる様子です.
ソースコードはこちらです.(noetic-devel
のURLです.)
コード解説
.ui
ファイルをつくる
タイトルにもなっている.ui
ファイルを作ります.
このファイルはパネルのデザインを記述したファイルで,qtcreator
で直感的に配置を作ることができます.
qtcreator
はapt
で入れれるので,入ってない方は入れてみてください.
$ sudo apt install qtcreator
何か既に出来上がっている.ui
ファイルを開いてみましょう.
今回は,rviz_plugin_examples/src/ui/dial_panel.ui
を開きます.
すると下記のようなWindowが出るはずです.
$ cd <rviz_plugin_examples>/src/ui
$ qtcreator dial_panel.ui
このツールのいじり方は割愛しますが,おそらく説明が無くても大体わかるんじゃないかと思います.
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
ファイルで作ったウインドウはコンパイル時に自動でクラスとヘッダファイルができます.
なので,ここでは宣言だけしておく必要があります.
namespace
はUi
で固定.
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内の要素SLOT
をSIGNAL
でつなげます.
つなげる際にはconnect()
関数を使います.
ここで書いている書き方はQt4
で用いられている書き方です.
この書き方はQt5
でも使用できる上,わかりやすいのでこれを使っています.
中身は日本語で書くと,
connect(送信元, 送信SIGNAL,受信元, SLOT)
です.
きちんと型を知りたい方や,Qt5
の書き方(Fanctor
ベースの書き方)を知りたいという方は下記記事がおすすめです.
例えば1つ目のconnect()
関数を見てみると
connect(ui_->dial, SIGNAL(valueChanged(int)), this , SLOT(dialValueChanged(int)));
となっています.
送信元は先程qtcreator
で作ったダイアルのオブジェクト(QObject
といいます)です.
QObject
の名前(オブジェクト名)は下図のように,qtcreator
上で入力したものと合わせる必要があります.
逆に言うと,好きな名前に変更できます.
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は使ってないんですが,いつも設定値に入れるので消し忘れてました 見逃してください...
# 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()
も定義しています.
今回は QtCore
とQtWidget
を使うので,これらの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