🍃

ROS2: PyQt5+Qt Desingerでmsg可視化アプリを試作する

2022/11/15に公開

海洋ロボコンをやってた人です。
今回は、PyQt5+Qt DesingerでGUIプログラムしたアプリをROS2対応させ、msg可視化アプリを作ったときのメモおよびプログラムたちを備忘録としてまとめました。

他にも、「ROSboard」や「JSK Visualization」といったROSの強力な可視化ツールがたくさん揃っているので、車輪の再発明な点もありますが、Qt「キュート」の勉強も兼ねています。


※2023/02/26追記

本記事の「ROS2」表記、正しくは「ROS 2」です。

本記事のプログラム

この記事で扱うプログラムのコードはGithubよりクローンしてご活用ください。

https://github.com/tasada038/pyqt_ros2_app

ROS2: PyQt5+Qt Desingerでmsg可視化アプリを試作する

この記述を読むことで

https://twitter.com/tasada038/status/1592464497918480384

のように、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のパスの設定
  • 各シグナルとスロットの設定

を記述します。

main_pyqt_ros2.py
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を与え、タイムアウト時間を超えたら、非接続に、そうでない場合は接続されるような関数を記述します。

main_pyqt_ros2.py
    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類の関数

を定義します。

main_pyqt_ros2.py
    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)

よりアプリケーションっぽくするためにQMenuQActionなどで装飾します。

main_pyqt_ros2.py
    ### 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 ConnectionFloat64 msgのチェックボックスにチェックを入れて、アプリケーションの動作を確認します。

データをリアルタイムで可視化できていればOKです。

以上

Reference

PyQt5とpython3によるGUIプログラミング:実践編[0]

Ar-Ray-code/ROS2PyQt5-example

Qt Creator でのGUIアプリケーション開発 5:Signal/slot editor を用いたイベント処理

ゼッツプログラTV: Qt5/Qt6入門 #6 Qt Creatorの使い方1

Discussion