Skip to content

Commit

Permalink
Support usage of ros messages as parameters (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
fred-labs committed Aug 27, 2024
1 parent 42ffa7a commit f12703b
Show file tree
Hide file tree
Showing 36 changed files with 345 additions and 133 deletions.
2 changes: 2 additions & 0 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Implement an Action
- Make use of ``kwargs['logger']``, available in ``setup()``
- If you want to draw markers for RViz, use ``kwargs['marker_handler']``, available in ``setup()`` (with ROS backend)
- Use arguments from ``__init__()`` for a longer running initialization in ``setup()`` and the arguments from ``execute()`` to set values just before executing the action.
- ``__init__()`` does not need to contain all osc2-defined arguments. This can be convenient as variable argument resolving might not be available during ``__init__()``.
- ``execute()`` contains all osc2-arguments.
- ``setup()`` provides several arguments that might be useful:
- ``input_dir``: Directory containing the scenario file
- ``output_dir``: If given on command-line, contains the directory to save output to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from enum import Enum

import py_trees
from scenario_execution.actions.base_action import BaseAction
from scenario_execution.actions.base_action import BaseAction, ActionError

import docker
import tempfile
Expand Down Expand Up @@ -46,20 +46,20 @@ def setup(self, **kwargs):
# self.output_dir = tempfile.mkdtemp() # for testing: does not remove directory afterwards

if "input_dir" not in kwargs:
raise ValueError("input_dir not defined.")
raise ActionError("input_dir not defined.", action=self)
input_dir = kwargs["input_dir"]
# check docker image
self.client = docker.from_env()
image_name = 'floorplan:latest'
filterred_images = self.client.images.list(filters={'reference': image_name})
if len(filterred_images) == 0:
raise ValueError(f"Required docker image '{image_name}' does not exist.")
raise ActionError(f"Required docker image '{image_name}' does not exist.", action=self)

# check files
if not os.path.isabs(self.file_path):
self.file_path = os.path.join(input_dir, self.file_path)
if not os.path.isfile(self.file_path):
raise ValueError(f"Floorplan file {self.file_path} not found.")
raise ActionError(f"Floorplan file {self.file_path} not found.", action=self)
self.floorplan_name = os.path.splitext(os.path.basename(self.file_path))[0]

def update(self) -> py_trees.common.Status:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import os
import py_trees
from scenario_execution_gazebo.actions.utils import SpawnUtils
from scenario_execution.actions.base_action import BaseAction
from scenario_execution.actions.base_action import BaseAction, ActionError
from shutil import which
import tempfile

Expand All @@ -31,15 +31,15 @@ def __init__(self, associated_actor, sdf_template: str, arguments: list):

def setup(self, **kwargs):
if which("xacro") is None:
raise ValueError("'xacro' not found.")
raise ActionError("'xacro' not found.", action=self)
if "input_dir" not in kwargs:
raise ValueError("input_dir not defined.")
raise ActionError("input_dir not defined.", action=self)
input_dir = kwargs["input_dir"]

if not os.path.isabs(self.sdf_template):
self.sdf_template = os.path.join(input_dir, self.sdf_template)
if not os.path.isfile(self.sdf_template):
raise ValueError(f"SDF Template {self.sdf_template} not found.")
raise ActionError(f"SDF Template {self.sdf_template} not found.", action=self)
self.tmp_file = tempfile.NamedTemporaryFile(suffix=".sdf") # for testing, do not delete temp file: delete=False

def execute(self, associated_actor, sdf_template: str, arguments: list):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from tf2_ros.transform_listener import TransformListener
from tf2_geometry_msgs import PoseStamped
from .gazebo_spawn_actor import GazeboSpawnActor
from scenario_execution.actions.base_action import ActionError


class GazeboRelativeSpawnActor(GazeboSpawnActor):
Expand Down Expand Up @@ -97,4 +98,4 @@ def calculate_new_pose(self):
f' w: {new_pose.pose.orientation.w} x: {new_pose.pose.orientation.x} y: {new_pose.pose.orientation.y} z: {new_pose.pose.orientation.z}' \
' } }'
except TransformException as e:
raise ValueError(f"No transform available ({self.parent_frame_id}->{self.frame_id})") from e
raise ActionError(f"No transform available ({self.parent_frame_id}->{self.frame_id})", action=self) from e
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSHistoryPolicy, QoSReliabilityPolicy
from rclpy.node import Node
import py_trees
from scenario_execution.actions.base_action import ActionError
from scenario_execution.actions.run_process import RunProcess
from .utils import SpawnUtils

Expand Down Expand Up @@ -68,7 +69,7 @@ def setup(self, **kwargs):
except KeyError as e:
error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(
self.name, self.__class__.__name__)
raise KeyError(error_message) from e
raise ActionError(error_message, action=self) from e

self.utils = SpawnUtils(logger=self.logger)

Expand All @@ -88,12 +89,12 @@ def setup(self, **kwargs):
self.entity_model, self.entity_name, self.xacro_arguments)

if not self.sdf:
raise ValueError(f'Invalid model specified ({self.entity_model})')
raise ActionError(f'Invalid model specified ({self.entity_model})', action=self)
self.current_state = SpawnActionState.MODEL_AVAILABLE

def execute(self, associated_actor, spawn_pose: list, world_name: str, xacro_arguments: list, model: str): # pylint: disable=arguments-differ
if self.entity_model != model or set(self.xacro_arguments) != set(xacro_arguments):
raise ValueError("Runtime change of model not supported.")
raise ActionError("Runtime change of model not supported.", action=self)
self.spawn_pose = spawn_pose
self.world_name = world_name

Expand Down Expand Up @@ -175,7 +176,7 @@ def get_spawn_pose(self):
f' w: {quaternion[0]} x: {quaternion[1]} y: {quaternion[2]} z: {quaternion[3]}' \
' } }'
except KeyError as e:
raise ValueError("Could not get values") from e
raise ActionError("Could not get values", action=self) from e
return pose

def set_command(self, command):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

from .nav2_common import NamespaceAwareBasicNavigator
from scenario_execution_ros.actions.common import get_pose_stamped, NamespacedTransformListener
from scenario_execution.actions.base_action import BaseAction
from scenario_execution.actions.base_action import BaseAction, ActionError


class InitNav2State(Enum):
Expand Down Expand Up @@ -91,7 +91,7 @@ def setup(self, **kwargs):
except KeyError as e:
error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(
self.name, self.__class__.__name__)
raise KeyError(error_message) from e
raise ActionError(error_message, action=self) from e

self.tf_buffer = Buffer()
self.tf_listener = NamespacedTransformListener(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# SPDX-License-Identifier: Apache-2.0

import py_trees
from scenario_execution.actions.base_action import BaseAction
from scenario_execution.actions.base_action import BaseAction, ActionError
import pybullet as p
import math

Expand All @@ -30,10 +30,10 @@ def setup(self, **kwargs):
try:
tick_period: float = kwargs['tick_period']
except KeyError as e:
raise KeyError("didn't find 'tick_period' in setup's kwargs") from e
raise ActionError("didn't find 'tick_period' in setup's kwargs", action=self) from e
if not math.isclose(240 % tick_period, 0., abs_tol=1e-4):
raise ValueError(
f"Scenario Execution Tick Period of {tick_period} is not compatible with PyBullet stepping. Please set step-duration to be a multiple of 1/240s")
raise ActionError(
f"Scenario Execution Tick Period of {tick_period} is not compatible with PyBullet stepping. Please set step-duration to be a multiple of 1/240s", action=self)
self.sim_steps_per_tick = round(240 * tick_period)
self.logger.info(f"Forward simulation by {self.sim_steps_per_tick} step per scenario tick.")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from enum import Enum
import py_trees
import os
from scenario_execution.actions.base_action import ActionError
from scenario_execution.actions.run_process import RunProcess


Expand All @@ -36,12 +37,12 @@ def __init__(self, output_filename: str, frame_rate: float):

def setup(self, **kwargs):
if "DISPLAY" not in os.environ:
raise ValueError("capture_screen() requires environment variable 'DISPLAY' to be set.")
raise ActionError("capture_screen() requires environment variable 'DISPLAY' to be set.", action=self)

if kwargs['output_dir']:
if not os.path.exists(kwargs['output_dir']):
raise ValueError(
f"Specified destination dir '{kwargs['output_dir']}' does not exist")
raise ActionError(
f"Specified destination dir '{kwargs['output_dir']}' does not exist", action=self)
self.output_dir = kwargs['output_dir']

def execute(self, output_filename: str, frame_rate: float): # pylint: disable=arguments-differ
Expand Down
11 changes: 10 additions & 1 deletion scenario_execution/scenario_execution/actions/base_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import py_trees
from scenario_execution.model.types import ParameterDeclaration, ScenarioDeclaration
from scenario_execution.model.error import OSC2Error


class BaseAction(py_trees.behaviour.Behaviour):
Expand Down Expand Up @@ -78,7 +79,7 @@ def get_blackboard_namespace(node: ParameterDeclaration):

def register_access_to_associated_actor_variable(self, variable_name):
if not self._model.actor:
raise ValueError("Model does not have 'actor'.")
raise ActionError("Model does not have 'actor'.", action=self)
blackboard = self.get_blackboard_client()
model_blackboard_name = self._model.actor.get_fully_qualified_var_name(include_scenario=False)
model_blackboard_name += "/" + variable_name
Expand All @@ -94,3 +95,11 @@ def get_associated_actor_variable(self, variable_name):
model_blackboard_name = self.register_access_to_associated_actor_variable(variable_name)
self.logger.debug(f"Get variable '{model_blackboard_name}'")
return getattr(self.get_blackboard_client(), model_blackboard_name)


class ActionError(OSC2Error):

def __init__(self, msg: str, action: BaseAction, *args) -> None:
if action is not None:
ctx = action._model.get_ctx()
super().__init__(msg, ctx, *args)
4 changes: 2 additions & 2 deletions scenario_execution/scenario_execution/actions/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import py_trees
from py_trees.common import Status
from scenario_execution.actions.base_action import BaseAction
from scenario_execution.actions.base_action import BaseAction, ActionError


class Log(BaseAction):
Expand All @@ -38,7 +38,7 @@ def update(self) -> py_trees.common.Status:
if not self.published:
self.published = True
if not self.msg:
raise ValueError("log(): Empty message.")
raise ActionError("log(): Empty message.", action=self)
self.logger.info(f"{self.msg}")

return Status.SUCCESS
38 changes: 25 additions & 13 deletions scenario_execution/scenario_execution/model/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,36 @@
from antlr4 import ParserRuleContext


class OSC2ParsingError(Exception):
"""
Error class for OSC2 parser
"""
class OSC2Error(Exception):

def __init__(self, msg: str, context, *args) -> None:
super().__init__(*args)
self.msg = msg
if isinstance(context, ParserRuleContext):
self.line = context.start.line
self.column = context.start.column
self.context = context.getText()
self.filename = ""
self.osc_ctx = (context.start.line, context.start.column, context.getText(), "")
else:
self.line = context[0]
self.column = context[1]
self.context = context[2]
self.filename = context[3]
self.osc_ctx = context

def __str__(self) -> str:
return self.msg
error_str = ""
if self.osc_ctx is not None:
if len(self.osc_ctx) == 4:
context = self.osc_ctx[2].replace('\n', '')
error_str = f"(line: {self.osc_ctx[0]}, column: {self.osc_ctx[1]} in '{self.osc_ctx[3]}') -> {context}: "
else:
error_str = f"<invalid context: {self.osc_ctx}>: "
error_str += self.msg
return error_str


class OSC2ParsingError(OSC2Error):
"""
Error class for OSC2 parser
"""

def __init__(self, msg: str, context, *args) -> None:
if isinstance(context, ParserRuleContext):
ctx = (context.start.line, context.start.column, context.getText(), "")
else:
ctx = context
super().__init__(msg, ctx, *args)
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def create_py_tree_blackboard(model, tree, logger, log_tree):
try:
model_blackboard.build(model, tree, log_tree)
except OSC2ParsingError as e:
raise ValueError(
f'Error while creating py-tree:\nTraceback <line: {e.line}, column: {e.column}> in "{e.filename}":\n -> {e.context}\n{e.__class__.__name__}: {e.msg}') from e
raise ValueError(f'Error while creating py-tree: {e}') from e


class ModelToBlackboard(object):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ def resolve_internal_model(model, tree, logger, log_tree):
try:
osc2scenario_resolver.visit(model)
except OSC2ParsingError as e:
raise ValueError(
f'Error while creating tree:\nTraceback <line: {e.line}, column: {e.column}> in "{e.filename}":\n -> {e.context}\n{e.__class__.__name__}: {e.msg}') from e
raise ValueError(f'Error while creating tree: {e}') from e

if log_tree:
logger.info("----Internal model (resolved)-----")
Expand Down
32 changes: 17 additions & 15 deletions scenario_execution/scenario_execution/model/model_to_py_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ def create_py_tree(model, tree, logger, log_tree):
try:
final_tree = model_to_py_tree.build(model, tree, log_tree)
except OSC2ParsingError as e:
raise ValueError(
f'Error while creating py-tree:\nTraceback <line: {e.line}, column: {e.column}> in "{e.filename}":\n -> {e.context}\n{e.__class__.__name__}: {e.msg}') from e
raise ValueError(f'Error while creating py-tree: {e}') from e
return final_tree


Expand Down Expand Up @@ -102,15 +101,15 @@ def update(self):
return Status.SUCCESS


class ExpressionBehavior(py_trees.behaviour.Behaviour):

def __init__(self, name: "ExpressionBehavior", expression: Expression):
super().__init__(name)
class ExpressionBehavior(BaseAction): # py_trees.behaviour.Behaviour):

def __init__(self, name: "ExpressionBehavior", expression: Expression, model, logger):
super().__init__()
self._set_base_properities(name, model, logger)
self.expression = expression

def update(self):
if self.expression.eval():
if self.expression.eval(self.get_blackboard_client()):
return Status.SUCCESS
else:
return Status.RUNNING
Expand Down Expand Up @@ -215,7 +214,7 @@ def compare_method_arguments(self, method, expected_args, behavior_name, node):
if error_string:
error_string += ", "
error_string += "unknown: " + ", ".join(unknown_args)
return method_args, error_string
return method_args, error_string, missing_args

def create_decorator(self, node: ModifierDeclaration, resolved_values):
available_modifiers = ["repeat", "inverter", "timeout", "retry", "failure_is_running", "failure_is_success",
Expand Down Expand Up @@ -311,18 +310,19 @@ def visit_behavior_invocation(self, node: BehaviorInvocation):
# if __init__() is defined, check parameters. Allowed:
# - __init__(self)
# - __init__(self, resolve_variable_reference_arguments_in_execute)
# - __init__(self, <all-osc-defined-args>)
init_args, error_string = self.compare_method_arguments(init_method, expected_args, behavior_name, node)
# - __init__(self, <some-or-all-osc-defined-args>)
init_args, error_string, args_not_in_init = self.compare_method_arguments(
init_method, expected_args, behavior_name, node)
if init_args != ["self"] and \
init_args != ["self", "resolve_variable_reference_arguments_in_execute"] and \
set(init_args) != set(expected_args):
not all(x in expected_args for x in init_args):
raise OSC2ParsingError(
msg=f'Plugin {behavior_name}: __init__() either only has "self" argument or all arguments defined in osc. {error_string}\n'
msg=f'Plugin {behavior_name}: __init__() either only has "self" argument and osc-defined arguments. {error_string}\n'
f'expected definition with all arguments: {expected_args}', context=node.get_ctx()
)
execute_method = getattr(behavior_cls, "execute", None)
if execute_method is not None:
_, error_string = self.compare_method_arguments(execute_method, expected_args, behavior_name, node)
_, error_string, _ = self.compare_method_arguments(execute_method, expected_args, behavior_name, node)
if error_string:
raise OSC2ParsingError(
msg=f'Plugin {behavior_name}: execute() arguments differ from osc-definition: {error_string}.', context=node.get_ctx()
Expand All @@ -336,12 +336,14 @@ def visit_behavior_invocation(self, node: BehaviorInvocation):
f"Instantiate action '{action_name}', plugin '{behavior_name}'. with:\nExpected execute() arguments: {expected_args}")
try:
if init_args is not None and init_args != ['self'] and init_args != ['self', 'resolve_variable_reference_arguments_in_execute']:
final_args = node.get_resolved_value(self.blackboard)
final_args = node.get_resolved_value(self.blackboard, skip_keys=args_not_in_init)

if node.actor:
final_args["associated_actor"] = node.actor.get_resolved_value(self.blackboard)
final_args["associated_actor"]["name"] = node.actor.name

for k in args_not_in_init:
del final_args[k]
instance = behavior_cls(**final_args)
else:
instance = behavior_cls()
Expand All @@ -363,7 +365,7 @@ def visit_event_condition(self, node: EventCondition):
expression = ""
for child in node.get_children():
if isinstance(child, (RelationExpression, LogicalExpression)):
expression = ExpressionBehavior(name=node.get_ctx()[2], expression=self.visit(child))
expression = ExpressionBehavior(name=node.get_ctx()[2], expression=self.visit(child), model=node, logger=self.logger)
elif isinstance(child, ElapsedExpression):
elapsed_condition = self.visit_elapsed_expression(child)
expression = py_trees.timers.Timer(name=f"wait {elapsed_condition}s", duration=float(elapsed_condition))
Expand Down
Loading

0 comments on commit f12703b

Please sign in to comment.