From b5ea3be12bd79e62a7148df595d98762d555c640 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 8 Oct 2024 12:07:05 +0200 Subject: [PATCH 01/46] Initial verasion of BT control nodes scxml Signed-off-by: Marco Lampacrescia --- .../bt_control_nodes/bt_if_then_else.scxml | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml diff --git a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml new file mode 100644 index 00000000..39649288 --- /dev/null +++ b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5cb16d3bfb8b7e5c594ad7b64dca2ad003f8fc4d Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 8 Oct 2024 17:50:24 +0200 Subject: [PATCH 02/46] Second iteration of scxml_control_nodes Signed-off-by: Marco Lampacrescia --- .../bt_control_nodes/bt_if_then_else.scxml | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml index 39649288..18fc9c76 100644 --- a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml +++ b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml @@ -6,48 +6,63 @@ name="IfThenElse" model_src="https://raw.githubusercontent.com/BehaviorTree/BehaviorTree.CPP/refs/heads/v3.8/src/controls/if_then_else_node.cpp"> + + + - - - - - + + + + + + - - + + + - - - - - + - - + + + - - + + - + - + - - + - - + + + + + + + + + + + + + + + + From 1e353c3b022ef5260113940aa4c9e5235896fb43 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 08:25:05 +0200 Subject: [PATCH 03/46] Rename BT Ports scxml declarations Signed-off-by: Marco Lampacrescia --- .pre-commit-config.yaml | 2 +- src/as2fm/scxml_converter/scxml_entries/__init__.py | 3 ++- .../scxml_entries/{scxml_bt.py => scxml_bt_ports.py} | 0 src/as2fm/scxml_converter/scxml_entries/scxml_root.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename src/as2fm/scxml_converter/scxml_entries/{scxml_bt.py => scxml_bt_ports.py} (100%) 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/src/as2fm/scxml_converter/scxml_entries/__init__.py b/src/as2fm/scxml_converter/scxml_entries/__init__.py index 76c393ed..d51cb1bb 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 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 100% rename from src/as2fm/scxml_converter/scxml_entries/scxml_bt.py rename to src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 25b52fb1..e061acdf 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 ( From 7fd7fc6fa67e72c21c449f63774169aada783af6 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 09:21:16 +0200 Subject: [PATCH 04/46] One more iteration over the BT Control node SCXML Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_bt_ports.py | 2 +- .../bt_control_nodes/bt_if_then_else.scxml | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py index 662c24a0..370d95e4 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.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 diff --git a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml index 18fc9c76..0c6f363c 100644 --- a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml +++ b/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml @@ -19,28 +19,30 @@ + - - + + + - + - + - - + + - - - - - - - + + + + + + + @@ -49,17 +51,17 @@ - + - - - + + + - - - - - + + + + + From 8bd28edcc737d11c82a1ca0b67fa063774467a5b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 09:48:25 +0200 Subject: [PATCH 05/46] STart implementing new scxml bt tags Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_bt_ticks.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py 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..39317c3d --- /dev/null +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -0,0 +1,75 @@ +# 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 Optional, Union + +from lxml import etree as ET + +from as2fm.scxml_converter.scxml_entries import ScxmlSend, ScxmlTransition +from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument + + +class BtTick(ScxmlTransition): + """ + Tick a BT plugin/control node, 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) + return BtTick(target, condition) + + def __init__(self, target: str, condition: Optional[str] = None): + super().__init__(target, ["bt_tick"], condition) + + 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) + 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) + child_id: str = get_xml_argument(BtTickChild, xml_tree, "id") + return BtTickChild(child_id) + + def __init__(self, child_id: Union[str, int]): + assert isinstance( + child_id, (str, int) + ), f"Error: SCXML BT Tick Child: invalid child id type {type(child_id)}." + self._child = child_id + + def as_xml(self) -> ET.Element: + xml_bt_tick_child = ET.Element(BtTickChild.get_tag_name(), {"id": str(self._child)}) + return xml_bt_tick_child From 02b74548d63b1aeff430134cd05737ec985bb5ea Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 10:34:44 +0200 Subject: [PATCH 06/46] First skeleton on new BT tags Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 17 +++++ .../scxml_entries/scxml_bt_ticks.py | 75 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index cdc40250..00fda812 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 @@ -27,6 +28,22 @@ RESERVED_BT_PORT_NAMES = ["NAME", "ID", "category"] +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.") + + def is_bt_event(event_name: str) -> bool: """Given an event name, returns whether it is related to a BT event or not.""" bt_events = [f"bt_{suffix}" for suffix in ["tick", "running", "success", "failure"]] diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 39317c3d..d7c7b456 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -21,13 +21,19 @@ from lxml import etree as ET -from as2fm.scxml_converter.scxml_entries import ScxmlSend, ScxmlTransition +from as2fm.scxml_converter.scxml_entries import ( + ScxmlExecutionBody, + ScxmlSend, + ScxmlTransition, + execution_body_from_xml, +) +from as2fm.scxml_converter.scxml_entries.bt_utils import BtResponse from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument class BtTick(ScxmlTransition): """ - Tick a BT plugin/control node, triggering the related transition. + Process a BT plugin/control node tick, triggering the related transition. """ @staticmethod @@ -73,3 +79,68 @@ def __init__(self, child_id: Union[str, int]): def as_xml(self) -> ET.Element: xml_bt_tick_child = ET.Element(BtTickChild.get_tag_name(), {"id": str(self._child)}) 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) + child_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_id, target, condition, body) + + def __init__( + self, + child_id: Union[str, int], + target: str, + condition: Optional[str] = None, + body: Optional[ScxmlExecutionBody] = None, + ): + self._child_id = child_id + self._target = target + self._condition = condition + self._body = body + + def as_xml(self) -> ET.Element: + xml_bt_child_status = ET.Element( + BtChildStatus.get_tag_name(), {"id": str(self._child_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 as_xml(self) -> ET.Element: + return ET.Element(BtReturnStatus.get_tag_name(), {"status": self._status}) From 019d6c3ec3515c191447a93401cd1d433e1b1022 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 10:51:36 +0200 Subject: [PATCH 07/46] Prepare for testing new bt functionality Signed-off-by: Marco Lampacrescia --- .../battery_drainer.scxml | 30 +++++ .../battery_manager.scxml | 26 +++++ .../battery_properties.jani | 107 ++++++++++++++++++ .../_test_data/ros_example_w_bt_new/bt.xml | 8 ++ .../bt_topic_action.scxml | 22 ++++ .../bt_topic_condition.scxml | 31 +++++ .../_test_data/ros_example_w_bt_new/main.xml | 20 ++++ .../test_systemtest_scxml_to_jani.py | 6 + 8 files changed, 250 insertions(+) create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml create mode 100644 test/jani_generator/_test_data/ros_example_w_bt_new/main.xml diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml new file mode 100644 index 00000000..5e17f313 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml new file mode 100644 index 00000000..ee8b8299 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani b/test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani new file mode 100644 index 00000000..fc6fc3e6 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/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_charge_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_new/bt.xml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml new file mode 100644 index 00000000..e5eadfbe --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml new file mode 100644 index 00000000..8fd0168c --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml new file mode 100644 index 00000000..58ba0daa --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/main.xml b/test/jani_generator/_test_data/ros_example_w_bt_new/main.xml new file mode 100644 index 00000000..cf6888f0 --- /dev/null +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/main.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index d7b7f581..188e6b64 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -257,6 +257,12 @@ 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) + @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") + def test_battery_example_w_new_bt_battery_depleted(self): + """Here we expect the property to be *not* satisfied.""" + # TODO: Improve properties under evaluation! + self._test_with_main("ros_example_w_bt_new", True, "battery_depleted", False) + def test_battery_example_w_bt_battery_depleted(self): """Here we expect the property to be *not* satisfied.""" # TODO: Improve properties under evaluation! From 045f5f79761997eeeb82c005059efb626a28c082 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 11:29:07 +0200 Subject: [PATCH 08/46] Prepare example using new format Signed-off-by: Marco Lampacrescia --- .../bt_control_nodes/bt_if_then_else.scxml | 10 ++-- .../bt_reactive_sequence.scxml | 59 +++++++++++++++++++ .../scxml_entries/scxml_bt_ticks.py | 15 ++++- .../_test_data/ros_example_w_bt_new/bt.xml | 4 +- .../bt_topic_action.scxml | 7 +-- .../bt_topic_condition.scxml | 8 +-- .../test_systemtest_scxml_to_jani.py | 2 +- 7 files changed, 86 insertions(+), 19 deletions(-) rename {test/scxml_converter/_test_data => src/as2fm/scxml_converter/resources}/bt_control_nodes/bt_if_then_else.scxml (91%) create mode 100644 src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml diff --git a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml b/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_if_then_else.scxml similarity index 91% rename from test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml rename to src/as2fm/scxml_converter/resources/bt_control_nodes/bt_if_then_else.scxml index 0c6f363c..56e69388 100644 --- a/test/scxml_converter/_test_data/bt_control_nodes/bt_if_then_else.scxml +++ b/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_if_then_else.scxml @@ -38,10 +38,10 @@ - + - + @@ -53,14 +53,14 @@ - + - + - + diff --git a/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml b/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml new file mode 100644 index 00000000..25b1e6bf --- /dev/null +++ b/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index d7c7b456..1b6b9b85 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -45,15 +45,24 @@ 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) - return BtTick(target, condition) + body = execution_body_from_xml(xml_tree) + return BtTick(target, condition, body) - def __init__(self, target: str, condition: Optional[str] = None): - super().__init__(target, ["bt_tick"], condition) + def __init__( + self, + target: str, + condition: Optional[str] = None, + body: Optional[ScxmlExecutionBody] = None, + ): + super().__init__(target, ["bt_tick"], condition, 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 diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml index e5eadfbe..ea27d272 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml @@ -1,8 +1,8 @@ - + - + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml index 8fd0168c..d177d50b 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml @@ -12,11 +12,10 @@ - + - - - + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml index 58ba0daa..7106ca9c 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml @@ -19,13 +19,13 @@ - + - + - + - + diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 188e6b64..4483b70a 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -257,7 +257,7 @@ 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) - @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") + # @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") def test_battery_example_w_new_bt_battery_depleted(self): """Here we expect the property to be *not* satisfied.""" # TODO: Improve properties under evaluation! From 1f8f6ffb38197db77bf744d3cafc229bd046fc27 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 14:07:43 +0200 Subject: [PATCH 09/46] Start implementing new BT SCXML generator Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 60 +++++++++++++++++++ .../scxml_converter/scxml_entries/__init__.py | 1 + .../test_systemtest_scxml_to_jani.py | 2 +- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 3dedf344..64f04775 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -26,9 +26,12 @@ 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, @@ -156,3 +159,60 @@ def bt_converter( generated_scxmls.append(bt_scxml_root) return generated_scxmls + + +def bt_converter_new( + bt_xml_path: str, bt_plugins_scxml_paths: List[str], bt_tick_rate: float +) -> List[ScxmlRoot]: + """ + Generate all Scxml files resulting from a Behavior Tree (BT) in XML format. + """ + xml_tree: ET.ElementBase = ET.parse(bt_xml_path).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, bt_plugins_scxml_paths + ) + return generated_scxmls + + +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(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=[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) + # TODO: BT children handling interface must be finalized + bt_scxml_root.append_bt_child_id(tick_id) + # TODO: Decide how to handle the BT children (might be expanded when getting the plain SCXML) + return bt_scxml_root + + +def generate_bt_children_scxmls( + bt_xml_tree: ET.ElementBase, root_child_tick_idx: int, bt_plugins_scxml_paths: List[str] +) -> List[ScxmlRoot]: + """ + Generate the SCXML files for the children of a Behavior Tree. + """ + pass diff --git a/src/as2fm/scxml_converter/scxml_entries/__init__.py b/src/as2fm/scxml_converter/scxml_entries/__init__.py index d51cb1bb..ec1f299c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/__init__.py +++ b/src/as2fm/scxml_converter/scxml_entries/__init__.py @@ -25,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/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 4483b70a..188e6b64 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -257,7 +257,7 @@ 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) - # @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") + @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") def test_battery_example_w_new_bt_battery_depleted(self): """Here we expect the property to be *not* satisfied.""" # TODO: Improve properties under evaluation! From f86b9042535ebf47d3f9d4ddc3270bcc1631ecca Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 15:38:44 +0200 Subject: [PATCH 10/46] Integrate bt_children ids in the scxml root Signed-off-by: Marco Lampacrescia --- pyproject.toml | 2 +- src/as2fm/scxml_converter/bt_converter.py | 6 +++--- .../scxml_entries/scxml_bt_ports.py | 2 +- .../scxml_entries/scxml_executable_entries.py | 14 ++++++------- .../scxml_entries/scxml_root.py | 21 ++++++++++++++----- .../scxml_entries/scxml_ros_action_client.py | 6 +++--- .../scxml_entries/scxml_state.py | 19 +++++++++++++---- .../scxml_entries/scxml_transition.py | 5 +++-- .../test_systemtest_scxml_entries.py | 2 +- test/scxml_converter/test_systemtest_xml.py | 2 +- 10 files changed, 51 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a8691e2d..17495264 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ where = ["src"] include = ["as2fm", "as2fm.*"] [tool.setuptools.package-dir] -as2fm = "src/as2fm" +"as2fm" = "src/as2fm" [tool.setuptools.package-data] "as2fm.trace_visualizer" = ["data/slkscr.ttf"] diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 64f04775..8372a961 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -104,14 +104,14 @@ def bt_converter( 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) + scxml_plugin_instance.set_bt_plugin_id(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() + scxml_plugin_instance.instantiate_bt_information() assert ( scxml_plugin_instance.check_validity() ), f"Error: SCXML plugin instance {instance_name} is not valid." @@ -205,7 +205,7 @@ def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> S bt_scxml_root.add_state(error_state) # TODO: BT children handling interface must be finalized bt_scxml_root.append_bt_child_id(tick_id) - # TODO: Decide how to handle the BT children (might be expanded when getting the plain SCXML) + bt_scxml_root.instantiate_bt_information() return bt_scxml_root diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py index 370d95e4..58d00570 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py @@ -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_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index f0843409..f6bcc62d 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -51,7 +51,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. @@ -61,7 +61,7 @@ def instantiate_exec_body_bt_events( """ if exec_body is not None: for entry in exec_body: - entry.instantiate_bt_events(instance_id) + entry.instantiate_bt_events(instance_id, children_ids) def update_exec_body_bt_ports_values( @@ -153,11 +153,11 @@ 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]) -> None: """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) def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): for _, exec_body in self._conditional_executions: @@ -285,7 +285,7 @@ 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, _) -> None: """Instantiate the behavior tree events in the send action, if available.""" # 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): @@ -378,7 +378,7 @@ 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, _, __) -> None: """This functionality is not needed in this class.""" return diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index e061acdf..5fe87e4c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -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,9 @@ 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: str) -> 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 add_state(self, state: ScxmlState, *, initial: bool = False): """Append a state to the list of states in the SCXML model. @@ -207,8 +208,17 @@ 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 childrebn IDs in the SCXML entries.""" + n_bt_children = len(self._bt_children_ids) + # 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 +226,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]: 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..8f7a173b 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, _, __) -> "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..ce26cafc 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -141,12 +141,23 @@ 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_transition = transition.instantiate_bt_events(instance_id, children_ids) + if isinstance(new_transition, ScxmlTransition): + instantiated_transitions.append(new_transition) + elif isinstance(new_transition, list) and all( + isinstance(t, ScxmlTransition) for t in new_transition + ): + instantiated_transitions.extend(new_transition) + else: + raise ValueError( + f"Error: SCXML state {self._id}: found invalid transition in state body." + ) + 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.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index 7bb65c89..f3613db5 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -116,7 +116,7 @@ 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]) -> "ScxmlTransition": """Instantiate the BT events of this transition.""" # Make sure to replace received events only for ScxmlTransition objects. if type(self) is ScxmlTransition: @@ -125,7 +125,8 @@ def instantiate_bt_events(self, instance_id: str): if is_bt_event(event_str): self._events[event_id] = replace_bt_event(event_str, instance_id) # 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.""" diff --git a/test/scxml_converter/test_systemtest_scxml_entries.py b/test/scxml_converter/test_systemtest_scxml_entries.py index 3077181e..cffb4b95 100644 --- a/test/scxml_converter/test_systemtest_scxml_entries.py +++ b/test/scxml_converter/test_systemtest_scxml_entries.py @@ -196,7 +196,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..2c61b430 100644 --- a/test/scxml_converter/test_systemtest_xml.py +++ b/test/scxml_converter/test_systemtest_xml.py @@ -97,7 +97,7 @@ def ros_to_plain_scxml_test( scxml_obj = ScxmlRoot.from_scxml_file(input_file) if fname in scxml_bt_ports: 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: From f7e8f90c7ec6ba79abd42281618520ac11858b2d Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 15:51:54 +0200 Subject: [PATCH 11/46] Fix package not found in ipython Signed-off-by: Marco Lampacrescia --- pyproject.toml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17495264..ae3884cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,15 +35,16 @@ dependencies = [ ] requires-python = ">=3.10" -[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"] +# 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"] [project.scripts] as2fm_convince_to_plain_jani = "as2fm.jani_generator.main:main_convince_to_plain_jani" From f11c651d22243935c87d01e6fed44cd7c32800f6 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 16:20:46 +0200 Subject: [PATCH 12/46] Move controllers scxml to another folder Signed-off-by: Marco Lampacrescia --- .../resources/bt_control_nodes/bt_if_then_else.scxml | 0 .../resources/bt_control_nodes/bt_reactive_sequence.scxml | 0 .../scxml_converter/scxml_entries/scxml_executable_entries.py | 3 +++ 3 files changed, 3 insertions(+) rename src/as2fm/{scxml_converter => }/resources/bt_control_nodes/bt_if_then_else.scxml (100%) rename src/as2fm/{scxml_converter => }/resources/bt_control_nodes/bt_reactive_sequence.scxml (100%) diff --git a/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_if_then_else.scxml b/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml similarity index 100% rename from src/as2fm/scxml_converter/resources/bt_control_nodes/bt_if_then_else.scxml rename to src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml diff --git a/src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml b/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml similarity index 100% rename from src/as2fm/scxml_converter/resources/bt_control_nodes/bt_reactive_sequence.scxml rename to src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml 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 f6bcc62d..c84e07bf 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -466,6 +466,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 ( From dab8efe18f3a0ac36ee41549a2c1748c994ff46d Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 9 Oct 2024 17:07:06 +0200 Subject: [PATCH 13/46] Check type of BtTickChild id Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 5 +++ .../scxml_entries/scxml_bt_ticks.py | 36 +++++++++++++++++-- .../scxml_entries/scxml_transition.py | 18 +++++----- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 00fda812..eb4cb222 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -44,6 +44,11 @@ def str_to_int(resp_str: str) -> int: raise ValueError(f"Error: {resp_str} is an invalid BT Status type.") +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 is_bt_event(event_name: str) -> bool: """Given an event name, returns whether it is related to a BT event or not.""" bt_events = [f"bt_{suffix}" for suffix in ["tick", "running", "success", "failure"]] diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 1b6b9b85..f2c1b3cb 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -17,17 +17,20 @@ SCXML entries related to Behavior Tree Ticks and related responses. """ -from typing import Optional, Union +from typing import List, Optional, Union from lxml import etree as ET from as2fm.scxml_converter.scxml_entries import ( ScxmlExecutionBody, + ScxmlIf, ScxmlSend, ScxmlTransition, execution_body_from_xml, + instantiate_exec_body_bt_events, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import BtResponse +from as2fm.scxml_converter.scxml_entries.bt_utils import BtResponse, generate_bt_tick_event +from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument @@ -56,6 +59,14 @@ def __init__( ): 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]) -> 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: @@ -84,6 +95,27 @@ def __init__(self, child_id: Union[str, int]): child_id, (str, int) ), f"Error: SCXML BT Tick Child: invalid child id type {type(child_id)}." self._child = child_id + if isinstance(child_id, str): + child_id = child_id.strip() + try: + self._child = int(child_id) + except ValueError: + self._child = child_id + assert is_non_empty_string(BtTickChild, "id", self._child) + assert ( + self._child.isidentifier() + ), f"Error: SCXML BT Tick Child: invalid child id '{self._child}'." + + 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. + """ + raise NotImplementedError("Error: SCXML BT Tick Child: instantiation not implemented.") def as_xml(self) -> ET.Element: xml_bt_tick_child = ET.Element(BtTickChild.get_tag_name(), {"id": str(self._child)}) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index f3613db5..6b65fe86 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -39,7 +39,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): @@ -145,22 +149,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 From 64e1ed499314c9fa5644f49cf342573cff907912 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 14:53:38 +0200 Subject: [PATCH 14/46] Continue new import of bt plugins Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 59 ++++++++++++++++--- .../scxml_entries/scxml_root.py | 9 ++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 8372a961..5e561e2e 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -21,7 +21,7 @@ import re from copy import deepcopy from enum import Enum, auto -from typing import List +from typing import Dict, List from btlib.bt_to_fsm.bt_to_fsm import Bt2FSM from btlib.bts import xml_to_networkx @@ -161,12 +161,23 @@ def bt_converter( return generated_scxmls +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}) + return available_bt_plugins + + def bt_converter_new( bt_xml_path: str, bt_plugins_scxml_paths: List[str], bt_tick_rate: float ) -> List[ScxmlRoot]: """ Generate all Scxml files resulting from a Behavior Tree (BT) in XML format. """ + # TODO: load SCXML plugins in as2fm's resources + available_bt_plugins = load_available_bt_plugins(bt_plugins_scxml_paths) xml_tree: ET.ElementBase = ET.parse(bt_xml_path).getroot() root_children = xml_tree.getchildren() assert len(root_children) == 1, f"Error: Expected one root element, found {len(root_children)}." @@ -181,7 +192,7 @@ def bt_converter_new( 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, bt_plugins_scxml_paths + bt_children[0], root_child_tick_idx, available_bt_plugins ) return generated_scxmls @@ -194,25 +205,59 @@ def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> S 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)])] + "idle", body=[RosRateCallback(ros_rate_decl, "wait_tick_res", None, [BtTickChild(tick_id)])] ) wait_res_state = ScxmlState( - "wait_tick_res", body=[RosRateCallback(ros_rate_decl, "error"), BtChildStatus(0, "idle")] + "wait_tick_res", + body=[RosRateCallback(ros_rate_decl, "error"), BtChildStatus(tick_id, "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) - # TODO: BT children handling interface must be finalized + # The BT root's ID is set to 0 (unused anyway) + bt_scxml_root.set_bt_plugin_id(0) 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: + """ + 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 generate_bt_children_scxmls( - bt_xml_tree: ET.ElementBase, root_child_tick_idx: int, bt_plugins_scxml_paths: List[str] + 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. """ - pass + generated_scxmls: List[ScxmlRoot] = [] + 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_bt_plugin_id(subtree_tick_idx) + 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/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 5fe87e4c..eba5a657 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -155,10 +155,14 @@ def get_state_by_id(self, state_id: str) -> Optional[ScxmlState]: return state return None - def set_bt_plugin_id(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.""" 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. If initial is True, set it as the initial state.""" @@ -214,8 +218,9 @@ def append_bt_child_id(self, child_id: int): self._bt_children_ids.append(child_id) def instantiate_bt_information(self): - """Instantiate the values of BT ports and childrebn IDs in the SCXML entries.""" + """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)) From 44651f84eb501958105a4fc4263048772ddd5757 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 15:31:21 +0200 Subject: [PATCH 15/46] Install and import plugins in resources Signed-off-by: Marco Lampacrescia --- pyproject.toml | 7 ++++--- src/as2fm/scxml_converter/bt_converter.py | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ae3884cc..4060234a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,10 @@ requires-python = ">=3.10" # # [tool.setuptools.package-dir] # "as2fm" = "src/as2fm" -# -# [tool.setuptools.package-data] -# "as2fm.trace_visualizer" = ["data/slkscr.ttf"] + +[tool.setuptools.package-data] +"as2fm.trace_visualizer" = ["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/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 5e561e2e..65c93688 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -21,6 +21,7 @@ import re from copy import deepcopy from enum import Enum, auto +from importlib.resources import path as resource_path from typing import Dict, List from btlib.bt_to_fsm.bt_to_fsm import Bt2FSM @@ -167,6 +168,11 @@ def load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, Sc 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_path("as2fm", "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 From a9bf8de96402976efa9b06aaf0998bcc67621071 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 16:52:23 +0200 Subject: [PATCH 16/46] Continue integration Signed-off-by: Marco Lampacrescia --- .../scxml_helpers/top_level_interpreter.py | 4 +- .../bt_control_nodes/bt_if_then_else.scxml | 10 ++--- .../bt_reactive_sequence.scxml | 8 ++-- .../scxml_converter/scxml_entries/bt_utils.py | 12 ++++++ .../scxml_entries/scxml_bt_ticks.py | 39 ++++++++++++++++++- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py index 066a6aa9..9c4d72d8 100644 --- a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py +++ b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py @@ -35,7 +35,7 @@ from as2fm.jani_generator.ros_helpers.ros_service_handler import RosServiceHandler from as2fm.jani_generator.ros_helpers.ros_timer import RosTimer, make_global_timer_scxml from as2fm.jani_generator.scxml_helpers.scxml_to_jani import convert_multiple_scxmls_to_jani -from as2fm.scxml_converter.bt_converter import bt_converter +from as2fm.scxml_converter.bt_converter import bt_converter_new from as2fm.scxml_converter.scxml_entries import ScxmlRoot @@ -159,7 +159,7 @@ def generate_plain_scxml_models_and_timers( ros_scxmls.append(ScxmlRoot.from_scxml_file(fname)) # Convert behavior tree and plugins to ROS-SCXML if model.bt is not None: - ros_scxmls.extend(bt_converter(model.bt, model.plugins, model.bt_tick_rate)) + ros_scxmls.extend(bt_converter_new(model.bt, model.plugins, model.bt_tick_rate)) # Convert the loaded entries to plain SCXML plain_scxml_models = [] all_timers: List[RosTimer] = [] diff --git a/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml b/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml index 56e69388..7014fa3d 100644 --- a/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml +++ b/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml @@ -20,7 +20,7 @@ - + @@ -34,10 +34,10 @@ - + - + @@ -47,9 +47,9 @@ - + - + diff --git a/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml b/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml index 25b1e6bf..dd886cd2 100644 --- a/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml +++ b/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml @@ -21,7 +21,7 @@ - + @@ -31,7 +31,7 @@ - + @@ -46,11 +46,11 @@ - + - + diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index eb4cb222..f4714eda 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -43,12 +43,24 @@ def str_to_int(resp_str: str) -> int: 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: """Given an event name, returns whether it is related to a BT event or not.""" bt_events = [f"bt_{suffix}" for suffix in ["tick", "running", "success", "failure"]] diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index f2c1b3cb..3b093737 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -24,12 +24,17 @@ from as2fm.scxml_converter.scxml_entries import ( 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_tick_event +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 is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument @@ -150,8 +155,32 @@ def __init__( self._child_id = child_id self._target = target self._condition = condition + if self._condition is not None: + self._condition = BtResponse.process_expr(self._condition) self._body = body + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int] + ) -> List[ScxmlTransition]: + if isinstance(self._child_id, int): + # Handling specific child ID, return a single transition + assert self._child_id < len(children_ids), ( + f"Error: SCXML BT Child Status: invalid child ID {self._child_id} " + f"for {len(children_ids)} children." + ) + target_child_id = children_ids[self._child_id] + return [ + ScxmlTransition( + self._target, + [generate_bt_tick_event(target_child_id)], + self._condition, + self._body, + ).instantiate_bt_events(instance_id, children_ids) + ] + else: + # Handling a generic child ID, return a transition for each child + raise NotImplementedError(f"BtChildStatus need to handle a variable {self._child_id}.") + def as_xml(self) -> ET.Element: xml_bt_child_status = ET.Element( BtChildStatus.get_tag_name(), {"id": str(self._child_id), "target": self._target} @@ -183,5 +212,13 @@ 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, children_ids: List[int]) -> ScxmlSend: + return ScxmlSend( + generate_bt_response_event(instance_id), [ScxmlParam("status", expr=self._status_id)] + ) + def as_xml(self) -> ET.Element: return ET.Element(BtReturnStatus.get_tag_name(), {"status": self._status}) From bc16c967c522e274557c772947252308e3834ab1 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 17:59:42 +0200 Subject: [PATCH 17/46] Implement missing conversions Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 8 +++--- .../scxml_entries/scxml_bt_ticks.py | 27 +++++++++++++++++-- .../scxml_entries/scxml_executable_entries.py | 16 ++++++----- .../scxml_entries/scxml_state.py | 1 + 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 65c93688..7670e52f 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -211,18 +211,18 @@ def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> S 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(tick_id)])] + "idle", body=[RosRateCallback(ros_rate_decl, "wait_tick_res", None, [BtTickChild(0)])] ) wait_res_state = ScxmlState( "wait_tick_res", - body=[RosRateCallback(ros_rate_decl, "error"), BtChildStatus(tick_id, "idle")], + body=[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 0 (unused anyway) - bt_scxml_root.set_bt_plugin_id(0) + # 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 diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 3b093737..584f9bf7 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -120,7 +120,21 @@ def instantiate_bt_events( """ Convert the BtTickChild to ScxmlSend if the child id is constant and an ScxmlIf otherwise. """ - raise NotImplementedError("Error: SCXML BT Tick Child: instantiation not implemented.") + if isinstance(self._child, int): + # We know the exact child ID we want to tick + assert self._child < len(children_ids), ( + f"Error: SCXML BT Tick Child: invalid child ID {self._child} " + f"for {len(children_ids)} children." + ) + return ScxmlSend(generate_bt_tick_event(children_ids[self._child])) + else: + # The children to tick depends on the index of the self._child variable at runtime + if_bodies = [] + for child_id in children_ids: + if_bodies.append( + (f"{self._child} == {child_id}", [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)}) @@ -179,7 +193,16 @@ def instantiate_bt_events( ] else: # Handling a generic child ID, return a transition for each child - raise NotImplementedError(f"BtChildStatus need to handle a variable {self._child_id}.") + condition_prefix = "" if self._condition is None else f"({self._condition}) && " + return [ + ScxmlTransition( + self._target, + [generate_bt_tick_event(child_id)], + condition_prefix + f"({self._child_id} == {child_id})", + self._body, + ).instantiate_bt_events(instance_id, children_ids) + for child_id in children_ids + ] def as_xml(self) -> ET.Element: xml_bt_child_status = ET.Element( 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 c84e07bf..bca16326 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -60,8 +60,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, children_ids) + 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 +155,12 @@ def get_else_execution(self) -> ScxmlExecutionBody: """Get the else execution.""" return self._else_execution - def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> 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, 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 +288,13 @@ def get_params(self) -> List[ScxmlParam]: """Get the parameters to send.""" return self._params - def instantiate_bt_events(self, instance_id: int, _) -> None: + def instantiate_bt_events(self, instance_id: int, _) -> "ScxmlSend": """Instantiate the behavior tree events in the send action, if available.""" # 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): # Those are expected to be only bt_success, bt_failure and bt_running self._event = replace_bt_event(self._event, 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.""" @@ -378,9 +382,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.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index ce26cafc..41d7af19 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -156,6 +156,7 @@ def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> No raise ValueError( f"Error: SCXML state {self._id}: found invalid transition in state body." ) + 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) From 5d2f706f9fc0a2439d3d2b04d20356360d4fe37f Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 18:10:29 +0200 Subject: [PATCH 18/46] First scxml without errors Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py | 3 ++- .../scxml_entries/scxml_executable_entries.py | 2 +- src/as2fm/scxml_converter/scxml_entries/scxml_root.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 584f9bf7..a9272f97 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -240,7 +240,8 @@ def check_validity(self) -> bool: def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> ScxmlSend: return ScxmlSend( - generate_bt_response_event(instance_id), [ScxmlParam("status", expr=self._status_id)] + generate_bt_response_event(instance_id), + [ScxmlParam("status", expr=f"{self._status_id}")], ) def as_xml(self) -> ET.Element: 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 bca16326..fa20d7db 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -310,7 +310,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: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index eba5a657..9917cbfd 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -257,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 ) From f93c9e3bb51344aa85ada1fba8e8d03b52655849 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 18:29:19 +0200 Subject: [PATCH 19/46] Substitution of xml escape sequence and re-order ecmascript entities handling Signed-off-by: Marco Lampacrescia --- src/as2fm/as2fm_common/common.py | 10 ++++ .../as2fm_common/ecmascript_interpretation.py | 3 +- .../scxml_helpers/scxml_expression.py | 51 ++++++++++--------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/as2fm/as2fm_common/common.py b/src/as2fm/as2fm_common/common.py index b0b494c1..19f26ad0 100644 --- a/src/as2fm/as2fm_common/common.py +++ b/src/as2fm/as2fm_common/common.py @@ -59,6 +59,16 @@ def remove_namespace(tag: str) -> str: return tag_wo_ns +def substitute_xml_escaping(text: str) -> str: + """ + Substitute the XML escaping characters in the text. + + :param text: The text to substitute the characters in. + :return: The text with the characters substituted. + """ + return text.replace("<", "<").replace(">", ">").replace("&", "&") + + def is_comment(element: _Element) -> bool: """ Check if an element is a comment. diff --git a/src/as2fm/as2fm_common/ecmascript_interpretation.py b/src/as2fm/as2fm_common/ecmascript_interpretation.py index 1437f3db..13f3f479 100644 --- a/src/as2fm/as2fm_common/ecmascript_interpretation.py +++ b/src/as2fm/as2fm_common/ecmascript_interpretation.py @@ -22,7 +22,7 @@ import js2py -from as2fm.as2fm_common.common import ValidTypes +from as2fm.as2fm_common.common import ValidTypes, substitute_xml_escaping BasicJsTypes = Union[int, float, bool] @@ -36,6 +36,7 @@ def interpret_ecma_script_expr( :param expr: The ECMA script expression :return: The interpreted object """ + expr = substitute_xml_escaping(expr) if variables is None: variables = {} context = js2py.EvalJs(variables) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py index be790a0c..17180b07 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py @@ -22,6 +22,7 @@ import esprima +from as2fm.as2fm_common.common import substitute_xml_escaping from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import ( CALLABLE_OPERATORS_MAP, OPERATORS_TO_JANI_MAP, @@ -53,7 +54,11 @@ 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) + ecmascript = substitute_xml_escaping(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 +80,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 +100,18 @@ def _parse_ecmascript_to_jani_expression( "right": _parse_ecmascript_to_jani_expression(ast.argument, 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 == "ArrayExpression": assert array_info is not None, "Array info must be provided for ArrayExpressions." entry_type: Type = array_info.array_type @@ -105,13 +131,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 +149,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 From 54ea01eacfe5c4c13650f89f6f52b5887fb56424 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 14 Oct 2024 18:32:03 +0200 Subject: [PATCH 20/46] First jani result Signed-off-by: Marco Lampacrescia --- src/as2fm/jani_generator/scxml_helpers/scxml_expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py index 17180b07..617f31ee 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py @@ -100,7 +100,7 @@ def _parse_ecmascript_to_jani_expression( "right": _parse_ecmascript_to_jani_expression(ast.argument, array_info), } ) - elif ast.type == "BinaryExpression": + elif ast.type == "BinaryExpression" or ast.type == "LogicalExpression": # It is a more complex expression assert ( ast.operator in OPERATORS_TO_JANI_MAP From e00f5994974024bab462973c45b767c8d25570c7 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 10:19:50 +0200 Subject: [PATCH 21/46] Various fixes Signed-off-by: Marco Lampacrescia --- src/as2fm/as2fm_common/common.py | 10 --------- .../as2fm_common/ecmascript_interpretation.py | 3 +-- .../scxml_helpers/scxml_expression.py | 2 -- .../scxml_entries/scxml_bt_ticks.py | 17 +++++++++----- .../scxml_converter/scxml_entries/utils.py | 22 +++++++++++-------- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/as2fm/as2fm_common/common.py b/src/as2fm/as2fm_common/common.py index 19f26ad0..b0b494c1 100644 --- a/src/as2fm/as2fm_common/common.py +++ b/src/as2fm/as2fm_common/common.py @@ -59,16 +59,6 @@ def remove_namespace(tag: str) -> str: return tag_wo_ns -def substitute_xml_escaping(text: str) -> str: - """ - Substitute the XML escaping characters in the text. - - :param text: The text to substitute the characters in. - :return: The text with the characters substituted. - """ - return text.replace("<", "<").replace(">", ">").replace("&", "&") - - def is_comment(element: _Element) -> bool: """ Check if an element is a comment. diff --git a/src/as2fm/as2fm_common/ecmascript_interpretation.py b/src/as2fm/as2fm_common/ecmascript_interpretation.py index 13f3f479..1437f3db 100644 --- a/src/as2fm/as2fm_common/ecmascript_interpretation.py +++ b/src/as2fm/as2fm_common/ecmascript_interpretation.py @@ -22,7 +22,7 @@ import js2py -from as2fm.as2fm_common.common import ValidTypes, substitute_xml_escaping +from as2fm.as2fm_common.common import ValidTypes BasicJsTypes = Union[int, float, bool] @@ -36,7 +36,6 @@ def interpret_ecma_script_expr( :param expr: The ECMA script expression :return: The interpreted object """ - expr = substitute_xml_escaping(expr) if variables is None: variables = {} context = js2py.EvalJs(variables) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py index 617f31ee..7e92ccb5 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_expression.py @@ -22,7 +22,6 @@ import esprima -from as2fm.as2fm_common.common import substitute_xml_escaping from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import ( CALLABLE_OPERATORS_MAP, OPERATORS_TO_JANI_MAP, @@ -54,7 +53,6 @@ def parse_ecmascript_to_jani_expression( :param array_info: The type and max size of the array, if required. :return: The jani expression. """ - ecmascript = substitute_xml_escaping(ecmascript) try: ast = esprima.parseScript(ecmascript) except esprima.error_handler.Error as e: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index a9272f97..b625613e 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -35,7 +35,11 @@ generate_bt_response_event, generate_bt_tick_event, ) -from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string +from as2fm.scxml_converter.scxml_entries.utils import ( + CallbackType, + get_plain_expression, + is_non_empty_string, +) from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument @@ -176,6 +180,9 @@ def __init__( 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_id, int): # Handling specific child ID, return a single transition assert self._child_id < len(children_ids), ( @@ -186,18 +193,18 @@ def instantiate_bt_events( return [ ScxmlTransition( self._target, - [generate_bt_tick_event(target_child_id)], - self._condition, + [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 self._condition is None else f"({self._condition}) && " + condition_prefix = "" if plain_cond_expr is None else f"({plain_cond_expr}) && " return [ ScxmlTransition( self._target, - [generate_bt_tick_event(child_id)], + [generate_bt_response_event(child_id)], condition_prefix + f"({self._child_id} == {child_id})", self._body, ).instantiate_bt_events(instance_id, children_ids) diff --git a/src/as2fm/scxml_converter/scxml_entries/utils.py b/src/as2fm/scxml_converter/scxml_entries/utils.py index 38f71f92..b35a7459 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 From dc64bac93d3ab9b8fe585879d6e2b2568be1de6a Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 11:19:15 +0200 Subject: [PATCH 22/46] Fix bug and improve variables naming Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_bt_ticks.py | 114 +++++++++++------- .../scxml_converter/scxml_entries/utils.py | 13 ++ 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index b625613e..42d1442e 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -17,11 +17,12 @@ SCXML entries related to Behavior Tree Ticks and related responses. """ -from typing import List, Optional, Union +from typing import List, Optional, Type, Union from lxml import etree as ET from as2fm.scxml_converter.scxml_entries import ( + ScxmlBase, ScxmlExecutionBody, ScxmlIf, ScxmlParam, @@ -35,14 +36,32 @@ generate_bt_response_event, generate_bt_tick_event, ) -from as2fm.scxml_converter.scxml_entries.utils import ( - CallbackType, - get_plain_expression, - is_non_empty_string, -) +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. @@ -96,24 +115,18 @@ def get_tag_name() -> str: @staticmethod def from_xml_tree(xml_tree: ET.Element) -> "BtTickChild": assert_xml_tag_ok(BtTickChild, xml_tree) - child_id: str = get_xml_argument(BtTickChild, xml_tree, "id") - return BtTickChild(child_id) - - def __init__(self, child_id: Union[str, int]): - assert isinstance( - child_id, (str, int) - ), f"Error: SCXML BT Tick Child: invalid child id type {type(child_id)}." - self._child = child_id - if isinstance(child_id, str): - child_id = child_id.strip() - try: - self._child = int(child_id) - except ValueError: - self._child = child_id - assert is_non_empty_string(BtTickChild, "id", self._child) - assert ( - self._child.isidentifier() - ), f"Error: SCXML BT Tick Child: invalid child id '{self._child}'." + # 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 @@ -124,24 +137,27 @@ def instantiate_bt_events( """ Convert the BtTickChild to ScxmlSend if the child id is constant and an ScxmlIf otherwise. """ - if isinstance(self._child, int): + if isinstance(self._child_seq_id, int): # We know the exact child ID we want to tick - assert self._child < len(children_ids), ( - f"Error: SCXML BT Tick Child: invalid child ID {self._child} " + 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])) + 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_id in children_ids: + for child_seq_n, child_id in enumerate(children_ids): if_bodies.append( - (f"{self._child} == {child_id}", [ScxmlSend(generate_bt_tick_event(child_id))]) + ( + 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)}) + xml_bt_tick_child = ET.Element(BtTickChild.get_tag_name(), {"id": str(self._child_seq_id)}) return xml_bt_tick_child @@ -157,23 +173,33 @@ def get_tag_name() -> str: @staticmethod def from_xml_tree(xml_tree): assert_xml_tag_ok(BtChildStatus, xml_tree) - child_id = get_xml_argument(BtChildStatus, xml_tree, "id") + # 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_id, target, condition, body) + return BtChildStatus(child_seq_id, target, condition, body) def __init__( self, - child_id: Union[str, int], + child_seq_id: Union[str, int], target: str, condition: Optional[str] = None, body: Optional[ScxmlExecutionBody] = None, ): - self._child_id = child_id + """ + 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 @@ -183,13 +209,13 @@ def instantiate_bt_events( 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_id, int): - # Handling specific child ID, return a single transition - assert self._child_id < len(children_ids), ( - f"Error: SCXML BT Child Status: invalid child ID {self._child_id} " + 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_id] + target_child_id = children_ids[self._child_seq_id] return [ ScxmlTransition( self._target, @@ -205,15 +231,15 @@ def instantiate_bt_events( ScxmlTransition( self._target, [generate_bt_response_event(child_id)], - condition_prefix + f"({self._child_id} == {child_id})", + condition_prefix + f"({self._child_seq_id} == {child_seq_n})", self._body, ).instantiate_bt_events(instance_id, children_ids) - for child_id in children_ids + for child_seq_n, child_id in enumerate(children_ids) ] def as_xml(self) -> ET.Element: xml_bt_child_status = ET.Element( - BtChildStatus.get_tag_name(), {"id": str(self._child_id), "target": self._target} + 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) @@ -245,7 +271,7 @@ def __init__(self, status: str): def check_validity(self) -> bool: return True - def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> ScxmlSend: + def instantiate_bt_events(self, instance_id: int, _) -> ScxmlSend: return ScxmlSend( generate_bt_response_event(instance_id), [ScxmlParam("status", expr=f"{self._status_id}")], diff --git a/src/as2fm/scxml_converter/scxml_entries/utils.py b/src/as2fm/scxml_converter/scxml_entries/utils.py index b35a7459..ed43e315 100644 --- a/src/as2fm/scxml_converter/scxml_entries/utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/utils.py @@ -198,6 +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. """ + arg_value = arg_value.strip() valid_str = isinstance(arg_value, str) and len(arg_value) > 0 if not valid_str: print( @@ -207,6 +208,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]: """ From dd6f90830c3d8f109a8b1c3c667a98cb8c0c5294 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 11:29:01 +0200 Subject: [PATCH 23/46] Yet another bug Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 7670e52f..fe94483d 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -215,7 +215,12 @@ def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> S ) wait_res_state = ScxmlState( "wait_tick_res", - body=[RosRateCallback(ros_rate_decl, "error"), BtChildStatus(0, "idle")], + 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) From 84a3e5729ac863327c204631e3bfd20027b28e9e Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 11:36:04 +0200 Subject: [PATCH 24/46] First working version Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 1 + test/jani_generator/test_systemtest_scxml_to_jani.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index fe94483d..6701281f 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -262,6 +262,7 @@ def generate_bt_children_scxmls( 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) generated_scxmls.append(bt_plugin_scxml) next_tick_idx = subtree_tick_idx + 1 diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 188e6b64..7aceb51b 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -257,12 +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) - @pytest.mark.skip(reason="Not yet working. Enable after BT engine refactoring.") def test_battery_example_w_new_bt_battery_depleted(self): """Here we expect the property to be *not* satisfied.""" - # TODO: Improve properties under evaluation! self._test_with_main("ros_example_w_bt_new", True, "battery_depleted", False) + def test_battery_example_w_new_bt_main_alarm_and_charge(self): + """Here we expect the property to be *not* satisfied.""" + self._test_with_main("ros_example_w_bt_new", 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! From c018af379510801d9b184f310d95b2cfb0ae0dd0 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 13:02:24 +0200 Subject: [PATCH 25/46] Support for old implementation Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_executable_entries.py | 24 ++++++++++++++----- .../scxml_entries/scxml_transition.py | 22 ++++++++++++----- .../_test_data/ros_example_w_bt/bt.xml | 4 ++-- 3 files changed, 36 insertions(+), 14 deletions(-) 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 fa20d7db..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, @@ -290,10 +287,25 @@ def get_params(self) -> List[ScxmlParam]: 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): diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index 6b65fe86..ce7b5c6b 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, @@ -122,12 +119,25 @@ def get_executable_body(self) -> ScxmlExecutionBody: def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> "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): # 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, children_ids) return self 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 @@ - + - + From 6e2692ae9dd6ced7e0e567d592c38972fc7c6df7 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 13:06:12 +0200 Subject: [PATCH 26/46] Rename battery_depleted_bt_tests Signed-off-by: Marco Lampacrescia --- .../_test_data/ros_example_w_bt/battery_properties.jani | 2 +- .../_test_data/ros_example_w_bt/bt_topic_action.scxml | 7 +++---- .../_test_data/ros_example_w_bt/bt_topic_condition.scxml | 8 ++++---- .../battery_drainer.scxml | 0 .../battery_manager.scxml | 0 .../battery_properties.jani | 2 +- .../bt.xml | 0 .../bt_topic_action.scxml | 7 ++++--- .../bt_topic_condition.scxml | 8 ++++---- .../main.xml | 0 test/jani_generator/test_systemtest_scxml_to_jani.py | 8 ++++---- 11 files changed, 21 insertions(+), 21 deletions(-) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/battery_drainer.scxml (100%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/battery_manager.scxml (100%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/battery_properties.jani (98%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/bt.xml (100%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/bt_topic_action.scxml (75%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/bt_topic_condition.scxml (83%) rename test/jani_generator/_test_data/{ros_example_w_bt_new => ros_example_w_bt_deprecated}/main.xml (100%) 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_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 @@ - + - - - + + 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 @@ - + - + - + - + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_drainer.scxml similarity index 100% rename from test/jani_generator/_test_data/ros_example_w_bt_new/battery_drainer.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_drainer.scxml diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_manager.scxml similarity index 100% rename from test/jani_generator/_test_data/ros_example_w_bt_new/battery_manager.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_manager.scxml diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani similarity index 98% rename from test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani index fc6fc3e6..6e9c167f 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/battery_properties.jani +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/battery_properties.jani @@ -65,7 +65,7 @@ "right": { "op": "∧", "left": "topic_alarm_msg.ros_fields__data", - "right": "topic_charge_msg.valid" + "right": "topic_alarm_msg.valid" } } }, diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt.xml similarity index 100% rename from test/jani_generator/_test_data/ros_example_w_bt_new/bt.xml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt.xml diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml similarity index 75% rename from test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml index d177d50b..8fd0168c 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_action.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_action.scxml @@ -12,10 +12,11 @@ - + - - + + + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml similarity index 83% rename from test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml index 7106ca9c..58ba0daa 100644 --- a/test/jani_generator/_test_data/ros_example_w_bt_new/bt_topic_condition.scxml +++ b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/bt_topic_condition.scxml @@ -19,13 +19,13 @@ - + - + - + - + diff --git a/test/jani_generator/_test_data/ros_example_w_bt_new/main.xml b/test/jani_generator/_test_data/ros_example_w_bt_deprecated/main.xml similarity index 100% rename from test/jani_generator/_test_data/ros_example_w_bt_new/main.xml rename to test/jani_generator/_test_data/ros_example_w_bt_deprecated/main.xml diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 7aceb51b..d5e0593e 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -257,13 +257,13 @@ 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_new_bt_battery_depleted(self): + 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_new", True, "battery_depleted", False) + self._test_with_main("ros_example_w_bt_deprecated", True, "battery_depleted", False) - def test_battery_example_w_new_bt_main_alarm_and_charge(self): + 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_new", False, "battery_alarm_on", True) + 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.""" From 3a9704e4027f574ee16ae86c34141aef6c89043e Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 13:21:38 +0200 Subject: [PATCH 27/46] Remove btlib-based BT conversion Signed-off-by: Marco Lampacrescia --- .../scxml_helpers/top_level_interpreter.py | 4 +- src/as2fm/scxml_converter/bt_converter.py | 133 +----------------- 2 files changed, 4 insertions(+), 133 deletions(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py index 9c4d72d8..066a6aa9 100644 --- a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py +++ b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py @@ -35,7 +35,7 @@ from as2fm.jani_generator.ros_helpers.ros_service_handler import RosServiceHandler from as2fm.jani_generator.ros_helpers.ros_timer import RosTimer, make_global_timer_scxml from as2fm.jani_generator.scxml_helpers.scxml_to_jani import convert_multiple_scxmls_to_jani -from as2fm.scxml_converter.bt_converter import bt_converter_new +from as2fm.scxml_converter.bt_converter import bt_converter from as2fm.scxml_converter.scxml_entries import ScxmlRoot @@ -159,7 +159,7 @@ def generate_plain_scxml_models_and_timers( ros_scxmls.append(ScxmlRoot.from_scxml_file(fname)) # Convert behavior tree and plugins to ROS-SCXML if model.bt is not None: - ros_scxmls.extend(bt_converter_new(model.bt, model.plugins, model.bt_tick_rate)) + ros_scxmls.extend(bt_converter(model.bt, model.plugins, model.bt_tick_rate)) # Convert the loaded entries to plain SCXML plain_scxml_models = [] all_timers: List[RosTimer] = [] diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 6701281f..3f6fa8f9 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -18,150 +18,22 @@ """ import os -import re from copy import deepcopy -from enum import Enum, auto from importlib.resources import path as resource_path from typing import Dict, List -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, ) -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 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 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. - - 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. - """ - bt_graph, _ = xml_to_networkx(bt_xml_path) - - 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 = [] - 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.set_bt_plugin_id(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.instantiate_bt_information() - 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) - - return generated_scxmls - - def load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, ScxmlRoot]: available_bt_plugins = {} for path in bt_plugins_scxml_paths: @@ -176,15 +48,14 @@ def load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, Sc return available_bt_plugins -def bt_converter_new( +def bt_converter( bt_xml_path: str, bt_plugins_scxml_paths: List[str], bt_tick_rate: float ) -> List[ScxmlRoot]: """ Generate all Scxml files resulting from a Behavior Tree (BT) in XML format. """ - # TODO: load SCXML plugins in as2fm's resources available_bt_plugins = load_available_bt_plugins(bt_plugins_scxml_paths) - xml_tree: ET.ElementBase = ET.parse(bt_xml_path).getroot() + 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 ( From 9797151236f7794bac8a2dc41d223e40c1063b1d Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 14:11:25 +0200 Subject: [PATCH 28/46] Add input ports support Signed-off-by: Marco Lampacrescia --- .../bt_reactive_fallback.scxml | 64 +++++++++++++++++++ src/as2fm/scxml_converter/bt_converter.py | 11 +++- .../scxml_converter/scxml_entries/bt_utils.py | 14 ++-- .../_test_data/delibws24_p1/bt.xml | 4 +- .../robot_navigation_with_bt/bt.xml | 8 +-- .../_test_data/battery_drainer_w_bt/bt.xml | 4 +- 6 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml diff --git a/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml b/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml new file mode 100644 index 00000000..0d55ed2b --- /dev/null +++ b/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 3f6fa8f9..6ceed1fb 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -20,7 +20,7 @@ import os from copy import deepcopy from importlib.resources import path as resource_path -from typing import Dict, List +from typing import Dict, List, Tuple from lxml import etree as ET @@ -119,6 +119,14 @@ def get_bt_plugin_type(bt_xml_subtree: ET.ElementBase) -> str: 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 + + def generate_bt_children_scxmls( bt_xml_subtree: ET.ElementBase, subtree_tick_idx: int, @@ -135,6 +143,7 @@ def generate_bt_children_scxmls( 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(): diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index f4714eda..980496ae 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -25,7 +25,7 @@ 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): @@ -88,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. @@ -162,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/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 @@ - + - - + + - + - + 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 @@ - + - + From bc240735f7f957f4520fd50a4c1497190785ecdc Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 14:43:28 +0200 Subject: [PATCH 29/46] Fixing various tests Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/utils.py | 3 +- .../_test_data/bt_ports_only/bt.xml | 4 +- .../bt_ports_only/bt_topic_action.scxml | 4 +- .../gt_bt_scxml/1000_BtTopicAction.scxml | 15 ----- .../gt_bt_scxml/1000_ReactiveSequence.scxml | 60 +++++++++++++++++++ .../gt_bt_scxml/1001_BtTopicAction.scxml | 9 ++- .../gt_bt_scxml/1002_BtTopicAction.scxml | 14 +++++ .../bt_ports_only/gt_bt_scxml/bt.scxml | 41 +++---------- .../test_systemtest_scxml_entries.py | 1 + test/scxml_converter/test_systemtest_xml.py | 6 +- 10 files changed, 97 insertions(+), 60 deletions(-) delete mode 100644 test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_BtTopicAction.scxml create mode 100644 test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1000_ReactiveSequence.scxml create mode 100644 test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/1002_BtTopicAction.scxml diff --git a/src/as2fm/scxml_converter/scxml_entries/utils.py b/src/as2fm/scxml_converter/scxml_entries/utils.py index ed43e315..f52ba05a 100644 --- a/src/as2fm/scxml_converter/scxml_entries/utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/utils.py @@ -198,8 +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. """ - arg_value = arg_value.strip() - 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__}: " 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 @@ - + - + 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 @@ - + @@ -32,7 +32,7 @@ - + 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 @@ - - - - - - - - - - - - - - - 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - - + - + - + - + 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 @@ + + + + + + + + + + + + + + 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 index 9bdd99e6..7e91ae69 100644 --- 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 @@ -1,35 +1,12 @@ - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/test/scxml_converter/test_systemtest_scxml_entries.py b/test/scxml_converter/test_systemtest_scxml_entries.py index cffb4b95..b89a20a0 100644 --- a/test/scxml_converter/test_systemtest_scxml_entries.py +++ b/test/scxml_converter/test_systemtest_scxml_entries.py @@ -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")) diff --git a/test/scxml_converter/test_systemtest_xml.py b/test/scxml_converter/test_systemtest_xml.py index 2c61b430..8ebf43bf 100644 --- a/test/scxml_converter/test_systemtest_xml.py +++ b/test/scxml_converter/test_systemtest_xml.py @@ -52,7 +52,7 @@ 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)}." + assert len(scxml_objs) == 4, f"Expecting 4 scxml objects, found {len(scxml_objs)}." if store_generated: clear_output_folder(test_folder) for scxml_obj in scxml_objs: @@ -67,7 +67,6 @@ def bt_to_scxml_test( 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,12 +89,15 @@ 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.instantiate_bt_information() plain_scxmls, _ = scxml_obj.to_plain_scxml_and_declarations() From 0a69d0e26d3a52d48c7fb83d229b9d434f7ccf62 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 15:41:39 +0200 Subject: [PATCH 30/46] Finish tests fixes Signed-off-by: Marco Lampacrescia --- .../resources/bt_control_nodes/inverter.scxml | 45 ++++++++++++++ .../scxml_entries/scxml_root.py | 5 +- .../scxml_entries/scxml_state.py | 5 ++ .../bt_topic_action.scxml | 6 +- .../bt_topic_condition.scxml | 8 +-- .../gt_bt_scxml/10000_BtTopicCondition.scxml | 31 ---------- .../gt_bt_scxml/1000_ReactiveSequence.scxml | 60 +++++++++++++++++++ .../gt_bt_scxml/1001_BtTopicAction.scxml | 22 ------- .../gt_bt_scxml/1001_Inverter.scxml | 30 ++++++++++ .../gt_bt_scxml/1002_BtTopicCondition.scxml | 22 +++++++ .../gt_bt_scxml/1003_BtTopicAction.scxml | 11 ++++ .../battery_drainer_w_bt/gt_bt_scxml/bt.scxml | 41 +++---------- .../gt_parsed_scxml/bt_topic_condition.scxml | 8 +-- .../gt_plain_scxml/bt_topic_action.scxml | 9 +-- .../gt_plain_scxml/bt_topic_condition.scxml | 12 ++-- test/scxml_converter/test_systemtest_xml.py | 14 ++++- 16 files changed, 221 insertions(+), 108 deletions(-) create mode 100644 src/as2fm/resources/bt_control_nodes/inverter.scxml delete mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml create mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1000_ReactiveSequence.scxml delete mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml create mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_Inverter.scxml create mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1002_BtTopicCondition.scxml create mode 100644 test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1003_BtTopicAction.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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 9917cbfd..fe425aa4 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -290,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_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index 41d7af19..d52c2dea 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, @@ -228,6 +229,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/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 @@ - + - - + + 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 @@ - + - + - + - + diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml deleted file mode 100644 index 33609b7f..00000000 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/10000_BtTopicCondition.scxml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml b/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml deleted file mode 100644 index c94e163d..00000000 --- a/test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/1001_BtTopicAction.scxml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + 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 index 8849a482..7e91ae69 100644 --- 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 @@ -1,35 +1,12 @@ - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + 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 @@ - + - + - + - + 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 @@ - - - - + + + + + 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 @@ - + - + - + + + - + + + diff --git a/test/scxml_converter/test_systemtest_xml.py b/test/scxml_converter/test_systemtest_xml.py index 8ebf43bf..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) == 4, f"Expecting 4 scxml objects, found {len(scxml_objs)}." if store_generated: clear_output_folder(test_folder) for scxml_obj in scxml_objs: @@ -61,6 +60,12 @@ 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") @@ -146,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(): From 02307be0b3b5b5b18d9dcd64b96d7735a5818b72 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 15:50:13 +0200 Subject: [PATCH 31/46] Adjust BT implementations Signed-off-by: Marco Lampacrescia --- .../robot_navigation_with_bt/bt_drive_robot.scxml | 6 +++--- .../robot_navigation_with_bt/bt_goal_check.scxml | 14 +++++++------- .../test_systemtest_scxml_entries.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) 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 @@ - + @@ -50,8 +50,8 @@ - - + + 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 @@ - - - + + + @@ -33,13 +33,13 @@ - + - + - + - + diff --git a/test/scxml_converter/test_systemtest_scxml_entries.py b/test/scxml_converter/test_systemtest_scxml_entries.py index b89a20a0..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")), From 8c913643835e9684bf6cac10081914c959732e74 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 16:15:30 +0200 Subject: [PATCH 32/46] Fix ReactiveFallback control node Signed-off-by: Marco Lampacrescia --- .../resources/bt_control_nodes/bt_reactive_fallback.scxml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml b/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml index 0d55ed2b..e944075e 100644 --- a/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml +++ b/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml @@ -38,11 +38,11 @@ - + - + From 816430236b1caa24a3b221a88dc24755ac9436cb Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 17:06:16 +0200 Subject: [PATCH 33/46] Documentation Signed-off-by: Marco Lampacrescia --- docs/source/howto.rst | 75 ++++++++++++++++++++++++++++++++++++--- docs/source/tutorials.rst | 2 +- 2 files changed, 72 insertions(+), 5 deletions(-) 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 `: 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 `: A set of XML tags for modeling the BT Communication interface, based on BT ticks and BT responses. * :ref:`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 + + + + + + + + + +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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +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 `_ 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/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 `_ with the following content: +The `as2fm_scxml_to_jani` tool takes a main XML file, e.g., `main.xml `_ with the following content: * one or multiple ROS nodes in SCXML: From 29e7a5626bc6c2fbf7515af01cac7b105d5189b9 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 17:48:18 +0200 Subject: [PATCH 34/46] Remove deprecated use of importlib's path Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/bt_converter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 6ceed1fb..c7678ecb 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -19,7 +19,7 @@ import os from copy import deepcopy -from importlib.resources import path as resource_path +from importlib.resources import files as resource_files from typing import Dict, List, Tuple from lxml import etree as ET @@ -40,7 +40,9 @@ def load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, Sc 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_path("as2fm", "resources").joinpath("bt_control_nodes") + 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)) From 5301d90a642c6252ce6156939beedb029bd47bb7 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 17:55:38 +0200 Subject: [PATCH 35/46] Remove unused btlib Signed-off-by: Marco Lampacrescia --- .github/workflows/deploy.yml | 20 ++------------------ .github/workflows/test.yml | 26 +------------------------- 2 files changed, 3 insertions(+), 43 deletions(-) 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/test.yml b/.github/workflows/test.yml index ffc1dbfa..29031aee 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 @@ -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/ From bb267d7c8b8389c860446ccdd9428b52dba78816 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 17:59:26 +0200 Subject: [PATCH 36/46] Remove old dependencies Signed-off-by: Marco Lampacrescia --- .github/workflows/lint.yml | 12 ------------ .github/workflows/test.yml | 4 ++-- docs/source/conf.py | 1 - docs/source/installation.rst | 3 +-- pyproject.toml | 2 -- 5 files changed, 3 insertions(+), 19 deletions(-) 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 29031aee..e0d8e783 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,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 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/installation.rst b/docs/source/installation.rst index 82617edb..2f585084 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 `_ -* `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** and then running: .. code-block:: bash diff --git a/pyproject.toml b/pyproject.toml index 4060234a..3e22ca34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,6 @@ 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", From 46478bd43f1c5673eaf87fc8f8ea13eeb511137a Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 15 Oct 2024 18:04:02 +0200 Subject: [PATCH 37/46] Fix font path in toml file Signed-off-by: Marco Lampacrescia --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e22ca34..2e4d2706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ requires-python = ">=3.10" # "as2fm" = "src/as2fm" [tool.setuptools.package-data] -"as2fm.trace_visualizer" = ["trace_visualizer/data/slkscr.ttf"] +"as2fm.trace_visualizer" = ["data/slkscr.ttf"] "as2fm.resources" = ["bt_control_nodes/*.scxml"] [project.scripts] From aeafe3d8495de2fd5d44f997ad2cb6df3881aae1 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 16 Oct 2024 16:00:23 +0200 Subject: [PATCH 38/46] Test for reactive sequence Signed-off-by: Marco Lampacrescia --- .../bt_control_nodes/always_failure.scxml | 32 ++++++++ .../bt_control_nodes/always_success.scxml | 32 ++++++++ .../bt_test_models/bt_count_ticks.scxml | 31 ++++++++ .../bt_test_reactive_sequence.xml | 10 +++ .../main_test_reactive_sequence.xml | 15 ++++ .../property_test_reactive_sequence.jani | 36 +++++++++ .../test_systemtest_behavior_tree_scxml.py | 77 +++++++++++++++++++ .../test_systemtest_scxml_to_jani.py | 2 +- 8 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 src/as2fm/resources/bt_control_nodes/always_failure.scxml create mode 100644 src/as2fm/resources/bt_control_nodes/always_success.scxml create mode 100644 test/jani_generator/_test_data/bt_test_models/bt_count_ticks.scxml create mode 100644 test/jani_generator/_test_data/bt_test_models/bt_test_reactive_sequence.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/main_test_reactive_sequence.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.jani create mode 100644 test/jani_generator/test_systemtest_behavior_tree_scxml.py 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..e76e44fa --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_count_ticks.scxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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..43190fd1 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_sequence.xml @@ -0,0 +1,10 @@ + + + + + + + + + + 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..5533fbf2 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_reactive_sequence.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.jani b/test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.jani new file mode 100644 index 00000000..0de53e53 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.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_systemtest_behavior_tree_scxml.py b/test/jani_generator/test_systemtest_behavior_tree_scxml.py new file mode 100644 index 00000000..12bc9483 --- /dev/null +++ b/test/jani_generator/test_systemtest_behavior_tree_scxml.py @@ -0,0 +1,77 @@ +# 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, + ) + + +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 d5e0593e..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) From 5cb1a43bc8479363ce7851b8af2d6c8c553f25ec Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 16 Oct 2024 16:07:41 +0200 Subject: [PATCH 39/46] Add test for reactive fallbacks Signed-off-by: Marco Lampacrescia --- .../bt_test_models/bt_test_reactive_fallback.xml | 12 ++++++++++++ .../main_test_reactive_fallback.xml | 15 +++++++++++++++ .../main_test_reactive_sequence.xml | 2 +- ...jani => property_test_reactive_behaviors.jani} | 0 .../test_systemtest_behavior_tree_scxml.py | 8 ++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/jani_generator/_test_data/bt_test_models/bt_test_reactive_fallback.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/main_test_reactive_fallback.xml rename test/jani_generator/_test_data/bt_test_models/{property_test_reactive_sequence.jani => property_test_reactive_behaviors.jani} (100%) 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..70a02c98 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/bt_test_reactive_fallback.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + 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 index 5533fbf2..e826b445 100644 --- 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 @@ -10,6 +10,6 @@ - + diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.jani b/test/jani_generator/_test_data/bt_test_models/property_test_reactive_behaviors.jani similarity index 100% rename from test/jani_generator/_test_data/bt_test_models/property_test_reactive_sequence.jani rename to test/jani_generator/_test_data/bt_test_models/property_test_reactive_behaviors.jani diff --git a/test/jani_generator/test_systemtest_behavior_tree_scxml.py b/test/jani_generator/test_systemtest_behavior_tree_scxml.py index 12bc9483..4adb66a7 100644 --- a/test/jani_generator/test_systemtest_behavior_tree_scxml.py +++ b/test/jani_generator/test_systemtest_behavior_tree_scxml.py @@ -72,6 +72,14 @@ def test_reactive_sequence(self): 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, + ) + if __name__ == "__main__": pytest.main(["-s", "-v", __file__]) From ad544f4b663bce662111fb5958d020d640ecd61b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 17 Oct 2024 11:50:26 +0200 Subject: [PATCH 40/46] Implement BT sequence model and update test plugin for counting ticks Signed-off-by: Marco Lampacrescia --- ..._if_then_else.scxml => if_then_else.scxml} | 0 ...fallback.scxml => reactive_fallback.scxml} | 0 ...sequence.scxml => reactive_sequence.scxml} | 0 .../resources/bt_control_nodes/sequence.scxml | 56 +++++++++++++++++++ .../bt_test_models/bt_count_ticks.scxml | 26 ++++++++- .../bt_test_reactive_fallback.xml | 4 +- .../bt_test_reactive_sequence.xml | 4 +- 7 files changed, 83 insertions(+), 7 deletions(-) rename src/as2fm/resources/bt_control_nodes/{bt_if_then_else.scxml => if_then_else.scxml} (100%) rename src/as2fm/resources/bt_control_nodes/{bt_reactive_fallback.scxml => reactive_fallback.scxml} (100%) rename src/as2fm/resources/bt_control_nodes/{bt_reactive_sequence.scxml => reactive_sequence.scxml} (100%) create mode 100644 src/as2fm/resources/bt_control_nodes/sequence.scxml diff --git a/src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml b/src/as2fm/resources/bt_control_nodes/if_then_else.scxml similarity index 100% rename from src/as2fm/resources/bt_control_nodes/bt_if_then_else.scxml rename to src/as2fm/resources/bt_control_nodes/if_then_else.scxml diff --git a/src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml b/src/as2fm/resources/bt_control_nodes/reactive_fallback.scxml similarity index 100% rename from src/as2fm/resources/bt_control_nodes/bt_reactive_fallback.scxml rename to src/as2fm/resources/bt_control_nodes/reactive_fallback.scxml diff --git a/src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml b/src/as2fm/resources/bt_control_nodes/reactive_sequence.scxml similarity index 100% rename from src/as2fm/resources/bt_control_nodes/bt_reactive_sequence.scxml rename to src/as2fm/resources/bt_control_nodes/reactive_sequence.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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 index e76e44fa..0d6dd352 100644 --- 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 @@ -5,13 +5,27 @@ version="1.0" name="BtCountTicks" model_src=""> + + + + + + + + + + + + - - @@ -24,7 +38,13 @@ - + + + + + + + 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 index 70a02c98..8fcbaf96 100644 --- 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 @@ -3,10 +3,10 @@ - + - + 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 index 43190fd1..eb93327c 100644 --- 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 @@ -2,9 +2,9 @@ - + - + From 0a615989ca7bd74d1f9463b51ab826fe369da327 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 17 Oct 2024 13:02:10 +0200 Subject: [PATCH 41/46] Test for sequence controller Signed-off-by: Marco Lampacrescia --- .../bt_test_models/bt_count_ticks.scxml | 7 ++- .../bt_test_reactive_fallback.xml | 2 +- .../bt_test_reactive_sequence.xml | 2 +- .../bt_test_models/bt_test_sequence.xml | 17 ++++++ .../bt_test_models/main_test_sequence.xml | 15 +++++ .../property_test_sequence.jani | 57 +++++++++++++++++++ .../test_systemtest_behavior_tree_scxml.py | 8 +++ 7 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 test/jani_generator/_test_data/bt_test_models/bt_test_sequence.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/main_test_sequence.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani 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 index 0d6dd352..91b4303e 100644 --- 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 @@ -33,14 +33,13 @@ + - - - + @@ -48,4 +47,6 @@ + + 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 index 8fcbaf96..04b72adc 100644 --- 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 @@ -1,4 +1,4 @@ - + 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 index eb93327c..1d9f4a49 100644 --- 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 @@ -1,4 +1,4 @@ - + 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 @@ + + + + + + + + + + + + + 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..0ffe7455 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/main_test_sequence.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani b/test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani new file mode 100644 index 00000000..b966ba65 --- /dev/null +++ b/test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani @@ -0,0 +1,57 @@ +{ + "properties": [ + { + "name": "sequence_success_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_systemtest_behavior_tree_scxml.py b/test/jani_generator/test_systemtest_behavior_tree_scxml.py index 4adb66a7..91912f2d 100644 --- a/test/jani_generator/test_systemtest_behavior_tree_scxml.py +++ b/test/jani_generator/test_systemtest_behavior_tree_scxml.py @@ -80,6 +80,14 @@ def test_reactive_fallback(self): True, ) + def test_sequence(self): + """Test the sequence behavior.""" + self._test_with_main( + os.path.join("bt_test_models", "main_test_sequence.xml"), + "sequence_success_test", + True, + ) + if __name__ == "__main__": pytest.main(["-s", "-v", __file__]) From 6eb2c4c35cf12c04499e9fbe0fea9e6c3166d9f0 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 17 Oct 2024 13:12:18 +0200 Subject: [PATCH 42/46] Implement Fallback control Signed-off-by: Marco Lampacrescia --- .../resources/bt_control_nodes/fallback.scxml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/as2fm/resources/bt_control_nodes/fallback.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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c5cb74e64135ed15f55f25dc2a45755bee48427b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 17 Oct 2024 13:21:24 +0200 Subject: [PATCH 43/46] Add test for fallback bt controller Signed-off-by: Marco Lampacrescia --- .../bt_test_models/bt_test_fallback.xml | 23 +++++++++++++++++++ .../bt_test_models/main_test_fallback.xml | 15 ++++++++++++ .../bt_test_models/main_test_sequence.xml | 2 +- ...i => property_test_regular_behaviors.jani} | 2 +- .../test_systemtest_behavior_tree_scxml.py | 10 +++++++- 5 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 test/jani_generator/_test_data/bt_test_models/bt_test_fallback.xml create mode 100644 test/jani_generator/_test_data/bt_test_models/main_test_fallback.xml rename test/jani_generator/_test_data/bt_test_models/{property_test_sequence.jani => property_test_regular_behaviors.jani} (98%) 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 @@ + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + 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 index 0ffe7455..c3d30f97 100644 --- 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 @@ -10,6 +10,6 @@ - + diff --git a/test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani b/test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani similarity index 98% rename from test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani rename to test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani index b966ba65..4ac52766 100644 --- a/test/jani_generator/_test_data/bt_test_models/property_test_sequence.jani +++ b/test/jani_generator/_test_data/bt_test_models/property_test_regular_behaviors.jani @@ -1,7 +1,7 @@ { "properties": [ { - "name": "sequence_success_test", + "name": "regular_bt_test", "expression": { "op": "filter", "fun": "values", diff --git a/test/jani_generator/test_systemtest_behavior_tree_scxml.py b/test/jani_generator/test_systemtest_behavior_tree_scxml.py index 91912f2d..4f5d4580 100644 --- a/test/jani_generator/test_systemtest_behavior_tree_scxml.py +++ b/test/jani_generator/test_systemtest_behavior_tree_scxml.py @@ -84,7 +84,15 @@ def test_sequence(self): """Test the sequence behavior.""" self._test_with_main( os.path.join("bt_test_models", "main_test_sequence.xml"), - "sequence_success_test", + "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, ) From 18712ea6772df858879a8819cbffe5e02052cf04 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 18 Oct 2024 13:04:31 +0200 Subject: [PATCH 44/46] Small TODO Signed-off-by: Marco Lampacrescia --- docs/source/installation.rst | 2 +- src/as2fm/jani_generator/scxml_helpers/scxml_event.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 2f585084..84fd239b 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -45,7 +45,7 @@ AS2FM can be installed using pip: # Editable mode python3 -m pip install -e AS2FM/ -Verify your installation by **sourcing your ROS distribution** and then running: +Verify your installation by **sourcing your ROS distribution** (i.e. running `source /opt/ros//setup.bash`) and then running: .. code-block:: bash 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") From 2a3c64cf7c2ed8531cb707daf1ac85e5dc3e9c8b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 18 Oct 2024 13:32:24 +0200 Subject: [PATCH 45/46] Prevent extra self-loops in the BT root jani automaton Signed-off-by: Marco Lampacrescia --- src/as2fm/jani_generator/scxml_helpers/scxml_tags.py | 5 +++++ src/as2fm/scxml_converter/bt_converter.py | 11 ++++++++++- .../gt_bt_scxml/{bt.scxml => bt_root_fsm_bt.scxml} | 0 .../gt_bt_scxml/{bt.scxml => bt_root_fsm_bt.scxml} | 0 4 files changed, 15 insertions(+), 1 deletion(-) rename test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/{bt.scxml => bt_root_fsm_bt.scxml} (100%) rename test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/{bt.scxml => bt_root_fsm_bt.scxml} (100%) 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/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index c7678ecb..286aa2de 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -33,6 +33,15 @@ ScxmlState, ) +BT_ROOT_PREFIX = "bt_root_fsm_" + + +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 load_available_bt_plugins(bt_plugins_scxml_paths: List[str]) -> Dict[str, ScxmlRoot]: available_bt_plugins = {} @@ -80,7 +89,7 @@ def generate_bt_root_scxml(scxml_name: str, tick_id: int, tick_rate: float) -> S """ Generate the root SCXML for a Behavior Tree. """ - bt_scxml_root = ScxmlRoot(scxml_name) + 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( 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_root_fsm_bt.scxml similarity index 100% rename from test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt.scxml rename to test/scxml_converter/_test_data/battery_drainer_w_bt/gt_bt_scxml/bt_root_fsm_bt.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_root_fsm_bt.scxml similarity index 100% rename from test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt.scxml rename to test/scxml_converter/_test_data/bt_ports_only/gt_bt_scxml/bt_root_fsm_bt.scxml From e5d36668ff35a222eb394bcb11106794d752136f Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 25 Oct 2024 13:41:51 +0200 Subject: [PATCH 46/46] Always return a List of transitions when instantiating BT events Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_bt_ticks.py | 32 +++++++++++-------- .../scxml_entries/scxml_ros_action_client.py | 4 +-- .../scxml_entries/scxml_state.py | 16 +++------- .../scxml_entries/scxml_transition.py | 8 +++-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 42d1442e..a0ef8d81 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -90,10 +90,12 @@ def __init__( def check_validity(self) -> bool: return super().check_validity() - def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> ScxmlTransition: + 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) + 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}) @@ -216,26 +218,28 @@ def instantiate_bt_events( 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) - ] + 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}) && " - return [ - ScxmlTransition( + 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) - for child_seq_n, child_id in enumerate(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( 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 8f7a173b..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, _, __) -> "RosActionHandleGoalResponse": + def instantiate_bt_events(self, _, __) -> List["RosActionHandleGoalResponse"]: # We do not expect a body with BT events requiring substitutions - return self + 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 d52c2dea..90dea72d 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -146,17 +146,11 @@ def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> No """Instantiate the BT events in all entries belonging to a state.""" instantiated_transitions: List[ScxmlTransition] = [] for transition in self._body: - new_transition = transition.instantiate_bt_events(instance_id, children_ids) - if isinstance(new_transition, ScxmlTransition): - instantiated_transitions.append(new_transition) - elif isinstance(new_transition, list) and all( - isinstance(t, ScxmlTransition) for t in new_transition - ): - instantiated_transitions.extend(new_transition) - else: - raise ValueError( - f"Error: SCXML state {self._id}: found invalid transition in state body." - ) + 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) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index ce7b5c6b..84f14e60 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -117,14 +117,16 @@ 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: int, children_ids: List[int]) -> "ScxmlTransition": + 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): warnings.warn( @@ -140,7 +142,7 @@ def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> "S ) # The body of a transition needs to be replaced on derived classes, too instantiate_exec_body_bt_events(self._body, instance_id, children_ids) - return self + return [self] def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports."""