Skip to content

Commit 75fe45d

Browse files
authored
Feature/clock msg from timer (#96)
* Reworked timer and events automata generation Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Support base types in JaniVariable definition Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Address bugs Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Minor cleanup and update tests Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Some more mypy errors Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Add clock publisher Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Fix some tests Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Found failure reason Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Make use of clock topic in uc2 example Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Adjust the UC2 model to always succeed after 3 attempts. Add one more test Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Update comment in uc2 move_success property Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Small renaming of variables Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> * Test multiple rates in one system explicitly Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com> --------- Signed-off-by: Marco Lampacrescia <marco.lampacrescia@de.bosch.com>
1 parent 03ad743 commit 75fe45d

File tree

22 files changed

+448
-412
lines changed

22 files changed

+448
-412
lines changed

src/as2fm/jani_generator/jani_entries/jani_edge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
"""And edge defining the possible transition from one state to another in jani."""
1717

18-
from typing import Dict, List, Optional
18+
from typing import Any, Dict, List, Optional
1919

2020
from as2fm.jani_generator.jani_entries import (
2121
JaniAssignment,
@@ -35,7 +35,7 @@ def __init__(self, edge_dict: dict):
3535
self.guard = None
3636
if "guard" in edge_dict:
3737
self.guard = JaniGuard(edge_dict["guard"])
38-
self.destinations = []
38+
self.destinations: List[Dict[str, Any]] = []
3939
if "destinations" not in edge_dict:
4040
return
4141
for dest in edge_dict["destinations"]:

src/as2fm/jani_generator/jani_entries/jani_model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ def get_features(self) -> List[str]:
8383
def add_jani_variable(self, variable: JaniVariable):
8484
self._variables.update({variable.name(): variable})
8585

86+
def add_jani_variables(self, variables: List[JaniVariable]):
87+
for jani_var in variables:
88+
self.add_jani_variable(jani_var)
89+
8690
def add_variable(
8791
self,
8892
variable_name: str,

src/as2fm/jani_generator/jani_entries/jani_variable.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121

2222
from as2fm.as2fm_common.common import ValidTypes
2323
from as2fm.jani_generator.jani_entries import JaniExpression, JaniValue
24+
from as2fm.jani_generator.jani_entries.jani_expression import SupportedExp
2425

2526

2627
class JaniVariable:
2728
@staticmethod
2829
def from_dict(variable_dict: dict) -> "JaniVariable":
2930
variable_name = variable_dict["name"]
3031
initial_value = variable_dict.get("initial-value", None)
31-
variable_type: type = JaniVariable.python_type_from_json(variable_dict["type"])
32+
variable_type = JaniVariable.python_type_from_json(variable_dict["type"])
3233
if initial_value is None:
3334
return JaniVariable(
3435
variable_name, variable_type, None, variable_dict.get("transient", False)
@@ -60,10 +61,12 @@ def __init__(
6061
self,
6162
v_name: str,
6263
v_type: Type[ValidTypes],
63-
init_value: Optional[Union[JaniExpression, JaniValue]] = None,
64+
init_value: Optional[Union[JaniExpression, JaniValue, SupportedExp]] = None,
6465
v_transient: bool = False,
6566
):
66-
assert init_value is None or isinstance(init_value, (JaniExpression, JaniValue)), (
67+
assert init_value is None or isinstance(
68+
init_value, (JaniExpression, JaniValue, int, float, bool)
69+
), (
6770
f"Expected {v_name} init_value {init_value} to be of type "
6871
f"(JaniExpression, JaniValue), found {type(init_value)} instead."
6972
)
@@ -116,7 +119,7 @@ def as_dict(self):
116119
return d
117120

118121
@staticmethod
119-
def python_type_from_json(json_type: Union[str, dict]) -> ValidTypes:
122+
def python_type_from_json(json_type: Union[str, dict]) -> Type[ValidTypes]:
120123
"""
121124
Translate a (Jani) type string or dict to a Python type.
122125
"""

src/as2fm/jani_generator/ros_helpers/ros_timer.py

Lines changed: 37 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,10 @@
2020
from math import floor, gcd
2121
from typing import List, Optional, Tuple
2222

23-
from as2fm.jani_generator.jani_entries import (
24-
JaniAssignment,
25-
JaniAutomaton,
26-
JaniEdge,
27-
JaniExpression,
28-
JaniGuard,
29-
JaniVariable,
30-
)
31-
from as2fm.jani_generator.jani_entries.jani_expression_generator import (
32-
and_operator,
33-
equal_operator,
34-
lower_operator,
35-
modulo_operator,
36-
not_operator,
37-
plus_operator,
38-
)
3923
from as2fm.scxml_converter.scxml_entries import (
24+
RosField,
25+
RosTopicPublish,
26+
RosTopicPublisher,
4027
ScxmlAssign,
4128
ScxmlData,
4229
ScxmlDataModel,
@@ -55,7 +42,7 @@
5542
"ns": 1e-9,
5643
}
5744

58-
GLOBAL_TIMER_NAME = "global_timer"
45+
GLOBAL_TIMER_AUTOMATON = "autogenerated_global_timer"
5946
GLOBAL_TIMER_TICK_ACTION = "global_timer_tick"
6047
ROS_TIMER_RATE_EVENT_PREFIX = "ros_time_rate."
6148

@@ -116,111 +103,6 @@ def get_gcd_of_timer_periods(timers: List[RosTimer]) -> Tuple[int, str]:
116103
return common_period, common_unit
117104

118105

119-
def make_global_timer_automaton(
120-
timers: List[RosTimer], max_time_ns: int
121-
) -> Optional[JaniAutomaton]:
122-
"""
123-
Create a global timer Jani automaton from a list of ROS timers.
124-
125-
:param timers: The list of ROS timers.
126-
:return: The global timer automaton.
127-
"""
128-
if len(timers) == 0:
129-
return None
130-
global_timer_period, global_timer_period_unit = get_gcd_of_timer_periods(timers)
131-
timers_map = {
132-
timer.name: convert_time_between_units(
133-
timer.period_int, timer.unit, global_timer_period_unit
134-
)
135-
for timer in timers
136-
}
137-
try:
138-
max_time = convert_time_between_units(max_time_ns, "ns", global_timer_period_unit)
139-
except AssertionError:
140-
raise ValueError(
141-
f"Max time {max_time_ns} cannot be converted to {global_timer_period_unit}. "
142-
"The max_time must have a unit that is greater or equal to the smallest timer period."
143-
)
144-
145-
# Automaton
146-
LOC_NAME = "loc"
147-
timer_automaton = JaniAutomaton()
148-
timer_automaton.set_name(GLOBAL_TIMER_NAME)
149-
timer_automaton.add_location(LOC_NAME, is_initial=True)
150-
151-
# Check if timers are correctly defined
152-
assert len(timers) > 0, "At least one timer is required."
153-
154-
# variables
155-
variable_names = [f"{timer.name}_needed" for timer in timers]
156-
timer_automaton.add_variable(JaniVariable("t", int, JaniExpression(0)))
157-
for variable_name in variable_names:
158-
timer_automaton.add_variable(JaniVariable(variable_name, bool, JaniExpression(True)))
159-
# it is initially true, because everything "x % 0 == 0"
160-
161-
# edges
162-
# timer assignments
163-
timer_assignments = []
164-
for i, (timer, variable_name) in enumerate(zip(timers, variable_names)):
165-
period_in_global_unit = timers_map[timer.name]
166-
timer_assignments.append(
167-
JaniAssignment(
168-
{
169-
"ref": variable_name,
170-
# t % {period_in_global_unit} == 0
171-
"value": equal_operator(modulo_operator("t", period_in_global_unit), 0),
172-
"index": i + 1,
173-
}
174-
)
175-
) # 1, because t is at index 0
176-
# guard for main edge
177-
# Max time not reached yet
178-
guard_exp = lower_operator("t", max_time)
179-
assert len(variable_names) > 0, "At least one timer is required."
180-
# No unprocessed timer callbacks present
181-
for variable_name in variable_names:
182-
unprocessed_timer_exp = not_operator(variable_name)
183-
# Append this expression to the guard using the and operator
184-
guard_exp = and_operator(guard_exp, unprocessed_timer_exp)
185-
# TODO: write test case for this (and switch to not(t1 or t2 or ... or tN) guard)
186-
assignments = [
187-
# t = t + global_timer_period
188-
JaniAssignment({"ref": "t", "value": plus_operator("t", global_timer_period), "index": 0})
189-
] + timer_assignments
190-
iterator_edge = JaniEdge(
191-
{
192-
"location": LOC_NAME,
193-
"guard": JaniGuard(guard_exp),
194-
"destinations": [{"location": LOC_NAME, "assignments": assignments}],
195-
"action": GLOBAL_TIMER_TICK_ACTION,
196-
}
197-
)
198-
timer_automaton.add_edge(iterator_edge)
199-
200-
# edges to sync with ROS timers
201-
for timer in timers:
202-
guard = JaniGuard(JaniExpression(f"{timer.name}_needed"))
203-
timer_edge = JaniEdge(
204-
{
205-
"location": LOC_NAME,
206-
"action": f"{ROS_TIMER_RATE_EVENT_PREFIX}{timer.name}_on_receive",
207-
"guard": guard,
208-
"destinations": [
209-
{
210-
"location": LOC_NAME,
211-
"assignments": [
212-
JaniAssignment(
213-
{"ref": f"{timer.name}_needed", "value": JaniExpression(False)}
214-
)
215-
],
216-
}
217-
],
218-
}
219-
)
220-
timer_automaton.add_edge(timer_edge)
221-
return timer_automaton
222-
223-
224106
def make_global_timer_scxml(timers: List[RosTimer], max_time_ns: int) -> Optional[ScxmlRoot]:
225107
"""
226108
Create a global timer SCXML automaton from a list of ROS timers.
@@ -244,27 +126,51 @@ def make_global_timer_scxml(timers: List[RosTimer], max_time_ns: int) -> Optiona
244126
f"Max time {max_time_ns}ns cannot be converted to '{global_timer_period_unit}'. "
245127
"The max_time must have a unit that is greater or equal to the smallest timer period."
246128
)
247-
scxml_root = ScxmlRoot("global_timer_automata")
248-
scxml_root.set_data_model(ScxmlDataModel([ScxmlData("current_time", "0", "int64")]))
129+
curr_time_var = "current_time"
130+
scxml_root = ScxmlRoot(GLOBAL_TIMER_AUTOMATON)
131+
clock_topic_decl = RosTopicPublisher("clock", "builtin_interfaces/Time")
132+
scxml_root.add_ros_declaration(clock_topic_decl)
133+
scxml_root.set_data_model(ScxmlDataModel([ScxmlData(curr_time_var, "0", "int64")]))
249134
idle_state = ScxmlState("idle")
250-
global_timer_tick_body: ScxmlExecutionBody = []
251-
global_timer_tick_body.append(
252-
ScxmlAssign("current_time", f"current_time + {global_timer_period}")
253-
)
135+
global_timer_tick_body: ScxmlExecutionBody = [
136+
ScxmlAssign(curr_time_var, f"{curr_time_var} + {global_timer_period}"),
137+
_get_current_time_to_clock_msg_publish(
138+
clock_topic_decl, curr_time_var, global_timer_period_unit
139+
),
140+
]
254141
for timer_name, timer_period in timers_map.items():
255142
global_timer_tick_body.append(
256143
ScxmlIf(
257144
[
258145
(
259-
f"(current_time % {timer_period}) == 0",
260-
[ScxmlSend(f"ros_time_rate.{timer_name}")],
146+
f"({curr_time_var} % {timer_period}) == 0",
147+
[ScxmlSend(f"{ROS_TIMER_RATE_EVENT_PREFIX}{timer_name}")],
261148
)
262149
]
263150
)
264151
)
265152
timer_step_transition = ScxmlTransition.make_single_target_transition(
266-
"idle", [], f"current_time < {max_time}", global_timer_tick_body
153+
"idle", [], f"{curr_time_var} < {max_time}", global_timer_tick_body
267154
)
268155
idle_state.add_transition(timer_step_transition)
269156
scxml_root.add_state(idle_state, initial=True)
270157
return scxml_root
158+
159+
160+
def _get_current_time_to_clock_msg_publish(
161+
clock_decl: RosTopicPublisher, curr_time_var: str, time_unit: str
162+
) -> RosTopicPublish:
163+
assert time_unit in TIME_UNITS
164+
if time_unit == "s":
165+
return RosTopicPublish(
166+
clock_decl, [RosField("sec", curr_time_var), RosField("nanosec", "0")]
167+
)
168+
n_units_per_sec = round(1.0 / TIME_UNITS[time_unit])
169+
time_unit_in_nsec = 1e9 / n_units_per_sec
170+
return RosTopicPublish(
171+
clock_decl,
172+
[
173+
RosField("sec", f"Math.floor({curr_time_var} / {n_units_per_sec})"),
174+
RosField("nanosec", f"({curr_time_var} % {n_units_per_sec}) * {time_unit_in_nsec}"),
175+
],
176+
)

src/as2fm/jani_generator/scxml_helpers/scxml_event.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,17 @@ def has_receivers(self) -> bool:
9999
"""Check if the event has one or more receivers."""
100100
return len(self.receivers) > 0
101101

102+
def is_timer_event(self) -> bool:
103+
"""Check if this is a timer event."""
104+
return self.name.startswith(ROS_TIMER_RATE_EVENT_PREFIX)
105+
102106
def must_be_skipped_in_jani_conversion(self):
103-
"""Indicate whether this must be considered in the conversion to jani."""
104-
return (
105-
# If the event is a timer event, there is only a receiver.
106-
# It is the edge that the user declared with the `ros_rate_callback` tag.
107-
# It will be handled in the `scxml_event_processor` module differently.
108-
self.name.startswith(ROS_TIMER_RATE_EVENT_PREFIX)
109-
or self.is_removable_interface()
110-
)
107+
"""
108+
Check if the event must be ignored.
109+
110+
Those are events related to specific interfaces providing only receivers, and no senders.
111+
"""
112+
return self.is_removable_interface()
111113

112114
def is_removable_interface(self):
113115
"""Indicate if the interface contained by this event shall be removed."""

0 commit comments

Comments
 (0)