For using robots and sensor you need descriptions of these systems in term of link and joints, as well as other useful descriptive elements like mesh for visual rendering and collision management. These descriptive elements are use by robocop to generate the World that contains all these instaciated concepts.

Create a description package

First of all you should create a package that contains your description, or use an existing one. If you create a package you should name it robocop-<robot or sensor type>-description.

Then its description is as usual in PID you can simply add the package to the robocop framework, in the model category. For instance in package robocop-force-sensor-description:

PID_Publishing(
    PROJECT https://gite.lirmm.fr/robocop/description/robocop-force-sensor-description
    DESCRIPTION Force sensors description
    FRAMEWORK robocop
    CATEGORIES model
)

Description package simply consist in providing adequate descriptive models of robots and sensors. The best way is to follow the given pattern:

  • create a folder robocop-<robot or sensor type>-description in share/resources. This folder will contain all description and data provided by the package.

  • in src/CMakeLists.txt define a header library with name <robot or sensor type>-description that provides the previous folder as a runtime resource but define not public headers. This component is just useful for exporting all the descriptions provided by the package so that they can be exploited later easily. For instance in robocop-force-sensor-description:

PID_Component(
    HEADER
    NAME force-sensor-description
    RUNTIME_RESOURCES
        robocop-force-sensor-description
)

You then just have to feed the folder robocop-<robot or sensor type>-description with all required description data.

URDF description

The basic data description format used is the URDF format. So you should use existing URDF files to manage existing robots, or create new URDF descriptions for your own designed robots.

The following instructions explains specificities of URDF writing in robocop which is similar but not exactly the same as URDF writing in ROS.

If you robot is quite basic then you can simply copy/paste an existing URDF. For instance in robocop-force-sensor-description one can find the file share/resources/robocop-force-sensor-description/ati_axia80.urdf that looks like:

<?xml version="1.0" ?>
<robot name="ati_axia80">
    <link name="force_sensor">
        <inertial>
            <origin xyz="0 0 0.0127" rpy="0 0 0"/>
            <mass value="0.3"/>
            <inertia ixx="0.0000161416075" ixy="0" ixz="0" iyy="0.0000161416075" iyz="0" izz="2.5215000000000004e-8" />
        </inertial>
        <visual>
            <geometry>
                <cylinder radius="0.041" length="0.0254" />
            </geometry>
            <material name="sensor_color">
                <color rgba="0.86 0.86 0.86 1"/>
            </material>
        </visual>
        <collision>
            <geometry>
                <cylinder radius="0.041" length="0.0254" />
            </geometry>
        </collision>
    </link>
    <link name="tool_plate" />
    <joint name="to_tool_plate" type="fixed">
        <parent link="force_sensor" />
        <child link="tool_plate" />
        <origin xyz="0 0 0.0254" rpy="0 0 0" />
    </joint>
</robot>

As you may see there is nothing special in this super simple URDF.

Importing data

Nevertheless, most of time robot description requires data like meshes (for vizualization and collision). In this situation create the folder share/resources/robocop-<robot or sensor>-description/meshes that will contains the meshes (e.g. stl files). For clarity you should put your URDF models into a share/resources/robocop-<robot or sensor>-description/models folder. Since additional data is put into a subfolder of the exported folder share/resources/robocop-<robot or sensor>-description programs will be able to find them at execution time.

One big difference with classic URDF is the way data is referenced. In a classic URDF file you would have something like:

<link name="link_1">
    <visual>
      <origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
      <geometry>
        <mesh filename="package://robocop-kuka-lwr-description/robot_model/lwr/meshes/lwr/link1.stl"/>
      </geometry>
      <material name="color_j1"/>
    </visual>
</link>

But Robocop uses another runtime resources management system wich leads to this slightly different description:

<link name="link_1">
    <visual>
      <origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
      <geometry>
        <mesh filename="robocop-kuka-lwr-description/meshes/lwr/link1.stl"/>
      </geometry>
      <material name="color_j1"/>
    </visual>
</link>

The underlying idea is exactly the same: finding a runtime resource relative to a package folder. Here the target runtime resource robocop-kuka-lwr-description refers to the folder share/resources/robocop-kuka-lwr-description supposed to be exported by the robocop-kuka-lwr-description robocop package.

Same as classic syntax we could have target another package for instance a robocop-kuka-lwr-base-description/meshes/lwr/link1.stl as soon as local robocop-kuka-lwr-description packages would depend on robocop-kuka-lwr-base-description.

How to deal with Xacro descriptions

For now there is no Xacro support in robocop so you cannot directly use URDF templates like in ROS. Instead you can use xacro tool to generate your URDF classic models from Xacro template and then use these URDF in robocop.

Combining descriptions

Refering to the previous section, composition of URDF files can be done using Xacro but exclusively outside of Robocop which is not super flexible. Instead Robocop propose a simple descriptive mechanism based on YAML format.

Please consider deploying the package robocop-bazar-description in your workspace to follow the remaining sections of this tutorial. As a first example:

models:
  - include: robocop-neobotix-description/models/mpo700_fixed_hokuyo.yaml
    namespace: base
  - include: robocop-bazar-description/models/bazar_upper_body.yaml
    joint:
      parent: base_mpo700_top

joint_groups:
  motion_control: base_planar_control
  odometry: base_odometry

The models section corresponds to a sequence of imported models:

  • To import URDF the include keyword is used
  • To import YAML models the path keyword is used

When imported models can be “namespaced” so that every link and joint they define is prefixed with the given namespace. This mechanism allows to import a same model multiple times and more generally drastically reduce name collisions in the models. In the example, every link and body defined in mpo700_fixed_hokuyo model is prefixed with base_.

Each model so contains links and joints defining a tree structure, so each imported model has a unique root body. This root body is attached to a super tree using a unique joint that is either unspecified or defined using the joint feature.

Imported models root bodies are attached:

  • to the current (i.e. importing) model implicit root body. This is the default behavior if no joint specified or if no parent defined for the specified joint. In the example the mpo700 mobile base is attached to the implicit root body of the current model
  • to a body of another imported model. This is the behavior when the parent of the specified joint is defined. In the example bazar_upper_body model root body is attached to the base_mpo700_top body defined in mpo700_fixed_hokuyo.

By default (if attachment is unspecified or if no type given to the joint) the joint type is fixed with a position at 0. A non zero initial position can be given by defining its type as fixed and by setting its position (position angular and linear compnents can be set individually). Otherwise other types of joints can be used: revolute, prismatic, free, planar, etc.

YAML models can also define joint groups (which is not possible in URDF). This is usefull to enforce joint group names used. These groups can be defined from sequence of joints and/or other joint groups. In the example two joint groups are defined : motion_control and odometry. One important thing to notice is that joint groups are also subject to “namespacing”: if the current model is namespaced then all joint groups it defines are aslo namespaced.

In the end a complete robot description is achieved using a tree of description files, whose leafs are URDFs and other nodes are YAML models.

Combining description from multiple packages

As far as possible, reusable descriptions should be isolated into dedicated packages.

More complex description are defined in dedicated packages that use more elementay description packages. For instance the package containing the description of LIRMM’s BAZAR cobot (robocop-bazar-description) is based on:

  • robocop-kuka-lwr-description providing description of its arms
  • robocop-force-sensor-description providing description of the force sensors on its arms end effectors
  • robocop-flir-ptu-description providing description of its pan-tilt head

Internally robocop-bazar-description descriptions bound these elements all together to build up a consistent description.

In this case of course the package describing the composition must declare the packages proving elementary descriptions as dependencies. For instance in robocop-bazar-description package’s root CMakeLists.txt:

...
PID_Dependency(robocop-core VERSION 0.1.0)
# import descriptions used as elementary elements of the local descriptions
PID_Dependency(robocop-kuka-lwr-description VERSION 0.1)
PID_Dependency(robocop-force-sensor-description VERSION 0.1)
PID_Dependency(robocop-flir-ptu-description VERSION 0.1)

The library exporting the description should also export all the corresponding components proving the descriptions. For instance in robocop-bazar-description package’s src/CMakeLists.txt file:

PID_Component(
    HEADER
    NAME bazar-description
    EXPORT
        robocop/kuka-lwr-description
        robocop/force-sensor-description
        robocop/flir-ptu-description
    RUNTIME_RESOURCES
        robocop-bazar-description
)

You have to remember to export all the components (using EXPORT keyword) required to get access to all description files in use.

Testing description

Everytime a package provide new descriptions (whether they are elementary or composite) it is important to test them to ensure their correctness. Testing description is somehow impossible in an automated way, that is why here we consider testing as a way for the developper to “see” if the model “looks” correct.

In order to test the description we so suggest to use a simulator. The package robocop-sim-mujoco provides a simulator that can be mostly automatically configured from the system description. The advised way to do so is to provide example applications in simulations.

1. Declare examples

For instance in robocop-bazar-description package’s root CMakeLists.txt:

...
PID_Dependency(robocop-core VERSION 0.1.0)
...
if(BUILD_EXAMPLES)
    PID_Dependency(robocop-sim-mujoco VERSION 0.1.0)
    PID_Dependency(robocop-model-pinocchio VERSION 0.1.0)
endif()

In robocop-bazar-description package’s apps/CMakeLists.txt:

When examples are built we use the simulator interface robocop-sim-mujoco. A simulator interface requires to manipulate the model at runtime (to get pose of parent child) bodies for instance tha is why we alsu the package robocop-model-pinocchio (robocop-model-rbdyn could have been used as an alternative).

PID_Component(
    bazar-sim-example
    EXAMPLE
    DESCRIPTION Loads a BAZAR robot into the MuJoCo simulator
    DEPEND
        robocop/bazar-description
        robocop/sim-mujoco
        robocop/model-pinocchio
    CXX_STANDARD 17
    WARNING_LEVEL ALL
    RUNTIME_RESOURCES
        robocop-bazar-description-examples
)

Robocop_Generate(
    bazar-sim-example
    robocop-bazar-description-examples/config.yaml
)

Then we declare the example application bazar-sim-example wich depends on the locally defined library bazar-description (see previous section) and on external libraries sim-mujoco and model-pinocchio. The application is generated/configured using a configuration file robocop-bazar-description-examples/config.yaml.

2. Simulation programming

We first need to generate the application description using the robocop generator. We defined the file robocop-bazar-description-examples/config.yaml to do so.

models:
  - include: robocop-bazar-description/models/bazar.yaml
    namespace: bazar
    joint:
      type: free

processors:
  simulator:
    type: robocop-sim-mujoco/processors/sim-mujoco
    options:
      gui: true
      mode: real_time
      target_simulation_speed: 1
      joints:
        - group: bazar_arms
          command_mode: force
          gravity_compensation: true

  model:
    type: robocop-model-ktm/processors/model-ktm
    options:
      implementation: pinocchio
      input: state
      forward_kinematics: true
      forward_velocity: true

The application uses a single instance of the bazar robot model and configure the simulator and the kinematic model. To get the generated world source code:

pid cd robocop-bazar-description
pid build -DBUILD_EXAMPLES=ON

Now the application is configured we need to write the application code:

#include "robocop/world.h"
#include <robocop/model/pinocchio.h>
#include <robocop/sim/mujoco.h>
#include <chrono>
#include <thread>

int main(int argc, const char* argv[]) {
    using namespace std::literals;
    using namespace phyq::literals;

    robocop::World world;
    robocop::ModelKTM model{world, "model"};

    constexpr auto time_step = phyq::Period{1ms};
    robocop::SimMujoco sim{world, model, time_step, "simulator"};
    sim.set_gravity(
        phyq::Linear<phyq::Acceleration>({0., 0., -9.81}, "world"_frame));

    //do not move default command
    auto arm_cmd = robocop::JointVelocity{phyq::zero, 7};
    auto& left_arm = world.joint_group("bazar_left_arm");
    left_arm.command().set(arm_cmd);
    auto& right_arm = world.joint_group("bazar_right_arm");
    right_arm.command().set(arm_cmd);
    sim.init();
    ...
    auto iter{0};
    while (sim.is_gui_open()) {
        if (sim.step()) {
            sim.read();
            if (iter < 100) {
                ++iter;
            }
            sim.write();
        } else {
            std::this_thread::sleep_for(100ms);
        }
    }
}

Finally build it:

pid build

When you run the example you should see the the robot not moving. First thing to check is the validity of the description in terms of frame placements, and in a second time visuals. You can apply external forces on it using the simulator interface, or you can implement some test procedure to see if the robot is behaving normally (rotation direction of joints for instance).