Tutorial: The basics

Architecture overview

peekabot is a distributed visualization system comprised of the peekabot server (the GUI application - run as "peekabot") and any number of clients. The peekabot server is accessed through a C++ client library, which takes care of networking, marshalling and other low-level details.

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 interchangeably.

Our goal is to provide an easy to use, flexible visualization tool that is useful in wide variety of situations and setups - simulations, data display for real robots, multi-robot systems, etc.

Visualization in peekabot is done by adding and manipulating different kinds of objects - spheres/ellipsoids, point clouds, occupancy grids, etc. All objects share some common properties: a name used to reference the object, a pose, a layer in which the object is rendered, etc. Objects in turn are organized in a tree structure:

scene_tree.png

An object's pose, opacity (by default) and hidden flag affect the its entire subtree, whereas other properties do not.

Objects in the scene are referred to by slash (/) separated paths, where each element of the path is an object name in the tree. Paths, absolute or relative, are used e.g. when adding new objects from the client. For example, "robot/chassis/left_wheel" refers to the left wheel in the above example. Note that there is no explicit path root; the root is implied from the context where appropriate. Since object paths are used to uniquely identify objects, object names must be unique among its siblings.

peekabot uses a right-handed coordinate system with positive Z up, as illustrated by the figure below. Positive rotation is counterclockwise about the axis of rotation and positive X is defined as the forward direction of all objects. If you use another coordinate system, you may experience some problems with this assumption - how the navigation works, or that Z is up by default for cylinders. One easy fix is to simply swap and reflect axes as appropriate before passing your coordinates to peekabot.

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 operations.

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

Using the "Property inspector" and "Scene browser" frames, on the right side of the GUI by default, you can browse the scene and inspect and change individual properties of all objects.

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>

#include <cmath> // needed for sinf
#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;
    client.connect("localhost");

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

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

    return 0;
}

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

What is and why is the call to PeekabotClient::sync() needed? Technically it's not needed, but highly recommended in most situations. 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 acute reader will have noticed that calling the SphereProxy::set_position() method a few million times per second might cause some problems - which the sync() call solves. The PeekabotClient::sync() method will block until all data is processed by the server. If you don't use syncing of some sort or otherwise limit the amount of data sent, the risk that either the client or the server will choke is high.

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 manually overridden, this directory is used for storing all peekabot files - a configuration file, screenshots, logs, etc. peekabot also searches for data files (models, scenes and textures) in this directory.

By default, peekabot will look for data files in .peekabot/data, and failing that it will look in a system-wide directory (or any user configured search paths, see Configuration options). You can also use client-side files by first uploading them to the server with the PeekabotClient::upload_file() method. Uploaded files always have precedence over server-side files.

Using peekabot to control a robot

This time around, we're going to make something that's a bit more useful than a simple sphere bobbing up and down - we're going to (ab)use peekabot for controlling a simple robot.

Let's start with some "theory": every method that involves doing something on the peekabot server returns a DelayedDispatch object on which you can call DelayedDispatch::status() to return a Status object tracking the outcome of the operation. I.e. if it has been processed, and if the operation succeeded or failed (and an error message, if it did). You rarely or ever have to use statuses if you don't want to, but obviously they're good for error checking.

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

// Block until the operation is processed, then check
// for succees
if( s.suceeded() )
{
    std::cout << "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().

Results are similar to status objects, but instead of just tracking the outcome of an operation they also fetch some data from the server. ObjectProxyBase::get_position() is one example. To illustrate how they are used, here's an example:

peekabot::Result<peekabot::Vector3f> 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;

Now, let us put the get_position() method to use in creating our small client controlling a robot based on the user's input. We'll create a small marker object which the user can move around, get the position of it and adjust our controller accordingly. On to the code!

#include <iostream>
#include <cassert>
#include <cmath>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <peekabot.hh>

namespace
{
    double normalize_rad(double rad)
    {
        rad = fmod(rad, 2*M_PI);
        if( rad > M_PI )
            rad = rad-2*M_PI;
        else if( rad < -M_PI )
            rad = rad+2*M_PI;
        return rad;
    }
}

int main(int argc, char *argv[])
{
    peekabot::PeekabotClient client;
    client.connect("localhost");

    peekabot::GroupProxy grp;
    grp.add(client, "grp", peekabot::REPLACE_ON_CONFLICT);

    peekabot::SphereProxy marker;
    marker.add(grp, "marker");
    marker.set_scale(0.1);
    marker.set_opacity(0.3);
    marker.set_position(0,0,1);

    peekabot::ModelProxy robot;
    client.upload_file("../data/pioneer_3dx.pbmf");
    robot.add(grp, "robot", "../data/pioneer_3dx.pbmf");
    double x = 0, y = 0, theta = 0;
    double v = 0, w = 0;

    peekabot::LineCloudProxy path, dir;
    path.add(grp, "path");
    path.set_color(0,0,1);
    std::size_t path_segments = 0;
    dir.add(robot, "direction");
    dir.set_color(0,0,0);
    dir.set_line_width(2);

    std::cout << "Select the 'marker' object in the GUI and move it around "
              << "in the XY-plane, and watch the robot follow it around!"
              << std::endl;

    // Control parameters
    const double k_rho = 3;
    const double k_alpha = 0.59;
    const double k_v = 1;
    const double v_max = 2;

    boost::posix_time::ptime t1(
        boost::posix_time::microsec_clock::local_time());

    while( client.is_connected() )
    {
        peekabot::Result<peekabot::Vector3f> marker_pos =
            marker.get_position();

        if( !marker_pos.succeeded() )
        {
            std::cerr << "Failed to get position of marker!" << std::endl;
            return -1;
        }

        double xT = marker_pos.get_result()(0) - x;
        double yT = marker_pos.get_result()(1) - y;
        double alpha = normalize_rad(atan2(yT, xT)-theta);
        double rho = sqrt(xT*xT + yT*yT);

        if( rho > 0.1 )
        {
            marker.set_color(0,1,0);
            v = k_v * v_max*cos(alpha) * tanh(rho/k_rho);
            w = k_alpha * alpha + k_v * v_max * tanh(rho/k_rho) * sin(2*alpha)/(2*rho);
        }
        else
        {
            marker.set_color(0,0,1);
            v = w = 0;
        }

        boost::posix_time::ptime t2(
            boost::posix_time::microsec_clock::local_time());
        double dt = (t2-t1).total_milliseconds() / 1000.0;
        t1 = t2;

        if( ++path_segments > 5000 )
        {
            path_segments = 1;
            path.clear_vertices();
        }

        path.add_line(
            x, y, 0,
            x + v*dt*cos(theta), y + v*dt*sin(theta), 0);

        x += v*dt*cos(theta);
        y += v*dt*sin(theta);
        theta = theta + w*dt;

        robot.set_pose(x,y,0,theta);
        dir.clear_vertices();
        dir.add_line(0,0,0, 2*v,0,0);
    }

    return 0;
}

The code and the required model file can be found in the examples directory of the distribution. Compile it, select the marker object in the GUI and move it around and the robot should follow it around.

Note how we specified the peekabot::REPLACE_ON_CONFLICT policy for resolving name conflicts, which will simply replace the old object if there's already an object at the same path. The default is peekabot::AUTO_ENUMERATE_ON_CONFLICT, which will generate a non-conflicting name if there's a conflict.

Bundles: Controlling when things are drawn

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.

Note that, since 0.8.0, each thread has its own bundle - calling begin_bundle() or end_bundle() in one thread will not affecting bundling in other threads. This allows bundles to be safely used in multi-threaded setups.

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();

Using bundles also has other benefits - typically allowing better compression ratios, for example (everything that is compressible gets compressed automatically, using bundles increases the block size and thus enables better compression ratios). Unless you have a good reason not to use bundles you probably should.

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 can be prohibitive.

One method that suffers less from latency issues is to sync data from the last frame - i.e. initialize synchronization, send data for the next frame and then make sure that 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();
}

If you only want to make sure your client wont choke because it 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. A call to PeekabotClient::flush() will block until all operations done prior to the call has been sent to the server. 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.

Thread safety

Since version 0.6, all proxy classes and the PeekabotClient class are fully thread-safe. Auxillary classes, such as VertexSet, are not.