0
$\begingroup$

Rosanswers logo

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 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.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
        with self.gui_lock:
            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):
        with self.gui_lock:
            try:
                self.s_run(stdsrvs.SetBoolRequest(True))
            except rospy.service.ServiceException as e:
                print(e.message)

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

    def _clock_sub(self, msg):
        if self.gui_lock.locked():
            rospy.logdebug('{}: Lock locked, dropping clock '
                           'update.'.format(rospy.get_name()))
            return
        with self.gui_lock:
            clock = msg.clock.to_sec()
            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

And the .ui file:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SimControl</class>
 <widget class="QWidget" name="SimControl">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>188</width>
    <height>126</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Simulation Control</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout_3">
   <item>
    <layout class="QVBoxLayout" name="verticalLayout_2">
     <item>
      <widget class="QLabel" name="label_2">
       <property name="text">
        <string>Simulation Control</string>
       </property>
      </widget>
     </item>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout">
       <item>
        <widget class="QPushButton" name="pause_button">
         <property name="text">
          <string>Pause</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="run_button">
         <property name="text">
          <string>Run</string>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
   <item>
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <widget class="QLabel" name="label">
       <property name="frameShadow">
        <enum>QFrame::Plain</enum>
       </property>
       <property name="text">
        <string>Simulated clock</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLineEdit" name="clock_field">
       <property name="palette">
        <palette>
         <active>
          <colorrole role="Base">
           <brush brushstyle="SolidPattern">
            <color alpha="255">
             <red>186</red>
             <green>189</green>
             <blue>182</blue>
            </color>
           </brush>
          </colorrole>
         </active>
         <inactive>
          <colorrole role="Base">
           <brush brushstyle="SolidPattern">
            <color alpha="255">
             <red>186</red>
             <green>189</green>
             <blue>182</blue>
            </color>
           </brush>
          </colorrole>
         </inactive>
         <disabled>
          <colorrole role="Base">
           <brush brushstyle="SolidPattern">
            <color alpha="255">
             <red>239</red>xcept rospy.service.ServiceException as e:
                print(e.message)

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

    def _clock_sub(self, msg):
        if self.gui_lock.locked():
            rospy.logdebug('{}: Lock locked, dropping clock '
                           'update.'.format(rospy.get_name()))
            return
        with self.gui_lock:
            clock = msg.clock.to_sec()
            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

             <green>235</green>
             <blue>231</blue>
            </color>
           </brush>
          </colorrole>
         </disabled>
        </palette>
       </property>
       <property name="readOnly">
        <bool>true</bool>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

Originally posted by bjoernolav on ROS Answers with karma: 45 on 2020-04-28

Post score: 0

$\endgroup$

1 Answer 1

0
$\begingroup$

Rosanswers logo

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

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/Writing%20a%20Python%20Plugin#Using_rospy


Originally posted by Dirk Thomas with karma: 16276 on 2020-04-28

This answer was ACCEPTED on the original site

Post score: 0


Original comments

Comment by bjoernolav on 2020-04-29:
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.

Comment by Dirk Thomas on 2020-04-29:
In some cases the slot decorator is options: see https://stackoverflow.com/questions/14421897/is-the-pyside-slot-decorator-necessary

$\endgroup$

Your Answer

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