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

action

Behaviour

Actions are derived from scenario_execution.actions.base_action.BaseAction which is derived from py_trees.behaviour.Behaviour

event

blackboard entry and Behaviour

Behaviour is used to read and write blackboard variable

modifier

Decorator

var

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

action

partially, see details below

actor

partially, see details below

as

bool

call

cover

def

only external

default

do

elapsed

emit

enum

event

every

expression

method bodies are external only

extend

external

method implementation qualifier

fall

float

global

hard

if

guards an event reference, see below

import

inherits

int

is

it

keep

list

of

on

one_of

only

method implementation qualifier

parallel

range

record

remove_default

rise

scenario

serial

SI

string

struct

type

uint

undefined

unit

until

var

wait

with

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.