We're building visual tools allowing you, among other things, to view, monitor, and interact with your ROS setup for the purpose of development, debugging or meditation.
Many ROS visualizers allow you to view you ROS setup, but we didn't find ones that allow you interact with it (e.g. manually call a service or publish a message). So we're building one.
Rust libraries for ROS2
At the time of writing, there are two libraries to interact with ROS2 from Rust I found useful:
RCL bindings. It can do anything ROS can, but...
it's a bit awkward to use in Rust (e.g. C FFI doesn't speak async)
it requires working ROS setup to compile and run
I'm already hearing the question from audience: "how can you use ROS without having set up ROS", for which there is a nice solution:
This is a native Rust reimplementation of ROS2, which brings nice benefits. You can use it without having ROS on your computer (e.g. can communicate with ROS on another machine or a docker container), and it's API is idiomatic Rust.
But since it's a reimplementation and not a binding, it doesn't implement everything that RCL does.
We ended up using both: r2r
for discovery and ros2-client
for pubsub.
ROS discovery
We want to show you what nodes/topics/services they have, available types for each, and QoS settings if applicable. This information is currently used for autocompletion in the Intrepid platform, can also be checked in the future to warn about conflicting QoS settings and such.
There are many existing tools to visualize ROS network infrastructure. I'll give a shout-out to rqt_network, which helped me to understand what information is available out there. Lets make another tool like those! Maybe better :)
Here it is in all its glory - or watch the tutorial ⬇️⬇️
So, how do we get ROS graph information in Rust and constantly update it at runtime?
ros2-client
doesn't support discovery at the time of writing it, so we had to user2r
our visual tools must be able to run without ROS2 set up, so we must call
r2r
from a different thing (program or library)the state of dynamic libraries in Rust is quite horrible (no stable ABI, you have to either use C ABI, or wrappers around every single data structure)
So we've implemented a simple program that connects to ROS and outputs topics every second (in json or bitcode).
https://github.com/IntrepidAI/intrepid-ros-monitor
$ ./intrepid-ros-monitor
{"ts":1739992983391,"type":"node_added","name":"intrepid","namespace":"/_discovery","properties":{"enclave":"/","publishers":{},"subscribers":{},"clients":{},"services":{}}}
We read that, send to React-based dashboard via websocket, so that's graph visualization done.
You will find pre-compiled binaries ready to download on the frontpage of https://intrepid.ai
Dynamic types
ROS discovery is a thing that everyone does. Dynamic types is a thing that nobody does. So I knew it was going to be... interesting...
Can you subscribe to a ROS topic without knowing what message type goes there? Does it even make sense?
Yes.
In fact, there are two common use-cases.
Maybe you want to write a tool that takes bytes from one topic and sends it to another topic as is (or simply stores data for analysis later).
Maybe you want to write a tool that can subscribe to anything, then visualize received ROS messages at runtime (so user can give you
.msg
at runtime, but you don't know about it at compile-time).
Our case is the second one. User gives us .msg
at runtime and asks to subscribe/debug a particular topic.
1. Parsing
How to parse .msg
? Frustratingly enough, there are no existing .msg
parsers in Rust. Every crate I've seen compiles msg into rust, but I just want to read .msg as AST.
So I ended up porting rosidl
parser from Python to Rust.
Here it is:
https://github.com/IntrepidAI/rosmsg
It took about a day of work, and I believe it produces exactly the same output as rosidl parser (with all the bugs faithfully ported as well).
2. Subscribing
let subscriber: Subscription<???> = node.create_subscription(&topic, Some(qos))?;
let stream = subscriber.async_stream();
let data = stream.next().await?;
dbg!(&data);
RCL straight up doesn't support subscribing without knowing types!
See comment here:
Thankfully, ros2-client
implements this part better. You are required to pass in a type to subscribe, but it may be just a wrapper around Vec<u8>
, that you can parse yourself later with CDR.
We parse .msg
at runtime, then pass that data structure to deserializer as serde seed. Hundred lines of code in total, but here's the important part:
struct RosMessageType(Arc<rosmsg::MessageSpecification>);
struct RosMessage(Vec<u8>);
let type_info = RosMessageType(Arc::new(rosmsg::parse_message_string(&pkg_name, &msg_name, &contents)));
let subscriber: Subscription<RosMessage> = node.create_subscription(&topic, Some(qos))?;
let stream = subscriber.async_stream_seed(type_info.clone());
let data = stream.next().await?;
dbg!(&data);
Serde allows you to pass context (seed) to deserialize your message, and parsed .msg
is that context.
What do we deserialize into? In Python, you'd use untyped dict. In Rust, HashMap<String, serde_value::Value>
may be a usual replacement.
But we can do better than this... 😁
3. Reflection
So we've got a ROS message in some abstract format, which we didn't know at compile time. What can we do with it?
The ability to convert anything to string, or add two things of any type, or compare two things of any type, is something you get for granted in scripting languages like Python. In Rust - not so much.
Thankfully, in programming there's a concept called "reflection". We're using bevy_reflect
library for this. It gives you abstract data types (Type, List, Set, Map, etc.), as well as the ability to add your own. If you add your own, you can register ToString
or PartialEq
implementations for it at runtime.
So, displaying received data from ROS topic just looks like this:
struct RosMessageType(Arc<rosmsg::MessageSpecification>);
struct RosMessage(Box<dyn PartialReflect>);
let type_info = RosMessageType(Arc::new(rosmsg::parse_message_string(&pkg_name, &msg_name, &contents)));
let subscriber: Subscription<RosMessage> = node.create_subscription(&topic, Some(qos))?;
let stream = subscriber.async_stream_seed(type_info.clone());
let data = stream.next().await?;
println!("{}", data);
All that's left to do is an implementation of serde::de::DeserializeSeed
with Value=RosMessage
on RosMessageType
calling Bevy's TypedReflectDeserializer
.
Sounds fun, right? 😋
Note that overuse of dynamic types hurts performance, so you can decide to compile some types statically (we actually do that for Vec2/Vec3), or compile everything statically later when software is ready for release.
See it in action
We are the folks at Intrepid, and we have built all these concepts into our platform - an AI-assisted solution for autonomous robotics. Check it out at https://intrepid.ai
For a hands-on demonstration, check out tutorial Episode 7: Blazing Fast. Ultra-Performant. Meet Intrepid ROS Integration.
This video provides a step-by-step guide on discovering ROS2 nodes, topics, and services simply by connecting to an existing ROS2 ecosystem. The Intrepid agent automatically performs dynamic deserialization using reflection in Rust.
Conclusion
With these tools, developers can modify program's behaviour and change types at runtime without recompilation or restart.
So any in-memory state stays unmodified, any TCP connections you may have stay open.
Think about HMR (Hot Module Replacement) in React App, but for ROS2 App, that's the idea.
Connect with Us
🤖 Sign up now on the official website of Intrepid AI to get started
💬 Join the conversation on our Discord community
📩 Subscribe to our Newsletter for the latest updates:
References
Intrepid AI official website https://intrepid.ai/
Intrepid AI ROS Monitor https://github.com/IntrepidAI/intrepid-ros-monitor
Intrepid AI ROS IDL Parser in Rust https://github.com/IntrepidAI/rosmsg
Easy to use, runtime-agnostic, async rust bindings for ROS2 https://github.com/sequenceplanner/r2r
Bevy’s Crate Reflect https://docs.rs/bevy/latest/bevy/reflect/index.html