diff --git a/examples/10_tugbot/README.md b/examples/10_tugbot/README.md new file mode 100644 index 0000000..a1ffc58 --- /dev/null +++ b/examples/10_tugbot/README.md @@ -0,0 +1,133 @@ +# Example 10: Tugbot Warehouse Simulation (Ignition Gazebo) + +This example demonstrates how to control a **Tugbot mobile robot** inside a warehouse environment using **Ignition Gazebo (Fortress)** and the **ROS-MCP Server**. + + + +Using natural language and the `ROS-MCP server`, you can control and navigate the robot in the simulation, inspect its sensors (Lidar), and check its position. + +## Working Demo +![Demo](images/output.gif) + + +## 📋 Tested On + +This example has been tested and verified on: + + * **OS:** Ubuntu 22.04 LTS + * **ROS Distro:** ROS 2 Humble + * **Simulator:** Ignition Gazebo Fortress + * **Python Manager:** `uv` + +> **Note:** We recommend using a Linux-based OS or a VM, as Gazebo compatibility may vary on other operating systems. + +## 🛠️ Prerequisites + +Before running this example, ensure you have the necessary ROS 2 and Gazebo packages installed on your system: + +```bash +sudo apt update +sudo apt install ros-humble-ros-gz # Bridge between ROS 2 and Ignition +sudo apt install ros-humble-rosapi # Required for introspection (listing topics) +sudo apt install ros-humble-rosbridge-server # WebSocket connection for MCP +``` + +## 📦 Installation & Setup + +This project uses `uv` for environment management. Because ROS nodes (like `rosbridge` and `rosapi`) run inside this virtual environment, we must install specific system bindings into the virtual environment. + +1. **Create and Activate Virtual Environment:** + Navigate to the root of the repository and run: + + ```bash + uv venv + source .venv/bin/activate + ``` + +2. **Install Dependencies:** + We have defined a special `bridge` dependency group that handles the ROS-Python bindings (fixing common `ModuleNotFoundError: bson/tornado` errors). + + ```bash + # Install the package plus the bridge requirements + uv pip install -e .[bridge] + ``` + +## 🚀 How to Run + +### 1\. Launch the Simulation & Bridges + +We have provided a custom launch file (`tugbot_sim.launch.py`) that starts Ignition Gazebo, the ROS-GZ Bridge, the Rosbridge Websocket, and the ROS API node simultaneously. + +```bash +source /opt/ros/humble/setup.bash +source .venv/bin/activate + +# Launch the simulation +ros2 launch examples/11_tugbot/tugbot_sim.launch.py +``` + +*Wait until you see the warehouse environment and the robot appear in the simulation window.* + +### 2\. Start the MCP Server + +Use the [robot-mcp-client](https://github.com/robotmcp/robot-mcp-client) or any of the MCP Desktop clients (Claude Desktop/Goose/etc). + +## 🤖 Sample Prompts & Use Cases + +Once connected, the AI has full access to the robot's navigation and sensor data. Here are prompts that are tested and working: + +### 1\. Navigation (Movement) + +The robot uses a differential drive controller listening on `/cmd_vel`. + +> "Make the robot go in a circle" + +> "Move the robot forward at 0.5 m/s." + +> "Turn the robot 90 degrees to the left." + +> "Stop the robot immediately." + +> "Drive forward for 3 seconds, then stop." + +### 2\. Discovery + +The AI can query the system to understand what tools are available. + +> "List all available topics." + +> "Check the active nodes and tell me if the simulation bridge is running." + +> "What kind of message type does the /cmd\_vel topic expect?" + +### 3\. Perception & State (Sensors) + +The Tugbot is equipped with a Lidar and Odometry sensors. + +> "What is the robot's current position (check odometry)?" + +> "Read the /scan topic and tell me if there are obstacles nearby." + +> "Monitor the robot's velocity." + +## ⚠️ Troubleshooting + +**Issue: "Service /rosapi/topics does not exist"** + + * **Cause:** The `rosapi` node is not running or crashed. + * **Fix:** Ensure you installed the dependencies using `uv pip install -e .[bridge]`. The `bridge` extras install `netifaces`, which `rosapi` requires to start. + +**Issue: Robot doesn't move when commanded** + + * **Cause:** Topic mismatch between Ignition and ROS. + * **Fix:** Verify the bridge mapping in the launch file. It should map `/model/tugbot/cmd_vel` (Ignition) to `/cmd_vel` (ROS). + +**Issue: "ModuleNotFoundError: No module named 'bson' or 'tornado'"** + + * **Cause:** Missing Python bindings in the `uv` environment. + * **Fix:** Run `uv pip install pymongo tornado` (or reinstall with the `.[bridge]` flag). + +## 📂 File Structure + + * `tugbot_sim.launch.py`: The main entry point. Orchestrates Gazebo, ROS Bridge, and MCP connection. + * `tugbot_depot.sdf`: The 3D environment file (downloaded from Gazebo Fuel). diff --git a/examples/10_tugbot/images/output.gif b/examples/10_tugbot/images/output.gif new file mode 100644 index 0000000..6b59711 Binary files /dev/null and b/examples/10_tugbot/images/output.gif differ diff --git a/examples/10_tugbot/images/sim.png b/examples/10_tugbot/images/sim.png new file mode 100644 index 0000000..0075a6c Binary files /dev/null and b/examples/10_tugbot/images/sim.png differ diff --git a/examples/10_tugbot/tugbot_depot.sdf b/examples/10_tugbot/tugbot_depot.sdf new file mode 100644 index 0000000..1f56870 --- /dev/null +++ b/examples/10_tugbot/tugbot_depot.sdf @@ -0,0 +1,405 @@ + + + + + 0.01 + 1 + + + + + + + ogre2 + + + 1 1 1 1 + 0.3 0.7 0.9 1 + 0 + 1 + + + + 1 + + + + + 0 0 1 + 1 1 + + + + + + + + + + + + 0 0 0 0 0 0 + + + + + https://fuel.ignitionrobotics.org/1.0/OpenRobotics/models/Depot + + 7.19009 1.09982 0 0 0 0 + + + + 1 + 7.19009 1.09982 0 0 0 0 + + 0 0 0 0 0 0 + + 0 -7.6129 4.5 0 0 0 + + + 30.167 0.08 9 + + + + + 0 7.2875 4.5 0 0 0 + + + 30.167 0.08 9 + + + + + -15 0 4.5 0 0 0 + + + 0.08 15.360 9 + + + + + 15 0 4.5 0 0 0 + + + 0.08 15.360 9 + + + + + 0.22268 -4.7268 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 3.1727 -4.7268 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 5.95268 -4.7268 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 8.55887 -4.7268 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 11.326 -4.7268 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 0.22268 -2.37448 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 3.1727 -2.37448 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 5.95268 -2.37448 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 8.55887 -2.37448 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + 11.326 -2.37448 0.68506 0 0 0 + + + 1.288 1.422 1.288 + + + + + -1.2268 4.1557 0.68506 0 0 -1.02799893 + + + 1.288 1.422 1.288 + + + + + -7.5402 3.6151 1 0 0 0 + + + 0.465 0.465 2 + + + + + 7.4575 3.6151 1 0 0 0 + + + 0.465 0.465 2 + + + + + -7.5402 -3.8857 1 0 0 0 + + + 0.465 0.465 2 + + + + + 7.4575 -3.8857 1 0 0 0 + + + 0.465 0.465 2 + + + + + -0.6144 -2.389 0.41838 0 0 0 + + + 0.363 0.440 0.719 + + + + + -1.6004 4.8225 0.41838 0 0 0 + + + 0.363 0.244 0.719 + + + + + 13.018 3.1652 0.25 0 0 0 + + + 1.299 0.6 0.5 + + + + + 1.4662 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 2.6483 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 5.3247 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 6.5063 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 9.0758 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 10.258 -0.017559 0.5 0 0 0 + + + 0.03 + 1 + + + + + 1.4662 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 2.6483 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 5.3247 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 6.5063 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 9.0758 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 10.258 2.5664 0.5 0 0 0 + + + 0.03 + 1 + + + + + 1.4662 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + 2.6483 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + 5.3247 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + 6.5063 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + 9.0758 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + 10.258 5.1497 0.5 0 0 0 + + + 0.03 + 1 + + + + + + + + + https://fuel.ignitionrobotics.org/1.0/MovAi/models/Tugbot + + tugbot + 2.75268 1.0014 0.132279 0 0 0 + + + diff --git a/examples/10_tugbot/tugbot_sim.launch.py b/examples/10_tugbot/tugbot_sim.launch.py new file mode 100644 index 0000000..944c35c --- /dev/null +++ b/examples/10_tugbot/tugbot_sim.launch.py @@ -0,0 +1,63 @@ +from launch.actions import DeclareLaunchArgument, ExecuteProcess +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + +from launch import LaunchDescription + + +def generate_launch_description(): + """Generate the launch description for Gazebo, Bridge, Rosbridge AND Rosapi.""" + + # 1. Define the World Source + world_file = "tugbot_depot.sdf" + + # 2. Start Ignition Gazebo Server + gazebo_sim = ExecuteProcess(cmd=["ign", "gazebo", "-r", world_file], output="screen") + + # 3. The Bridge + bridge = Node( + package="ros_gz_bridge", + executable="parameter_bridge", + arguments=[ + "/model/tugbot/cmd_vel@geometry_msgs/msg/Twist]ignition.msgs.Twist", + "/model/tugbot/odometry@nav_msgs/msg/Odometry[ignition.msgs.Odometry", + "/scan@sensor_msgs/msg/LaserScan[ignition.msgs.LaserScan", + "/model/tugbot/tf@tf2_msgs/msg/TFMessage[ignition.msgs.Pose_V", + "/clock@rosgraph_msgs/msg/Clock[ignition.msgs.Clock", + ], + remappings=[ + ("/model/tugbot/cmd_vel", "/cmd_vel"), + ("/model/tugbot/odometry", "/odom"), + ("/model/tugbot/tf", "/tf"), + ], + output="screen", + ) + + # 4. Rosbridge Arguments + port_arg = DeclareLaunchArgument( + "port", default_value="9090", description="Port for rosbridge websocket server" + ) + + # 5. Rosbridge Node (The Connection) + rosbridge_node = Node( + package="rosbridge_server", + executable="rosbridge_websocket", + name="rosbridge_websocket", + output="screen", + parameters=[{"port": LaunchConfiguration("port")}], + ) + + # 6. ROSAPI Node (The Introspection - MISSING PART) + # This node allows the MCP server to ask "What topics exist?" + rosapi_node = Node(package="rosapi", executable="rosapi_node", name="rosapi", output="screen") + + # 7. FINAL RETURN + return LaunchDescription( + [ + port_arg, + gazebo_sim, + bridge, + rosbridge_node, + rosapi_node, + ] + ) diff --git a/pyproject.toml b/pyproject.toml index c990680..bfb5e16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,12 @@ dev = [ "pytest" # for tests ] +bridge = [ + "pymongo", # Required by rosbridge for BSON + "tornado", # Required by rosbridge web server + "netifaces", # Required by rosapi for network discovery +] + [project.scripts] ros-mcp = "server:main"