Interaction Improvements

Keywords: #ros2 #ornis #tui

One of the earliest requirements I laid out for ORNIS was the ability to send/recieve service calls. While the ‘ros2 service’ interface may be convenient, it is cumbersome to say the least. I have had countless issues with attempting to fill out larger service requests via the CLI, only to accidently mess up the indentation. The resulting failure to request always strikes a nerve.

Ironically, my initial implementation of the service interaction had the exact same issue. Representing the message as a string (and keeping it that way for editing) is a fast and simple solution. Now that I’ve verified that the fundamentals work, I went and replaced the string representation with a tree data format. In this post, I’ll discuss some of the interesting things I learned while figuring it all out.

Wait, isn’t ORNIS written in C++?

Yes! Some of you may already see the difficulty with ORNIS attempting to send/recieve service calls at the user’s request at run-time. If you are one of those people: Holy shit, wow you’re smart. Your neck must be sore from keeping that massive head of yours upright! I need to learn from you, because goddamn I didn’t see the issue until WAY too late.

If you’re NOT one of those people, I invite you to have a quick look at the Ros2 Writing a simple service and client tutorial. Things get interesting when you see that the Create client function is templated. This means that we need to know what the service message type is at compile time. Right, this is a bit of an issue! I obviously knew this when I started with ornis, but I just assumed there would be some super simple work-around in the API that I could use instead of the primary create_client() function. Unfortuanately (or fortuanately, depending on how much you enjoy this post) this was not the case.

After realising this, It took me a day or two before I stumbled upon ros2_introspection package by Davide Faconti. It was exactly what I was looking for, a way to introspect about message types at run-time in c++. Things were looking up, from here it was just a sneaky copy paste and I’d be done! It was smooth sailing until I got to here.

Good god. No stress, there has to be another way of figuring this out. Enter Dynamic message introspection by OSRF. Awesome, they made a CLI tool which can grab a message at the user’s request, and convert it to and from a YAML format. Let’s see how they did it. Yeah, alright, another cast. Clearly, this calls for deeper investigation.

Getting the message

I came across some docs on the Ros2 internal interfaces, which conveniently has a section about the Rosidl api, which can be seen in abundance in the previously mentioned repos. A look at the rosidl_typesupport_introspection_cpp/message_introspection.hpp is enough to inform us of the make up of a message.

MessageMember_s


typedef struct ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC MessageMember_s
{
  const char * name_;
  uint8_t type_id_;
  size_t string_upper_bound_;
  const rosidl_message_type_support_t * members_;
  bool is_array_;
  size_t array_size_;
  bool is_upper_bound_;
  uint32_t offset_;
  const void * default_value_;
  size_t (* size_function)(const void *);
  const void * (*get_const_function)(const void *, size_t index);
  void * (*get_function)(void *, size_t index);
  void (* resize_function)(void *, size_t size);
} MessageMember;

MessageMembers_s


typedef struct ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC MessageMembers_s
{
  const char * message_namespace_;
  const char * message_name_;
  uint32_t member_count_;
  size_t size_of_;
  const MessageMember * members_;
  void (* init_function)(void *, rosidl_runtime_cpp::MessageInitialization);
  void (* fini_function)(void *);
} MessageMembers;

So, a Msg can be interpreted as a MessageMembers_s struct, which itself contains a series of MessageMember_s structs. We have the number of members in the uint32 member_count_, which allows us to iterate through the members, read their type_id, and cast the value as the corresponding type. So, what about a service? What’s going on there?

ServiceMembers_s


typedef struct ServiceMembers_s
{
  const char * service_namespace_;
  const char * service_name_;
  const MessageMembers * request_members_;
  const MessageMembers * response_members_;
} ServiceMembers;

Makes sense. This indicates that the same process for introspecting about messages can be applied to service calls. This gives a rough idea of what composes a message. The next part is figuring out what the details of the composition. There isn’t really a way to introspect the data without first knowing its structure. Further digging lead me to how the authors solved this problem. It looks more complicated than it is. Ths is just dynamically loading a library at runtime, which is thankfully a simple endeavour as the location is predictable. this blog post proved to be extremely valuable in helping to understand the process of dynamically loading a library.

With this context, the purpose of the static/reinterpret becomes clear: To cast the recieved data to the type information in the dynamically loaded library. Determining the message contents can be done by iterating through the message_members contents (member count, size etc) in the type information and casting the type accordingly.

Representing the data

The aforementioned repositories I’ve linked have taken two different approaches to solving the problem of storing the message as a useful data format. Dynamic message introspection went with a Yaml format, while ros2_introspection went with a Custom tree format. The latter was chosen for two reasons; first is that going full yaml felt a bit overkill, second is that rolling a custom tree format allows flags to be added for any circumstances (e.g. “Is the leaf editable”, “Is the leaf currently being edited?”, “get the next editable leaf”). These features in particular not only smooth the process of editing the service call by supporting tabbing between editable fields, but also allow for coloring the editable fields. It may not sound like much, but it’s made service calls far more streamlined. Currently, there is no support for vectors in service calls, whether that be a vector of ints, or an array of other messages. Small demo video below.

I’m not sure what the next feature for ornis shall be, there are plenty of things to choose from! I would like to add support for more message types for visualisation, adding support for camera feeds, point clouds, etc. Knowing me however, the next feature will probably be adding more colour schemes.