Tutorial

Architecture overview

peekabot is a distributed visualization system comprised of the peekabot server (the GUI application - run as "peekabot") and any number of clients. To facilitate easy use a client library is provided. It exposes a C++ API which the client(s) use to communicate with the peekabot server, the client library itself takes care of all the nitty gritty details like networking and marshalling.

sys_arch.png

Although, technically, "peekabot" is used to refer to the entire system, the visualization server and the client library, the terms "peekabot server" and "peekabot" are used interchangably.

The goal of peekabot is to provide easy-to-use, flexible and lightweight real-time visualization for your robotics application no matter how and where it is deployed. Typical scenarios where peekabot could be used are:

In addition, systems that operate both on simulated data and in the real world will enjoy the same visualization capabilities in both settings.

Hopefully, you now have a good overview of what peekabot does, how it is structured and what terms are used to refer to different parts of it.

Core concepts

You do visualization with peekabot by adding and manipulating different kinds of objects - spheres, robots, sensors, point clouds, etc. Some are very general (e.g. cubes, spheres) and some are very specific to the robotics domain (e.g sensors, robots, occupancy grids) but they all share some common properties:

Pose and name excepted, all these properties relate only to how the objects are visualized.

Objects are in turn organized in a tree structure, where some properties affect all the descendents of an object.

scene_tree.png

An object's pose is specified relative the parent object - if the parent moves, all of its descendents move as well. Opacity is also specified relative the parent node unless overriden. Hiding an object hides the entire subtree. All other properties affect only the object itself.

Object names and paths
Object names are not allowed to contain dots, which are used to delimit different names in an path. Paths are used to identify objects from the client. For example, "root.robot.chassis.left wheel" refers to the left wheel in the above example.
Since object paths are used to uniquely identify objects, object names must be unique among its siblings.
Coordinate system
As illustrated by the figure below, peekabot uses a right-handed coordinate system. Positive rotation is counterclockwise about the axis of rotation and positive X is defined as the forward direction of all objects.
coord_sys.png

An overview of the GUI

Before you continue, take a second to familiarize yourself with the GUI of the peekabot server. Use the left mouse button to pan the camera, the right mouse button to rotate and the middle mouse button or the mouse wheel to zoom. Pressing CTRL and SHIFT modualtes the sensitivity of mouse opertions.

You can select objects in the view by left-clicking, combine with CTRL to toggle select and SHIFT to add to the selection. Right-clicking deselects all objects.

gui_overview.png

A minimal example client

We're going to start out with a very simple example: adding and moving a sphere. The code is fairly self-explanatory so I'll let it speak for itself:

#include <cmath>
#include <peekabot.hh>

int main(int argc, char **argv)
{
    // You always need a peekabot client, it handles the connection
    // to the server and takes care of "low level" stuff
    peekabot::PeekabotClient client;

    // Connect to server on localhost, port 5050 (throws on error)
    client.connect("localhost", 5050);

    // All objects are manipulated through proxy objects
    peekabot::SphereProxy sphere;
    // Add a sphere object called "my_sphere" under the root node
    sphere.add(client, "root.my_sphere");

    float t = 0;
    while( client.is_connected() )
    {
        sphere.set_position(0, 0, sinf(t += 0.1f));
        client.sync();
    }

    return 0;
}

What is and why is the call to PeekabotClient::sync() needed? With a few exceptions, all client API methods are asynchronous - i.e. they will not block until the operation has been carried out, or even sent to the server. The data will simply be queued up for transmission as soon as possible.

This is a good thing - you can mix peekabot visualization calls with the code controlling the robot without worrying about stalling due to a slow connection or similar. If a method blocks or has a non-neglible execution time it's always explicitly documented.

The PeekabotClient::sync() method will block until all data is processed by the server. If you don't do any type of synchronization, you run the risk of queuing more data than can be sent and/or processed by the server, choking either the client or the server as a result. If you only send small amounts of data at a low frequency, the PeekabotClient::sync() call can safely be omitted, although we wouldn't recommend it.

To compile the example, link with the peekabot client library (-lpeekabot).

Scene definition files

In order to make extending peekabot with your own robots easy, robots are defined using XML markup in what we call scene definition files, or scene files for short. They're used to define the rendered geometry of the robot, its sensors and any attached manipulators.

Not only can scene files be used to define new robots, but also for defining non-robot geometry - the lab where the robot is operating, for example. The benefits of using scene files over manually constructing robots and its environments from the client is twofold: less code has to be written and you can change the graphical representation without recompiling the client.

The .peekabot directory

Before we continue on to make a second, more interesting, client, we're going to go through where peekabot stores and searches for resources.

When you first ran peekabot, it created a .peekabot directory in your home directory (on Windows, it uses your Application Data directory rather than your home directory).

Unless you manually overriden, this directory is used for storing all files saved - a configuration file, snaphots, logs, etc. peekabot also looks for resources in this directory.

By default, peekabot will look for scene files in .peekabot/scenes, and failing that it will look in a system-wide directory (/usr/share/peekabot/scenes or similar, depending on where you installed it). Of course, you can still specify absolute or relative paths as usual.

peekabot tries to load model files and textures from a similar set of locations. First it will search for the file in the same directory as the file that referenced it (e.g. if "foo.xml" requests "model.pbmf" to be loaded, the directory containing "foo.xml" is first searched) and then, by default, it searches in .peekabot/scenes and .peekabot/models.

Note:
Search paths for all resource types are configurable, see the section on Configuration.

Our second client

This time around, we're going to make something that's a bit more useful than a simple sphere bobbing up and down.

The scenario is still pretty basic: we'll define a robot with a laser scanner, load it from a client and send it some made-up readings.

Let's start with the XML definition of our robot:

<?xml version="1.0"?>
<scene>
  <model> <!-- attach a model of a P3-DX -->
    <file>pioneer3dx.pbmf</file>
  </model>

  <!-- Add a rangefinder sensor -->
  <sensor type="rangefinder">
    <name>scanner</name>

    <!-- Position the rangefinder -->
    <transform>
      <translate system="world">0.05 0 0.30</translate>
    </transform>

    <!-- Supplement the readings with some pretty graphics -->
    <children>
      <model>
        <file>sick_lms200.pbmf</file>
      </model>
    </children>

    <!-- Set the number of samples, etc. -->
    <params>
      <int name="samples">361</int>
      <float name="angle_uncertainty">0.25</float>
    </params>
  </sensor>
</scene>

Copy, paste and save the above as test_robot.xml in your .peekabot/scenes directory. The LMS200 and P3-DX models are included in the peekabot distribution so you don't have to worry about those.

Observe that the rangefinder sensor definition includes a name element - we'll use the name property later on to refer to it. If no name is given explicitly, one will be automatically generated.

More information on what sensor parameters are available, and their semantics, can be found in the documentation for the respective sensor type (e.g. Rangefinders).

Below is a client that loads the robot we just specified and sends it some readings:

#include <iostream>
#include <cstdlib>
#include <peekabot.hh>

int main(int argc, char*argv)
{
    peekabot::PeekabotClient client;
    try
    {
        client.connect("localhost", 5050);
    }
    catch(std::exception &e)
    {
        std::cerr << "Could not connect to peekabot server: " 
                  << e.what() << std::endl;
        return -1;
    }

    // Add a robot and give it the name "optimus_prime"
    peekabot::RobotProxy robot;
    robot.add(client, "root.optimus_prime", 
              peekabot::REPLACE_ON_CONFLICT);

    // Load our defined sensors and geometry
    robot.load_scene(robot, "test_robot.xml");

    // Assign a proxy to its laser scanner
    peekabot::SensorProxy scanner;
    scanner.assign(robot, "scanner");

    while( client.is_connected() )
    {
        peekabot::SensorProxy::SensorData reading;
        float d[361];
        // Make up some bogus rangings...
        for( int i = 0; i < 361; ++i )
            d[i] = 3*(1+rand()/float(RAND_MAX)*0.02-0.01);
        
        // Buffer the data to be sent in the sensor proxy
        reading.write(d, 361);
        // Send the readings to peekabot and update the sensor's visualization
        scanner.update(reading);

        // Make sure the server processed what we've sent
        client.sync();
    }

    return 0;
}

A few new things was introduced in the code above:

Note:
Proxies are lightweight objects that can safely be stored in STL containers with very little memory overhead. There's usually no need to mess with new and delete.

Bundles

Usually it is important that the effects of a set of operations are shown in the same frame. For example, if you're visualizing a sensor reading of some sort it's probably desirable that a frame never contains a mix of new and old readings - since that's likely to mislead the observer. Since clients don't have a say in when peekabot chooses to render a frame this can happen if you don't use bundles, which were devised to solve this problem.

Every operation made between a pair of calls to PeekabotClient::begin_bundle() and PeekabotClient::end_bundle() are guaranteed to take effect inbetween consecutive frames.

peekabot::PeekabotClient client;
...
client.begin_bundle();
// These operations are guaranteed to be executed between two frames
object1.set_scale(1,2,0.8);
object2.set_pose(8,-6,w);
...
proxy.end_bundle();

As an extra benefit, using bundles lowers the bandwidth usage somewhat, with a strong emphasis on somewhat.

Checking for failures

Every method that involves doing something on the peekabot server returns a DelayedDispatch object. Calling DelayedDispatch::status() will return a Status object you can use to monitor whether the operation suceeded or failed, and whether or not it has been processed by the server yet.

peekabot::PointCloudProxy pc;
peekabot::Status s = pc.add(client, "root.foo.bar").status();

// Block until the operation is processed, then check
// for succees
if( s.suceeded() )
{
    std::cout << "root.foo.bar added!" << std::endl;
}
else
{
    std::cerr << "Adding point cloud failed! Error: " 
              << s.get_error_message() << std::endl;
}

You can also do non-blocking checks on the Status object by using Status::has_completed() and Status::get_outcome().

Requesting data from the server

There are some operations that fetch data from the server, ObjectProxyBase::get_position() is one example. Such methods always return a Result object, which is used to determine when the data is available and to access the data.

Result objects are built on top of Status objects, so the usual way of checking whether the operation has completed can be used.

To illustrate how they are used, here's an example:

peekabot::Result<peekabot::Vector3ru> r = some_object.get_position();

// Block until the opertion is completed and check to 
// see if it succeeded
if( r.succeeded() )
  std::cout << "The position of some_object is " << r.get_result() << std::endl;
else
  std::cout << "Oops, could not get position of some_object" << std::endl;

Synchronization and flushing

We already mentioned that you can use sync() to perform synchronization with the server. But if you want to update at high frequencies, the latency involved with a PeekabotClient::sync() call can be prohibitive.

Another method that suffers less from latency issues is to sync data from the last frame - i.e. intialize synchronization, then send data for the next frame and at last make sure the data from the previous frame was received and processed:

while(should_keep_sending_frames)
{
    // Perform a no-op and track it's status
    peekabot::Status s = client.noop().status();

    // Send frame data here:
    ...

    s.wait_until_completed();
}

The benefits of doing synchronization from the last frame will be much less dramatic if you've enabled low latency mode, described in the next section.

If you only want to make sure your client wont choke because it's proxy has too much data queued up for transmission, or you must make sure some data was sent (but not necessarily processed but the server) then there's another tool at your disposal: Flushing. Flushing makes sure all operations done prior to calling the PeekabotClient::flush() method are sent. We only recommend this if you know what you are doing, since it's quite easy to send more than the server can handle if you're not careful.

Low latency mode

The latency introduced due to packet merging in the TCP protocol can be significant, up to 40 ms on typical Linux machines. This is significant only if you need really snappy response in the server or use a lot of operations that require data to be transmitted back to the client (status monitoring, results).

Each TCP packet has a header attached. If the packets are small the bandwidth overhead introduced by the headers will be significant. To remedy this small packets are merged into bigger ones. This is a good thing if you're looking to achieve high transfer rates, but interferes with getting low latency as packets will be delayed waiting for other packets to merge with. Note that transmitting more data when packet merging is enabled can actually lower the latency.

Packet merging can be turned off when calling PeekabotClient::connect() by specifying true for the third (optional) argument. Low latency mode is disabled by default.


Get peekabot at SourceForge.net. Fast, secure and Free Open Source software downloads
Generated on Tue Mar 17 22:47:22 2009 for peekabot by  doxygen 1.5.6