RPN Rules - xoseperez/espurna GitHub Wiki

RPN Rules

Notice: For the full RPNlib documentation, see https://github.com/mcspr/rpnlib#expressions

The "RPN Rules" module is an advanced feature that let's you define pretty flexible custom rules to execute actions based on different inputs. Rules are defined as a series of commands using Reverse Polish Notation, also called "postfix" notation, where operators come after the operands.

This might look somewhat complicated but it actually has major benefits over the usual "infix" notation:

  • Commands (rules) are faster to process
  • Rules are simple to understand
  • It's easier to implement

Table of Contents

Main concepts

Postfix notation

First you should familiarize yourself with RPN calculation. Reverse Polish notation (RPN) is a mathematical notation in which operators follow their operands. It does not need any parentheses as long as each operator has a fixed number of operands.

A simple calculation in infix notation might look like this:

( 4 - 2 ) * 5 + 1 =

The same calculation in RPN (postfix) will look like this:

4 2 - 5 * 1 +

It results in a shorter expression since parenthesis are unnecessary. Also the equals sign is not needed since all results are stored in the stack. From the computer point of view is much simpler to evaluate since it doesn't have to look forward for the operands.

Tokens

Each element in an expression is called "token" (not a standard name). Tokens can be:

  • Numbers, always considered as real numbers (floats)
  • Operators (+, -, ...)
  • Variables (starting with a $ sign)

Stack

As I already mentioned, operands (numbers) are pushed into a stack and later operators pop the number of operands from the stack. Let's take a closer look to a simple RPN expression: 4 2 - 5 * 1 +. The first column shows the next token to process and the third column the status of the stack after processing the token. At the beginning the stack is (usually) empty.

token action stack
4 pushes '4' into the stack 4
2 pushes '2' into the stack 4
2
- pops 2 values from the stack
performs the subtraction
and pushes the result to the stack
2
5 pushes '5' into the stack 2
5
* pops 2 values from the stack
performs the multiplication
and pushes the result to the stack
10
1 pushes '1' into the stack 10
1
+ pops 2 values from the stack
performs the sum
and pushes the result to the stack
11

Operators

Notice: For the full list of operators, see https://github.com/mcspr/rpnlib#operators

Of course you need to know the vocabulary, i.e. the available operators. There are different types of operators:

  • Mathematical (+, -, *, ...)
  • Stack manipulation (dup, swap, ...)
  • Comparison (eq, gt, cmp, ...)
  • Logical (and, or, ...)
  • Time management (now, hour, minute,...)
  • ESPurna related (relay, channel, oneshot_ms, every_ms,...)

Wall clock time and date

Stack Operator Description Notes
- now Pushes the current epoch time into the stack Will stop execution when NTP is not synced
timestamp hour Gets an unix epoch timestamp from the stack and pushes the hour
timestamp minute Gets an unix epoch timestamp from the stack and pushes the minute
timestamp dow Gets an unix epoch timestamp from the stack and pushes the day of week Sunday is 1, Monday is 2, etc.
timestamp day Gets an unix epoch timestamp from the stack and pushes the day of month
timestamp month Gets an unix epoch timestamp from the stack and pushes the month January is 1, February is 2, etc.

Timers

Stack Operator Description
milliseconds oneshot_ms Gets milliseconds value from the stack and starts an 'One Shot' timer with that value
milliseconds every_ms Same as 'oneshot_ms', but restarts the timer automatically

Both require that the operator is reached at least once for the timer to start; we don't parse the expression before hand to figure out the timers, operator itself starts the timer. Until the timer is expired, operator will stop the current rule execution. After the timer is expired, rule will be allowed to continue.

If timer was specified as 'oneshot_ms', to restart the timer it would need to be called again and the timer will be removed after the last RPN rule.
For 'every_ms', timer is restarted automatically.

Both operators are intended to be used with rpnRule#, rpn.test won't be able to trigger things based off of the timer expiration.

Relays

Stack Operator Description
relayID status relay Gets a value and a relay number from the stack and changes that relay to the given status, status can be 0, 1 or 2 (toggle)
relayID status relay_reset Similar to relay, but re-schedules any timers attached to the relay (Flood, ON / OFF delays, Pulse etc.)

Lights

Stack Operator Description
black Turns off all channels (only lights)
update Updates channels
channelID value channel Gets a value and a channel number from the stack and changes that chanel to the given value (from 0 to 255). Does not update the light, you must call "update" at the end.

RFBridge

Every received code is preserved in the internal cache (see RFB.CODES terminal output)

Stack Operator Description
count protocol code rfb_match
protocol code protocol code rfb_sequence Gets code + protocol twice. Checks if specified pairs happen in sequence and continues execution, removing both codes from cache
time count protocol code rfb_match_wait Similar to rfb_match, but waiting for at least 'time' (in ms) via the one-shot runner
protocol code rfb_info Gets code string and protocol number, pushes code's latest timestamp and counter on the stack
protocol code rfb_pop Gets code string and protocol number, removes the specified protocol + code from the internal cache

Cache configuration:

Key Description
rfbRepeatWindow How long to wait until resetting code counter back to 1 (ms, default 2000)
rfbMatchWindow Do not process codes in rfb_match that at are older than this value (ms, default 2000)
rfbStaleDelay How long to wait until removing code from the cache (ms, default 10000)

Protocol argument depends on the RFB_PROVIDER flag. For the stock RFBridge, there is only a single protocol (0u, 2nd argument of the rfb_match). For example, to update $movement value each time we see code "123456":

1u 0u "123456" rfb_match millis &movement =

Or, to toggle the relay when the code is received twice:

2u 0u "567891" rfb_match 2 0 relay

System

Stack Operator Description
mode seconds sleep Turns of the WiFi 1 and enters deep sleep for the specified time (GPIO16 needs to be connected to the RST). Mode is either: 0 (RF_DEFAULT), 1 (RF_CAL), 2 (RF_NO_CAL) or 3 (RF_DISABLED).2
reset Reboots the device
yield Switches out into the system context, stopping rule execution until the re-entry
ms delay Switches out into the system context for the specified time (unsigned milliseconds)
block mem_read Reads nth (see rtcmem.dump output) 32 bit block from the RAM.
value block mem_write Writes the value as 32 bit unsigned integer into the target block.
mem? Pushes current rtcmem status as boolean value. RTC memory gets erased in case of manually using RST pin and after either Hardware or Software WDT reset

1 See https://github.com/esp8266/Arduino/tree/master/libraries/esp8266/examples/LowPowerDemo#low-power-demo for more information about the WiFi operation.
2 JustWifi boot sequence will interfere with the RF_DISABLED by always enabling the WiFi connection.

WiFi

Stack Operator Description
stations Pushes number of connected stations when in softAP mode. When not in softAP mode, stops execution of the rule
disconnect Disconnects from the active AP and restarts the connection loop. Does not stop execution when not connected.
rssi Pushes signal strength of Wi-Fi network as a signed integer (in dBm, ref. esp8266/Arduino documentation)

Debugging

Stack Operator Description
showstack Shows current stack contents in the global debug log
string dbgmsg Prints the specified string to the global debug log
millis Pushes number of milliseconds passed since the boot (unsigned, value overflows at ~50 days)

Execution control

Stack Operator Description
end Ends rule execution when top stack value resolves to boolean false
WARNING! Before 1.15.0, operator used to end rule execution when stack value resolved to boolean true
condition for_true for_false ifn Will choose between 1st (top) and 2nd stack elements, depending if 3rd stack element resolves to true (choose 2nd) or false (choose 1st)

Note that ifn, unlike generic programming language's if, does not allow different 'branches' and RPNlib does not allow operator references (yet) to be stored on the stack. Any token encountered while parsing the expression and matching with the operator name will immediately call the associated function.

Variables

Variables are stored in the module context so they can be used from your expressions. Variables are fed from different sources:

  • Relay status ($relay0, $relay1,...)
  • Lights channel values ($channel0, $channel1,...)
  • Sensors ($temperature0, $humidity0,...)
  • MQTT (custom names)

By default they are persistent (meaning that once defined they are available for any rule execution in the future) but they do not persist across reboots. This behavior is called "sticky variables" and can be changed from the UI. Non sticky variables get deleted after the next rule execution. This can be useful to avoid dead locks when used with other features like relay pulses.

It is possible to create or overwrite the existing variable by changing '$' to '&' before the variable's name and using a special operator =. For example:

11 &variable = 

If 'variable' exists, it's value will be replaced with '11'. If 'variable' does not exist, it will be automatically created and assigned the value.

When using '$variable' syntax, expression will not continue if 'variable' does not exist.

MQTT variables

MQTT variables are a special type of variable that gets its content from MQTT topics. You can define an MQTT and a variable name and every time the topic receives a message the variables will get updated and the rules will be executed.

At this time, the only type available for MQTT variables is floating point.

Rule execution

Rules get executed X milliseconds after the last execution trigger. The execution delay is 100ms by default but it can be changed from the web UI. This allows for a buffer time so all variables coming from sensors get updated before rules are executed.

All variables changes trigger the rule execution after the delay time. So every time a relay is toggled, every time a sensor reports values or every time one of the MQTT topic gets updated the rules will be executed.

Examples

Toggle a heater depending on a temperature sensor

Imagine you have a device with a relay controlling a heater and a temperature sensor. You can implement a simple thermostat with hysteresis using the input variables of the sensor ($temperature0) and the relay ($relay0):

$temperature0 18 24 cmp3 1 + [ 1 $relay0 0 ] index 0 relay

Let's split the expression in parts:

sub-expression description output
$temperature0 18 24 cmp3 Compares the temperature to the 18-24 range, it will output -1 if temperature is below 18, +1 if it is above 24 and 0 in the middle -1 to 1
1 + We need to shift the cmp3 result to use the index operator 0 to 2
[ 1 $relay0 0 ] index We define an index of 3 different values (number of items between [ and ]) that will be: 1, the current status of the relay (0 or 1) and 0. The sub-expression will return one of them depending on the value in the stack preceding the [ 0 or 1
0 relay It will set the relay number 0 status according to the value on the stack (empty)

This expression will ensure the relay in ON if the temperature is below 18C and OFF if it's above 24C. If the reported temperature is in between 18 and 24 the relay status will not change.

Open light at night if there is presence

In this case image we have a simple WiFi relay device and a PIR in the same room that reports presence via MQTT to the /livingroom/motion topic. We first define an MQTT topic for it using motion as the variable name. Then we can trigger the light if there is motion between 22h and 8h in the morning.

now hour 8 23 cmp3 abs $motion and 0 relay

Again, let's analyze it step by step

sub-expression description output
now hour Will return the current hour 0 to 23
8 23 cmp3 abs Compare the hour in the stack to the 8-23 range and get the absolute value, will output 1 if it's below 8 or above 23, 0 otherwise 0 or 1
$motion and We do an AND with the motion value, so we will now have a 1 if it's nighttime AND there is motion 0 or 1
0 relay We use the result value to turn on or off the relay #0 (empty)

Since our device will only have a relay, this rule will be executed every minute (triggered by the NTP module) and when there is a new value in the motion topic. Alternatively, if you disable the "stickiness" of the variables, the expression will fail except when the $motion is defined which will only happen after we receive a message to the motion topic.

Keep in mind that if the result changed the relay status, the relay change will trigger the rule execution again! Try to avoid loops in the rules like, for instance: 1 $relay0 - 0 relay. This simple expression will turn the relay ON and OFF and ON again and OFF again forever!!

Emulate the schedule

The schedule is different to the previous examples since it will only perform an action at a given time, not every time there is a time or status update like the RPN Rules module. But we can easily emulate this behaviour by using $tick1m variable that is set every minute:

$tick1m dup minute 0 eq end hour 8 eq end null &tick1m = 1 0 relay 

The $tick1m variable contains the timestamp, while the 2nd expression using &tick1m resets it to null, removing it from variables list after the expression is done. The end operator takes and argument from the stack and ends the execution if the argument resolves to false. Hence:

sub-expression description output
$tick1m dup Check if $tick1m is set and duplicate it's value on the stack
minute 0 eq end Will end the execution if the current minute is not 0 (empty)
hour 8 eq end Will end the execution if the current hour is not 8 (empty)
null &tick1m = Set tick1m variable's value to null, after which the variable will be disallowed to be used by the $tick1m expression
1 0 relay Turn relay 0 to ON, this will only happen at 8:00!

It is also possible to schedule execution to happen every hour by checking for $tick1h variable.

Emulate periodic tasks

RPN allows to start a periodic or one-shot timers (aka RPN runners), that will trigger rule execution when expired.

For example, execute the right part of the expression after timer is done:

5000u every_ms 2 0 relay

Execute the right part of the expression when variable is set to true, but wait 5 seconds first and then set variable to false:

$test end 5000u oneshot_ms false &test = "topic" "message" mqtt_send

Note that 5000u every_ms defined in multiple rules will create a single timer.

Light color following power consumption

Some devices, like the Smartlife Mini Smart Socket, have an LED light (sometime RGB) and a power consumption monitor. Wouldn't it be cool that the light would turn more and more red as the power consumption rises?

black $power0 0 1000 0 255 map 0 channel update

This expression maps a power value between 0 and 1000W to a number between 0 and 255 and then it feeds it to channel 0 (usually red). Initially, it sets all channels to 0 (black) and at the end forces the light driver to update the color. Nice.

Individually synchronize relays

ESPurna has a relay synchronization feature that offers different ways to synchronize relays when there is more than one available, but all of these options perform actions of all the relays, turning them ON or OFF to match the required pattern. What if you have 4 different relays and you want to synchronize relays 0 and 1 together but leave 2 and 3 untied? Easy.

$relay0 1 relay

What if you want them to have opposite value?

1 $relay0 - 1 relay

Now you can control relays 0 and 1 by just changing relay 0. If you want to be able to perform an action on either 0 or 1 and toggle the other as well, you can create one rule for each but remember to disable "Sticky variables" to avoid getting into a loop:

1 $relay0 - 1 relay
1 $relay1 - 0 relay

Terminal commands

The module exposes different terminal commands to test and evaluate expressions and variables.

  • RPN.OPS will output the list of available operators and the number of required arguments for each of them
  • RPN.TEST "<expression>" will execute the expression and output the stack at the end, useful to do partial tests of sub-expressions
  • RPN.VARS will output the current defined variables and their values
  • RPN.RUNNERS will show the currently running timers
⚠️ **GitHub.com Fallback** ⚠️