wmx_r2_package#
Overview#
wmx_r2_package is where WMX R2 actually runs. It holds the ten nodes
that talk to the WMX3 SDK, the three launch files that start them, and the
per-robot example configurations.
Two rules shape the whole package:
No robot is baked into any launch file. Every launch file takes its
paths as arguments — config_file and wmx_param_file, plus
urdf_file and controllers_file on the ros2_control variants — so
one launch file serves every robot of that kind. Moving to a different robot
changes the files you pass, not the code.
Every node except the engine and the lifecycle manager is a managed
(lifecycle) node. They start unconfigured and attach to the WMX device
only at configure. Their ROS interfaces are created at activate and
destroyed at deactivate, so an inactive node advertises nothing.
flowchart TB
subgraph GEN["wmx_r2_general_nodes.launch.py"]
ENG["wmx_engine_node<br/><i>owns the engine</i>"]
LCM["wmx_lifecycle_manager_node<br/><i>drives every managed node</i>"]
CM["wmx_core_motion_node<br/><i>lifecycle</i>"]
IO["wmx_io_node<br/><i>lifecycle</i>"]
EC["wmx_ethercat_node<br/><i>lifecycle</i>"]
end
subgraph MAN["+ wmx_r2_manipulator.launch.py"]
JSB["joint_state_broadcaster"]
JTC["joint_trajectory_controller"]
JPC["joint_position_controller"]
GC["gripper_controller<br/><i>use_gripper:=true</i>"]
end
subgraph DIF["+ wmx_r2_differential.launch.py"]
JSB2["joint_state_broadcaster"]
DDC["differential_drive_controller"]
end
ENG -->|engine status| LCM
LCM -->|configure / activate| CM & IO & EC
LCM -->|configure / activate| JSB & JTC & JPC & GC
LCM -->|configure / activate| JSB2 & DDC
SDK["WMX3 SDK /opt/wmx3"]
ENG --> SDK
CM --> SDK
IO --> SDK
EC --> SDK
JSB --> SDK
JTC --> SDK
JPC --> SDK
DDC --> SDK
wmx_r2_package node layout#
Package structure#
wmx_r2_package/
├── src/
│ ├── wmx_engine_node.cpp # engine + device
│ ├── wmx_lifecycle_manager_node.cpp # lifecycle supervisor
│ ├── wmx_core_motion_node.cpp # axes services + status
│ ├── wmx_io_node.cpp # digital I/O
│ ├── wmx_ethercat_node.cpp # EtherCAT master
│ ├── joint_state_broadcaster.cpp # encoder feedback
│ ├── joint_trajectory_controller.cpp # FollowJointTrajectory
│ ├── joint_position_controller.cpp # MoveIt Servo stream
│ ├── gripper_controller.cpp # gripper output bit
│ └── differential_drive_controller.cpp # diff-drive + odometry
├── include/ # one header per node
├── launch/
│ ├── wmx_r2_general_nodes.launch.py
│ ├── wmx_r2_manipulator.launch.py
│ └── wmx_r2_differential.launch.py
├── config/
│ ├── wmx_r2_general_nodes_config.yaml # generic, no robot
│ └── wmx_parameters.xml
├── example/
│ ├── cr3a_manipulator_config.yaml
│ ├── cr3a_wmx_parameters.xml
│ ├── cr5a_manipulator_config.yaml
│ ├── cr5a_wmx_parameters.xml
│ ├── diffbot_differential_config.yaml
│ └── diffbot_wmx_parameters.xml
├── test/ # launch tests, no hardware needed
├── CMakeLists.txt
└── package.xml
Dependencies#
Package dependencies (package.xml)#
<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>rclcpp_lifecycle</depend>
<depend>lifecycle_msgs</depend>
<depend>std_srvs</depend>
<depend>std_msgs</depend>
<depend>sensor_msgs</depend>
<depend>control_msgs</depend>
<depend>trajectory_msgs</depend>
<depend>geometry_msgs</depend>
<depend>nav_msgs</depend>
<depend>tf2</depend>
<depend>tf2_ros</depend>
<depend>wmx_r2_message</depend>
<exec_depend>ros2launch</exec_depend>
<exec_depend>joint_state_publisher</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>rviz2</exec_depend>
<exec_depend>xacro</exec_depend>
WMX libraries (external)#
The package compiles against the WMX3 SDK at the CMake cache path
WMX3_SDK_PATH, default /opt/wmx3. The same path is compiled in and
passed to CreateDevice at runtime by every node.
Library |
Used for |
|---|---|
|
Device create/close, engine and communication control |
|
Axis status, servo power, position/velocity/jog motion |
|
C-spline trajectory execution ( |
|
Digital input and output |
|
EtherCAT master and slave diagnostics |
Important
At runtime the dynamic linker must find the SDK’s shared libraries
(libimdll.so and friends): either an ld.so.conf.d entry for
/opt/wmx3/lib — the SDK installer’s default — or
LD_LIBRARY_PATH=/opt/wmx3/lib. This matters in containers that only
mount the SDK.
Manipulator and differential launches need root for real-time
scheduling. Start them with sudo --preserve-env on the host, or with
wros in the container.
Node coordination#
wmx_engine_node owns the WMX3 engine and nothing else.
wmx_lifecycle_manager_node watches it and drives every other node.
While the engine is communicating, every managed node found on the graph is
brought up to active; a node that joins late or respawns is picked up on
a later sweep. When the engine stops or its device is closed, all of them are
deactivated and cleaned back to unconfigured, because their device
handles are dead, and brought up again when the engine returns.
This is not limited to WMX nodes. Any managed node in the same namespace — a
lifecycle joint_state_publisher, a Nav2 node, one of your own — is driven
the same way. Order it with the manager’s managed_nodes parameter, or
drive nodes by hand through /wmx/lifecycle/set_node_state.
stateDiagram-v2
direction LR
[*] --> unconfigured : process starts
unconfigured --> inactive : configure
inactive --> unconfigured : cleanup
inactive --> active : activate
active --> inactive : deactivate
unconfigured --> [*] : shutdown
note left of unconfigured
No WMX device.
No ROS interfaces.
end note
note right of inactive
WMX device attached
(CreateDevice done).
Still no ROS interfaces.
end note
note right of active
Publishers, subscriptions,
services, action servers
and timers exist.
This is the only state
that answers.
end note
What each lifecycle state means for a WMX node#
Trigger |
Effect |
|---|---|
Engine reaches |
The manager brings every managed node up to |
Engine stops, or its device closes |
Every node is deactivated and cleaned back to |
A node joins late or respawns |
Picked up on the next discovery sweep and brought up |
|
Manual override for one node, or for all of them with an empty
|
Important
Two consequences worth remembering:
A configured-but-inactive node advertises nothing. An empty
ros2 service listis a state problem far more often than a crash.Deactivating
joint_state_broadcasterswitches the servos off, which drops an arm’s holding torque. It is the only node in the stack that touches servo power.
General nodes#
wmx_engine_node#
Creates the WMX3 device, starts and stops EtherCAT communication, reports the engine state, and imports the WMX parameter XML. It is an ordinary node, not a lifecycle node — it is what the lifecycle manager follows.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
CPU core for the real-time engine ( |
|
|
CPU affinity bitmask ( |
|
|
XML imported right after the device is created; injected by launch
from the |
Services: /wmx/engine/set_engine, set_communication,
get_engine_status, import_and_set_all, get_axis_param.
wmx_lifecycle_manager_node#
Polls /wmx/engine/get_engine_status every discovery_period seconds
and drives every managed node to match.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Bring-up order; take-down is the reverse |
|
|
Seconds between engine-state sweeps |
Services: /wmx/lifecycle/set_node_state,
/wmx/lifecycle/get_node_states.
wmx_core_motion_node#
Lifecycle. Serves all /wmx/axes/* services and publishes
/wmx/axes/status. It also arbitrates: while any node listed in
motion_controllers is active, the manual motion services answer
success: false. See ROS2 Services.
wmx_io_node#
Lifecycle. Serves all /wmx/io/* services — input and output, by bit
and by byte, scalar and scattered.
wmx_ethercat_node#
Lifecycle. Serves all /wmx/ecat/* services — master info, register
read, statistics reset, network scan, hot-connect.
Manipulator controllers#
Four lifecycle nodes on top of the general nodes. Each attaches to the WMX3 device itself and calls CoreMotion / AdvancedMotion / IO directly — the general nodes own the engine, not the motion these controllers command.
joint_state_broadcaster#
Reads the encoder every cycle and publishes /joint_states, plus the
optional Isaac Sim and Gazebo mirrors.
It is also the only node in the stack that touches servo power. At
activate it calls /wmx/axes/clear_amp_alarm then
/wmx/axes/set_servo_on for every joint_axes entry, retrying up to
five times before failing the transition. At deactivate it switches the
servos off — so deactivating the broadcaster drops the arm’s holding
torque.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
WMX3 axis index per joint |
|
|
Joint names published in |
|
|
Publish rate in Hz; |
|
|
Real-robot feedback topic; deployments set |
|
|
Isaac Sim mirror, published with a zero header stamp |
|
|
|
|
|
Axis order for that topic |
|
|
|
|
|
Axis order for that topic |
|
|
Extra joint names appended to the feedback message |
|
|
|
|
|
Reported joint value while the bit is 0 |
|
|
Reported joint value while the bit is 1 |
joint_trajectory_controller#
Serves FollowJointTrajectory and executes it as a WMX3 time-based
C-spline. See ROS2 Actions for the full execution contract.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
WMX3 axis index per trajectory joint |
|
|
Joint name per axis; goal columns are matched by name |
|
|
Action server name; must equal the controller name in MoveIt2 |
joint_position_controller#
Follows MoveIt Servo’s streamed JointTrajectory using WMX3 linear
interpolation, so every axis arrives at the same instant. Only the last point
of each message is used.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
WMX3 axis index per joint |
|
|
Name-to-axis map for incoming |
|
|
Streamed trajectory input from MoveIt Servo |
|
|
Velocity used when |
|
|
Fraction of the step spent accelerating; deployments use |
|
|
Deadband against the commanded position; deployments use |
gripper_controller#
Serves /wmx/set_gripper (std_srvs/SetBool) and drives one WMX output
bit. Started only with use_gripper:=true.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Service name (the parameter is called “topic” for historical reasons) |
|
|
|
|
|
Run the gripper power-up sequence at |
Note
The defaults here are deliberate non-values. Topic and action names
default to /<name>/no_param and the axis lists default to empty, so an
unconfigured node is loud and inert rather than silently wrong. Every
deployment supplies a YAML.
All parameters are read once at node construction. There is no
parameter-set callback: ros2 param set is accepted by rclcpp but
changes nothing. Edit the YAML and restart.
Differential drive controller#
differential_drive_controller#
Lifecycle. Drives two EtherCAT wheel axes with CoreMotion StartVel and
exposes the autonomy contract: /cmd_vel_safe in, /odom_enc and the
wheel-velocity topics out. See ROS2 Topics.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
WMX3 axis index of each drive wheel |
|
|
Drive-wheel radius in m; must be > 0 |
|
|
Wheel separation in m; must be > 0 |
|
|
Control-loop rate in Hz; one loop does one |
|
|
|
|
|
Seconds without a command before the wheel target is forced to zero |
|
|
Publish |
|
|
Frame ids for |
|
|
Wheel joint names, in |
Launch files#
Launch file |
Starts |
|---|---|
|
|
|
the general nodes, plus |
|
the general nodes, plus |
The ros2_control variants live in the wmx_r2_control package; see
MoveIt2 Motion Planning and
Nav2 Navigation (Mobile Base).
wmx_r2_general_nodes.launch.py#
Argument |
Default |
Description |
|---|---|---|
|
|
Use the simulation clock |
|
|
YAML with the general node parameters. Empty loads no parameter file: launch warns and every node falls back to its compiled defaults. |
|
|
WMX3 parameter XML imported at engine start. Empty imports nothing. |
wros ros2 launch wmx_r2_package wmx_r2_general_nodes.launch.py \
use_sim_time:=false \
'config_file:=$(ros2 pkg prefix --share wmx_r2_package)/config/wmx_r2_general_nodes_config.yaml' \
'wmx_param_file:=$(ros2 pkg prefix --share wmx_r2_package)/config/wmx_parameters.xml'
wmx_r2_manipulator.launch.py#
Argument |
Default |
Description |
|---|---|---|
|
|
Use the simulation clock |
|
required |
YAML with the manipulator node parameters |
|
|
WMX3 parameter XML imported at engine start |
|
|
Start |
The manipulator launch passes config_file down to the general-nodes
launch unchanged, so engine core and affinity, the WMX parameter path, and
the bring-up order all live in that one YAML.
# Dobot CR3A
wros ros2 launch wmx_r2_package wmx_r2_manipulator.launch.py \
use_sim_time:=false \
'config_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/cr3a_manipulator_config.yaml' \
'wmx_param_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/cr3a_wmx_parameters.xml' \
use_gripper:=true
# Dobot CR5A
wros ros2 launch wmx_r2_package wmx_r2_manipulator.launch.py \
use_sim_time:=false \
'config_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/cr5a_manipulator_config.yaml' \
'wmx_param_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/cr5a_wmx_parameters.xml'
wmx_r2_differential.launch.py#
Same three arguments as the general-nodes launch, with config_file
required.
wros ros2 launch wmx_r2_package wmx_r2_differential.launch.py \
use_sim_time:=false \
'config_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/diffbot_differential_config.yaml' \
'wmx_param_file:=$(ros2 pkg prefix --share wmx_r2_package)/example/diffbot_wmx_parameters.xml'
Configuration files#
A deployment is one YAML plus one XML.
File |
Owns |
|---|---|
ROS parameter YAML |
Everything on the ROS side: which axis is which joint, topic and action names, feedback rates, gripper wiring, bring-up order. |
WMX parameter XML |
Everything on the engine side: gear ratio, feedback, soft limits,
|
Important
Joint values pass through the ROS nodes unconverted. There is no
gear-ratio or offset parameter anywhere in wmx_r2_package. If the
robot moves the wrong distance, the XML is wrong — not the YAML. See
Robot Parameter Configuration and Validation.
cr3a_manipulator_config.yaml#
joint_state_broadcaster:
ros__parameters:
joint_feedback_rate: 100
joint_axes: [0, 1, 2, 3, 4, 5]
joint_name: ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
gripper_joint_name: ["picker_1_joint", "picker_2_joint"]
gripper_address: [0, 0]
gripper_open_value: 0.00
gripper_close_value: 0.045
encoder_joint_topic: /joint_states
isaacsim_joint_topic: /isaacsim/joint_command
gazebo_position_joint_topic: /gazebo_position_controller/commands
gazebo_position_joint_axes: [0, 1, 2, 3, 4, 5]
joint_trajectory_controller:
ros__parameters:
joint_axes: [0, 1, 2, 3, 4, 5]
joint_name: ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
joint_trajectory_action: /movensys_manipulator_arm_controller/follow_joint_trajectory
joint_position_controller:
ros__parameters:
joint_axes: [0, 1, 2, 3, 4, 5]
joint_name: ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
joint_trajectory_topic: /movensys_manipulator_arm_controller/joint_trajectory
accel_ratio: 0.3
default_velocity: 0.5
min_step: 0.001
gripper_controller:
ros__parameters:
wmx_gripper_topic: /wmx/set_gripper
gripper_address: [0, 0]
pre_setup_io: true
wmx_engine_node:
ros__parameters:
core: -1
affinity_mask: 0
wmx_core_motion_node:
ros__parameters:
axes_status_rate: 100
motion_controllers:
- joint_trajectory_controller
- joint_position_controller
wmx_lifecycle_manager_node:
ros__parameters:
managed_nodes: # device-level nodes first
- wmx_core_motion_node
- wmx_io_node
- wmx_ethercat_node
- joint_state_broadcaster
- joint_trajectory_controller
- joint_position_controller
- gripper_controller
discovery_period: 1.0
cr5a_manipulator_config.yaml is the same file with the CR5A’s values.
On a gripperless arm, drop gripper_controller from managed_nodes and
leave use_gripper at false.
diffbot_differential_config.yaml#
differential_drive_controller:
ros__parameters:
left_axis: 0
right_axis: 1
rate: 100
wheel_radius: 0.095
wheel_to_wheel: 0.55
cmd_vel_timeout: 0.25
publish_tf: false
odom_frame: odom
base_frame: base_link
joint_name: ["drivewheel_left_joint", "drivewheel_right_joint"]
cmd_vel_topic: /cmd_vel_safe
cmd_omega_topic: /omega_cmd
encoder_odometry_topic: /odom_enc
encoder_omega_topic: /omega_enc
joint_state_broadcaster:
ros__parameters:
joint_feedback_rate: 100
joint_axes: [0, 1]
joint_name: ["drivewheel_left_joint", "drivewheel_right_joint"]
encoder_joint_topic: /joint_states
gazebo_velocity_joint_topic: /velocity_controller/commands
gazebo_velocity_joint_axes: [0, 1]
wmx_core_motion_node:
ros__parameters:
motion_controllers:
- differential_drive_controller
wmx_lifecycle_manager_node:
ros__parameters:
managed_nodes:
- wmx_core_motion_node
- wmx_io_node
- wmx_ethercat_node
- joint_state_broadcaster
- differential_drive_controller
What to change per robot#
Always per robot |
|
|---|---|
Per gripper |
|
Per planning stack |
|
Usually defaults |
|
Never |
The 1000-point spline buffer size, the arbitration topics, the clear-alarm/servo-on sequence, and any gear-ratio scaling — the WMX XML owns that one |
Building the package#
Build the messages first, then the rest:
wros colcon build --packages-select wmx_r2_message
wros colcon build
The package builds only where the WMX3 SDK is present. wmx_r2_message
does not need the SDK and can be built anywhere.
Tests under test/ are launch tests and need no hardware:
wros colcon test --packages-select wmx_r2_package
See also#
ROS2 Services – every service, with call examples
ROS2 Topics – every topic, with QoS and rates
ROS2 Actions – the
FollowJointTrajectorycontractwmx_r2_message – interface definitions
Robot Parameter Configuration and Validation – what belongs in the WMX XML