Getting Started with ROSjs

From Brown University Robotics

(Difference between revisions)
Jump to: navigation, search
m
m
Line 364: Line 364:
       <script type="text/javascript">
       <script type="text/javascript">
     function nop() {}
     function nop() {}
-
 
     /* console for logging */
     /* console for logging */
     var console = null;
     var console = null;
-
 
     /* state */
     /* state */
     var active = false;
     var active = false;
     var wall = false;
     var wall = false;
     var bump = false;
     var bump = false;
-
 
     function log(msg) {
     function log(msg) {
         console.innerHTML = console.innerHTML + msg + "<br/>";
         console.innerHTML = console.innerHTML + msg + "<br/>";
     }
     }
-
 
     function init() {
     function init() {
         function waitForDOM() {
         function waitForDOM() {
Line 387: Line 383:
             }
             }
         }
         }
-
 
         setTimeout(waitForDOM, 100);
         setTimeout(waitForDOM, 100);
     }
     }
-
 
     function main() {
     function main() {
         log('console initialized');
         log('console initialized');
-
 
         var connectInfo = document.location.toString();
         var connectInfo = document.location.toString();
-
 
         log('url: ' + connectInfo);
         log('url: ' + connectInfo);
-
 
         var addressMatches = connectInfo.match(/address=([^&]*)/);
         var addressMatches = connectInfo.match(/address=([^&]*)/);
-
 
         if (addressMatches == null) {
         if (addressMatches == null) {
             log('Problem extracting address!');
             log('Problem extracting address!');
             return;
             return;
         }
         }
-
 
         var address = addressMatches[1];
         var address = addressMatches[1];
-
 
         log('address: ' + address);
         log('address: ' + address);
-
 
         var portMatches = connectInfo.match(/.*&port=([^&]*)/);
         var portMatches = connectInfo.match(/.*&port=([^&]*)/);
-
 
         if (portMatches == null) {
         if (portMatches == null) {
             log('Problem extracting port!');
             log('Problem extracting port!');
             return;
             return;
         }
         }
-
 
         var port = portMatches[1];
         var port = portMatches[1];
-
 
         log('port: ' + port);
         log('port: ' + port);
-
 
         log('creating ROSProxy object...');
         log('creating ROSProxy object...');
-
 
         var ros = null;
         var ros = null;
         try {
         try {
Line 429: Line 411:
             return;
             return;
         }
         }
-
 
         log('created');
         log('created');
-
 
         log('connecting to ' + address + ' on port ' + port + '...');
         log('connecting to ' + address + ' on port ' + port + '...');
-
 
         ros.setOnClose(function (e) {
         ros.setOnClose(function (e) {
             log('connection closed');
             log('connection closed');
         });
         });
-
 
         ros.setOnError(function (e) {
         ros.setOnError(function (e) {
             log('network error!');
             log('network error!');
         });
         });
-
 
         ros.setOnOpen(function (e) {
         ros.setOnOpen(function (e) {
             log('connected');
             log('connected');
-
 
             log('initializing ROSProxy...');
             log('initializing ROSProxy...');
             try {
             try {
Line 453: Line 429:
             }
             }
             log('initialized');
             log('initialized');
-
 
             log('registering handler for sensorPacket...');
             log('registering handler for sensorPacket...');
             try {
             try {
Line 461: Line 436:
                         bump = true;
                         bump = true;
                     }
                     }
-
 
                     wall = msg.wall;
                     wall = msg.wall;
-
 
                     if (msg.advance) {
                     if (msg.advance) {
                         active = false;
                         active = false;
                     }
                     }
-
 
                     if (msg.play) {
                     if (msg.play) {
                         active = true;
                         active = true;
                     }
                     }
-
 
                 });
                 });
             } catch (error) {
             } catch (error) {
Line 486: Line 457:
             }
             }
             log('subscribed');
             log('subscribed');
-
 
             log('setting closed loop control policy...');
             log('setting closed loop control policy...');
-
 
             var aligned = true;
             var aligned = true;
             var turned = 0;
             var turned = 0;
             var target = 150;
             var target = 150;
-
 
             function twistMsg(x, z) {
             function twistMsg(x, z) {
                 return '{"linear":{"x":' + x + ',"y":0,"z":0},"angular":{"x":0,"y":0,"z":' + z + '}}';
                 return '{"linear":{"x":' + x + ',"y":0,"z":0},"angular":{"x":0,"y":0,"z":' + z + '}}';
             }
             }
-
 
             setInterval(function () {
             setInterval(function () {
                 var x = 0;
                 var x = 0;
                 var z = 0;
                 var z = 0;
-
 
                 if (wall) aligned = true;
                 if (wall) aligned = true;
                 if (bump) aligned = false;
                 if (bump) aligned = false;
-
 
                 if (!aligned) {
                 if (!aligned) {
                     if (active) {
                     if (active) {
Line 515: Line 480:
                     if (!wall) z = -.5;
                     if (!wall) z = -.5;
                 }
                 }
-
 
                 if (turned &gt; target) aligned = true;
                 if (turned &gt; target) aligned = true;
-
 
                 if (!active) {
                 if (!active) {
                     x = 0;
                     x = 0;
                     z = 0;
                     z = 0;
                 }
                 }
-
 
                 ros.publish('/cmd_vel', 'geometry_msgs/Twist', twistMsg(x, z));
                 ros.publish('/cmd_vel', 'geometry_msgs/Twist', twistMsg(x, z));
             }, 100);
             }, 100);

Revision as of 16:02, 5 August 2010

Contents

Introduction

Do you want to try your hand at programming robotics, but only know Javascript? Are you a rockstar web-developer looking to leverage your leet skills in a domain where you can cause actual damage to life, limb, and personal property? Do you have a moral failing that causes you to want to use Javascript as much as possible? ROSjs is for you! ROSjs is a Javascript binding for WillowGarage's Robot Operating System (ROS): a robot middle-ware system that provides access to a wide range of robots (such as Willow's own PR2, or iRobot's Create) and autonomous capabilities (such as blob-finding, and mapping).

ROS is a large and sophisticated research tool used and developed by hundreds (if not thousands) of roboticist world-wide and ROSjs exposes almost all of its capabilities out-of-the-box. Obviously, it will take more than a few days to master the skills necessary to take over the world via a robot army, but it's surprisingly easy to get a robot running around driven by logic running right in your browser. In fact, that's what this tutorial is all about. We assume at least a passing familiarity with Javascript and it's idioms (such as callbacks) but no ROS or robot specific knowledge. If you have access to an iRobot Create and a web-browser, you should be able to follow along.

A Viciously Short Primer on ROS

Background

Before you can make use of ROSjs, there are a few things you need to know about ROS itself. Besides the practical stuff like installation, running, etc. (which we'll get to), it's important to know how ROS is architected and how it exposes all of the capabilities it provides.

If you're familiar with ROS, feel free to skim or skimp this section. Most of the material will be covered in more depth in the #Programming ROS from ROSjs section. On the flipside: if you're very unfamiliar with ROS, you would do well checking out the great tutorials and other documentation on ROS.org

Topics and Services

ROS exposes capabilities in one of two ways, as topics or as services. services should be very familiar to you if you've programmed before. They are very similar to Javascript's function call. services take arguments and return a response. One important difference from more vanilla functions you may have used in the past is that services always respond with (return) an object. As in most programming languages, this object may have fields and in this way a service may return almost arbitrarily complicated data.

Functions are often the dominant way of implementing logic in most programming languages, but this is not the case with ROS services (and is even less the case when using ROSjs). This is for two reasons: 1) ROS is network based (more on this later), so a service "call" often requires data go to ROS and back 2) service calls block forcing code to pause while it waits for ROS to respond.

topics are streams of objects more akin to Javascript events. You register a handler function (much as you would a listener in Javascript) and whenever a new topic object is available, the handler is called with that object as it's argument. Completing the analogy with Javascript events: just as you can "generate" Javascript events, you can "publish" topic objects which will then be processed by all of the handlers that have subscribed to them.

This is probably all getting a bit abstract, so let's look at a concrete example of using both a topic and a service.

   ros.addHandler('/sensorPacket',function(msg) {
       if (msg.bumpLeft || msg.bumpRight) {
           alert('bump');    
       }
   });
   ros.callService('/rosjs/subscribe','["/sensorPacket",0]',function(rsp) {
       alert('subscribed to /sensorPacket');
   });


Don't worry if you find much of this code confusing, the point here is to get more familiar with the concepts of topics and services. This above snippet has examples of both. Consider:

   ros.addHandler('/sensorPacket',function(msg) {
       if (msg.bumpLeft || msg.bumpRight) {
           alert('bump');    
       }
   });

We'll try not to belabor the parallels to Javascript's addEventListener. Here a handler function is associated with the '/sensorPacket' topic. Notice that /sensorPacket is proceeded by a slash. Now whenever the ROSjs environment receives an object from the /sensorPacket topic it will be passed (as an argument named msg) to the anonymous function created here. How does the ROSjs environment tell ROS that it's interested in /sensorPacket objects? Through a service call:

   ros.callService('/rosjs/subscribe','["/sensorPacket",0]',function(rsp) {
       alert('got response: ' + rsp);
   });

The service we are calling is named /rosjs/subscribe (as above, the slashes are important). We give /rosjs/subscribe its arguments in the form of a JSON object. If you are unfamiliar with JSON, you can read more about it. Here, we are manually creating a JSON object, but we could just as easily have used a JSON library to help us. The arguments are the name of the topic that we want to subscribe to: /sensorPacket and the minimum delay (in milliseconds) between topic objects that we can tolerate (in this case we don't really care). This argument can never be used to make topic objects stream in faster than they would have, but it can be used to slow them down.

You might think that because service calls block, that service responses would be in the form of a return value from the ros.callService function. However, to allow for maximum flexibility (and in keeping with the idioms of Javascript), service responses are handled by a callback as shown. This callback is mandatory and must be a valid (callable) function. Thus it's a common ROSjs idiom to define a nop function

   function nop() {};

and to use it with service calls you don't particularly care about the response to

   ros.callService('/soundOf','["tree falling in woods"],nop);

Notice that the /soundOf service expects only one argument, while /rosjs/subscribe expects two.

Besides being able to subscribe to a stream of objects associated with a particular topic, ROSjs can also create them. This is referred to as "publishing" to a topic.

   //code that assigns the proper value to x and y ...
   ros.publish('/mouse','mouse_msgs/CursorPosition','{"x":' + x + ',"y":' + y + '}');

The publish function takes three arguments: 1) the topic to publish to 2) the type of the object to publish (again, we'll cover this more in-depth later) 3) the JSON form of the object to publish. Notice that once again, for the sake of making these examples self-contained we have manually created a JSON object. In real code, it would be far easier and less error prone to depend on a JSON library.

Nodes and Types

At its heart, ROS is really a Remote Procedure Call (RPC) and data-sharing mechanism. ROS provides a server program, roscore, that acts a central registry for (amongst other things) topics and services. This facilitates ROS-compatible programs, called nodes, communicating with each other through named channels (topics and services). Of course, just having a /topic or /service is not enough to facilitate communication. There must me some kind of hope that the programs will understand one another. This is where types come in. In ROS, an object's structure (the organization of its fields, and the kinds of values those fields can take on) is called its type. ROS nodes can be confident in their ability to process the objects published by a particular topic because each topic is associated with a specific type. In our earlier example, we published to the /mouse topic. In this case the /mouse topic is associated with the mouse_msgs/CursorPosition type. If a node knew how to examine mouse_msgs/CursorPosition objects, it could confidently subscribe to /mouse knowing there would be no surprises in the structure of the data it receives.

While it would be next to impossible to make use of ROS without some knowledge of the types involved, with ROSjs the only time you need to explicitly handle ROS types is when publishing.

Dive into ROSjs

Enough talk about abstractions and types and whatnot. Let's move a robot from our web-browser! To get started there are a few things you'll need:

Prerequisites

ROSjs is not specific to the materials listed here, but we do use them as our example.

1) ROS
see this page
2) An iRobot Create
we've written this tutorial with this robot in mind, but with slight adaptation, but the ROSjs examples themselves should work with any that respond to Twist msgs on /cmd_vel
3) svn
4) A websocket compatible browser, such as the latest Chrome, Safari, or Firefox (others may work just fine too).

Setting up your ROSjs environment

We'll assume for the moment that you already have ROS installed, and that it is installed in /opt/ros/cturtle/ . If you have a newer (or older) version of ROS or a non-binary version, make appropriate adjustments to the paths as we go along. Let's assume you are staring at a bash prompt on a Linux machine.

Source the appropriate ROS environment:

   source /opt/ros/cturtle/setup.sh

now make a directory to house ROSjs and the iRobot Create

   mkdir diveIn

add this directory to your ROS_PACKAGE_PATH:

   export ROS_PACKAGE_PATH=$ROS_PACKAGE_PATH:/absolute/path/to/diveIn

now enter the diveIn directory and fetch both ROSjs and the irobot_create_2_1 driver

   cd diveIn
   svn co https://brown-ros-pkg.googlecode.com/svn/trunk/experimental/ROSjs ROSjs
   svn co https://brown-ros-pkg.googlecode.com/svn/trunk/unstable/irobot_create_2_1 irobot_create_2_1

make the irobot_create_2_1 driver (you may have to type this command twice, sometimes ROS has a hard time finding freshly added packages)

   rosmake irobot_create_2_1

If the make fails, it is most likely due to the lack of some prerequisite ROS package. Depending on how you installed ROS, you may need to apt-get the packages indicated by the error messages, or download and make them yourself.

Once irobot_create_2_1 builds, attach the robot to /dev/ttyUSB0 and start up the driver

   rosrun irobot_create_2_1 driver.py &

If your robot is attached to a port other than /dev/ttyUSB0, you'll need to set a rosparam before using rosrun

   rosparam set /brown/irobot_create_2_1/port /dev/otherPort

Finally, you're ready to run ROSjs

   cd ROSjs
   ./ROSjs

If you don't get an error from ROSjs, you're ready to write your first ROSjs enabled webpage.

About ROSjs webpages

ROSjs listens for websocket connections on port 9090 of the machine it's started on. Since ROSjs uses websockets and not AJAX, the machine that serves the HTML content does not necessarily need to be the same machine that ROSjs is running on. With ROSjs, a direct connection is made from the web-browser viewing the page and the machine running ROSjs. If the browsing machine can reach 9090 on the machine running ROSjs, all is well. If it can't... well... not much will happen. Make routing arrangements accordingly.

One advantage of this flexibility is that you don't need a web-server to start experimenting with ROSjs, you can view a locally hosted file and everything will work just as it would if the page were served from Apache, IIS, or what-have-you.

Your First ROSjs webpage

We'll start with the following HTML skeleton

   <html>
     <head>
       <script type="text/javascript" src="http://brown-ros-pkg.googlecode.com/svn/trunk/experimental/ROSjs/ROS.min.js"></script>
       <script type="text/javascript">
   function main() {
       /* insert code here */
   }
       </script>
     </head>
   <body onload="main()">
   </body>
   </html>

The first script tag imports the latest minified version of ROSjs. The second is where we'll define the main function called by the body onload.

The Create's basic motion is controlled by two parameters a forward velocity, x, and a angular velocity z. We'll start by defining two variables for these parameters.

   var x = 0;
   var z = 0;

Now to create a ROSjs object. This object will be our gateway to ROS services and topics. To initialize it, we'll need the address or hostname of the machine running ROSjs.

   var ros = new ROS("ws://hostname:9090");

To use this ROSjs object, we need to register three callbacks: one for when a server connection becomes closed, one for when errors occur, and one for when a connection is successfully opened.

     ros.setOnClose(function (e) {
       document.write('connection closed<br/>');
     });
     ros.setOnError(function (e) {
       document.write('error!<br/>');
     });
     ros.setOnOpen(function (e) {
       document.write('connected to ROS<br/>');
     });

Here's what our webpage looks like so far:

   <html>
     <head>
       <script type="text/javascript" src="http://brown-ros-pkg.googlecode.com/svn/trunk/experimental/ROSjs/ROS.min.js"></script>
       <script type="text/javascript">
       </script>
     </head>
   <body onload="main()">
   function main() {
     var x = 0;
     var z = 0;
     var ros = new ROS("ws://hostname:9090");
     ros.setOnClose(function (e) {
       document.write('connection closed<br/>');
     });
     ros.setOnError(function (e) {
       document.write('error!<br/>');
     });
     ros.setOnOpen(function (e) {
       document.write('connected to ROS<br/>');
     });
   </body>
   </html>

Since we need a connection to ROS to do pretty much anything at all, most of the action will happen inside the OnOpen callback.

First let's define a local function that will set the Create's linear and angular velocity based on the values of x and z.

   function pub() {
     ros.publish('/cmd_vel', 'geometry_msgs/Twist', '{"linear":{"x":' + x + ',"y":0,"z":0}, "angular":{"x":0,"y":0,"z":' + z + '}}');
   }

As with our earlier examples, we are hand-crafting a JSON object. This is fine for this toy example, but in general you will want to make use of a real JSON library.

So now by calling pub, we can set the robot's motion to that of x and z. Not very interesting if x and z remain 0. Here's where Javascript's and the browser's extensive even handling come into play. We'll define a function that provides some basic key controls.

   function handleKey(code, down) {
       var scale = 0;
       if (down == true) {
         scale = 1;
       }
       switch (code) {
       case 37:
         //left
         z = 1 * scale;
         break;
       case 38:
         //up
         x = .5 * scale;
         break;
       case 39:
         //right 
         z = -1 * scale;
         break;
       case 40:
         //down
         x = -.5 * scale;
         break;
       }
       pub();
     }

Now we simply register this function with the as a key event handler for the webpage body.

   document.addEventListener('keydown', function (e) {
     handleKey(e.keyCode, true);
   }, true);
   document.addEventListener('keyup', function (e) {
     handleKey(e.keyCode, false);
   }, true);

The complete webpage looks like this:

   <html>
     <head>
       <script type="text/javascript" src="http://brown-ros-pkg.googlecode.com/svn/trunk/experimental/ROSjs/ROS.min.js"></script>
       <script type="text/javascript">
   function main() {
     var x = 0;
     var z = 0;
     var ros = new ROS("ws://hostname:9090");
     ros.setOnClose(function (e) {
       document.write('connection closed<br/>');
     });
     ros.setOnError(function (e) {
       document.write('error!<br/>');
     });
     ros.setOnOpen(function (e) {
       document.write('connected to ROS<br/>');
       function pub() {
         ros.publish('/cmd_vel', 'geometry_msgs/Twist', '{"linear":{"x":' + x + ',"y":0,"z":0}, "angular":{"x":0,"y":0,"z":' + z + '}}');
       }
       function handleKey(code, down) {
         var scale = 0;
         if (down == true) {
           scale = 1;
         }
         switch (code) {
         case 37:
           //left
           z = 1 * scale;
           break;
         case 38:
           //up
           x = .5 * scale;
           break;
         case 39:
           //right 
           z = -1 * scale;
           break;
         case 40:
           //down
           x = -.5 * scale;
           break;
         }
         pub();
       }
       document.addEventListener('keydown', function (e) {
         handleKey(e.keyCode, true);
       }, true);
       document.addEventListener('keyup', function (e) {
         handleKey(e.keyCode, false);
       }, true);
     });
   }
       </script>
     </head>
   <body onload="main()">
   </body>
   </html>

That's really it. Change hostname to the name or IP of the machine running ROSjs, and you're good to go. Load the page in a websocket supporting browser (you can check for compatibility here). Remote teleop in less than 60 lines of HTML!

Teleop is great and all, but what about actual control? Here is a more realistic example that implements wall following.

   <html>
   <head>
     <script type="text/javascript" src="http://brown-ros-pkg.googlecode.com/svn/trunk/experimental/ROSjs/ROS.min.js"></script>
     <script type="text/javascript">
   function nop() {}
   /* console for logging */
   var console = null;
   /* state */
   var active = false;
   var wall = false;
   var bump = false;
   function log(msg) {
       console.innerHTML = console.innerHTML + msg + "
"; } function init() { function waitForDOM() { var cnsl = document.getElementById('console'); if (cnsl == null) { setTimeout(waitForDOM, 100); } else { console = cnsl; setTimeout(main, 0); } } setTimeout(waitForDOM, 100); } function main() { log('console initialized'); var connectInfo = document.location.toString(); log('url: ' + connectInfo); var addressMatches = connectInfo.match(/address=([^&]*)/); if (addressMatches == null) { log('Problem extracting address!'); return; } var address = addressMatches[1]; log('address: ' + address); var portMatches = connectInfo.match(/.*&port=([^&]*)/); if (portMatches == null) { log('Problem extracting port!'); return; } var port = portMatches[1]; log('port: ' + port); log('creating ROSProxy object...'); var ros = null; try { ros = new ROS('ws://' + address + ':' + port); } catch (err) { log('Problem creating proxy object!'); return; } log('created'); log('connecting to ' + address + ' on port ' + port + '...'); ros.setOnClose(function (e) { log('connection closed'); }); ros.setOnError(function (e) { log('network error!'); }); ros.setOnOpen(function (e) { log('connected'); log('initializing ROSProxy...'); try { ros.callService('/rosjs/topics', '[]', nop); } catch (error) { log('Problem initializing ROSProxy!'); return; } log('initialized'); log('registering handler for sensorPacket...'); try { ros.addHandler('/sensorPacket', function (msg) { bump = false; if (msg.bumpLeft || msg.bumpRight) { bump = true; } wall = msg.wall; if (msg.advance) { active = false; } if (msg.play) { active = true; } }); } catch (error) { log('Problem registering handler!'); return; } log('registered');
           log('subscribing to sensorPacket...');
           try {
               ros.callService('/rosjs/subscribe', '["/sensorPacket",0]', nop);
           } catch (error) {
               log('Problem subscribing!');
           }
           log('subscribed');
           log('setting closed loop control policy...');
           var aligned = true;
           var turned = 0;
           var target = 150;
           function twistMsg(x, z) {
               return '{"linear":{"x":' + x + ',"y":0,"z":0},"angular":{"x":0,"y":0,"z":' + z + '}}';
           }
           setInterval(function () {
               var x = 0;
               var z = 0;
               if (wall) aligned = true;
               if (bump) aligned = false;
               if (!aligned) {
                   if (active) {
                       z = .5;
                       turned = turned + 1;
                   }
               } else {
                   x = .25;
                   turned = 0;
                   target = 150 + Math.floor(Math.random() * 150);
                   if (!wall) z = -.5;
               }
               if (turned > target) aligned = true;
               if (!active) {
                   x = 0;
                   z = 0;
               }
               ros.publish('/cmd_vel', 'geometry_msgs/Twist', twistMsg(x, z));
           }, 100);
           log('running');
       });
   }
       </script>
     </head>
   <body onload="init()">
   </body>
   </html>

Advanced ROSjs webpages

Programming ROS from ROSjs