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 r2r_spl package, capable of listening to different msg types #47

Merged
merged 11 commits into from
Jan 20, 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
2 changes: 2 additions & 0 deletions .github/workflows/build_and_test_humble.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ jobs:

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v4
- uses: ros-tooling/setup-ros@v0.7
with:
use-ros2-testing: true
- uses: ros-tooling/action-ros-ci@v0.3
with:
target-ros2-distro: humble
vcs-repo-file-url: dependencies.repos
2 changes: 2 additions & 0 deletions .github/workflows/build_and_test_iron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ jobs:

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v4
- uses: ros-tooling/setup-ros@v0.7
with:
use-ros2-testing: true
- uses: ros-tooling/action-ros-ci@v0.3
with:
target-ros2-distro: iron
vcs-repo-file-url: dependencies.repos
2 changes: 2 additions & 0 deletions .github/workflows/build_and_test_rolling.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ jobs:

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v4
- uses: ros-tooling/setup-ros@v0.7
with:
use-ros2-testing: true
- uses: ros-tooling/action-ros-ci@v0.3
with:
target-ros2-distro: rolling
vcs-repo-file-url: dependencies.repos
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
5 changes: 5 additions & 0 deletions dependencies.repos
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
repositories:
game_controller_spl:
type: git
url: https://github.com/ros-sports/game_controller_spl.git
version: rolling
25 changes: 25 additions & 0 deletions r2r_spl/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>r2r_spl</name>
<version>0.0.0</version>
<description>Robot-to-Robot Communication in RoboCup Standard Platform League</description>
<maintainer email="kenjibrameld@gmail.com">ijnek</maintainer>
<license>Apache License 2.0</license>

<exec_depend>python3-construct</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>rosidl_parser</exec_depend>
<exec_depend>game_controller_spl_interfaces</exec_depend>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-numpy</test_depend>
<test_depend>python3-pytest</test_depend>
<test_depend>r2r_spl_test_interfaces</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
Empty file added r2r_spl/r2r_spl/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions r2r_spl/r2r_spl/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2023 Kenji Brameld
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

class ParameterNotSetException(Exception):

def __init__(self, message):
self.message = message
super().__init__(message)
179 changes: 179 additions & 0 deletions r2r_spl/r2r_spl/r2r_spl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Copyright 2023 Kenji Brameld
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import socket
from threading import Thread

import construct
from game_controller_spl_interfaces.msg import RCGCD15
from r2r_spl.exceptions import ParameterNotSetException
from r2r_spl.serialization import Serialization
import rclpy
from rclpy.node import Node

MAX_ALLOWED_MSG_SIZE = 128


class R2RSPL(Node):
"""Node that runs on the robot to communicate with teammates (Robot-To-Robot) in SPL."""

_loop_thread = None
_sock = None
_publisher = None
_team_num = None
_budget_reached = False

def __init__(self, node_name='r2r_spl', **kwargs):
super().__init__(node_name, **kwargs)

# Declare parameters
self.declare_parameters(
namespace='',
parameters=[
('team_num', 0),
('player_num', 0),
('msg_type', ''),
('filter_own', False),
]
)

# Read and log parameters
self._team_num = self.get_parameter('team_num').value
self.get_logger().debug('team_num: {}'.format(self._team_num))
if self._team_num == 0:
self.get_logger().warn('"team_num" parameter is 0. This is problematic in a game.')

self.player_num = self.get_parameter('player_num').value
self.get_logger().debug('player_num: {}'.format(self.player_num))
if self.player_num == 0:
self.get_logger().warn('"player_num" parameter is 0. This is problematic in a game.')

self.msg_type = self.get_parameter('msg_type').value
self.get_logger().debug('msg_type: {}'.format(self.msg_type))

self.filter_own = self.get_parameter('filter_own').value
self.get_logger().debug('filter_own: {}'.format(self.filter_own))

# Setup subscriber that listens to message budget
self._subscriber_rcgcd = self.create_subscription(
RCGCD15, 'gc/data', self._rcgcd_callback, 10)

# Evalulate and import message type
if self.msg_type == '':
raise ParameterNotSetException('"msg_type" parameter must be set.')
index_last_dot = self.msg_type.rfind('.')
assert index_last_dot != -1, \
f'msg_type must be in the form "package_name.<namespace>.MsgName" ' \
f'(eg. geometry_msgs.msg.PoseStamped). Got: {self.msg_type}'
assert index_last_dot != len(self.msg_type) - 1, \
f'msg_type must be in the form "package_name.<namespace>.MsgName" ' \
f'(eg. geometry_msgs.msg.PoseStamped). Got: {self.msg_type}'
class_name = self.msg_type[index_last_dot + 1:]
mod = __import__(self.msg_type[:index_last_dot], fromlist=[class_name])
msg_class = getattr(mod, class_name)

# Setup serialization
self._serialization = Serialization(
msg_class, player_num=self.player_num if self.filter_own else None)

# Setup publisher
self._publisher = self.create_publisher(msg_class, 'r2r/recv', 10)

# Setup subscriber
self._subscriber = self.create_subscription(
msg_class, 'r2r/send', self._topic_callback, 10)

# UDP Client - adapted from https://github.com/ninedraft/python-udp/blob/master/client.py
self._sock = socket.socket(
socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # UDP
# This has to be SO_REUSEADDR instead of SO_REUSEPORT to work with TCM
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self._sock.bind(('', 10000 + self._team_num))
# Set timeout so _loop can constantly check for rclpy.ok()
self._sock.settimeout(0.1)

# Start thread to continuously poll
self._loop_thread = Thread(target=self._loop)
self._loop_thread.start()

def _loop(self):
while rclpy.ok():
try:
data, _ = self._sock.recvfrom(1024)
self.get_logger().debug(f'received: {data}')

# Convert data to ROS msg
try:
msg = self._serialization.deserialize(data)

if msg:
# Publish. Make sure we haven't shutdown, since node could have been
# shutdown while waiting for the packet.
if rclpy.ok():
self._publisher.publish(msg)
else:
# Reaches here if we filtered out our own message
# (only if filtering is enabled via filter_own parameter)
pass

except construct.core.StreamError:
# Deserialization failed
self.get_logger().error(
f'deserialization failed, please ensure other robots are using the '
f'matching message type {self.msg_type}', once=True)

except TimeoutError:
pass

def _topic_callback(self, msg):
if not self._budget_reached:
data = self._serialization.serialize(msg)

if len(data) > MAX_ALLOWED_MSG_SIZE:
self.get_logger().error(
f'Cannot send message of size {len(data)} bytes. Maximum size is 128 bytes.')
else:
# Broadcast data on team's UDP port
self._sock.sendto(data, ('<broadcast>', 10000 + self._team_num))

def _rcgcd_callback(self, msg):
team_found = False
for team in msg.teams:
if team.team_number == self._team_num:
team_found = True

if not self._budget_reached and team.message_budget < 10:
self.get_logger().info('Budget almost reached, not sending anymore messages')
self._budget_reached = True
elif self._budget_reached and team.message_budget > 10:
self.get_logger().info('Extra budget available, sending messages again')
self._budget_reached = False

if not team_found:
self.get_logger().warn(
f'Received messages from Game Controller about teams {msg.teams[0].team_number} '
f'and {msg.teams[1].team_number}, but team_num parameter is {self._team_num}. '
f'This is problematic if in a game.', once=True)


def main(args=None):
rclpy.init(args=args)
r2r_spl = R2RSPL()
rclpy.spin(r2r_spl)
rclpy.shutdown()


if __name__ == '__main__':
main()
Loading