Introduction to libURBI

Matthieu Nottale


Table of Contents

Introduction
Getting started
Connecting
Sending URBI commands
Sending binary data.
Sending a sound
Receiving
Synchronous operations
Reading the value of a device
Getting an image
Getting sound
Conversion functions
Putting it all together: some examples
Programming hints
Portability functions

Introduction

Liburbi-c++ is a library designed to encapsulate an URBI connection. It handles the TCP connection with the URBI server, and the dispatching of messages it sends. The library is thread-safe and reentrant.

The library consists of two C++ classes, UClient and USyncClient, and a few helpful functions.

We expect the reader to be a bit familiar with the URBI syntax.

Getting started

Connecting

To connect to an URBI server, simply create a new instance of UClient ( or USyncClient if you want to use the synchronous functions described bellow), passing the name or the address of the server as the first parameter, and optionnaly the port as the second parameter:

UClient * client = new UClient("myrobot.ensta.fr");

//a wrapper is also available in the urbi namespace:
UClient * client = urbi::connect("myrobot.ensta.fr");

The constructor will start an independant thread that will listen for incoming messages from the URBI server.

You can check if the connection was successfuly established by calling the error function, which returns a zero value on success, or a nonzero error code in case of failure.

Sending URBI commands

The method send is the simplest way to send commands to the URBI server. It accepts a syntax similmar to the printf function. To send a sequence of commands without risk of having an other thread sending commands at the same time, the lockSend and unlockSend methods can be used to lock and then unlock the send buffer.

int sleeptime = 50;

client->send("motoron;");

client->lockSend(); //send() call by other threads will be blocked from this point until unlockSend is called

for (float val=0; val<=1; val+=0.05) 
    client->send("neck.val = %f;wait (%d);", val, sleeptime);

client->unlockSend();

Alternatively, the UClient class inherits from ostreaam, so you can use the << operator:

 client << "headPan.val = " <<12 << urbi::comma; 

The constants 'comma', 'semicolon', 'pipe' and 'parallel' are defined in the urbi namespace for ',', ';', '|' and '&' respectively.

A third possible way is to use the URBI macro, which uses the default connection (the first connection created with your program):

 URBI((
  
   headPan.val = 12 , 
   echo "coucou" | speaker.play("test.wav") & leds.val = 1

 )); 

 //note the absence of double-quotes to delimit the URBI code
 //the double-parenthesis are required

 URBI() << "headPan.val = " << 12 <<  urbi::semicolon; 

You can force the creation of a client for this macro to use it, without having an explicit UClient object:

 urbi::connect("myrobot.ensta.fr");

The function urbi::setDefaultClient(UClient *cl) can be used to change the default client.

Sending binary data.

To send binary data to the robot, the method sendBin must be used. It takes as parameters the buffer to send and its size, and optionnaly a header.

client->sendBin(soundData, soundDataSize,"speaker.val = BIN %d raw 2 16000 16 1;", soundDataSize);

Sending a sound

Although you could use sendBin to play a sound on the robot, a specific and efficient method has been written for this purpose: sendSound.

client->sendSound(sound, "endsound");

The first parameter is a USound structure describing the sound to send. The second is an optionnal tag that will be used by the server to issue a "stop" system message when the sound has finished playing. The function convert can be used to convert between various sound formats.

There is no limit to the size of the sound buffer, since it will be automatically cut into small chunks by the library. The data is copied by the library: the USound parameter and its associated data can be safely freed as soon as the function returns.

Receiving

Most of the messages received from the URBI server are the results of a previously sent command. The mechanism of URBI tags enables to link a message to its reply: with each command is associated a tag, and this tag is repeated in the reply message. The UClient class handles the reception of those messages in the independant thread created by the constructor, parses them and fills a UMessage structure. Callback functions with an associated tag can be registered with the method registerCallback: each time a message with this tag is sent by the server, the callback function will be called with a UMessage structure as a parameter. The two basic forms of registerCallback are:

typedef UCallbackAction (*UCallback)             (const UMessage &msg);
typedef UCallbackAction (*UCustomCallback)       (void * callbackData, const UMessage &msg);

UCallbackID 	setCallback (UCallback cb, const char *tag)
UCallbackID 	setCallback (UCustomCallback cb, void *callbackData, const char *tag)

The first parameter is always a pointer to the function to call. callbackData is a pointer that will be given back to the callback function each time it is called. The callback function must return URBI_CONTINUE, or URBI_REMOVE, in which case the function will be unregistered.

A few examples:


UCallbackAction onImage(const UMessage &msg) {
  if (msg.binaryType != BINARYMESSAGE_IMAGE)
    return URBI_CONTINUE;
  msg.client.printf("Image of size (%d,%d) received from server at %d\n",msg.image.width, msg.image.height, msg.timestamp);

  unsigned char *image = new unsigned char[msg.image.width*msg.image.height*3];
  int sz = msg.image.width*msg.image.height*3;

  if (msg.image.imageFormat == IMAGE_JPEG) 
    convertJPEGtoRGB((const byte *) msg.image.data, msg.image.size, (byte *) image, sz); //provided by liburbi
  if (msg.image.imageFormat == IMAGE_YCbCr)
    convertYCrCbtoRGB((const byte *) msg.image.data, msg.image.size, (byte *) image);  //provided by liburbi

  myDisplayRGBImage(image, msg.image.width, msg.image.height);
  delete image;
  return URBI_CONTINUE;
}

UCallbackAction onSound(const UMessage &msg) {
  if (msg.binaryType != BINARYMESSAGE_SOUND)
    return URBI_CONTINUE;

  //convert the sound to a wav 16KHz 16bit.
  USound snd;
  snd.soundFormat = SOUND_WAV;
  snd.rate = 16000;
  snd.sampleSize = 16;
  snd.sampleFormat = SAMPLE_SIGNED;
  snd.channels = 0; //take the value from source
  snd.data = 0;
  snd.size = 0;
  convert(msg.sound, snd); //this function is provided by liburbi
  myPlayWAV(snd.data, snd.size);
  return URBI_CONTINUE;
}

UCallbackAction onJoint(const UMessage &msg) {
  if (msg.type != MESSAGE_DOUBLE)
    return URBI_CONTINUE;
  msg.client.printf("The joint value si %lf\n", msg.doubleValue);
  return URBI_CONTINUE;
}

int main(int argc, const char * argv[]) {
  UClient * cl = new UClient(argv[1]);
  if (cl->error()) urbi::exit(1); //portability call explaned below
  cl->setCallback(&onImage, "img");
  cl->setCallback(&onSound, "snd");
  cl->setCallback(&onJoint, "joint");
  cl->send("img: camera.val;");
  cl->send("loop snd: micro.val,");
  cl->send("joint: headPan.val;");
  urbi::execute();  //portability call explaned below
}

The UMessage structure is capable of storing the informations contained in any kind of URBI message by using a "type" field and a union of type-dependant structures. It is defined as follows:

class UMessage {
 public:  
  UAbstractClient    &client;   // connection from which originated the message
  int                timestamp;     // server-side timestamp
  char               *tag;          // associated tag
  
  UMessageType       type;          // type of the message
  UBinaryMessageType binaryType;    // type of binary message
  
  union {
    double        doubleValue;
    char          *stringValue;
    char          *systemValue;
    char          *message;        // filled if type is unknown (MESSAGE_UNKNOWN)
    USound        sound;           // filled if binary data is of the sound type
    UImage        image;           // filled if binary data is of the image type
    UBinary       binary;          // filled if binary data is of an unrecognised type
  }; 


class USound {
 public:
 char                  *data;            // pointer to sound data
 int                   size;             // total size in byte
 int                   channels;         // number of audio channels
 int                   rate;             // rate in Hertz
 int                   sampleSize;       // sample size in bit
 USoundFormat          soundFormat;      // format of the sound data (SOUND_RAW, SOUND_WAV, SOUDN_MP3...)
 USoundSampleFormat    sampleFormat;     // sample format
};

class UImage {
 public:
  char                  *data;            // pointer to image data
  int                   size;             // image size in byte
  int                   width, height;    // size of the image
  UImageFormat          imageFormat;      // IMAGE_RGB, IMAGE_YCbCr, IMAGE_JPEG...
};

The type field can be MESSAGE_DOUBLE, MESSAGE_STRING, MESSAGE_SYSTEM, MESSAGE_BINARY or MESSAGE_UNKNOWN. Depending of this field, the corresponding value in the union will be set. If the message is of the binary type, binaryType will give additional informations on the type of data (BINARYMESSAGE_SOUND, BINARYMESSAGE_IMAGE or BINARYMESSAGE_UNKNOWN), and the appropriate sound or image structure will be filled.

Template versions of registerCallback are also defined. They allow to set callbacks on member functions, with from 0 to 4 custom parameters of any type (including pointers and references). The only constraint on the function signature is that it must return a UCallbackAction, and take a const UMessage& as its last parameter. A few examples:


class Test {
   public:
     UCallbackAction onJoint(int value, const UMessage &msg);
}:

UCallbackAction  Test::onJoint(int value, const UMessage &msg) {
  msg.client.printf("got a message at %d with tag %s, our int is %d\n",msg.timestamp, msg.tag, value);
  return URBI_REMOVE;  //unregister ourself
}

int main(int argc, const char * argv[]) {
  Test *a = new Test();
  UClient * cl= new UClient(argv[1]);
  if (cl->error()) urbi::exit(1);
  cl->setCallback(*a, &Test::onJoint, 12, "tag");
  cl->send("tag: headPan.val;");
  urbi::execute();
}

Synchronous operations

The derived class USyncClient implements methods to synchronously get the result of URBI commands. You must be aware that these functions are less efficient, and that they will not work in the OPEN-R version of the liburbi, for instance.

Reading the value of a device

To get the value of a device, you can use the method syncGetDevice or syncGetNormalizedDevice. The first parameter is the name of the device (for instance, "neck"), the second is a double that is filled with the received value. The difference between the two methods is that syncGetDevice retreives the value with a "val" command, whereas syncGetNormalizedDevice uses "valn" (see urbidoc.html for more details about "val" and "valn").

double neckVal;
syncClient->syncGetDevice("neck",neckVal):

Getting an image

You can use the method syncGetImage to synchronously get an image. The method will send the appropriate command, and wait for the result, thus blocking your thread until the image is received.

client->send("camera.resolution = 0;camera.gain = 2;");
int width, height;
client->syncGetImage("camera", myBuffer, myBufferSize, URBI_RGB, URBI_TRANSMIT_JPEG, width, height);

The first parameter is the name of the camera device. The second is the buffer which will be filled with the image data. The third must be an integer variable equal to the size of the buffer. The function will set this variable to the size of the data. If the buffer is too small, data will be truncated .

The fourth parameter is the format in which you want to receive the image data. Possible values are URBI_RGB for a raw RGB 24 bit per pixel image, URBI_PPM for a PPM file, URBI_YCbCr for raw YCbCr data, and URBI_JPEG for a jpeg-compressed file.

The fifth parameter can be either URBI_TRANSMIT_JPEG or URBI_TRANSMIT_YCbCR and specifies how the image will be transmitted between the robot and the client. Transmitting JPEG images increases the frame rate and should be used for better performances.

Finaly the width and height parameters are filled with the with and height of the image on return.

Getting sound

The method syncGetSound can be used to get a sound sample of any length from the server.

client->syncGetSound("micro", duration, sound);

The first parameter is the name of the device from which to request sound, the second is the duration requested, in milliseconds. Sound is a USound structure that will be filled with the recorded sound on output.

Conversion functions

We also have included a few functions to convert between different image and sound formats. The usage of the image conversion functions is pretty straightforward:

int convertRGBtoYCrCb(const byte* source, int sourcelen, byte* dest);
int convertYCrCbtoRGB(const byte* source, int sourcelen, byte* dest);
int convertJPEGtoYCrCb(const byte* source, int sourcelen, byte* dest, int &size);
int convertJPEGtoRGB(const byte* source, int sourcelen, byte* dest, int &size);

The size parameter must be set to the size of the destination buffer. On return it will be set to the size of the output data.

To convert between different sound formats, the function convert can be used. It takes two USound structures as its parameters. The two audio formats currently supported are SOUND_RAW and SOUND_WAV, but support for compressed sound formats such as Ogg Vorbis and MP3 is planned.. If any field is set to zero in the destination structure, the corresponding value from the source sound will be used.

Putting it all together: some examples

Have a look at the examples, in the "example" or "utils" directory of the liburbi distribution. It currently contains:

  • urbiimage: Display the images taken by the camera in realtime, or save a snapshot to a file.

  • urbisound: Play the sound from the robot's microphone on the computer speaker, or record it to a file.

  • urbisendsound: Play a wav file from the computer, on the robot, converting it if neccesary.

  • urbiping: Send the URBI command 'ping' at regular intervals to measure latency.

  • urbibandwidth: Measure the effective bandwidth.

  • urbisend: Send a set of commands contained in a file to the robot.

  • urbirecord: Record all the movements of the robot to a file.

  • urbiplay: Play a file recorded with urbirecord, or dump it in a human-readable form.

  • urbimirror: Copy the movements of a robot on an other robot. Same as piping urbirecord to urbiplay, but with less latency.

  • urbiscale: Change the speed of a file recorded with urbirecord.

  • urbireverse: Reverse a file recorded with urbirecord.

  • urbicycle: Detects and extract cycles in a file recorded with urbirecord.

  • urbiballtrackinghead: Port of the OPEN-R ballTrackingHead example to URBI.

Each program when invoked with no option will display its command line syntax and additional informations when apropriate.

Programming hints

  • Except if what you are doing is trivial, try not to use the sync* functions. They are less efficient than the asynchronous ones.

  • The callback functions should return as fast as possible, since all callbacks are called by the same thread. If you have time-consuming operations, you should spawn an other thread and use synchronisation mechanisms such as semaphores or mutexes.

Portability functions

When other versions of the liburbi will be available, (in particular an OPEN-R version that will allow programs to run on the robot), it will be possible to compile the same code for both libraries, if a few rules are respected:

  • Do not use USyncClient.

  • Use the printf method of UClient instad of the standard version.

  • Use the getCurrentTime method of UClient instad of functions from the stdlib.

  • Use the urbi::exit function (in the "urbi" namespace) instead of exit.

  • At the end of your main, call urbi::execute.

  • Do not use threads, or any function call not implemented in the OPEN-R version of stdlib.