-
Notifications
You must be signed in to change notification settings - Fork 17
Tutorial: Publisher 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).
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:
- convert the
UTCTime
value into its HaskellString
representation usingshow
- prepend the message with the string
"Hello world "
- construct an
S.String
value by feeding the HaskellString
to theS.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).