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());
}
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:
| Step | Call | Role |
|---|---|---|
rclcpp::init() | Initialize ROS2 runtime | Required for Bridge to create ROS2 nodes |
Robot::builder().build() | Create robot instance | Config validation, Factory call, resource allocation |
Bridge bridge(*robot) | Create ROS2 adapter | Prepare monitoring/mode control/state publisher nodes |
robot->start() | Start RT thread | EtherCAT communication, torque calculation, IPC bridge |
bridge.start() | Start ROS2 node threads | Activate /rt_raw, /set_mode, /robot_status topics |
wait_for_shutdown() | Wait for termination signal | Returns when Ctrl+C (SIGINT) received |
monitoring::shutdown() | Clean monitoring queue | Drain RT queue |
rclcpp::shutdown() | Clean ROS2 runtime | Shutdown nodes |
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 File | Role | Main Arguments |
|---|---|---|
rt_driver.launch.py | Run RT driver | eth_interface, description_package, robot_type |
ros2_control.launch.py | Start ros2_control stack | description_package, robot_type |
moveit.launch.py | Start MoveIt motion planning | description_package, robot_type |
moveit_only_launch.py | Single robot MoveIt + namespace isolation | name, robot_type |
multi_robot.launch.py | Run multiple robots simultaneously | robots (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)
Scaffold Launch File (Recommended for External Users)
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 Type | Path Convention | Purpose |
|---|---|---|
| URDF | urdf/<robot_name>.urdf.xacro | Robot kinematics, ros2_control tags |
| SRDF | srdf/<robot_name>.srdf.xacro | MoveIt configuration (planning groups, poses) |
| Joint Limits | config/joint_limits.yaml | Position/velocity/acceleration/jerk limits |
| Controllers | config/controllers.yaml | ros2_control controller configuration |
| Initial Positions | config/initial_positions.yaml | Robot starting pose (optional) |
Robot Name Detection: Automatically extracted by removing the _description suffix from the package name (ur5e_description → ur5e).
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
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
| Symptom | Cause | Solution |
|---|---|---|
No /dev/EtherCAT0 | Service not started | sudo systemctl restart ethercat |
No network interface | ethercat.conf not configured | Set MASTER0_DEVICE="eth0" |
| Permission error | Group not configured | sudo groupadd -g 1001 ethercat + udev rules |
| WKC mismatch | Slave not responding | Check cables, verify status with ethercat slaves |
Integration Checklist
Description Package Creation
-
Create package
ros2 run plem_bringup create_description -
Edit URDF — Define geometry, mesh, joint structure
-
Configure joint limits (
config/joint_limits.yaml)- Use full precision values (e.g.,
3.14159265358979— do not round) max_effortfield is mandatory- Template: See Description Protocol
- Use full precision values (e.g.,
-
Build
colcon build --packages-select <name>_description -
Verify
colcon test --packages-select <name>_description -
Run
ros2 launch <name>_description bringup.launch.py -
Optional: Custom MoveIt Config (Edit SRDF, configure planning groups)
C++ Robot Extension Creation
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.
-
Create package
ros2 run plem_bringup create_robot -
Edit Config —
kInertial,kURDFKinematics(kinematics/dynamics data)URDF auto-parsingIf URDF is ready, you can auto-extract with
parse_urdf_to_config.py:ros2 run plem_bringup parse_urdf_to_config my_robot.urdf.xacroCopy the output C++ constexpr arrays to your config header.
-
Cross-validation — Automatic comparison of YAML ↔ C++ limits
colcon build --packages-select <name>_description
colcon test --packages-select <name>_robot -
Build
colcon build --packages-select <name>_robot -
Run system (Automatic sequencing of RT driver + ROS2 stack)
ros2 launch <name>_robot bringup.launch.py
Post-Integration
- Check RT jitter with
/rt_rawtopic (< 50µs is good) - Verify joint states with
/joint_statestopic - 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:
| File | Purpose |
|---|---|
<model>_config.hpp | Config struct + RobotTraits specialization |
<manufacturer>_factory.hpp | RobotFactory specialization |
<model>_robot.hpp/.cpp | PImpl delegation class (when EtherCAT selected) |
main.cpp | Entry point (static_assert validation) |
test_<model>_config.cpp | Concept verification + YAML cross-validation tests |
CMakeLists.txt | Build configuration |
package.xml | ROS2 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:
| Fieldbus | Config | Factory | PImpl | Notes |
|---|---|---|---|---|
| EtherCAT CiA402 | Full | Full | Full (EtherCATRobotBase) | Recommended |
| EtherCAT Custom | Full | Full | Full | Custom PDO mapping required |
| Modbus (experimental) | skeleton | stub | None | Direct IRobot implementation required |
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
- Description Protocol - Convention-based file conventions
- ROS2 Launch System - Composable Launch structure
- Safety Modes API - Understanding SafetyMode, RobotMode