Sunday, July 15, 2018

The ROS Service/Topic abstractions

Although I quickly outlined the two messaging facilities of ROS (topics and services), I then focused on possible underlying I/O models, and said that a polling mechanism makes for a good primitive.

I think it's clear that without the ability to poll multiple event sources, it's hard to implement efficient asynchronous IO. On the other hand, the opposite direction is really easy. Let's take the code from last time:
rospy.poll(sub1, sub2)
data = sub1.receive()
if data is not None:
    rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)
data = sub2.receive()
if data is not None:
    rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)
To perform a blocking receive, all you have to do is write this:
rospy.poll(sub)
data = sub.receive()
So supporting polling does not push one into nonblocking IO, but not supporting it does push one into blocking IO.

Now let's look a level higher at the actual messaging. ROS provides us with two tools here:
  • topics allow multiple subscribers to receive all messages from multiple publishers;
  • services allow multiple clients to send requests and receive replies from a single server.
You will notice that both types of connections are asymmetric: topic subscribers and servers can't initiate communication (subscribers can't send messages at all). So this pushes us into a specific approach. These can't support arbitrary protocols, right?

Not on their own, but we could use a pair of topics or a pair of services to model a full-duplex connection:
Node 1          Node 2
   --- topic_12 -->    Node 1 publishes to node 2
   <-- topic_21 ---    Node 2 publishes to node 1
or
Node 1          Node 2
   -- service_12 ->    Node 1 is client of node 2
   <- service_21 --    Node 2 is client of node 1
In the first case, you basically send independent messages, in the second case each message gets a response as well. I also want to show a third topology:
Node 1          Node 2
   -- service_12 ->    Node 1 is client of node 2
   <-- topic_21 ---    Node 2 publishes to node 1
This is also full-duplex, and way better aligned to real-world needs. This approach comes rather naturally when designing a system:
  • I (Node 1) want to give my robot (Node 2) commands, for this I need a service.
  • I also want to know my robot's state, for this I need a topic.
  • (and this approach, unlike the others, is easily extended to multiple clients)
That's the value behind the topic/service abstraction: although it seems to constrain you, it actually helps with the patterns that appear in the real world. Another aspect: topics and services are constrained to a single message type (or request/reply message type pair). How do you work with that? Easy, use separate services and topics for different kinds of data. What seems like a constraint at first is actually just pushing you into separation of concerns!

No comments: