EXCH Language

The EXCH language implements a basic set of operations to enable robust data exchange capabilities between different systems (physical or cloud) with a small set of commands.

EXCH is small with simple rules, and accessible by non-programmers.

Statements structure

  • Each statement must stay on a single line.
  • Multiple spaces (outside of quoted strings) are allowed, and treated as a single space.
  • Empty lines are neglected.
  • Keywords (statements, functions, operators) are case insensitive (e.g., writeField, writefield, WRITEFIELD, all work).
  • Comments start with an # symbol. Comment lines are not executed.

Data types

The following data types are implemented:

  • number: both integer and float are numbers
  • string: both single-quoted and double-quoted are accepted
  • boolean: true or false, case insensitive
  • null: representing the absence of value
  • mqtt topic name: mqtt topic name in IOhub format (protocol/measure)

Program variables

Program variables are automatically created upon usage; no declaration is needed.

The variable is accessed (both as a value and as a reference) using ${<variableName>}.

The variable name is case sensitive.

Variables can be named using letters, numbers and _. The first character must be a letter.

The scope of each variable is at the program level.

Variables assignment

The value of a variable can be altered at subsequent points in execution using further assignment operations.

The assignment statement in EXCH involves using the = operator to set the value of a variable, like in the example below.

${a} = 123

A special case of assignment is done through the initVar statement (see the dedicated section here).

You should use the initVar statement to guarantee a correct variable initialization.

Global variables

Global variables are shared among all programs.

A global variable is named liked a Program variable prefixed by a @.
They follow the same rules as the program variables.

Using the index variable, different programs are creating different variables; each program is accessing its private copy.
Using the @index variable, different programs are creating/accessing the same variable, shared among the different programs.

${index} = 55
${@index} = 23

Permanent variables

Permanent variables retain their values across restart. Their value is stored inside on the file system visible by the ezvpn-fld-exchanger container.

You can configure a variable as permanent adding a ! (exclamation mark symbol) at the end of the variable name.

For example:

  • the variable ${counter!} is a permanent program variable.
  • the variable ${@counter!} is a permanent global variable.

Permanent variables were added in version 1.1.6. If you are going to use the permanent variables in an exchanger container installed before 1.1.6, you must manually add a local mount point to /data to the ezvpn-fld-exchanger configuration.

Starting from 1.1.6, the /data mount is automatically added, no action is required.

All the permanent variables are automatically persisted to disk every 5 seconds.

Reserved variables

Some variables are always present in EXCH, having a special meaning.

  • _p represents the protocol of the incoming payload.
  • _m represents the measure name of the incoming payload.
  • _v represents the value of the incoming payload.
  • _t represents the timestamp of the incoming payload.

They can be accessed using the normal variable syntax, ${_p}, ${_m}, ${_v}, ${_t}

When accessing protocol/measure you are always getting the original value received by the mqtt broker; _v represents the local program copy of the initial value that could have been modified.

See the example below.

Example

A sample program, triggered by a value change on modbus/temperature

logValue modbus/temperature
logValue ${_v}
# same value is printed

${_v} = ${_v} + 1
logValue modbus/temperature
logValue ${_v}
# different values are printed

Payload matching

Depending on how the program has been triggered, the values of _v, _p, _m, _t are filled in different modes.

Program triggered by a from type

The expected payload is a JSON string with the following structure:
{ "value": "<measure value>", "ts": <timestamp> },
arriving on the topic
fld/<protocol>/r/<name>.

  • _v is the value field of the incoming payload.
  • _p is the protocol field of the from definition.
  • _m is the name field of the from definition.
  • _t is the ts field of the incoming payload.

Program triggered by a topic type

The expected payload can have any value.

  • _v is the raw incoming payload.
  • _p is the part of the incoming topic before the first matched /.
    • if the topic is a/b/c/d, _p is a
    • if the topic is a/b, _p is a
    • if the topic is a, _p is a
  • _m is the part of the incoming topic after the first matched /.
    • if the topic is a/b/c/d, _m is b/c/d
    • if the topic is a/b, _m is b
    • if the topic is a, _m is an empty string
  • _t is a timestamp generated upon payload receive.

Values / Expressions

Whenever value (or expression) is used below, we mean a read-only value. A value can be:

  • a number literal: 12, -34.7
  • a string literal: "hello world", 'hello world again'
  • a boolean literal: true, false, True, FALSE
  • the null literal
  • a variable reference: ${FOOBAR}, ${anotherVar}, ${@aGlobalVar}
  • a value from an mqtt field measure: modbus/temperature, opcua/plc1.humidity
  • the result of operations on values:
    • 1 + ${aVar}
    • 24 % @{temp} - step7/pressure
    • "Reading from measurement " + modbus/temperature + " and the boolean value is " + true
  • the value inside parenthesis: (2 + 5 * 8) (the value is 42)
  • the result of a function call: call(min, 3, 45) (the value is 3)

Whenever numeric value (or numeric expression) is used, we mean one of:

  • a number literal: 12, -34.7
  • a variable reference: ${FOOBAR}, ${anotherVar}, ${@aGlobalVar}

If the variable contains a non-number value (a string, a boolean), an automatic cast is performed. If the conversion fails, zero is returned, and a warning is logged.

  • true is converted in the number 1.
  • false is converted in the number 0.

When reading variables which have not been assigned a value or MQTT field measures not yet received, the program behavior is defined by the strict statement as described later.

Math operators

The following operators operate on numbers. All the operations are floating point.

  • +: sum (1 + 2 gives 3). See note below.
  • -: subtract (7 - 2.5 gives 4.5)
  • *: multiply (5 * 5.6 gives 28)
  • /: divide (28 / 4 gives 5.6)
  • %: remainder (10 % 3 gives 1)
  • ^: exp (7 ^ 3 gives 343)

If at least one of the operands of the + operator is not a number, a string concatenation is executed, e.g.

  • 1 + 2 gives 3 (the number 3)
  • "1" + 2 gives "12" (the string "12")

Operator precedence

From the highest to the lowest, this below is the operators precedence:

  • %, ^
  • *, /
  • +, -

The operator precedence can be altered using parenthesis grouping:

  • 1 + 2 * 3 gives 7
  • (1 + 2) * 3 gives 9

Operator associativity

When the operators have the same precedence, the associativity is left to right:

  • 1 + 2 - 3 is like (1 + 2) - 3
  • 1 / 2 * 3 / 4 is like ((1 / 2) * 3) / 4

Conditional execution

If Then Else

A conditional execution flow (if/elif/else) is available.

Syntax

if (<condition>) then
    <instructions>
endif

An optional else is available. An else statement contains the block of code that executes if the conditional expression in the if statement resolves to false:

if (<condition>) then
    <instructions>
else
    <instructions>
endif

If you need to match one out of multiple conditions, instead of nesting ifs, you can use the elif clause.

Unlike else, for which there can be at most one statement, there can be an arbitrary number of elif statements following an if.

if (<condition>) then
    <instructions>
elif (<condition>) then
    <instructions>
elif (<condition>) then
    <instructions>
else
    <instructions>
endif

instructions is a set of one or more statements, as described above.

Conditions

Condition are expressions returning boolean values.

Listed below are the existing conditional operators:

conditional operators

  • <: less then (e.g. ${a} < 23)
  • <=: less or equal (e.g. ${a} <= 23)
  • >: greater then (e.g. ${a} > 23)
  • >=: greater or equal (e.g. ${a} >= 23)
  • ==: equal to (e.g. ${a} == 23)
  • !=: not equal to (e.g. ${a} != 23)

Examples

if (${a} < 23) then
    publishValue "opcua/plc1.status" 1
    publishValue "slack" "Heater temperature is too low"
endif

== and != can be used to test truthy/falsy of variables. When compared to true/false, values are cast to boolean before comparison.

if (${a} == true) then
    publishValue "opcua/plc1.status" 1
    publishValue "slack" "Heater temperature is too low"
endif

boolean operators

  • not
  • and
  • or

not

You can conditionally execute a block of instructions evaluating a value. Before evaluation, the value is cast to a boolean, using the toBoolean operator.

The boolean are case insensitive. AND, and, And are all valid.

The not keyword negates the boolean evaluation.

Examples

# checked as (not (${a} > 3)) or 5
if (not ${a} > 3 or 5) then
    publishValue "opcua/plc1.status" 1
    publishValue "slack" "Heater temperature is too low"
endif

and

The and operator implements a short circuit boolean AND.

Examples

if (${_v} > 3 and ${b} < 8) then
    ${a} = 3
endif

or

The or operator implements a short circuit boolean OR.

Examples

if (${_v} > 3 or ${b} < 8) then
    ${a} = 3
endif

You can combine boolean operators either using round brackets or by their precedence.

if (NOT ((${a} > ${_v} OR ${b} > modbus/ss) AND 1 > 2)) THEN
    ${a} = 3
endif

Conditional operator precedence

From the highest to the lowest, this below is the operators precedence:

  • <, <=, >, >=, ==, !=
  • not
  • and
  • or

The operator precedence can be altered using parenthesis grouping:

${a} or ${b} and ${c} == ${a} or (${b} and ${c})
${a} or ${b} and ${c} != (${a} or (${b}) and ${c}

Operator associativity

When the operators have the same precedence, the associativity is left to right:

${a} or ${b} or ${c} is like (${a} or ${b}) or ${c}

Full operator precedence summary

When conditions and values are mixed, these below are the precedence rules.

  • <, <=, >, >=, ==, !=
  • not
  • and
  • or
  • %, ^
  • *, /
  • +, -

${a} < 3 + 5 * 26^3 equals to (${a} < 3) + (5 * (26^3))

When the operators have the same precedence, the associativity is left to right:
26^3^4^5 is ((26^3)^4)^5)

Program initialization

An optional initialization block can be used in the program. If used, it must be written before any other instruction.

Syntax

init
    <instructions>
endinit

<main program instructions>

The initialization block is executed once, it if runs without errors.
The rest of the program is executed only after the init block has completed its execution.
If the init block returns with an error (see fail and check), it is not considered as executed; in the next iteration, it will be re-run.

Examples

This program will keep running the init block while the system is not running (opcua/plc1.running != true). When the opcua/plc1.running has value true, the main program is executed.

init
    if ( opcua/plc1.running != true ) then
        fail "waiting for system ready"
    endif
endinit

# main program

Variables initialization

Even if you can execute normal assignments to initialize variables, the best option is to use the initVar statement.

e.g.

normal assignment

init
    ${a} = 0
endinit

using initVar

init
    initVar ${a} 0
endinit

The initVar statement is executed only if the variable has never been initialized. If you run it on the same variable again, it has no effect.

e.g.

# working
initVar ${a} 0

# no effect
initVar ${a} 1

# no effect
initVar ${a} 2

# here the value of ${a} is still 0

initVar becomes necessary if you want to initialize a permanent variable. If you want to inizialize a permanent variable only the very first time it gets created, even after reboots, you can use initVar, who takes care of the permanent value.

# ==== correct execution

initVar ${@counter!} 0
# we initialized a permanent variable to 0
# at the next reboot, the variable will not be set to 0 again

# ==== wrong execution

${@counter!} = 0
# we are setting the permanent value to 0 at each reboot

Functions invocation

A function is an EXCH predefined set of instructions returning a value, from some parameters. EXCH functions are idempotent.

call: invoke a function

A function is invoked using the call keyword.

Syntax

call(<function name>, <parameter 1>, <parameter 1>, ..., <parameter n>)

e.g. call(min, 2.4, 5) invokes the min function on the parameters 2.4 and 5, giving 2.4.

Math functions

  • min: returns the smaller of two numbers, call(min, 2.4, 5) gives 2.4.
  • max: returns the bigger of two numbers, call(min, 2.4, 5) gives 5.
  • minmax: returns the third parameter if within a range defined by the first two parameters, otherwise it returns the range limit, e.g.
    • call(minmax, 1, 5, 3) gives 3
    • call(minmax, 1, 5, 30) gives 5
    • call(minmax, 1, 5, 0) gives 1
    • the range parameters do not need to be in ascending order; the two calls below are identical.
      • call(minmax, 1, 5, 300)
      • call(minmax, 5, 1, 300).
  • round with decimals: round keeping some decimals, e.g. call(round, 13.87933, 2) gives 13.88.
  • round without decimals: round to the nearest integer, e.g. call(round, 2.4) gives 2
  • scale: scale a value, e.g.
${a} = 25000
# scale a from 0:65535 to -100:100
logValue call(scale, 0, 65535, -100, 100, ${a})
# result: logValue: -23.7060546875 (number)

${a} = 250
# scale a from 0:1000 to -100:100
logValue call(scale, 0, 1000, -100, 100, ${a})
# result: logValue: -50 (number)

the range parameters do not need to be in ascending order; the two calls below are identical.

  • call(scale, 0, 1000, -100, 100, ${a})
  • call(scale, 1000, 0, 100, -100, ${a}).

Conversion functions

  • toNumber: returns the result of conversion to number. If the conversion fails, zero is returned, and a warning is logged. e.g. call(toNumber, "123") gives 123 as number.
  • toString: returns the result of conversion to string. e.g. call(toString, 123) gives "123" as a string.
  • toBoolean: returns the result of conversion to a boolean, applying the rules below:
    • if a number, 0 becomes false, otherwise true, e.g. call(toBoolean, 123) gives true
    • if a string, the empty string, strings containing only spaces, and the string "0" become false, otherwise true, e.g. call(toBoolean, "") gives false

String functions

  • concat: returns the result of string concatenation. The parameter are converted into string before the concatenation.
logValue call(concat, "Hi ", ${a})
# same as: logValue "Hi " + ${a}

concat always converts the operand to strings before concatenating. It behaves like the + operator unless the operands are both numbers.

  • call(concat, 1, 1) gives 11
  • 1 + 1 gives 2.

Engineering unit functions

Temperature

  • f_to_c: Convert from Fahrenheit to Celsius
  • c_to_f: Convert from Celsius to Fahrenheit
logValue "60 Fahrenheit = " + call(f_to_c, 60) + " Celsius"
logValue "400 Celsius = " + call(c_to_f, 400) + " Fahrenheit"

Pressure

  • psi_to_bar: Convert from psi to bar
  • bar_to_psi: Convert from bar to psi
logValue "400 psi = " + call(psi_to_bar, 400) + " bar"
logValue "60 bar = " + call(bar_to_psi, 60) + " psi"

Weight

  • lb_to_kg: Convert from pounds to kilograms
  • kg_to_lb: Convert from kilograms to pounds
logValue "400 lbs = " + call(lb_to_kg, 400) + " kgs"
logValue "60 kgs = " + call(kg_to_lb, 60) + " lbs"
  • oz_to_gr: Convert from ounces to grams
  • gr_to_oz: Convert from grams to ounces
logValue "60 ounces = " + call(oz_to_gr, 60) + " grams"
logValue "400 grams = " + call(gr_to_oz, 400) + " ounces"

Length

  • ft_to_mt: Convert from feet to meters
  • mt_to_ft: Convert from meters to feet
logValue "60 feet = " + call(ft_to_mt, 60) + " meters"
logValue "400 meters = " + call(mt_to_ft, 400) + " feet"
  • in_to_mm: Convert from inches to millimeters
  • mm_to_in: Convert from millimeters to inches
logValue "60 inches = " + call(in_to_mm, 60) + " millimeters"
logValue "400 millimeters = " + call(mm_to_in, 400) + " inches"

Volume

  • gal_to_lit: Convert from US gallons (liquid) to liters
  • lit_to_gal: Convert from liters to US gallons (liquid)
logValue "60 gallons = " + call(gal_to_lit, 60) + " liters"
logValue "400 liters = " + call(lit_to_gal, 400) + " gallons"

MQTT functions

Create Payload

  • create_payload: Create a payload in the standard format, as if received from a field container.
logJSON call(create_payload, 18)

# logs
# logJSON: {
#   "value": 18,
#   "ts": 1616380945079
# }

JSON functions

The JSON function allow you to manage in an easy way JSON payloads. You can get/set/add/delete field values, array elements.

The JSON functions allow to interact with external systems exchanging JSON messages, dynamically composing and interpreting commands/statuses.

All the JSON function require the first two parameters as:

  • the JSON payload to query/update, e.g.
    • 123: a number
    • true: a boolean
    • '[1, 2, 3]': a string representing an array
    • '{ "name": "mark", "age": 26 }': a string representing a JSON object
    • 'Low Level': a string representing a string
  • the path of the property/element to update, see next paragraph

Path selector

It is possible to access any property/array element using a selector. See some examples below.

example JSON

{
    "firstName": "mark",
    "address": [
        {
            "street": "101 West Broadway",
            "city": "San Diego",
            "state": "CA",
            "zip": 92101
        },
        {
            "street": "4th Street",
            "city": "New York",
            "state": "NY",
            "zip": 10003
        }
    ]
}

example of selectors

  • "firstName" => The string "Mark"
  • "firstName.address" => The array [ { "street": "101 West Broadway", "city": "San Diego", "state": "CA", "zip": 92101 }, { "street": "4th Street", "city": "New York", "state": "NY", "zip": 10003 } ]
  • "firstName.address[1]" => The second element '{ "street": "4th Street", "city": "New York", "state": "NY" }
  • "firstName.address[0].zip" => The number 92101

example JSON

[
    {
        "name": "Mark",
        "age": 24,
        "married": false
    },
    {
        "name": "John",
        "age": 29,
        "married": true
    }
]

example of selectors

  • "[1]" => The object { "name": "John", "age": 29, "married": true }
  • "[0].married" => The boolean false
  • "[1].age" => The number 29

Raw Path advanced selector

The path selector is an easy way to express jsonpath queries, returning the first value matching the condition.

If the path is written with a leading $, the path is evaluated as raw jsonpat, using the full jsonpath syntax syntax.

The example below shows a sophisticated raw path selector. Considering the variable ${json} having the following value:

{
  "firstName": "John",
  "lastName" : "doe",
  "age"      : 26,
  "address"  : {
    "streetAddress": "naist street",
    "city"         : "Nara",
    "postalCode"   : "630-0192"
  },
  "phoneNumbers": [
    {
      "type"  : "iPhone",
      "number": "0123-4567-8888"
    },
    {
      "type"  : "home",
      "number": "0123-4567-8910"
    }
  ]
}

the EXCH program below

logJSON call(json_get, ${obj}, '$.phoneNumbers[0:].type')

gives the following result

logJSON: [
  "iPhone",
  "home"
]

json_new_obj

Create an empty JSON object.

  • json_new_obj
logValue call(json_new_obj)
# ==> logValue: {} (string)

json_new_arr

Create an empty JSON array.

  • json_new_arr
logValue call(json_new_arr)
# ==> logValue: [] (string)

json_get

Returns a property/element value, or the default value if missing.

  • json_get, <json>, <path>[, <default value>]
${json} = '{ "name": "mark", "age": 29, "hobbies": ["golf", "tennis"] }'

logValue call(json_get, ${json}, 'age')
# logValue: 29 (string)

logValue call(json_get, ${json}, 'hobbies')
# ==> logValue: ["golf","tennis"] (string)

logValue call(json_get, ${json}, 'hobbies[1]')
# ==> logValue: tennis (string)

If the property is missing, json_get produces an error, stopping the program execution. To avoid the error, it is possibile to pass an additional parameter containing the value to return in case of missing property/element.

${json} = '{ "name": "mark", "age": 29, "hobbies": ["golf", "tennis"] }'

logValue call(json_get, ${json}, 'work', "unemployed")
# ==> logValue: unemployed (string)

logValue call(json_get, ${json}, 'work')
# ==> 'work' has an empty value, no default value provided
# program execution stops

json_set

Set a property/element.

  • json_set, <json>, <path>, <new value>
${json} = '{ "name": "mark" }'

# add a property
${json} = call(json_set, ${json}, 'age', 29)
logValue ${json}
# ==> logValue: {"name":"mark","age":29} (string)

# update an existing property
${json} = call(json_set, ${json}, 'age', 30)
logValue ${json}
# ==> logValue: {"name":"mark","age":30} (string)

json_del

Remove a property/element.

  • json_del, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis", "surf"] }'

# remove a property
${json} = call(json_del, ${json}, 'name')
logValue ${json}
# ==> logValue: {"hobbies":["golf","tennis","surf"]} (string)

# remove an array element
${json} = call(json_del, ${json}, 'hobbies[1]')
logValue ${json}
# ==> logValue: {"hobbies":["golf",null,"surf"]} (string)

json_push

Add a value to the end of an array

  • json_push, <json>, <path>, <value to add>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_push, ${json}, 'hobbies', "surf")
# ==> logValue: {"name":"mark","hobbies":["golf","tennis","surf"]} (string)

json_unshift

Add a value at the beginning of an array

  • json_unshift, <json>, <path>, <value to add>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_unshift, ${json}, 'hobbies', "surf")
# ==> logValue: {"name":"mark","hobbies":["surf","golf","tennis"]} (string)

json_shift

Remove the first array element.

  • json_shift, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_shift, ${json}, 'hobbies')
# ==> logValue: {"name":"mark","hobbies":["tennis"]} (string)

json_pop

Remove the last array element.

  • json_pop, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_pop, ${json}, 'hobbies')
# ==> logValue: {"name":"mark","hobbies":["golf"]} (string)

json_exists

Check if a path exists in a json object; returns a boolean.

  • json_exists, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_exists, ${json}, 'hobbies[0]')
# ==> logValue: true (boolean)

json_is_obj

Check if a path is a json object; returns a boolean.

  • json_is_obj, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_is_obj, ${json}, 'hobbies[0]')
# ==> logValue: false (boolean)

logValue call(json_is_obj, ${json}, '')
# ==> logValue: true (boolean)

json_is_arr

Check if a path is a json array; returns a boolean.

  • json_is_arr, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_is_arr, ${json}, 'hobbies[0]')
# ==> logValue: false (boolean)

logValue call(json_is_arr, ${json}, 'hobbies')
# ==> logValue: true (boolean)

json_arr_len

Returns the array length. It fails is the path does not contain an array.

  • json_arr_len, <json>, <path>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_arr_len, ${json}, 'hobbies')
# ==> logValue: 2 (number)

logValue call(json_arr_len, ${json}, 'name')
# ==> path 'name' does not contain an array

json_find_index

Returns the index in a json array of the first element whose value is equal to <value>. It fails is the path does not contain an array.

If the element is not found, it returns -1.

  • json_find_index, <json>, <path>, <value>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_find_index, ${json}, 'hobbies', "tennis")
# ==> logValue: 1 (number)

logValue call(json_find_index, ${json}, 'hobbies', "baseball")
# ==> logValue: -1 (number)

logValue call(json_find_index, ${json}, 'name', 'some value')
# ==> path 'name' does not contain an array

json_del_index

Returns the array after removing of the element at position <index>. It fails is the path does not contain an array.

  • json_del_index, <json>, <path>, <index>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis"] }'

logValue call(json_del_index, ${json}, 'hobbies', 1)
# ==> logValue: {"name":"mark","hobbies":["golf"]} (string)

logValue call(json_del_index, ${json}, 'hobbies', 100)
# ==> logValue: {"name":"mark","hobbies":["golf","tennis"]} (string)

logValue call(json_del_index, ${json}, 'name', 50)
# ==> path 'name' does not contain an array

json_del_arr

Returns the array after removing all the elements whose value is equal to <value>. It fails is the path does not contain an array.

  • json_del_arr, <json>, <path>, <value>
${json} = '{ "name": "mark", "hobbies": ["golf", "tennis", "tennis", "baseball", "tennis"] }'

logValue call(json_del_arr, ${json}, 'hobbies', "tennis")
# ==> logValue: {"name":"mark","hobbies":["golf","baseball"]} (string)

logValue call(json_del_arr, ${json}, 'hobbies', "basket")
# ==> logValue: {"name":"mark","hobbies":["golf","tennis","tennis","baseball","tennis"]} (string)

logValue call(json_del_arr, ${json}, 'name', 'some value')
# ==> path 'name' does not contain an array

Manipulating arrays

Whenever the JSON payload is an array and a path is needed, just pass '', (an empty string)

${myarray} = call(json_new_arr)
${myarray} = call(json_push, ${myarray}, '', 123)
${myarray} = call(json_push, ${myarray}, '', true)
${myarray} = call(json_push, ${myarray}, '', "456")
${myarray} = call(json_unshift, ${myarray}, '', null)
${myarray} = call(json_unshift, ${myarray}, '', 'initial element')
logJSON ${myarray}
# ==> logJSON: [123, true, "456" ]

the printed result is

logJSON: [
  "initial element",
  null,
  123,
  true,
  "456"
]

Nested JSON calls

Every JSON function returning a JSON object can be nested, to create objects/arrays in a single call

logJSON call(json_set, call(json_set, call(json_new_obj), 'field1', 123), 'field2', 'some value')
logJSON call(json_push, call(json_push, call(json_new_arr), '', 123), '', 'some value')

prints

logJSON: {
  "field1": 123,
  "field2": "some value"
}
logJSON: [
  123,
  "some value"
]

Date functions

Functions to get the current date. The date is returned in the UTC time zone.

date_time

Returns the milliseconds elapsed from Jan 1 1970 UTC.

  • date_time
logValue call(date_time)
# ==> logValue: 1616787975666 (number)

date_day

Returns the day of month.

  • date_day
logValue call(date_day)
# ==> logValue: 26 (number)

date_month

Returns the month (1 to 12).

  • date_month
logValue call(date_month)
# ==> logValue: 3 (number)

date_year

Returns the year.

  • date_year
logValue call(date_year)
# ==> logValue: 2021 (number)

Environment variables functions

You can access environment variables, defined during the container creation, as long as their name begins with EXCH_.

Any other variable, whose name does not begin with EXCH_, is not accessible inside the EXCH program.

Using environment variables, you can create parametrized programs, by cloning the same template, providing different values (customer names, serial numbers, credentials, ...).

env_var

Returns the value of the environment variable, defined during the container creation.

  • env_var, EXCH_<a name>

If an environment variable with name EXCH_Customer has been defined, with value Acme Inc.

logValue call(env_var, "EXCH_Customer")
# ==> logValue: "Acme Inc." (string)

In case the variable is not defined

logValue call(env_var, "EXCH_Customer")
# ==> logValue: null (null)

Encoding functions

Functions to encode/decode string values.

encode_base64

Encode to base64.

  • encode_base64
logValue call(encode_base64, "123456")
# ==> logValue: MTIzNDU2 (string)

decode_base64

Decode from base64.

  • decode_base64
logValue call(decode_base64, "MTIzNDU2")
# ==> logValue: 123456 (string)

Nested functions

Functions are values and, as in value expressions, they can be nested.

call(f_to_c, call(c_to_f, 25)) gives 25

call(min, call(c_to_f, 25), call(c_to_f, 30)) gives 86

MQTT Topics

MQTT topics can be used as read-only variables. Each incoming change in value in each field topic (modbus, opcua, etc.) is stored in an internal cache.

If a statement references an MQTT topic having no value (because not existing or is not yet initialized), the behavior depends on the strict mode (see below).

MQTT Topics can be used as normal values in every statement (assignments, conditionals, boolean operators, ...).

MQTT Statements

writeField: Write a value to a field container

Syntax

writeField [<remote destination> | LOCAL | GLOBAL] <protocol> <measure> <value> [<optional delay>]

The value is published to the topic fld/<protocol>/w/<measure> of the default mqtt broker. If a field container listening on protocol <protocol> is running, the value is sent to the field.

Examples

writeField "modbus" "setpoint" 123

${_v} = ${_v} + 10 
writeField "opcua" "plc1.setpoint" ${_v}

writeField "ethernetip" "setpoint[12]" "ON"

If a delay is defined, the message is posted to mqtt after delay milliseconds. The delayed request is not blocking, the program execution is not interrupted. delay, if defined, must be > 0.

If LOCAL or GLOBAL is used, the value is forcibly published on the local/global mqtt broker. If the requested broker is not configured, an error is thrown and the program is stopped.

A remote destination can be optionally defined. Check here to understand about remote destinations.

publishValue: Send a value to an MQTT topic

Syntax

publishValue [<remote destination> | LOCAL | GLOBAL] <mqtt topic> <value> [<optional delay>]

The value is published on the default mqtt broker, on the topic <mqtt topic>. <mqtt topic> must be a quoted string.

Examples

publishValue "slack" "alert on temperature"
publishValue "mysql/write/table1/measure1" ${counter}

If a delay is defined, the message is posted to mqtt after delay milliseconds. The delayed request is not blocking, the program execution is not interrupted. delay, if defined, must be > 0.

If LOCAL or GLOBAL is used, the value is forcibly published on the local/global mqtt broker. If the requested broker is not configured, an error is thrown and the program is stopped.

A remote destination can be optionally defined. Check here to understand about remote destinations.

publishPayload: Send the incoming payload to an MQTT topic

Syntax

publishPayload [<remote destination> | LOCAL | GLOBAL] <mqtt topic> [<optional delay>]

The incoming payload is published on the default mqtt broker, on the topic <mqtt topic>. <mqtt topic> must be a quoted string.

If you modified _v, _t, or some metadata, those modifications will be included in the payload sent.

Examples

${_v} = 123
setMetadata "event" "highTemp"
publishPayload "slack/mychannel"

clearMetadata
publishPayload "mycontainer/mychannel"

If a delay is defined, the message is posted to mqtt after delay milliseconds. The delayed request is not blocking, the program execution is not interrupted. delay, if defined, must be > 0.

If LOCAL or GLOBAL is used, the value is forcibly published on the local/global mqtt broker. If the requested broker is not configured, an error is thrown and the program is stopped.

A remote destination can be optionally defined. Check here to understand about remote destinations.

Remote destinations

If your IOhubTM instance belongs to a network, you can write to any remote IOhubTM MQTT broker in the same network.

The use of remote destination allows the creation of distributed networks, at no effort.

The format of a remote network is [<device name>/<application name>], where:

  • device name: the IOhubTM ID of the instance, in the network, as assigned in the management.
  • application name: the name of the application, as assigned in the management.

Both the device name and the application name can have the value *, meaning every instance name.

Examples

  • [mybox/dbserver]: the application named dbserver in the instance named mybox.
  • [*/dbserver]: every application named dbserver in the network.
  • [mybox/*]: every application in the instance named mybox.
  • [*/*]: every application in the network.

Each remote destination definition, does not include the sender.

EXCH examples

publish a value to every MQTT broker named notifier

publishValue [*/notifier] "slack" "temperature is too high"

Write a value to a specific application

writeField [machine123/packager] "modbus" "speed" 150

Remote destination can include other applications on the same IOhubTM instance, as long as they are in the same network.

If a program sends a message to a remote MQTT broker while your instance does not belong to a network:

  • the message will not be delivered
  • a warning is logged on the console.
  • the program keeps working normally

Technical specs

Each IOhubTM instance is connected to its network over an encrypted connection, with 1024 bits authentication.

The MQTT commands having a remote destination are executed asyncronously to the program. They do not affect the program execution flow.

Any network communication error is logged on the console of the exchanger container.

HTTP Statements

These statements allow to execute HTTP calls, typically to invoke REST API endpoints, or to be used as webhooks.

The HTTP methods implemented are:

  • get
  • post
  • put
  • delete

The calls are asynchronous, the program execution is not blocked by the call.

If the call fails, an error is logged on the console.

If needed, custom HTTP headers can be passed along with the http call. The headers can be passed as:

  • a json object array, with each element describing a header through the properties headerName and headerValue, e.g. [{ "headerName": "X-Auth-Token", "headerValue": "mytoken" }]
  • a single header as a pair of values, representing the header name and the header value

If you need the response body, an optional mqttTopic can be passed; the response body will be sent as a message to the topic specified after mqttTopic.

if LOCAL or GLOBAL is used, the value is forcibly published on the local/global mqtt broker. If the requested broker is not configured, an error is thrown and the program is stopped.

httpGet: execute an http get call

Syntax

httpGet url [HEADERS <headers> | HEADER name value] [LOCAL|GLOBAL] [MQTTTOPIC <destination topic>]

An http get call is executed, using the optional headers if defined.

Examples

httpGet "https://httpbin.org/get"
httpGet "https://httpbin.org/get" mqttTopic "someTopic"
httpGet "https://httpbin.org/get" global mqttTopic "someTopic"
httpGet "https://httpbin.org/get" HEADER "x-auth-token" "mytoken"
httpGet "https://httpbin.org/get" HEADERS '[{ "headerName": "X-Auth-Token", "headerValue": "mytoken" }, { "headerName": "X-Username", "headerValue": "myusername" }]'

httpPost: execute an http post call

Syntax

httpPost url [HEADERS <headers> | HEADER name value] [payload] [LOCAL|GLOBAL] [MQTTTOPIC <destination topic>]

An http post call is executed, using the optional headers if defined.

The optional payload is sent as body.

Examples

httpPost "https://httpbin.org/post"
httpPost "https://httpbin.org/post" HEADER "x-auth-token" "mytoken"
httpPost "https://httpbin.org/post" "my string payload"
httpPost "https://httpbin.org/post" '{ "data": "some string" }'

httpPut: execute an http put call

Syntax

httpPut url [HEADERS <headers> | HEADER name value] [payload] [LOCAL|GLOBAL] [MQTTTOPIC <destination topic>]

An http put call is executed, using the optional headers if defined.

The optional payload is sent as body.

Examples

httpPut "https://httpbin.org/put"
httpPut "https://httpbin.org/put" HEADER "x-auth-token" "mytoken"
httpPut "https://httpbin.org/put" "my string payload"
httpPut "https://httpbin.org/put" '{ "data": "some string" }'

httpDelete: execute an http delete call

Syntax

httpDelete url [HEADERS <headers> | HEADER name value] [LOCAL|GLOBAL] [MQTTTOPIC <destination topic>]

An http delete call is executed, using the optional headers if defined.

Examples

httpDelete "https://httpbin.org/delete"
httpDelete "https://httpbin.org/delete" HEADER "x-auth-token" "mytoken"
httpDelete "https://httpbin.org/delete" HEADERS '[{ "headerName": "X-Auth-Token", "headerValue": "mytoken" }, { "headerName": "X-Username", "headerValue": "myusername" }]'

Comments

#

Each line starting with # (not considering initial white spaces) is considered as a comment.

Syntax

# <any text>

Examples

# incrementing the counter
${counter} = ${counter} + 1

Metadata Statements

The metadata statements allow to modify the metadata on the outgoing payload dynamically. Metadata gets added to the usual metadata field.

setMetadata: Set a metadata field to the outgoing payload

Syntax

setMetadata <metadata key> <value>

<metadata key> must be a quoted string. <value> is the value set to the <metadata key> property. It is set as a string.

Examples

setMetadata "batch" ethernetip/batchnum
publishPayload "slack/mychannel"

The resulting payload will be, as an example:

{
    "value": 115,
    "ts": 1612043702457,
    "metadata": {
        "batch": "2567B"
    }
}

setJsonMetadata: Set a json metadata field to the outgoing payload

Syntax

setMetadata <metadata key> <value>

<metadata key> must be a quoted string. <value> is the value set to the <metadata key> property. Must be a valid json value. If the value cannot be valid JSON, use setMetadata to publish the value in raw format.

Examples

${employee} = call(json_new_obj)
${employee} = call(json_set, ${employee}, 'name', "John")
${employee} = call(json_set, ${employee}, 'age', 33)
setMetadata "employee" ${employee}
publishPayload "slack/mychannel"

The resulting payload will be, as an example:

{
    "value": 115,
    "ts": 1612043702457,
    "metadata": {
        "employee": { "name": "John", "age": 33 }
    }
}

delMetadata: Remove a metadata field from the outgoing payload

Syntax

delMetadata <metadata key>

<metadata key> must be a quoted string. The property is removed from the outgoing metadata.

clearMetadata: Clear all metadata fields on the outgoing payload

Syntax

clearMetadata

The metadata payload is removed from the outgoing payload.

Subroutines

In case you have some repetead code, you can group it into a subroutine.

EXCH subroutines are simple; they are synctactic sugar to group instructions. No parameters can be passed, no value can be returned.

Syntax

A group of instructions can be grouped, under a name, and then later invoked, using the following syntax.

sub <name>
  <instructions>
endsub

...
callsub <name>

Examples

sub sendMessage
  publishValue "slack" "Temperature is " + ${msg}
endsub

if (modbus/temp > 100) then
  ${msg} = "hot"
  callsub sendMessage
elif (modbus/temp > 50) then
  ${msg} = "warm"
  callsub sendMessage
fi

Recursion

By default, the callsub statement is not allowed inside a sub. If you try to use it, you get a fail error message logged on the console, and the program is terminated.

If you want to enable callsub inside a sub, you need to set unsafe to on, like in the example below.

sub recurse
  if (${a} < 10) then
    ${a} = ${a} + 1
    logValue ${a}
    callsub recurse
  endif
endsub

init
  unsafe on
  initVar ${a} 0
endinit

callsub recurse

produces

logValue: 1 (number)
logValue: 2 (number)
logValue: 3 (number)
logValue: 4 (number)
logValue: 5 (number)
logValue: 6 (number)
logValue: 7 (number)
logValue: 8 (number)
logValue: 9 (number)
logValue: 10 (number)

The use of recursion might produce infinite loops. set unsafe to on only if your program has been fully tested during development.

Loops

EXCH does not provide classical for/while loops. EXCH only allows to invoke a subroutine on each array element or object property.

The iterable structures are:

  • JSON Arrays
  • JSON Objects

Syntax

sub <name>
  <instructions>
endsub

...
callSubOn <name> <iterable> <value variable> [<key variable>]

The subroutine name <name> get invoked once for every array element / object property in <iterable>. In each subroutine call the variable <value variable> gets the value of the iterable value; If <key variable> is defined, in each subroutine call the variable <key variable> gets the value of the iterable value (index for arrays, property for objects).

Examples

sub print
  logValue ${index} + ': ' + ${value}
endsub

${array} = call(json_new_arr)
${array} = call(json_push, ${array}, '', "one")
${array} = call(json_push, ${array}, '', "two")
${array} = call(json_push, ${array}, '', "three")

callSubOn print ${array} ${value} ${index}

The result is

logValue: 0: one (string)
logValue: 1: two (string)
logValue: 2: three (string)
sub print
  logValue ${key} + ': ' + ${value}
endsub

${obj} = call(json_new_obj)
${obj} = call(json_set, ${obj}, 'name', 'mark')
${obj} = call(json_set, ${obj}, 'age', 29)
${obj} = call(json_set, ${obj}, 'city', 'NY')

callSubOn print ${obj} ${value} ${key}

The result is

logValue: name: mark (string)
logValue: age: 29 (string)
logValue: city: NY (string)

Flow Control Statements

return: Stop program execution

Syntax

return

Examples

Only "BEFORE RETURN" is logged. any instruction following the return is not executed.

logValue "BEFORE RETURN"
return
logValue "AFTER RETURN"

Only the current execution is affected. If the program is triggered again, it is fully executed again.

fail: Stop program execution with error

Syntax

fail <value>

Examples

A fail command:

  • stops the current program execution, like the return comand;
  • logs an error on the console;
  • sets an error flag.

In the example below, if the fail command is executed, the init block is not considered as completed. The next iteration will run again the init block.

init
    strict on
    check modbus/foo
    if ( modbus/foo < 10 ) then
        fail "modbus/foo is still " + modbus/foo
    endif
endinit

# main program here

Only the current execution is affected. If the program is triggered again, it is fully executed again.

strict: Defines the behavior when reading uninitialized variables or field mqtt measures

Syntax

strict on|off

When strict mode is on, each access to an uninitialized variable or a not yet initialized mqtt field measure will stop the program execution. A warning is logged on the console.

Note that the program halt is dynamically trigged upon variable reading; any side effect produced by the previously executed statements might produce undesired results (look at the check statement, delay program execution until all the needed values have been initialized).

When strict mode is off, uninitialized variables or unknown mqtt field measure are evaluated as empty string.

The strict mode is on by default. You can dynamically change the strict mode during the program execution.

check: Try to access the variable, to check if defined

When strict mode is on, you can use the check statement to prevent the program from running when some values are not yet initialized.

Using check is like accessing an uninitialized variable, to generate a program error/halt before the program is executed and avoiding interruptions in the middle of the execution.

  • When strict mode is on, checking an uninitialized variable halts the program execution and logs a warning on the console.
  • When strict mode is off, the check statement does not affect.

Syntax

check <variable | mqttTopic>

Examples

strict on

# check all measures are available
check modbus/measure1
check modbus/measure2
# all variables are available

# main program
${a} = modbus/measure1
${a} = ${a} / 10

unsafe: Enable/disable callsub inside a sub

When unsafe is off, the use of callsub inside a sub is not allowed. If callsub is invoked insiude a subroutine, nn error is generated, logged on the console, and the program returns immediately.

If you want to enable the use of callsub inside a subroutine, set unsafe to on. By default unsafe is off.

Syntax

unsafe on|off

Unsafe mode can be set and changed at runtime

Examples

unsafe off
sub a
  logValue "in a"
  callsub b
endsub

sub b
  logValue "in b"
endsub

callsub a

produces

logValue: in a (string)
Cannot run sub programs in this program section
unsafe on
sub a
  logValue "in a"
  callsub b
endsub

sub b
  logValue "in b"
endsub

unsafe on
callsub a

produces

logValue: in a (string)
logValue: in b (string)

Troubleshooting

logValue: Logs the value to console

Syntax

logValue <value>

The value is printed on the console. To use only for debugging purposes.

Examples

logValue ${a}
logValue "The incoming value is " + ${_v}
logValue "The protocol is " + ${_p}

logJSON: Logs the JSON value to console

Syntax

logJSON <value>

The value is converted into a json value and pretty-printed on the console. To use only for debugging purposes.

If the value cannot be converted into JSON, a warning message is logged on the console.

Examples

logJSON ${_v}

EXCH examples

Watering system temperature alert

EXCHANGER_CFG environment variable: upon each value incoming on the topic opcua/myplc1.temperature, the EXCH instructions defined in the WATERING environment variable are executed.

when can have the value "always" (the EXCH is executed upon every incoming value, even if equal to the previous value) or "onChange" (the EXCH is executed upon every incoming value, only if different from the previous value.)

[
    {
        "from": {
            "protocol": "opcua",
            "name": "myplc1.temperature"
        },
        "trigger": {
            "when": "always",
            "exch": "WATERING"
        }
    }
]

WATERING environment variable: get the temperature in tenths of C degrees, divide by 10

If above a dynamic setpoint:

  • stop the system
  • turn on an alarm on the plant.
  • send an sms notification (publishing a message on the topic subscribed by the Twilio container)
  • send a Telegram group notification to the subscribed users (publishing a message on the topic subscribed by the Telegram container)
  • Do it only if the system is running.
if (opcua/plc1.running == true) then
    ${_v} = ${_v} / 10
    if (${_v} > opcua/plc1.dangerLimit) then
        # stop the system
        writeField "opcua" "plc1.running" false

        # turn on alert
        writeField "modbus" "siren" 1

        # compose a detailed message
        ${message} = "Alert: Temp too high: " + ${_v} + " degrees reached"

        # send sms and telegram notifications
        publishValue "twilio/operator1" ${message}
        publishValue "telegram/alerts" ${message}
    endif
endif

Save a production order on the ERP, upon a batch completion

A Fanuc robot (communicating over a standard Fanuc OPC/UA hub) packages a number of pieces. When the count of the pieces moved are over a threshold:

  • stop the robot (waiting for the operator preparing the new boxes and re-enabling the robot with a new batch number)
  • turn on a light, to get the operator attention
  • save a production batch order on an Odoo ERP system

EXCHANGER_CFG environment variable

[
    {
        "from": {
            "protocol": "opcua",
            "name": "fanuc1.packagedCount"
        },
        "trigger": {
            "when": "onChange",
            "exch": "BATCHDONE"
        }
    }
]

BATCHDONE environment variable

if (opcua/fanuc1.packagedCount > 100) then
    # stop the robot
    writeField "opcua" "fanuc1.running" false

    # turn on the light
    writeField "modbus" "whitelight" 1

    # add the batch number to the outgoing metadata
    setMetadata "productionBatch" opcua/fanuc1.batchNumber

    # modify the outgoing metadata
    ${_v} = opcua/fanuc1.packagedCount

    # send the updated payload to the odoo container
    publishPayload "odoo/batch"
endif

Changelog

v1.1.12
  • Global mqtt broker integration added
v1.1.11
  • Exchanger ready notification added
v1.1.10
  • All content types accepted on http calls
v1.1.9
  • encode_base64, decode_base64
v1.1.8
  • Bug on permament store
v1.1.7
  • Dynamic delay on mqtt actions
v1.1.6
  • permanent variables
  • initvar instruction
v1.1.5
  • http call instructions
  • round with decimal
  • mqttTopic on http call instructions
  • timezone in cron
  • more detailed error message on compile
v1.1.4
  • null type
  • json_exists function
  • json_is_obj function
  • json_is_arr function
  • json_arr_len function
  • date_time function
  • date_day function
  • date_month function
  • date_year function
v1.1.3
  • sub statement
  • unsafe statement
  • delay on MQTT operations
v1.1.2
  • setMetadata statement
  • setJsonMetadata statement
  • create_payload function
  • JSON functions
v1.1.1
  • logJSON statement
v1.1.0 v1.0.0
  • First Release