Qt libraries with ROS
Installation
This is a tutorial for installation of Qt 5.7.0 to Ubuntu 20.04. It may be used for some newer versions of Qt and Ubuntu.
- Install Qt 5:
sudo apt install build-essential qtcreator qt5-default qtbase5-examples
- Install Qt to Python converter:
sudo apt install pyqt5-dev-tools
If error: g++: Command not found:
sudo apt-get purge build-essential
sudo apt-get install build-essential
If error: GL/gl.h: No such file or directory
sudo apt-get install mesa-common-dev
Tutorial start
In this tutorial, we will create a GUI package and interact with ROS nodes and messages via this package.
Create a ROS package
Since in this tutorial our aim is to use the Qt libraries together with ROS, we will have eveything in our ros2_ws
. However, please remember that you don’t need to have ROS to use the Qt libraries in any Python script. For the sake of the tutorial, we will learn the usage of Qt in a ROS scenario - but the steps would be the same for another scenarios, as well.
You need to have your files in your ros2_ws
. Therefore, we start by creating a new ROS package for this tutorial.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --node-name my_gui_pkg my_gui_pkg
cd ..
colcon build
cd ~/ros2_ws/src/my_gui_pkg
mkdir ui
Design your widget/app
- Open Qt Designer
- Select Widget and default screen size
- Press create
- Select two “Push Button”s and one “Plain Text Edit”, and place them somewhere in the Widget window.
- Change their name something nice, and give a representative class names.
- Save by hitting Ctrl+s
- Select the “ui” folder in your ROS package.
- Change the file name to “main.ui”
Connecting UI and ROS
There are four steps:
- Converting UI to Python script.
- We will create (or copy) a publisher for test purposes. We will start this publisher with “Run ROS node” button.
- (Maybe) We will set a value a for an action with “Set action” button by reading from the plain text edit.
Converting UI to Python script
- Go to the direction where your “main.ui” is:
cd ~/ros2_ws/src/my_gui_pkg/ui
- Convert the UI file into a Python file:
pyuic5 -x main.ui -o main.py
Note that whenever you change something in your design, you must convert it into the Python file.
-
Open VS code and find your package in the explorer. Add this line at the top:
#!/usr/bin/env python3
. At this point, if you run the script via VS code, you should see your widget app :) -
Move your
main.py
intomy_gui_pkg/my_gui_pkg
, where you would normally place all your ROS nodes. -
Since
main.py
is a simple Python class, we can just import it like this:from main import Ui_Form as Main_Widget
. To do that, we will usemy_gui_pkg.py
. Copy the codes below inmy_gui_pkg.py
.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
import subprocess
I know there seem to be redundant imports but I find it easier to use it like that. Feel free to optimize it if it hurts your brain.
- Since we don’t want to modify the code that is generated by the
pyuc5
, we will actually make a subclass from what we have inmain.py
. It will look like this:class GUI_Window(QWidget, Main_Widget): def __init__(self, parent=None): super(GUI_Window, self).__init__(parent) self.setupUi(self) # self.resize(854, 480) self.setWindowTitle("My GUI app") self.show()
- And finally we will add these lines so that we can run the script as we press the play button.
if __name__ == '__main__': app = QApplication(sys.argv) w = GUI_Window() sys.exit(app.exec_())
Creating necessary nodes:
-
You can use any ROS node for this step. I will just copy paste
my_publisher.py
from the previous tutorials into my_gui_pkg/my_gui_pkg. It is a simple hello world publisher. Make sure that you do the necessary changes insetup.py
. -
We need to create a function that will start a subprocess of
ros2 run my_gui_pkg my_publisher
. Additionally, we need to connect this function with our button click. To do that we need this code inside the class constructor afterself.setWindowTitle("My GUI app")
.
self.rosNodeBtn.clicked.connect(self.start_ros_node)
self.my_publisher_proc = None
The self.rosNodeBtn
is the object taken from our UI class. You can reach each object in your UI like this.
- Now it is time to define the function. Copy the code after the class constructior and before
if __name__ == __main__:
def start_ros_node(self):
if self.my_publisher_proc == None: # because you don't want to start several processes in each click
self.my_publisher_proc = subprocess.Popen(["ros2", "run", "my_gui_pkg", "my_publisher"], text=True)
else:
print("The node is already exist")
Pay attention how similar subprocess.Popen syntax with the regular terminal command.
You will experince lots of simultaneously running processes of the same type. You need to pay attention that you actually kill the ROS node process as you press exit button.
def kill_processes(self):
# This is to kill rosnodes as you close the windiw
try:
self.my_publisher_proc.kill()
except Exception as e:
print(e)
def closeEvent(self, event):
self.kill_processes()
event.accept() # let the window close
Don’t forget to colcon build
before you test it. Otherwise, you will get the error No executable found
.
Setting an action with the text edit data
(NOT COMPLETE) (Note, sorry that I haven’t had enough time to modify it into an action server exercise, but I hope passing the data into a publisher will give you enough idea to implement it when/if you need to do it with action server/client)
The method with subprocess is easy, rather clean and effective. However, sometimes you want to interact the data that you have an access through the UI. You can very well set a ROS parameter with this data before you start the relevant node, and use this ROS parameter inside this node.
But I have a better idea.
- Let’s start adding necessary lines for this task into the class construstor:
## Set Action button
self.msg_data = None
self.setActionBtn.clicked.connect(self.set_action)
self.my_action_proc = None
- Create a new
set_action(self)
function in this class. For now, put a dummy data to see that we can actually ready the TextEdit data correctly.
def set_action(self):
self.msg_data = self.actionTextEdit.toPlainText() # we read the data
print(self.msg_data)
If you run the script, you should be able to see what you write in the text edit as you press the “set action” button.
Extra
While I was preparing this tutorial, I found some extra resources that might be helpful. I list them here: