Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ROS actions in SCXML #28

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
406c842
Add support to BT Input Ports
MarcoLm993 Aug 19, 2024
71fcbd3
Support for Arrays on Jani entries and example for ROS actions in SCXML
MarcoLm993 Aug 19, 2024
ff9777d
Reduce code duplication and introduce some action interfaces
MarcoLm993 Aug 19, 2024
97255b4
Integrate action functionalities in RosDeclarationsContainer
MarcoLm993 Aug 19, 2024
eac1372
Handle empty jani guard
MarcoLm993 Aug 19, 2024
cb24981
Fix bugs related to JaniEntries
MarcoLm993 Aug 19, 2024
eccf758
Missing supported types update
MarcoLm993 Aug 20, 2024
928b1c8
Some refinements in SCXML Ros Service definiion and fibonacci client …
MarcoLm993 Aug 20, 2024
e84d4c6
First action client scxml implementation
MarcoLm993 Aug 20, 2024
c72e898
Fix missing comma
MarcoLm993 Aug 20, 2024
3c7ced2
Define first batch of SCXML classes for action server
MarcoLm993 Aug 20, 2024
c416b51
Implement ActionServer declaration. Prepare RosDeclaration base class…
MarcoLm993 Aug 20, 2024
d189df3
Reduce code duplication in ROS declarations
MarcoLm993 Aug 20, 2024
5855ef7
Move action server thread to separated file
MarcoLm993 Aug 20, 2024
a4a1a0c
Merge action server and its thread in the same xml file
MarcoLm993 Aug 20, 2024
0e044e3
Introduce RosCallback base class and integrate in topic callback
MarcoLm993 Aug 20, 2024
93eb004
Action server goal handle implemented
MarcoLm993 Aug 20, 2024
fa2e1d0
Base class for ROS interface trigger
MarcoLm993 Aug 20, 2024
dbd0f4e
Implement accept and reject action server responses
MarcoLm993 Aug 20, 2024
a8a3f76
RosActionStartThread
MarcoLm993 Aug 21, 2024
eb98056
Finish entries for Action server
MarcoLm993 Aug 21, 2024
9b23b35
Start looking into the server thread functionalities
MarcoLm993 Aug 21, 2024
920912d
Finish designing preliminary interface of RosActionThread
MarcoLm993 Aug 21, 2024
0817a1f
Avoid inheriting from ScxmlRoot
MarcoLm993 Aug 22, 2024
93a2ec5
Thread start and cancel and rework interface of action thread
MarcoLm993 Aug 22, 2024
94093a5
Finish action thread SCXML class
MarcoLm993 Aug 22, 2024
6ee1387
Add missing function to scxml state
MarcoLm993 Aug 22, 2024
218642d
Reduce code duplication in scxml ros services
MarcoLm993 Aug 22, 2024
86cf975
More base functionalities in RosBase classes and make use of it in ac…
MarcoLm993 Aug 22, 2024
613ca84
Code reduction in action server
MarcoLm993 Aug 22, 2024
d73d6f1
Additional code reduction
MarcoLm993 Aug 22, 2024
9b26539
Reduce duplication in action scxml classes, too
MarcoLm993 Aug 22, 2024
661268b
Set max line size in flake8
MarcoLm993 Aug 22, 2024
7b07aef
Prepare tests to support multiple returned Scxml objects
MarcoLm993 Aug 22, 2024
47b11fd
Get rid of scxml_ros_entries
MarcoLm993 Aug 22, 2024
12e605e
Move ros declaration handling out of scxml root
MarcoLm993 Aug 22, 2024
2c590fa
Start integration of threads in scxml root
MarcoLm993 Aug 22, 2024
c66743f
Add thread cancel option
MarcoLm993 Aug 23, 2024
ffacbb2
Allow conditions in RosCallback
MarcoLm993 Aug 23, 2024
bf936e9
Adapt thread start interface to new sxcml action server example
MarcoLm993 Aug 23, 2024
84046d0
Bugfix in scxml_if
MarcoLm993 Aug 23, 2024
11d40a3
Handle array in scxml data
MarcoLm993 Aug 23, 2024
e2ef0fb
Improve printed messages
MarcoLm993 Aug 23, 2024
c3a664b
Bugfixing
MarcoLm993 Aug 23, 2024
4a1524c
Make scxml parser less verbose
MarcoLm993 Aug 23, 2024
61e7b25
More fixing
MarcoLm993 Aug 23, 2024
a3ea205
First running action server + threads conversion
MarcoLm993 Aug 23, 2024
597ff25
First complete translation of action server and client
MarcoLm993 Aug 23, 2024
54538a1
Assert on unknown state tags and improve action example
MarcoLm993 Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
use-pydocstyle: false
extra-pylint-options: ""
extra-pycodestyle-options: ""
extra-flake8-options: ""
extra-flake8-options: "--max-line-length=100"
extra-black-options: ""
extra-mypy-options: "--ignore-missing-imports"
extra-isort-options: ""
27 changes: 18 additions & 9 deletions as2fm_common/src/as2fm_common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@
Common functionalities used throughout the toolchain.
"""

from typing import Union
from typing import List, Union

ValidTypes = Union[bool, int, float]
"""We define the basic types that are supported by the Jani language:
"""

Check warning on line 22 in as2fm_common/src/as2fm_common/common.py

View workflow job for this annotation

GitHub Actions / as2fm_common ⏩ pylint

String statement has no effect
Set of basic types that are supported by the Jani language.

// Types
// We cover only the most basic types at the moment.
// In the remainder of the specification, all requirements like "y must be of type x" are to be interpreted
// as "type x must be assignable from y's type".
Basic types (from Jani docs):
Types
We cover only the most basic types at the moment.
In the remainder of the specification, all requirements like "y must be of type x" are to be

Check warning on line 28 in as2fm_common/src/as2fm_common/common.py

View workflow job for this annotation

GitHub Actions / as2fm_common ⏩ pycodestyle

line too long (92 > 79 characters)
interpreted as "type x must be assignable from y's type".
var BasicType = schema([
"bool", // assignable from bool
"int", // numeric; assignable from int and bounded int
"real" // numeric; assignable from all numeric types
]);
src https://docs.google.com/document/d/\
1BDQIzPBtscxJFFlDUEPIo8ivKHgXT8_X6hz5quq7jK0/edit"""
1BDQIzPBtscxJFFlDUEPIo8ivKHgXT8_X6hz5quq7jK0/edit

Additionally, we support the array types from the array extension.
"""
ValidTypes = Union[bool, int, float, List[int], List[float]]


def ros_type_name_to_python_type(type_str: str) -> type:
Expand All @@ -51,6 +56,10 @@
return int
if type_str in ['float32', 'float64']:
return float
if type_str in ['sequence<int32>', 'sequence<int64>']:
return List[int]
if type_str in ['sequence<float32>', 'sequence<float64>']:
return List[float]
raise NotImplementedError(f"Type {type_str} not supported.")


Expand All @@ -67,4 +76,4 @@
tag_wo_ns = tag.split('}')[-1]
else:
tag_wo_ns = tag
return tag_wo_ns
return tag_wo_ns
559 changes: 559 additions & 0 deletions docs/source/graphics/ros_action_to_scxml.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 71 additions & 15 deletions docs/source/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@ ROS Topics are used to publish (via a ROS Publisher) and receive (via a ROS Subs
.. code-block:: xml

<!-- ROS Topic Subscriber -->
<ros_topic_subscriber topic="/topic1" type="std_msgs/Bool" />
<ros_topic_subscriber name="bool_topic" topic="/topic1" type="std_msgs/Bool" />
<!-- ROS Topic Publisher -->
<ros_topic_publisher topic="/topic2" type="std_msgs/Int32" />
<ros_topic_publisher name="int_topic" topic="/topic2" type="std_msgs/Int32" />

Once created, subscribers and publishers can be referenced using the `topic` name, and can be used in the states to send messages and perform callbacks upon receiving messages:
The two declarations above will create a ROS subscriber called `bool_topic` that reads messages of type `std_msgs/Bool` from the topic `/topic1` and a ROS publisher called `int_topic` that writes messages of type `std_msgs/Int32` on the topic `/topic2`.
The `name` argument is optional, and if not provided, it will be set to the same value as the `topic` argument.

Once created, subscribers and publishers can be referenced using their names (`bool_topic` and `int_topic`), and can be used in the states to send messages and perform callbacks upon receiving messages:

.. code-block:: xml

Expand All @@ -117,19 +120,19 @@ Once created, subscribers and publishers can be referenced using the `topic` nam
</datamodel>

<state id="src_state">
<ros_topic_callback topic="/topic1" target="target_state">
<ros_topic_callback name="bool_topic" target="target_state">
<assign location="internal_var" expr="_msg.data" />
</ros_topic_callback>
</state>

<state id="target_state">
<onentry>
<if cond="internal_bool">
<ros_topic_publish topic="/topic2" >
<ros_topic_publish name="int_topic" >
<field name="data" expr="10">
</ros_topic_publish>
<else />
<ros_topic_publish topic="/topic2" >
<ros_topic_publish name="int_topic" >
<field name="data" expr="20">
</ros_topic_publish>
</if>
Expand All @@ -154,11 +157,11 @@ The declaration of a ROS Service server and the one of a client can be achieved
.. code-block:: xml

<!-- ROS Service Server -->
<ros_service_server service_name="/service1" type="std_srvs/SetBool" />
<ros_service_server name="the_srv" service_name="/service1" type="std_srvs/SetBool" />
<!-- ROS Service Client -->
<ros_service_client service_name="/service2" type="std_srvs/Trigger" />
<ros_service_client name="the_client" service_name="/service2" type="std_srvs/Trigger" />

Once created, servers and clients can be referenced using the `service_name` name, and can be used in the states of a SCXML model to provide and request services.
Once created, servers and clients can be referenced using the provided `name` (i.e. `the_srv` and `the_client`), and can be used in the states of a SCXML model to provide and request services.
In the following, an exemplary client is provided:

.. code-block:: xml
Expand All @@ -169,16 +172,16 @@ In the following, an exemplary client is provided:

<state id="send_req">
<onentry>
<ros_service_send_request service_name="/service2">
<ros_service_send_request name="the_client">
</ros_service_send_request>
</onentry>
<ros_service_handle_response service_name="/service2" target="done">
<ros_service_handle_response name="the_client" target="done">
<assign location="internal_bool" expr="_res.success" />
</ros_service_handle_response>
</state>

To send a request, the `ros_service_send_request` can be used where any other executable content may be used.
After the server has processed the service, `ros_service_handle_response`, can be used similarly to a SCXML transition and is triggered by the server.
After the server has processed the service, `ros_service_handle_response`, can be used similarly to a SCXML transition and is triggered when a response from the server is received.
The data of the request can be accessed with the `_res` field.

And here, an example of a server:
Expand All @@ -190,9 +193,9 @@ And here, an example of a server:
</datamodel>

<state id="idle">
<ros_service_handle_request service_name="/service1" target="idle">
<ros_service_handle_request name="the_srv" target="idle">
<assign location="temp_data" expr="_req.data" />
<ros_service_send_response service_name="/adder">
<ros_service_send_response name="the_srv">
<field name="success" expr="temp_data" />
</ros_service_send_response>
</ros_service_handle_request>
Expand All @@ -215,7 +218,60 @@ TODO
Creating a SCXML model of a BT plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

TODO
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:

* :ref:`BT communication <bt_communication>`: A set of special events that are used in each BT plugins for starting a BT Node and provide results.
* :ref:`BT Ports <bt_ports>`: A special BT interface to parametrize a specific plugin instance.


.. _bt_communication:

BT Communication
_________________

TODO: describe `bt_tick`, `bt_running`, `bt_success`, `bt_failure`.


.. _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.

As in the case of ROS functionalities, BT Ports need to be declared before being used, to provide the port name and expected type.

.. code-block:: xml

<bt_port key="my_string_port" type="string" />
<bt_port key="start_value" type="int32">

Once declared, it is possible to reference to the port in multiple SCXML entries.

For example, we can use `my_string_port` to define the topic used by a ROS publisher.

.. code-block:: xml

<ros_topic_publisher name="int_topic" type="std_msgs/Int32">
<topic>
<bt_get_input key="my_string_port" />
</topic>
</ros_topic_publisher>

Or we can use `start_value` to define the initial value of a variable.

.. code-block:: xml

<datamodel>
<data id="counter" type="int32">
<expr>
<bt_get_input key="start_value" />
</expr>
</data>
</datamodel>


BT ports can also be linked to variables in the `BT Blackboard` by wrapping the variable name in curly braces in the BT xml file. However, this feature is not yet supported.


.. _additional_params_howto:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@


def __convince_env_model_to_jani(base_model: JaniModel, env_model: dict):
"""Add the converted entries from the convince environment model to the provided JaniModel object."""
"""Add the converted entries from the convince environment model to the provided JaniModel."""

Check warning on line 40 in jani_generator/src/jani_generator/convince_jani_helpers/convince_to_plain_jani.py

View workflow job for this annotation

GitHub Actions / jani_generator ⏩ pycodestyle

line too long (98 > 79 characters)
# Check if the base_model is a JaniModel instance
assert isinstance(base_model, JaniModel), "The base_model should be a JaniModel instance"

Check warning on line 42 in jani_generator/src/jani_generator/convince_jani_helpers/convince_to_plain_jani.py

View workflow job for this annotation

GitHub Actions / jani_generator ⏩ pycodestyle

line too long (93 > 79 characters)
# Check if the env_model is a dictionary
assert isinstance(env_model, dict), "The env_model should be a dictionary"
# Check if the env_model has the required keys
Expand All @@ -61,25 +61,32 @@
# The robot pose should be stored using integers -> centimeters and degrees
base_model.add_variable(f"robots.{robot_name}.pose.x_cm", int, to_cm(robot_pose["x"]))
base_model.add_variable(f"robots.{robot_name}.pose.y_cm", int, to_cm(robot_pose["y"]))
base_model.add_variable(f"robots.{robot_name}.pose.theta_deg", int, to_deg(robot_pose["theta"]))
base_model.add_variable(f"robots.{robot_name}.pose.theta_deg", int,
to_deg(robot_pose["theta"]))
base_model.add_variable(f"robots.{robot_name}.pose.x", float, transient=True)
base_model.add_variable(f"robots.{robot_name}.pose.y", float, transient=True)
base_model.add_variable(f"robots.{robot_name}.pose.theta", float, transient=True)
base_model.add_variable(f"robots.{robot_name}.goal.x", float, transient=True)
base_model.add_variable(f"robots.{robot_name}.goal.y", float, transient=True)
base_model.add_variable(f"robots.{robot_name}.goal.theta", float, transient=True)
robot_shape = robot["shape"]
base_model.add_constant(f"robots.{robot_name}.shape.radius", float, float(robot_shape["radius"]))
base_model.add_constant(f"robots.{robot_name}.shape.height", float, float(robot_shape["height"]))
base_model.add_constant(f"robots.{robot_name}.linear_velocity", float, float(robot["linear_velocity"]))
base_model.add_constant(f"robots.{robot_name}.angular_velocity", float, float(robot["angular_velocity"]))
base_model.add_constant(f"robots.{robot_name}.shape.radius", float,
float(robot_shape["radius"]))
base_model.add_constant(f"robots.{robot_name}.shape.height", float,
float(robot_shape["height"]))
base_model.add_constant(f"robots.{robot_name}.linear_velocity", float,
float(robot["linear_velocity"]))
base_model.add_constant(f"robots.{robot_name}.angular_velocity", float,
float(robot["angular_velocity"]))
if "obstacles" in env_model:
# Extract the obstacles from the env_model
# TODO

Check warning on line 83 in jani_generator/src/jani_generator/convince_jani_helpers/convince_to_plain_jani.py

View workflow job for this annotation

GitHub Actions / jani_generator ⏩ pylint

TODO
pass
# TODO: Discuss the perception part

Check warning on line 85 in jani_generator/src/jani_generator/convince_jani_helpers/convince_to_plain_jani.py

View workflow job for this annotation

GitHub Actions / jani_generator ⏩ pylint

TODO: Discuss the perception part
# TODO: Discuss the possibility of generating a base automata for each robot + standard edges (i.e. switch_on/off, drive, rotate)
# This would make sense, allowing the definition of default mobile robots without the need of defining how they drive.
# TODO: Discuss the possibility of generating a base automata for each robot + standard edges

Check warning on line 86 in jani_generator/src/jani_generator/convince_jani_helpers/convince_to_plain_jani.py

View workflow job for this annotation

GitHub Actions / jani_generator ⏩ pylint

TODO: Discuss the possibility of generating a base automata for each robot + standard edges
# (i.e. switch_on/off, drive, rotate)
# This would make sense, allowing the definition of default mobile robots without the need of
# defining how they drive.
# By the way, keeping this out for now!


Expand All @@ -104,7 +111,8 @@
assert isinstance(base_model, JaniModel), "The base_model should be a JaniModel instance"
for property_dict in properties:
assert isinstance(property_dict, dict), "The properties list should contain dictionaries"
base_model.add_jani_property(JaniProperty(property_dict["name"], property_dict["expression"]))
base_model.add_jani_property(JaniProperty(property_dict["name"],
property_dict["expression"]))


def convince_jani_parser(base_model: JaniModel, convince_jani_path: str):
Expand All @@ -119,7 +127,8 @@
# ---- Metadata ----
base_model.set_name(convince_jani_json["name"])
# Make sure we are loading a convince-jani file
assert "features" in convince_jani_json and "convince_extensions" in convince_jani_json["features"], \
assert "features" in convince_jani_json and \
"convince_extensions" in convince_jani_json["features"], \
"The provided file is not a convince-jani file (missing feature entry)"
# Extract the environment model from the convince-jani file
# ---- Environment Model ----
Expand Down
11 changes: 8 additions & 3 deletions jani_generator/src/jani_generator/jani_entries/jani_automaton.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@


class JaniAutomaton:
@staticmethod
def from_dict(automaton_dict: dict) -> "JaniAutomaton":
return JaniAutomaton(automaton_dict=automaton_dict)

def __init__(self, *, automaton_dict: Optional[Dict[str, Any]] = None):
self._locations: Set[str] = set()
self._initial_locations: Set[str] = set()
Expand All @@ -34,7 +38,7 @@ def __init__(self, *, automaton_dict: Optional[Dict[str, Any]] = None):
self._name = automaton_dict["name"]
self._generate_locations(
automaton_dict["locations"], automaton_dict["initial-locations"])
self._generate_variables(automaton_dict["variables"])
self._generate_variables(automaton_dict.get("variables", []))
self._generate_edges(automaton_dict["edges"])

def get_name(self):
Expand Down Expand Up @@ -86,7 +90,8 @@ def remove_empty_self_loop_edges(self):
"""Remove all self-loop edges from the automaton."""
self._edges = [edge for edge in self._edges if not edge.is_empty_self_loop()]

def _generate_locations(self, location_list: List[Dict[str, Any]], initial_locations: List[str]):
def _generate_locations(self,
location_list: List[Dict[str, Any]], initial_locations: List[str]):
for location in location_list:
self._locations.add(location["name"])
for init_location in initial_locations:
Expand All @@ -100,7 +105,7 @@ def _generate_variables(self, variable_list: List[dict]):
is_transient = False
if "transient" in variable:
is_transient = variable["transient"]
var_type = JaniVariable.jani_type_from_string(variable["type"])
var_type = JaniVariable.python_type_from_json(variable["type"])
self._local_variables.update({variable["name"]: JaniVariable(
variable["name"], var_type, init_expr, is_transient)})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@


class JaniComposition:
@staticmethod
def from_dict(composition_dict: dict) -> "JaniComposition":
return JaniComposition(composition_dict=composition_dict)

def __init__(self, composition_dict: Optional[Dict[str, Any]] = None):
if composition_dict is None:
self._elements = []
Expand Down
Loading
Loading