Repository navigation
ROS2 + Gazebo Example #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| .vscode/* | ||
| .vscode/* | ||
| .env | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| build/ | ||
| install/ | ||
| log/ | ||
| .git/ | ||
| .env | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Copy this file to .env and fill in a device token to enable Foxglove Remote Access. | ||
| # Create a device + token on the Devices page: https://app.foxglove.dev/~/devices | ||
| # Leave FOXGLOVE_DEVICE_TOKEN unset (or this file absent) to skip remote access and | ||
| # connect locally at ws://localhost:8765 instead -- see the README's "Remote access" section. | ||
| FOXGLOVE_DEVICE_TOKEN= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| cmake_minimum_required(VERSION 3.8) | ||
| project(fg_gazebo_example) | ||
|
|
||
| find_package(ament_cmake REQUIRED) | ||
| find_package(rclpy REQUIRED) | ||
|
|
||
| install(PROGRAMS | ||
| scripts/move_viewpoints.py | ||
| scripts/teleop_arm_bridge.py | ||
| DESTINATION lib/${PROJECT_NAME} | ||
| ) | ||
|
|
||
| install(DIRECTORY | ||
| launch | ||
| urdf | ||
| worlds | ||
| bridge | ||
| meshes | ||
| models | ||
| DESTINATION share/${PROJECT_NAME} | ||
| ) | ||
|
|
||
| if(BUILD_TESTING) | ||
| find_package(ament_lint_auto REQUIRED) | ||
| ament_lint_auto_find_test_dependencies() | ||
| endif() | ||
|
|
||
| ament_package() |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,174 @@ | ||||||
| --- | ||||||
| title: "ROS 2 Gazebo Simulation Demo" | ||||||
| short_description: "Code reference for a ROS 2 + Gazebo Sim simulation, runnable headless in Docker with Foxglove as the UI" | ||||||
| --- | ||||||
|
|
||||||
| # ROS 2 Gazebo simulation demo | ||||||
|
|
||||||
| A ROS 2 port of the [ROS 1 Gazebo simulation tutorial](../../ros1/gazebo/README.md). Gazebo Classic (used by | ||||||
| the original tutorial) is end-of-life, so this uses [Gazebo Sim](https://gazebosim.org/) ("Gazebo Harmonic") | ||||||
| via the [`ros_gz`](https://github.com/gazebosim/ros_gz) bridge packages, targeting **ROS 2 Jazzy Jalisco**. | ||||||
|
|
||||||
| This `ament_cmake` package contains launch, URDF/xacro, and world files for a simple simulated robot arm with | ||||||
| a wrist-mounted RGB-D camera and a separate world-fixed RGB camera, plus a Foxglove-branded box for the camera | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice the box contains an old logo. Can we see if it can upgrade to the latest one? |
||||||
| to look at. The arm sits on a table inside a warehouse "workcell" building (walls, storage racks, pallets, | ||||||
| a conveyor line, a control panel) reused from | ||||||
| [`warehouse_simulation_toolkit`](https://github.com/wh200720041/warehouse_simulation_toolkit) | ||||||
| (`models/workcell`, BSD-3 licensed -- see `models/workcell/LICENSE`); that toolkit's AGV robot, pedestrian | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove all this text:
And just mention:
|
||||||
| actors, and `workcell_bin` prop model weren't pulled in, since this tutorial's arm replaces the AGV and the | ||||||
| actors use a Gazebo-Classic-only collision plugin that doesn't exist in Gazebo Sim. It's set up to run | ||||||
| **headless** (no GUI, no GPU) so it can run inside Docker -- including on a Mac, where you'd otherwise have no | ||||||
| way to run Gazebo's GUI or access a GPU -- with the [Foxglove](https://foxglove.dev/) desktop app on your host | ||||||
| machine as the UI, connected over `foxglove_bridge`. | ||||||
|
|
||||||
| Differences from the ROS1 tutorial, and why: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove the differences. Almost noone cares about ROS1 anymore. Let's treat this tutorial as it's own thing |
||||||
|
|
||||||
| - **UR5e + MoveIt -> a small custom arm driven by Gazebo's `JointPositionController`.** The original UR5e/MoveIt | ||||||
| stack is UR-specific and Gazebo-Classic-era. Gazebo Sim has no `gazebo_ros_control` equivalent built in, and | ||||||
| pulling in `ros2_control` and MoveIt would add a lot of bulk to what's meant to be a Gazebo/Foxglove tutorial. | ||||||
| Instead, each joint is driven directly via a `JointPositionController` system plugin, commanded over a topic -- | ||||||
| see `urdf/robot.xacro`. | ||||||
| - **`libgazebo_ros_openni_kinect.so` / `libgazebo_ros_camera.so` -> native Gazebo Sim sensors.** Gazebo Sim | ||||||
| simulates camera/RGB-D sensors natively and publishes on gz-transport topics; `ros_gz_bridge` (see | ||||||
| `bridge/gazebo_bridge.yaml`) bridges those to ROS 2 topics instead of a Gazebo-Classic ROS plugin doing it. | ||||||
| - **`roslaunch` XML -> Python launch files.** | ||||||
|
|
||||||
| ## Building this package | ||||||
|
|
||||||
| You'll need a [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/Installation.html) installation with | ||||||
| [Gazebo Harmonic / `ros_gz`](https://gazebosim.org/docs/harmonic/ros_installation) (`ros-jazzy-ros-gz`), plus | ||||||
| `robot_state_publisher`, `joint_state_publisher_gui`, `rviz2`, `xacro`, and `foxglove_bridge`. | ||||||
|
|
||||||
| 1. Create a [colcon workspace](https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html) | ||||||
| 2. Clone this repository into the workspace's `src` folder | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe let's ask to only copy the ros2/gazebo directory for a cleaner workspace? |
||||||
| 3. Build the package with `colcon build --packages-select fg_gazebo_example` | ||||||
| 4. Source the workspace | ||||||
|
|
||||||
| ## Running the simulation | ||||||
|
|
||||||
| ```sh | ||||||
| ros2 launch fg_gazebo_example simulation.launch.py | ||||||
| ``` | ||||||
|
|
||||||
| This starts Gazebo Sim server-only with headless rendering (no GUI window), spawns the arm, the static camera, | ||||||
| and the Foxglove box, starts the `ros_gz_bridge` topic bridge, `foxglove_bridge` on port 8765, and the teleop | ||||||
| bridge node (see below). Connect the Foxglove app to `ws://localhost:8765` to see the two camera feeds, the | ||||||
| point cloud, and TF. The arm starts in a bent, non-zero rest pose (see `urdf/robot.xacro`) so it reads as an | ||||||
| arm even before you move it, rather than as a straight pole. | ||||||
|
|
||||||
| ### Moving the arm | ||||||
|
|
||||||
| There are three ways, from easiest (inside Foxglove, no terminal) to most direct: | ||||||
|
|
||||||
| **1. Jog it with a Teleop panel.** The [included layout](foxglove_layouts/Gazebo_Tutorial.json) has two Teleop | ||||||
| panels wired up: "Shoulder" (up/down = shoulder tilt, left/right = shoulder pan) and "Wrist" (up/down = wrist | ||||||
| tilt). Holding a button publishes `geometry_msgs/Twist` to `/foxglove_arm/teleop/shoulder` or | ||||||
| `/foxglove_arm/teleop/wrist`; `scripts/teleop_arm_bridge.py` (launched automatically by `simulation.launch.py`) | ||||||
| converts that into an incremental joint-angle target on the matching `cmd_pos` topic, clamped to the joint's | ||||||
| limits, and stops moving as soon as you release the button. This is a bridge rather than a direct connection | ||||||
| because the Teleop panel always publishes Twist-shaped messages, and the arm's joints expect an absolute | ||||||
| `std_msgs/Float64` angle -- see the comment at the top of `teleop_arm_bridge.py` for the exact field mapping. | ||||||
|
|
||||||
| **2. Sweep through a few preset viewpoints** (replaces the original's MoveIt-driven `move_viewpoints.py`): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ```sh | ||||||
| ros2 run fg_gazebo_example move_viewpoints.py | ||||||
| ``` | ||||||
|
|
||||||
| **3. Command a single joint directly** to an exact angle -- each joint has its own `cmd_pos` topic | ||||||
| (`std_msgs/msg/Float64`, radians): | ||||||
|
|
||||||
| ```sh | ||||||
| ros2 topic pub /foxglove_arm/shoulder_pan_joint/cmd_pos std_msgs/msg/Float64 "{data: 0.8}" # yaw, limit ±3.14 | ||||||
| ros2 topic pub /foxglove_arm/shoulder_tilt_joint/cmd_pos std_msgs/msg/Float64 "{data: -0.5}" # pitch, limit ±1.57 | ||||||
| ros2 topic pub /foxglove_arm/wrist_tilt_joint/cmd_pos std_msgs/msg/Float64 "{data: 0.3}" # pitch, limit ±1.9 | ||||||
| ``` | ||||||
|
|
||||||
| You can also do this from a Foxglove **Publish** panel with type `std_msgs/msg/Float64` if you want an exact | ||||||
| value rather than jogging -- it isn't in the default layout, but is easy to add. | ||||||
|
|
||||||
| Only use one of these at a time per joint -- the Teleop bridge, `move_viewpoints.py`, and manual `topic pub` all | ||||||
| write to the same `cmd_pos` topics and will fight each other if run together. | ||||||
|
|
||||||
| To preview just the arm's URDF in RViz without Gazebo (native, requires a display): | ||||||
|
|
||||||
| ```sh | ||||||
| ros2 launch fg_gazebo_example view_robot.launch.py | ||||||
| ``` | ||||||
|
|
||||||
| ## Running headless in Docker (e.g. on a Mac) | ||||||
|
|
||||||
| Docker Desktop on a Mac has no GPU passthrough and no display, which is exactly what `simulation.launch.py` | ||||||
| is set up for: Gazebo runs server-only (`-s`) with `--headless-rendering`, using Mesa's software (`llvmpipe`) | ||||||
| OpenGL renderer instead of a GPU. Jazzy has `arm64` packages, so this also runs natively on Apple Silicon -- | ||||||
| no emulation needed. | ||||||
|
|
||||||
| 1. Build and start the container: | ||||||
|
|
||||||
| ```sh | ||||||
| docker compose up --build | ||||||
| ``` | ||||||
|
|
||||||
| 2. On your Mac, open the [Foxglove desktop app](https://foxglove.dev/download), choose **Open connection** -> | ||||||
| **Foxglove WebSocket**, and connect to `ws://localhost:8765`. Docker Desktop forwards the container's | ||||||
| published `8765` port to `localhost` on the Mac automatically, so no `host.docker.internal` or extra | ||||||
| networking is needed. | ||||||
| 3. Load the included layout ([`foxglove_layouts/Gazebo_Tutorial.json`](foxglove_layouts/Gazebo_Tutorial.json)): | ||||||
| in Foxglove, go to the **Layouts** sidebar -> **Import from file** -> select that file. It sets up a 3D panel | ||||||
| (TF + the URDF + the `/wrist_camera/points` point cloud), an Image panel each for `/wrist_camera/image` and | ||||||
| `/static_camera/image`, a Raw Messages panel for `/joint_states`, and two Teleop panels for jogging the arm | ||||||
| (see [Moving the arm](#moving-the-arm) above). You can also just add those panels manually if you'd rather | ||||||
| build your own layout. | ||||||
| 4. Move the arm straight from the layout's Teleop panels -- no container shell needed, since Foxglove is already | ||||||
| connected to `foxglove_bridge`. If you'd rather run `move_viewpoints.py` or a manual `topic pub` instead, open | ||||||
| a second shell into the running container: | ||||||
|
|
||||||
| ```sh | ||||||
| docker compose exec gazebo bash -lc \ | ||||||
| "source /opt/ros/jazzy/setup.bash && source /ros2_ws/install/setup.bash && ros2 run fg_gazebo_example move_viewpoints.py" | ||||||
| ``` | ||||||
|
|
||||||
| Software rendering is noticeably slower than a GPU, so expect the camera topics to publish at a low framerate -- | ||||||
| that's expected, not a bug. | ||||||
|
|
||||||
| ### Remote access | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an awesome idea to include it. Nice! |
||||||
|
|
||||||
| Instead of connecting to `ws://localhost:8765` yourself, [Foxglove Remote Access](https://docs.foxglove.dev/docs/visualization/connecting/live/remote-access) | ||||||
| lets you (or anyone in your Foxglove org) open the simulation from the **Devices** page at | ||||||
| [app.foxglove.dev](https://app.foxglove.dev/~/devices) and click **Connect** -- no port forwarding, and it works | ||||||
| even if the container is on a network you can't reach directly. `foxglove_bridge` here is built from source with | ||||||
| this enabled (see `docker/Dockerfile`), rather than installed from apt, since the apt package doesn't include it. | ||||||
|
|
||||||
| 1. On the [Devices page](https://app.foxglove.dev/~/devices), create a device and a device token for it. | ||||||
| 2. Export the token in the shell you run `docker compose` from, or copy [`.env.example`](.env.example) to | ||||||
| `.env` next to `docker-compose.yml` and fill it in there instead (`.env` is gitignored, so it's never | ||||||
| committed): | ||||||
|
|
||||||
| ```sh | ||||||
| export FOXGLOVE_DEVICE_TOKEN=fox_dt-... | ||||||
| docker compose up --build | ||||||
| ``` | ||||||
|
|
||||||
| `simulation.launch.py` checks for `FOXGLOVE_DEVICE_TOKEN` at launch and turns remote access on automatically | ||||||
| when it's set -- there's no separate flag to pass. Leave it unset and everything behaves exactly as in the | ||||||
| local-only steps above. | ||||||
| 3. The device should appear on the Devices page shortly after the container starts; click **Connect** to open it. | ||||||
|
|
||||||
| The local `ws://localhost:8765` connection keeps working the same way whether or not remote access is enabled, so | ||||||
| you can use either one interchangeably. | ||||||
|
|
||||||
| ### Troubleshooting | ||||||
|
|
||||||
| - **No image on the camera topics / black frames:** confirm the `Sensors` system plugin loaded with | ||||||
| `render_engine: ogre2` (see `worlds/foxglove_demo.sdf`) and that `LIBGL_ALWAYS_SOFTWARE=1` is set (it's baked | ||||||
| into the image and set again in `docker-compose.yml`). | ||||||
| - **`/joint_states` is empty, or `/foxglove_arm/<joint>/cmd_pos` doesn't move the arm:** Gazebo Sim's model/world | ||||||
| topic-scoping determines the exact gz-transport topic names for the `JointPositionController` and | ||||||
| `JointStatePublisher` plugins in `urdf/robot.xacro`. Run `gz topic -l` inside the container while the sim is | ||||||
| running (`docker compose exec gazebo gz topic -l`) and update `bridge/gazebo_bridge.yaml` if the names differ | ||||||
| from what's there. | ||||||
| - **`/wrist_camera/points` looks rotated relative to the wrist images:** the point cloud is stamped with | ||||||
| `wrist_camera_link` (Gazebo's body frame, +X forward). `/wrist_camera/image`, `/wrist_camera/depth_image`, and | ||||||
| `/wrist_camera/camera_info` stay on `wrist_camera_optical_frame`. | ||||||
| - **Slow first `docker compose up`:** the image installs Gazebo Harmonic and builds the workspace on first build; | ||||||
| subsequent builds/starts are much faster. | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # Bridges topics between Gazebo Sim (gz-transport) and ROS 2, consumed by | ||
| # ros_gz_bridge's `parameter_bridge` node. | ||
| # Schema reference: https://github.com/gazebosim/ros_gz/blob/jazzy/ros_gz_bridge/README.md | ||
| # | ||
| # All gz_topic_name values below were confirmed against a running container with | ||
| # `gz topic -l` / `gz topic -i -t <name>`. Notably, JointPositionController's | ||
| # <topic> tag (urdf/robot.xacro) is used LITERALLY, with no /model/<name>/ auto-scoping | ||
| # applied -- so the cmd_pos topics are bare (e.g. /shoulder_pan_joint/cmd_pos), not | ||
| # under /model/foxglove_arm/. JointStatePublisher (no <topic> override) does get the | ||
| # default scoped name, /world/foxglove_demo/model/foxglove_arm/joint_state. | ||
|
|
||
| - ros_topic_name: "/clock" | ||
| gz_topic_name: "/clock" | ||
| ros_type_name: "rosgraph_msgs/msg/Clock" | ||
| gz_type_name: "gz.msgs.Clock" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| - ros_topic_name: "/joint_states" | ||
| gz_topic_name: "/world/foxglove_demo/model/foxglove_arm/joint_state" | ||
| ros_type_name: "sensor_msgs/msg/JointState" | ||
| gz_type_name: "gz.msgs.Model" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| - ros_topic_name: "/foxglove_arm/shoulder_pan_joint/cmd_pos" | ||
| gz_topic_name: "/shoulder_pan_joint/cmd_pos" | ||
| ros_type_name: "std_msgs/msg/Float64" | ||
| gz_type_name: "gz.msgs.Double" | ||
| direction: ROS_TO_GZ | ||
|
|
||
| - ros_topic_name: "/foxglove_arm/shoulder_tilt_joint/cmd_pos" | ||
| gz_topic_name: "/shoulder_tilt_joint/cmd_pos" | ||
| ros_type_name: "std_msgs/msg/Float64" | ||
| gz_type_name: "gz.msgs.Double" | ||
| direction: ROS_TO_GZ | ||
|
|
||
| - ros_topic_name: "/foxglove_arm/wrist_tilt_joint/cmd_pos" | ||
| gz_topic_name: "/wrist_tilt_joint/cmd_pos" | ||
| ros_type_name: "std_msgs/msg/Float64" | ||
| gz_type_name: "gz.msgs.Double" | ||
| direction: ROS_TO_GZ | ||
|
|
||
| # Wrist-mounted RGB-D camera (replaces the ROS1 tutorial's Kinect) | ||
| - ros_topic_name: "/wrist_camera/image" | ||
| gz_topic_name: "/wrist_camera/image" | ||
| ros_type_name: "sensor_msgs/msg/Image" | ||
| gz_type_name: "gz.msgs.Image" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| - ros_topic_name: "/wrist_camera/depth_image" | ||
| gz_topic_name: "/wrist_camera/depth_image" | ||
| ros_type_name: "sensor_msgs/msg/Image" | ||
| gz_type_name: "gz.msgs.Image" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| - ros_topic_name: "/wrist_camera/points" | ||
| gz_topic_name: "/wrist_camera/points" | ||
| ros_type_name: "sensor_msgs/msg/PointCloud2" | ||
| gz_type_name: "gz.msgs.PointCloudPacked" | ||
| direction: GZ_TO_ROS | ||
| # Gazebo RGB-D XYZ is body-frame (+X forward), not ROS optical (+Z forward). | ||
| # Images/camera_info stay on wrist_camera_optical_frame. | ||
| frame_id: "wrist_camera_link" | ||
|
|
||
| - ros_topic_name: "/wrist_camera/camera_info" | ||
| gz_topic_name: "/wrist_camera/camera_info" | ||
| ros_type_name: "sensor_msgs/msg/CameraInfo" | ||
| gz_type_name: "gz.msgs.CameraInfo" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| # World-fixed static RGB camera. | ||
| # Unlike rgbd_camera (used for wrist_camera above), a plain `type="camera"` sensor in | ||
| # gz-sim does NOT scope its sub-topics under its <topic> tag the same way: the image | ||
| # publishes on the bare <topic> value itself (no "/image" suffix), and camera_info | ||
| # publishes on a bare, unscoped /camera_info -- not <topic>/camera_info. Confirmed live | ||
| # via `gz topic -i -t <name>`. This only works cleanly because there's a single plain | ||
| # `camera` sensor in this world; a second one would collide on /camera_info. | ||
| - ros_topic_name: "/static_camera/image" | ||
| gz_topic_name: "/static_camera" | ||
| ros_type_name: "sensor_msgs/msg/Image" | ||
| gz_type_name: "gz.msgs.Image" | ||
| direction: GZ_TO_ROS | ||
|
|
||
| - ros_topic_name: "/static_camera/camera_info" | ||
| gz_topic_name: "/camera_info" | ||
| ros_type_name: "sensor_msgs/msg/CameraInfo" | ||
| gz_type_name: "gz.msgs.CameraInfo" | ||
| direction: GZ_TO_ROS |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| services: | ||
| gazebo: | ||
| build: | ||
| context: . | ||
| dockerfile: docker/Dockerfile | ||
| ports: | ||
| # foxglove_bridge websocket -- Docker Desktop forwards this to localhost | ||
| # on the Mac host automatically, no host.docker.internal needed. | ||
| - "8765:8765" | ||
| environment: | ||
| - LIBGL_ALWAYS_SOFTWARE=1 | ||
| # Set this in your shell (or a .env file next to this docker-compose.yml) to turn on | ||
| # Foxglove Remote Access -- see the "Remote access" section of the README. Left unset, | ||
| # this is a no-op and the bridge behaves exactly as before. | ||
| - FOXGLOVE_DEVICE_TOKEN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this paragraph. No need to reference the old stuff, let's just say that it targets Gazebo Harmonic and Jazzy Jalisco
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We mention System dependencies lower down, so we can skip mentioning the ROS2 and Gazebo versions here