diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0cedd22f..fd6d8f50 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,31 +25,15 @@ jobs: uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: humble - # Get bt_tools TODO: remove after the release of bt_tools - - name: Checkout bt_tools - uses: actions/checkout@v2 - with: - repository: boschresearch/bt_tools - ref: main - path: colcon_ws/src/bt_tools - # Compile bt_tools TODO: remove after the release of bt_tools - - name: Compile bt_tools - run: | - source /opt/ros/humble/setup.bash - # Install dependencies - cd colcon_ws - rosdep update && rosdep install --from-paths src --ignore-src -y - # Build and install bt_tools - colcon build --symlink-install # Install packages - name: Install our packages run: | - source colcon_ws/install/setup.bash + source /opt/ros/humble/setup.bash pip install . # build the documentation - name: Build documentation run: | - source colcon_ws/install/setup.bash + source /opt/ros/humble/setup.bash cd docs make html # upload the documentation to GitHub Pages diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b8d35374..1da8dca1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,13 +38,6 @@ jobs: with: timezoneLinux: "Europe/Berlin" - uses: actions/checkout@v3 - # Get bt_tools TODO: remove after the release of bt_tools - - name: Checkout bt_tools - uses: actions/checkout@v2 - with: - repository: boschresearch/bt_tools - ref: main - path: bt_tools - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -53,11 +46,6 @@ jobs: run: | pip install --upgrade pip pip install setuptools_rust - # Install btlib TODO: remove after the release of bt_tools - - name: Install btlib - run: | - cd bt_tools - pip install -e btlib/. - name: Install packages run: | pip install . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffc1dbfa..e0d8e783 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,29 +26,6 @@ jobs: uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: ${{ matrix.ros-distro }} - # Get bt_tools TODO: remove after the release of bt_tools - - name: Checkout bt_tools - uses: actions/checkout@v2 - with: - repository: boschresearch/bt_tools - ref: main - path: colcon_ws/src/bt_tools - # Remove unused packages from checked out bt_tools - - name: Remove packages we don't need - run: | - rm -rf colcon_ws/src/bt_tools/bt_live - rm -rf colcon_ws/src/bt_tools/bt_tools - rm -rf colcon_ws/src/bt_tools/bt_tools_common - rm -rf colcon_ws/src/bt_tools/bt_view - # Compile bt_tools TODO: remove after the release of bt_tools - - name: Compile bt_tools - run: | - source /opt/ros/${{ matrix.ros-distro }}/setup.bash - # Install dependencies - cd colcon_ws - rosdep update && rosdep install --from-paths src --ignore-src -y - # Build and install bt_tools - colcon build --symlink-install # Get smc_storm for testing - name: Get smc_storm id: get_smc_storm @@ -72,9 +49,9 @@ jobs: pip install ${{ matrix.os == 'ubuntu-24.04' && '--break-system-packages' || '' }} . # this solves # E ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject - - name: Downgrade numpy, networkx to match + - name: Downgrade numpy run: | - pip install numpy==1.26.4 networkx==2.8.8 + pip install numpy==1.26.4 if: ${{ matrix.os == 'ubuntu-22.04' }} # lint packages # TODO: add linting @@ -82,6 +59,5 @@ jobs: - name: Run tests run: | export PATH=$PATH:${{ steps.get_smc_storm.outputs.SMC_STORM_PATH }} - # source /opt/ros/${{ matrix.ros-distro }}/setup.bash - source colcon_ws/install/setup.bash # TODO: remove after the release of bt_tools + source /opt/ros/${{ matrix.ros-distro }}/setup.bash pytest-3 -vs test/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 659538fb..2ff80f91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/docs/source/conf.py b/docs/source/conf.py index 3c533021..0928da59 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,7 +24,6 @@ # intersphinx_mapping = { # 'python': ('https://docs.python.org/3/', None), # 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), -# 'networkx': ('https://networkx.org/documentation/stable/', None), # } intersphinx_disabled_domains = ["std"] diff --git a/docs/source/howto.rst b/docs/source/howto.rst index 431bd4f0..908da181 100644 --- a/docs/source/howto.rst +++ b/docs/source/howto.rst @@ -218,9 +218,11 @@ TODO Creating an SCXML model of a BT plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -SCXML models of BT plugins can be done similarly to the ones for ROS nodes. However, in BT plugins there are a few special functionalities that are provided: +As for ROS nodes, in AS2FM we support the implementation of custom BT plugins using ROS-SCXML. -* :ref:`BT communication <bt_communication>`: A set of special events that are used in each BT plugin for starting a BT node and providing results. +Since BT plugins rely on a specific interface, we extended the SCXML language to support the following features: + +* :ref:`BT communication <bt_communication>`: A set of XML tags for modeling the BT Communication interface, based on BT ticks and BT responses. * :ref:`BT Ports <bt_ports>`: A special BT interface to parametrize a specific plugin instance. @@ -229,15 +231,80 @@ SCXML models of BT plugins can be done similarly to the ones for ROS nodes. Howe BT Communication _________________ -TODO: describe `bt_tick`, `bt_running`, `bt_success`, `bt_failure`. +Normally, a BT plugin (or BT node), is idle until it receives a BT tick from a control node. +The BT tick is used to trigger the execution of the BT plugin, which will then return a BT response to the control node that sent the tick. + +The BT plugin `AlwaysSuccess`, that returns `SUCCESS` each time it is ticked, can be implemented as follows: + +.. code-block:: xml + + <scxml name="AlwaysSuccess" initial="idle"> + <state id="idle"> + <bt_tick target="idle"> + <bt_return_status status="SUCCESS" /> + </bt_tick> + </state> + </scxml> + +In this example, there is only the `idle` state, always listening for an incoming `bt_tick` event. +When the tick is received, the plugin starts executing the body of the `bt_tick` tag, that returns a `SUCCESS` response and starts listening for a new `bt_tick`. + +Additionally, it is possible to model BT control nodes, that can send ticks to their children (that, in turns, are BT nodes as well) and receive their responses: + +.. code-block:: xml + + <scxml initial="wait_for_tick" name="Inverter"> + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count != 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_child" /> + </state> + + <state id="tick_child"> + <onentry> + <bt_tick_child id="0"/> + </onentry> + <bt_child_status id="0" cond="_bt.status == SUCCESS" target="wait_for_tick"> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == FAILURE" target="wait_for_tick"> + <bt_return_status status="SUCCESS" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + + </scxml> + +In this example, the `Inverter` control node waits for a tick, then sends a tick to its child (identified by the id `0`), and waits for the response. +Once the child response is available, the control node inverts the response and sends it back to the control node that ticked it in the first place. + +In this model, the `CHILDREN_COUNT` BT port is used to access the number of children of a control node instance, to check it is correctly configured. +Additional control nodes implementations are available in the `src/as2fm/resources <https://github.com/convince-project/AS2FM/blob/main/src/as2fm/resources/bt_control_nodes>`_ folder, and can be used as a reference to implement new ones. .. _bt_ports: BT Ports ________ -Additionally, when loading a BT plugin in the BT XML tree, it is possible to configure a specific plugin instance by means of the BT ports. +When loading a BT plugin in the BT XML tree, it is possible to configure a specific plugin instance by means of the BT ports. As in the case of ROS functionalities, BT ports need to be declared before being used, to provide the port name and expected type. diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 82617edb..84fd239b 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,7 +11,6 @@ The scripts have been tested with Python 3.10 and pip version 24.0. Additionally, the following dependencies are required to be installed: * `ROS Humble <https://docs.ros.org/en/humble/index.html>`_ -* `bt_tools <https://github.com/boschresearch/bt_tools>`_ AS2FM Package Installations @@ -46,7 +45,7 @@ AS2FM can be installed using pip: # Editable mode python3 -m pip install -e AS2FM/ -Verify your installation by **sourcing the ROS workspace containing btlib** and then running: +Verify your installation by **sourcing your ROS distribution** (i.e. running `source /opt/ros/<ros-distro>/setup.bash`) and then running: .. code-block:: bash diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst index 377197b5..891b0d20 100644 --- a/docs/source/tutorials.rst +++ b/docs/source/tutorials.rst @@ -57,7 +57,7 @@ In addition, in this main file, all the components of the example are put togeth Structure of Inputs ````````````````````` -The `as2fm_scxml_to_jani` tool takes a main XML file, e.g., `main.xml <https://github.com/convince-project/AS2FM/blob/main/test/jani_generator/_test_data/ros_example_w_bt/main.xmll>`_ with the following content: +The `as2fm_scxml_to_jani` tool takes a main XML file, e.g., `main.xml <https://github.com/convince-project/AS2FM/blob/main/test/jani_generator/_test_data/ros_example_w_bt/main.xml>`_ with the following content: * one or multiple ROS nodes in SCXML: diff --git a/pyproject.toml b/pyproject.toml index a8691e2d..2e4d2706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,23 +27,23 @@ dependencies = [ "webcolors", "plantuml", # scxml_converter - "networkx", - # "btlib", (would be good to declare here but then this is only installable in a ros environment) # trace_visualizer "pandas", "Pillow", ] requires-python = ">=3.10" -[tool.setuptools.packages.find] -where = ["src"] -include = ["as2fm", "as2fm.*"] - -[tool.setuptools.package-dir] -as2fm = "src/as2fm" +# Comment these lines out, since they prevent the package from being found in code +# [tool.setuptools.packages.find] +# where = ["src"] +# include = ["as2fm", "as2fm.*"] +# +# [tool.setuptools.package-dir] +# "as2fm" = "src/as2fm" [tool.setuptools.package-data] "as2fm.trace_visualizer" = ["data/slkscr.ttf"] +"as2fm.resources" = ["bt_control_nodes/*.scxml"] [project.scripts] as2fm_convince_to_plain_jani = "as2fm.jani_generator.main:main_convince_to_plain_jani" diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_event.py b/src/as2fm/jani_generator/scxml_helpers/scxml_event.py index 114d99ac..c640bd66 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_event.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_event.py @@ -105,6 +105,7 @@ def must_be_skipped_in_jani_conversion(self): def is_bt_response_event(self): """Check if the event is a behavior tree response event (running, success, failure). They may have no sender if the plugin does not implement it.""" + # TODO: Remove it when deprecated support for running, success, failure BT events is removed return self.name.startswith("bt_") and ( self.name.endswith("_running") or self.name.endswith("_success") diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py index be790a0c..7e92ccb5 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py @@ -53,7 +53,10 @@ def parse_ecmascript_to_jani_expression( :param array_info: The type and max size of the array, if required. :return: The jani expression. """ - ast = esprima.parseScript(ecmascript) + try: + ast = esprima.parseScript(ecmascript) + except esprima.error_handler.Error as e: + raise RuntimeError(f"Failed parsing ecmascript: {ecmascript}. Error: {e}.") assert len(ast.body) == 1, "The ecmascript must contain exactly one expression." ast = ast.body[0] try: @@ -75,8 +78,17 @@ def _parse_ecmascript_to_jani_expression( :param array_info: The type and max size of the array, if required. :return: The jani expression. """ - if ast.type == "Literal": + if ast.type == "ExpressionStatement": + return _parse_ecmascript_to_jani_expression(ast.expression, array_info) + elif ast.type == "Literal": return JaniExpression(JaniValue(ast.value)) + elif ast.type == "Identifier": + # If it is an identifier, we do not need to expand further + assert ast.name not in ("True", "False"), ( + f"Boolean {ast.name} mistaken for an identifier. " + "Did you mean to use 'true' or 'false' instead?" + ) + return JaniExpression(ast.name) elif ast.type == "UnaryExpression": assert ast.prefix is True and ast.operator == "-", "Only unary minus is supported." return JaniExpression( @@ -86,6 +98,18 @@ def _parse_ecmascript_to_jani_expression( "right": _parse_ecmascript_to_jani_expression(ast.argument, array_info), } ) + elif ast.type == "BinaryExpression" or ast.type == "LogicalExpression": + # It is a more complex expression + assert ( + ast.operator in OPERATORS_TO_JANI_MAP + ), f"ecmascript to jani expression: unknown operator {ast.operator}" + return JaniExpression( + { + "op": OPERATORS_TO_JANI_MAP[ast.operator], + "left": _parse_ecmascript_to_jani_expression(ast.left, array_info), + "right": _parse_ecmascript_to_jani_expression(ast.right, array_info), + } + ) elif ast.type == "ArrayExpression": assert array_info is not None, "Array info must be provided for ArrayExpressions." entry_type: Type = array_info.array_type @@ -105,13 +129,6 @@ def _parse_ecmascript_to_jani_expression( # Add dummy elements to make sure the full array is assigned elements_list.extend([entry_type(0)] * elements_to_add) return array_value_operator(elements_list) - elif ast.type == "Identifier": - # If it is an identifier, we do not need to expand further - assert ast.name not in ("True", "False"), ( - f"Boolean {ast.name} mistaken for an identifier. " - "Did you mean to use 'true' or 'false' instead?" - ) - return JaniExpression(ast.name) elif ast.type == "MemberExpression": object_expr = _parse_ecmascript_to_jani_expression(ast.object, array_info) if ast.computed: @@ -130,20 +147,6 @@ def _parse_ecmascript_to_jani_expression( ), "Dot notation can be used only to access object's members." field_complete_name = f"{object_expr_str}.{ast.property.name}" return JaniExpression(field_complete_name) - elif ast.type == "ExpressionStatement": - return _parse_ecmascript_to_jani_expression(ast.expression, array_info) - elif ast.type == "BinaryExpression": - # It is a more complex expression - assert ( - ast.operator in OPERATORS_TO_JANI_MAP - ), f"ecmascript to jani expression: unknown operator {ast.operator}" - return JaniExpression( - { - "op": OPERATORS_TO_JANI_MAP[ast.operator], - "left": _parse_ecmascript_to_jani_expression(ast.left, array_info), - "right": _parse_ecmascript_to_jani_expression(ast.right, array_info), - } - ) elif ast.type == "CallExpression": # We expect function calls to be of the form Math.function_name(args) (JavaScript-like) # The "." operator is represented as a MemberExpression diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py index 9d1ec412..3ba20ef4 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py @@ -53,6 +53,7 @@ ArrayInfo, parse_ecmascript_to_jani_expression, ) +from as2fm.scxml_converter.bt_converter import is_bt_root_scxml from as2fm.scxml_converter.scxml_entries import ( ScxmlAssign, ScxmlBase, @@ -533,6 +534,10 @@ def handle_entry_state(self): def add_unhandled_transitions(self): """Add self-loops in each state for transitions that weren't handled yet.""" + if is_bt_root_scxml(self.element.get_name()): + # The autogenerated BT Root should have no autogenerated empty self-loop. + # This prevents the global timer to advance uncontrolled without the BT being ticked + return transitions_set = set() for child in self.children: if isinstance(child, StateTag): diff --git a/src/as2fm/resources/bt_control_nodes/always_failure.scxml b/src/as2fm/resources/bt_control_nodes/always_failure.scxml new file mode 100644 index 00000000..ad8f88db --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/always_failure.scxml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="idle" + version="1.0" + name="AlwaysFailure" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/include/behaviortree_cpp_v3/actions/always_failure_node.h"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="idle"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count != 0" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="idle"> + <bt_return_status status="FAILURE" /> + </bt_tick> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/always_success.scxml b/src/as2fm/resources/bt_control_nodes/always_success.scxml new file mode 100644 index 00000000..533298bd --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/always_success.scxml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="idle" + version="1.0" + name="AlwaysSuccess" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/include/behaviortree_cpp_v3/actions/always_success_node.h"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="idle"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count != 0" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="idle"> + <bt_return_status status="SUCCESS" /> + </bt_tick> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/fallback.scxml b/src/as2fm/resources/bt_control_nodes/fallback.scxml new file mode 100644 index 00000000..eb521dc1 --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/fallback.scxml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="Fallback" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/fallback_node.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="current_child_idx" type="int8" expr="0" /> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count < 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_children"/> + </state> + + <state id="tick_children"> + <onentry> + <if cond="current_child_idx < children_count"> + <bt_tick_child id="current_child_idx"/> + </if> + </onentry> + <bt_child_status id="current_child_idx" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + <bt_child_status id="current_child_idx" cond="_bt.status == SUCCESS" target="wait_for_tick"> + <assign location="current_child_idx" expr="0" /> + <bt_return_status status="SUCCESS" /> + </bt_child_status> + <bt_child_status id="current_child_idx" cond="_bt.status == FAILURE" target="tick_children"> + <assign location="current_child_idx" expr="current_child_idx + 1" /> + </bt_child_status> + <!-- All children returned success --> + <transition target="wait_for_tick" cond="current_child_idx == children_count"> + <assign location="current_child_idx" expr="0" /> + <bt_return_status status="FAILURE" /> + </transition> + <!-- The current_child_idx is out of bounds --> + <transition target="error" cond="current_child_idx < 0 || current_child_idx > children_count" /> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/if_then_else.scxml b/src/as2fm/resources/bt_control_nodes/if_then_else.scxml new file mode 100644 index 00000000..7014fa3d --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/if_then_else.scxml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="IfThenElse" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/if_then_else_node.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="child_idx" type="int8" expr="0" /> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count < 2 || children_count > 3" /> + <!-- React to an incoming BT Tick --> + <bt_tick cond="child_idx == 0" target="tick_condition_child" /> + <bt_tick cond="child_idx > 0" target="tick_exec_child" /> + </state> + + <state id="tick_condition_child"> + <onentry> + <bt_tick_child id="0"/> + </onentry> + <transition target="error" cond="child_idx != 0" /> + <bt_child_status id="0" cond="_bt.status == SUCCESS" target="tick_exec_child"> + <assign location="child_idx" expr="1" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == FAILURE && children_count == 3" target="tick_exec_child"> + <assign location="child_idx" expr="2" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == FAILURE && children_count < 3" target="wait_for_tick"> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + </state> + + <state id="tick_exec_child"> + <onentry> + <bt_tick_child id="child_idx"/> + </onentry> + <transition target="error" cond="child_idx != 1 && child_idx != 2" /> + <transition target="error" cond="child_idx >= children_count" /> + <bt_child_status id="child_idx" cond="_bt.status == SUCCESS" target="wait_for_tick"> + <assign location="child_idx" expr="0" /> + <bt_return_status status="SUCCESS" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == FAILURE" target="wait_for_tick"> + <assign location="child_idx" expr="0" /> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/inverter.scxml b/src/as2fm/resources/bt_control_nodes/inverter.scxml new file mode 100644 index 00000000..96456b77 --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/inverter.scxml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="Inverter" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/decorators/inverter_node.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count != 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_child" /> + </state> + + <state id="tick_child"> + <onentry> + <bt_tick_child id="0"/> + </onentry> + <bt_child_status id="0" cond="_bt.status == SUCCESS" target="wait_for_tick"> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == FAILURE" target="wait_for_tick"> + <bt_return_status status="SUCCESS" /> + </bt_child_status> + <bt_child_status id="0" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/reactive_fallback.scxml b/src/as2fm/resources/bt_control_nodes/reactive_fallback.scxml new file mode 100644 index 00000000..e944075e --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/reactive_fallback.scxml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="ReactiveFallback" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/reactive_fallback.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="failure_count" type="int8" expr="0" /> + <data id="child_idx" type="int8" expr="0" /> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count < 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_children"> + <assign location="failure_count" expr="0" /> + <assign location="child_idx" expr="0" /> + </bt_tick> + </state> + + <state id="tick_children"> + <onentry> + <if cond="child_idx < children_count"> + <bt_tick_child id="child_idx"/> + </if> + </onentry> + <bt_child_status id="child_idx" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == FAILURE" target="tick_children"> + <assign location="failure_count" expr="failure_count + 1" /> + <assign location="child_idx" expr="child_idx + 1" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == SUCCESS" target="wait_for_tick"> + <bt_return_status status="SUCCESS" /> + </bt_child_status> + <!-- Exit conditions --> + <!-- All children returned success --> + <transition target="wait_for_tick" cond="failure_count == children_count"> + <bt_return_status status="FAILURE" /> + </transition> + <!-- We looped over all children, but not enough failures (will never happen) --> + <transition target="wait_for_tick" cond="child_idx == children_count"> + <bt_return_status status="RUNNING" /> + </transition> + <!-- The child_idx is out of bounds --> + <transition target="error" cond="child_idx < 0 || child_idx > children_count" /> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/reactive_sequence.scxml b/src/as2fm/resources/bt_control_nodes/reactive_sequence.scxml new file mode 100644 index 00000000..dd886cd2 --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/reactive_sequence.scxml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="ReactiveSequence" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/reactive_sequence.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="success_count" type="int8" expr="0" /> + <data id="child_idx" type="int8" expr="0" /> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count < 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_children"> + <assign location="success_count" expr="0" /> + <assign location="child_idx" expr="0" /> + </bt_tick> + </state> + + <state id="tick_children"> + <onentry> + <if cond="success_count < children_count"> + <bt_tick_child id="child_idx"/> + </if> + </onentry> + <bt_child_status id="child_idx" cond="_bt.status == SUCCESS" target="tick_children"> + <assign location="success_count" expr="success_count + 1" /> + <assign location="child_idx" expr="child_idx + 1" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == FAILURE" target="wait_for_tick"> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="child_idx" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + <!-- All children returned success --> + <transition target="wait_for_tick" cond="success_count == children_count"> + <bt_return_status status="SUCCESS" /> + </transition> + <!-- The child_idx is out of bounds --> + <transition target="error" cond="child_idx < 0 || child_idx >= children_count" /> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/resources/bt_control_nodes/sequence.scxml b/src/as2fm/resources/bt_control_nodes/sequence.scxml new file mode 100644 index 00000000..be076a5f --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/sequence.scxml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="wait_for_tick" + version="1.0" + name="Sequence" + model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/sequence_node.cpp"> + + <!-- A default BT port reporting the amount of children --> + <bt_declare_port_in key="CHILDREN_COUNT" type="int8" /> + + <datamodel> + <data id="current_child_idx" type="int8" expr="0" /> + <data id="children_count" type="int8"> + <expr> + <bt_get_input key="CHILDREN_COUNT" /> + </expr> + </data> + </datamodel> + + <state id="wait_for_tick"> + <!-- Check if the state is valid. If not, go to error and stop --> + <transition target="error" cond="children_count < 1" /> + <!-- React to an incoming BT Tick --> + <bt_tick target="tick_children"/> + </state> + + <state id="tick_children"> + <onentry> + <if cond="current_child_idx < children_count"> + <bt_tick_child id="current_child_idx"/> + </if> + </onentry> + <bt_child_status id="current_child_idx" cond="_bt.status == SUCCESS" target="tick_children"> + <assign location="current_child_idx" expr="current_child_idx + 1" /> + </bt_child_status> + <bt_child_status id="current_child_idx" cond="_bt.status == FAILURE" target="wait_for_tick"> + <assign location="current_child_idx" expr="0" /> + <bt_return_status status="FAILURE" /> + </bt_child_status> + <bt_child_status id="current_child_idx" cond="_bt.status == RUNNING" target="wait_for_tick"> + <bt_return_status status="RUNNING" /> + </bt_child_status> + <!-- All children returned success --> + <transition target="wait_for_tick" cond="current_child_idx == children_count"> + <assign location="current_child_idx" expr="0" /> + <bt_return_status status="SUCCESS" /> + </transition> + <!-- The current_child_idx is out of bounds --> + <transition target="error" cond="current_child_idx < 0 || current_child_idx > children_count" /> + </state> + + <!-- A state to transition to when something did not work --> + <state id="error" /> + +</scxml> diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 3dedf344..286aa2de 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -18,141 +18,149 @@ """ import os -import re from copy import deepcopy -from enum import Enum, auto -from typing import List +from importlib.resources import files as resource_files +from typing import Dict, List, Tuple -from btlib.bt_to_fsm.bt_to_fsm import Bt2FSM -from btlib.bts import xml_to_networkx -from btlib.common import NODE_CAT +from lxml import etree as ET from as2fm.scxml_converter.scxml_entries import ( - RESERVED_BT_PORT_NAMES, + BtChildStatus, + BtTickChild, RosRateCallback, RosTimeRate, ScxmlRoot, - ScxmlSend, ScxmlState, - ScxmlTransition, ) +BT_ROOT_PREFIX = "bt_root_fsm_" -class BT_EVENT_TYPE(Enum): - """Event types for Behavior Tree.""" - TICK = auto() - SUCCESS = auto() - FAILURE = auto() - RUNNING = auto() - - @staticmethod - def from_str(event_name: str) -> "BT_EVENT_TYPE": - event_name = event_name.replace("event=", "") - event_name = event_name.replace('"', "") - event_name = event_name.replace("bt_", "") - return BT_EVENT_TYPE[event_name.upper()] +def is_bt_root_scxml(scxml_name: str) -> bool: + """ + Check if the SCXML name matches with the BT root SCXML name pattern. + """ + return scxml_name.startswith(BT_ROOT_PREFIX) -def bt_event_name(node_id: str, event_type: BT_EVENT_TYPE) -> str: - """Return the event name for the given node and event type.""" - return f"bt_{node_id}_{event_type.name.lower()}" +def load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, ScxmlRoot]: + available_bt_plugins = {} + for path in bt_plugins_scxml_paths: + assert os.path.exists(path), f"SCXML must exist. {path} not found." + bt_plugin_scxml = ScxmlRoot.from_scxml_file(path) + available_bt_plugins.update({bt_plugin_scxml.get_name(): bt_plugin_scxml}) + internal_bt_plugins_path = ( + resource_files("as2fm").joinpath("resources").joinpath("bt_control_nodes") + ) + for plugin_path in internal_bt_plugins_path.iterdir(): + if plugin_path.is_file() and plugin_path.suffix == ".scxml": + bt_plugin_scxml = ScxmlRoot.from_scxml_file(str(plugin_path)) + available_bt_plugins.update({bt_plugin_scxml.get_name(): bt_plugin_scxml}) + return available_bt_plugins def bt_converter( bt_xml_path: str, bt_plugins_scxml_paths: List[str], bt_tick_rate: float ) -> List[ScxmlRoot]: """ - Convert a Behavior Tree (BT) in XML format to SCXML. + Generate all Scxml files resulting from a Behavior Tree (BT) in XML format. + """ + available_bt_plugins = load_available_bt_plugins(bt_plugins_scxml_paths) + xml_tree: ET.ElementBase = ET.parse(bt_xml_path, ET.XMLParser(remove_comments=True)).getroot() + root_children = xml_tree.getchildren() + assert len(root_children) == 1, f"Error: Expected one root element, found {len(root_children)}." + assert ( + root_children[0].tag == "BehaviorTree" + ), f"Error: Expected BehaviorTree root, found {root_children[0].tag}." + bt_children = root_children[0].getchildren() + assert ( + len(bt_children) == 1 + ), f"Error: Expected one BehaviorTree child, found {len(bt_children)}." + root_child_tick_idx = 1000 + bt_name = os.path.basename(bt_xml_path).replace(".xml", "") + bt_scxml_root = generate_bt_root_scxml(bt_name, root_child_tick_idx, bt_tick_rate) + generated_scxmls = [bt_scxml_root] + generate_bt_children_scxmls( + bt_children[0], root_child_tick_idx, available_bt_plugins + ) + return generated_scxmls - Args: - bt_xml_path: The path to the Behavior Tree in XML format. - bt_plugins_scxml_paths: The paths to the SCXML files of BT plugins. - bt_tick_rate: The rate at which the BT should tick. - Returns: - A list of the generated SCXML objects. +def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> ScxmlRoot: + """ + Generate the root SCXML for a Behavior Tree. + """ + bt_scxml_root = ScxmlRoot(BT_ROOT_PREFIX + scxml_name) + ros_rate_decl = RosTimeRate(f"{scxml_name}_tick", tick_rate) + bt_scxml_root.add_ros_declaration(ros_rate_decl) + idle_state = ScxmlState( + "idle", body=[RosRateCallback(ros_rate_decl, "wait_tick_res", None, [BtTickChild(0)])] + ) + wait_res_state = ScxmlState( + "wait_tick_res", + body=[ + # If we allow timer ticks here, the automata will generate timer callbacks and make the + # BT automaton transition to error state (since our concept of time is not real). + # RosRateCallback(ros_rate_decl, "error"), + BtChildStatus(0, "idle") + ], + ) + error_state = ScxmlState("error") + bt_scxml_root.add_state(idle_state, initial=True) + bt_scxml_root.add_state(wait_res_state) + bt_scxml_root.add_state(error_state) + # The BT root's ID is set to -1 (unused anyway) + bt_scxml_root.set_bt_plugin_id(-1) + bt_scxml_root.append_bt_child_id(tick_id) + bt_scxml_root.instantiate_bt_information() + return bt_scxml_root + + +def get_bt_plugin_type(bt_xml_subtree: ET.ElementBase) -> str: """ - bt_graph, _ = xml_to_networkx(bt_xml_path) + Get the BT plugin node type from the XML subtree. + """ + plugin_type = bt_xml_subtree.tag + assert plugin_type not in ( + "BehaviorTree", + "SubTree", # SubTrees support will be integrated later on + "root", + ), f"Error: Unexpected BT plugin tag {plugin_type}." + if plugin_type in ("Condition", "Action"): + plugin_type = bt_xml_subtree.attrib["ID"] + return plugin_type + + +def get_bt_child_ports(bt_xml_subtree: ET.ElementBase) -> List[Tuple[str, str]]: + """ + Get the ports of a BT child node. + """ + ports = [(attr_key, attr_value) for attr_key, attr_value in bt_xml_subtree.attrib.items()] + return ports - bt_plugins_scxmls = {} - for path in bt_plugins_scxml_paths: - assert os.path.exists(path), f"SCXML must exist. {path} not found." - bt_plugin_scxml = ScxmlRoot.from_scxml_file(path) - bt_plugin_name = bt_plugin_scxml.get_name() - assert ( - bt_plugin_name not in bt_plugins_scxmls - ), f"Plugin name must be unique. {bt_plugin_name} already exists." - bt_plugins_scxmls[bt_plugin_name] = bt_plugin_scxml - leaf_node_ids = [] +def generate_bt_children_scxmls( + bt_xml_subtree: ET.ElementBase, + subtree_tick_idx: int, + available_bt_plugins: Dict[str, ScxmlRoot], +) -> List[ScxmlRoot]: + """ + Generate the SCXML files for the children of a Behavior Tree. + """ generated_scxmls: List[ScxmlRoot] = [] - # Generate the instances of the plugins used in the BT - for node in bt_graph.nodes: - assert "category" in bt_graph.nodes[node], "Node must have a category." - if bt_graph.nodes[node]["category"] == NODE_CAT.LEAF: - leaf_node_ids.append(node) - assert "ID" in bt_graph.nodes[node], "Leaf node must have a type." - node_type = bt_graph.nodes[node]["ID"] - node_id = node - assert ( - node_type in bt_plugins_scxmls - ), f"Leaf node must have a plugin. {node_type} not found." - instance_name = f"{node_id}_{node_type}" - scxml_plugin_instance: ScxmlRoot = deepcopy(bt_plugins_scxmls[node_type]) - scxml_plugin_instance.set_name(instance_name) - scxml_plugin_instance.instantiate_bt_events(node_id) - bt_ports = [ - (p_name, p_value) - for p_name, p_value in bt_graph.nodes[node].items() - if p_name not in RESERVED_BT_PORT_NAMES - ] - scxml_plugin_instance.set_bt_ports_values(bt_ports) - scxml_plugin_instance.update_bt_ports_values() - assert ( - scxml_plugin_instance.check_validity() - ), f"Error: SCXML plugin instance {instance_name} is not valid." - generated_scxmls.append(scxml_plugin_instance) - # Generate the BT SCXML - fsm_graph = Bt2FSM(bt_graph).convert() - bt_scxml_root = ScxmlRoot("bt") - name_with_id_pattern = re.compile(r"[0-9]+_.+") - for node in fsm_graph.nodes: - state = ScxmlState(node) - node_id = None - if name_with_id_pattern.match(node): - node_id = int(node.split("_")[0]) - if node_id in leaf_node_ids: - state.append_on_entry(ScxmlSend(bt_event_name(node_id, BT_EVENT_TYPE.TICK))) - for edge in fsm_graph.edges(node): - target = edge[1] - transition = ScxmlTransition(target) - if node_id is not None and node_id in leaf_node_ids: - if "label" not in fsm_graph.edges[edge]: - continue - label = fsm_graph.edges[edge]["label"] - if label == "on_success": - event_type = BT_EVENT_TYPE.SUCCESS - elif label == "on_failure": - event_type = BT_EVENT_TYPE.FAILURE - elif label == "on_running": - event_type = BT_EVENT_TYPE.RUNNING - else: - raise ValueError(f"Invalid label: {label}") - event_name = bt_event_name(node_id, event_type) - transition.add_event(event_name) - state.add_transition(transition) - if node in ["success", "failure", "running"]: - state.add_transition(ScxmlTransition("wait_for_tick")) - bt_scxml_root.add_state(state) - # TODO: Make BT rate configurable, e.g. from main.xml - rtr = RosTimeRate("bt_tick", bt_tick_rate) - bt_scxml_root.add_ros_declaration(rtr) - - wait_for_tick = ScxmlState("wait_for_tick") - wait_for_tick.add_transition(RosRateCallback(rtr, "tick")) - bt_scxml_root.add_state(wait_for_tick, initial=True) - assert bt_scxml_root.check_validity(), "Error: SCXML root tag is not valid." - generated_scxmls.append(bt_scxml_root) - + plugin_type = get_bt_plugin_type(bt_xml_subtree) + assert ( + plugin_type in available_bt_plugins + ), f"Error: BT plugin {plugin_type} not found. Available plugins: {available_bt_plugins.keys()}" + bt_plugin_scxml = deepcopy(available_bt_plugins[plugin_type]) + bt_plugin_scxml.set_name(f"{subtree_tick_idx}_{plugin_type}") + bt_plugin_scxml.set_bt_plugin_id(subtree_tick_idx) + bt_plugin_scxml.set_bt_ports_values(get_bt_child_ports(bt_xml_subtree)) + generated_scxmls.append(bt_plugin_scxml) + next_tick_idx = subtree_tick_idx + 1 + for child in bt_xml_subtree.getchildren(): + bt_plugin_scxml.append_bt_child_id(next_tick_idx) + child_scxmls = generate_bt_children_scxmls(child, next_tick_idx, available_bt_plugins) + generated_scxmls.extend(child_scxmls) + next_tick_idx = generated_scxmls[-1].get_bt_plugin_id() + 1 + bt_plugin_scxml.instantiate_bt_information() return generated_scxmls diff --git a/src/as2fm/scxml_converter/scxml_entries/__init__.py b/src/as2fm/scxml_converter/scxml_entries/__init__.py index 76c393ed..ec1f299c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/__init__.py +++ b/src/as2fm/scxml_converter/scxml_entries/__init__.py @@ -3,8 +3,9 @@ from .scxml_base import ScxmlBase # noqa: F401 from .utils import CallbackType # noqa: F401 from .bt_utils import RESERVED_BT_PORT_NAMES # noqa: F401 -from .scxml_bt import ( # noqa: F401 +from .scxml_bt_ports import ( # noqa: F401 BtInputPortDeclaration, + BtPortDeclarations, BtOutputPortDeclaration, BtGetValueInputPort, ) # noqa: F401 @@ -24,6 +25,7 @@ instantiate_exec_body_bt_events, ) # noqa: F401 from .scxml_transition import ScxmlTransition # noqa: F401 +from .scxml_bt_ticks import BtTick, BtTickChild, BtChildStatus, BtReturnStatus # noqa: F401 from .scxml_state import ScxmlState # noqa: F401 from .scxml_ros_timer import RosTimeRate, RosRateCallback # noqa: F401 from .scxml_ros_topic import ( # noqa: F401 diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index cdc40250..980496ae 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -16,6 +16,7 @@ """Collection of SCXML utilities related to BT functionalities.""" import re +from enum import Enum, auto from typing import Dict, Tuple, Type from as2fm.scxml_converter.scxml_entries.utils import SCXML_DATA_STR_TO_TYPE @@ -24,7 +25,40 @@ VALID_BT_OUTPUT_PORT_TYPES: Dict[str, Type] = SCXML_DATA_STR_TO_TYPE """List of keys that are not going to be read as BT ports from the BT XML definition.""" -RESERVED_BT_PORT_NAMES = ["NAME", "ID", "category"] +RESERVED_BT_PORT_NAMES = ["ID", "name"] + + +class BtResponse(Enum): + """Enumeration of possible BT responses.""" + + SUCCESS = auto() + FAILURE = auto() + RUNNING = auto() + + @staticmethod + def str_to_int(resp_str: str) -> int: + """Convert the BT response to an integer.""" + for response in BtResponse: + if response.name == resp_str: + return response.value + raise ValueError(f"Error: {resp_str} is an invalid BT Status type.") + + @staticmethod + def process_expr(expr: str) -> str: + """Substitute occurrences of BT responses in the expression.""" + for response in BtResponse: + expr = re.sub(rf"{response.name}", f"{response.value}", expr) + return expr + + +def generate_bt_tick_event(instance_id: str) -> str: + """Generate the BT tick event name for a given BT node instance.""" + return f"bt_{instance_id}_tick" + + +def generate_bt_response_event(instance_id: str) -> str: + """Generate the BT response event name for a given BT node instance.""" + return f"bt_{instance_id}_response" def is_bt_event(event_name: str) -> bool: @@ -54,9 +88,11 @@ class BtPortsHandler: @staticmethod def check_port_name_allowed(port_name: str) -> None: """Check if the port name is allowed.""" - assert ( - port_name not in RESERVED_BT_PORT_NAMES - ), f"Error: Port name {port_name} is reserved in BT" + # All port IDs are valid + pass + # assert ( + # port_name not in RESERVED_BT_PORT_NAMES + # ), f"Error: Port name {port_name} is reserved in BT" def __init__(self): # For each port name, store the port type string and value. @@ -128,8 +164,8 @@ def set_port_value(self, port_name: str, port_value: str) -> None: elif self.out_port_exists(port_name): self._set_out_port_value(port_name, port_value) else: - # The 'name' port can be set even if undeclared, since it defines the node name in BT. - if port_name != "name": + # The reserved port IDs can be set in the bt.xml even if they are unused in the plugin + if port_name not in RESERVED_BT_PORT_NAMES: raise RuntimeError(f"Error: Port {port_name} is not declared.") def _set_in_port_value(self, port_name: str, port_value: str): diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py similarity index 97% rename from src/as2fm/scxml_converter/scxml_entries/scxml_bt.py rename to src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py index 662c24a0..58d00570 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py @@ -14,7 +14,7 @@ # limitations under the License. """ -SCXML entries related to Behavior Trees. +SCXML entries related to Behavior Trees' Ports. """ from typing import Union @@ -138,7 +138,7 @@ def get_key_name(self) -> str: def as_plain_scxml(self, _) -> ScxmlBase: # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot - raise RuntimeError("Error: SCXML BT Ports declarations cannot be converted to plain SCXML.") + raise RuntimeError("Error: SCXML BT Port value getter cannot be converted to plain SCXML.") def as_xml(self) -> ET.Element: assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py new file mode 100644 index 00000000..a0ef8d81 --- /dev/null +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -0,0 +1,285 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# 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. + +""" +SCXML entries related to Behavior Tree Ticks and related responses. +""" + +from typing import List, Optional, Type, Union + +from lxml import etree as ET + +from as2fm.scxml_converter.scxml_entries import ( + ScxmlBase, + ScxmlExecutionBody, + ScxmlIf, + ScxmlParam, + ScxmlSend, + ScxmlTransition, + execution_body_from_xml, + instantiate_exec_body_bt_events, +) +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BtResponse, + generate_bt_response_event, + generate_bt_tick_event, +) +from as2fm.scxml_converter.scxml_entries.utils import CallbackType, get_plain_expression, to_integer +from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument + + +def _process_child_seq_id( + scxml_type: Type[ScxmlBase], child_seq_id: Union[str, int] +) -> Union[str, int]: + """ + Convert the child sequence ID to int or string depending on the content. + """ + if isinstance(child_seq_id, int): + return child_seq_id + elif isinstance(child_seq_id, str): + child_seq_id = child_seq_id.strip() + int_seq_id = to_integer(scxml_type, "id", child_seq_id) + if int_seq_id is not None: + return int_seq_id + assert ( + child_seq_id.isidentifier() + ), f"Error: {scxml_type.get_tag_name()}: invalid child seq id name '{child_seq_id}'." + return child_seq_id + raise TypeError( + f"Error: {scxml_type.get_tag_name()}: invalid child seq id type '{type(child_seq_id)}'." + ) + + +class BtTick(ScxmlTransition): + """ + Process a BT plugin/control node tick, triggering the related transition. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_tick" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtTick": + assert_xml_tag_ok(BtTick, xml_tree) + target: str = get_xml_argument(BtTick, xml_tree, "target") + condition: Optional[str] = get_xml_argument(BtTick, xml_tree, "cond", none_allowed=True) + body = execution_body_from_xml(xml_tree) + return BtTick(target, condition, body) + + def __init__( + self, + target: str, + condition: Optional[str] = None, + body: Optional[ScxmlExecutionBody] = None, + ): + super().__init__(target, ["bt_tick"], condition, body) + + def check_validity(self) -> bool: + return super().check_validity() + + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int] + ) -> List[ScxmlTransition]: + self._events = [generate_bt_tick_event(instance_id)] + instantiate_exec_body_bt_events(self._body, instance_id, children_ids) + return [ScxmlTransition(self._target, self._events, self._condition, self._body)] + + def as_xml(self) -> ET.Element: + xml_bt_tick = ET.Element(BtTick.get_tag_name(), {"target": self._target}) + if self._condition is not None: + xml_bt_tick.set("cond", self._condition) + if self._body is not None: + for executable_entry in self._body: + xml_bt_tick.append(executable_entry.as_xml()) + return xml_bt_tick + + +class BtTickChild(ScxmlSend): + """Tick one child of a control node.""" + + @staticmethod + def get_tag_name() -> str: + return "bt_tick_child" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtTickChild": + assert_xml_tag_ok(BtTickChild, xml_tree) + # Proposal: to avoid confusion, we could name the xml argument seq_id, too + # child_seq_id = n -> the n-th children of the control node in the BT XML + child_seq_id: str = get_xml_argument(BtTickChild, xml_tree, "id") + return BtTickChild(child_seq_id) + + def __init__(self, child_seq_id: Union[str, int]): + """ + Generate a new BtTickChild instance. + + :param child_seq_id: Which BT control node children to tick (relative the the BT-XML file). + """ + self._child_seq_id = _process_child_seq_id(BtTickChild, child_seq_id) + + def check_validity(self) -> bool: + return True + + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int] + ) -> Union[ScxmlIf, ScxmlSend]: + """ + Convert the BtTickChild to ScxmlSend if the child id is constant and an ScxmlIf otherwise. + """ + if isinstance(self._child_seq_id, int): + # We know the exact child ID we want to tick + assert self._child_seq_id < len(children_ids), ( + f"Error: SCXML BT Tick Child: invalid child ID {self._child_seq_id} " + f"for {len(children_ids)} children." + ) + return ScxmlSend(generate_bt_tick_event(children_ids[self._child_seq_id])) + else: + # The children to tick depends on the index of the self._child variable at runtime + if_bodies = [] + for child_seq_n, child_id in enumerate(children_ids): + if_bodies.append( + ( + f"{self._child_seq_id} == {child_seq_n}", + [ScxmlSend(generate_bt_tick_event(child_id))], + ) + ) + return ScxmlIf(if_bodies).instantiate_bt_events(instance_id, children_ids) + + def as_xml(self) -> ET.Element: + xml_bt_tick_child = ET.Element(BtTickChild.get_tag_name(), {"id": str(self._child_seq_id)}) + return xml_bt_tick_child + + +class BtChildStatus(ScxmlTransition): + """ + Process the response received from a BT child. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_child_status" + + @staticmethod + def from_xml_tree(xml_tree): + assert_xml_tag_ok(BtChildStatus, xml_tree) + # Same as in BtTickChild + child_seq_id = get_xml_argument(BtChildStatus, xml_tree, "id") + target = get_xml_argument(BtChildStatus, xml_tree, "target") + condition = get_xml_argument(BtChildStatus, xml_tree, "cond", none_allowed=True) + body = execution_body_from_xml(xml_tree) + return BtChildStatus(child_seq_id, target, condition, body) + + def __init__( + self, + child_seq_id: Union[str, int], + target: str, + condition: Optional[str] = None, + body: Optional[ScxmlExecutionBody] = None, + ): + """ + Generate a BtChildStatus instance. + + :param child_seq_id: Which BT control node children to tick (relative the the BT-XML file). + :param target: The target state to transition to. + :param condition: The condition to check before transitioning. + :param body: The body to execute before the transition. + """ + self._child_seq_id = _process_child_seq_id(BtChildStatus, child_seq_id) + self._target = target + self._condition = condition + if self._condition is not None: + # Substitute the responses string with the corresponding integer + self._condition = BtResponse.process_expr(self._condition) + self._body = body + + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int] + ) -> List[ScxmlTransition]: + plain_cond_expr = None + if self._condition is not None: + plain_cond_expr = get_plain_expression(self._condition, CallbackType.BT_RESPONSE) + if isinstance(self._child_seq_id, int): + # Handling specific child seq. ID, return a single transition + assert self._child_seq_id < len(children_ids), ( + f"Error: SCXML BT Child Status: invalid child seq. ID {self._child_seq_id} " + f"for {len(children_ids)} children." + ) + target_child_id = children_ids[self._child_seq_id] + return ScxmlTransition( + self._target, + [generate_bt_response_event(target_child_id)], + plain_cond_expr, + self._body, + ).instantiate_bt_events(instance_id, children_ids) + else: + # Handling a generic child ID, return a transition for each child + condition_prefix = "" if plain_cond_expr is None else f"({plain_cond_expr}) && " + generated_transitions = [] + for child_seq_n, child_id in enumerate(children_ids): + generated_transition = ScxmlTransition( + self._target, + [generate_bt_response_event(child_id)], + condition_prefix + f"({self._child_seq_id} == {child_seq_n})", + self._body, + ).instantiate_bt_events(instance_id, children_ids) + assert ( + len(generated_transition) == 1 + ), "Error: SCXML BT Child Status: Expected a single transition." + generated_transitions.append(generated_transition[0]) + return generated_transitions + + def as_xml(self) -> ET.Element: + xml_bt_child_status = ET.Element( + BtChildStatus.get_tag_name(), {"id": str(self._child_seq_id), "target": self._target} + ) + if self._condition is not None: + xml_bt_child_status.set("cond", self._condition) + if self._body is not None: + for executable_entry in self._body: + xml_bt_child_status.append(executable_entry.as_xml()) + return xml_bt_child_status + + +class BtReturnStatus(ScxmlSend): + """ + Send a status response to a BT parent node. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_return_status" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtReturnStatus": + assert_xml_tag_ok(BtReturnStatus, xml_tree) + status = get_xml_argument(BtReturnStatus, xml_tree, "status") + return BtReturnStatus(status) + + def __init__(self, status: str): + self._status: str = status + self._status_id: int = BtResponse.str_to_int(status) + + def check_validity(self) -> bool: + return True + + def instantiate_bt_events(self, instance_id: int, _) -> ScxmlSend: + return ScxmlSend( + generate_bt_response_event(instance_id), + [ScxmlParam("status", expr=f"{self._status_id}")], + ) + + def as_xml(self) -> ET.Element: + return ET.Element(BtReturnStatus.get_tag_name(), {"status": self._status}) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index f0843409..cd657312 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -17,6 +17,7 @@ Definition of SCXML Tags that can be part of executable content """ +import warnings from typing import Dict, List, Optional, Tuple, Union, get_args from lxml import etree as ET @@ -28,11 +29,7 @@ ScxmlParam, ScxmlRosDeclarationsContainer, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import ( - BtPortsHandler, - is_bt_event, - replace_bt_event, -) +from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_bt_event from as2fm.scxml_converter.scxml_entries.utils import ( CallbackType, get_plain_expression, @@ -51,7 +48,7 @@ def instantiate_exec_body_bt_events( - exec_body: Optional[ScxmlExecutionBody], instance_id: str + exec_body: Optional[ScxmlExecutionBody], instance_id: int, children_ids: List[int] ) -> None: """ Instantiate the behavior tree events in the execution body. @@ -60,8 +57,10 @@ def instantiate_exec_body_bt_events( :param instance_id: The instance ID of the BT node """ if exec_body is not None: - for entry in exec_body: - entry.instantiate_bt_events(instance_id) + for id in range(len(exec_body)): + entry = exec_body[id].instantiate_bt_events(instance_id, children_ids) + assert entry is not None, f"Error instantiating BT events in {exec_body[id]}: got None." + exec_body[id] = entry def update_exec_body_bt_ports_values( @@ -153,11 +152,12 @@ def get_else_execution(self) -> ScxmlExecutionBody: """Get the else execution.""" return self._else_execution - def instantiate_bt_events(self, instance_id: str) -> None: + def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> "ScxmlIf": """Instantiate the behavior tree events in the If action, if available.""" for _, exec_body in self._conditional_executions: - instantiate_exec_body_bt_events(exec_body, instance_id) - instantiate_exec_body_bt_events(self._else_execution, instance_id) + instantiate_exec_body_bt_events(exec_body, instance_id, children_ids) + instantiate_exec_body_bt_events(self._else_execution, instance_id, children_ids) + return self def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): for _, exec_body in self._conditional_executions: @@ -285,12 +285,28 @@ def get_params(self) -> List[ScxmlParam]: """Get the parameters to send.""" return self._params - def instantiate_bt_events(self, instance_id: str) -> None: + def instantiate_bt_events(self, instance_id: int, _) -> "ScxmlSend": """Instantiate the behavior tree events in the send action, if available.""" + # Support for deprecated BT events handling. Remove the whole if block once transition done. + from as2fm.scxml_converter.scxml_entries.scxml_bt_ticks import BtReturnStatus + # Make sure this method is executed only on ScxmlSend objects, and not on derived classes if type(self) is ScxmlSend and is_bt_event(self._event): + warnings.warn( + "Deprecation warning: BT events should not be found in SCXML send. " + "Use the 'bt_return_status' ROS-scxml tag instead.", + DeprecationWarning, + ) # Those are expected to be only bt_success, bt_failure and bt_running - self._event = replace_bt_event(self._event, instance_id) + event_to_status = { + "bt_success": "SUCCESS", + "bt_failure": "FAILURE", + "bt_running": "RUNNING", + } + return BtReturnStatus(event_to_status[self._event]).instantiate_bt_events( + instance_id, [] + ) + return self def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): """Update the values of potential entries making use of BT ports.""" @@ -306,7 +322,7 @@ def check_validity(self) -> bool: if not valid_event: print("Error: SCXML send: event is not valid.") if not valid_params: - print("Error: SCXML send: one or more param entries are not valid.") + print(f"Error: SCXML send: one or more param invalid entries of event '{self._event}'.") return valid_event and valid_params def check_valid_ros_instantiations(self, _) -> bool: @@ -378,9 +394,9 @@ def get_expr(self) -> Union[str, BtGetValueInputPort]: """Get the expression to assign.""" return self._expr - def instantiate_bt_events(self, _) -> None: + def instantiate_bt_events(self, _, __) -> "ScxmlAssign": """This functionality is not needed in this class.""" - return + return self def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" @@ -466,6 +482,9 @@ def execution_entry_from_xml(xml_tree: ET.Element) -> ScxmlExecutableEntry: tag_to_cls: Dict[str, ScxmlExecutableEntry] = { cls.get_tag_name(): cls for cls in _ResolvedScxmlExecutableEntry } + tag_to_cls.update( + {cls.get_tag_name(): cls for cls in ScxmlSend.__subclasses__() if cls != RosTrigger} + ) tag_to_cls.update({cls.get_tag_name(): cls for cls in RosTrigger.__subclasses__()}) exec_tag = xml_tree.tag assert ( diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 25b52fb1..fe425aa4 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -27,6 +27,7 @@ from as2fm.scxml_converter.scxml_entries import ( BtInputPortDeclaration, BtOutputPortDeclaration, + BtPortDeclarations, RosActionThread, ScxmlBase, ScxmlDataModel, @@ -34,7 +35,6 @@ ScxmlState, ) from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler -from as2fm.scxml_converter.scxml_entries.scxml_bt import BtPortDeclarations from as2fm.scxml_converter.scxml_entries.scxml_ros_base import RosDeclaration from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import ( @@ -125,6 +125,8 @@ def __init__(self, name: str): self._data_model: Optional[ScxmlDataModel] = None self._ros_declarations: List[RosDeclaration] = [] self._bt_ports_handler = BtPortsHandler() + self._bt_plugin_id: Optional[int] = None + self._bt_children_ids: List[int] = [] self._additional_threads: List[RosActionThread] = [] def get_name(self) -> str: @@ -153,10 +155,13 @@ def get_state_by_id(self, state_id: str) -> Optional[ScxmlState]: return state return None - def instantiate_bt_events(self, instance_id: str) -> None: + def set_bt_plugin_id(self, instance_id: int) -> None: """Update all BT-related events to use the assigned instance ID.""" - for state in self._states: - state.instantiate_bt_events(instance_id) + self._bt_plugin_id = instance_id + + def get_bt_plugin_id(self) -> Optional[int]: + """Get the ID of the BT plugin instance, if any.""" + return self._bt_plugin_id def add_state(self, state: ScxmlState, *, initial: bool = False): """Append a state to the list of states in the SCXML model. @@ -207,8 +212,18 @@ def set_bt_ports_values(self, ports_values: List[Tuple[str, str]]): for port_name, port_value in ports_values: self.set_bt_port_value(port_name, port_value) - def update_bt_ports_values(self): - """Update the values of the declared BT ports in the SCXML object.""" + def append_bt_child_id(self, child_id: int): + """Append a child ID to the list of child IDs.""" + assert isinstance(child_id, int), "Error: SCXML root: invalid child ID type." + self._bt_children_ids.append(child_id) + + def instantiate_bt_information(self): + """Instantiate the values of BT ports and children IDs in the SCXML entries.""" + n_bt_children = len(self._bt_children_ids) + assert self._bt_plugin_id is not None, "Error: SCXML root: BT plugin ID not set." + # Automatically add the correct amount of children to the specific port + if self._bt_ports_handler.in_port_exists("CHILDREN_COUNT"): + self._bt_ports_handler.set_port_value("CHILDREN_COUNT", str(n_bt_children)) if self._data_model is not None: self._data_model.update_bt_ports_values(self._bt_ports_handler) for ros_decl_scxml in self._ros_declarations: @@ -216,6 +231,7 @@ def update_bt_ports_values(self): for scxml_thread in self._additional_threads: scxml_thread.update_bt_ports_values(self._bt_ports_handler) for state in self._states: + state.instantiate_bt_events(self._bt_plugin_id, self._bt_children_ids) state.update_bt_ports_values(self._bt_ports_handler) def _generate_ros_declarations_helper(self) -> Optional[ScxmlRosDeclarationsContainer]: @@ -241,14 +257,14 @@ def check_validity(self) -> bool: for scxml_thread in self._additional_threads ) if not valid_data_model: - print("Error: SCXML root: datamodel is not valid.") + print(f"Error: SCXML root({self._name}): datamodel is not valid.") if not valid_states: - print("Error: SCXML root: states are not valid.") + print(f"Error: SCXML root({self._name}): states are not valid.") if not valid_threads: - print("Error: SCXML root: additional threads are not valid.") + print(f"Error: SCXML root({self._name}): additional threads are not valid.") valid_ros = self._check_valid_ros_declarations() if not valid_ros: - print("Error: SCXML root: ROS declarations are not valid.") + print(f"Error: SCXML root({self._name}): ROS declarations are not valid.") return ( valid_name and valid_initial_state and valid_states and valid_data_model and valid_ros ) @@ -274,8 +290,9 @@ def _check_valid_ros_declarations(self) -> bool: def is_plain_scxml(self) -> bool: """Check whether there are ROS specific features or all entries are plain SCXML.""" assert self.check_validity(), "SCXML: found invalid root object." - # If this is a valid scxml object, just check the absence of ROS and thread declarations - return len(self._ros_declarations) == 0 and len(self._additional_threads) == 0 + has_ros_entries = len(self._ros_declarations) > 0 or len(self._additional_threads) > 0 + has_bt_entries = any(state.has_bt_tick_transitions() for state in self._states) + return not (has_ros_entries or has_bt_entries) def to_plain_scxml_and_declarations( self, diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py index 34d849f8..61d43b6c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py @@ -135,9 +135,9 @@ def check_validity(self) -> bool: ) return valid_name and valid_accept and valid_reject - def instantiate_bt_events(self, _: str): - # We do not expect a body with BT ports to be substituted - pass + def instantiate_bt_events(self, _, __) -> List["RosActionHandleGoalResponse"]: + # We do not expect a body with BT events requiring substitutions + return [self] def update_bt_ports_values(self, _) -> None: """Update the values of potential entries making use of BT ports.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index cb71c616..90dea72d 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -23,6 +23,7 @@ from as2fm.as2fm_common.common import is_comment from as2fm.scxml_converter.scxml_entries import ( + BtTick, ScxmlBase, ScxmlExecutableEntry, ScxmlExecutionBody, @@ -141,12 +142,18 @@ def set_thread_id(self, thread_idx: int): if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_idx) - def instantiate_bt_events(self, instance_id: str) -> None: + def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> None: """Instantiate the BT events in all entries belonging to a state.""" + instantiated_transitions: List[ScxmlTransition] = [] for transition in self._body: - transition.instantiate_bt_events(instance_id) - instantiate_exec_body_bt_events(self._on_entry, instance_id) - instantiate_exec_body_bt_events(self._on_exit, instance_id) + new_transitions = transition.instantiate_bt_events(instance_id, children_ids) + assert isinstance(new_transitions, list) and all( + isinstance(t, ScxmlTransition) for t in new_transitions + ), f"Error: SCXML state {self._id}: found invalid transition in state body." + instantiated_transitions.extend(new_transitions) + self._body = instantiated_transitions + instantiate_exec_body_bt_events(self._on_entry, instance_id, children_ids) + instantiate_exec_body_bt_events(self._on_exit, instance_id, children_ids) def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" @@ -216,6 +223,10 @@ def _check_valid_ros_instantiations( entry.check_valid_ros_instantiations(ros_declarations) for entry in body ) + def has_bt_tick_transitions(self) -> bool: + """Check if the state has BT tick transitions.""" + return any(isinstance(entry, BtTick) for entry in self._body) + def as_plain_scxml(self, ros_declarations: ScxmlRosDeclarationsContainer) -> "ScxmlState": """Convert the ROS-specific entries to be plain SCXML""" set_execution_body_callback_type(self._on_entry, CallbackType.STATE) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index 7bb65c89..84f14e60 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -17,6 +17,7 @@ A single transition in SCXML. In XML, it has the tag `transition`. """ +import warnings from typing import List, Optional from lxml import etree as ET @@ -27,11 +28,7 @@ ScxmlExecutionBody, ScxmlRosDeclarationsContainer, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import ( - BtPortsHandler, - is_bt_event, - replace_bt_event, -) +from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_bt_event from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import ( execution_body_from_xml, instantiate_exec_body_bt_events, @@ -39,7 +36,11 @@ valid_execution_body, valid_execution_body_entry_types, ) -from as2fm.scxml_converter.scxml_entries.utils import CallbackType, get_plain_expression +from as2fm.scxml_converter.scxml_entries.utils import ( + CallbackType, + get_plain_expression, + is_non_empty_string, +) class ScxmlTransition(ScxmlBase): @@ -116,16 +117,32 @@ def get_executable_body(self) -> ScxmlExecutionBody: """Return the executable content of this transition.""" return self._body if self._body is not None else [] - def instantiate_bt_events(self, instance_id: str): + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int] + ) -> List["ScxmlTransition"]: """Instantiate the BT events of this transition.""" + # Old handling of BT events is deprecated: remove this if block after support removed + from as2fm.scxml_converter.scxml_entries.scxml_bt_ticks import BtTick + # Make sure to replace received events only for ScxmlTransition objects. if type(self) is ScxmlTransition: - for event_id, event_str in enumerate(self._events): + for event_str in self._events: # Those are expected to be only ticks if is_bt_event(event_str): - self._events[event_id] = replace_bt_event(event_str, instance_id) + warnings.warn( + "Deprecation warning: BT events should not be found in SCXML transitions. " + "Use the 'bt_tick' ROS-scxml tag instead.", + DeprecationWarning, + ) + assert ( + len(self._events) == 1 and event_str == "bt_tick" + ), f"Unexpected BT event '{event_str}' in SCXML transition." + return BtTick(self._target, self._condition, self._body).instantiate_bt_events( + instance_id, children_ids + ) # The body of a transition needs to be replaced on derived classes, too - instantiate_exec_body_bt_events(self._body, instance_id) + instantiate_exec_body_bt_events(self._body, instance_id, children_ids) + return [self] def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" @@ -144,22 +161,18 @@ def append_body_executable_entry(self, exec_entry: ScxmlExecutableEntry): ), "Error SCXML transition: invalid body entry found after extension." def check_validity(self) -> bool: - valid_target = isinstance(self._target, str) and len(self._target) > 0 + valid_target = is_non_empty_string(type(self), "target", self._target) + valid_condition = self._condition is None or ( + is_non_empty_string(type(self), "condition", self._condition) + ) valid_events = self._events is None or ( isinstance(self._events, list) and all(isinstance(ev, str) for ev in self._events) ) - valid_condition = self._condition is None or ( - isinstance(self._condition, str) and len(self._condition) > 0 - ) valid_body = self._body is None or valid_execution_body(self._body) - if not valid_target: - print("Error: SCXML transition: target is not valid.") if not valid_events: print("Error: SCXML transition: events are not valid.\nList of events:") for event in self._events: print(f"\t-'{event}'.") - if not valid_condition: - print("Error: SCXML transition: condition is not valid.") if not valid_body: print("Error: SCXML transition: executable content is not valid.") return valid_target and valid_events and valid_condition and valid_body diff --git a/src/as2fm/scxml_converter/scxml_entries/utils.py b/src/as2fm/scxml_converter/scxml_entries/utils.py index 38f71f92..f52ba05a 100644 --- a/src/as2fm/scxml_converter/scxml_entries/utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/utils.py @@ -71,6 +71,7 @@ class CallbackType(Enum): ROS_ACTION_GOAL = auto() # Action callback ROS_ACTION_RESULT = auto() # Action callback ROS_ACTION_FEEDBACK = auto() # Action callback + BT_RESPONSE = auto() # BT response callback @staticmethod def get_expected_prefixes(cb_type: "CallbackType") -> List[str]: @@ -90,6 +91,8 @@ def get_expected_prefixes(cb_type: "CallbackType") -> List[str]: return ["_action.goal_id", "_wrapped_result.code", "_wrapped_result.result."] elif cb_type == CallbackType.ROS_ACTION_FEEDBACK: return ["_action.goal_id", "_feedback."] + elif cb_type == CallbackType.BT_RESPONSE: + return ["_bt.status"] @staticmethod def get_plain_callback(cb_type: "CallbackType") -> "CallbackType": @@ -147,27 +150,28 @@ def _contains_prefixes(msg_expr: str, prefixes: List[str]) -> bool: return False -def get_plain_expression(msg_expr: str, cb_type: CallbackType) -> str: +def get_plain_expression(in_expr: str, cb_type: CallbackType) -> str: """ Convert a ROS interface expressions (using ROS-specific PREFIXES) to plain SCXML. - :param msg_expr: The expression to convert. + :param in_expr: The expression to convert. :param cb_type: The type of callback the expression is used in. """ expected_prefixes = CallbackType.get_expected_prefixes(cb_type) # pre-check over the expression if PLAIN_SCXML_EVENT_PREFIX not in expected_prefixes: - assert not _contains_prefixes(msg_expr, [PLAIN_SCXML_EVENT_PREFIX]), ( - "Error: SCXML ROS conversion: " - f"unexpected {PLAIN_SCXML_EVENT_PREFIX} prefix in expr. {msg_expr}" + assert not _contains_prefixes(in_expr, [PLAIN_SCXML_EVENT_PREFIX]), ( + "Error: SCXML-ROS expression conversion: " + f"unexpected {PLAIN_SCXML_EVENT_PREFIX} prefix in expr. {in_expr}" ) forbidden_prefixes = ROS_EVENT_PREFIXES.copy() if len(expected_prefixes) == 0: forbidden_prefixes.append(PLAIN_SCXML_EVENT_PREFIX) - new_expr = _replace_ros_interface_expression(msg_expr, expected_prefixes) - assert not _contains_prefixes( - new_expr, forbidden_prefixes - ), f"Error: SCXML ROS conversion: unexpected ROS interface prefixes in expr.: {msg_expr}" + new_expr = _replace_ros_interface_expression(in_expr, expected_prefixes) + assert not _contains_prefixes(new_expr, forbidden_prefixes), ( + "Error: SCXML-ROS expression conversion: " + f"unexpected ROS interface prefixes in expr.: {in_expr}" + ) return new_expr @@ -194,7 +198,7 @@ def is_non_empty_string(scxml_type: Type[ScxmlBase], arg_name: str, arg_value: s :param arg_value: The value of the argument to be checked. :return: True if the string is non-empty, False otherwise. """ - valid_str = isinstance(arg_value, str) and len(arg_value) > 0 + valid_str = isinstance(arg_value, str) and len(arg_value.strip()) > 0 if not valid_str: print( f"Error: SCXML entry from {scxml_type.__name__}: " @@ -203,6 +207,18 @@ def is_non_empty_string(scxml_type: Type[ScxmlBase], arg_name: str, arg_value: s return valid_str +def to_integer(scxml_type: Type[ScxmlBase], arg_name: str, arg_value: str) -> Optional[int]: + """ + Try to convert a string to an integer. Return None if not possible. + """ + arg_value = arg_value.strip() + assert is_non_empty_string(scxml_type, arg_name, arg_value) + try: + return int(arg_value) + except ValueError: + return None + + # ------------ Datatype-related utilities ------------ def get_data_type_from_string(data_type: str) -> Optional[Type]: """ diff --git a/test/jani_generator/_test_data/bt_test_models/bt_count_ticks.scxml b/test/jani_generator/_test_data/bt_test_models/bt_count_ticks.scxml new file mode 100644 index 00000000..91b4303e --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_count_ticks.scxml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + initial="initial" + version="1.0" + name="BtCountTicks" + model_src=""> + <!-- + A BT plugin that, upon tick, increases a counter, publishes its value on a topic and + * n_ticks_running times, returns running. + * After n_ticks_running times, returns success and waits for n_ticks_running ticks before returning success again. + --> + + <!-- The topic used to publish the tick count --> + <bt_declare_port_in key="topic_name" type="string" /> + <!-- How many ticks to get before sending out a success --> + <bt_declare_port_in key="n_ticks_running" type="int8" /> + + <datamodel> + <data id="counter" type="int16" expr="0" /> + <!-- Assign this for accessing the port value in conditions... --> + <data id="ticks_before_success" type="int8"> + <expr> + <bt_get_input key="n_ticks_running" /> + </expr> + </data> + </datamodel> + + <ros_topic_publisher type="std_msgs/Int16" name="counter_pub"> + <topic> + <bt_get_input key="topic_name" /> + </topic> + </ros_topic_publisher> + + <state id="initial"> + <transition target="error" cond="ticks_before_success < 0" /> + <bt_tick target="initial"> + <assign location="counter" expr="counter + 1" /> + <ros_topic_publish name="counter_pub"> + <field name="data" expr="counter" /> + </ros_topic_publish> + <if cond="counter % (ticks_before_success + 1) > 0"> + <bt_return_status status="RUNNING" /> + <else/> + <bt_return_status status="SUCCESS" /> + </if> + </bt_tick> + </state> + + <state id="error" /> + +</scxml> diff --git a/test/jani_generator/_test_data/bt_test_models/bt_test_fallback.xml b/test/jani_generator/_test_data/bt_test_models/bt_test_fallback.xml new file mode 100644 index 00000000..363e2352 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_fallback.xml @@ -0,0 +1,23 @@ +<!-- Test that as soon as the Sequence returns success (tick_count_3 = 1): + - tick_count_0 is ticked one time + - tick_count_1 is ticked three times + - tick_count_2 is ticked one time + --> +<root BTCPP_format="4" > + <BehaviorTree ID="MainTree"> + <ReactiveFallback> + <Inverter> + <Fallback> + <Inverter> + <Action ID="BtCountTicks" topic_name="tick_count_0" n_ticks_running="0" /> + </Inverter> + <Inverter> + <Action ID="BtCountTicks" topic_name="tick_count_1" n_ticks_running="2" /> + </Inverter> + <Action ID="BtCountTicks" topic_name="tick_count_2" n_ticks_running="0" /> + </Fallback> + </Inverter> + <Action ID="BtCountTicks" topic_name="tick_count_3" n_ticks_running="0" /> + </ReactiveFallback> + </BehaviorTree> +</root> diff --git a/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_fallback.xml b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_fallback.xml new file mode 100644 index 00000000..04b72adc --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_fallback.xml @@ -0,0 +1,12 @@ +<!-- Test: We expect that tick_count_0 is ticked multiple times and tick_count_1 is never ticked --> +<root BTCPP_format="4" > + <BehaviorTree ID="MainTree"> + <ReactiveFallback name="root_sequence"> + <Inverter> + <Action ID="BtCountTicks" topic_name="tick_count_0" n_ticks_running="0" /> + </Inverter> + <Action ID="AlwaysSuccess" /> + <Action ID="BtCountTicks" topic_name="tick_count_1" n_ticks_running="0" /> + </ReactiveFallback> + </BehaviorTree> +</root> diff --git a/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_sequence.xml b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_sequence.xml new file mode 100644 index 00000000..1d9f4a49 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_sequence.xml @@ -0,0 +1,10 @@ +<!-- Test: We expect that tick_count_0 is ticked multiple times and tick_count_1 is never ticked --> +<root BTCPP_format="4" > + <BehaviorTree ID="MainTree"> + <ReactiveSequence name="root_sequence"> + <Action ID="BtCountTicks" topic_name="tick_count_0" n_ticks_running="0" /> + <Action ID="AlwaysFailure" /> + <Action ID="BtCountTicks" topic_name="tick_count_1" n_ticks_running="0" /> + </ReactiveSequence> + </BehaviorTree> +</root> diff --git a/test/jani_generator/_test_data/bt_test_models/bt_test_sequence.xml b/test/jani_generator/_test_data/bt_test_models/bt_test_sequence.xml new file mode 100644 index 00000000..abf69a5c --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_sequence.xml @@ -0,0 +1,17 @@ +<!-- Test that as soon as the Sequence returns success (tick_count_3 = 1): + - tick_count_0 is ticked one time + - tick_count_1 is ticked three times + - tick_count_2 is ticked one time + --> +<root BTCPP_format="4" > + <BehaviorTree ID="MainTree"> + <ReactiveSequence> + <Sequence> + <Action ID="BtCountTicks" topic_name="tick_count_0" n_ticks_running="0" /> + <Action ID="BtCountTicks" topic_name="tick_count_1" n_ticks_running="2" /> + <Action ID="BtCountTicks" topic_name="tick_count_2" n_ticks_running="0" /> + </Sequence> + <Action ID="BtCountTicks" topic_name="tick_count_3" n_ticks_running="0" /> + </ReactiveSequence> + </BehaviorTree> +</root> diff --git a/test/jani_generator/_test_data/bt_test_models/main_test_fallback.xml b/test/jani_generator/_test_data/bt_test_models/main_test_fallback.xml new file mode 100644 index 00000000..ae79a272 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_fallback.xml @@ -0,0 +1,15 @@ +<convince_mc_tc> + <mc_parameters> + <max_time value="100" unit="s" /> + <bt_tick_rate value="1.0" /> + </mc_parameters> + + <behavior_tree> + <input type="bt.cpp-xml" src="./bt_test_fallback.xml" /> + <input type="bt-plugin-ros-scxml" src="./bt_count_ticks.scxml" /> + </behavior_tree> + + <properties> + <input type="jani" src="./property_test_regular_behaviors.jani" /> + </properties> +</convince_mc_tc> diff --git a/test/jani_generator/_test_data/bt_test_models/main_test_reactive_fallback.xml b/test/jani_generator/_test_data/bt_test_models/main_test_reactive_fallback.xml new file mode 100644 index 00000000..1a9b113f --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_reactive_fallback.xml @@ -0,0 +1,15 @@ +<convince_mc_tc> + <mc_parameters> + <max_time value="100" unit="s" /> + <bt_tick_rate value="1.0" /> + </mc_parameters> + + <behavior_tree> + <input type="bt.cpp-xml" src="./bt_test_reactive_fallback.xml" /> + <input type="bt-plugin-ros-scxml" src="./bt_count_ticks.scxml" /> + </behavior_tree> + + <properties> + <input type="jani" src="./property_test_reactive_behaviors.jani" /> + </properties> +</convince_mc_tc> diff --git a/test/jani_generator/_test_data/bt_test_models/main_test_reactive_sequence.xml b/test/jani_generator/_test_data/bt_test_models/main_test_reactive_sequence.xml new file mode 100644 index 00000000..e826b445 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_reactive_sequence.xml @@ -0,0 +1,15 @@ +<convince_mc_tc> + <mc_parameters> + <max_time value="100" unit="s" /> + <bt_tick_rate value="1.0" /> + </mc_parameters> + + <behavior_tree> + <input type="bt.cpp-xml" src="./bt_test_reactive_sequence.xml" /> + <input type="bt-plugin-ros-scxml" src="./bt_count_ticks.scxml" /> + </behavior_tree> + + <properties> + <input type="jani" src="./property_test_reactive_behaviors.jani" /> + </properties> +</convince_mc_tc> diff --git a/test/jani_generator/_test_data/bt_test_models/main_test_sequence.xml b/test/jani_generator/_test_data/bt_test_models/main_test_sequence.xml new file mode 100644 index 00000000..c3d30f97 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_sequence.xml @@ -0,0 +1,15 @@ +<convince_mc_tc> + <mc_parameters> + <max_time value="100" unit="s" /> + <bt_tick_rate value="1.0" /> + </mc_parameters> + + <behavior_tree> + <input type="bt.cpp-xml" src="./bt_test_sequence.xml" /> + <input type="bt-plugin-ros-scxml" src="./bt_count_ticks.scxml" /> + </behavior_tree> + + <properties> + <input type="jani" src="./property_test_regular_behaviors.jani" /> + </properties> +</convince_mc_tc> diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_reactive_behaviors.jani b/test/jani_generator/_test_data/bt_test_models/property_test_reactive_behaviors.jani new file mode 100644 index 00000000..0de53e53 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/property_test_reactive_behaviors.jani @@ -0,0 +1,36 @@ +{ + "properties": [ + { + "name": "ten_tick_zero_no_tick_one", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "step-bounds": { + "lower": 100 + }, + "left": true, + "op": "U", + "right": { + "left": { + "op": "=", + "left": "topic_tick_count_0_msg.ros_fields__data", + "right": 10 + }, + "op": "∧", + "right": { + "op": "¬", + "exp": "topic_tick_count_1_msg.valid" + } + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani b/test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani new file mode 100644 index 00000000..4ac52766 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani @@ -0,0 +1,57 @@ +{ + "properties": [ + { + "name": "regular_bt_test", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "comment": "As soon as tick_count_3 is published, all previous tick_counts should be at th expected value", + "step-bounds": { + "lower": 100 + }, + "left": { + "op": "¬", + "exp": "topic_tick_count_3_msg.valid" + }, + "op": "U", + "right": { + "op": "∧", + "left": { + "op": "=", + "left": "topic_tick_count_0_msg.ros_fields__data", + "right": 1 + }, + "right": { + "op": "∧", + "left": { + "op": "=", + "left": "topic_tick_count_1_msg.ros_fields__data", + "right": 3 + }, + "right": { + "op": "∧", + "left": { + "op": "=", + "left": "topic_tick_count_2_msg.ros_fields__data", + "right": 1 + }, + "right": { + "op": "=", + "left": "topic_tick_count_3_msg.ros_fields__data", + "right": 1 + } + } + } + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} diff --git a/test/jani_generator/_test_data/delibws24_p1/bt.xml b/test/jani_generator/_test_data/delibws24_p1/bt.xml index 8e7c2ba6..8b449252 100644 --- a/test/jani_generator/_test_data/delibws24_p1/bt.xml +++ b/test/jani_generator/_test_data/delibws24_p1/bt.xml @@ -1,7 +1,7 @@ <root BTCPP_format="4"> <BehaviorTree> <!-- For now use a normal sequence, since failures aren't expected yet --> - <Sequence> + <ReactiveSequence> <!-- loc 0 .. pantry @@ -15,6 +15,6 @@ <Action ID="PickAction" name="pick" data="0" /> <Action ID="NavigateAction" name="navigate" data="1" /> <Action ID="PlaceAction" name="place" /> - </Sequence> + </ReactiveSequence> </BehaviorTree> </root> diff --git a/test/jani_generator/_test_data/robot_navigation_with_bt/bt.xml b/test/jani_generator/_test_data/robot_navigation_with_bt/bt.xml index 01f34cf3..6dc5eb70 100644 --- a/test/jani_generator/_test_data/robot_navigation_with_bt/bt.xml +++ b/test/jani_generator/_test_data/robot_navigation_with_bt/bt.xml @@ -2,12 +2,12 @@ <BehaviorTree> <!-- If the robot goal is in front (goal.x > goal.y), go forward (direction=0). --> <!-- Otherwise, rotate in place (direction=1). --> - <Fallback> - <Sequence> + <ReactiveFallback> + <ReactiveSequence> <Action ID="GoalCheck" name="goal_in_front" topic="/goal_position" /> <Action ID="DriveRobot" name="drive_fwd" direction="0" topic="/cmd_vel" /> - </Sequence> + </ReactiveSequence> <Action ID="DriveRobot" name="drive_rotate" direction="1" topic="/cmd_vel" /> - </Fallback> + </ReactiveFallback> </BehaviorTree> </root> diff --git a/test/jani_generator/_test_data/robot_navigation_with_bt/bt_drive_robot.scxml b/test/jani_generator/_test_data/robot_navigation_with_bt/bt_drive_robot.scxml index 224f0e25..7a9c360e 100644 --- a/test/jani_generator/_test_data/robot_navigation_with_bt/bt_drive_robot.scxml +++ b/test/jani_generator/_test_data/robot_navigation_with_bt/bt_drive_robot.scxml @@ -39,7 +39,7 @@ </state> <state id="wait_for_tick"> - <transition event="bt_tick" target="wait_for_tick"> + <bt_tick target="wait_for_tick"> <ros_topic_publish name="cmd_pub"> <!-- 2D robots require only x and theta controls --> <field name="linear.x" expr="x_vel" /> @@ -50,8 +50,8 @@ <field name="angular.x" expr="0.0" /> <field name="angular.y" expr="0.0" /> </ros_topic_publish> - <send event="bt_success" /> - </transition> + <bt_return_status status="SUCCESS" /> + </bt_tick> </state> </scxml> diff --git a/test/jani_generator/_test_data/robot_navigation_with_bt/bt_goal_check.scxml b/test/jani_generator/_test_data/robot_navigation_with_bt/bt_goal_check.scxml index ccbb0e98..f33673ce 100644 --- a/test/jani_generator/_test_data/robot_navigation_with_bt/bt_goal_check.scxml +++ b/test/jani_generator/_test_data/robot_navigation_with_bt/bt_goal_check.scxml @@ -23,9 +23,9 @@ <!-- Assumption: We have to send an event to the BT, that is either "bt_success" or "bt_failure". --> <state id="wait_first_msg"> - <transition event="bt_tick" target="wait_first_msg"> - <send event="bt_running" /> - </transition> + <bt_tick target="wait_first_msg"> + <bt_return_status status="RUNNING" /> + </bt_tick> <ros_topic_callback name="goal_sub" target="ready_to_check"> <assign location="goal_x" expr="_msg.x" /> <assign location="goal_y" expr="_msg.y" /> @@ -33,13 +33,13 @@ </state> <state id="ready_to_check"> - <transition event="bt_tick" target="ready_to_check"> + <bt_tick target="ready_to_check"> <if cond="goal_x > Math.abs(goal_y)"> - <send event="bt_success" /> + <bt_return_status status="SUCCESS" /> <else /> - <send event="bt_failure" /> + <bt_return_status status="FAILURE" /> </if> - </transition> + </bt_tick> <ros_topic_callback name="goal_sub" target="ready_to_check"> <assign location="goal_x" expr="_msg.x" /> <assign location="goal_y" expr="_msg.y" /> diff --git a/test/jani_generator/_test_data/ros_example_w_bt/battery_properties.jani b/test/jani_generator/_test_data/ros_example_w_bt/battery_properties.jani index 6e9c167f..fc6fc3e6 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt/battery_properties.jani +++ b/test/jani_generator/_test_data/ros_example_w_bt/battery_properties.jani @@ -65,7 +65,7 @@ "right": { "op": "∧", "left": "topic_alarm_msg.ros_fields__data", - "right": "topic_alarm_msg.valid" + "right": "topic_charge_msg.valid" } } }, diff --git a/test/jani_generator/_test_data/ros_example_w_bt/bt.xml b/test/jani_generator/_test_data/ros_example_w_bt/bt.xml index e5eadfbe..ea27d272 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt/bt.xml +++ b/test/jani_generator/_test_data/ros_example_w_bt/bt.xml @@ -1,8 +1,8 @@ <root BTCPP_format="4"> <BehaviorTree> - <Sequence> + <ReactiveSequence> <Condition ID="TopicCondition" name="alarm" /> <Action ID="TopicAction" name="charge" /> - </Sequence> + </ReactiveSequence> </BehaviorTree> </root> diff --git a/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_action.scxml b/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_action.scxml index 8fd0168c..d177d50b 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_action.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_action.scxml @@ -12,11 +12,10 @@ <!-- Assumption: We have to send an event to the BT, that is either "bt_success" or "bt_failure". --> <state id="initial"> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <ros_topic_publish name="charge" /> - <!-- Let's assume this is always successful. --> - <send event="bt_success" /> - </transition> + <bt_return_status status="SUCCESS" /> + </bt_tick> </state> </scxml> diff --git a/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_condition.scxml b/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_condition.scxml index 58ba0daa..7106ca9c 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_condition.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt/bt_topic_condition.scxml @@ -19,13 +19,13 @@ <ros_topic_callback name="alarm" target="initial"> <assign location="last_msg" expr="_msg.data" /> </ros_topic_callback> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <if cond="last_msg"> - <send event="bt_success" /> + <bt_return_status status="SUCCESS" /> <else /> - <send event="bt_failure" /> + <bt_return_status status="FAILURE" /> </if> - </transition> + </bt_tick> </state> </scxml> diff --git a/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_drainer.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_drainer.scxml new file mode 100644 index 00000000..5e17f313 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_drainer.scxml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + initial="use_battery" + version="1.0" + name="BatteryDrainer" + model_src="" + xmlns="http://www.w3.org/2005/07/scxml"> + + <datamodel> + <data id="battery_percent" expr="100" type="int16" /> + </datamodel> + + <ros_topic_subscriber topic="charge" type="std_msgs/Empty" /> + <ros_topic_publisher topic="level" type="std_msgs/Int32" /> + <ros_time_rate rate_hz="1" name="my_timer" /> + + <state id="use_battery"> + <onentry> + <ros_topic_publish name="level"> + <field name="data" expr="battery_percent" /> + </ros_topic_publish> + </onentry> + <ros_rate_callback name="my_timer" target="use_battery" cond="battery_percent > 0"> + <assign location="battery_percent" expr="battery_percent - 1" /> + </ros_rate_callback> + <ros_topic_callback name="charge" target="use_battery"> + <assign location="battery_percent" expr="100" /> + </ros_topic_callback> + </state> +</scxml> diff --git a/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_manager.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_manager.scxml new file mode 100644 index 00000000..ee8b8299 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_manager.scxml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scxml + initial="check_battery" + version="1.0" + name="BatteryManager" + model_src="" + xmlns="http://www.w3.org/2005/07/scxml"> + + <datamodel> + <data id="battery_alarm" expr="false" type="bool" /> + </datamodel> + + <ros_topic_subscriber topic="level" type="std_msgs/Int32" /> + <ros_topic_publisher topic="alarm" type="std_msgs/Bool" /> + + <state id="check_battery"> + <ros_topic_callback name="level" target="check_battery"> + <assign location="battery_alarm" expr="_msg.data < 30" /> + </ros_topic_callback> + <onentry> + <ros_topic_publish name="alarm"> + <field name="data" expr="battery_alarm" /> + </ros_topic_publish> + </onentry> + </state> +</scxml> diff --git a/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani new file mode 100644 index 00000000..6e9c167f --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani @@ -0,0 +1,107 @@ +{ + "properties": [ + { + "name": "battery_depleted", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "left": true, + "op": "U", + "right": { + "left": { + "op": "≤", + "left": "topic_level_msg.ros_fields__data", + "right": 0 + }, + "op": "∧", + "right": "topic_level_msg.valid" + } + } + }, + "states": { + "op": "initial" + } + } + }, + { + "name": "battery_below_20", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "left": true, + "op": "U", + "right": { + "left": { + "op": "<", + "left": "topic_level_msg.ros_fields__data", + "right": 20 + }, + "op": "∧", + "right": "topic_level_msg.valid" + } + } + }, + "states": { + "op": "initial" + } + } + }, + { + "name": "battery_alarm_on", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "left": true, + "op": "U", + "right": { + "op": "∧", + "left": "topic_alarm_msg.ros_fields__data", + "right": "topic_alarm_msg.valid" + } + } + }, + "states": { + "op": "initial" + } + } + }, + { + "name": "battery_charged", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "step-bounds": { + "lower": 100 + }, + "left": true, + "op": "U", + "right": { + "left": { + "op": "=", + "left": "topic_level_msg.ros_fields__data", + "right": 100 + }, + "op": "∧", + "right": "topic_level_msg.valid" + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} diff --git a/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt.xml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt.xml new file mode 100644 index 00000000..ea27d272 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt.xml @@ -0,0 +1,8 @@ +<root BTCPP_format="4"> + <BehaviorTree> + <ReactiveSequence> + <Condition ID="TopicCondition" name="alarm" /> + <Action ID="TopicAction" name="charge" /> + </ReactiveSequence> + </BehaviorTree> +</root> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml similarity index 71% rename from test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml index c94e163d..8fd0168c 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml @@ -2,20 +2,20 @@ <scxml initial="initial" version="1.0" - name="1001_BtTopicAction" + name="TopicAction" model_src="" xmlns="http://www.w3.org/2005/07/scxml"> - <ros_topic_publisher name="charge" topic="charge" type="std_msgs/Empty" /> + <ros_topic_publisher topic="charge" type="std_msgs/Empty" /> <!-- Assumption: We get an event when the node is ticked by the BT, named "bt_tick". --> <!-- Assumption: We have to send an event to the BT, that is either "bt_success" or "bt_failure". --> <state id="initial"> - <transition event="bt_1001_tick" target="initial"> + <transition event="bt_tick" target="initial"> <ros_topic_publish name="charge" /> <!-- Let's assume this is always successful. --> - <send event="bt_1001_success" /> + <send event="bt_success" /> </transition> </state> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml similarity index 65% rename from test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml index 33609b7f..58ba0daa 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml @@ -2,15 +2,15 @@ <scxml initial="initial" version="1.0" - name="10000_BtTopicCondition" + name="TopicCondition" model_src="" xmlns="http://www.w3.org/2005/07/scxml"> <datamodel> - <data id="last_msg" expr="False" type="bool" /> + <data id="last_msg" expr="false" type="bool" /> </datamodel> - <ros_topic_subscriber name="alarm" topic="alarm" type="std_msgs/Bool" /> + <ros_topic_subscriber topic="alarm" type="std_msgs/Bool" /> <!-- Assumption: We get an event when the node is ticked by the BT, named "bt_tick". --> <!-- Assumption: We have to send an event to the BT, that is either "bt_success" or "bt_failure". --> @@ -19,11 +19,11 @@ <ros_topic_callback name="alarm" target="initial"> <assign location="last_msg" expr="_msg.data" /> </ros_topic_callback> - <transition event="bt_10000_tick" target="initial"> + <transition event="bt_tick" target="initial"> <if cond="last_msg"> - <send event="bt_10000_success" /> - <else/> - <send event="bt_10000_failure" /> + <send event="bt_success" /> + <else /> + <send event="bt_failure" /> </if> </transition> </state> diff --git a/test/jani_generator/_test_data/ros_example_w_bt_deprecated/main.xml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/main.xml new file mode 100644 index 00000000..cf6888f0 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/main.xml @@ -0,0 +1,20 @@ +<convince_mc_tc> + <mc_parameters> + <max_time value="100" unit="s" /> + </mc_parameters> + + <behavior_tree> + <input type="bt.cpp-xml" src="./bt.xml" /> + <input type="bt-plugin-ros-scxml" src="./bt_topic_condition.scxml" /> + <input type="bt-plugin-ros-scxml" src="./bt_topic_action.scxml" /> + </behavior_tree> + + <node_models> + <input type="ros-scxml" src="./battery_drainer.scxml" /> + <input type="ros-scxml" src="./battery_manager.scxml" /> + </node_models> + + <properties> + <input type="jani" src="./battery_properties.jani" /> + </properties> +</convince_mc_tc> diff --git a/test/jani_generator/test_systemtest_behavior_tree_scxml.py b/test/jani_generator/test_systemtest_behavior_tree_scxml.py new file mode 100644 index 00000000..4f5d4580 --- /dev/null +++ b/test/jani_generator/test_systemtest_behavior_tree_scxml.py @@ -0,0 +1,101 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# 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. + +"""Test the SCXML conversion to JANI""" + +import os +import unittest + +import pytest + +from as2fm.jani_generator.scxml_helpers.top_level_interpreter import interpret_top_level_xml + +from ..as2fm_common.test_utilities_smc_storm import run_smc_storm_with_output + + +# pylint: disable=too-many-public-methods +class TestConversion(unittest.TestCase): + """ + Test the conversion of SCXML to JANI. + """ + + def _test_with_main(self, path_to_main_xml: str, property_name: str, success: bool): + """ + Testing the model resulting from the main xml file with the entrypoint. + + :param path_to_main_xml: The path to the main xml file. + :param property_name: The property name to test. + :param success: If the property is expected to be always satisfied or always not satisfied. + """ + test_folder = os.path.join(os.path.dirname(__file__), "_test_data") + main_xml_full_path = os.path.join(test_folder, path_to_main_xml) + generated_scxml_path = "generated_plain_scxml" + jani_file = "main.jani" + test_folder = os.path.dirname(main_xml_full_path) + interpret_top_level_xml(main_xml_full_path, "main.jani", generated_scxml_path) + jani_file_path = os.path.join(test_folder, jani_file) + generated_scxml_path = os.path.join(test_folder, generated_scxml_path) + self.assertTrue(os.path.exists(jani_file_path)) + pos_res = "Result: 1" if success else "Result: 0" + neg_res = "Result: 0" if success else "Result: 1" + run_smc_storm_with_output( + f"--model {jani_file_path} --properties-names {property_name}", + [property_name, jani_file_path, pos_res], + [neg_res], + ) + # Remove generated file (in case of test passed) + if os.path.exists(jani_file_path): + os.remove(jani_file_path) + if os.path.exists(generated_scxml_path): + for file in os.listdir(generated_scxml_path): + assert file.endswith(".scxml") + os.remove(os.path.join(generated_scxml_path, file)) + os.removedirs(generated_scxml_path) + + def test_reactive_sequence(self): + """Test the reactive_sequence behavior.""" + self._test_with_main( + os.path.join("bt_test_models", "main_test_reactive_sequence.xml"), + "ten_tick_zero_no_tick_one", + True, + ) + + def test_reactive_fallback(self): + """Test the reactive_fallback behavior.""" + self._test_with_main( + os.path.join("bt_test_models", "main_test_reactive_fallback.xml"), + "ten_tick_zero_no_tick_one", + True, + ) + + def test_sequence(self): + """Test the sequence behavior.""" + self._test_with_main( + os.path.join("bt_test_models", "main_test_sequence.xml"), + "regular_bt_test", + True, + ) + + def test_fallback(self): + """Test the sequence behavior.""" + self._test_with_main( + os.path.join("bt_test_models", "main_test_fallback.xml"), + "regular_bt_test", + True, + ) + + +if __name__ == "__main__": + pytest.main(["-s", "-v", __file__]) diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index d7b7f581..5196d8d8 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -218,7 +218,7 @@ def _test_with_main( :param folder: The folder containing the test data. :param store_generated_scxmls: If the generated SCXMLs should be stored. :param property_name: The property name to test. - :param success: If the property is expected to be always satisfied of always not satisfied. + :param success: If the property is expected to be always satisfied or always not satisfied. :param skip_smc: If the model shall be executed using SMC (uses smc_storm). """ test_data_dir = os.path.join(os.path.dirname(__file__), "_test_data", folder) @@ -257,6 +257,14 @@ def test_battery_ros_example_alarm_on(self): """Here we expect the property to be *not* satisfied.""" self._test_with_main("ros_example", False, "alarm_on", False) + def test_battery_example_w_bt_battery_depleted_deprecated(self): + """Here we expect the property to be *not* satisfied.""" + self._test_with_main("ros_example_w_bt_deprecated", True, "battery_depleted", False) + + def test_battery_example_w_bt_main_alarm_and_charge_deprecated(self): + """Here we expect the property to be *not* satisfied.""" + self._test_with_main("ros_example_w_bt_deprecated", False, "battery_alarm_on", True) + def test_battery_example_w_bt_battery_depleted(self): """Here we expect the property to be *not* satisfied.""" # TODO: Improve properties under evaluation! diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt.xml b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt.xml index 49f3f6ae..83b2621a 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt.xml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt.xml @@ -1,10 +1,10 @@ <root BTCPP_format="4"> <BehaviorTree> - <Sequence> + <ReactiveSequence> <Inverter> <Condition ID="BtTopicCondition" name="alarm" /> </Inverter> <Action ID="BtTopicAction" name="charge" /> - </Sequence> + </ReactiveSequence> </BehaviorTree> </root> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_action.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_action.scxml index 8927f32b..74585b6f 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_action.scxml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_action.scxml @@ -12,11 +12,11 @@ <!-- Assumption: We have to send an event to the BT, that is either "bt_success" or "bt_failure". --> <state id="initial"> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <ros_topic_publish name="charge" /> <!-- Let's assume this is always successful. --> - <send event="bt_success" /> - </transition> + <bt_return_status status="SUCCESS" /> + </bt_tick> </state> </scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_condition.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_condition.scxml index c5e9bbd0..18f0f346 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_condition.scxml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/bt_topic_condition.scxml @@ -19,13 +19,13 @@ <ros_topic_callback name="alarm" target="initial"> <assign location="last_msg" expr="_msg.data" /> </ros_topic_callback> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <if cond="last_msg"> - <send event="bt_success" /> + <bt_return_status status="SUCCESS" /> <else /> - <send event="bt_failure" /> + <bt_return_status status="FAILURE" /> </if> - </transition> + </bt_tick> </state> </scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1000_ReactiveSequence.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1000_ReactiveSequence.scxml new file mode 100644 index 00000000..abea24c4 --- /dev/null +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1000_ReactiveSequence.scxml @@ -0,0 +1,60 @@ +<scxml name="1000_ReactiveSequence" version="1.0" model_src="" initial="wait_for_tick" xmlns="http://www.w3.org/2005/07/scxml"> + <datamodel> + <data id="success_count" expr="0" type="int8"/> + <data id="child_idx" expr="0" type="int8"/> + <data id="children_count" expr="2" type="int8"/> + </datamodel> + <state id="wait_for_tick"> + <transition target="error" cond="children_count < 1"/> + <transition target="tick_children" event="bt_1000_tick"> + <assign location="success_count" expr="0"/> + <assign location="child_idx" expr="0"/> + </transition> + </state> + <state id="tick_children"> + <onentry> + <if cond="success_count < children_count"> + <if cond="child_idx == 0"> + <send event="bt_1001_tick"/> + <elseif cond="child_idx == 1"/> + <send event="bt_1003_tick"/> + </if> + </if> + </onentry> + <transition target="tick_children" event="bt_1001_response" cond="(_event.status == 1) && (child_idx == 0)"> + <assign location="success_count" expr="success_count + 1"/> + <assign location="child_idx" expr="child_idx + 1"/> + </transition> + <transition target="tick_children" event="bt_1003_response" cond="(_event.status == 1) && (child_idx == 1)"> + <assign location="success_count" expr="success_count + 1"/> + <assign location="child_idx" expr="child_idx + 1"/> + </transition> + <transition target="wait_for_tick" event="bt_1001_response" cond="(_event.status == 2) && (child_idx == 0)"> + <send event="bt_1000_response"> + <param name="status" expr="2"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1003_response" cond="(_event.status == 2) && (child_idx == 1)"> + <send event="bt_1000_response"> + <param name="status" expr="2"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1001_response" cond="(_event.status == 3) && (child_idx == 0)"> + <send event="bt_1000_response"> + <param name="status" expr="3"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1003_response" cond="(_event.status == 3) && (child_idx == 1)"> + <send event="bt_1000_response"> + <param name="status" expr="3"/> + </send> + </transition> + <transition target="wait_for_tick" cond="success_count == children_count"> + <send event="bt_1000_response"> + <param name="status" expr="1"/> + </send> + </transition> + <transition target="error" cond="child_idx < 0 || child_idx >= children_count"/> + </state> + <state id="error"/> +</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_Inverter.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_Inverter.scxml new file mode 100644 index 00000000..2b936544 --- /dev/null +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_Inverter.scxml @@ -0,0 +1,30 @@ +<scxml name="1001_Inverter" version="1.0" model_src="" initial="wait_for_tick" xmlns="http://www.w3.org/2005/07/scxml"> + <datamodel> + <data id="children_count" expr="1" type="int8"/> + </datamodel> + <state id="wait_for_tick"> + <transition target="error" cond="children_count != 1"/> + <transition target="tick_child" event="bt_1001_tick"/> + </state> + <state id="tick_child"> + <onentry> + <send event="bt_1002_tick"/> + </onentry> + <transition target="wait_for_tick" event="bt_1002_response" cond="_event.status == 1"> + <send event="bt_1001_response"> + <param name="status" expr="2"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1002_response" cond="_event.status == 2"> + <send event="bt_1001_response"> + <param name="status" expr="1"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1002_response" cond="_event.status == 3"> + <send event="bt_1001_response"> + <param name="status" expr="3"/> + </send> + </transition> + </state> + <state id="error"/> +</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1002_BtTopicCondition.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1002_BtTopicCondition.scxml new file mode 100644 index 00000000..0570e345 --- /dev/null +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1002_BtTopicCondition.scxml @@ -0,0 +1,22 @@ +<scxml name="1002_BtTopicCondition" version="1.0" model_src="" initial="initial" xmlns="http://www.w3.org/2005/07/scxml"> + <datamodel> + <data id="last_msg" expr="False" type="bool"/> + </datamodel> + <ros_topic_subscriber name="alarm" topic="alarm" type="std_msgs/Bool"/> + <state id="initial"> + <ros_topic_callback name="alarm" target="initial"> + <assign location="last_msg" expr="_msg.data"/> + </ros_topic_callback> + <transition target="initial" event="bt_1002_tick"> + <if cond="last_msg"> + <send event="bt_1002_response"> + <param name="status" expr="1"/> + </send> + <else/> + <send event="bt_1002_response"> + <param name="status" expr="2"/> + </send> + </if> + </transition> + </state> +</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1003_BtTopicAction.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1003_BtTopicAction.scxml new file mode 100644 index 00000000..b1e474cd --- /dev/null +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1003_BtTopicAction.scxml @@ -0,0 +1,11 @@ +<scxml name="1003_BtTopicAction" version="1.0" model_src="" initial="initial" xmlns="http://www.w3.org/2005/07/scxml"> + <ros_topic_publisher name="charge" topic="charge" type="std_msgs/Empty"/> + <state id="initial"> + <transition target="initial" event="bt_1003_tick"> + <ros_topic_publish name="charge"/> + <send event="bt_1003_response"> + <param name="status" expr="1"/> + </send> + </transition> + </state> +</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt.scxml deleted file mode 100644 index 8849a482..00000000 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt.scxml +++ /dev/null @@ -1,35 +0,0 @@ -<?xml version='1.0' encoding='utf-8'?> -<scxml name="bt" version="1.0" model_src="" initial="wait_for_tick" xmlns="http://www.w3.org/2005/07/scxml"> - <ros_time_rate name="bt_tick" rate_hz="1.0" /> - <state id="tick"> - <transition target="10000_BtTopicCondition" /> - </state> - <state id="success"> - <transition target="wait_for_tick" /> - </state> - <state id="failure"> - <transition target="wait_for_tick" /> - </state> - <state id="running"> - <transition target="wait_for_tick" /> - </state> - <state id="10000_BtTopicCondition"> - <onentry> - <send event="bt_10000_tick" /> - </onentry> - <transition target="failure" event="bt_10000_success" /> - <transition target="running" event="bt_10000_running" /> - <transition target="1001_BtTopicAction" event="bt_10000_failure" /> - </state> - <state id="1001_BtTopicAction"> - <onentry> - <send event="bt_1001_tick" /> - </onentry> - <transition target="success" event="bt_1001_success" /> - <transition target="failure" event="bt_1001_failure" /> - <transition target="running" event="bt_1001_running" /> - </state> - <state id="wait_for_tick"> - <ros_rate_callback name="bt_tick" target="tick" /> - </state> -</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt_root_fsm_bt.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt_root_fsm_bt.scxml new file mode 100644 index 00000000..7e91ae69 --- /dev/null +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt_root_fsm_bt.scxml @@ -0,0 +1,12 @@ +<scxml name="bt" version="1.0" model_src="" initial="idle" xmlns="http://www.w3.org/2005/07/scxml"> + <ros_time_rate rate_hz="1.0" name="bt_tick"/> + <state id="idle"> + <ros_rate_callback name="bt_tick" target="wait_tick_res"> + <send event="bt_1000_tick"/> + </ros_rate_callback> + </state> + <state id="wait_tick_res"> + <transition target="idle" event="bt_1000_response"/> + </state> + <state id="error"/> +</scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_parsed_scxml/bt_topic_condition.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_parsed_scxml/bt_topic_condition.scxml index c5e9bbd0..18f0f346 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_parsed_scxml/bt_topic_condition.scxml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_parsed_scxml/bt_topic_condition.scxml @@ -19,13 +19,13 @@ <ros_topic_callback name="alarm" target="initial"> <assign location="last_msg" expr="_msg.data" /> </ros_topic_callback> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <if cond="last_msg"> - <send event="bt_success" /> + <bt_return_status status="SUCCESS" /> <else /> - <send event="bt_failure" /> + <bt_return_status status="FAILURE" /> </if> - </transition> + </bt_tick> </state> </scxml> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_action.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_action.scxml index 05826d37..cddf8fd6 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_action.scxml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_action.scxml @@ -1,10 +1,11 @@ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="initial" version="1.0" name="BtTopicAction" model_src=""> <state id="initial"> - <transition event="bt_tick" target="initial"> - <send event="topic_charge_msg" /> - - <send event="bt_success" /> + <transition target="initial" event="bt_1001_tick"> + <send event="topic_charge_msg"/> + <send event="bt_1001_response"> + <param name="status" expr="1"/> + </send> </transition> </state> diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_condition.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_condition.scxml index 14ee218d..6c6c2365 100644 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_condition.scxml +++ b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_plain_scxml/bt_topic_condition.scxml @@ -6,13 +6,17 @@ <state id="initial"> <transition target="initial" event="topic_alarm_msg"> - <assign location="last_msg" expr="_event.ros_fields__data" /> + <assign location="last_msg" expr="_event.ros_fields__data"/> </transition> - <transition event="bt_tick" target="initial"> + <transition target="initial" event="bt_1002_tick"> <if cond="last_msg"> - <send event="bt_success" /> + <send event="bt_1002_response"> + <param name="status" expr="1"/> + </send> <else/> - <send event="bt_failure" /> + <send event="bt_1002_response"> + <param name="status" expr="2"/> + </send> </if> </transition> </state> diff --git a/test/scxml_converter/_test_data/bt_ports_only/bt.xml b/test/scxml_converter/_test_data/bt_ports_only/bt.xml index a4531cc1..706c5ecf 100644 --- a/test/scxml_converter/_test_data/bt_ports_only/bt.xml +++ b/test/scxml_converter/_test_data/bt_ports_only/bt.xml @@ -1,8 +1,8 @@ <root BTCPP_format="4" > <BehaviorTree ID="MainTree"> - <Sequence name="root_sequence"> + <ReactiveSequence name="root_sequence"> <Action ID="BtTopicAction" name="answer" data="42" /> <Action ID="BtTopicAction" name="answer_incremented" data="43" /> - </Sequence> + </ReactiveSequence> </BehaviorTree> </root> diff --git a/test/scxml_converter/_test_data/bt_ports_only/bt_topic_action.scxml b/test/scxml_converter/_test_data/bt_ports_only/bt_topic_action.scxml index 417b7136..ec645b0c 100644 --- a/test/scxml_converter/_test_data/bt_ports_only/bt_topic_action.scxml +++ b/test/scxml_converter/_test_data/bt_ports_only/bt_topic_action.scxml @@ -23,7 +23,7 @@ </ros_topic_publisher> <state id="initial"> - <transition event="bt_tick" target="initial"> + <bt_tick target="initial"> <assign location="number"> <expr> <bt_get_input key="data" /> @@ -32,7 +32,7 @@ <ros_topic_publish name="answer_pub"> <field name="data" expr="number" /> </ros_topic_publish> - </transition> + </bt_tick> </state> </scxml> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_BtTopicAction.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_BtTopicAction.scxml deleted file mode 100644 index 2f8e8539..00000000 --- a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_BtTopicAction.scxml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version='1.0' encoding='utf-8'?> -<scxml name="1000_BtTopicAction" version="1.0" model_src="" initial="initial" xmlns="http://www.w3.org/2005/07/scxml"> - <datamodel> - <data id="number" expr="0" type="int16" /> - </datamodel> - <ros_topic_publisher name="answer_pub" topic="answer" type="std_msgs/Int16" /> - <state id="initial"> - <transition target="initial" event="bt_1000_tick"> - <assign location="number" expr="42" /> - <ros_topic_publish name="answer_pub"> - <field name="data" expr="number" /> - </ros_topic_publish> - </transition> - </state> -</scxml> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_ReactiveSequence.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_ReactiveSequence.scxml new file mode 100644 index 00000000..a9fa5ce5 --- /dev/null +++ b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_ReactiveSequence.scxml @@ -0,0 +1,60 @@ +<scxml name="1000_ReactiveSequence" version="1.0" model_src="" initial="wait_for_tick" xmlns="http://www.w3.org/2005/07/scxml"> + <datamodel> + <data id="success_count" expr="0" type="int8"/> + <data id="child_idx" expr="0" type="int8"/> + <data id="children_count" expr="2" type="int8"/> + </datamodel> + <state id="wait_for_tick"> + <transition target="error" cond="children_count < 1"/> + <transition target="tick_children" event="bt_1000_tick"> + <assign location="success_count" expr="0"/> + <assign location="child_idx" expr="0"/> + </transition> + </state> + <state id="tick_children"> + <onentry> + <if cond="success_count < children_count"> + <if cond="child_idx == 0"> + <send event="bt_1001_tick"/> + <elseif cond="child_idx == 1"/> + <send event="bt_1002_tick"/> + </if> + </if> + </onentry> + <transition target="tick_children" event="bt_1001_response" cond="(_event.status == 1) && (child_idx == 0)"> + <assign location="success_count" expr="success_count + 1"/> + <assign location="child_idx" expr="child_idx + 1"/> + </transition> + <transition target="tick_children" event="bt_1002_response" cond="(_event.status == 1) && (child_idx == 1)"> + <assign location="success_count" expr="success_count + 1"/> + <assign location="child_idx" expr="child_idx + 1"/> + </transition> + <transition target="wait_for_tick" event="bt_1001_response" cond="(_event.status == 2) && (child_idx == 0)"> + <send event="bt_1000_response"> + <param name="status" expr="2"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1002_response" cond="(_event.status == 2) && (child_idx == 1)"> + <send event="bt_1000_response"> + <param name="status" expr="2"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1001_response" cond="(_event.status == 3) && (child_idx == 0)"> + <send event="bt_1000_response"> + <param name="status" expr="3"/> + </send> + </transition> + <transition target="wait_for_tick" event="bt_1002_response" cond="(_event.status == 3) && (child_idx == 1)"> + <send event="bt_1000_response"> + <param name="status" expr="3"/> + </send> + </transition> + <transition target="wait_for_tick" cond="success_count == children_count"> + <send event="bt_1000_response"> + <param name="status" expr="1"/> + </send> + </transition> + <transition target="error" cond="child_idx < 0 || child_idx >= children_count"/> + </state> + <state id="error"/> +</scxml> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1001_BtTopicAction.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1001_BtTopicAction.scxml index a490237e..fbf09693 100644 --- a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1001_BtTopicAction.scxml +++ b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1001_BtTopicAction.scxml @@ -1,14 +1,13 @@ -<?xml version='1.0' encoding='utf-8'?> <scxml name="1001_BtTopicAction" version="1.0" model_src="" initial="initial" xmlns="http://www.w3.org/2005/07/scxml"> <datamodel> - <data id="number" expr="0" type="int16" /> + <data id="number" expr="0" type="int16"/> </datamodel> - <ros_topic_publisher name="answer_pub" topic="answer_incremented" type="std_msgs/Int16" /> + <ros_topic_publisher name="answer_pub" topic="answer" type="std_msgs/Int16"/> <state id="initial"> <transition target="initial" event="bt_1001_tick"> - <assign location="number" expr="43" /> + <assign location="number" expr="42"/> <ros_topic_publish name="answer_pub"> - <field name="data" expr="number" /> + <field name="data" expr="number"/> </ros_topic_publish> </transition> </state> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1002_BtTopicAction.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1002_BtTopicAction.scxml new file mode 100644 index 00000000..92fcee6f --- /dev/null +++ b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1002_BtTopicAction.scxml @@ -0,0 +1,14 @@ +<scxml name="1002_BtTopicAction" version="1.0" model_src="" initial="initial" xmlns="http://www.w3.org/2005/07/scxml"> + <datamodel> + <data id="number" expr="0" type="int16"/> + </datamodel> + <ros_topic_publisher name="answer_pub" topic="answer_incremented" type="std_msgs/Int16"/> + <state id="initial"> + <transition target="initial" event="bt_1002_tick"> + <assign location="number" expr="43"/> + <ros_topic_publish name="answer_pub"> + <field name="data" expr="number"/> + </ros_topic_publish> + </transition> + </state> +</scxml> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt.scxml deleted file mode 100644 index 9bdd99e6..00000000 --- a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt.scxml +++ /dev/null @@ -1,35 +0,0 @@ -<?xml version='1.0' encoding='utf-8'?> -<scxml name="bt" version="1.0" model_src="" initial="wait_for_tick" xmlns="http://www.w3.org/2005/07/scxml"> - <ros_time_rate rate_hz="1.0" name="bt_tick" /> - <state id="tick"> - <transition target="1000_BtTopicAction" /> - </state> - <state id="success"> - <transition target="wait_for_tick" /> - </state> - <state id="failure"> - <transition target="wait_for_tick" /> - </state> - <state id="running"> - <transition target="wait_for_tick" /> - </state> - <state id="1000_BtTopicAction"> - <onentry> - <send event="bt_1000_tick" /> - </onentry> - <transition target="failure" event="bt_1000_failure" /> - <transition target="running" event="bt_1000_running" /> - <transition target="1001_BtTopicAction" event="bt_1000_success" /> - </state> - <state id="1001_BtTopicAction"> - <onentry> - <send event="bt_1001_tick" /> - </onentry> - <transition target="success" event="bt_1001_success" /> - <transition target="failure" event="bt_1001_failure" /> - <transition target="running" event="bt_1001_running" /> - </state> - <state id="wait_for_tick"> - <ros_rate_callback name="bt_tick" target="tick" /> - </state> -</scxml> diff --git a/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt_root_fsm_bt.scxml b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt_root_fsm_bt.scxml new file mode 100644 index 00000000..7e91ae69 --- /dev/null +++ b/test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt_root_fsm_bt.scxml @@ -0,0 +1,12 @@ +<scxml name="bt" version="1.0" model_src="" initial="idle" xmlns="http://www.w3.org/2005/07/scxml"> + <ros_time_rate rate_hz="1.0" name="bt_tick"/> + <state id="idle"> + <ros_rate_callback name="bt_tick" target="wait_tick_res"> + <send event="bt_1000_tick"/> + </ros_rate_callback> + </state> + <state id="wait_tick_res"> + <transition target="idle" event="bt_1000_response"/> + </state> + <state id="error"/> +</scxml> diff --git a/test/scxml_converter/test_systemtest_scxml_entries.py b/test/scxml_converter/test_systemtest_scxml_entries.py index 3077181e..1f2a9533 100644 --- a/test/scxml_converter/test_systemtest_scxml_entries.py +++ b/test/scxml_converter/test_systemtest_scxml_entries.py @@ -20,6 +20,7 @@ from as2fm.scxml_converter.scxml_entries import ( BtGetValueInputPort, BtInputPortDeclaration, + BtTick, RosField, RosRateCallback, RosTimeRate, @@ -177,9 +178,8 @@ def test_bt_action_with_ports_from_code(): init_state = ScxmlState( "initial", body=[ - ScxmlTransition( + BtTick( "initial", - ["bt_tick"], None, [ ScxmlAssign("number", BtGetValueInputPort("data")), @@ -189,6 +189,7 @@ def test_bt_action_with_ports_from_code(): ], ) scxml_root = ScxmlRoot("BtTopicAction") + scxml_root.set_bt_plugin_id(0) scxml_root.set_data_model(data_model) scxml_root.add_bt_port_declaration(BtInputPortDeclaration("name", "string")) scxml_root.add_bt_port_declaration(BtInputPortDeclaration("data", "int16")) @@ -196,7 +197,7 @@ def test_bt_action_with_ports_from_code(): scxml_root.add_state(init_state, initial=True) assert not scxml_root.check_validity(), "Currently, we handle unspecified BT entries as invalid" scxml_root.set_bt_ports_values([("name", "/sys/add_srv"), ("data", "25")]) - scxml_root.update_bt_ports_values() + scxml_root.instantiate_bt_information() _test_scxml_from_code( scxml_root, os.path.join( diff --git a/test/scxml_converter/test_systemtest_xml.py b/test/scxml_converter/test_systemtest_xml.py index 493eff9f..356419b5 100644 --- a/test/scxml_converter/test_systemtest_xml.py +++ b/test/scxml_converter/test_systemtest_xml.py @@ -52,7 +52,6 @@ def bt_to_scxml_test( bt_file = os.path.join(test_data_path, bt_file) plugin_files = [os.path.join(test_data_path, f) for f in bt_plugins] scxml_objs = bt_converter(bt_file, plugin_files, 1.0) - assert len(scxml_objs) == 3, f"Expecting 3 scxml objects, found {len(scxml_objs)}." if store_generated: clear_output_folder(test_folder) for scxml_obj in scxml_objs: @@ -61,13 +60,18 @@ def bt_to_scxml_test( ) with open(output_file, "w", encoding="utf-8") as f_o: f_o.write(scxml_obj.as_xml_string()) + # Evaluate generated artifacts + gt_scxml_dir_path = os.path.join(test_data_path, "gt_bt_scxml") + n_gt_models = len([f for f in os.listdir(gt_scxml_dir_path) if f.endswith(".scxml")]) + assert ( + len(scxml_objs) == n_gt_models + ), f"Expecting {n_gt_models} scxml objects, found {len(scxml_objs)}." for scxml_root in scxml_objs: scxml_name = scxml_root.get_name() gt_scxml_path = os.path.join(test_data_path, "gt_bt_scxml", f"{scxml_name}.scxml") with open(gt_scxml_path, "r", encoding="utf-8") as f_o: gt_xml = remove_empty_lines(canonicalize_xml(f_o.read())) scxml_xml = remove_empty_lines(canonicalize_xml(scxml_root.as_xml_string())) - assert scxml_xml == gt_xml @@ -90,14 +94,17 @@ def ros_to_plain_scxml_test( scxml_files = [file for file in os.listdir(test_data_path) if file.endswith(".scxml")] if store_generated: clear_output_folder(test_folder) + bt_index = 1000 for fname in scxml_files: input_file = os.path.join(test_data_path, fname) # gt_file = os.path.join(test_data_path, 'gt_plain_scxml', fname) try: scxml_obj = ScxmlRoot.from_scxml_file(input_file) if fname in scxml_bt_ports: + bt_index += 1 + scxml_obj.set_bt_plugin_id(bt_index) scxml_obj.set_bt_ports_values(scxml_bt_ports[fname]) - scxml_obj.update_bt_ports_values() + scxml_obj.instantiate_bt_information() plain_scxmls, _ = scxml_obj.to_plain_scxml_and_declarations() if store_generated: for generated_scxml in plain_scxmls: @@ -144,7 +151,12 @@ def test_bt_to_scxml_battery_drainer(): def test_ros_to_plain_scxml_battery_drainer(): """Test the conversion of the battery drainer with ROS macros to plain SCXML.""" - ros_to_plain_scxml_test("battery_drainer_w_bt", {}, {}, True) + ros_to_plain_scxml_test( + "battery_drainer_w_bt", + {"bt_topic_action.scxml": [], "bt_topic_condition.scxml": []}, + {}, + True, + ) def test_bt_to_scxml_bt_ports():