Help wanted: PlotJuggler for ROS2

Hi everybody,

With release 2.4.0 of PlotJuggler (freshly baked), all the features I could think of have been finally implemented.

It is time to focus now on the integration with ROS2 and DDS, but I confess I am kind of lost,

Integration with ROS1 is achieved with ros_type_introspection, that is conceptually similar to variant_topic_tools and ros_babel_fish.

I need (we?) to create something similar for ROS2 and the DDS serialization protocol, but I really don’t know where to start.

Is there any ROS2/DDS guru out there willing to contribute and work together with me to build this?

What is needed:

  1. Subscribe to a generic topic similarly to topic_tools::ShapeShifter (is this available in ROS2?)
  2. We need to know the IDL at run-time and from rosbags.
  3. Given the raw blob and the IDL, the deserialization must be done without any code generated header (before I start from scratch, maybe some library might help me do that?)
9 Likes

I’ll do my best to answer questions and give guidance, though I don’t have lots of time to dedicate to this at the moment.

You can get the raw data buffer for a received message (untyped) already, see:

This is the interface that rosbag2 uses (sort of), the wrinkle being that you still need to create a typed subscription so that doesn’t completely satisfy your needs. There is a “generic” subscription in the rosbag2 sources which accomplishes this, but has some drawbacks:

The drawback is (and @karsten please correct me if I’m wrong) that the type support for the type you’re subscribing to is available on the local system. Which means you still need the message package for the message locally on your machine. This isn’t how the other things you linked to from ROS 1 work (AFAIK), as they instead do runtime deserialization if needed. Whereas this solution from rosbag2 dynamically loads the appropriate type support at runtime, but the code that does the deserialization is still compiled at some point. You can see how that works here:

We do want a completely runtime based (de)serialization API, but we don’t have that exactly yet. We do have something like it with our “type support introspection” which is a special type support used by some of the rmw implementations to do runtime (de)serialization rather than generating ROS specific code to do it. This type support exposes to you member information like so (sensor_msgs/msg/Image as an example):

static const ::rosidl_typesupport_introspection_cpp::MessageMember Image_message_member_array[7] = {
  {
    "header",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_MESSAGE,  // type
    0,  // upper bound of string
    ::rosidl_typesupport_introspection_cpp::get_message_type_support_handle<std_msgs::msg::Header>(),  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, header),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "height",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_UINT32,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, height),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "width",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_UINT32,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, width),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "encoding",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_STRING,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, encoding),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "is_bigendian",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_UINT8,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, is_bigendian),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "step",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_UINT32,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    false,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, step),  // bytes offset in struct
    nullptr,  // default value
    nullptr,  // size() function pointer
    nullptr,  // get_const(index) function pointer
    nullptr,  // get(index) function pointer
    NULL  // resize(index) function pointer
  },
  {
    "data",  // name
    ::rosidl_typesupport_introspection_cpp::ROS_TYPE_UINT8,  // type
    0,  // upper bound of string
    NULL,  // members of sub message
    true,  // is array
    0,  // array size
    false,  // is upper bound
    offsetof(sensor_msgs::msg::Image, data),  // bytes offset in struct
    nullptr,  // default value
    size_function__Image__data,  // size() function pointer
    get_const_function__Image__data,  // get_const(index) function pointer
    get_function__Image__data,  // get(index) function pointer
    resize_function__Image__data  // resize(index) function pointer
  }
};

With this information a specific rmw implementation can do serialization for a specific format (e.g. CDR which is the binary format used by DDS), see rmw fastrtps “dynamic” using it:

What’s missing for true runtime deserialization is being able to generate this information using only a runtime string as input. That currently doesn’t exist, even in Python (though the parts are there since the msg/idl parsing is written in Python), but we do want that in the future too.

Hopefully that gives you a bit to think about and some starting places for discussion if not implementation.

2 Likes

Hopefully, I might be able to implement that myself :wink:

Thank you for the help.

1 Like

Hi Davide,

Last summer I created a plugin to read ros2 bags in PlotJuggler.
I never got around to finish it completely, but it worked good enough for us to analyze our ros2 bags.
It is still missing some important features, like :

  • a cmakelists.txt and package.xml that works for both ROS1 and ROS2
  • arrays / strings support
  • support for other serialization formats (for the moment sqlite3 & CDR is hardcoded)
  • plot real-time data (only bags are supported at the moment)

Also because in ros2-bags don’t contain the .msg file data (unlike ROS1), you have to source the workspace containing the compiled .msg files :confused:

Anyway you can take a look at the code here : https://github.com/1r0b1n0/PlotJuggler/tree/ros2/plugins/ROS2/DataLoadROS

1 Like

This is great! I am sure I can use this initial work to get more familiar with ROS2 myself and CDR!

Awesome.

I’ve done some work on getting a ROS2 DataStreamer plugin implemented for PlotJuggler based on the great information above. It’s far from complete, but I’ve got the basic functionality there: creating a ROS2 node in a separate thread, subscribing to topics, pulling data out via introspection, and plotting. I’d be interested in some feedback on what you would like to see in order to accept a pull request, assuming you haven’t already been working on this yourself.

As stated by others above, a workspace containing the compiled .msg files for messages you want to view must be sourced in order to have introspection. These are not needed at compile time though (the libraries are dynamically loaded), and as far as I know the same restriction applies for all the other ROS2 tools (e.g. the standard rqt plugins).

You can see what I’ve done so far here:

Main things that still need to be done from my point of view:

  • Support for array members
  • Ability to use header/timestamp data
  • Ability to save data to a rosbag
  • Plugin state/settings save/load functionality
  • Performance improvements
  • Code documentation and cleanup
  • A lot more testing
1 Like

that is awesome, I will have a look at it

image

Thanks to everybody, in particular to @tjpotts and @wjwwood.

The experimental support includes both robag2 and streaming from live messages.

Have a look to version 2.7.x

12 Likes