(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Create an rqt_bag Plugin

Description: Create a custom visualization for rqt_bag

Keywords: rqt_bag bag data rqt

Tutorial Level: INTERMEDIATE

Let's say you have bags of data and you want to be able to visualize them. rqt_bag gives you the ability to scroll through the recorded messages and visualize the raw message values. However, often may want something more visual, or to do some post-processing on the raw message. For that, you can write an rqt_bag plugin, using the proof-of-concept Python plugin system. That way you can go from a simple visualization of the messages...

base.png

to something like this:

full.png

Package Setup

We're going to create a package called rqt_bag_diagnostics_demo. Insert the following into your package.xml.

   1     <depend>diagnostic_msgs</depend>
   2     <depend>rqt_bag</depend>
   3     <export>
   4       <rqt_bag plugin="${prefix}/plugins.xml"/>
   5     </export>

Since we'll be making a python library, we'll need the standard Python setup. In your CMake

catkin_python_setup()

In setup.py

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup

package_info = generate_distutils_setup(
    packages=['rqt_bag_diagnostics_demo'],
    package_dir={'': 'src'}
)

setup(**package_info)

Finally, we're going to define the plugin in an xml file called plugins.xml (as referenced in the package.xml)

   1 <library path="src">
   2   <class name="DiagnosticBagPlugin"
   3          type="rqt_bag_diagnostics_demo.the_plugin.DiagnosticBagPlugin"
   4          base_class_type="rqt_bag::Plugin">
   5     <description>
   6     </description>
   7   </class>
   8 </library>

The name is the name of the class we'll create. The type is the way we would import the class in Python, i.e. package_name.name_of_file.class_name

Defining the Plugin

As with all Python libraries, we'll make sure a (blank) src/package_name/__init__.py file exists. As referenced in the plugins.xml, all the code that follows will be in src/rqt_bag_diagnostics_demo/the_plugin.py.

First, the core Plugin class.

   1 from rqt_bag.plugins.plugin import Plugin
   2 from python_qt_binding.QtCore import Qt
   3 from diagnostic_msgs.msg import DiagnosticStatus
   4 
   5 
   6 def get_color(diagnostic):
   7     if diagnostic.level == DiagnosticStatus.OK:
   8         return Qt.green
   9     elif diagnostic.level == DiagnosticStatus.WARN:
  10         return Qt.yellow
  11     else:  # ERROR or STALE
  12         return Qt.red
  13 
  14 
  15 class DiagnosticBagPlugin(Plugin):
  16     def __init__(self):
  17         pass
  18 
  19     def get_view_class(self):
  20         return None
  21 
  22     def get_renderer_class(self):
  23         return None
  24 
  25     def get_message_types(self):
  26         return ['diagnostic_msgs/DiagnosticStatus']

Here we have some basic imports, and helper function that we'll use later, and a class that defines the three parts of an rqt_bag plugin.

  1. view_class - a.k.a. TopicMessageView - A separate panel that can be used for viewing individual messages.

  2. renderer_class - a.k.a. TimelineView - A tool for drawing onto the timeline view of the bag data.

  3. message_types - An array of strings that define what message types this plugin can be used for. You can return ['*'] for it to apply to all messages.

Since we return None for the first two methods, this plugin won't do anything. We'll tackle each of these separately.

TopicMessageView

Version 1

We're going to create a class that extends the TopicMessageView class.

   1 class DiagnosticPanel(TopicMessageView):
   2     name = 'Awesome Diagnostic'
   3 
   4     def message_viewed(self, bag, msg_details):
   5         super(DiagnosticPanel, self).message_viewed(bag, msg_details)
   6         t, msg, topic = msg_details
   7         print msg

Here we define two things. The name string defines what we'll see in the menu of rqt_bag. The message_viewed method defines what to do when the message is selected. So here, we'll just print the message to terminal for now.

We need to hook this class we've created into the plugin infrastructure, and for that, we return the class object itself in the get_view_class method.

   1     def get_view_class(self):
   2         return DiagnosticPanel

To see this in action, open up the provided bag file, and right click on the diagnostic track. It will give you three options under the "View": Raw, Plot and our "Awesome Diagnostic." Clicking this should open a panel and you can scroll through the messages and watch them print. panel.png

Version 2

TopicMessageView is itself an extension of a QObject. There's lots of things you could do with this using all the might and power of Qt. This is not a python Qt tutorial sadly. So we're going to just add a simple QWidget and draw on it.

   1 class DiagnosticPanel(TopicMessageView):
   2     name = 'Awesome Diagnostic'
   3 
   4     def __init__(self, timeline, parent, topic):
   5         super(DiagnosticPanel, self).__init__(timeline, parent, topic)
   6         self.widget = QWidget()
   7         parent.layout().addWidget(self.widget)
   8         self.msg = None
   9         self.widget.paintEvent = self.paintEvent
  10 
  11     def message_viewed(self, bag, msg_details):
  12         super(DiagnosticPanel, self).message_viewed(bag, msg_details)
  13         _, self.msg, _ = msg_details
  14         self.widget.update()
  15 
  16     def paintEvent(self, event):
  17         self.qp = QPainter()
  18         self.qp.begin(self.widget)
  19 
  20         rect = event.rect()
  21 
  22         if self.msg is None:
  23             self.qp.fillRect(0, 0, rect.width(), rect.height(), Qt.white)
  24         else:
  25             color = get_color(self.msg)
  26             self.qp.setBrush(QBrush(color))
  27             self.qp.drawEllipse(0, 0, rect.width(), rect.height())

In the constructor, we create a QWidget and override its paintEvent method. Now when we get a message with message_viewed, we save it, and update the widget, which will in turn call our paintEvent. Before a message is selected, we'll just paint a white rectangle. Otherwise, we'll draw a circle, using our handy helper method to relate the color to what level the diagnostic is at. circle.png

TimelineRenderer

Version 1

To draw on the timeline, we extend the TimelineRenderer class.

   1 class DiagnosticTimeline(TimelineRenderer):
   2     def __init__(self, timeline, height=80):
   3         TimelineRenderer.__init__(self, timeline, msg_combine_px=height)
   4 
   5     def draw_timeline_segment(self, painter, topic, start, end, x, y, width, height):
   6         painter.setBrush(QBrush(Qt.blue))
   7         painter.drawRect(x, y, width, height)

You can customize how tall the message's portion of the timeline is with the msg_combine_px parameter. The key method to override is the draw_timeline_segment method which gives you potions of the timeline to draw. For now we'll just draw blue rectangles on each segment.

Just like the message view, you also have to edit the plugin to return your class.

   1     def get_renderer_class(self):
   2         return DiagnosticTimeline

To view this, you have to enable "Thumbnails" (a misleading name) in the rqt_bag gui.

  • blue.png

Version 2

Okay, now we actually want to customize how the messages are drawn in the timeline based on the messages themselves. For that, there's a wonky bunch of magical incantations you need to read the messages out of the bag files.

   1     def draw_timeline_segment(self, painter, topic, start, end, x, y, width, height):
   2         bag_timeline = self.timeline.scene()
   3         for bag, entry in bag_timeline.get_entries_with_bags([topic], rospy.Time(start), rospy.Time(end)):
   4             topic, msg, t = bag_timeline.read_message(bag, entry.position)
   5             color = get_color(msg)
   6             painter.setBrush(QBrush(color))
   7             painter.setPen(QPen(color, 5))
   8 
   9             p_x = self.timeline.map_stamp_to_x(t.to_sec())
  10             painter.drawLine(p_x, y, p_x, y+height)

Using the topic, start and end parameters of the method, we can get the bag entries that correspond with this segment of the timeline. We can then get the actual message and use it to draw. Here we are drawing a line based on the level of the diagnostic message. We can automatically figure out where to draw the message horizontally using the map_stamp_to_x method.

timeline.png

The alternative to this weird way of accessing the messages is to Timeline Cache like the ImageTimelineViewer does, but figuring that out is left as an exercise to the reader.

Wiki: rqt_bag/Tutorials/Create an rqt_bag plugin (last edited 2018-02-05 17:47:54 by DavidLu)