Skip to content

Commit

Permalink
More actions functionality and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sea-bass committed Oct 31, 2023
1 parent 998be32 commit 1a2eae9
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2023, PickNik Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from rosbridge_library.capability import Capability
from rosbridge_library.internal import message_conversion, ros_loader

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2023, PickNik Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import fnmatch
import time

import rclpy
from rclpy.action import ActionServer
Expand All @@ -12,11 +45,12 @@ class AdvertisedActionHandler:

id_counter = 1

def __init__(self, action_name, action_type, protocol):
def __init__(self, action_name, action_type, protocol, sleep_time=0.001):
self.goal_futures = {}
self.action_name = action_name
self.action_type = action_type
self.protocol = protocol
self.sleep_time = sleep_time
# setup the action
self.action_server = ActionServer(
protocol.node_handle,
Expand All @@ -31,26 +65,30 @@ def next_id(self):
self.id_counter += 1
return id

async def execute_callback(self, goal):
def execute_callback(self, goal):
# generate a unique ID
goal_id = f"action_goal:{self.action}:{self.next_id()}"
goal_id = f"action_goal:{self.action_name}:{self.next_id()}"

future = rclpy.task.Future()
self.request_futures[goal_id] = future
self.goal_futures[goal_id] = future

# build a request to send to the external client
goal_message = {
"op": "send_action_goal",
"id": goal_id,
"action": self.action_name,
"args": message_conversion.extract_values(goal),
"action_type": self.action_type,
"args": message_conversion.extract_values(goal.request),
}
self.protocol.send(goal_message)

try:
return await future
finally:
del self.goal_futures[goal_id]
while not future.done():
time.sleep(self.sleep_time)

result = future.result()
goal.succeed()
del self.goal_futures[goal_id]
return result

def handle_result(self, goal_id, res):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2023, PickNik Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import fnmatch
from functools import partial
from threading import Thread

from rosbridge_library.capability import Capability
from rosbridge_library.internal.actions import ActionClientHandler, SendGoal


class SendActionGoal(Capability):

send_action_goal_msg_fields = [
(True, "action", str),
(True, "action_type", str),
(False, "fragment_size", (int, type(None))),
(False, "compression", str),
]

actions_glob = None

def __init__(self, protocol):
# Call superclass constructor
Capability.__init__(self, protocol)

# Register the operations that this capability provides
call_services_in_new_thread = (
protocol.node_handle.get_parameter("call_services_in_new_thread")
.get_parameter_value()
.bool_value
)
if call_services_in_new_thread:
# Sends the action goal in a separate thread so multiple actions can be processed simultaneously.
protocol.node_handle.get_logger().info("Sending action goal in new thread")
protocol.register_operation(
"send_action_goal",
lambda msg: Thread(target=self.send_action_goal, args=(msg,)).start(),
)
else:
# Sends the actions goal in this thread, so actions block and must be processed sequentially.
protocol.node_handle.get_logger().info("Sending action goal in existing thread")
protocol.register_operation("send_action_goal", self.send_action_goal)

def send_action_goal(self, message):
# Pull out the ID
cid = message.get("id", None)

# Typecheck the args
self.basic_type_check(message, self.send_action_goal_msg_fields)

# Extract the args
action = message["action"]
action_type = message["action_type"]
fragment_size = message.get("fragment_size", None)
compression = message.get("compression", "none")
args = message.get("args", [])

if SendActionGoal.actions_glob is not None and SendActionGoal.actions_glob:
self.protocol.log("debug", f"Action security glob enabled, checking action: {action}")
match = False
for glob in SendActionGoal.actions_glob:
if fnmatch.fnmatch(action, glob):
self.protocol.log(
"debug",
f"Found match with glob {glob}, continuing sending action goal...",
)
match = True
break
if not match:
self.protocol.log(
"warn",
f"No match found for action, cancelling sending action goal for: {action}",
)
return
else:
self.protocol.log("debug", "No action security glob, not checking sendign action goal.")

# Check for deprecated action ID, eg. /rosbridge/topics#33
cid = extract_id(action, cid)

# Create the callbacks
s_cb = partial(self._success, cid, action, fragment_size, compression)
e_cb = partial(self._failure, cid, action)
feedback = True # TODO: Implement
if feedback:
f_cb = partial(self._feedback, cid, action)
else:
f_cb = None

# Run action client handler in the same thread.
ActionClientHandler(
trim_action_name(action), action_type, args, s_cb, e_cb, f_cb, self.protocol.node_handle
).run()

def _success(self, cid, action, fragment_size, compression, message):
outgoing_message = {
"op": "action_result",
"action": action,
"values": message,
"result": True,
}
if cid is not None:
outgoing_message["id"] = cid
# TODO: fragmentation, compression
self.protocol.send(outgoing_message)

def _failure(self, cid, action, exc):
self.protocol.log("error", "send_action_goal %s: %s" % (type(exc).__name__, str(exc)), cid)
# send response with result: false
outgoing_message = {
"op": "action_result",
"service": action,
"values": str(exc),
"result": False,
}
if cid is not None:
outgoing_message["id"] = cid
self.protocol.send(outgoing_message)

def _feedback(self, cid, action, message):
outgoing_message = {
"op": "action_feedback",
"action": action,
"values": message,
}
if cid is not None:
outgoing_message["id"] = cid
# TODO: fragmentation, compression
print("SENDING FEEDBACK")
print(outgoing_message)
self.protocol.send(outgoing_message)


def trim_action_name(action):
if "#" in action:
return action[: action.find("#")]
return action


def extract_id(action, cid):
if cid is not None:
return cid
elif "#" in action:
return action[action.find("#") + 1 :]
Loading

0 comments on commit 1a2eae9

Please sign in to comment.