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:
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.
Objects are in turn organized in a tree structure, where some properties affect all the descendents of an object.
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.
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.
#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).
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.
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.
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:
peekabot::REPLACE_ON_CONFLICT policy for resolving name conflicts, which will simply replace the old object if there's already an object with the same path. The default is peekabot::AUTO_ENUMERATE_ON_CONFLICT, which will generate a non-conflicting name if there's a conflict. new and delete.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.
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().
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;
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.
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.
1.5.6