Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@
*.exe
*.out
*.app

# Python
__pycache__/
*.py[cod]
*$py.class
*.pyc
171 changes: 171 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Gazebo Classic to Gazebo Sim Migration Guide

This document describes the migration from Gazebo Classic to Gazebo Sim (formerly Ignition Gazebo) for the hunter_robot package.

## Overview

The hunter_gazebo package now supports both Gazebo Classic and Gazebo Sim, allowing users to choose their preferred simulation environment.

## What Changed

### 1. Package Dependencies (hunter_gazebo/package.xml)

Added new dependencies for Gazebo Sim while keeping Gazebo Classic dependencies:

```xml
<exec_depend>ros_gz_sim</exec_depend>
<exec_depend>ros_gz_bridge</exec_depend>
<exec_depend>gz_ros2_control</exec_depend>
```

### 2. Launch Files

- **launch_sim.launch.py** (unchanged): For Gazebo Classic
- **launch_gz_sim.launch.py** (new): For Gazebo Sim

Key differences in the Gazebo Sim launch file:
- Uses `ros_gz_sim/gz_sim.launch.py` instead of `gazebo_ros/gazebo.launch.py`
- Uses `ros_gz_sim/create` instead of `gazebo_ros/spawn_entity.py`
- Adds clock bridge: `/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock`
- Uses `controller_manager/spawner` nodes instead of `ExecuteProcess` with ros2 CLI

### 3. URDF/Xacro (hunter_description/description/ros2_control.xacro)

Added conditional plugin loading using xacro arguments to prevent both plugins from loading simultaneously:

```xml
<!-- Argument to select simulator type: 'classic' or 'sim' (default: classic) -->
<xacro:arg name="sim_gazebo" default="classic"/>

<!-- Gazebo Classic plugin - loaded when sim_gazebo:=classic -->
<xacro:if value="$(arg sim_gazebo == 'classic')">
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml</parameters>
</plugin>
</gazebo>
</xacro:if>

<!-- Gazebo Sim plugin - loaded when sim_gazebo:=sim -->
<xacro:if value="$(arg sim_gazebo == 'sim')">
<gazebo>
<plugin filename="libgz_ros2_control-system.so" name="gz_ros2_control::GazeboSimROS2ControlPlugin">
<parameters>$(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml</parameters>
</plugin>
</gazebo>
</xacro:if>
```

The launch files pass the appropriate `sim_gazebo` argument to ensure only the correct plugin is loaded for each simulator. Also updated the parameter path syntax from `$(find ...)` (ROS1 style) to `$(find-pkg-share ...)` (ROS2 style).

## Usage

### Gazebo Sim (Recommended)

```bash
ros2 launch hunter_gazebo launch_gz_sim.launch.py
```

### Gazebo Classic (Legacy)

```bash
ros2 launch hunter_gazebo launch_sim.launch.py
```

### Teleop Control (works with both)

```bash
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/ackermann_like_controller/cmd_vel
```

## Technical Details

### Controller Loading

**Gazebo Classic approach** (ExecuteProcess):
```python
load_joint_state_broadcaster = ExecuteProcess(
cmd=['ros2', 'control', 'load_controller', '--set-state', 'active',
'joint_state_broadcaster'],
output='screen'
)
```

**Gazebo Sim approach** (spawner node):
```python
load_joint_state_broadcaster = Node(
package='controller_manager',
executable='spawner',
arguments=['joint_state_broadcaster'],
output='screen'
)
```

### Clock Synchronization

Gazebo Sim requires explicit clock bridging:
```python
bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
arguments=['/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock'],
output='screen'
)
```

### Plugin Selection

The URDF uses xacro conditional logic to load only one plugin at a time based on the `sim_gazebo` argument:
- `sim_gazebo:=classic` (default) loads `libgazebo_ros2_control.so` for Gazebo Classic
- `sim_gazebo:=sim` loads `libgz_ros2_control-system.so` for Gazebo Sim

This prevents both plugins from attempting to load simultaneously.

## Backward Compatibility

All changes maintain backward compatibility:
- Original Gazebo Classic launch file remains unchanged
- Only the appropriate plugin loads based on which simulator is running (controlled by xacro argument)
- Original dependencies are kept alongside new ones
- Default behavior (when no argument is passed) is Gazebo Classic

## Testing

To test the migration:

1. **Build the package**:
```bash
colcon build --packages-select hunter_gazebo hunter_description
source install/setup.bash
```

2. **Test Gazebo Sim**:
```bash
ros2 launch hunter_gazebo launch_gz_sim.launch.py
```

3. **Test Gazebo Classic** (ensure backward compatibility):
```bash
ros2 launch hunter_gazebo launch_sim.launch.py
```

4. **Verify controllers**:
```bash
ros2 control list_controllers
```

Expected output:
- `joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active`
- `ackermann_like_controller[tricycle_controller/TricycleController] active`

5. **Test robot control**:
```bash
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/ackermann_like_controller/cmd_vel
```

## References

- [LCAS Migration Guide](https://github.com/LCAS/aoc_distro/wiki/Migration-to-new-Gazebo)
- [ros_gz_sim Documentation](https://github.com/gazebosim/ros_gz)
- [gz_ros2_control Documentation](https://github.com/ros-controls/gz_ros2_control)
- [RBT1001 Reference Implementation](https://github.com/LCAS/RBT1001)
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,17 @@ This launch file starts Rviz and loads the Hunter V2 model.

### 2. Simulate the Robot in Gazebo

To load the Hunter V2 in a Gazebo simulation environment:
#### Option A: Gazebo Sim (Recommended for new installations)

To load the Hunter V2 in Gazebo Sim (formerly Ignition Gazebo):

```bash
ros2 launch hunter_gazebo launch_gz_sim.launch.py
```

#### Option B: Gazebo Classic (Legacy)

To load the Hunter V2 in Gazebo Classic:

```bash
ros2 launch hunter_gazebo launch_sim.launch.py
Expand Down
26 changes: 21 additions & 5 deletions hunter_description/description/ros2_control.xacro
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">

<!-- Argument to select simulator type: 'classic' or 'sim' (default: classic for backward compatibility) -->
<xacro:arg name="sim_gazebo" default="classic"/>

<ros2_control name="GazeboSystem" type="system">
<hardware>
<plugin>gazebo_ros2_control/GazeboSystem</plugin>
Expand Down Expand Up @@ -38,10 +41,23 @@
</joint>

</ros2_control>
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find hunter_description)/config/ackermann_like_controller.yaml</parameters>
</plugin>
</gazebo>

<!-- Gazebo Classic plugin -->
<xacro:if value="$(arg sim_gazebo == 'classic')">
<gazebo>
<plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
<parameters>$(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml</parameters>
</plugin>
</gazebo>
</xacro:if>

<!-- Gazebo Sim (Ignition/Gz) plugin -->
<xacro:if value="$(arg sim_gazebo == 'sim')">
<gazebo>
<plugin filename="libgz_ros2_control-system.so" name="gz_ros2_control::GazeboSimROS2ControlPlugin">
<parameters>$(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml</parameters>
</plugin>
</gazebo>
</xacro:if>

</robot>
128 changes: 128 additions & 0 deletions hunter_gazebo/launch/launch_gz_sim.launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os

from launch_ros.actions import Node

from launch import LaunchDescription
from launch.event_handlers import OnProcessExit

from launch_ros.substitutions import FindPackageShare
from launch_ros.parameter_descriptions import ParameterValue

from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration
from launch.actions import IncludeLaunchDescription, RegisterEventHandler, DeclareLaunchArgument

from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
# Declare the use_sim_time argument
use_sim_time = DeclareLaunchArgument(
'use_sim_time',
default_value='true',
description='Use simulation time'
)

# Launch configuration for use_sim_time
use_sim_time_config = LaunchConfiguration('use_sim_time')

# Include the Gazebo Sim launch file
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('ros_gz_sim'),
'launch',
'gz_sim.launch.py'
])
]),
launch_arguments={'gz_args': '-r -v 4'}.items()
)

hunter_description_path = os.path.join(
get_package_share_directory('hunter_description'))

# Get URDF via xacro with sim_gazebo argument set to 'sim'
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("hunter_description"), "description", 'robot.urdf.xacro']
),
" ",
"sim_gazebo:=sim",
]
)
robot_description = {
"robot_description": ParameterValue(robot_description_content, value_type=str)
}

node_robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
output='screen',
parameters=[robot_description, {'use_sim_time': use_sim_time_config}]
)

# Spawn entity in Gazebo Sim
spawn_entity = Node(
package='ros_gz_sim',
executable='create',
arguments=['-topic', 'robot_description', '-name', 'hunter', '-allow_renaming', 'true'],
output='screen'
)

# Bridge for clock
bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
arguments=['/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock'],
output='screen'
)

# Load joint state broadcaster using spawner
load_joint_state_broadcaster = Node(
package='controller_manager',
executable='spawner',
arguments=['joint_state_broadcaster'],
output='screen'
)

# Load ackermann controller using spawner
load_ackermann_controller = Node(
package='controller_manager',
executable='spawner',
arguments=['ackermann_like_controller'],
output='screen'
)

rviz = Node(
package='rviz2',
executable='rviz2',
arguments=[
'-d',
os.path.join(hunter_description_path, 'rviz/robot_view.rviz'),
],
output='screen',
parameters=[{'use_sim_time': use_sim_time_config}]
)

return LaunchDescription([
use_sim_time,
RegisterEventHandler(
event_handler=OnProcessExit(
target_action=spawn_entity,
on_exit=[load_joint_state_broadcaster],
)
),
RegisterEventHandler(
event_handler=OnProcessExit(
target_action=load_joint_state_broadcaster,
on_exit=[load_ackermann_controller],
)
),
gazebo,
bridge,
rviz,
node_robot_state_publisher,
spawn_entity,
])
4 changes: 3 additions & 1 deletion hunter_gazebo/launch/launch_sim.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ def generate_launch_description():
hunter_description_path = os.path.join(
get_package_share_directory('hunter_description'))

# Get URDF via xacro
# Get URDF via xacro with sim_gazebo argument set to 'classic'
robot_description_content = Command(
[
PathJoinSubstitution([FindExecutable(name="xacro")]),
" ",
PathJoinSubstitution(
[FindPackageShare("hunter_description"), "description", 'robot.urdf.xacro']
),
" ",
"sim_gazebo:=classic",
]
)
robot_description = {
Expand Down
3 changes: 3 additions & 0 deletions hunter_gazebo/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<exec_depend>hunter_description</exec_depend>
<exec_depend>gazebo_ros</exec_depend>
<exec_depend>gazebo_ros2_control</exec_depend>
<exec_depend>ros_gz_sim</exec_depend>
<exec_depend>ros_gz_bridge</exec_depend>
<exec_depend>gz_ros2_control</exec_depend>
<exec_depend>controller_manager</exec_depend>
<exec_depend>teleop_twist_keyboard</exec_depend>

Expand Down