Skip to main content
Version: 0.6.0

Actions

This article has examples in the following target languages:

Action Declaration​

An action declaration has one of the following forms:

logical action <name>(<min_delay>, <min_spacing>, <policy>) physical action <name>(<min_delay>, <min_spacing>, <policy>)

The min_delay, min_spacing, and policy are all optional. If only one argument is given in parentheses, then it is interpreted as an min_delay, if two are given, then they are interpreted as min_delay and min_spacing. The min_delay and min_spacing are time values. The policy argument is a string that can be one of the following: "defer" (the default), "drop", or "replace". Note that the quotation marks are needed.

If the action is to carry a payload, then a type must be given as well:

logical action <name>(<min_delay>, <min_spacing>, <policy>):<type> physical action <name>(<min_delay>, <min_spacing>, <policy>):<type>

Logical Actions​

Timers are useful to trigger reactions once or periodically. Actions are used to trigger reactions more irregularly. An action, like an output or input port, can carry data, but unlike a port, an action is visible only within the reactor that defines it.

There are two kinds of actions, logical and physical. A logical action is used by a reactor to schedule a trigger at a fixed logical time interval d into the future. The time interval d, which is called a delay, is relative to the logical time t at which the scheduling occurs. If a reaction executes at logical time t and schedules an action a with delay d, then any reaction that is triggered by a will be invoked at logical time t + d. For example, the following reaction schedules something (printing the current elapsed logical time) 200 msec after an input x arrives:

target C; reactor Schedule { input x:int; logical action a; reaction(x) -> a {= lf_schedule(a, MSEC(200)); =} reaction(a) {= interval_t elapsed_time = lf_time_logical_elapsed(); printf("Action triggered at logical time %lld nsec after start.\n", elapsed_time); =} }

Lingua Franca diagram

Here, the delay is specified in the call to schedule within the target language code. Notice that in the diagram, a logical action is shown as a triangle with an L. Logical actions are always scheduled within a reaction of the reactor that declares the action.

The time argument is required to be non-negative. If it is zero, then the action will be scheduled one microstep later. See Superdense Time.

The arguments to the lf_schedule() function are the action named a and a time. The action a has to be declared as an effect of the reaction in order to reference it in the call to lf_schedule(). If you fail to declare it as an effect (after the -> in the reaction signature), then you will get an error message.

The time argument to the lf_schedule() function has data type interval_t, which, with the exception of some embedded platforms, is a C int64_t. A collection of convenience macros is provided like the MSEC macro above to specify time values in a more readable way. The provided macros are NSEC, USEC (for microseconds), MSEC, SEC, MINUTE, HOUR, DAY, and WEEK. You may also use the plural of any of these, e.g. WEEKS(2).

An action may have a data type, in which case, a variant of the lf_schedule() function can be used to specify a payload, a data value that is carried from where the lf_schedule() function is called to the reaction that is triggered by the action. See the Target Language Details.

Physical Actions​

A physical action is used to schedule reactions at logical times determined by the local physical clock. If a physical action with delay d is scheduled at physical time T, then the logical time assigned to the event is T + d. For example, the following reactor schedules the physical action p to trigger at a logical time equal to the physical time at which the input x arrives:

target C; reactor Physical { input x:int; physical action a; reaction(x) -> a {= lf_schedule(a, 0); =} reaction(a) {= interval_t elapsed_time = lf_time_logical_elapsed(); printf("Action triggered at logical time %lld nsec after start.\n", elapsed_time); =} }

Lingua Franca diagram

If you drive this with a timer, using for example the following structure:

Lingua Franca diagram

then running the program will yield an output something like this:

Action triggered at logical time 201491000 nsec after start.
Action triggered at logical time 403685000 nsec after start.
Action triggered at logical time 603669000 nsec after start.
...

Here, logical time is lagging physical time by a few milliseconds. Note that, unless the fast option is given, logical time t chases physical time T, so t < T. Hence, the event being scheduled in the reaction to input x is assured of being in the future in logical time.

Whereas logical actions are required to be scheduled within a reaction of the reactor that declares the action, physical actions can be scheduled by code that is outside the Lingua Franca system. For example, some other thread or a callback function may call schedule(), passing it a physical action. For example:

target C { keepalive: true // Do not exit when event queue is empty. } preamble {= #include "platform.h" // Defines lf_sleep() and thread functions. =} main reactor { preamble {= // Schedule an event roughly every 200 msec. void* external(void* a) { while (true) { lf_sleep(MSEC(200)); lf_schedule(a, 0); } } =} state thread_id: lf_thread_t = 0 physical action a(100 msec): int reaction(startup) -> a {= // Start a thread to schedule physical actions. lf_thread_create(&self->thread_id, &external, a); =} reaction(a) {= interval_t elapsed_time = lf_time_logical_elapsed(); printf("Action triggered at logical time %lld nsec after start.\n", elapsed_time); =} }
Lingua Franca diagram: AsynchronousAsynchronous12Pmin delay: 100 msec

Physical actions are the mechanism for obtaining input from the outside world. Because they are assigned a logical time derived from the physical clock, their logical time can be interpreted as a measure of the time at which some external event occurred.

In the above example, at startup, the main reactor creates an external thread that schedules a physical action roughly every 200 msec.

First, the file-level preamble has #include "platform.h", which includes the declarations for functions it uses, lf_sleep and lf_thread_create (see Libraries Available to Programmers).

Second, the thread uses a function lf_sleep(), which abstracts platform-specific mechanisms for stalling the thread for a specified amount of time, and lf_thread_create(), which abstracts platform-specific mechanisms for creating threads.

The external function executed by the thread is defined in a reactor-level preamble section. See Preambles.

Triggering Time for Actions​

An action will trigger at a logical time that depends on the arguments given to the schedule function, the <min_delay>, <min_spacing>, and <policy> arguments in the action declaration, and whether the action is physical or logical.

For a logical action a, the tag assigned to the event resulting from a call to schedule() is computed as follows. First, let t be the current logical time. For a logical action, t is just the logical time at which the reaction calling schedule() is called. The preliminary time of the action is then just t + <min_delay> + <offset>. This preliminary time may be further modified, as explained below.

For a physical action, the preliminary time is similar, except that t is replaced by the current physical time T when schedule() is called.

If a <min_spacing> has been declared, then it gives a minimum logical time interval between the tags of two subsequently scheduled events. If the preliminary time is closer than <min_spacing> to the time of the previously scheduled event (if there is one), then <policy> (if supported by the target) determines how the minimum spacing constraint is enforced. Note that "previously scheduled" here means specifically the tag resulting from the most recent call to lf_schedule for the same action.

warning

Since calls to lf_schedule can specify arbitrary extra delays, <min_spacing> does not necessarily result in events with minimum spacing between them. If your calls to lf_schedule result in monotonically increasing tags, however, you will get events with minimum spacing between them.

The <policy> is one of the following:

  • "defer": (the default) The event is added to the event queue with a tag that is equal to earliest time that satisfies the minimal spacing requirement. Assuming the time of the preceding event is t_prev, then the tag of the new event simply becomes t_prev + <min_spacing>.
  • "drop": The new event is dropped and schedule() returns without having modified the event queue.
  • "replace": The payload (if any) of the new event is assigned to the preceding event if it is still pending in the event queue; no new event is added to the event queue in this case. If the preceding event has already been pulled from the event queue, the default "defer" policy is applied.

Note that while the "defer" policy is conservative in the sense that it does not discard events, it could potentially cause an unbounded growth of the event queue.

Testing an Action for Presence​

When a reaction is triggered by more than one action or by an action and an input, it may be necessary to test within the reaction whether the action is present. Just like for inputs, this can be done in the C target with a->is_present C++ target with a.is_present() TypeScript target with a != undefined Rust target with ctx.is_present(a) Python target with a.is_present, where a is the name of the action.