wiki:Tutorials/Beginner/Crash Course 14.08 and 13.10

Finroc Crash Course

This tutorial is supposed to make you familiar with the Finroc Framework - briefly covering various fundamental topics for application development.

We will create a simple simulation from scratch: A robot moves in a planar environment with a wall. It is equipped with two distance sensors and it can be destroyed if it hits the wall too hard ("crash course").

As this is a crash course, do not worry about the rather steep learning curve. If you have difficulties completing certain steps, you can find the tutorial source files here (make sure you look at the 14.08 or 13.10 branch). Apart from that, we are always happy to receive feedback on any issues - and can hopefully help.

Note: The tutorial shows several ways of doing the same things: either in source code or with (optional) graphical tooling. This can be a little confusing. In your projects, you can later choose whatever you prefer. Among Finroc developers, preferences are almost equally distributed.

Dependencies

Install the dependencies we will need in this tutorial (we do not need any of the optional components):

~/finroc$ finroc_get finroc_plugins_structure finroc_plugins_tcp rrlib_canvas finroc_tools_gui-java finroc_tools_finstruct-java finroc_plugins_tcp-java rrlib_xml

Create project

All C++ source code is located in the subdirectory sources/cpp of the finroc home directory. All projects are located in sources/cpp/projects. We will now create a directory for our new project:

~/finroc$ mkdir -p sources/cpp/projects/crash_course

Finroc Application Structure & Decomposition in a Nutshell

Similar to many other robotic frameworks (MCA2 in particular), Finroc applications are constructed from interconnected components. A significant set of all kinds of reusable components are available in finroc_libraries_* repositories (currently, only few of them are available on finroc.org). Similar to MCA2, the structural elements in Finroc applications are modules (basic components), groups (composite components) and parts (executables/processes).

Modules are the basic application building blocks. Their interfaces are a set of ports. Often, these are data ports: Output ports are used to publish data, whereas Input Ports receive data. Edges are used to connect two ports. Ports can be connected n:m (however, typically only 1:n makes sense inside robotic applications). Modules with data ports connected by edges form a kind of data flow graph - which can be visualized with the finstruct tool.

Edges can connect components running on different systems just as well as components running inside the same process. This allows to easily create distributed systems.

Apart from data ports there are also rpc ports: Server ports provide an interface with remote procedure calls that client ports can connect to.

Finroc applications may consist of thousands of components. In order to maintain a clear application structure, modules can be placed in groups that have their own interface. Using groups, an application hierarchy is established.

Modules are typically assigned to thread containers. The thread inside the thread container will trigger any periodic update tasks continuously with a certain cycle time. It is, however, also possible to react to asynchronous events.

Similar to the MCA2 framework, Finroc distinguishes between sensor and controller data. Sensor data (yellow) flows upwards in application visualization. Controller data (red) flows downwards. Distinguishing between sensor and controller data in this way, supports clear application visualization and structure.

Finroc supports different component types. The SenseControlModule and SenseControlGroup are the respective component types from MCA2, extended by service ports. Apart from that, there are plain modules and groups or e.g. ib2c behaviours.

Create robot simulation group

As stated in the introduction, we will now create a simple simulation of a robot moving in a planar environment with a wall. It is equipped with two distance sensors and it can be destroyed if it hits the wall too hard.

The wall is built from (2, -∞) to (2, ∞).

It will be a very high-level implementation of a robot simulation. So we won't implement any modules for kinematics etc.

The robot simulation group will have two Controller Input ports: the velocity and angular velocity.

Sensor Output ports are the current position in the world coordinate system and two outputs from our simulated IR sensors: IR distance front and IR distance rear.

Creating the (empty) group

The simulation will consist of several modules. We will encapsulate all these modules in a single composite component - the simulation group. The group has its own interface - and the outside may only interact with the simulation using this interface.

The interactive finroc_create script is used to create code templates for all kinds of Finroc source files (edit /etc/content_templates.xml if you want to make changes to the default header). We will use it to create an empty group "Simulation":

~/finroc$ finroc_create

Choose C++ => Finroc Projects => SenseControlGroup => leave Implementation File (cpp) selected and choose OK => crash_course => Select "." => Enter "Simulation" => Enter "This group realizes a simple simulation for a differential-driven robot". Press Ctrl-D (possibly twice) => Enter you name => OK

This will create gSimulation.h and gSimulation.cpp in your project directory. These files already contain some code and examples.

Open sources/cpp/projects/crash_course/gSimulation.h and locate the block where the ports are defined.

//----------------------------------------------------------------------
// Ports (These are the only variables that may be declared public)
//----------------------------------------------------------------------
public:

  tControllerInput<double> ci_signal_1;

This section defines the interface of your group to the outside.

Replace the example port with the ports we want to have for our group:

  /*! Desired velocity (in m/s) */
  tControllerInput<double> velocity;

  /*! Desired angular velocity */
  tControllerInput<double> angular_velocity;

  /*! Position of our robot in the world coordinate system */
  tSensorOutput<rrlib::math::tPose2D> pose;

  /*! Simulated distance sensor values to the front and to the rear */
  tSensorOutput<double> ir_distance_front, ir_distance_rear;

The type in the brackets is the data type that will be transferred via this port. This can be any C++ type for which the stream operators for serialization have been overloaded (see Advanced/Suitable Port Data Types).

Note, that we use the type tPose2D from the rrlib_math library. That's why the following include needs to be added to the External includes section (in gSimulation.h):

//----------------------------------------------------------------------
// External includes (system with <>, local with "")
//----------------------------------------------------------------------
#include "rrlib/math/tPose2D.h"

Creating Modules

Main simulation module

Now, we will create the first module - for the main simulation. In the following, it is demonstrated how a module is is created.

~/finroc$ finroc_create

Create a SenseControlModule named "MainSimulation" in sources/cpp/projects/crash_course in more or less the same way as we created the group. Comment: "This module simulates a differential-driven robot and two distance sensors".

Note, that the generated source files already contain various comments on what kind of code belongs where.

  • The module shall have the same interface as our group. Open sources/cpp/projects/crash_course/mMainSimulation.h and replace the example ports with the same ports as in the group.
  • Add the include tPose2D.h again.
  • Below these ports, add two more ports and some parameters:
  /*! Pose of last collision */
  tSensorOutput<rrlib::math::tPose2D> last_collision_pose;

  /*! Counts the number of spawned robots */
  tSensorOutput<int> robot_counter;

  /*! Maximum acceleration of robot - in m/s² */
  tParameter<double> max_acceleration;

  /*! If the robot hits the wall with more than this speed, it is destroyed */
  tParameter<double> destructive_collision_speed;

  /*! Maximum range of IR sensors */
  tParameter<double> max_ir_sensor_distance;
  • We need some internal variables for our calculations. They belong in the private area of the class (before the private destructor):
//----------------------------------------------------------------------
// Private fields and methods
//----------------------------------------------------------------------
private:

  /*! Robot's current speed */
  double current_speed;

  /*! Robot's current position and orientation */
  rrlib::math::tPose2D current_pose;

  /*! Counts the number of spawned robots (internal) */
  uint robot_counter_internal;

Member variables must be initialized in the constructor - in the mMainSimulation.cpp. Otherwise, the variables with an elementary type will have an arbitrary/undefined value. Using an initializer list is the preferred way of initializing class members in C++.

Furthermore, we set some default values for parameters and some SI units for ports and parameters. Note that support for units is an experimental feature. However, later in this tutorial you will see what this feature is currently capable of. Choosing millimeters as output for the IR sensors here, is for demonstation purposes.

mMainSimulation::mMainSimulation(finroc::core::tFrameworkElement *parent, const std::string &name) :
  tSenseControlModule(parent, name),
  velocity(tUnit::m_s),
  ir_distance_front(tUnit::mm),
  ir_distance_rear(tUnit::mm),
  max_acceleration(0.3),
  destructive_collision_speed(0.9, tUnit::m_s),
  max_ir_sensor_distance(2000, tUnit::mm),
  current_speed(0),
  current_pose(),
  robot_counter_internal(0)
{}

For convenience, we will import/use some more namespaces.

Please note, that namespaces should should never be imported in header files (.h and .hpp).

//----------------------------------------------------------------------
// Namespace usage
//----------------------------------------------------------------------
using namespace finroc::data_ports;
using namespace rrlib::math;

The module's Sense() and Control() methods will be called regularly - every 40ms in this tutorial. It is important to ensure that the control flow does not get stuck inside these methods.

For now, just fill the Sense() method with this slightly clumsy simulation implementation (feel free to improve it). It is not important to understand the details of this particular implementation. You can just copy/paste it. Note that in this tutorial we do not use any existing Finroc components. Therefore, we need a little bit more code to get started and let the simulation do something somewhat interesting.

  rrlib::time::tDuration cycle_time = scheduling::tThreadContainerThread::CurrentThread()->GetCycleTime(); // cycle time of thread container
  double delta_t = std::chrono::duration_cast<std::chrono::milliseconds>(cycle_time).count() / 1000.0; // cycle time in seconds
  rrlib::time::tTimestamp now = rrlib::time::Now();

  // Calculate new speed
  double desired_speed = velocity.Get();
  double new_speed = 0;
  if (desired_speed >= 0)
  {
    new_speed = current_speed < 0 ? 0 : ((desired_speed < current_speed) ? desired_speed :
        std::min<double>(velocity.Get(), current_speed + max_acceleration.Get() * delta_t));
  }
  else
  {
    new_speed = current_speed > 0 ? 0 : ((desired_speed > current_speed) ? desired_speed :
        std::max<double>(velocity.Get(), current_speed - max_acceleration.Get() * delta_t));
  }
  double avg_speed = (current_speed + new_speed) / 2;
  current_speed = new_speed;

  // Calculate new orientation
  tAngleRad new_direction = current_pose.Yaw() + tAngleRad(angular_velocity.Get() * delta_t); 
  tAngleRad avg_direction = (current_pose.Yaw() + tAngleRad(new_direction)) / 2; 
  
  // Calculate new coordinates
  double s = avg_speed * delta_t;
  current_pose.Set(current_pose.X() + s * avg_direction.Cosine(), current_pose.Y() + s * avg_direction.Sine(), new_direction);

  // Did we collide with the wall?
  tPose2D back_left = current_pose;
  back_left.ApplyRelativePoseTransformation(tPose2D(-0.2, 0.2));
  tPose2D back_right = current_pose;
  back_right.ApplyRelativePoseTransformation(tPose2D(-0.2, -0.2));
  tPose2D front_center = current_pose;
  front_center.ApplyRelativePoseTransformation(tPose2D(0.1, 0));
  double max_x = std::max(back_left.X(), std::max(back_right.X(), front_center.X() + 0.2));
  if (max_x > 2)
  {
    last_collision_pose.Publish(current_pose, now);
    if (fabs(avg_speed) < fabs(destructive_collision_speed.Get()))
    {
      current_pose.SetPosition(current_pose.X() - (max_x - 2), current_pose.Y());
      current_speed = 0;
      FINROC_LOG_PRINTF(WARNING, "Robot collided with wall at a speed of %f m/s.", avg_speed);
    }
    else
    {
      robot_counter_internal++;
      robot_counter.Publish(robot_counter_internal);
      current_pose.Reset();
      current_speed = 0;
      FINROC_LOG_PRINTF(ERROR, "Robot crashed at a speed of %f m/s and was destroyed. Respawning. Destroyed Robots: %d.", avg_speed, robot_counter_internal);
    }
  }
  FINROC_LOG_PRINT(DEBUG_VERBOSE_1, "New robot position: ", current_pose);

  // Calculate sensor values
  tPose2D robot_in_2m = current_pose.Translated(tVec2d(2, 0).Rotated(current_pose.Yaw()));
  tPose2D robot_2m_back = current_pose.Translated(tVec2d(-2, 0).Rotated(current_pose.Yaw()));
  double front_distance = max_ir_sensor_distance.Get();
  double rear_distance = max_ir_sensor_distance.Get();
  double dx = fabs(robot_in_2m.X() - current_pose.X());
  if (robot_in_2m.X() > 2)
  {
    front_distance = (2 * (2 - current_pose.X()) / dx) * 1000.0;
  }
  if (robot_2m_back.X() > 2)
  {
    rear_distance = (2 * (2 - current_pose.X()) / dx) * 1000.0;
  }

  // publish updated values
  pose.Publish(current_pose, now);
  ir_distance_front.Publish(front_distance, now);
  ir_distance_rear.Publish(rear_distance, now);

Add the external include

#include "plugins/scheduling/tThreadContainerThread.h"

As you probably noticed, the generated source files contain prose text with instructions (that won't compile) at places you should pay attention to, before using the module. The methods OnStaticParameterChange() and OnParameterChange() are such candidates. As we do not use them in this module, you can delete them completely (from the .h and the .cpp file). Also, delete the content of the Control() method.

Sensor noise simulation module

Now we will create a plain Module (no SenseControlModule) for simulating sensor noise. It will add a gauss-distributed random value to the numeric input. Call it "AddNoise". Comment: "Adds noise (a gauss-distributed random value) to the numeric input."

~/finroc$ finroc_create

We create a plain Module instead of a SenseControlModule, because we cannot really say if the processed values in this module are always sensor data (in this tutorial we can, but let's say it will become a generic library module).

Open sources/cpp/projects/crash_course/mAddNoise.h and replace the example ports with:

  /*! Input value */
  tInput<double> input;

  /*! Output value (= input value with added noise) */
  tOutput<double> output;

  /*! Standard deviation for added noise */
  tParameter<double> standard_deviation;

Furthermore, we add two private variables for random number generation:

  std::normal_distribution<double> normal_distribution;
  std::mt19937 eng;

This requires an external include:

#include <random>

In the mAddNoise.cpp file, add

using namespace finroc::data_ports;

In the constructor, we set the port's unit to meters (only for demonstration of the unit feature ;-) ). Furthermore, we set the module's standard deviation to 0.05m.

mAddNoise::mAddNoise(finroc::core::tFrameworkElement *parent, const std::string &name)
    : tModule(parent, name),
    input(tUnit::m),
    output(tUnit::m),
    standard_deviation(0.05, tUnit::m),
    normal_distribution(0, 0.05),
    eng(1234)
{}

This module only has an Update() method to fill in. We want to add random noise to the input signal and publish the result - every cycle. Open the file mAddNoise.cpp and insert:

output.Publish(input.Get() + normal_distribution(eng));

Note: As noise is always to be added (not only if the input changes), we removed the if-block from Update().

When the parameter changes, we need to reinitialize the normal_distribution. Insert the following in OnParameterChange():

void mAddNoise::OnParameterChange()
{
  normal_distribution = std::normal_distribution<double>(0, standard_deviation.Get());
}

OnParameterChange() is called by the framework, whenever one of the module's parameters change (and never concurrently to Update()).

Again, delete the obsolete code and prose fragments from the source files.

Instantiate and connect modules

Now, we need to put such modules inside our group. We can do this either in source code or with a graphical tool (modifying XML structure information). We will do this in source code here. Currently, our group is empty.

The modules that we created should be instantiated in the group's constructor. Open sources/cpp/projects/crash_course/gSimulation.cpp and insert the following block into the constructor:

  // create modules
  mMainSimulation* main_sim = new mMainSimulation(this);
  mAddNoise* add_noise = new mAddNoise(this, "AddNoise Front");

  // connect some ports
  velocity.ConnectTo(main_sim->velocity);
  angular_velocity.ConnectTo(main_sim->angular_velocity);
  main_sim->pose.ConnectTo(pose);
  main_sim->ir_distance_front.ConnectTo(add_noise->input);
  add_noise->output.ConnectTo(ir_distance_front);

This will create the two modules and connect their ports. (We will instantiate and connect a second AddNoise module for the rear sensor later - graphically).

Furthermore, we need to include the two module types. (These belong in the internal includes section, because they come from the same project/repository)

#include "projects/crash_course/mMainSimulation.h"
#include "projects/crash_course/mAddNoise.h"

Create part

In Finroc, there are two ways to create an application that can actually be executed.

  • Create a binary executable
  • Create a .finroc file using finstruct and execute it using finroc_run

We will create a binary executable here. When the finstruct tool is introduced in the next chapter, the second option is explained.

~/finroc$ finroc_create

Create a part in sources/cpp/projects/crash_course and name it "CrashCourse".

Open sources/cpp/projects/crash_course/pCrashCourse.cpp. At the end, replace

  new finroc::crash_course::mSimulation(main_thread);

with (we want to place our simulation group below the top-level thread container):

  new finroc::crash_course::gSimulation(main_thread);

Then, set the main thread's cycle time to to 40ms:

  main_thread->SetCycleTime(std::chrono::milliseconds(40));

Furthermore, projects/crash_course/gSimulation.h needs to be included instead of projects/crash_course/mCrashCourse.h.

Furthermore we can rename the application's root element to "Crash Course" by changing the variable cMAIN_THREAD_CONTAINER_NAME.

Finally, we need to tell our build system what it should build. Therefore we create a make.xml file in sources/cpp/projects/crash_course:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<targets>
  <library>
    <sources exclude="pCrashCourse.cpp">
      *.cpp
    </sources>
  </library>

  <program name="finroc_crash_course">
    <sources>
      pCrashCourse.cpp
    </sources>
  </program>
</targets>

This builds a library libfinroc_projects_crash_course.so containing all our new components. Furthermore, a program called finroc_crash_course will be created. See Advanced/MakeBuilder for details on the make.xml file format.

Build Part

So now everything should be fine and we can start compilation. If Finroc is not installed on your system, this will take a moment.

in Finroc 13.10:

~/finroc$ makeSafe -j4

in any newer version of Finroc:

~/finroc$ make -j4

(4 stands for the number of parralel build jobs. A value of number of CPU-cores + 1 is recommended)

If there are no errors, the build operation finishes with a done message.

Start Part

We can now start the part. As all Finroc scripts and programs, the --help command line options lists the available already predefined options for starting the program:

~/finroc$ finroc_crash_course --help

The default options are fine.

~/finroc$ finroc_crash_course

Finstruct

Now, the part is running, but we do not see anything except of the console output yet. To inspect and interact with this part, we have the two tools fingui and finstruct.

Since our console running the part is blocked, we need to open another one. (In the new console we need to run source scripts/setenv in the Finroc directory again.)

Finstruct should be started by typing

~/finroc$ finstruct

At startup it asks where to connect to. localhost:4444 is fine.

Click on Simulation in the left tree. You should now see the structure of gSimulation group we just edited.

The tree view on the left can be used to navigate through the hierarchy of framework elements.

If you want, you can experiment with this tool. You should not worry about breaking anything. Since the part is hard-coded, it is always possible to restart it and everything should return to normal.

Inspect port and parameter values

Navigate to the AddNoise Front module's Outputs. The view will change and show the current value of the output port.

Now select the "AddNoise" module in the left tree and View->Port Data (View->PortView in Finroc 13.10) from the menu. This will show all the module's ports and its parameter.

If you press Single Update (blue toolbar button on the right), the displayed port values will be updated once (currently, only Output will change). If you activate Auto Update, the port values will be updated regularly.

Deactivate auto-update and enter 5 m as Standard Deviation Parameter and press the green "Apply" button. Note, how the output value range changes (you need to update values again to see the effect).

Now enter 5 cm.

Here you can see what effects setting units in ports and parameters has: Values retrieved via Get() are converted to the port's unit if the incoming value has another unit. Units of output ports are merely attached to the value (Nevertheless, we encourage developers to use plain SI units whenever possible - e.g. meter, gram).

Now, set the view back to Auto Select in the View menu.

Graphically create and connect modules

So now we are going to create a second AddNoise module from within finstruct. Navigate back to see the structure of the gSimulation again (as in the first finstruct image) and select Connect mode in the tree toolbar.

If you click on the arrows in the diagram on the right, the corresponding connections are shown in the connection panel on the left.

Right-click in the diagram on the right and select Create Element.... A dialog appers. Change the module name to AddNoise Rear.

Click on Create & Edit to create the module.

Now click on Main Simulation and drag it to AddNoise Rear.

Both modules will be expanded in the trees on the left.

Now connect Ir Distance Rear with Input via dragging again.

In the same way, connect AddNoise Rear/Output with Sensor Output/Ir Distance Rear (of the Simulation group!).

If you look at the ports of AddNoise Rear, you can see that it is already operating.

Now save the changes to our group by right-clicking in the right diagram and selecting Save "projects/crash_course/gSimulation.h.xml".

The result can be viewed in sources/cpp/projects/crash_course/gSimulation.h.xml.

<?xml version="1.0" encoding="UTF-8"?>
<FinstructableGroup>
  <element name="AddNoise Rear" group="finroc_projects_crash_course" type="AddNoise">
    <parameters/>
  </element>
  <edge src="MainSimulation/Sensor Output/Ir Distance Rear" dest="AddNoise Rear/Input/Input"/>
  <edge src="AddNoise Rear/Output/Output" dest="Sensor Output/Ir Distance Rear"/>
</FinstructableGroup>

Note, that such files may also be edited with a text editor.

If you stop the part (press Ctrl-C) and start it again, the AddNoise Rear module should be instantiated and connected.

Graphically create a part

As mentioned earlier, parts can also be created using finstruct.

Terminate the finroc_crash_course part.

Make sure, the $FINROC_PROJECT_HOME environment variable is set to /user/home/finroc/sources/cpp/projects/crash_course . You will probably have to call

~/finroc$ source scripts/setenv -p crash_course

once. You only need to add -p if you want to change the current project.

Changing the project is important so that the CrashCourse.finroc file containing the part will be loaded from and saved relative to the project directory.

.finroc files can be instantiated and executed using the finroc_run command. If the file does not yet exist, it will be created on saving.

~/finroc$ finroc_run CrashCourse.finroc

Since $FINROC_PROJECT_HOME/CrashCourse.finroc does not exist yet, an empty part will be loaded.

In finstruct right-click and select Create Element.... Our modules are not loaded yet. Therefore, click on Load... and double-click on finroc_projects_crash_course.

Select the component Simulation (finroc_projects_crash_course) and click on Create & Edit.

Now, we have the same part.

If you right-click and save (Save "CrashCourse.finroc"), the file $FINROC_PROJECT_HOME/CrashCourse.finroc will be written.

Note, that depending on where you are navigating, either the group or the part is saved: If the part is selected in the left tree, the part is saved. If the group is selected, the group is saved. You can see in the part's command line output which file was saved.

Fingui

Now, we will actually interact with our part using a graphical user interface.

GUI files, as well as config files and all other non-finroc code, is typically stored in an etc folder below our project directory.

~/finroc$ mkdir -p sources/cpp/projects/crash_course/etc

Start the gui.

~/finroc$ fingui

You will see an empty canvas where GUI widgets can be placed.

The Fingui allows to select your preferred Look & Feel in the Edit-menu. Here's a brief summary on the default setting ("Office-like"):

  • Select widget: <Strg> + left mouse button or select with a box (you can drag it by clicking on empty space and moving the mouse over the widgets)
  • Change widget properties: Right-click on a selected widget

Create a GeometryRenderer, a VirtualJoystick, and two LCD numeric displays. You can assign labels to the LCDs in their properties dialog.

Download this .svg file and add it to the map objects in the GeometryRenderer properties.

As the coordinates are in mm, select artos.svg, press Edit..., and set the scale to 0.001.

artos.svg

Furthermore, you need to invert the X axis of the virtual Joystick: X Left should be 1 and X Right should be -1.

Select File->Connect->TCP from the main menu and accept localhost:4444.

Now click on View->Connection Panel in the menu to open the connection panel you should already be familiar with from finstruct.

Connect the widgets in this way:

Now save the gui to your project's etc folder.

You can now experiment with the application you just built.

More advanced visualization using tCanvas2D

Often, it is helpful to visualize internals of a robotic application. The class tCanvas2D provides a flexible and powerful solution for this purpose.

The visual representation of the simulation in the geometry renderer is currently very basic. We will now create a visualization of the simulation in a seperate module that displays the wall etc.

Create a plain (non-sense-control) module mVisualization ("Visualizes the simulated scene using a tCanvas2D.").

Ports are

  /*! Position and orientation of robot in the world coordinate system */
  tInput<rrlib::math::tPose2D> pose;

  /*! Pose of last collision */
  tInput<rrlib::math::tPose2D> last_collision_pose;

  /*! Counts the number of spawned robots */
  tInput<int> robot_counter;
  
  /*! Visualization of simulation */
  tOutput<rrlib::canvas::tCanvas2D> visualization;

The includes for the specified port types need to be added:

#include "rrlib/math/tPose2D.h"
#include "rrlib/canvas/tCanvas2D.h"

Furthermore, we add a private variable that is to be initialized with false in the constructor.

  /*! Was last collision destructive? */
  bool last_collision_destructive;

The Update() method illustrates how geometry can be drawn to the canvas objects and how it can be published via ports:

  if (this->InputChanged())
  {
    // visualize using tCanvas2D
    // obtain buffer
    data_ports::tPortDataPointer<rrlib::canvas::tCanvas2D> canvas = visualization.GetUnusedBuffer();
    canvas->Clear();

    // Draw wall
    canvas->SetFill(true);
    canvas->SetColor(128, 64, 0); // brown
    canvas->DrawBox(2, -1000, 2000, 2000);

    // Draw Robot Text
    rrlib::math::tPose2D current_pose = pose.Get();
    canvas->Translate(current_pose.X(), current_pose.Y());
    canvas->SetColor(255, 255, 255);
    char robot_text[128];
    snprintf(robot_text, 128, "Tutorial Robot #%d", robot_counter.Get() + 1);
    canvas->DrawText(0.0, 0.28, robot_text);
    canvas->ResetTransformation();

    // Draw Robot
    canvas->Transform(current_pose);
    canvas->SetEdgeColor(0, 0, 0);
    canvas->SetFillColor(200, 200, 255);
    canvas->DrawEllipsoid(0.1, 0.0, 0.4, 0.4);
    canvas->DrawBox(-0.2, -0.2, 0.3, 0.4);
    canvas->ResetTransformation();

    // Indicate Collision
    if (last_collision_pose.HasChanged())
    {
      last_collision_destructive = (current_pose == rrlib::math::tPose2D::Zero();
    }
    rrlib::time::tTimestamp last_collision_timestamp;
    rrlib::math::tPose2D last_collision = last_collision_pose.Get(last_collision_timestamp);
    rrlib::time::tDuration since_last_collision = rrlib::time::Now() - last_collision_timestamp;
    if (since_last_collision < std::chrono::milliseconds(2000))
    {
      canvas->Translate(last_collision.X(), last_collision.Y());
      double time_factor = 1.0 - (std::chrono::duration_cast<std::chrono::milliseconds>(since_last_collision).count() / 2000.0);
      canvas->SetAlpha(static_cast<uint8_t>(255.0 * time_factor * time_factor));
      canvas->SetColor(last_collision_destructive ? 255 : 50, 50, last_collision_destructive ? 50 : 255);
      canvas->DrawText(0.0, 0.0, last_collision_destructive ? "KABOOM!" : "Plonk");
    }

    visualization.Publish(canvas);
  }

Build the project, restart the part, add the mVisualization module to the simulation group, and connect the ports appropriately (3 ports from MainSimulation/Sensor Output to Visualization/Input).

Remove artos.svg from the geometry renderer widget of the GUI and connect the output of the mVisualization module instead.

You should be able to observe how each of the objects in the module's source code are drawn.

Parameters and config files

Parameters' default values in Finroc applications can also be set via config files.

Note that parameters and config files are an area, which we plan to significantly improve in the near future. Especially the graphical tool support is still in an experimental and very basic state.

Nevertheless, let's create a simple config file fragile_bot.xml in sources/cpp/projects/crash_course/etc with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <value name="noiselevel">15 cm</value>
  <node name="some structural element">
    <value name="destructive_collision_speed">0.1</value>
  </node>
</root>

Now start the part with this config file:

~/finroc$ finroc_run -c sources/cpp/projects/crash_course/etc/fragile_bot.xml CrashCourse.finroc

In finstruct, navigate to the simulation group and select Parameter-Connect mode from the tree toolbar.

You can now see the config file entries on the right. They can be connected to the parameters. Connect the Standard Deviation Parameter of both AddNoise modules to noiselevel. Also connect the main simulation module's Destructive Collision Speed parameter.

Save the part (CrashCourse.finroc).

(You might notice some new parameter entries in the .finroc file.)

This way, it is possible to create different configurations for the same application.

If you prefer to attach parameters to config file entries in source code, you could, for instance, add the following line to the constructor of gSimulation.cpp instead:

main_sim->destructive_collision_speed.SetConfigEntry("some structural element/destructive_collision_speed");

It is furthermore possible to set a (default) value from source code:

main_sim->destructive_collision_speed.Set(0.1);

Set parameters from the command line

It is also possible to configure parameters to be read from the command line. However, this is not yet possible from finstruct.

Open the CrashCourse.finroc file and add cmdline="front-noise" to the front noise parameter:

  <parameter link="Simulation/AddNoise Front/Parameters/Standard Deviation" config="noiselevel" cmdline="front-noise"/>

You can now see, that a new command line parameter has been added:

~/finroc$ finroc_run --help CrashCourse.finroc

Exercises

Up to this point, you completed a Finroc crash course. The many topics that are covered might seem a little overwhelming and confusing at first - especially as several ways of doing the same thing are presented. As experience shows, however, things become much easier as you continue using Finroc. If you had particular difficulties completing one of the steps above, we are always thankful to receive a hint.

In order to deepen your knowledge on the topics covered in this tutorial, you can try to do the following exercises by yourself.

Add a robot control group and implement filter

Add a SenseControlGroup "Control" to the part. To keep the project directory tidy, use a subdirectory control (a subdirectory for simulation components could have been added as well). It's supposed to contain the robot control system. Add Sensor Input ports for the simulation's Sensor Outputs - and Controller Output ports for the simulation's Sensor Inputs. Add the control group to the part and connect it.

Implement a simple module (mNoiseFilter) to reduce the noise in the distance sensor signals. Structurally, it is somewhat similar to the one that adds the noise. Of course, it needs to do something different in Update().

It is part of the robot control, so instantiate and connect it there.

A simple filtering approach for this module is sufficient (e.g. sum up the last 5 values and calculate the arithmetic mean).

If you need a list - similar to Java's ArrayList - you could try std::vector.

Visualize distance sensor values

Add two input ports to the mVisualization module:

  /*! Simulated distance sensor values to the front and to the rear */
  tInput<double> ir_distance_front, ir_distance_rear;

Connect the distance sensor input ports to the outputs of the two AddNoise modules.

Modify mVisualization's Update() method so that the sensor values are visualized using the Canvas2D (possibly as lines).

Note that the simulated sensors measure the distance from the center of the robot.

Last modified 4 years ago Last modified on 04.11.2014 01:19:59

Attachments (18)

Download all attachments as: .zip