Skip to content

Commit

Permalink
Plain SCXML matches the standard (#63)
Browse files Browse the repository at this point in the history
Signed-off-by: Marco Lampacrescia <[email protected]>
  • Loading branch information
MarcoLm993 authored Oct 30, 2024
1 parent 6a8f1cc commit b5699a6
Show file tree
Hide file tree
Showing 39 changed files with 307 additions and 142 deletions.
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)
# 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."""
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,
):
"""
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."""
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"})
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

0 comments on commit b5699a6

Please sign in to comment.