ROS 2 port of Robotnik's RB-1 mobile base wired end-to-end through the ros2_control framework. The URDF/xacro description exposes two independent hardware systems — a differential drive (two rear rubber wheels) and a linear actuator (elevator lifting platform) — through the gazebo_ros2_control/GazeboSystem plugin. A single controller_manager spins three controllers on top: joint_state_broadcaster, rb1_base_controller (diff_drive_controller/DiffDriveController) and elevator_controller (effort_controllers/JointGroupEffortController). Validated in Gazebo Classic with the full RB-1 model (base + wheels + omni casters + elevator + Hokuyo laser + IMU + Orbbec Astra RGB-D camera).
rb1_ros2_base.urdf.xacroassembles the base (rb1_base_v3), two rubber wheels, three omni casters, the elevator platform and all sensors under therb1_robot/frame prefix- Two
<ros2_control>blocks declare the hardware systems — both backed bygazebo_ros2_control/GazeboSystem:diff_drive—velocitycommand interface onrobot_left_wheel_jointandrobot_right_wheel_joint(limits ±1), plusposition/velocity/effortstate interfaceslinear_actuator—effortcommand interface onrobot_elevator_platform_joint(limits ±10), plusposition/velocity/effortstate interfaces
- A
<gazebo>block loadslibgazebo_ros2_control.soand points it atconfig/rb1_controller.yaml, which the plugin hands to thecontroller_managerat startup
- Gazebo spawns the robot from the xacro-expanded
robot_descriptionpublished byrobot_state_publisher(withframe_prefix: rb1_robot/) controller_managerruns atupdate_rate: 10 Hzwithuse_sim_time: truejoint_state_broadcasteris loaded first so/joint_statesstarts flowing- An
OnProcessExitevent handler then loadsrb1_base_controller— theDiffDriveControllermaps/rb1_base_controller/cmd_vel_unstamped(geometry_msgs/Twist) into per-wheel velocity commands usingwheel_separation = 0.436 m,wheel_radius = 0.076 m, limitslinear.x ∈ [-1, 1] m/s/angular.z ∈ [-1, 1] rad/s/ accel±1, and publishes/rb1_base_controller/odomat50 Hzplus theodom → robot_base_linkTF - A
spawnernode brings upelevator_controller(JointGroupEffortController), which forwardsstd_msgs/Float64MultiArraymessages on/elevator_controller/commandsstraight to the effort command interface ofrobot_elevator_platform_joint
- Two-joint
ros2_controlsystemdiff_drivedeclared inxacro/rb1_ros2_base.urdf.xacrowithvelocitycommand interfaces on both driven wheels DiffDriveControllerconfigured inconfig/rb1_controller.yaml:left_wheel_names: [robot_left_wheel_joint],right_wheel_names: [robot_right_wheel_joint]wheel_separation: 0.436,wheel_radius: 0.076publish_rate: 50.0,odom_frame_id: odom,base_frame_id: robot_base_linkopen_loop: true,enable_odom_tf: true,use_stamped_vel: false,cmd_vel_timeout: 0.5- Velocity/acceleration limits enabled on
linear.xandangular.z(±1 m/s, ±1 rad/s, ±1 m/s²)
- Launch file
launch/rb1_ros2_xacro.launch.pysequences Gazebo →robot_state_publisher→spawn_entity.py→load_controller joint_state_broadcaster→ (OnProcessExit) →load_controller rb1_base_controller - Command topic:
/rb1_base_controller/cmd_vel_unstamped(geometry_msgs/Twist, unstamped) - Teleop validated with
ros2 topic pub --rate 10 /rb1_base_controller/cmd_vel_unstamped+ odom echoed on/rb1_base_controller/odom
- Single-joint
ros2_controlsystemlinear_actuatorforrobot_elevator_platform_jointwith aneffortcommand interface (limits ±10 Nm) - Controller
elevator_controllerconfigured inconfig/rb1_controller.yamlaseffort_controllers/JointGroupEffortController:joints: [robot_elevator_platform_joint]command_interfaces: [effort]state_interfaces: [position, velocity, effort]
- Loaded through the
controller_manager/spawnernode with--controller-manager-timeout 500 - Elevator lifts by publishing a positive effort on
/elevator_controller/commands, lowers with a negative effort:ros2 topic pub /elevator_controller/commands std_msgs/msg/Float64MultiArray "data: [5.0]"→ platform risesros2 topic pub /elevator_controller/commands std_msgs/msg/Float64MultiArray "data: [-5.0]"→ platform descends
| Name | Type | Description |
|---|---|---|
/rb1_base_controller/cmd_vel_unstamped |
geometry_msgs/Twist (sub) |
Unstamped velocity command for the diff drive |
/rb1_base_controller/odom |
nav_msgs/Odometry (pub) |
Wheel odometry (50 Hz) |
/elevator_controller/commands |
std_msgs/Float64MultiArray (sub) |
Effort command for the elevator joint |
/joint_states |
sensor_msgs/JointState (pub) |
All joint positions/velocities/efforts |
/tf, /tf_static |
tf2_msgs/TFMessage |
Robot TF tree under rb1_robot/ prefix (includes odom → robot_base_link) |
/controller_manager/* |
service set | list_controllers, load_controller, switch_controller, ... |
| Controllers | joint_state_broadcaster, rb1_base_controller, elevator_controller |
|
| Hardware interfaces | robot_left_wheel_joint/velocity (command), robot_right_wheel_joint/velocity (command), robot_elevator_platform_joint/effort (command); position/velocity/effort state on all three |
rb1_ros2_description/
├── xacro/
│ └── rb1_ros2_base.urdf.xacro # Base + wheels + omni + elevator + sensors + <ros2_control> tags + gazebo plugin
├── urdf/
│ ├── bases/rb1_base_v3.urdf.xacro
│ ├── wheels/rubber_wheel.urdf.xacro
│ ├── wheels/omni_wheel.urdf.xacro
│ ├── elevator/elevator.urdf.xacro
│ ├── all_sensors.urdf.xacro # Hokuyo UST-20LX + IMU Hector + Orbbec Astra
│ └── common.gazebo.xacro
├── config/
│ └── rb1_controller.yaml # controller_manager + DiffDriveController + JointGroupEffortController
├── launch/
│ └── rb1_ros2_xacro.launch.py # Gazebo → rsp → spawn → staged controller loading
├── meshes/
├── media/
├── CMakeLists.txt
└── package.xml
- ROS 2 Humble
ros2_control,ros2_controllers(diff_drive_controller,effort_controllers,joint_state_broadcaster)gazebo_ros2_control,gazebo_ros_pkgs- Gazebo Classic 11
xacro,robot_state_publisher
cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash# Terminal 1 - Gazebo + RB-1 + controller_manager + controllers
ros2 launch rb1_ros2_description rb1_ros2_xacro.launch.py# Forward at 0.3 m/s
ros2 topic pub --rate 10 /rb1_base_controller/cmd_vel_unstamped geometry_msgs/msg/Twist \
"{linear: {x: 0.3, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"
# Rotate in place at 0.5 rad/s
ros2 topic pub --rate 10 /rb1_base_controller/cmd_vel_unstamped geometry_msgs/msg/Twist \
"{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.5}}"# Lift
ros2 topic pub /elevator_controller/commands std_msgs/msg/Float64MultiArray "data: [5.0]"
# Lower
ros2 topic pub /elevator_controller/commands std_msgs/msg/Float64MultiArray "data: [-5.0]"# Hardware interfaces exposed by the plugin
ros2 control list_hardware_interfaces
# command: robot_left_wheel_joint/velocity [available] [claimed]
# command: robot_right_wheel_joint/velocity [available] [claimed]
# command: robot_elevator_platform_joint/effort [available] [claimed]
# state: robot_left_wheel_joint/{position,velocity,effort}
# state: robot_right_wheel_joint/{position,velocity,effort}
# state: robot_elevator_platform_joint/{position,velocity,effort}
# Controllers must all be active
ros2 control list_controllers
# joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active
# rb1_base_controller diff_drive_controller/DiffDriveController active
# elevator_controller effort_controllers/JointGroupEffortController active
# Odometry and joint states must flow
ros2 topic echo /rb1_base_controller/odom
ros2 topic echo /joint_statesros2_controlframework:<ros2_control>xacro tags,hardwareplugin,command_interfacevs.state_interface, per-interface limitsgazebo_ros2_control:GazeboSystemhardware plugin,<gazebo>plugin loader, YAML parameter injection intocontroller_managercontroller_manager:update_rate, controller types, spawner vs.ros2 control load_controller- Diff drive controller:
DiffDriveControllerwiring (left_wheel_names,right_wheel_names, geometry, odom frames, velocity/acceleration limits, unstamped command topic) - Joint group effort controller:
JointGroupEffortControllerwitheffortcommand interface for lifting actuators - Launch orchestration:
IncludeLaunchDescriptionfor Gazebo,robot_state_publisherwithframe_prefix,spawn_entity.py,ExecuteProcessforros2 control load_controller,RegisterEventHandler(OnProcessExit(...))to stage controller loading - Robot description pipeline: xacro expansion →
robot_descriptionparameter → Gazebo spawn from topic - Sensor stack: Hokuyo UST-20LX 2D laser, IMU Hector, Orbbec Astra RGB-D camera wired through the shared description
- ROS 2 Humble
ros2_control(controller_manager,diff_drive_controller,effort_controllers,joint_state_broadcaster)gazebo_ros2_control(GazeboSystem)- Gazebo Classic 11
- Robotnik RB-1 mobile base (2× rubber wheels + 3× omni casters + elevator platform)
- Hokuyo UST-20LX, IMU, Orbbec Astra RGB-D
- C++ 17 / Python 3 / XML (URDF/SDF/xacro) / YAML

