Skip to main content

Robot Integration

This guide explains how to integrate new robots into PLEM from an external user perspective.

Quick Overview

Integration Flowchart

Using the Builder Pattern (C++ API)

When creating robots directly in C++ applications:

#include <plem/robot/robot_builder.hpp>
#include "plem/robot/models/your_robot.hpp" // Custom robot Config

// 1. Prepare Config
YourRobot config;
config.network_interface = "eth0";
config.cycle_time = std::chrono::microseconds(1000); // 1kHz
config.rt_priority = 98;
// joint_offsets: Encoder offsets (int32_t array, robot-specific calibration values)
// Actual values are loaded from RobotProfile or determined during calibration
config.joint_offsets = {0, 0, 0, 0, 0, 0};

// 2. Create robot with Builder
auto result = Robot::builder<YourRobot>(config).build();
if (!result) {
std::cerr << "Failed to create robot: " << result.message() << std::endl;
return -1;
}

std::unique_ptr<IRobot> robot = std::move(result.value());

// 3. Start
robot->start();

// 4. Inject planner and executor
robot->set_planner(std::make_shared<PilzPlanner>(node));
robot->set_executor(std::make_shared<TrajectoryExecutor>(node, "joint_trajectory_controller"));

// 5. Motion commands
auto traj = robot->move_j({0.0, 0.5, -0.5, 0.0, 1.0, 0.0}, 0.3);
if (traj) {
robot->execute(traj.value());
}
Using free functions

You can also use free functions directly without set_planner()/set_executor(). plem::robot::move_j(planner, dof, joint_names, planning_group, target, speed) — See <plem/robot/motion_functions.hpp> for details.

Multiple Robots (Same Host)

When running multiple robots on one host, use with_robot_id() to automatically isolate IPC and ROS2 namespaces:

// Assign unique ID and separate EtherCAT master to each robot
config_a.network_interface = "eth0";
config_a.master_index = 0;
auto robot_a = Robot::builder<YourRobot>(config_a)
.with_robot_id("arm_left")
.build().value();

config_b.network_interface = "eth1";
config_b.master_index = 1;
auto robot_b = Robot::builder<YourRobot>(config_b)
.with_robot_id("arm_right")
.build().value();
// IPC: /arm_left_joint_actual_values, /arm_right_joint_actual_values
// ROS2: /arm_left/rt_raw, /arm_right/rt_raw

If robot_id is omitted, it behaves the same as the legacy single-robot mode.

Production Executable (main.cpp)

The Builder example above shows only the core API. In production environments, you need ROS2 Bridge, monitoring, and signal handling.

Here's a complete main.cpp template:

#include <memory>
#include <string>

#include <rclcpp/rclcpp.hpp>

#include <plem/core/logging/logger.hpp>
#include <plem/monitoring.hpp>
#include <plem/robot/robot_builder.hpp>
#include <plem/ros2_adapter/bridge.hpp>

#include "your_robot_package/your_factory.hpp" // Include RobotFactory specialization

using plem::core::logger;

int main(int argc, char* argv[]) {
// 1. Initialize ROS2 (required before Bridge.start())
rclcpp::init(argc, argv);

// 2. Prepare robot Config + Create with Builder
your_ns::YourRobotConfig config;
config.network_interface = "eth0";
config.rt_priority = 98;
config.joint_offsets = {0, 0, 0, 0, 0, 0};

auto result = plem::robot::Robot::builder(config)
.with_robot_id("my_robot") // IPC/ROS2 namespace isolation
.build();

if (!result) {
logger.error("Failed to create robot: {}", result.message());
rclcpp::shutdown();
return EXIT_FAILURE;
}
auto robot = std::move(result).value();

// 3. Create ROS2 Bridge (monitoring, mode control, state publishing)
plem::ros2_adapter::Bridge bridge(*robot);

// 4. Start RT thread → Start Bridge
int rc = robot->start();
if (rc != 0) {
logger.error("Failed to start robot");
rclcpp::shutdown();
return EXIT_FAILURE;
}
bridge.start();

// 5. Wait until Ctrl+C (signal handling is automatic inside PLEM)
logger.info("Robot running. Press Ctrl+C to stop.");
robot->wait_for_shutdown();

// 6. Cleanup (Bridge destructor automatically calls stop())
plem::monitoring::shutdown();
rclcpp::shutdown();

return EXIT_SUCCESS;
}

Execution Order Summary:

StepCallRole
rclcpp::init()Initialize ROS2 runtimeRequired for Bridge to create ROS2 nodes
Robot::builder().build()Create robot instanceConfig validation, Factory call, resource allocation
Bridge bridge(*robot)Create ROS2 adapterPrepare monitoring/mode control/state publisher nodes
robot->start()Start RT threadEtherCAT communication, torque calculation, IPC bridge
bridge.start()Start ROS2 node threadsActivate /rt_raw, /set_mode, /robot_status topics
wait_for_shutdown()Wait for termination signalReturns when Ctrl+C (SIGINT) received
monitoring::shutdown()Clean monitoring queueDrain RT queue
rclcpp::shutdown()Clean ROS2 runtimeShutdown nodes
Scaffold minimal skeleton

The main.cpp generated by create_robot scaffold includes only a minimal skeleton (robot->run()). If you need ROS2 topic monitoring, extend it with the pattern above.

Launch Architecture

PLEM provides a composable launch structure.

Individual Component Launch Files

Launch FileRoleMain Arguments
rt_driver.launch.pyRun RT drivereth_interface, description_package, robot_type
ros2_control.launch.pyStart ros2_control stackdescription_package, robot_type
moveit.launch.pyStart MoveIt motion planningdescription_package, robot_type
moveit_only_launch.pySingle robot MoveIt + namespace isolationname, robot_type
multi_robot.launch.pyRun multiple robots simultaneouslyrobots (comma-separated names)

Unified Launch File (For Internal Development/Quick Testing)

plem_launch.py orchestrates all components and runs them sequentially:

# Unified launch (quick test, start all components)
ros2 launch plem_bringup plem_launch.py description_package:=my_robot_description

# Or use built-in robot type
ros2 launch plem_bringup plem_launch.py robot_type:=indy7_v2

Purpose: Prototyping, full feature testing, internal development

Features: Automatically runs RT driver, ROS2 Control, MoveIt, Python Bridge (plem_ai_server)

For external robot integration, use the launcher generated by the scaffold tool:

# 1. Create Description package (launch files are generated by default)
ros2 run plem_bringup create_description --name my_robot --dof 6

# 2. Edit generated bringup.launch.py to enable only needed components

# 3. Run custom launcher
ros2 launch my_robot_description bringup.launch.py

Purpose: Production deployment, resource optimization

Features: Select only needed components, robot-specific customization

See the ROS2 Launch System guide for details.

Convention-Based Discovery

When providing the description_package argument, PLEM automatically discovers files using these conventions:

File TypePath ConventionPurpose
URDFurdf/<robot_name>.urdf.xacroRobot kinematics, ros2_control tags
SRDFsrdf/<robot_name>.srdf.xacroMoveIt configuration (planning groups, poses)
Joint Limitsconfig/joint_limits.yamlPosition/velocity/acceleration/jerk limits
Controllersconfig/controllers.yamlros2_control controller configuration
Initial Positionsconfig/initial_positions.yamlRobot starting pose (optional)

Robot Name Detection: Automatically extracted by removing the _description suffix from the package name (ur5e_descriptionur5e).

See the Description Protocol document for detailed protocol specifications.

Scaffold Tool

Quickly generate new description packages:

ros2 run plem_bringup create_description \
--name my_robot \
--dof 6 \
--manufacturer "Vendor"

Generated Structure:

my_robot_description/
├── CMakeLists.txt # ament_cmake build configuration
├── package.xml # ROS2 package metadata
├── PLEM_COMPAT_VERSION # Compatibility version file
├── README.md # Usage guide
├── config/
│ └── joint_limits.yaml # Joint limits (SSoT)
├── launch/
│ └── bringup.launch.py # Scaffold launcher
└── urdf/
├── my_robot.urdf.xacro # Robot URDF (joint limits auto-loaded from YAML)
└── my_robot.ros2_control.xacro # ros2_control hardware interface
URDF joint limits

Joint limits in URDF are automatically loaded from joint_limits.yaml using xacro.load_yaml. No need to hardcode limits in URDF.

Next Steps: Edit the generated templates with your robot's actual parameters.

Verification Procedure

Real Hardware Testing

# 1. Check EtherCAT configuration
sudo systemctl status ethercat
ethercat slaves # Check if servos are detected

# 2. Start system with launch file
ros2 launch my_robot_description bringup.launch.py

# 3. Monitor status
ros2 topic echo /joint_states
ros2 topic echo /rt_raw # RT performance metrics

Troubleshooting

SymptomCauseSolution
No /dev/EtherCAT0Service not startedsudo systemctl restart ethercat
No network interfaceethercat.conf not configuredSet MASTER0_DEVICE="eth0"
Permission errorGroup not configuredsudo groupadd -g 1001 ethercat + udev rules
WKC mismatchSlave not respondingCheck cables, verify status with ethercat slaves

Integration Checklist

Description Package Creation

  1. Create package

    ros2 run plem_bringup create_description
  2. Edit URDF — Define geometry, mesh, joint structure

  3. Configure joint limits (config/joint_limits.yaml)

    • Use full precision values (e.g., 3.14159265358979 — do not round)
    • max_effort field is mandatory
    • Template: See Description Protocol
  4. Build

    colcon build --packages-select <name>_description
  5. Verify

    colcon test --packages-select <name>_description
  6. Run

    ros2 launch <name>_description bringup.launch.py
  7. Optional: Custom MoveIt Config (Edit SRDF, configure planning groups)

C++ Robot Extension Creation

Core concept

Applications created with create_robot completely replace PLEM's built-in RT driver. The main.cpp generated by create_robot directly performs EtherCAT communication, torque calculation, and safety monitoring with your robot configuration.

  1. Create package

    ros2 run plem_bringup create_robot
  2. Edit ConfigkInertial, kURDFKinematics (kinematics/dynamics data)

    URDF auto-parsing

    If URDF is ready, you can auto-extract with parse_urdf_to_config.py:

    ros2 run plem_bringup parse_urdf_to_config my_robot.urdf.xacro

    Copy the output C++ constexpr arrays to your config header.

  3. Cross-validation — Automatic comparison of YAML ↔ C++ limits

    colcon build --packages-select <name>_description
    colcon test --packages-select <name>_robot
  4. Build

    colcon build --packages-select <name>_robot
  5. Run system (Automatic sequencing of RT driver + ROS2 stack)

    ros2 launch <name>_robot bringup.launch.py

Post-Integration

  • Check RT jitter with /rt_raw topic (< 50µs is good)
  • Verify joint states with /joint_states topic
  • Test emergency stop

Supporting New Manufacturer Robots

PLEM provides two integration paths:

Path A: Description Package (ROS2 Integration Only)

For simulation, MoveIt Planning, or using already-supported robots:

ros2 run plem_bringup create_description --name my_robot --dof 6

This method is sufficient for most users.

Path B: C++ Robot Extension (Hardware Control)

To integrate new manufacturer robots with real hardware:

ros2 run plem_bringup create_robot

The interactive wizard generates a complete C++ extension package:

FilePurpose
<model>_config.hppConfig struct + RobotTraits specialization
<manufacturer>_factory.hppRobotFactory specialization
<model>_robot.hpp/.cppPImpl delegation class (when EtherCAT selected)
main.cppEntry point (static_assert validation)
test_<model>_config.cppConcept verification + YAML cross-validation tests
CMakeLists.txtBuild configuration
package.xmlROS2 package metadata

Features:

  • No need to modify PLEM source code
  • Compile-time type safety with C++20 Concepts
  • Support for various DOF (4, 5, 6, 7, 12) and fieldbuses

Auto-generation Scope by Fieldbus:

FieldbusConfigFactoryPImplNotes
EtherCAT CiA402FullFullFull (EtherCATRobotBase)Recommended
EtherCAT CustomFullFullFullCustom PDO mapping required
Modbus (experimental)skeletonstubNoneDirect IRobot implementation required
Using unified wrapper

To generate Description and Driver together, use the unified wrapper:

ros2 run plem_bringup create_robot_package          # Interactive: select scope
ros2 run plem_bringup create_robot_package --scope=both # Both at once

Each tool (create_description, create_robot) can still be run independently.

Next Steps