ROS Resources: Documentation | Support | Discussion Forum | Index | Service Status | ros @ Robotics Stack Exchange |
1 | initial version |
I'm not really an expert on this and I myself struggled the last two weeks with ROS2, Gazebo and ROS2_Control, but I think you should be able to use ROS2_Control for that.
Here some useful links:
You have to add a ros2_control
tag to your URDF file. (ref). Mine looks like this:
<!-- ROS Control for the rims -->
<ros2_control name="cassiopeia_rim_control" type="system">
<xacro:if value="$(arg use_sim)">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
</xacro:if>
<xacro:unless value="$(arg use_sim)">
<hardware>
<xacro:if value="$(arg use_fake_hardware)">
<plugin>fake_components/GenericSystem</plugin>
<param name="fake_sensor_commands">$(arg fake_sensor_commands)</param>
<param name="state_following_offset">0.0</param>
</xacro:if>
<xacro:unless value="$(arg use_fake_hardware)">
<!-- TODO: If we want this feature, we have to write an
hardware controller plugin in C++ for ROS2 Control.
-->
</xacro:unless>
</hardware>
</xacro:unless>
<joint name="a_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="b_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="c_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find pkg_name)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
It is worth noting that, if you want to use ROS2 Control with gazebo, you have to use one ros2_control
tag with type system
for all joints. Additionally, the xacro:if
in the beginning is if you want to use the same model in Gazebo and on the real robot. If you are only interested in Gazebo, just evaluate use_sim
to true
.
Then you'll also need the specified controllers.yaml
. Here is mine:
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
wheel_velocity_controller:
type: velocity_controllers/JointGroupVelocityController
wheel_velocity_controller:
ros__parameters:
joints:
- a_rim_joint
- b_rim_joint
- c_rim_joint
command_interfaces:
- velocity
state_interfaces:
- position
- velocity
Here we have one controller for all three joints, that can be controlled with topic: /wheel_velocity_controller/commands
of type: Type: std_msgs/msg/Float64MultiArray
. If you want different topics for each joint, just define more controllers in the yaml file.
2 | No.2 Revision |
I'm not really an expert on this and I myself struggled the last two weeks with ROS2, Gazebo and ROS2_Control, but I think you should be able to use ROS2_Control for that.
Here some useful links:
You have to add a ros2_control
tag to your URDF file. (ref). Mine looks like this:
<!-- ROS Control for the rims -->
<ros2_control name="cassiopeia_rim_control" type="system">
<xacro:if value="$(arg use_sim)">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
</xacro:if>
<xacro:unless value="$(arg use_sim)">
<hardware>
<xacro:if value="$(arg use_fake_hardware)">
<plugin>fake_components/GenericSystem</plugin>
<param name="fake_sensor_commands">$(arg fake_sensor_commands)</param>
<param name="state_following_offset">0.0</param>
</xacro:if>
<xacro:unless value="$(arg use_fake_hardware)">
<!-- TODO: If we want this feature, we have to write an
hardware controller plugin in C++ for ROS2 Control.
-->
</xacro:unless>
</hardware>
</xacro:unless>
<joint name="a_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="b_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="c_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find pkg_name)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
It is worth noting that, if you want to use ROS2 Control with gazebo, you have to use one ros2_control
tag with type system
for all joints. Additionally, the xacro:if
in the beginning is if you want to use the same model in Gazebo and on the real robot. If you are only interested in Gazebo, just evaluate use_sim
to true
.
Then you'll also need the specified controllers.yaml
. Here is mine:
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
wheel_velocity_controller:
type: velocity_controllers/JointGroupVelocityController
wheel_velocity_controller:
ros__parameters:
joints:
- a_rim_joint
- b_rim_joint
- c_rim_joint
command_interfaces:
- velocity
state_interfaces:
- position
- velocity
Here we have one controller for all three joints, that can be controlled with topic: /wheel_velocity_controller/commands
of type: Type: std_msgs/msg/Float64MultiArray
. If you want different topics for each joint, just define more controllers in the yaml file.
Update 1: Here is my launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os
def generate_launch_description():
pkg_path = FindPackageShare("cassiopeia_description")
use_sim = LaunchConfiguration("use_sim")
use_fake_hardware = LaunchConfiguration("use_fake_hardware")
fake_sensor_commands = LaunchConfiguration("fake_sensor_commands")
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "cassiopeia.urdf.xacro"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Get URDF from Xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
robot_description = {"robot_description": robot_description_content}
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"controllers.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
return LaunchDescription(
[
#
# Launch Arguments
#
DeclareLaunchArgument(
"use_sim",
default_value="false",
description="Use simulation (Gazebo) clock if true",
),
DeclareLaunchArgument(
"use_fake_hardware",
default_value="true",
description="Start robot with fake hardware mirroring command to its states.",
),
DeclareLaunchArgument(
"fake_sensor_commands",
default_value="false",
description="Enable fake command interfaces for sensors used for simple simulations. \
Used only if 'use_fake_hardware' parameter is true.",
),
#
# Nodes
#
Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[robot_description, controller_config_path],
output={
"stdout": "screen",
"stderr": "screen",
},
condition=UnlessCondition(use_sim),
),
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description, {"use_sim_time": use_sim}],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
#
# Launch Files
#
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("kamaro_gazebo"), "launch", "simulation.launch.py"]
)
]
),
launch_arguments={"verbose": "false"}.items(),
condition=IfCondition(use_sim),
),
]
)
Please note, that in foxy
and below, you have to avoid :
in your xml comments in the xacro file. See here: https://answers.ros.org/question/382000/ros2-makes-launch-files-crazy-too-soon-to-be-migrating/?answer=382141#post-id-382141
3 | No.3 Revision |
I'm not really an expert on this and I myself struggled the last two weeks with ROS2, Gazebo and ROS2_Control, but I think you should be able to use ROS2_Control for that.
Here some useful links:
You have to add a ros2_control
tag to your URDF file. (ref). Mine looks like this:
<!-- ROS Control for the rims -->
<ros2_control name="cassiopeia_rim_control" type="system">
<xacro:if value="$(arg use_sim)">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
</xacro:if>
<xacro:unless value="$(arg use_sim)">
<hardware>
<xacro:if value="$(arg use_fake_hardware)">
<plugin>fake_components/GenericSystem</plugin>
<param name="fake_sensor_commands">$(arg fake_sensor_commands)</param>
<param name="state_following_offset">0.0</param>
</xacro:if>
<xacro:unless value="$(arg use_fake_hardware)">
<!-- TODO: If we want this feature, we have to write an
hardware controller plugin in C++ for ROS2 Control.
-->
</xacro:unless>
</hardware>
</xacro:unless>
<joint name="a_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="b_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="c_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find pkg_name)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
It is worth noting that, if you want to use ROS2 Control with gazebo, you have to use one ros2_control
tag with type system
for all joints. Additionally, the xacro:if
in the beginning is if you want to use the same model in Gazebo and on the real robot. If you are only interested in Gazebo, just evaluate use_sim
to true
.
Then you'll also need the specified controllers.yaml
. Here is mine:
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
wheel_velocity_controller:
type: velocity_controllers/JointGroupVelocityController
wheel_velocity_controller:
ros__parameters:
joints:
- a_rim_joint
- b_rim_joint
- c_rim_joint
command_interfaces:
- velocity
state_interfaces:
- position
- velocity
Here we have one controller for all three joints, that can be controlled with topic: /wheel_velocity_controller/commands
of type: Type: std_msgs/msg/Float64MultiArray
. If you want different topics for each joint, just define more controllers in the yaml file.
Update 1: Here is my launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os
def generate_launch_description():
pkg_path = FindPackageShare("cassiopeia_description")
use_sim = LaunchConfiguration("use_sim")
use_fake_hardware = LaunchConfiguration("use_fake_hardware")
fake_sensor_commands = LaunchConfiguration("fake_sensor_commands")
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "cassiopeia.urdf.xacro"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Get URDF from Xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
robot_description = {"robot_description": robot_description_content}
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"controllers.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
return LaunchDescription(
[
#
# Launch Arguments
#
DeclareLaunchArgument(
"use_sim",
default_value="false",
description="Use simulation (Gazebo) clock if true",
),
DeclareLaunchArgument(
"use_fake_hardware",
default_value="true",
description="Start robot with fake hardware mirroring command to its states.",
),
DeclareLaunchArgument(
"fake_sensor_commands",
default_value="false",
description="Enable fake command interfaces for sensors used for simple simulations. \
Used only if 'use_fake_hardware' parameter is true.",
),
#
# Nodes
#
Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[robot_description, controller_config_path],
output={
"stdout": "screen",
"stderr": "screen",
},
condition=UnlessCondition(use_sim),
),
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description, {"use_sim_time": use_sim}],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
#
# Launch Files
#
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("kamaro_gazebo"), "launch", "simulation.launch.py"]
)
]
),
launch_arguments={"verbose": "false"}.items(),
condition=IfCondition(use_sim),
),
]
)
Please note, that in foxy
and below, you have to avoid :
in your xml comments in the xacro file. See here: https://answers.ros.org/question/382000/ros2-makes-launch-files-crazy-too-soon-to-be-migrating/?answer=382141#post-id-382141
Update 2: It's not surprising, that you have a lot of issues. Here are some things that are don't make any sense.
robot_state_publisher
robot_description_content = Command( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", PathJoinSubstitution( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", xacro_path, " ", "use_sim:=", use_sim, " ", "use_fake_hardware:=", use_fake_hardware, " ", "fake_sensor_commands:=", fake_sensor_commands, FindPackageShare("my_robot_description"), "src/description", "my_robot.urdf", ] ), ] )
Here is what you are doing. run /path/to/xacro path/to/xacro path/to/model.urdf.xacro arg1:=a arg2:=b arg3:=c /path/to/robot_desc/src/desc/my_robot.urdf
Xacro is an command which takes a path to an xacro file und returns an urdf file. You can run xacro --help
to see this.
Here is what you should do: run xacro /file/to/model.urdf.xacro arg1:=a
. The arg part is only needed if you pass arguments to your xacro file. I'll make an example later.
You can also think of PathJoinSubstitution
as os.path.join
. The only thing it does, is evaluating the params a bit later.
For now, here is how this launch looks like:
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
In the end, this will make a call like this: /opt/ros/foxy/bin/xacro ~/ws/install/robot_description/share/robot_description/urdf/robot.urdf.xacro use_sim:=false use_fake_hardware:=true fake_sensor_commands:=false
And as I sayed, you only need those arguments at the end, if you use them in your xacro file. Here is how you do this:
In your robot.urdf.xacro
file:
<!-- Enable setting arguments from the launch file -->
<xacro:arg name="param1" default="true" />
<xacro:arg name="param2" default="1337" />
<!-- Use xacro params as if-else -->
<xacro:if value="$(arg use_sim)">
Test
</xacro:if>
<param name="name">$(arg param2)</param>
You can always debug your xacro file with the command xacro path/to/robot.urdf.xacro
.
robot_description = {"robot_description": robot_description_content}
is there twice.
I gave up and created a launch file, that does anything I think you'll need:
from launch import LaunchDescription, LaunchContext from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution from launch.conditions import IfCondition from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.substitutions import FindPackageShare from launch_ros.actions import Node
import os
def generate_launch_description(): pkg_path = FindPackageShare("my_robot_description")
# Check if xacro file does exist
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "my_robot.urdf"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Create robot_description
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
]
)
robot_description = {"robot_description": robot_description_content}
# Check if controller config does exist
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"gazebo_ros_control_params.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
# Check if rviz file does exist
rviz_config_file = PathJoinSubstitution([pkg_path, "rviz", "urdf_config.rviz"])
assert os.path.exists(rviz_config_file.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(rviz_config_file.perform(LaunchContext()))
+ ")"
)
# Launch configuration variables specific to simulation
gui = LaunchConfiguration("gui")
world = LaunchConfiguration("world")
return LaunchDescription(
[
# Launch arguments used for gazebo
DeclareLaunchArgument(
"gui", default_value="True", description="Whether to execute gzclient"
),
DeclareLaunchArgument(
"world",
default_value=PathJoinSubstitution([pkg_path, "worlds", "willowgarage.world"]),
description="Full path to world model file to load",
),
# Launch gazebo server and client
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzserver.launch.py"]
)
]
),
launch_arguments={"verbose": "true", "world": world}.items(),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzclient.launch.py"]
)
]
),
condition=IfCondition(gui),
),
# Launch rviz
Node(
package="rviz2",
executable="rviz2",
name="rviz2",
output="log",
arguments=["-d", rviz_config_file],
),
# Launch robot state publisher
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description],
),
# Spawn the robot in gazebo from the robot_description provided by robot_state_publisher
Node(
package="gazebo_ros",
executable="spawn_entity.py",
arguments=["-topic", "robot_description", "-entity", "robot"],
output="screen",
),
# Spawn joint_state_broadcaster
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
# Spawn custom controller
# YOU PROBABLY NEED TO MODIFY THIS AND ADD ONE OF THOSE FOR EACH CONTROLLER
# IN YOUR controller_config_path
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
]
)
4 | No.4 Revision |
I'm not really an expert on this and I myself struggled the last two weeks with ROS2, Gazebo and ROS2_Control, but I think you should be able to use ROS2_Control for that.
Here some useful links:
You have to add a ros2_control
tag to your URDF file. (ref). Mine looks like this:
<!-- ROS Control for the rims -->
<ros2_control name="cassiopeia_rim_control" type="system">
<xacro:if value="$(arg use_sim)">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
</xacro:if>
<xacro:unless value="$(arg use_sim)">
<hardware>
<xacro:if value="$(arg use_fake_hardware)">
<plugin>fake_components/GenericSystem</plugin>
<param name="fake_sensor_commands">$(arg fake_sensor_commands)</param>
<param name="state_following_offset">0.0</param>
</xacro:if>
<xacro:unless value="$(arg use_fake_hardware)">
<!-- TODO: If we want this feature, we have to write an
hardware controller plugin in C++ for ROS2 Control.
-->
</xacro:unless>
</hardware>
</xacro:unless>
<joint name="a_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="b_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="c_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find pkg_name)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
It is worth noting that, if you want to use ROS2 Control with gazebo, you have to use one ros2_control
tag with type system
for all joints. Additionally, the xacro:if
in the beginning is if you want to use the same model in Gazebo and on the real robot. If you are only interested in Gazebo, just evaluate use_sim
to true
.
Then you'll also need the specified controllers.yaml
. Here is mine:
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
wheel_velocity_controller:
type: velocity_controllers/JointGroupVelocityController
wheel_velocity_controller:
ros__parameters:
joints:
- a_rim_joint
- b_rim_joint
- c_rim_joint
command_interfaces:
- velocity
state_interfaces:
- position
- velocity
Here we have one controller for all three joints, that can be controlled with topic: /wheel_velocity_controller/commands
of type: Type: std_msgs/msg/Float64MultiArray
. If you want different topics for each joint, just define more controllers in the yaml file.
Update 1: Here is my launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os
def generate_launch_description():
pkg_path = FindPackageShare("cassiopeia_description")
use_sim = LaunchConfiguration("use_sim")
use_fake_hardware = LaunchConfiguration("use_fake_hardware")
fake_sensor_commands = LaunchConfiguration("fake_sensor_commands")
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "cassiopeia.urdf.xacro"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Get URDF from Xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
robot_description = {"robot_description": robot_description_content}
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"controllers.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
return LaunchDescription(
[
#
# Launch Arguments
#
DeclareLaunchArgument(
"use_sim",
default_value="false",
description="Use simulation (Gazebo) clock if true",
),
DeclareLaunchArgument(
"use_fake_hardware",
default_value="true",
description="Start robot with fake hardware mirroring command to its states.",
),
DeclareLaunchArgument(
"fake_sensor_commands",
default_value="false",
description="Enable fake command interfaces for sensors used for simple simulations. \
Used only if 'use_fake_hardware' parameter is true.",
),
#
# Nodes
#
Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[robot_description, controller_config_path],
output={
"stdout": "screen",
"stderr": "screen",
},
condition=UnlessCondition(use_sim),
),
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description, {"use_sim_time": use_sim}],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
#
# Launch Files
#
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("kamaro_gazebo"), "launch", "simulation.launch.py"]
)
]
),
launch_arguments={"verbose": "false"}.items(),
condition=IfCondition(use_sim),
),
]
)
Please note, that in foxy
and below, you have to avoid :
in your xml comments in the xacro file. See here: https://answers.ros.org/question/382000/ros2-makes-launch-files-crazy-too-soon-to-be-migrating/?answer=382141#post-id-382141
Update 2: It's not surprising, that you have a lot of issues. Here are some things that are don't make any sense.
robot_state_publisher
robot_description_content = Command( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", PathJoinSubstitution( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", xacro_path, " ", "use_sim:=", use_sim, " ", "use_fake_hardware:=", use_fake_hardware, " ", "fake_sensor_commands:=", fake_sensor_commands, FindPackageShare("my_robot_description"), "src/description", "my_robot.urdf", ] ), ] )
Here is what you are doing. run /path/to/xacro path/to/xacro path/to/model.urdf.xacro arg1:=a arg2:=b arg3:=c /path/to/robot_desc/src/desc/my_robot.urdf
Xacro is an command which takes a path to an xacro file und returns an urdf file. You can run xacro --help
to see this.
Here is what you should do: run xacro /file/to/model.urdf.xacro arg1:=a
. The arg part is only needed if you pass arguments to your xacro file. I'll make an example later.
You can also think of PathJoinSubstitution
as os.path.join
. The only thing it does, is evaluating the params a bit later.
For now, here is how this launch looks like:
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
In the end, this will make a call like this: /opt/ros/foxy/bin/xacro ~/ws/install/robot_description/share/robot_description/urdf/robot.urdf.xacro use_sim:=false use_fake_hardware:=true fake_sensor_commands:=false
And as I sayed, you only need those arguments at the end, if you use them in your xacro file. Here is how you do this:
In your robot.urdf.xacro
file:
<!-- Enable setting arguments from the launch file -->
<xacro:arg name="param1" default="true" />
<xacro:arg name="param2" default="1337" />
<!-- Use xacro params as if-else -->
<xacro:if value="$(arg use_sim)">
Test
</xacro:if>
<param name="name">$(arg param2)</param>
You can always debug your xacro file with the command xacro path/to/robot.urdf.xacro
.
robot_description = {"robot_description": robot_description_content}
is there twice.
I gave up and created a launch file, that does anything I think you'll need:need. Read through the ros2 control demos and other questions here. The Xacro params I use, like use_sim
are for making the robot_description
usable on the real robot as well as in gazebo. For what I understand, you only need the Gazebo capability. So don't forget your <ros2_control name="..." type="system">
tag in your xacro file, as well as, this gazebo tag:
<gazebo> <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control"> <parameters>$(find my_robot_description)/config/gazebo_ros_control_params.yaml</parameters> </plugin> </gazebo>
And here is your launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource PythonLaunchDescriptionSource
from launch_ros.substitutions import FindPackageShare
from launch_ros.actions import Node Node
import os os
def generate_launch_description():
pkg_path = FindPackageShare("my_robot_description")
FindPackageShare("my_robot_description")
# Check if xacro file does exist
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "my_robot.urdf"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Create robot_description
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
]
)
robot_description = {"robot_description": robot_description_content}
# Check if controller config does exist
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"gazebo_ros_control_params.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
# Check if rviz file does exist
rviz_config_file = PathJoinSubstitution([pkg_path, "rviz", "urdf_config.rviz"])
assert os.path.exists(rviz_config_file.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(rviz_config_file.perform(LaunchContext()))
+ ")"
)
# Launch configuration variables specific to simulation
gui = LaunchConfiguration("gui")
world = LaunchConfiguration("world")
return LaunchDescription(
[
# Launch arguments used for gazebo
DeclareLaunchArgument(
"gui", default_value="True", description="Whether to execute gzclient"
),
DeclareLaunchArgument(
"world",
default_value=PathJoinSubstitution([pkg_path, "worlds", "willowgarage.world"]),
description="Full path to world model file to load",
),
# Launch gazebo server and client
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzserver.launch.py"]
)
]
),
launch_arguments={"verbose": "true", "world": world}.items(),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzclient.launch.py"]
)
]
),
condition=IfCondition(gui),
),
# Launch rviz
Node(
package="rviz2",
executable="rviz2",
name="rviz2",
output="log",
arguments=["-d", rviz_config_file],
),
# Launch robot state publisher
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description],
),
# Spawn the robot in gazebo from the robot_description provided by robot_state_publisher
Node(
package="gazebo_ros",
executable="spawn_entity.py",
arguments=["-topic", "robot_description", "-entity", "robot"],
output="screen",
),
# Spawn joint_state_broadcaster
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
# Spawn custom controller
# YOU PROBABLY NEED TO MODIFY THIS AND ADD ONE OF THOSE FOR EACH CONTROLLER
# IN YOUR controller_config_path
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
]
)
5 | No.5 Revision |
I'm not really an expert on this and I myself struggled the last two weeks with ROS2, Gazebo and ROS2_Control, but I think you should be able to use ROS2_Control for that.
Here some useful links:
You have to add a ros2_control
tag to your URDF file. (ref). Mine looks like this:
<!-- ROS Control for the rims -->
<ros2_control name="cassiopeia_rim_control" type="system">
<xacro:if value="$(arg use_sim)">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
</hardware>
</xacro:if>
<xacro:unless value="$(arg use_sim)">
<hardware>
<xacro:if value="$(arg use_fake_hardware)">
<plugin>fake_components/GenericSystem</plugin>
<param name="fake_sensor_commands">$(arg fake_sensor_commands)</param>
<param name="state_following_offset">0.0</param>
</xacro:if>
<xacro:unless value="$(arg use_fake_hardware)">
<!-- TODO: If we want this feature, we have to write an
hardware controller plugin in C++ for ROS2 Control.
-->
</xacro:unless>
</hardware>
</xacro:unless>
<joint name="a_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="b_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="c_rim_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max">1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find pkg_name)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
It is worth noting that, if you want to use ROS2 Control with gazebo, you have to use one ros2_control
tag with type system
for all joints. Additionally, the xacro:if
in the beginning is if you want to use the same model in Gazebo and on the real robot. If you are only interested in Gazebo, just evaluate use_sim
to true
.
Then you'll also need the specified controllers.yaml
. Here is mine:
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
wheel_velocity_controller:
type: velocity_controllers/JointGroupVelocityController
wheel_velocity_controller:
ros__parameters:
joints:
- a_rim_joint
- b_rim_joint
- c_rim_joint
command_interfaces:
- velocity
state_interfaces:
- position
- velocity
Here we have one controller for all three joints, that can be controlled with topic: /wheel_velocity_controller/commands
of type: Type: std_msgs/msg/Float64MultiArray
. If you want different topics for each joint, just define more controllers in the yaml file.
Update 1: Here is my launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os
def generate_launch_description():
pkg_path = FindPackageShare("cassiopeia_description")
use_sim = LaunchConfiguration("use_sim")
use_fake_hardware = LaunchConfiguration("use_fake_hardware")
fake_sensor_commands = LaunchConfiguration("fake_sensor_commands")
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "cassiopeia.urdf.xacro"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Get URDF from Xacro
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
robot_description = {"robot_description": robot_description_content}
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"controllers.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
return LaunchDescription(
[
#
# Launch Arguments
#
DeclareLaunchArgument(
"use_sim",
default_value="false",
description="Use simulation (Gazebo) clock if true",
),
DeclareLaunchArgument(
"use_fake_hardware",
default_value="true",
description="Start robot with fake hardware mirroring command to its states.",
),
DeclareLaunchArgument(
"fake_sensor_commands",
default_value="false",
description="Enable fake command interfaces for sensors used for simple simulations. \
Used only if 'use_fake_hardware' parameter is true.",
),
#
# Nodes
#
Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[robot_description, controller_config_path],
output={
"stdout": "screen",
"stderr": "screen",
},
condition=UnlessCondition(use_sim),
),
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description, {"use_sim_time": use_sim}],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
#
# Launch Files
#
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("kamaro_gazebo"), "launch", "simulation.launch.py"]
)
]
),
launch_arguments={"verbose": "false"}.items(),
condition=IfCondition(use_sim),
),
]
)
Please note, that in foxy
and below, you have to avoid :
in your xml comments in the xacro file. See here: https://answers.ros.org/question/382000/ros2-makes-launch-files-crazy-too-soon-to-be-migrating/?answer=382141#post-id-382141
Update 2: It's not surprising, that you have a lot of issues. Here are some things that are don't make any sense.
robot_state_publisher
robot_description_content = Command( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", PathJoinSubstitution( [ PathJoinSubstitution([FindExecutable(name="xacro")]), " ", xacro_path, " ", "use_sim:=", use_sim, " ", "use_fake_hardware:=", use_fake_hardware, " ", "fake_sensor_commands:=", fake_sensor_commands, FindPackageShare("my_robot_description"), "src/description", "my_robot.urdf", ] ), ] )
Here is what you are doing. run /path/to/xacro path/to/xacro path/to/model.urdf.xacro arg1:=a arg2:=b arg3:=c /path/to/robot_desc/src/desc/my_robot.urdf
Xacro is an command which takes a path to an xacro file und returns an urdf file. You can run xacro --help
to see this.
Here is what you should do: run xacro /file/to/model.urdf.xacro arg1:=a
. The arg part is only needed if you pass arguments to your xacro file. I'll make an example later.
You can also think of PathJoinSubstitution
as os.path.join
. The only thing it does, is evaluating the params a bit later.
For now, here is how this launch looks like:
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
" ",
"use_sim:=",
use_sim,
" ",
"use_fake_hardware:=",
use_fake_hardware,
" ",
"fake_sensor_commands:=",
fake_sensor_commands,
]
)
In the end, this will make a call like this: /opt/ros/foxy/bin/xacro ~/ws/install/robot_description/share/robot_description/urdf/robot.urdf.xacro use_sim:=false use_fake_hardware:=true fake_sensor_commands:=false
And as I sayed, you only need those arguments at the end, if you use them in your xacro file. Here is how you do this:
In your robot.urdf.xacro
file:
<!-- Enable setting arguments from the launch file -->
<xacro:arg name="param1" default="true" />
<xacro:arg name="param2" default="1337" />
<!-- Use xacro params as if-else -->
<xacro:if value="$(arg use_sim)">
Test
</xacro:if>
<param name="name">$(arg param2)</param>
You can always debug your xacro file with the command xacro path/to/robot.urdf.xacro
.
robot_description = {"robot_description": robot_description_content}
is there twice.
I gave up and created a launch file, that does anything I think you'll need. Read through the ros2 control demos and other questions here. The Xacro params I use, like use_sim
are for making the robot_description
usable on the real robot as well as in gazebo. For what I understand, you only need the Gazebo capability. So don't forget your <ros2_control name="..." type="system">
tag in your xacro file, as well as, this gazebo tag:
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find my_robot_description)/config/gazebo_ros_control_params.yaml</parameters>
cassiopeia_description)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
And here is your launch file:
from launch import LaunchDescription, LaunchContext
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.substitutions import FindPackageShare
from launch_ros.actions import Node
import os
def generate_launch_description():
pkg_path = FindPackageShare("my_robot_description")
# Check if xacro file does exist
xacro_path = PathJoinSubstitution([pkg_path, "urdf", "my_robot.urdf"])
assert os.path.exists(xacro_path.perform(LaunchContext())), (
"The xacro file doesn't exist (" + str(xacro_path.perform(LaunchContext())) + ")"
)
# Create robot_description
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
xacro_path,
]
)
robot_description = {"robot_description": robot_description_content}
# Check if controller config does exist
controller_config_path = PathJoinSubstitution(
[
pkg_path,
"config",
"gazebo_ros_control_params.yaml",
]
)
assert os.path.exists(controller_config_path.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(controller_config_path.perform(LaunchContext()))
+ ")"
)
# Check if rviz file does exist
rviz_config_file = PathJoinSubstitution([pkg_path, "rviz", "urdf_config.rviz"])
assert os.path.exists(rviz_config_file.perform(LaunchContext())), (
"The controller config doesn't exist ("
+ str(rviz_config_file.perform(LaunchContext()))
+ ")"
)
# Launch configuration variables specific to simulation
gui = LaunchConfiguration("gui")
world = LaunchConfiguration("world")
return LaunchDescription(
[
# Launch arguments used for gazebo
DeclareLaunchArgument(
"gui", default_value="True", description="Whether to execute gzclient"
),
DeclareLaunchArgument(
"world",
default_value=PathJoinSubstitution([pkg_path, "worlds", "willowgarage.world"]),
description="Full path to world model file to load",
),
# Launch gazebo server and client
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzserver.launch.py"]
)
]
),
launch_arguments={"verbose": "true", "world": world}.items(),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
[
PathJoinSubstitution(
[FindPackageShare("gazebo_ros"), "launch", "gzclient.launch.py"]
)
]
),
condition=IfCondition(gui),
),
# Launch rviz
Node(
package="rviz2",
executable="rviz2",
name="rviz2",
output="log",
arguments=["-d", rviz_config_file],
),
# Launch robot state publisher
Node(
package="robot_state_publisher",
executable="robot_state_publisher",
output="both",
parameters=[robot_description],
),
# Spawn the robot in gazebo from the robot_description provided by robot_state_publisher
Node(
package="gazebo_ros",
executable="spawn_entity.py",
arguments=["-topic", "robot_description", "-entity", "robot"],
output="screen",
),
# Spawn joint_state_broadcaster
Node(
package="controller_manager",
executable="spawner.py",
arguments=[
"joint_state_broadcaster",
"--controller-manager",
"/controller_manager",
],
),
# Spawn custom controller
# YOU PROBABLY NEED TO MODIFY THIS AND ADD ONE OF THOSE FOR EACH CONTROLLER
# IN YOUR controller_config_path
Node(
package="controller_manager",
executable="spawner.py",
arguments=["wheel_velocity_controller", "-c", "/controller_manager"],
),
]
)