Skip to content
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

Add minimal support for ROS2 #123

Merged
merged 16 commits into from
Apr 17, 2024
Merged
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
16 changes: 4 additions & 12 deletions .github/workflows/build.yml → .github/workflows/build-ros1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,16 @@ on:
- main

jobs:
build-cpython:
build-ros1:
runs-on: ${{ matrix.os }}
strategy:
matrix:
name: [
"ubuntu-py37",
"ubuntu-py38",
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
]
include:
- name: "ubuntu-py37"
os: ubuntu-latest
python-version: "3.7"
- name: "ubuntu-py38"
os: ubuntu-latest
python-version: "3.8"
- name: "ubuntu-py39"
os: ubuntu-latest
python-version: "3.9"
Expand All @@ -53,15 +45,15 @@ jobs:
python -m pip install --no-cache-dir -r requirements-dev.txt
- name: Set up docker containers
run: |
docker build -t gramaziokohler/rosbridge:integration_tests ./docker
docker run -d -p 9090:9090 --name rosbridge gramaziokohler/rosbridge:integration_tests /bin/bash -c "roslaunch /integration-tests.launch"
docker build -t gramaziokohler/rosbridge:integration_tests_ros1 ./docker/ros1
docker run -d -p 9090:9090 --name rosbridge gramaziokohler/rosbridge:integration_tests_ros1 /bin/bash -c "roslaunch /integration-tests.launch"
docker ps -a
- name: Run linter
run: |
invoke check
- name: Run tests
run: |
pytest
pytest tests/ros1
- name: Tear down docker containers
run: |
docker rm -f rosbridge
59 changes: 59 additions & 0 deletions .github/workflows/build-ros2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: build

on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main

jobs:
build-ros2:
runs-on: ${{ matrix.os }}
strategy:
matrix:
name: [
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
]
include:
- name: "ubuntu-py39"
os: ubuntu-latest
python-version: "3.9"
- name: "ubuntu-py310"
os: ubuntu-latest
python-version: "3.10"
- name: "ubuntu-py311"
os: ubuntu-latest
python-version: "3.11"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install wheel
- name: Install
run: |
python -m pip install --no-cache-dir -r requirements-dev.txt
- name: Set up docker containers
run: |
docker build -t gramaziokohler/rosbridge:integration_tests_ros2 ./docker/ros2
docker run -d -p 9090:9090 --name rosbridge gramaziokohler/rosbridge:integration_tests_ros2 /bin/bash -c "ros2 launch /integration-tests.launch"
docker ps -a
- name: Run linter
run: |
invoke check
- name: Run tests
run: |
pytest tests/ros2
- name: Tear down docker containers
run: |
docker rm -f rosbridge
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Authors
* Hiroyuki Obinata `@obi-t4 <https://github.com/obi-t4>`_
* Pedro Pereira `@MisterOwlPT <https://github.com/MisterOwlPT>`_
* Domenic Rodriguez `@DomenicP <https://github.com/DomenicP>`_
* Ilia Baranov `@iliabaranov <https://github.com/iliabaranov>`_
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Unreleased

**Added**

* Added a ROS2-compatible header class in ``roslibpy.ros2.Header``.

**Changed**

**Fixed**
Expand Down
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ local ROS environment, allowing usage from platforms other than Linux.

The API of **roslibpy** is modeled to closely match that of `roslibjs`_.

ROS1 is fully supported. ROS2 support is still in progress.


Main features
-------------
Expand Down
4 changes: 1 addition & 3 deletions docker/Dockerfile → docker/ros1/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ RUN apt-get update && apt-get install -y \
# Clear apt-cache to reduce image size
&& rm -rf /var/lib/apt/lists/*

# Copy entrypoint
COPY ./ros_entrypoint.sh /
# Copy launch
COPY ./integration-tests.launch /

EXPOSE 9090

ENTRYPOINT ["/ros_entrypoint.sh"]
CMD ["bash"]
File renamed without changes.
21 changes: 21 additions & 0 deletions docker/ros2/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ros:iron
LABEL maintainer "Gonzalo Casas <[email protected]>"

SHELL ["/bin/bash","-c"]

# Install rosbridge
RUN apt-get update && apt-get install -y \
ros-iron-rosbridge-suite \
# ros-iron-tf2-web-republisher \
# ros-iron-ros-tutorials \
# ros-iron-actionlib-tutorials \
--no-install-recommends \
# Clear apt-cache to reduce image size
&& rm -rf /var/lib/apt/lists/*

# Copy launch
COPY ./integration-tests.launch /

EXPOSE 9090

CMD ["bash"]
4 changes: 4 additions & 0 deletions docker/ros2/integration-tests.launch
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<launch>
<include file="$(find-pkg-share rosbridge_server)/launch/rosbridge_websocket_launch.xml" />
<!-- <node pkg="tf2_web_republisher" type="tf2_web_republisher" name="tf2_web_republisher"></node> -->
</launch>
7 changes: 0 additions & 7 deletions docker/ros_entrypoint.sh

This file was deleted.

2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ local ROS environment, allowing usage from platforms other than Linux.

The API of **roslibpy** is modeled to closely match that of `roslibjs <http://wiki.ros.org/roslibjs>`_.

ROS1 is fully supported. ROS2 support is still in progress.

========
Contents
========
Expand Down
1 change: 1 addition & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ API Reference
.. automodule:: roslibpy
.. automodule:: roslibpy.actionlib
.. automodule:: roslibpy.tf
.. automodule:: roslibpy.ros2
13 changes: 13 additions & 0 deletions src/roslibpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@
Main ROS concepts
=================

ROS1 vs ROS2
------------

This library has been tested to work with ROS1. ROS2 should work, but it is still
in the works.

One area in which ROS1 and ROS2 differ is in the header interface. To use ROS2, use
the header defined in the `roslibpy.ros2` module.

.. autoclass:: roslibpy.ros2.Header
:members:


Topics
------

Expand Down
6 changes: 5 additions & 1 deletion src/roslibpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ def __init__(self, values=None):


class Header(UserDict):
"""Represents a message header of the ROS type std_msgs/Header."""
"""Represents a message header of the ROS type std_msgs/Header.

This header is only compatible with ROS1. For ROS2 headers, use :class:`roslibpy.ros2.Header`.

"""

def __init__(self, seq=None, stamp=None, frame_id=None):
self.data = {}
Expand Down
16 changes: 16 additions & 0 deletions src/roslibpy/ros2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from roslibpy import Header as ROS1Header
from roslibpy import Time

__all__ = [
"Header",
]


class Header(ROS1Header):
"""Represents a message header of the ROS type std_msgs/Header."""

def __init__(self, stamp=None, frame_id=None):
super(Header, self).__init__(stamp=stamp, frame_id=frame_id)
self.data["stamp"] = Time(stamp["secs"], stamp["nsecs"]) if stamp else None
self.data["frame_id"] = frame_id
del self.data["seq"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
59 changes: 59 additions & 0 deletions tests/ros2/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest

from roslibpy import Header, Time

REF_FLOAT_SECS_TIME = 1610122759.677662


def test_time_from_sec_based_on_time_module():
t = Time.from_sec(REF_FLOAT_SECS_TIME)
assert t.secs == 1610122759
assert t.nsecs == 677661895


def test_to_nsec():
t = Time.from_sec(REF_FLOAT_SECS_TIME)
assert t.to_nsec() == 1610122759677661895


def test_to_sec():
t = Time.from_sec(REF_FLOAT_SECS_TIME)
assert t.to_sec() == REF_FLOAT_SECS_TIME


def test_is_zero():
assert Time(0, 0).is_zero()
assert Time(1, 0).is_zero() is False


def test_header_ctor_supports_time():
header = Header(stamp=Time.from_sec(REF_FLOAT_SECS_TIME))
assert header["stamp"]["secs"] == 1610122759
assert header["stamp"]["secs"] == header["stamp"].secs
assert header["stamp"].to_sec() == REF_FLOAT_SECS_TIME


def test_header_ctor_supports_dict():
header = Header(stamp=dict(secs=1610122759, nsecs=677661895))
assert header["stamp"]["secs"] == 1610122759
assert header["stamp"]["secs"] == header["stamp"].secs
assert header["stamp"].to_sec() == REF_FLOAT_SECS_TIME


def test_time_accepts_only_ints():
with pytest.raises(ValueError):
Time(1.3, 1.0)
with pytest.raises(ValueError):
Time(100.0, 3.1)

t = Time(110.0, 0.0)
assert t.secs == 110
assert t.nsecs == 0


def test_time_properties_are_readonly():
t = Time.now()
with pytest.raises(AttributeError):
t.secs = 10
with pytest.raises(AttributeError):
t.nsecs = 10
95 changes: 95 additions & 0 deletions tests/ros2/test_ros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import print_function

import threading
import time

from roslibpy import Ros

host = "127.0.0.1"
port = 9090
url = "ws://%s:%d" % (host, port)


def test_reconnect_does_not_trigger_on_client_close():
ros = Ros(host, port)
ros.run()

assert ros.is_connected, "ROS initially connected"
time.sleep(0.5)
event = threading.Event()
ros.on("close", lambda m: event.set())
ros.close()
event.wait(5)

assert not ros.is_connected, "Successful disconnect"
assert not ros.is_connecting, "Not trying to re-connect"


def test_connection():
ros = Ros(host, port)
ros.run()
assert ros.is_connected
ros.close()


def test_url_connection():
ros = Ros(url)
ros.run()
assert ros.is_connected
ros.close()


def test_closing_event():
ros = Ros(url)
ros.run()
ctx = dict(closing_event_called=False, was_still_connected=False)

def handle_closing():
ctx["closing_event_called"] = True
ctx["was_still_connected"] = ros.is_connected
time.sleep(1.5)

ts_start = time.time()
ros.on("closing", handle_closing)
ros.close()
ts_end = time.time()
closing_was_handled_synchronously_before_close = ts_end - ts_start >= 1.5

assert ctx["closing_event_called"]
assert ctx["was_still_connected"]
assert closing_was_handled_synchronously_before_close


def test_multithreaded_connect_disconnect():
CONNECTIONS = 30
clients = []

def connect(clients):
ros = Ros(url)
ros.run()
clients.append(ros)

# First connect all
threads = []
for _ in range(CONNECTIONS):
thread = threading.Thread(target=connect, args=(clients,))
thread.daemon = False
thread.start()
threads.append(thread)

for thread in threads:
thread.join()

# Assert connection status
for ros in clients:
assert ros.is_connected

# Now disconnect all
for ros in clients:
ros.close()

time.sleep(0.5)

# Assert connection status
for ros in clients:
assert not ros.is_connected
Loading
Loading