This section drives you through the different steps to create and implement a new package to control a robot that was not integrated in RKCL before. As explained here, this requires to provide 3 distinct elements to RKCL:

  • A kinematic description of the robot using either a YAML or URDF description file.
  • A driver implementation that matches RKCL format, to communicate with the hardware components.
  • A dedicated application along with a configuration file which allows to parametrized almost everything.

This tutorial will take as an example the existing package rkcl-ur-robot to explain how one can achieve the integration and utilization of a new robot in RKCL. This package intends to provide resources for Universal Robots manipulator arms. So far, only the UR10 robot has been integrated but other models can be added in the future. The rkcl-ur-robot package also includes an application example that uses the provided model to perform a simple scenario.

In this application example, we use the existing V-REP driver to run the scenario on simulation. Implementing a new driver to communicate with a real robot is outside the scope of this tutorial. However, do not hesitate to contact us for any information on this purpose.

1. Deployment and overview of the package

Before getting to the heart of the matter, be sure to have your workspace ready to use. If not, please follow the install section before continuing. You should also download and launch the V-REP robot simulator (recently renamed CoppeliaSim) to run the scenario on a virtual environment. You can get the latest version of the software here

First, let’s start by deploying the rkcl-ur-robot package. As shown in the install section, this can be achieved by performing the following commands:

cd pid-workspace
pid deploy package=rkcl-ur-robot

This will download and install the rkcl-ur-robot package and all its dependencies. Now you can explore the content of the rkcl-ur-robot package which is stored in the pid-workspace/package folder.

The package is structured as any PID package. The CMakeLists.txt file at the root of the package declares rkcl-ur-robot as a PID package and list its dependencies. Thanks to this, the deploying process that we just performed was able to get and link the rkcl-ur-robot package with the proper versions of the dependencies.

What interests us in this package is located in the share and apps folders. Inside the resources sub-folder, the former contains the robot model, a V-REP scene and configuration files used to parametrize the application scenario. The latter contains C++ application files used to create executable files.

2. Robot description

The robot model is described in the file ur_models/ur10.yaml using the YAML language. The description is composed of two elements: first, the links of the robot are listed with user-selectable names. Then, the rest of the file is dedicated to the definition of the joints. They express how the previously enumerated links are connected together with different types of joints and indicate the spatial transforms between the links. It is also possible to define the limits of the robot that will be taken into account in the control process.

3. Declaring a new application

Now let’s see how to use this new robot in an application example. The C++ application file that is used to generate the executable can be found at apps/ur_simple_simu_example/main.cpp. To take it into account while building the package, the application has to be declared as a new PID component within the CMakeLists.txt file of the apps folder:

declare_PID_Component(
    EXAMPLE_APPLICATION
    NAME ur-simple-simu-example
    DIRECTORY ur_simple_simu_example
    RUNTIME_RESOURCES ur_models ur_config ur_logs
    DEPEND
        rkcl-ur-robot/rkcl-ur-robot
        rkcl-driver-vrep/rkcl-driver-vrep
        rkcl-otg-reflexxes/rkcl-otg-reflexxes
        pid-os-utilities/pid-signal-manager
        rkcl-app-utility/rkcl-app-utility
)

The declared component is of type EXAMPLE_APPLICATION, we assign a NAME, specify its containing DIRECTORY and list the RUNTIME_RESOURCES that are needed. Here, we specify the folders ur_models, ur_config and ur_logs containing respectively the robot description file, the application configuration file and an empty repository used to store the log files. Then we declare the dependencies for this application, which is formulated as <name of the package>/<name of the library>. If you look at the src folder of the package, a PID component has been declared in the CMakeLists.txt file. This component is a HEADER_LIB which defines a component rkcl-ur-robot used to include all the RKCL libraries always used for the control of UR robots. This way, when declaring the EXAMPLE_APPLICATION above, we can directly set the rkcl-ur-robot header library as a dependency to the application, and it automatically exports all the dependencies defined in rkcl-ur-robot. Other than that, the application also requires some specific packages:

  • rkcl-driver-vrep is used to make the communication between the controller and the simulator.
  • rkcl-otg-reflexxes is used to generate trajectories for the tasks.
  • pid-signal-manager allows to handle signals.
  • rkcl-app-utility is a library used to ease the development of applications, as explained below.

Note that a component of type EXAMPLE_APPLICATION is used to provide an example of what has been implemented in the package (in our case the robot model) but this is not the main purpose of the package. Hence, the compilation of an EXAMPLE_APPLICATION is made optional in CMake and is not enabled by default. To change this, go to the build folder of the package (rkcl-ur-robot/build) and execute the following commands:

cmake ..
ccmake ..

This will open the CMake curses interface which allows to set the various CMake options for this project. Change the value of BUILD_EXAMPLES to ON.

4. Implementing the application

Once the application is declared, the next step is to implement a C++ file containing the main() function. The file should be placed inside the DIRECTORY specified in the component declaration. Let’s open the file apps/ur_simple_simu_example/main.cpp to see how the application is implemented.

4.1 Including the headers

The file starts with some header inclusions:

#include <rkcl/robots/ur.h>
#include <rkcl/processors/app_utility.h>
#include <rkcl/processors/task_space_otg.h>
#include <rkcl/drivers/vrep_driver.h>
#include <rkcl/processors/vrep_visualization.h>
#include <pid/signal_manager.h>

This allows to use the various RKCL libraries to define the controller for the UR10 robot. The last inclusion specific to PID is required to be able to catch the interrupt signal (ctrl+c) to properly stop the application.

4.2 Objects instantiation and initialization

Now, let’s go inside the main function. The first step consists in the instantiation and initialization of RKCL objects:

rkcl::DriverFactory::add<rkcl::VREPMainDriver>("vrep_main");
rkcl::DriverFactory::add<rkcl::VREPJointDriver>("vrep_joint");
rkcl::QPSolverFactory::add<rkcl::OSQPSolver>("osqp");

auto conf = YAML::LoadFile(PID_PATH("ur_config/ur10_simu.yaml"));
auto app = rkcl::AppUtility::create<rkcl::ForwardKinematicsRBDyn>(conf);

rkcl::TaskSpaceOTGReflexxes task_space_otg(app.getRobot(), app.getTaskSpaceController().getControlTimeStep());

if (not app.init())
{
    throw std::runtime_error("Cannot initialize the application");
}

app.addDefaultLogging();

bool stop = false;
bool done = false;

pid::SignalManager::registerCallback(pid::SignalManager::Interrupt, "stop",
                                        [&](int) { stop = true; });

Let’s break down the operations:

rkcl::DriverFactory::add<rkcl::VREPMainDriver>("vrep_main");
rkcl::DriverFactory::add<rkcl::VREPJointDriver>("vrep_joint");
rkcl::QPSolverFactory::add<rkcl::OSQPSolver>("osqp");

To make RKCL as generic as possible, drivers and quadratic programming (QP) solvers, that are used to solve the inverse kinematics, all inherit from base classes defined in the rkcl-core library. Each time a new driver or solver is created, we add it to a dedicated factory which gathers all the derived components. At the beginning of the main program, it is necessary to declare the components from the factories that will be used in the application, otherwise RKCL would not find it. V-REP has a particular driver which is made opf two elements: a main driver VREPMainDriver which manages the simulation execution and one or several VREPJointDriver used to communicate (read/send data) with a specific joint group.

auto conf = YAML::LoadFile(PID_PATH("ur_config/ur10_simu.yaml"));
auto app = rkcl::AppUtility::create<rkcl::ForwardKinematicsRBDyn>(conf);

rkcl::TaskSpaceOTGReflexxes task_space_otg(app.getRobot(), app.getTaskSpaceController().getControlTimeStep());

We load and store in the variable conf the content of the configuration file. We can then create an AppUtility object using the configuration file as parameter. This object contains the main RKCL processor (forward kinematics, inverse kinematics, …) called during the control loop execution. All these processors are also configured with the configuration file. The AppUtility object is constructed with a template class for the forward kinematics processor. Here, the forward kinematics processor based on RBDyn is used. A trajectory generator for generating task space trajectories based on Reflexxes is instantiated at the end of the block. This is the only processor called in the control loop which is not included in the AppUtility (because it is optional).

if (not app.init())
{
    throw std::runtime_error("Cannot initialize the application");
}

app.addDefaultLogging();

We initialize the AppUtility object. It takes care of initializing every driver and processor that it manages. Then, we activate the default logging in the application object, which means that all relevant data is automatically logged. We will explain later in this tutorial how to see the results of log files.

bool stop = false;
bool done = false;

pid::SignalManager::registerCallback(pid::SignalManager::Interrupt, "stop",
                                        [&](int) { stop = true; });

We define two boolean variables: stop is a flag that becomes true whenever the user wants to interrupt the application (ctrl+c). To do so, we link this variable to the interrupt signal using the PID SignalManager class. The other boolean done remains false until all tasks are completed or something wrong happens in the control process.

4.3 The control loop

Once everything has been properly declared and initialized, the robot can start operating. To do so, the program iteratively load the next task to execute specified in the configuration file. Each joint group can be either controlled in joint space or task space. In the first case, the goal is to move the robot from an initial to a desired joint configuration while in the second case the objective is defined in the Cartesian space, through the use of control points. For advanced users, different types of task space control are available (e.g force control, damping control).

std::cout << "Starting control loop \n";
app.configureTask(0);
task_space_otg.reset();
while (not stop and not done)
{
    bool ok = app.runControlLoop(
        [&] {
            if (app.isTaskSpaceControlEnabled())
                return task_space_otg();
            else
                return true;
        });

We load the first task specified in the configuration file. Then, we run the control loop using the AppUtility object. It automatically detects whether the operation has to be performed in joint or task space, and manages the successive calls to the drivers ans processors accordingly. If the task space control is enabled, we add a call to the task space trajectory generator, which is not yet managed by the AppUtility class, before calling the other processes.

if (ok)
{
    done = true;
    if (app.isTaskSpaceControlEnabled())
    {
        done &= (app.getTaskSpaceController().getControlPointsPoseErrorGoalNormPosition() < 0.01);
    }
    if (app.isJointSpaceControlEnabled())
    {
        auto joint_group_error_pos_goal = app.getRobot().getJointGroup(0)->selection_matrix * (app.getRobot().getJointGroup(0)->goal.position - app.getRobot().getJointGroup(0)->state.position);
        done &= (joint_group_error_pos_goal.norm() < 0.001);
    }
}
else
{
    throw std::runtime_error("Something wrong happened in the control loop, aborting");
}

If everything went well during the control iteration (i.e. ok = true ), we check if the current task is completed. The corresponding condition variable done is evaluated depending on the control space currently enabled. In both cases, a task is considered achieved if the current position/pose is sufficiently close to the goal.

if (done)
{
    done = false;
    std::cout << "Task completed, moving to the next one" << std::endl;
    done = not app.nextTask();
    task_space_otg.reset();
}

If the operation is in progress (done = false), we keep looping until the user send the interrupt signal or the task is achieved.

When a task is completed (i.e. done = true ), we should load the next one regarding the sequence defined in the configuration file. We reset done at the beginning of a new task and reset the trajectory generator so that it is aware of the current state of the environment.

If no more tasks are available, app.nextTask() return false which set done = true again. This makes the program exit the main loop.

4.4 Ending the program

When all the tasks have been achieved, we exit the control loop and end the application:

pid::SignalManager::unregisterCallback(pid::SignalManager::Interrupt, "stop");

app.end();

We unregister the interrupt signal from the PID signal manager and call the end() function from the AppUtility object in order to stop the different processes and drivers properly

5. Configuring the application

As we have just seen in the previous section, the C++ application file does not contain any information concerning the operations executed by the robot and how it behaves with respect to the environment. In fact, the same C++ application file could be used for a large majority of scenarios, and the goal of RKCL is to make the maximum parameters tunable without having to go inside the C++ code.

Now, let’s see what the configuration file is made of: open the YAML configuration file located at share/resources/ur_config/ur10_simu.yaml.

Note for those who are not familiar with YAML, this language allows a concise and clear description where whitespace indentation is used for denoting structure. It is thus crucial to take care of how the different blocks are vertically aligned to respect the desired structure.

We will detail in what follows the role of each block (node) :

app:
    task_config_files:
        - name: ur_config/tasks/joint_init_pos.yaml
        - name: ur_config/tasks/task_init_pos.yaml
        - name: ur_config/tasks/task_trans_z+.yaml

The app node configure what concerns the scenario of the application. In particular, it allows to store the set of tasks respecting the order of the list. For each task, a specific auxiliary YAML file should be created. For the current application, 3 tasks have been implemented and put in the share/resources/ur_config/tasks folder. A task either modify joint groups or control points data depending on the selected control mode (joint/task control).

robot:
    control_points:
      -   name: tcp
          body_name: tcp
          priority: 1
          limits:
              max_velocity: [0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
              max_acceleration: [0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
          gains:
              position_control:
                proportional: [10, 10, 10, 10, 10, 10]

    joint_groups:
      -   name: arm
          is_controlled: true
          priority: 1
          joints: [joint1, joint2, joint3, joint4, joint5, joint6]
          control_time_step: 0.008
          limits:
              max_velocity: [1, 1, 1, 1, 1, 1]
              max_acceleration: [1, 1, 1, 1, 1, 1]

The robot node is used to create and configure the control points and joint groups. In this simple application, we define only one of each but you can specify as many as you want.

For control_points, you should give a name, and specify a body_name which is the name of the body it is attached to. This name should match a body from the robot model. We can assigned to each control point a priority. In case of multiple control points, the robot’s capabilities will be exploited in a way that maximum efforts will be dedicated to solve the highest priority task while lower priority task will be treated with remaining possibilities. The velocity and acceleration limits for the control point will be used to configure the trajectory generator in order to always satisfy these constraints. Finally, the gains used to compute the task space command for the control point are specified.

The joint_groups are also referred to using a name. We can decide to enable/disable the use of a joint group each time a new task is loaded. To do so, we set the parameter is_controlled to the desired boolean value. A priority can also be given for joint groups: if more than one group exist, the robot will try to solve all tasks using the highest priority joint group. If for one or more tasks there is some residual error, the next joint group regarding the priority will be used to compensate for the remaining error on tasks, and so on. Each joint group is composed of a set of joints among those defined in the robot model, they should be specified using the joints option. Considering that a robot can be made of different hardware components, the control_time_step for the joint group has to be set. Finally, joint limits can be specified for position/velocity/acceleration. If stated, these values overwrite existing joint constraints provided in the robot model.

joint_space_otgs:
    -     joint_group: arm
          input_data: PreviousOutput #CurrentState

The joint_space_otgs node allows to define the joint space trajectory generators. One instance should be defined for each joint group, otherwise the joint group would not be able to perform joint space operations. One trajectory generator should refer to one joint_group using its name. The parameters of the trajectory generator (maximum velocity/acceleration) are set using the joint group properties. This allows to generate a trajectory between the current and goal joint configuration which stays within the admissible range of motion. A trajectory generator can be used in open or closed loop, depending of the value of input_data. If PreviousOutput is chosen, the process is open loop as it reuses the last generated output to set the new input. If CurrentState is selected, the trajectory generation takes into account the actual configuration of the robot as the new input, making the process closed-loop.

task_space_controller:
    wrench_measure_enabled: false
    control_time_step: 0.008

As its name suggests, the task_space_controller node configure the task space controller. As explained in the RKCL main concepts page, to deal with synchronization issues in multi-robot systems, we define one main control loop which communicates with the different parts of the robot. This task space control loop has its own control rate which is defined by the control_time_step parameter. To be optimal, this value should be the same as the lowest joint group control time step. Other than that, we can can indicate if wrenches (force/torque) have to be considered in the control process.

ik_controller:
    IK_type: StandardQP
    QP_solver:
      type: osqp
      verbose: false
      check_termination: 10

The ik_controller node allows to configure the inverse kinematics controller. Different redundancy resolution strategies can be adopted. You can select one using the IK_type option. If you don’t know, just keep the StandardQP approach. Several library implementing QP solvers are wrapped in RKCL. You can choose which solver you want to use and parametrize specific options through the QP_solver node. It is recommended to use the osqp solver.

model:
    path: ur_models/ur10.yaml
    world_pose: [0, 0, 0, 0, 0, 0]

The model node is used to indicate the path to the configuration file (relative to share/resources). You can use either a YAML or a URDF file format. The world_pose parameter indicates the pose of the first robot link with respect to the world frame (in this case, the pose of the base link).

joint_group_drivers:
    -   type: vrep_main
        cycle_time: 0.008
    -   type: vrep_joint
        joint_group: arm
        control_mode: Velocity
        joint_names: [shoulder_pan_joint, shoulder_lift_joint, elbow_joint, wrist_1_joint, wrist_2_joint, wrist_3_joint]

The joint_group_drivers node lists all the drivers in charge of the communication between the joint groups and the hardware components. Here is a particular case because we run the application on simulation using V-REP. This way, we should define a main driver and one joint driver for each group (there is only one here). We should specify the cycle_time for the main driver, which should correspond to the simulation time step used in V-REP. For each joint driver, we should set the control_mode (position or velocity) and indicate the name of the joints used in the V-REP in interface so that the joint group data car be read/sent properly.

logger:
    log_folder: ur_logs

The logger node allows to configure the logging process. Just indicate the path to the log_folder (relative to share/resources) in which the log files should be stored.

6. Launching the application

Ok now everything is ready to use, we can build and execute the application !

Go to the build folder of the package and make sure that the BUILD_EXAMPLES option is activated using ccmake, as explained before at the beginning of this page. Then, in a terminal, type:

cmake ..
pid build

If everything went well, the package is now built and installed in pid-workspace/install/<platform>/rkcl-ur-robot/<version>/. From now, we call this folder <install> for simplicity.

Before starting the program execution, let’s load the scene in CoppeliaSim to visualize the robot: in the CoppeliaSim interface, go to File -> Open scene… and select the scene share/resources/vrep_scenes/ur10.ttt in the package folder.

Move to the <install>/bin folder where the executable file has been generated and execute it:

./ur-simple-simu-example

You should see the robot moving in V-REP ! Just let it complete all the operations.

After running an the application, you can see the generated log files in <install>/share/resources/ur_logs. You can plot the data using Gnuplot, you just have to load the gnuplot files that have been generated automatically.