Writing Formulas
Edge processing takes sensor data, aggregates it, and computes all the Calculations and Alerts all according to your Thing Type Specifications.
The following section describes the syntax for writing these functions. The base syntax for all these formulas is JavaScript (specifically Node.js).
Function Basics
Let’s assume our Example IoT Connected Asset produces a message which looks like this:
interface thing_message {
pressure_volts: number;
temperature: number;
state: string;
}
The Gateway starts building the following message:
const message = {
sensor: {
pressure_volts: 5,
temperature_volts: 6,
state: "on",
},
};
Let’s write the function for a Calculation named pressure_psi:
(sensor.pressure_volts * 20) / 10;
Sensor Namespace
In the above formula we have access to the current IoT Connected Asset’s aggregated sensor data under the namespacesensor. There are 4 additional namespaces covered
in Namespaces.
The Gateway message now looks like:
const message = {
sensor: {
pressure_volts: 5,
temperature_volts: 6,
state: "on",
},
calculation: {
pressure_psi: 10,
},
};
Now let’s write a formula for an alert on pressure named overpressure:
sensor.state === "on" && calculation.pressure_psi > 15;
JavaScript
Any valid JavaScript which returns a value can be used in these Calculations and Alerts. Take a look at JavaScript Basics for help on basic operators.Our Gateway message now looks something like:
const message = {
sensor: {
pressure_volts: 5,
temperature_volts: 6,
state: "on",
},
calculation: {
pressure_psi: 10,
},
alert: {
overpressure: false,
},
};
This is the basic structure you’re putting together when building your Thing Type specification.
Namespaces
There are 5 base namespaces which can be used to access data from these formulas:
sensor- Provides access to all the latest aggregated sensor values. Aggregation is
defined by the
Thing Type specificationSalesforce, ServiceNow. calculation- Provides access to all the latest calculated values. Calculations are defined by
the
Thing Type specificationSalesforce, ServiceNow. alert- Provides access to all the latest alert values. Alerts are defined by the
Thing Type specificationSalesforce, ServiceNow. context- Provides access to all the latest context values. Context is defined by the
Context settingsSalesforce, ServiceNow.
twin- Provides access to all the the following values:
-
role- the Digital Twin role for the referenced IoT Connected Asset
Priority
When specifying calculations and alerts in the thing-type specification (Salesforce, ServiceNow), the priority defines the execution order of each calculation/alert. Alerts are always evaluated after all calculations. Within each group (calculations and alerts), formulas are evaluated in priority order from smallest (0) to largest (infitinity). A calculatoin with priority 0 will be evaluated before a calculation with priority 1 and so on. For elements with equal priority, their order relative to each other is arbitrary. So in the following example:
calcA: priority = 0calcB: priority = 10calcC: priority = 10calcD: priority = 11
… calcA is evaluated first followed by calcC and calcB (in no defined order), then finally calcD is evaluated. Once all the calculations are evaluated, the alerts are then evaluated.
When referring to calculations which are evaluated later in the cycle from within calculations which happen earlier, there will be a unit-delay in the value retrieved.
Example:
calcA: priority = 0, formula =(calculation.calcB === undefined) ? 0 : calculation.calcB + 1calcB: priority = 1, formula =calculation.calcA + 1
- On the first pass,
calculation.calcBwill beundefined, socalcAwill be0,calcBis then1- On the second pass,
calcAwill be2,calcBwill be3- … and so on
Specification Metadata Access
In general, when you reference a sensor, calculation, or alert within a formula, it implicitly retrieves it’s current value (i.e. calculation.calcAbc implicitly becomes calculation.calcAbc.value). Alternatively, there are other metadata which can be retrieved from the given element.
In addition to the implicit value, the following parameters are available:
- Sensor
- name: the calculation name (e.g.
sensor.sensorAbc.name = "sensorAbc") - label: the calculation label (e.g.
sensor.sensorAbc.label = "Sensor ABC") - dataType: the calculation data-type (e.g.
sensor.sensorAbc.dataType = "Number") - aggregateType: the sensor’s aggregation-type (e.g.
sensor.sensorAbc.aggregateType = "Seconds") - aggregateFunction: the sensor’s aggregation-function (e.g.
sensor.sensorAbc.aggregateFunction = "MAX") - aggregateInterval: the sensor’s aggregation-interval (e.g.
sensor.sensorAbc.aggregateType = 300) - isStored: true if the sensor is stored in the cloud (e.g.
sensor.sensorAbc.isStored = true) - storageDataType: the cloud storage type (e.g.
sensor.sensorAbc.isStored = "Double")
- name: the calculation name (e.g.
- Calculation
- name: the calculation name (e.g.
calculation.calcAbc.name = "calcAbc") - label: the calculation label (e.g.
calculation.calcAbc.label = "Calculation ABC") - formula: the calculation formula (e.g.
calculation.calcAbc.formula = "1 + 1") - priority: the calculation priority (e.g.
calculation.calcAbc.priority = 14) - isActive: true if the calculation is active (e.g.
calculation.calcAbc.isActive = true) - isStored: true if the calculation is stored in the cloud (e.g.
calculation.calcAbc.isStored = true) - storageDataType: the cloud storage type (e.g.
calculation.calcAbc.isStored = "Double")
- name: the calculation name (e.g.
- Alert
- name: the alert name (e.g.
alert.alertAbc.name = "alertAbc") - label: the alert label (e.g.
alert.alertAbc.label = "Alert ABC") - alertType: the alert-type (e.g.
alert.alertAbc.alertType = "Threshold") - alertInterval: the interval which the alert re-drives to ServiceNow (e.g.
alert.alertAbc.alertInterval = 3600) - formula: the alert formula (e.g.
alert.alertAbc.formula = "calculation.calcAbc > 1") - priority: the alert priority (e.g.
alert.alertAbc.priority = 14) - isActive: true if the alert is active (e.g.
alert.alertAbc.isActive = true) - isStored: true if the alert is stored in the cloud (e.g.
alert.alertAbc.isStored = true)
- name: the alert name (e.g.
Examples
Example 1 Constant
Name: example1
Description: Always return a constant value for the calculation or alert.
Formula:
"hello world";
Result: constant { ... example1: 'hello world' ... } in each message
Example 2 Basic Arithmetic
Name: example2
Description: Scale sensorA
Formula:
sensor.sensorA * 10 + 2;
Result:
{
"sensorA": 3,
"example2": 32
}
Example 3 Conditional
Name: example3
Description: If the IoT Connected Asset’s sensorA is over 10, calculate, otherwise return 0.
Formula:
sensor.sensorA > 10 ? sensor.sensorB * 50 : 0;
Results:
{
sensorA: 3,
sensorB: 6,
example3: 0
}
{
sensorA: 11,
sensorB: 6,
example3: 300
}
Alternate
if (sensor.sensorA > 10) {
return sensor.sensorB * 50;
} else {
return 0;
}
Example 4 Digital Twin
Name: example4
Description: If the IoT Connected Asset’s parent is powered on, calculate, otherwise return 0.
Formula:
$parent.calculation.powerOn ? sensor.sensorB / $parent.sensor.inputVoltage : 0;
Results:
parentId: {
sensor: {
inputVoltage: 12
},
calculation: {
powerOn: true
}
}
{
"sensorB": 6,
"example4": 0.5
}
parentId.sensor.inputVoltage: 0
parentId.calculation.powerOn: false
{
"sensorB": 6,
"example4": 0
}
Alternate
if ($parent.calculation.powerOn) {
return sensor.sensorB / $parent.sensor.inputVoltage;
} else {
return 0;
}
Example 5 Context
Name: example5
Description: If the IoT Connected Asset’s entitlement level is either Platinum or Gold, evaluate the threshold, otherwise return false.
Formula:
["platinum", "gold"].includes(context.entitlementLevel.toLowerCase())
? calculation.someValue > 100
: false;
Results:
context.entitlementLevel = "Gold";
{
"someValue": 101,
"example5": true
}
context.entitlementLevel = "Silver";
{
"someValue": 101,
"example5": false
}
Alternates
if (["platinum", "gold"].includes(context.entitlementLevel.toLowerCase())) {
return calculation.someValue > 100;
} else {
return false;
}
function isCovered(level) {
const coveredLevels = ["platinum", "gold"];
return coveredLevels.includes(level.toLowerCase());
}
if (isCovered(context.entitlementLevel)) {
return calculation.someValue > 100;
} else {
return false;
}