Tutorial: Publisher Subscriber - acowley/roshask GitHub Wiki

Writing a Publisher and a Subscriber

Perhaps the most useful place to start is in the creation of two Nodes connected by a Topic that allows the Publisher Node to send messages to the Subscriber Node. This example may be found in the Examples/PubSub directory. The PubSub package was originally created using the command line roshask create PubSub std_msgs which creates a package named PubSub in the current directory with a dependency on the std_msgs ROS package. Before compiling the PubSub package, the command roshask dep should be executed in the PubSub directory to ensure that Haskell types have been generated for all the message types defined in packages PubSub depends on (in this case, std_msgs). This step will also be run during a rosmake, but if you are using cabal install to build your roshask package, then you can use roshask dep whenever you install a new version of roshask or are depending on a package or stack you haven't used before.

Note: This tutorial is aimed at programmers already somewhat familiar with Haskell, and aims to be specific regarding Haskell idioms and terminology. It is not an introduction to the relevant Haskell concepts. For that, please consult a tutorial such as Learn You a Haskell (which is outstanding).

Publisher

Let's look at the entire source code for the publisher Node, which we have named Talker, before breaking it down into pieces. The purpose of this Node is to publish a string message containing the current time at 1 second intervals.

  module Talker (main) where
  import Data.Time.Clock (getCurrentTime)
  import Ros.Node
  import Ros.Topic (repeatM)
  import qualified Ros.Std_msgs.String as S
  
  sayHello :: Topic IO S.String
  sayHello = repeatM (fmap mkMsg getCurrentTime)
    where mkMsg = S.String . ("Hello world " ++) . show
  
  main = runNode "talker" $ advertise "chatter" (topicRate 1 sayHello)

The first line specifies the name of the module we are defining, Talker, and defines an explicit export list specifying which identifiers should be visible outside this module. Since this is designed to be an executable program, we must export a main value, or indicate which value should be used as main in the .cabal file for our package. Note that Haskell module names must begin with a capital letter and must be the same as the file name in which they are saved.

The rest of the first section imports library components we will be using. We will import Ros.Node in all roshask nodes, as it provides most of the commonly needed features for constructing a ROS Node.

Our ultimate goal is to produce a stream of string messages. The String ROS message type is defined in the std_msgs ROS package, so we import the corresponding Haskell module using the syntax import qualified Ros.Std_msgs.String as S. Note that ROS package and stack names have their first character capitalized in roshask to match up with Haskell syntax requirements. We imported the Ros.Std_msgs.String module qualified in order to avoid name collisions with other data types. If we didn't import the module qualified, we would have a conflict with the standard Haskell String type. In general, qualified imports are good style as they signal to any reader of your code that a particular identifier is not one from the standard library, but instead was brought into scope by an import statement with a particular qualified name.

The "stream" that we are producing will, in roshask terminology, be a value of type Topic IO S.String. This value is an infinite stream of S.String values, each produced by an action in the IO monad. If you are not too familiar with Haskell, you can just use IO as the first parameter to the Topic type constructor and not worry about it. It signifies that some input/output action is associated with this Topic's production of values.

  sayHello :: Topic IO S.String

Within the scope of our Haskell code, we are giving our Topic the name sayHello. This is how we can refer to it elsewhere, much like a variable in any programming language. We first give a type signature for sayHello to make it clear to readers what we are doing, and so that the type checker can confirm that we have created the type of value we set out to define.

Originally we stated that we should output a message including the current time once a second. We could implement the body of this concept as we might in a traditional imperative language, then wrap it in a recursive call to construct the infinite stream of values our Topic should produce.

  sayHello = Topic $ do threadDelay 1000000
                        t <- getCurrentTime
                        let msg = S.String ("Hello world " ++ show t)
                        return (msg, sayHello)

That very specifically says, "Wait for 1 second, get the current time, build up a string message, push it out on our Topic... and repeat." However this specification is brittle and overly narrow. For instance, is sleeping for one second going to cause our messages to actually be produced at 1Hz? Not if the production of our message takes some amount of time, or if the CPU is otherwise busy. We could sleep for 990000 microseconds as a dirty hack, but this is an instance of a more abstract problem of figuring out how long to sleep before performing some action such that the action produces results at no greater than a specified frequency. That abstract problem is solved in the roshask library, so we will use that instead.

Another problem is that we simply say too much in this definition of sayHello, leading to an over-specification with respect to the conceptual ideal of a Topic that simply produces time-based messages. First, the desired update rate is hard-coded into the expression that produces values. What if we would also like to produce time-carrying messages at 10Hz? Would we need a second value almost exactly the same as sayHello? We could at least make the time delay a parameter to a sayHello function that then produces the desired Topic. But what if we want to use this time-message Topic without giving it a fixed delay? Perhaps we are going to join it with some other Topic, and then we want to limit the rate of that composition of Topics. Baking the notion of rate control into our Topic definition limits our options. Such a detail is an example of letting an orthogonal concern -- what rate the Topic should produce values at -- leak into a cleanly-designed Topic specification.

Next, we have given a name, t, to the current time value, and another name msg to the S.String we are outputting. These names are used in a very limited scope, and are not strictly necessary. The dangers of having unnecessary names include the risk of shadowing names in outer scopes, and causing confusion with convoluted control flow (e.g. a reader thinking, "Now, where did that t come from?"). If we can void giving names to values without causing even more confusion, we shall endeavor to do so (yes, this is a matter of style).

Now, on to the definition we actually want to use!

  sayHello = repeatM (fmap mkMsg getCurrentTime)
    where mkMsg = S.String . ("Hello world " ++) . show

The main building block of our Topic is the Haskell getCurrentTime action which we imported from the Data.Time.Clock module up in the import section. This value has the type IO UTCTime, which means that we can run the action in the IO monad to produce a value whose type is UTCTime. We turn values of type UTCTime into ROS S.String message values by lifting a helper function, mkMsg, into the IO Monad using fmap. The mkMsg function is used at the type UTCTime -> S.String, and is a composition of three parts:

  1. convert the UTCTime value into its Haskell String representation using show
  2. prepend the message with the string "Hello world "
  3. construct an S.String value by feeding the Haskell String to the S.String data constructor

This gives us a value of type IO S.String, which we expand into a Topic IO S.String using the repeatM function. The repeatM function simply runs the given monadic action repeatedly to produce values for a Topic.

  main = runNode "talker" $ advertise "chatter" (topicRate 1 sayHello)

The final part of constructing a ROS Node is setting up the plumbing: what is our Node's public name, what Topics do we subscribe to, and what Topics do we advertise. We run a Node using the runNode function, whose first argument is the public name of the Node. The second argument to runNode is a value in the Node monad that defines the Topic connections. Our "talker" Node doesn't subscribe to any Topics, but it does advertise our sayHello Topic with the name "chatter". The advertise function takes the name of the Topic to advertise and the Topic itself before returning a unit value in the Node monad. Remember that while sayHello does produce a Topic IO S.String, this Topic was created without any restriction on how quickly it produces values. We limit its rate just before advertising by using the topicRate function, whose first argument is the target rate in Hz. This function uses an adaptive controller to regulate the production rate better than a fixed argument to threadDelay could.

That completes the Publisher node definition. As usual, explaining all the details of a short Haskell program has taken many words. To see how the talker executable is built, see Examples/PubSub/PubSub.cabal. To try it yourself, run cabal install in the PubSub directory, and run bin/talker after starting a ROS master server.

##Subscriber

The subscriber node, which we have named Listener, subscribes to the "chatter" Topic, and prints every message it receives.

  module Listener (main) where
  import Ros.Node
  import qualified Ros.Std_msgs.String as S

  showMsg :: S.String -> IO ()
  showMsg = putStrLn . ("I heard " ++) . S._data

  main = runNode "listener" $ runHandler showMsg =<< subscribe "chatter"

The first section of this module is much like that of Talker, above. Please refer there for more information.

This Node applies the function showMsg to every value produced by the Topic we get as a result of the subscribe "chatter" expression. Since our showMsg function doesn't return anything, we use the runHandler function to apply our function to each value from the "chatter" Topic because all we are interested in are the side effects of those applications (i.e. the printed messages).