Skip to content

Expressions

Calin Crisan edited this page Sep 1, 2022 · 5 revisions

About

Simply put, expressions are formulas that can dictate the value of ports. Just as cell formulas allow adding some kind of logic in an Excel-like spreadsheet, expressions form the basis of automation in qToggle. Expressions are an optional feature of a qToggle device, as defined by the qToggle API.

By setting an expression to a port, the port value will be set to whatever the expression evaluates to. Using expression allows creating dependencies between ports, effectively enabling the user to build complex automation scenarios. When using a hub to control slave devices, one can create dependencies between different devices.

Here's a simple expression example that sets the value of port gpio3 to the logical and of gpio1 and gpio2:

gpio3.expression = "AND($gpio1, $gpio2)"

And here's another one that applies a linear transformation to the value read from adc0 and sets it to port vport:

vport1.expression = "ADD(2.5, MUL($adc0, 10))"

Types Of Expressions

Expressions can be classified as folows:

  • value expressions:
    • device-level (slave-level) value expressions
    • hub-level (master-level) value expressions
  • transform expressions:
    • transform-read expressions
    • transform-write expressions

Value Expressions

Value expressions are the ones that most users are looking for. They dictate the value written to ports while potentially depending on other ports.

Device-level expressions are defined inside (slave) device and may only refer to that device's ports. They allow creating logic that is limited to a particular device. Device-level expressions have the advantage of being fast, often real-time, given that they are evaluated inside the device. However, one cannot create logic between two separate devices using this type of expressions. A device-level expression is set to a port using the expression attribute.

Hub-level expressions are defined on a hub and may refer to ports from different slave devices that are managed by the hub. They may also refer to ports belonging to the hub itself. While this type of expressions allows creating complex logic involving many devices, their speed is considerably slower. They are practically limited by all network message exchanges required to pass the information between devices. A hub-level expression is set to a port using device_expression attributes.

Transform Expressions

Transform expressions are used to apply simple transformations to values read from or written to ports. A transform expression may only depend on the port to which it is applied. A transform-read expression will alter the raw value read from the port before supplying it further, while a transform-write expression will alter the value written to the port. A transform-read expression is set to a port using the transform_read attribute.

When setting a transform-write expression, you'll probably want to set its transform-read expression to the inverse formula. For example, if a functionality that is available on port gpio1 is active low (meaning that it's considered active when its value is 0 or false), you'll probably want to have the read value negated before being used by qToggle. The same applies when writing a value to the port. A transform-read expression is set to a port using the transform_write attribute.

Other uses of transform expressions include applying correction offsets or measurement unit transformations.

Syntax Explained

Formal Definition

Following is a simple formal definition of an expression:

expression = literal|$|$port_id|function(expression, ...)

Expressions are strings that may consist of literal values, port references and function calls. Function arguments are themselves expressions.

Expressions are case sensitive; that includes function names and port ids. Any number of white space characters can be present anywhere around literal values, function names or port names.

Data Types

Ports, in qToggle, may have one of the following types: number and boolean. In order to allow using expressions refering to (and applied to) different types of ports, types of values are automatically adjusted inside an expression. Conventions are similar to those of the C language: a boolean (logic) expression that needs to be interpreted as a number will have the values 0 or 1, corresponding to the false and true truth values, respectively. When numeric expressions need to be interpreted as booleans, the value 0 is considered false while any other value is considered true. When performing bitwise operations on floating point numbers, they are automatically rounded to their integer part.

Literal Values

The following literal values are recognized:

  • numbers expressed in base 10, optionally preceded by a - minus sign; the . dot character is used as decimal separator
  • boolean values false and true

Here are a few examples of valid literal values: false, true, 0, 16, 2.0, -3.14.

Port References

An expression may refer to one or more ports. A port reference starts with a $ dollar sign and is followed by the port id. For example, $my_sensor.temperature is a port reference to port my_sensor.temperature. The self port (the port to which expression is assigned), can be referenced by a simple $ dollar sign without id.

Functions

A function may have zero, one or more comma-separated arguments; regardless of the number of arguments, the function name must be followed by a pair of parentheses. Some functions may accept a variable number of arguments.

Check out the list of functions defined by the API specifications. Keep in mind that some of these functions are optional and thus may not be available in certain conditions.

Here are a few examples of function calls: ADD($vport1, 2.5), IF(GT($adc0, 500), 1, -1), MAX($vport1, $vport2, $vport3).

Expression Validation

Devices will do a validation on received expressions as soon as new expressions are attempted to be set via API. Invalid expressions will be rejected.

Besides checking the basic syntactic rules, devices will also check for circular dependencies involving expressions and ports. These could lead to evaluation loops and will therefore be rejected by device. An expression of a port can however depend on the port itself, without creating loops; in this case, the new value will in fact depend on the old value of that port.

A device will also reject transform expressions that depend on other ports than the port itself.

It is important to note that, as long as the port id is syntactically correct, any port id reference is considered valid inside an expression, even if the respective port does not (currently) exist. This allows defining an expression in the absence of certain ports. Expressions, even though valid, are ignored unless all referenced ports exist and yield valid values.

Here's a list of possible validation errors:

  • unknown function
  • invalid number of arguments
  • unbalanced parentheses
  • unexpected end
  • circular dependency
  • external dependency
  • unexpected character
  • too long

See expression validation for more details.

Understanding Expression Evaluation

Evaluation rules for transform expressions are as plain as possible: a transform-read expression will be evaluated each time the port value is read, while a transform-write expression will be evaluated whenever a port value is written to the port.

Rules for evaluating value expressions are on the other hand a bit more complex. In a nutshell, a value expression will be evaluated whenever one of its referenced ports changes its value. Resulted value will be written to the port. There are, however, some particularities:

  • If port owning the expression is disabled, evaluation will be skipped.
  • If any of the directly referenced ports is disabled or does not exist, evaluation will be skipped.
  • If resulted value is invalid, the previous port value will be kept.
  • If expression references its port, that port's value change will not trigger an evaluation more than once in a row.
  • Expressions containing time-related functions are evaluated each second or millisecond, depending on the function.
  • Expressions containing certain functions may follow specific evaluation timings imposed by those functions.

Devices keep track of the reason of value changes for each port. Here are the main reasons for a port value change:

For more details, see expression evaluation.

Common Use Cases

Simple Negation

Following expression will negate (invert) the value of the referenced port. This is particularly useful for boolean ports whose functionality is active low, when used as a transform-read expression:

gpio1.transform_read = "NOT($)"

Flip-flop

In digital electronics, a flip-flop is a circuit with two stable states. The current state is switched whenever the input signal changes from e.g. low to high. In qToggle, this could be implemented using the following expression:

light.expression = "IF($button, NOT($), $)"

Whenever port button becomes true, the value of light is switched from its current value to the opposite.

Limiting Values

If you want to make sure a port only receives values within a certain range, you can limit it using MIN and MAX:

pwm1.expression = "MIN(MAX(50, $adc0), 80)"

The width of the PWM signal on pwm1 will always stay between 50% and 80%.

Filtering Values

Values may often fluctuate when dealing with real hardware inputs. The following expression will smooth the values read from adc0:

adc0.transform_read = "FMAVG($, 5, 250)"

Smoothing is done by applying a moving average filter on input values, using a filter width of 5 and a sampling interval of 250 milliseconds.

The same expression can be applied to digital inputs, effectively debouncing read values.

Blinking A LED

Using the SEQUENCE function, you could make a LED blink:

led1.expression = "SEQUENCE(false, 800, true, 200)"

This sequence will keep the LED off for the majority of a second (800 milliseconds) and will shortly turn it on for 200 milliseconds.

Thermostat

A thermostat is normally a closed loop between a temperature sensor and an actuator that increases or decreases the temperature. At the core of a thermostat, we've got a hysteresis function that ensures the temperature is within a certain range centered in the temperature set point. The following expression helps implementing such a thermostat, keeping temperature at 22±0.2°C:

heater.expression = "HYST($temp_sensor, 21.8, 22.2)"

Working Schedule

Supposing a working schedule of 9 to 17, from Monday till Friday, following expression keeps the doors locked outside working schedule:

door_locked.exp = "NOT(AND(GTE(DOW(), 0), LTE(DOW(), 4), HMSINTERVAL(9, 0, 0, 16, 59, 59)))"