How can I create a simple GUI using an RQT plugin?

181 Views Asked by At

I'm new to ROS 2, and I'm trying to create a simple GUI using an RQT plugin. To do this I'm following along with the process described in this guide:

https://robobe.github.io/blog/ROS2/rqt/custom_plugin/step1/

Please note that I'm using ros2 iron with Python 3.10.12, and the operating system is Linux Mint.

Right now, the project structure is as follows:

~/ros2_custom_rqt_plugin $ tree
.
└── src
    └── rqt_mypkg
        ├── LICENSE
        ├── package.xml
        ├── plugin.xml
        ├── resource
        │   └── rqt_mypkg
        ├── rqt_mypkg
        │   ├── __init__.py
        │   └── my_module.py
        ├── setup.cfg
        ├── setup.py
        └── test
            ├── test_copyright.py
            ├── test_flake8.py
            └── test_pep257.py

5 directories, 11 files

In the my_module.py file, I have the following:

#!/usr/bin/env python3

# File: rqt_mypkg/rqt_mypkg/my_module.py

from rqt_gui_py.plugin import Plugin
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel

class MyPlugin(Plugin):
    def __init__(self, context):
        super(MyPlugin, self).__init__(context)
        self.setObjectName('MyPlugin')

        # GUI initialization code 
        self._widget = MyWidget()
        self._widget.setObjectName('UniqueWidget') 
        self.setWidget(self._widget)

class MyWidget(QWidget):
    def __init__(self):
        super(MyWidget, self).__init__()

        # Create a layout
        layout = QVBoxLayout(self)

        # Create a button
        self.button = QPushButton('Click Me!', self)
        self.button.clicked.connect(self.on_button_click)

        # Create a label for text display
        self.label = QLabel('Hello, World!', self)

        # Add the button and label to the layout
        layout.addWidget(self.button)
        layout.addWidget(self.label)

    def on_button_click(self):
        self.label.setText('Button Clicked!')

In this code, the MyPlugin class is supposed to inherit from rqt_gui_py.Plugin and create an instance of the MyWidget class.

In the package.xml file, I have:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>rqt_mypkg</name>
  <version>0.0.0</version>
  <description>My RQT plugin</description>
  <maintainer email="[email protected]">Sam</maintainer>
  <license>Apache License 2.0</license>

  <depend>rclpy</depend>
  <depend>rqt_gui</depend>
  <depend>rqt_gui_py</depend>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
    <rqt_gui plugin="${prefix}/plugin.xml"/>
  </export>
</package>

Staying consistent with the linked guide, I've added the line <rqt_gui plugin="${prefix}/plugin.xml"/> inside the <export> tag.

In plugin.xml I have:

<library path="src">
  <class name="My Plugin" type="rqt_mypkg.my_module.MyPlugin" base_class_type="rqt_gui_py::Plugin">
    <description>
      An example Python GUI plugin to create a great user interface.
    </description>
    <qtgui>
      <group>
        <label>Visualization</label>
      </group>
      <label>My first Python Plugin</label>
      <icon type="theme">system-help</icon>
      <statustip>Great user interface to provide real value.</statustip>
    </qtgui>
  </class>
</library>

This code is exactly like the example provided in the linked guide.

In setup.py I have:

from setuptools import find_packages, setup

package_name = 'rqt_mypkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml', 'plugin.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='sam',
    maintainer_email='[email protected]',
    description='TODO: Package description',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
        ],
    },
)

Notably, I've modified the data_files section to include plugin.xml

To build the project and load the plugin, I tried executing the following:

~/ros2_custom_rqt_plugin $ colcon build
~/ros2_custom_rqt_plugin $ source install/setup.bash 
~/ros2_custom_rqt_plugin $ rqt --force-discover --standalone rqt_mypkg

The project appears to build without issues, but when I try to load the plugin I get the following:

~/ros2_custom_rqt_plugin $ rqt --force-discover --standalone rqt_mypkg
PluginManager._load_plugin() could not load plugin "rqt_mypkg/My Plugin":
Traceback (most recent call last):
  File "/opt/ros/iron/lib/python3.10/site-packages/qt_gui/plugin_handler.py", line 102, in load
    self._load()
  File "/opt/ros/iron/lib/python3.10/site-packages/qt_gui/plugin_handler_direct.py", line 55, in _load
    self._plugin = self._plugin_provider.load(self._instance_id.plugin_id, self._context)
  File "/opt/ros/iron/lib/python3.10/site-packages/qt_gui/composite_plugin_provider.py", line 72, in load
    instance = plugin_provider.load(plugin_id, plugin_context)
  File "/opt/ros/iron/lib/python3.10/site-packages/qt_gui/composite_plugin_provider.py", line 72, in load
    instance = plugin_provider.load(plugin_id, plugin_context)
  File "/opt/ros/iron/lib/python3.10/site-packages/rqt_gui_py/ros_py_plugin_provider.py", line 69, in load
    return super(RosPyPluginProvider, self).load(plugin_id, ros_plugin_context)
  File "/opt/ros/iron/lib/python3.10/site-packages/qt_gui/composite_plugin_provider.py", line 72, in load
    instance = plugin_provider.load(plugin_id, plugin_context)
  File "/opt/ros/iron/lib/python3.10/site-packages/rqt_gui/ros_plugin_provider.py", line 107, in load
    return class_ref(plugin_context)
  File "/home/sam/ros2_custom_rqt_plugin/install/rqt_mypkg/lib/python3.10/site-packages/rqt_mypkg/my_module.py", line 16, in __init__
    self.setWidget(self._widget)
AttributeError: 'MyPlugin' object has no attribute 'setWidget'

Failed to delete datawriter, at ./src/publisher.cpp:45 during '__function__'
Warning: class_loader.ClassLoader: SEVERE WARNING!!! Attempting to unload library while objects created by this loader exist in the heap! You should delete your objects before attempting to unload the library or destroying the ClassLoader. The library will NOT be unloaded.
         at line 127 in ./src/class_loader.cpp

From this message, it looks like the plugin couldn't be loaded due to an AttributeError:

AttributeError: 'MyPlugin' object has no attribute 'setWidget'

I had thought that the setWidget attribute was part of rqt_gui_py.plugin, so the MyPlugin class should be able to access it. What changes can I make to load the plugin correctly?

1

There are 1 best solutions below

0
MiserlyMark On

UPDATE: I was able to get the plugin working by putting the following in my_modules.py

#!/usr/bin/env python3

# File: rqt_mypkg/rqt_mypkg/my_module.py

from rqt_gui_py.plugin import Plugin
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel

class MyPlugin(Plugin):
    def __init__(self, context):
        super(MyPlugin, self).__init__(context)
        self.setObjectName('MyPlugin')

        # GUI initialization code 
        self._widget = MyWidget()
        print("MyWidget instance created")

        self._widget.show()
        
       
class MyWidget(QWidget):
    def __init__(self):
        super(MyWidget, self).__init__()

        print("MyWidget constructor called...")

        self.setStyleSheet("background-color: white;")

        # Create a layout
        layout = QVBoxLayout(self)

        # Create a button
        self.button = QPushButton('Click Me!', self)
        self.button.clicked.connect(self.on_button_click)

        # Create a label for text display
        self.label = QLabel('Hello, World!', self)

        # Add the button and label to the layout
        layout.addWidget(self.button)
        layout.addWidget(self.label)

         # Set the layout for the widget
        self.setLayout(layout)

    def on_button_click(self):
        self.label.setText('Button Clicked!')