OpenSCENARIO DSL
General
This tool supports a subset of the OpenSCENARIO DSL standard.
The official documentation is available here.
The standard library of OSC2 was adapted to be usable by the current parsing support of scenario execution.
Mapping to py-trees
OpenScenario2 |
py-trees |
Comment |
|---|---|---|
|
|
Actions are derived from |
|
blackboard entry and |
|
|
|
|
|
blackboard entry |
Variables are stored within the blackboard |
Supported features
In the following the OpenSCENARIO DSL keywords are listed with their current support status.
Element Tag |
Support |
Notes |
|---|---|---|
|
✅ |
partially, see details below |
|
✅ |
partially, see details below |
|
❌ |
|
|
✅ |
|
|
❌ |
|
|
❌ |
|
|
✅ |
only |
|
❌ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
|
|
❌ |
method bodies are |
|
❌ |
|
|
✅ |
method implementation qualifier |
|
❌ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
|
|
✅ |
guards an event reference, see below |
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
|
|
✅ |
|
|
✅ |
method implementation qualifier |
|
✅ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
❌ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
❌ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
Composition Types
Composition types are struct, actor, action, scenario.
Element Type |
Support |
Notes |
|---|---|---|
Event |
✅ |
|
Field |
✅ |
|
Constraint |
✅ |
partially |
Method |
✅ |
|
Coverage |
❌ |
|
Modifier |
✅ |
partially (only predefined) |
Patterns
Short, working answers to "how do I express this in a scenario", using the subset described above. Each entry is a shape that has been run, not a sketch.
This section is meant to grow: add to it whenever a use case takes more than one attempt to express, so the next reader finds the answer instead of rediscovering it. Keep the same form -- when to reach for it, the scenario, and any caveat that would otherwise be found the hard way.
Stopping a running action
Stop after a fixed time
When the action is scaffolding -- a recording, a background load -- and only the stopping matters.
ros_bag_record(topics: ['/scan', '/odom']) with:
until elapsed(30s)
until bounds the action it is written on: the action runs, and ends the moment the condition
holds. Written as a one_of against a timer it means the same thing, and is what to reach for
when the two really are peers rather than an action and the thing that stops it.
Caution
until ends the action's branch by invalidating it, so the action reports no status: this
shape can show that the timer fired, never that the action stopped or how it ended. An action
that does not support cancellation is left running until the scenario ends, silently. For a ROS
action whose cancellation is itself the thing under test, action_call() takes cancel_after
and expected_status instead.
Stop when something happens
until takes an event specification -- an elapsed(), an @event, or a condition over
variables -- not an action:
scenario stop_on_count:
var seen: int = 0
do serial:
nav_to_pose(goal_pose: ...) with:
until seen == 3
Several until directives on one invocation end it on the first of them to occur.
When the thing to wait for is an action rather than a condition, it has to be a branch of its own,
which is what one_of is for:
one_of:
nav_to_pose(goal_pose: ...)
wait_for_data(topic_name: '/obstacle_detected', topic_type: 'std_msgs.msg.Bool')
Stop from another branch
When the condition is established elsewhere in the scenario, carry it as an event. The emitting
branch decides when, the until decides what it ends, and neither refers to the other.
scenario stop_on_event:
event obstacle_detected
do parallel:
nav_to_pose(goal_pose: ...) with:
until @obstacle_detected
serial:
wait_for_data(topic_name: '/obstacle_detected', topic_type: 'std_msgs.msg.Bool')
emit obstacle_detected
This is the way to reach across branches. There is no way to name a running action and act on it directly, and there should not be: an event goes through the blackboard, so the two branches stay independent of each other's structure.
Bound a whole block
A with: block on a composition applies to everything inside it, which is the case one_of
cannot state without wrapping the block in another level first.
do serial:
log(msg: 'collecting')
run_process(command: 'sleep 60')
log(msg: 'never reached')
with:
until elapsed(3s)
Require more than the event
An event is a flag: once emitted it stays set, so the first emit ends the action. if guards
the event with a condition that is re-checked on every tick, and the action ends on the first tick
where both hold -- below, on the second batch rather than the first.
scenario until_guarded:
event batch_done
var batches: int = 0
do parallel:
run_process(command: 'sleep 60') with:
until @batch_done if batches == 2
serial:
repeat(3)
wait elapsed(1s)
increment(batches)
emit batch_done
Binding the event to a name with as is not supported: an event carries no payload here, so there
would be nothing for the name to hold.
Note
Two places where this differs from the standard. Termination lands on the first tick at which
the condition holds rather than at the instant of the event, which is true of every condition in
a ticked tree. And the standard describes until only for a behavior invocation, leaving a
with: block on a composition -- which its grammar allows -- unstated; the reading taken here
is that the composition ends on the event.
Inspecting another action
Wait for a process to log something
Label the process, then name the label:
do serial:
app: run_process('./server')
process_log_check('app', ['Ready'])
Combining modifiers
Modifiers stack, and they nest: the one written last ends up closest to the action.
run_process('./load-generator') with:
timeout(30s)
failure_is_success()
timeout() stops the process and reports failure, and failure_is_success() turns that into
the verdict the scenario wants. Order matters: the modifier written last ends up closest to the
action.