Skip to content

Commit

Permalink
Adding ROS Bridge (#515)
Browse files Browse the repository at this point in the history
* update to newest metadrive

* rename package

* update

* update rviz

* update readme

* update package name

* update package name

* reformat use indent=4space

* update readme

* fix dependency and install instruction

* update readme

* test is successful with ros humble

* add test

* fix test

* remove source

* remove source line

* add python dependency

* clean the code

* clean the code; run isort and yapf

* add python dependency

* remove container

* debug

* remove pip install -e .for test

* use container

* use container

* install gym outside the pip

* stop using container

* remove init

* fix init

* no update and init

* source

* source

* use sudo rosdep

* disable plugin

* fix bug server

* restore vision plugins

* remove line

* use default python

* test!

* bypass doc string check when merging to main

---------

Co-authored-by: QuanyiLi <quanyili0057@gmail.com>
  • Loading branch information
BoSmallEar and QuanyiLi committed Oct 20, 2023
1 parent c4d926e commit 486f190
Show file tree
Hide file tree
Showing 17 changed files with 859 additions and 1 deletion.
42 changes: 42 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
bash format.sh --test
code_docstring:
if: github.event_name != 'pull_request' || github.head_ref != 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -200,6 +201,47 @@ jobs:
cp ./examples/Basic_MetaDrive_Usages.ipynb ./tests/test_ipynb/
pytest --nbmake -n=auto ./tests/test_ipynb/
test_ros:
runs-on: ubuntu-22.04
# container:
# image: ubuntu:jammy
steps:
- name: Set up ROS2 humble
uses: ros-tooling/setup-ros@v0.7
with:
required-ros-distributions: humble
- name: checkout code
uses: actions/checkout@v2
- name: Blackbox tests
run: |
ls -al
pwd
source /opt/ros/humble/setup.bash
sudo rosdep update
pwd
pip install pyzmq
pip install gymnasium==0.28
ls -al
pip install -e .
python -m metadrive.pull_asset
cd bridges/ros_bridge
ls -al
python _delete_rviz_line.py
sudo rosdep install --from-paths src --ignore-src -y --rosdistro humble
pwd
colcon build
source install/setup.bash
pip install pyzmq
nohup ros2 launch metadrive_example_bridge metadrive_example_bridge.launch.py > ros.txt 2>&1 &
python ros_socket_server.py --test
# python ros_socket_server.py --test

# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1
# with:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@
**/test_read_copy_waymo
/metadrive/tests/vis_functionality/*.png
**/assets/

/bridges/ros_bridge/build
/bridges/ros_bridge/install
/bridges/ros_bridge/log

43 changes: 43 additions & 0 deletions bridges/ros_bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Connecting MetaDrive Simulator with ROS2

We provide an example bridge for connecting MetaDrive and ROS2 using zmq sockets, which publishes messages for camera
images, LiDAR point clouds, and object 3D bounding boxes.

## Installation

To install the bridge, first [install ROS2 **humble**](https://docs.ros.org/en/humble/Installation.html) and follow the scripts
below:

```bash
cd bridges/ros_bridge

# activate env, ${ROS_DISTRO} is something like foxy, iron, humble
source /opt/ros/${ROS_DISTRO}/setup.bash
# You may need to run init, if you are installing ros2 for the first time
sudo rosdep init
# zmq should be installed with system python interpreter
pip install pyzmq
rosdep update
rosdep install --from-paths src --ignore-src -y

# build
colcon build
source install/setup.bash
```

## Usage

```bash
# Terminal 1, launch ROS publishers
ros2 launch metadrive_example_bridge metadrive_example_bridge.launch.py
# Terminal 2, launch socket server
python ros_socket_server.py

```

[Demo Video](https://www.youtube.com/watch?v=WWwdnURnOBM&list=TLGGdRGbC4RGzhAxNzEwMjAyMw)


## Known Issues
If you are using the `conda`, it is very likely that the interpreter will not match the system interpreter
and will be incompatible with ROS 2 binaries.
28 changes: 28 additions & 0 deletions bridges/ros_bridge/_delete_rviz_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Only for github CI using! The visualization tool is not available on github server, remove it.
"""
import os


def delete_line_from_file(file_name, line_number):
# Check if the line number is valid
if line_number < 1:
return

lines = []
with open(file_name, 'r') as f:
lines = f.readlines()

# Check if the line number is beyond the file's length
if line_number > len(lines):
return

with open(file_name, 'w') as f:
for idx, line in enumerate(lines):
if idx + 1 != line_number:
f.write(line)


# Usage
if __name__ == '__main__':
delete_line_from_file(os.path.join(os.path.dirname(__file__), 'src', "metadrive_example_bridge", 'package.xml'), 10)
169 changes: 169 additions & 0 deletions bridges/ros_bridge/ros_socket_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# launch sockets to send sensor readings to ROS
import argparse
import struct

import numpy as np
import zmq

from metadrive.constants import HELP_MESSAGE
from metadrive.engine.asset_loader import AssetLoader
from metadrive.envs.scenario_env import ScenarioEnv
from metadrive.policy.replay_policy import ReplayEgoCarPolicy


class RosSocketServer():

def __init__(self):
self.context = zmq.Context().instance()
self.context.setsockopt(zmq.IO_THREADS, 2)
self.img_socket = self.context.socket(zmq.PUSH)
self.img_socket.setsockopt(zmq.SNDBUF, 4194304)
self.img_socket.bind("ipc:///tmp/rgb_camera")
self.img_socket.set_hwm(5)
self.lidar_socket = self.context.socket(zmq.PUSH)
self.lidar_socket.setsockopt(zmq.SNDBUF, 4194304)
self.lidar_socket.bind("ipc:///tmp/lidar")
self.lidar_socket.set_hwm(5)
self.obj_socket = self.context.socket(zmq.PUSH)
self.obj_socket.setsockopt(zmq.SNDBUF, 4194304)
self.obj_socket.bind("ipc:///tmp/obj")
self.obj_socket.set_hwm(5)

def run(self, test=False):
config = dict(
use_render=True if not test else False, # need this on to get the camera
num_scenarios=1,
horizon=1000,
image_observation=True if not test else False,
manual_control=False,
agent_policy=ReplayEgoCarPolicy,
rgb_clip=False,
show_logo=False,
show_fps=False,
show_interface=False,
physics_world_step_size=0.02,
vehicle_config=dict(
image_source="main_camera",
show_navi_mark=False,
),
data_directory=AssetLoader.file_path("nuscenes", return_raw_style=False),
)

env = ScenarioEnv(config)

try:

env.reset()
print(HELP_MESSAGE)
env.vehicle.expert_takeover = False
while True:
o = env.step([0, 0])
if test:
image_data = np.zeros((512, 512, 3)) # fake data for testing
image_data[::16, :, :] = 255
else:
image_data = o[0]['image'][..., -1]
# send via socket
image_data = image_data.astype(np.uint8)
dim_data = struct.pack('ii', image_data.shape[1], image_data.shape[0])
image_data = bytearray(image_data)
# concatenate the dimensions and image data into a single byte array
image_data = dim_data + image_data
try:
self.img_socket.send(image_data, zmq.NOBLOCK)
except zmq.error.Again:
msg = "ros_socket_server: error sending image"
if test:
raise ValueError(msg)
else:
print(msg)
del image_data # explicit delete to free memory

lidar_data, objs = env.vehicle.lidar.perceive(
env.vehicle,
env.engine.physics_world.dynamic_world,
env.vehicle.config["lidar"]["num_lasers"],
env.vehicle.config["lidar"]["distance"],
height=1.0,
)

ego_x = env.vehicle.position[0]
ego_y = env.vehicle.position[1]
ego_theta = np.arctan2(env.vehicle.heading[1], env.vehicle.heading[0])

num_data = struct.pack('i', len(objs))
obj_data = []
for obj in objs:
obj_x = obj.position[0]
obj_y = obj.position[1]
obj_theta = np.arctan2(obj.heading[1], obj.heading[0])

obj_x = obj_x - ego_x
obj_y = obj_y - ego_y
obj_x_new = np.cos(-ego_theta) * obj_x - np.sin(-ego_theta) * obj_y
obj_y_new = np.sin(-ego_theta) * obj_x + np.cos(-ego_theta) * obj_y

obj_data.append(obj_x_new)
obj_data.append(obj_y_new)
obj_data.append(obj_theta - ego_theta)
obj_data.append(obj.LENGTH)
obj_data.append(obj.WIDTH)
obj_data.append(obj.HEIGHT)
obj_data = np.array(obj_data, dtype=np.float32)
obj_data = bytearray(obj_data)
obj_data = num_data + obj_data
try:
self.obj_socket.send(obj_data, zmq.NOBLOCK)
except zmq.error.Again:
msg = "ros_socket_server: error sending objs"
if test:
raise ValueError(msg)
else:
print(msg)
del obj_data # explicit delete to free memory

# convert lidar data to xyz
lidar_data = np.array(lidar_data) * env.vehicle.config["lidar"]["distance"]
lidar_range = env.vehicle.lidar._get_lidar_range(
env.vehicle.config["lidar"]["num_lasers"], env.vehicle.lidar.start_phase_offset
)
point_x = lidar_data * np.cos(lidar_range)
point_y = lidar_data * np.sin(lidar_range)
point_z = np.ones(lidar_data.shape) # assume height = 1.0
lidar_data = np.stack([point_x, point_y, point_z], axis=-1).astype(np.float32)
dim_data = struct.pack('i', len(lidar_data))
lidar_data = bytearray(lidar_data)
# concatenate the dimensions and lidar data into a single byte array
lidar_data = dim_data + lidar_data
try:
self.lidar_socket.send(lidar_data, zmq.NOBLOCK)
except zmq.error.Again:
msg = "ros_socket_server: error sending lidar"
if test:
raise ValueError(msg)
else:
print(msg)
del lidar_data # explicit delete to free memory

if o[2]: # done
break

except Exception as e:
import traceback
traceback.print_exc()
raise e

finally:
env.close()


def main(test=False):
server = RosSocketServer()
server.run(test)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--test", action="store_true")
args = parser.parse_args()
main(args.test)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
ld = LaunchDescription()
camera_node = Node(package="metadrive_example_bridge", executable="camera_bridge", name='camera_bridge')
ld.add_action(camera_node)
lidar_node = Node(package="metadrive_example_bridge", executable="lidar_bridge", name='lidar_bridge')
ld.add_action(lidar_node)
obj_node = Node(package="metadrive_example_bridge", executable="obj_bridge", name='obj_bridge')
ld.add_action(obj_node)

pkg_dir = get_package_share_directory('metadrive_example_bridge')
rviz_node = Node(
package='rviz2', executable='rviz2', name='rviz2', arguments=['-d', [os.path.join(pkg_dir, 'default.rviz')]]
)
ld.add_action(rviz_node)
return ld


if __name__ == '__main__':
generate_launch_description()
Empty file.

0 comments on commit 486f190

Please sign in to comment.