ROS2: PyQt5+Qt Desingerでmsg可視化アプリを試作する
海洋ロボコンをやってた人です。
今回は、PyQt5+Qt DesingerでGUIプログラムしたアプリをROS2対応させ、msg可視化アプリを作ったときのメモおよびプログラムたちを備忘録としてまとめました。
他にも、「ROSboard」や「JSK Visualization」といったROSの強力な可視化ツールがたくさん揃っているので、車輪の再発明な点もありますが、Qt「キュート」の勉強も兼ねています。
※2023/02/26追記
本記事の「ROS2」表記、正しくは「ROS 2」です。
本記事のプログラム
この記事で扱うプログラムのコードはGithubよりクローンしてご活用ください。
ROS2: PyQt5+Qt Desingerでmsg可視化アプリを試作する
この記述を読むことで
のように、msg可視化の簡易アプリを試作できます。
個人的なポイントとしては
- ROS2の接続/非接続の可視化
- Rviz同様、トピックのON/OFF切り替え
- Rviz同様、トピック名の変更にも対応
- アプリのアイコンも作成
という点です。
使用環境は以下です。
- Laptop PC
- Ubuntu 20.04 Foxy
sudo apt install pyqt5-dev-tools
Qt DesingerでGUI部分を作る
まずQt「キュート」について、Qtはクロスプラットホームでアプリケーション開発できるフレームワークになります。
基本的にはC++で開発されており、Qt Desinger, Qt Creator といった開発ツールも整っています。
商用利用する場合には、商用ライセンスの購入が必要ですが、趣味での開発ならオープンソースライセンスで問題ないかと思います。
また、RvizのPanelなどはQtライブラリで実装もされており、身近なところで実は使われています。
Qt Desingerで装飾する
基本的なパーツを使って、装飾していきます。
左側の「Widget Box」から以下をドラッグして、パーツをメインウィンドウ内に設置します。
- Buttons
- Check Box
- Input Widgets
- Line Edit
- Containers
- Tab Widget
- Display Widgets
- Label
パーツは全選択後、ヘッダーパネル部の「Layout Horizontal」や「Layout Vertical」でまとめます。
各、ラベルやタイトルは任意の名前に変更してください。
また、Pythonプログラム側で、各ラベルを区別したいので
QObject > objectName
にもオブジェクトの名称をつけておきます。
すべてのラベルやテキストの準備が完了したら保存し、.uiファイルを.pyファイルへ変更します。
pyuic5 ui_LabelTest01.ui -o ui_LabelTest01.py
上記はui_LabelTest01
というファイルの変換ですが、任意の名称に変えてもらえば変換できます。
python側でROS2通信部を作る
続いて、UI部分のプログラムと同じ階層に、mainのプログラムを記述していきます。
まずはコンストラクタに
- デフォルトのtopic名やiconのパスの設定
- 各シグナルとスロットの設定
を記述します。
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.float_topic_name = '/micro_ros_arduino_node_publisher'
self.icon_path = "image/qt_ros_logo.png"
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowIcon(QIcon(self.icon_path))
self.create_menubars()
self.show()
self.ui.checkBox_float_01.stateChanged.connect(self.connect_ros_float)
self.ui.checkBox_float_02.stateChanged.connect(self.update_ros_float)
self.ui.label_topic_setting_float.setText(self.float_topic_name)
self.ui.lineEdit_topic_float.editingFinished.connect(self.change_float_topic)
続いて、ROSの接続/非接続判断としてrclpy.spin_oneにtimeout_set
を与え、タイムアウト時間を超えたら、非接続に、そうでない場合は接続されるような関数を記述します。
def connect_ros_float(self, state):
if (Qt.Checked == state):
try:
# ROS2 init
rclpy.init(args=None)
self.node = Node('Qt_view_node')
self.pub = self.node.create_subscription(
Float64,
self.float_topic_name,
self.sub_float_callback,
10,
)
# spin once, timeout_sec 5[s]
timeout_sec_rclpy = 5
timeout_init = time.time()
rclpy.spin_once(self.node, timeout_sec=timeout_sec_rclpy)
timeout_end = time.time()
ros_connect_time = timeout_end - timeout_init
# Error Handle for rclpy timeout
if ros_connect_time >= timeout_sec_rclpy:
self.ui.label_ros2_state_float.setText("Couldn't Connect")
self.ui.label_ros2_state_float.setStyleSheet(
"color: rgb(255,255,255);"
"background-color: rgb(255,0,51);"
"border-radius:5px;"
)
else:
self.ui.label_ros2_state_float.setText("Connected")
self.ui.label_ros2_state_float.setStyleSheet(
"color: rgb(255,255,255);"
"background-color: rgb(18,230,95);"
"border-radius:5px;"
)
except:
pass
else:
self.node.destroy_node()
rclpy.shutdown()
あとは
- データをアップデートする
update_ros_float
関数 - 編集完了(Enterキー)したときのイベントを示す
change_float_topic
関数 - 指定したトピック名でcallbackする
sub_float_callback
類の関数
を定義します。
def update_ros_float(self, state):
if (Qt.Checked == state):
# create timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.timer_float_update)
self.timer.start(10)
else:
self.timer.stop()
def change_float_topic(self):
self.float_topic_name = self.ui.lineEdit_topic_float.text()
self.ui.label_topic_setting_float.setText(self.float_topic_name)
### ROS2 Data Updater
def sub_float_callback(self, msg):
self.number = round(msg.data, 2)
# print(self.number)
self.update_float_data_label()
def update_float_data_label(self):
self.ui.label_data_num_float.setText(str(self.number))
self.show()
def timer_float_update(self):
rclpy.spin_once(self.node)
self.update_float_data_label()
self.show()
self.timer.start(10)
よりアプリケーションっぽくするためにQMenu
やQAction
などで装飾します。
### QMenu
def create_menubars(self):
menuBar = self.menuBar()
# Creating menus using a QMenu object
fileMenu = QMenu("&File", self)
fileMenu.addAction(self.exit_action())
fileMenu.addMenu(self.prefer_action())
menuBar.addMenu(fileMenu)
# Creating menus using a title
editMenu = menuBar.addMenu("&Edit")
editMenu.addMenu("Undo")
helpMenu = menuBar.addMenu("&Help")
helpMenu.addMenu("Get Started")
def prefer_action(self):
preferMenu = QMenu('Preferences', self)
preferAct = QAction(QIcon('image/setting.jpg'),'Setting', self)
preferMenu.addAction(preferAct)
return preferMenu
def exit_action(self):
# Exit Action, connect
exitAction = QAction(self.style().standardIcon(QStyle.SP_DialogCancelButton),
'&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(qApp.quit)
self.statusBar()
return exitAction
プログラムが完成したらターミナルを開き、mainのpythonプログラムを実行してみましょう。
python3 main_pyqt_ros2.py
表示できたら、ROS2のデモノードを起動させ
ros2 run demo_nodes_cpp talker_loaned_message
Topic Changed
に/chatter_pod
を入力後、Enterキーで反映。
ROS2 Connection
とFloat64 msg
のチェックボックスにチェックを入れて、アプリケーションの動作を確認します。
データをリアルタイムで可視化できていればOKです。
以上
Reference
PyQt5とpython3によるGUIプログラミング:実践編[0]
Discussion