rosclj – ROS node in Clojure

Playing with robots, and learning ROS (robot operating system), I was happy to find out about rosclj, Clojure bindings for ROS.  I would be able to control robots from the REPL; how nice!

However, I quickly discovered that the only thing remaining from rosclj is its documentation: the code (originally in a SVN repo of Berkeley) has disappeared.  But never mind.  Clojure can do Java interoperop, right?  So I gave it a shot, and it turned out pretty well, so I thought I’d publish my findings because there is not much up-to-date information out there.

I started by familiarising myself with rosjava and create a pub-sub sample project following the nice tutorial here.  But don’t worry, you don’t really need to fiddle out with catkin to get Clojure talk to a ROS node.  This is actually simpler.

 

Your first ROS node in Clojure

Here are the steps:

1. Create a lein project (I called mine roscljtest), and add rosjava repository and dependencies to your project.clj.  Here is mine:

(defproject roscljtest "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.ros.rosjava_core/rosjava "0.2.1"]
                 [org.ros.rosjava_messages/geometry_msgs "1.11.7"]]
  :repositories [["rosjava" "https://raw.githubusercontent.com/rosjava/rosjava_mvn_repo/master"]]
  :aot :all)

2. Write a ROS node in Clojure.  I simply translated the Listener from the rosjava tutorial to clojure:

(ns roscljtest.core
  (:import [org.apache.commons.logging Log]
           [org.ros.message MessageListener]
           [org.ros.namespace GraphName]
           [org.ros.node AbstractNodeMain ConnectedNode NodeMain]
           [org.ros.node.topic Subscriber]))

(gen-class
  :name "roscljtest.core.CljListener"
  :extends org.ros.node.AbstractNodeMain
  :prefix "clist-")

(defn clist-getDefaultNodeName [this]
  (GraphName/of "rosjava/listener"))

(defn clist-onStart [this connectedNode]
  (let [log (.getLog connectedNode)]
    (doto (.newSubscriber connectedNode "chatter" std_msgs.String/_TYPE)
      (.addMessageListener
        (proxy [MessageListener] []
          (onNewMessage [msg]
            (.info log (str "I heard: \"" (.getData msg) "\""))))))))

3. Build the Clojure code, and create a uberjar, which will be useful to avoid messing up with the classpath:

> lein uberjar

If everything goes well, the following jar file should be generated, and (notice the :aot :all in our project.clj) contain all .class files: roscljtest-0.1.0-SNAPSHOT-standalone.jar

4. Start ROS core and the Talker from the Java tutorial (don’t start the Listener, since we will start the one we just wrote in Clojure)

> roscore &
> cd src/rosjava_catkin_package_a/my_pub_sub_tutorial
> cd build/install/my_pub_sub_tutorial/bin
> ./my_pub_sub_tutorial com.github.rosjava.rosjava_catkin_package_a.my_pub_sub_tutorial.Talker &

5. Finally, start the Clojure node, as follows:

> java -cp target/roscljtest-0.1.0-SNAPSHOT-standalone.jar org.ros.RosRun roscljtest.core.CljListener

Once the connection is established, you should see the following output flow, at a rate of about 1 message per second:

Oct 17, 2015 8:28:18 PM org.ros.internal.node.RosoutLogger info
INFO: I heard: "Hello world! 26"
Oct 17, 2015 8:28:19 PM org.ros.internal.node.RosoutLogger info
INFO: I heard: "Hello world! 27"

Next Steps

OK, this is great.  And the thing I like the most, is that we don’t even need our project to be “catkinized”: we can just use Leiningen as usual (provided we specify the correct repo in our project.clj).

I was actually even able to run the CljListener on a separate machine (provided I set ROS_MASTER_URI and ROS_IP correctly on both machines): I did not even have to install ROS on the machine running the CljListener!

Next step will be to make this a little more idiomatic Clojure, because extending Java classes and overriding methods is no fun.  And I want to be able to run commands from the REPL, which I haven’t tried yet.

But all in all, this looks very promising.