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

Feature/add missing fields in plain scxml #63

Merged
merged 14 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 11 additions & 6 deletions src/as2fm/jani_generator/jani_entries/jani_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import Any, Dict, Optional, Tuple, Union

from as2fm.jani_generator.jani_entries import JaniValue
from as2fm.scxml_converter.scxml_entries.utils import PLAIN_SCXML_EVENT_DATA_PREFIX

SupportedExp = Union[str, int, float, bool, dict, list]

Expand Down Expand Up @@ -190,20 +191,24 @@ def get_expression_type(self) -> JaniExpressionType:
raise RuntimeError("Unknown expression type")

def replace_event(self, replacement: Optional[str]):
"""Replace `_event` with `replacement`.
"""Replace the default SCXML event prefix with the provided replacement.

Within a transitions, scxml can access data of events from the `_event` variable. We
have to replace this by the global variable where we stored the data from the received
Within a transitions, scxml can access to the event's parameters using a specific prefix.
We have to replace this by the global variable where we stored the data from the received
event.

:param replacement: The string to replace `_event` with.
:param replacement: The string to replace `PLAIN_SCXML_EVENT_DATA_PREFIX` with.
:return self: for the convenience of chain-ability
"""
if replacement is None:
# No replacement needed!
return self
if self.identifier is not None and self.identifier.startswith("_event."):
self.identifier = f"{replacement}.{self.identifier.removeprefix('_event.')}"
if self.identifier is not None and self.identifier.startswith(
PLAIN_SCXML_EVENT_DATA_PREFIX
):
self.identifier = (
f"{replacement}.{self.identifier.removeprefix(PLAIN_SCXML_EVENT_DATA_PREFIX)}"
)
return self
if self.value is not None:
return self
Expand Down
4 changes: 2 additions & 2 deletions src/as2fm/jani_generator/ros_helpers/ros_action_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
)
from as2fm.scxml_converter.scxml_entries.utils import (
PLAIN_FIELD_EVENT_PREFIX,
PLAIN_SCXML_EVENT_PREFIX,
PLAIN_SCXML_EVENT_DATA_PREFIX,
ROS_FIELD_PREFIX,
)

Expand Down Expand Up @@ -114,7 +114,7 @@ def _generate_srv_event_transition(
scxml_transition = ScxmlTransition(goal_state.get_id(), [srv_event_name])
for entry_name in extra_entries:
scxml_transition.append_body_executable_entry(
ScxmlAssign(entry_name, PLAIN_SCXML_EVENT_PREFIX + entry_name)
ScxmlAssign(entry_name, PLAIN_SCXML_EVENT_DATA_PREFIX + entry_name)
)
out_params: List[ScxmlParam] = []
for entry_name in additional_data:
Expand Down
2 changes: 1 addition & 1 deletion src/as2fm/jani_generator/scxml_helpers/scxml_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ def write_model(self):
else:
guard = None

original_transition_body = self.element.get_executable_body()
original_transition_body = self.element.get_body()

merged_transition_body = []
if current_state.get_onexit() is not None:
Expand Down
53 changes: 35 additions & 18 deletions src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import json
import os
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

Expand All @@ -36,7 +37,7 @@
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.scxml_entries import ScxmlRoot
from as2fm.scxml_converter.scxml_entries import EventsToAutomata, ScxmlRoot


@dataclass()
Expand Down Expand Up @@ -194,6 +195,38 @@ def generate_plain_scxml_models_and_timers(
return plain_scxml_models, all_timers


def export_plain_scxml_models(
generated_scxml_path: str,
plain_scxml_models: List[ScxmlRoot],
all_timers: List[RosTimer],
max_time: int,
):
"""Generate the plain SCXML files adding all compatibility entries to fit the SCXML standard."""
os.makedirs(generated_scxml_path, exist_ok=True)
models_to_export = deepcopy(plain_scxml_models)
global_timer_scxml = make_global_timer_scxml(all_timers, max_time)
if global_timer_scxml is not None:
models_to_export.append(global_timer_scxml)
# Compute the set of target automaton for each event
event_targets: EventsToAutomata = {}
for scxml_model in models_to_export:
for event in scxml_model.get_transition_events():
if event not in event_targets:
event_targets[event] = set()
event_targets[event].add(scxml_model.get_name())
# Add the target automaton to each event sent
for scxml_model in models_to_export:
scxml_model.add_targets_to_scxml_sends(event_targets)
EnricoGhiorzi marked this conversation as resolved.
Show resolved Hide resolved
# Export the models
for scxml_model in models_to_export:
with open(
os.path.join(generated_scxml_path, f"{scxml_model.get_name()}.scxml"),
"w",
encoding="utf-8",
) as f:
f.write(scxml_model.as_xml_string(data_type_as_attribute=False))


def interpret_top_level_xml(
xml_path: str, jani_file: str, generated_scxmls_dir: Optional[str] = None
):
Expand All @@ -213,23 +246,7 @@ def interpret_top_level_xml(

if generated_scxmls_dir is not None:
plain_scxml_dir = os.path.join(model_dir, generated_scxmls_dir)
os.makedirs(plain_scxml_dir, exist_ok=True)
for scxml_model in plain_scxml_models:
with open(
os.path.join(plain_scxml_dir, f"{scxml_model.get_name()}.scxml"),
"w",
encoding="utf-8",
) as f:
f.write(scxml_model.as_xml_string())
# Additionally, write the timers SCXML model
global_timer_scxml = make_global_timer_scxml(all_timers, model.max_time)
if global_timer_scxml is not None:
with open(
os.path.join(plain_scxml_dir, f"{global_timer_scxml.get_name()}.scxml"),
"w",
encoding="utf-8",
) as f:
f.write(global_timer_scxml.as_xml_string())
export_plain_scxml_models(plain_scxml_dir, plain_scxml_models, all_timers, model.max_time)

jani_model = convert_multiple_scxmls_to_jani(
plain_scxml_models, all_timers, model.max_time, model.max_array_size
Expand Down
21 changes: 13 additions & 8 deletions src/as2fm/scxml_converter/scxml_entries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@
from .scxml_data_model import ScxmlDataModel # noqa: F401
from .ros_utils import ScxmlRosDeclarationsContainer # noqa: F401
from .scxml_executable_entries import ScxmlAssign, ScxmlIf, ScxmlSend # noqa: F401
from .scxml_executable_entries import ScxmlExecutableEntry, ScxmlExecutionBody # noqa: F401
from .scxml_executable_entries import ( # noqa: F401
ScxmlExecutableEntry,
ScxmlExecutionBody,
EventsToAutomata,
) # noqa: F401
from .scxml_executable_entries import ( # noqa: F401
execution_body_from_xml,
as_plain_execution_body, # noqa: F401
as_plain_execution_body,
execution_entry_from_xml,
valid_execution_body, # noqa: F401
valid_execution_body,
valid_execution_body_entry_types,
instantiate_exec_body_bt_events,
add_targets_to_scxml_send,
) # noqa: F401
from .scxml_transition import ScxmlTransition # noqa: F401
from .scxml_bt_ticks import BtTick, BtTickChild, BtChildStatus, BtReturnStatus # noqa: F401
Expand All @@ -37,27 +42,27 @@
from .scxml_ros_service import ( # noqa: F401
RosServiceServer,
RosServiceClient,
RosServiceHandleRequest, # noqa: F401
RosServiceHandleRequest,
RosServiceHandleResponse,
RosServiceSendRequest,
RosServiceSendResponse,
) # noqa: F401
from .scxml_ros_action_client import ( # noqa: F401
RosActionClient,
RosActionSendGoal,
RosActionHandleGoalResponse, # noqa: F401
RosActionHandleGoalResponse,
RosActionHandleFeedback,
RosActionHandleSuccessResult, # noqa: F401
RosActionHandleSuccessResult,
RosActionHandleCanceledResult,
RosActionHandleAbortedResult,
) # noqa: F401
from .scxml_ros_action_server import ( # noqa: F401
RosActionServer,
RosActionHandleGoalRequest,
RosActionAcceptGoal, # noqa: F401
RosActionAcceptGoal,
RosActionRejectGoal,
RosActionStartThread,
RosActionSendFeedback, # noqa: F401
RosActionSendFeedback,
RosActionSendSuccessResult,
) # noqa: F401
from .scxml_ros_action_server_thread import ( # noqa: F401
Expand Down
18 changes: 14 additions & 4 deletions src/as2fm/scxml_converter/scxml_entries/scxml_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ def __init__(
def get_name(self) -> str:
return self._id

def get_type_str(self) -> str:
"""Get the type of the data as a string."""
return self._data_type

def get_type(self) -> type:
"""Get the type of the data as a Python type."""
EnricoGhiorzi marked this conversation as resolved.
Show resolved Hide resolved
python_type = get_data_type_from_string(self._data_type)
assert (
python_type is not None
Expand Down Expand Up @@ -176,11 +181,16 @@ def check_validity(self) -> bool:
valid_bounds = self.check_valid_bounds()
return valid_id and valid_expr and valid_bounds

def as_xml(self) -> ET.Element:
def as_xml(self, type_as_attribute: bool = True) -> ET.Element:
"""
Generate the XML element representing the single data entry.

:param type_as_attribute: If True, the type of the data is added as an attribute.
"""
assert self.check_validity(), "SCXML: found invalid data object."
xml_data = ET.Element(
ScxmlData.get_tag_name(), {"id": self._id, "expr": self._expr, "type": self._data_type}
)
xml_data = ET.Element(ScxmlData.get_tag_name(), {"id": self._id, "expr": self._expr})
if type_as_attribute:
xml_data.set("type", self._data_type)
if self._lower_bound is not None:
xml_data.set("lower_bound_incl", str(self._lower_bound))
if self._upper_bound is not None:
Expand Down
13 changes: 11 additions & 2 deletions src/as2fm/scxml_converter/scxml_entries/scxml_data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,20 @@ def check_validity(self) -> bool:
return False
return True

def as_xml(self) -> Optional[ET.Element]:
def as_xml(self, type_as_attribute: bool = True) -> Optional[ET.Element]:
"""
Store the datamodel, containing all model's data entries, as an XML element.

:param type_as_attribute: If True, store data types as arguments, if False as Comments
"""
assert self.check_validity(), "SCXML: found invalid datamodel object."
if self._data_entries is None or len(self._data_entries) == 0:
return None
xml_datamodel = ET.Element(ScxmlDataModel.get_tag_name())
for data_entry in self._data_entries:
xml_datamodel.append(data_entry.as_xml())
if not type_as_attribute:
xml_datamodel.append(
ET.Comment(f" TYPE {data_entry.get_name()}:{data_entry.get_type_str()} ")
)
xml_datamodel.append(data_entry.as_xml(type_as_attribute))
return xml_datamodel
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"""

import warnings
from typing import Dict, List, Optional, Tuple, Union, get_args
from copy import deepcopy
from typing import Dict, List, Optional, Set, Tuple, Union, get_args

from lxml import etree as ET

Expand All @@ -45,6 +46,8 @@
ScxmlExecutableEntry = Union["ScxmlAssign", "ScxmlIf", "ScxmlSend"]
ScxmlExecutionBody = List[ScxmlExecutableEntry]
ConditionalExecutionBody = Tuple[str, ScxmlExecutionBody]
# Map each event ID to a list of automata transitioning using that event
EventsToAutomata = Dict[str, Set[str]]


def instantiate_exec_body_bt_events(
Expand Down Expand Up @@ -251,26 +254,33 @@ def from_xml_tree(xml_tree: ET.Element) -> "ScxmlSend":
xml_tree.tag == ScxmlSend.get_tag_name()
), f"Error: SCXML send: XML tag name is not {ScxmlSend.get_tag_name()}."
event = xml_tree.attrib["event"]
target = xml_tree.attrib.get("target")
params: List[ScxmlParam] = []
assert params is not None, "Error: SCXML send: params is not valid."
for param_xml in xml_tree:
if is_comment(param_xml):
continue
params.append(ScxmlParam.from_xml_tree(param_xml))
return ScxmlSend(event, params)
return ScxmlSend(event, params, target)

def __init__(self, event: str, params: Optional[List[ScxmlParam]] = None):
def __init__(
self,
event: str,
params: Optional[List[ScxmlParam]] = None,
target_automaton: Optional[str] = None,
EnricoGhiorzi marked this conversation as resolved.
Show resolved Hide resolved
):
"""
Construct a new ScxmlSend object.

:param event: The name of the event sent when executing this entry.
:param params: The parameters to send as part of the event.
:param cb_type: The kind of callback executing this SCXML entry.
:param target_automaton: The target automaton for this send event.
"""
if params is None:
params = []
self._event = event
self._params = params
self._target_automaton = target_automaton
self._cb_type: Optional[CallbackType] = None

def set_callback_type(self, cb_type: CallbackType) -> None:
Expand All @@ -285,6 +295,14 @@ def get_params(self) -> List[ScxmlParam]:
"""Get the parameters to send."""
return self._params

def get_target_automaton(self) -> Optional[str]:
"""Get the target automata associated to this send event."""
return self._target_automaton

def set_target_automaton(self, target_automaton: str) -> None:
"""Set the target automata associated to this send event."""
EnricoGhiorzi marked this conversation as resolved.
Show resolved Hide resolved
self._target_automaton = target_automaton

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.
Expand Down Expand Up @@ -349,6 +367,8 @@ def as_plain_scxml(self, _) -> "ScxmlSend":
def as_xml(self) -> ET.Element:
assert self.check_validity(), "SCXML: found invalid send object."
xml_send = ET.Element(ScxmlSend.get_tag_name(), {"event": self._event})
if self._target_automaton is not None:
xml_send.set("target", self._target_automaton)
for param in self._params:
xml_send.append(param.as_xml())
return xml_send
Expand Down Expand Up @@ -542,3 +562,35 @@ def as_plain_execution_body(
if exec_body is None:
return None
return [entry.as_plain_scxml(ros_declarations) for entry in exec_body if not is_comment(entry)]


def add_targets_to_scxml_send(
exec_body: Optional[ScxmlExecutionBody], events_to_automata: EventsToAutomata
) -> Optional[ScxmlExecutionBody]:
"""For each ScxmlSend in the body, generate instances containing the target automaton."""
if exec_body is None:
return None
new_body: ScxmlExecutionBody = []
for entry in exec_body:
if isinstance(entry, ScxmlIf):
if_conditionals = []
for cond, cond_body in entry.get_conditional_executions():
if_conditionals.append(
(cond, add_targets_to_scxml_send(cond_body, events_to_automata))
)
else_body = add_targets_to_scxml_send(entry.get_else_execution(), events_to_automata)
new_body.append(ScxmlIf(if_conditionals, else_body))
elif isinstance(entry, ScxmlSend):
target_automata = events_to_automata.get(entry.get_event(), {"NONE"})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EnricoGhiorzi on this one I am not entirely sure: there are cases in which we send an event, but we define no transition for it.
This might be desired in case the developer wants to expose a specific information from a single automaton only for referring to it in a property.
For now, I decided to add a "NONE" target for such cases, but we can find other alternatives

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the use for such a feature, but I worry about aligning the model with the implementation. Would this be how the real system implements such a feature? In other words, is it possible to monitor a topic that has a publisher but no subscribers? (I guess the monitor itself is a subscriber, but that is not modelled.)

Reguarding the "NONE" syntax, I looked up the SCXML specification but is provided no insight: there is no way to send an event into the void, and having a non-existing target would raise an error.

Another option could be to automatically create such a "NONE" automaton and just let it process any event it receives.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To monitor a topic, you need to subscribe to it, and as you mentioned above, the monitor will do exactly that.
If you are fine with it, you can assume to have the NONE automaton that acts as a receiver for all events (though I am not sure it makes sense to auto-generate it: maybe it is easier to have a script removing the send events with a NONE target)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it is easier to have a script removing the send events with a NONE target

I don't get it: if we remove these sends, the events will not be observable in SCAN either. I think we either need to send the events into the void (as it is now) and allow that in SCAN, or to generate a target NONE automaton, either at AS2FM or SCAN level, possibly consistently with the actual implementation. @SofiaFaraci might have some insight on this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment about (optionally) removing the NONE events was only for executing system outside of SCAN. Otherwise, I would be happy to keep it as is and allow the NONE target in SCAN, if that works for you.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we either need to send the events into the void (as it is now) and allow that in SCAN, or to generate a target NONE automaton, either at AS2FM or SCAN level, possibly consistently with the actual implementation. @SofiaFaraci might have some insight on this.

In the actual implementation there is no need for a target for the send tag, the events are directly handled by the generated C++ code of the SMs that sends the event.

assert (
entry.get_target_automaton() is None
), f"Error: SCXML send: target automaton already set for event {entry.get_event()}."
for automaton in target_automata:
new_entry = deepcopy(entry)
new_entry.set_target_automaton(automaton)
new_body.append(new_entry)
elif isinstance(entry, ScxmlAssign):
new_body.append(deepcopy(entry))
else:
raise ValueError(f"Error: SCXML send: invalid entry type {type(entry)}.")
return new_body
Loading
Loading