1
$\begingroup$

Rosanswers logo

Hello,

I want to create a GUI for my project using ROS2. I do not have any experience in developing graphical applications, so I decided to use Qt Designer for a more "Drag and Drop" experience. I was able to export the contents of the .ui file into a Python file, using pyuic5. I present both files, the generated Python file, and the main file below:

generated python file

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(230, 197)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.dataLabel = QtWidgets.QLabel(self.centralwidget)
        self.dataLabel.setIndent(10)
        self.dataLabel.setObjectName("dataLabel")
        self.verticalLayout.addWidget(self.dataLabel)
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.updateBtn = QtWidgets.QPushButton(self.centralwidget)
        self.updateBtn.setObjectName("updateBtn")
        self.verticalLayout.addWidget(self.updateBtn)
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.dataLabel.setText(_translate("MainWindow", "Subscription to topic : chatter"))
        self.label.setText(_translate("MainWindow", "TextLabel"))
        self.updateBtn.setText(_translate("MainWindow", "Update"))
        
        
class Window(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        
        self.counter_value = 0
        self.label.setText(str(self.counter_value))
        
        self.updateBtn.clicked.connect(self.callback_updateBtn)
        
    def callback_updateBtn(self):
        self.counter_value = self.counter_value + 1
        self.label.setText(str(self.counter_value))

As you can see, I have added a small class to initialise and mainly to implement the callback funciton. The main.py is as follows:

main file

#! /usr/bin/env python3

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.uic import loadUi

from simple import Window

def main():
    app = QApplication(sys.argv)
    win = Window()
    win.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

Problem: Now I want to make this GUI function with ROS2. I understand that I will have to create a class for my ROS2 Node (I would like the Window class to be my ROS2 node). I would also like to set up a subscriber in the constructor of the ROS2 Node, let us say that for the time being it listens to the chatter topic. I would like to use the callback_updateBtn function to act as the subscriber callback, and each time it receives a message on the topic, it would update the label with the value passed as the message.

My question is that, how do I go about defining them? I am quite confused, since the main function has the part

# rclpy.init(args=None)
# node = Window()
app = QApplication(sys.argv)
win = Window()
win.show()
# rclpy.spin(node)
sys.exit(app.exec())
# rclpy.shutdown()

which would need to be there, and for ROS2, I would also need to include the commented lines above. I looked at an approach online where they separated the execution on a different thread for ROS, but I do not understand how to go about that. In my mind, I think I should be able to inherit the properties of the rclpy.Node class into Window, set up the publishers and subscribers in the constructor, and simply modify the callback function so that now instead of looking at a counter value, I look at the data on the topic.

For my project, I would need to trigger some service calls with the use of Buttons as well. I am trying to get a basic idea of how I should go on about it.

Any help and guidance on this matter would be highly appreciated.

Thanks!


Originally posted by sampreets3 on ROS Answers with karma: 230 on 2021-12-21

Post score: 4

$\endgroup$

1 Answer 1

1
$\begingroup$

Rosanswers logo

I found a way to make a very simple Qt GUI application work with ROS2. The Python file is provided below:

gui_node.py

import rclpy
import time
import sys
from threading import Thread
from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node
from std_msgs.msg import String
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QWidget, QPushButton

class GuiNode(Node):
    def __init__(self):
        super().__init__("simple_gui_node")
        self.counter_    = 0
        self.button1_publisher_  = self.create_publisher(String, '/app/button_1', 10)
        self.button2_publisher_  = self.create_publisher(String, '/app/button_2', 10)

    def button_1_publisher_callback_(self):
        self.counter_ = self.counter_ + 1
        msg = String()
        msg.data = "Button 1 pressed"
        self.button1_publisher_.publish(msg)

    def button_2_publisher_callback_(self):
        self.counter_ = self.counter_ + 1
        msg = String()
        msg.data = "Button 2 pressed"
        self.button2_publisher_.publish(msg)

    def subscriber_callback_(self, msg):
        self.get_logger().info("Button clicked, " + msg.data)

def main(args=None):
    rclpy.init(args=args)
    ros_node=GuiNode()
    executor = MultiThreadedExecutor()        print("HMI preparation complete")

        gui = QMainWindow()

        gui.setWindowTitle("Test ROS2 GUI")
        gui.setGeometry(100, 100, 240, 240)
        #gui.move(100, 100)

        value_label = 'Value'
        button1 = QPushButton("Button 1", parent=gui)
        button1.move(60, 10)
        button1.clicked.connect(ros_node.button_1_publisher_callback_)

        button2 = QPushButton("Button 2", parent=gui)
        button2.move(60, 60)
        button2.clicked.connect(ros_node.button_2_publisher_callback_)

        gui.show()
        print("HMI preparation complete")
        sys.exit(app.exec())

    
    finally:
        ros_node.get_logger().info("Shutting down")
        ros_node.destroy_node()
        executor.shutdown()
        
        #thread.join()

if __name__ == '__main__':
    main()

While this method works, but I am not satisfied with the way it has been implemented. My main concerns are the following points:

  1. The way I have it implemented now, I would have to define a publisher for each button I have in my GUI. When I add more buttons, I would have to define a lot of publishers that do more or less the same action. I would ideally want a central publisher, where I can pass on the number of the button pressed, and work from there.
  2. This might work for publishers, but I don't know how to make it work for subscriptions. The problem is that, say I have a label in my GUI, and I would want to update the text in the label with the data subscribed. I know I can update the label with label.setText(<new-value>), but I cannot figure out how to do this within the subscriber callback function (mainly because I do not know how to access the function defined in main in a separate class).
  3. What I would really appreciate is to have all the callbacks to the GUI elements in a separate class. If there is a possible to mix the functionalities of the Node class and the QMainWindow together, I would love it the most.

If you have a better implementation of the GUI, especially in the OOP sense, do feel free to post. I would love to know how that can be implemented.

EDIT :

A quick way to have the features of PyQt5 within a ROS2 node turned out to be simpler than I had initially thought. One can pass the ui object while initialising the ROS2 node as is normally done. This will allow us to access all attributes of the ui and attach them to ROS2 callbacks, as one generally does.

EDIT :

A way to have the ROS2 node executing parallely with the HMI node can be done by using the MultiThreadedExecutor as follows:

import os, sys
import rclpy
from PyQt5 import QtWidgets
from threading import Thread 
from .hmi import Ui_MainWindow
from .gui_ros_node import MyGuiNode
from rclpy.executors import MultiThreadedExecutor

def main(args=None):
    rclpy.init(args=args)
    
    app = QtWidgets.QApplication(sys.argv)
    HMI = QtWidgets.QMainWindow()
    ui  = Ui_MainWindow()
    ui.setupUi(HMI)

    hmi_node = MyGuiNode(ui)
    executor = MultiThreadedExecutor()
    executor.add_node(hmi_node)

    # Start the ROS2 node on a separate thread
    thread = Thread(target=executor.spin)
    thread.start()
    hmi_node.get_logger().info("Spinned ROS2 Node . . .")

    # Let the app running on the main thread
    try:
        HMI.show()
        sys.exit(app.exec_())

    finally:
        hmi_node.get_logger().info("Shutting down ROS2 Node . . .")
        hmi_node.destroy_node()
        executor.shutdown()


if __name__ == '__main__':
    main()

Originally posted by sampreets3 with karma: 230 on 2021-12-22

This answer was ACCEPTED on the original site

Post score: 2


Original comments

Comment by Reuben on 2022-08-19:
Hi, can you please show how you used the MultiThreadedExecutor to spin the node while using app.exec()? The above snippet initialises a executor but doesn't seem to use it

Comment by sampreets3 on 2022-08-22:
Hi, sorry I did not see your message earlier. I have not been working with the GUI for some time now, so I will start it up and post the snippet. edit : Updated the answer to include the multi threaded executor

Comment by Reuben on 2022-08-23:
I did have an error with hmi_node.show() but once I changed it to HMI.show() its worked with my GUI node too. Thank you for the prompt response and for your help!

Comment by sampreets3 on 2022-08-23:
Yeah I was a bit pressed with my work so I did not have the time to test out the piece of code. I'm happy that it worked for you! I have incorporated the edit you mentioned in my response for future references.

Comment by markg on 2022-08-26:
Thank you for this post! I am working to set this up but I'm confused about the ROS node (MyGuiNode) that you are importing. Where is this defined? Is this a ros node that also inherits a ui object?

Comment by sampreets3 on 2022-08-28:
Hi @markg, yes this is my custom ROS node that I define. In the constructor, I inherit an ui object as self.ui, and then I can get access to all the attributes (buttons, inputs, etc) and use them in my publisher and subscriber calls

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.