-
Notifications
You must be signed in to change notification settings - Fork 2
Expressions
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))"
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 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 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.
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.
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.
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
andtrue
Here are a few examples of valid literal values: false
, true
, 0
, 16
, 2.0
, -3.14
.
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.
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)
.
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.
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:
- native/hardware (the physical port value has changed)
- an API call (see
PATCH /ports/{id}/value
) - a new sequence value (see
PATCH /ports/{id}/sequence
) - value expression evaluation
For more details, see expression evaluation.
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($)"
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.
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%.
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.
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.
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)"
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)))"