Ask Your Question
0

Random segfaults for python rqt plugin

asked 2020-04-28 02:44:23 -0500

bjoernolav gravatar image

updated 2020-04-28 02:48:27 -0500

I've tried to write a couple of rqt plugins in order to make interaction with our ROS system easier. I'm not confident or familiar with QT (or C++) from beforehand, so I wrote the plugins with basis in the rqt-plugin tutorials.

One of the plugins is a used for controlling a simulation. This interacts with a node controlling a simulated clock using a service, and echoes the simulated clock to a QLineEdit. The plugin works well, but randomly exits with segfault. This seems to happen more often when my system has been running for a while. If I restart the plugin after it segfaults, it may segfault straight away, or run for an extended time period. After a restart, the plugin runs for a long time before I experience problems.

Some thought on what may cause this, without being able to fix it:

  • Python garbage collection: I understand that python may garbage collect QT objects that have gone out of scope in Python, while the objects are still referenced/used in C++. I've made all my QT objects attributes of the plugin class to tackle this.
  • The clock-topic is published at a high rate (100Hz). I figured that it may be a problem that the clock-callback got "stuck" if the plugin was occupied in a service call. I tried adding a lock to ignore received clock topics in such cases, without it seeming to have an effect.

I've run the plugin both by launching rqt and loading it (using python 2 and PyQt5), and running it standalone (using python 3 and PySide2). The problem occurs for both approaches.

I'm running Meldic Morenia on ubuntu 18.04 with kernel 5.3.0-45-generic. All inputs or suggestions are highly appreciated!

The plugin code:

import os
import rospy
import rospkg

from qt_gui.plugin import Plugin
from python_qt_binding import loadUi
from python_qt_binding.QtWidgets import QWidget
from threading import Lock

import std_srvs.srv as stdsrvs
from rosgraph_msgs.msg import Clock


class SimControl(Plugin):
    gui_lock = Lock()

    def __init__(self, context):
        super(SimControl, self).__init__(context)
        # Give QObjects reasonable names
        self.setObjectName('SimControl')

        # Process standalone plugin command-line arguments
        from argparse import ArgumentParser
        parser = ArgumentParser()
        # Add argument(s) to the parser.
        parser.add_argument("-q", "--quiet", action="store_true",
                            dest="quiet",
                            help="Put plugin in silent mode")
        args, unknowns = parser.parse_known_args(context.argv())
        if not args.quiet:
            print('arguments: ', args)
            print('unknowns: ', unknowns)

        # Create QWidget
        self._widget = QWidget()
        # Get path to UI file which should be in the "resource" folder of this package
        ui_file = os.path.join(rospkg.RosPack().get_path('ros_af_sim'),
                               'resource', 'sim_control.ui')
        # Extend the widget with all attributes and children from UI file
        loadUi(ui_file, self._widget)
        # Give QObjects reasonable names
        self._widget.setObjectName('SimControlUi')
        # Show _widget.windowTitle on left-top of each plugin (when
        # it's set in _widget). This is useful when you open multiple
        # plugins at once. Also if you open multiple instances of your
        # plugin at once, these lines add number to make it easy to
        # tell from pane to ...
(more)
edit retag flag offensive close merge delete

2 Answers

Sort by ยป oldest newest most voted
0

answered 2020-04-28 14:31:48 -0500

Dirk Thomas gravatar image

Please see the Qt documentation about the gui thread and worked threads: https://doc.qt.io/qt-5/thread-basics....

Your custom gui_lock isn't sufficient. You simply can't directly manipulate widgets in a ROS callback (which is being invoked on a different thread).

Also see the rqt documentation which mentions this: http://wiki.ros.org/rqt/Tutorials/Wri...

edit flag offensive delete link more

Comments

Thank you very much for the answer! Using a slot for updating the QLineEdit widget, and emitting a signal in the ROS callback function solved the problem.

A follow-up question: In the Qt for Python tutorial on signals/slots, slots are defined with a @slot() decorator. I noticed that this is not done in e.g. the bag plugin, where the _handle_play_clicked function is connected to a signal. Is there a reason for this? I've done it similarly in my plugin - it would be nice to verify that it's okay to do so.

bjoernolav gravatar image bjoernolav  ( 2020-04-29 06:24:17 -0500 )edit

In some cases the slotdecorator is options: see https://stackoverflow.com/questions/1...

Dirk Thomas gravatar image Dirk Thomas  ( 2020-04-29 12:51:05 -0500 )edit
0

answered 2020-04-29 06:32:25 -0500

bjoernolav gravatar image

updated 2020-04-29 10:45:29 -0500

For reference in case other people are interested: The complete code for the working plugin.

import os
import rospy
import rospkg

from qt_gui.plugin import Plugin
from python_qt_binding import loadUi
from python_qt_binding.QtWidgets import QWidget
from python_qt_binding.QtCore import Signal, Slot

import std_srvs.srv as stdsrvs
from rosgraph_msgs.msg import Clock


class SimControl(Plugin):
    update_clock_signal = Signal(float)  # Update clock signal, has to be defined as class attribute

    def __init__(self, context):
        super(SimControl, self).__init__(context)
        # Give QObjects reasonable names
        self.setObjectName('SimControl')

        # Process standalone plugin command-line arguments
        from argparse import ArgumentParser
        parser = ArgumentParser()
        # Add argument(s) to the parser.
        parser.add_argument("-q", "--quiet", action="store_true",
                            dest="quiet",
                            help="Put plugin in silent mode")
        args, unknowns = parser.parse_known_args(context.argv())
        if not args.quiet:
            print('arguments: ', args)
            print('unknowns: ', unknowns)

        # Create QWidget
        self._widget = QWidget()
        # Get path to UI file which should be in the "resource" folder of this package
        ui_file = os.path.join(rospkg.RosPack().get_path('ros_af_sim'),
                               'resource', 'sim_control.ui')
        # Extend the widget with all attributes and children from UI file
        loadUi(ui_file, self._widget)
        # Give QObjects reasonable names
        self._widget.setObjectName('SimControlUi')
        # Show _widget.windowTitle on left-top of each plugin (when
        # it's set in _widget). This is useful when you open multiple
        # plugins at once. Also if you open multiple instances of your
        # plugin at once, these lines add number to make it easy to
        # tell from pane to pane.
        if context.serial_number() > 1:
            self._widget.setWindowTitle(self._widget.windowTitle() + (
                        ' (%d)' % context.serial_number()))

        # Set up handlers
        run_service = '/sim_control/run'
        print('Waiting for services')
        rospy.wait_for_service(run_service, timeout=5.0)
        self.s_run = rospy.ServiceProxy(run_service, stdsrvs.SetBool)

        self._widget.run_button.clicked[bool].connect(self._handle_run_clicked)
        self._widget.pause_button.clicked[bool].connect(
            self._handle_pause_clicked)

        self.update_clock_signal.connect(self._update_clock)  # Connect GUI updater to signal
        self.clock_sub = rospy.Subscriber('/clock', Clock, self._clock_sub)
        print("Finished setup")

        # Add widget to the user interface
        context.add_widget(self._widget)

    def shutdown_plugin(self):
        # Unregister all publishers here
        self.clock_sub.unregister()
        self.s_run.close()

    def save_settings(self, plugin_settings, instance_settings):
        # TODO save intrinsic configuration, usually using:
        # instance_settings.set_value(k, v)
        pass

    def restore_settings(self, plugin_settings, instance_settings):
        # TODO restore intrinsic configuration, usually using:
        # v = instance_settings.value(k)
        pass

    def _handle_run_clicked(self):
        try:
            self.s_run(stdsrvs.SetBoolRequest(True))
        except rospy.service.ServiceException as e:
            print(e.message)

    def _handle_pause_clicked(self):
        try:
            self.s_run(stdsrvs.SetBoolRequest(False))
        except rospy.service.ServiceException as e:
            print(e.message)

    def _clock_sub(self, msg):
        clock = msg.clock.to_sec()
        self.update_clock_signal.emit(clock)  # Emit signal

    @Slot(float)
    def _update_clock(self, clock):
        self._widget.clock_field.setText('{}'.format(clock))

    # def trigger_configuration(self):
    # Comment in to signal that the plugin has a way to configure
    # This will enable a setting button (gear icon) in each dock widget title bar
    # Usually used to open a modal configuration dialog
edit flag offensive delete link more

Your Answer

Please start posting anonymously - your entry will be published after you log in or create a new account.

Add Answer

Question Tools

1 follower

Stats

Asked: 2020-04-28 02:44:23 -0500

Seen: 118 times

Last updated: Apr 29 '20