Assetwolf supports Phi, a simple language for running calculations and small programs in real-time.
When assets send data to an Assetwolf IoT portal, Assetwolf can process that data through its rules engine. Those rules can be really simple—such as "raise an alert if sensor reading exceeds [threshold]"—or by using a simple form of program code, they can be far more sophisticated.
For example, you may want to:
The Phi programming language allows those things to be done.
You can see the Phi programming interface when editing a Schemas in an Assetwolf portal.
The Schemas page specifies:
If you have an Assetwolf portal, simply go to Setup->Schemas, pick a schema and edit it, and you will see where Phi calculations can be entered.
Phi code is a language with C-style syntax. Variables can be defined like this:
a = 1 b = 'List of Important Bird Areas in Michigan'
String concatenation is done using the ~
operator:
message = greetingOfChoice ~ ' ' ~ recipient
...or the paste()
function (which automatically adds a space):
message = paste(greetingOfChoice, recipient)
You can use single or multi-line comments, e.g.:
#Single line comment (using a hash-sign) //Single line comment (using two forward-slashes) /* Multi-line comment */
You can use standard mathematical operations, e.g.:
a = 1 + 2 print(a) #3 a += 1 print(a) #4 a++ print(a) #5 a = 2 * 3 print(a) #6 a = 8 - 1 print(a) #7 a = 2 ^ 3 print(a) #8 a = 21 % 12 print(a) #9
While writing Phi code, you can use the print()
function to check the contents of variables. (This only has an effect while testing; the data-processor ignores this function.)
if
statements work like this:
shaken = true
stirred = false
speed = 12 if (shaken and not stirred) { beverage = 'cocktail' } else if (speed == 88) { beverage = 'cola' } else { beverage = 'tea' }
You can also use &&
, ||
and !
instead of and
, or
, and not
, e.g.:
if (shaken && !stirred) {
for
loops have three different forms:
#Fixed range for (i in 1:10) { print(i) } #Looping through an array numbers = [2, 3, 5, 7] for (p in numbers) { print(p) } #Looping through an object colors = {red: 0xff0000, green: 0x00ff00, blue: 0x0000ff} for (color, code in colors) { print(color, code) }
You can use continue
and break
in loops, e.g.:
primes = []
for (i in 1..100) {
if (i < 2) {
isPrime = false
} else if (i == 2) {
isPrime = true
} else {
isPrime = true
stop = i - 1
for (j in 2..stop) {
if (i % j == 0) {
isPrime = false
break
}
}
}
if (!isPrime) {
continue
}
primes[] = i
}
There are four sources of IoT data:
Here are the variables in more detail.
Here are all available variables.
The thisRow
object will contain the row from the database that's currently being processed by the data-processor.
Variable | Meaning |
thisRow.value |
An array containing the incoming data or assumed value* |
thisRow.actual |
An array containing the stated incoming data |
thisRow.lastKnown |
An array containing all of the entries from thisRow.value that were assumed |
thisRow.lastKnownAt |
An array containing the timestamps for all of the entries in thisRow.lastKnown |
prevRow.actual |
An array containing the incoming data from the previous row |
prevRow.lastKnown |
An array containing every entry from prevRow.value that was assumed |
prevRow.lastKnownAt |
An array containing the timestamps for all of the entries in prevRow.value |
* The "value" array is the simple way to get to the latest value of a field, and does not discern between known and assumed values. (Remember that Assetwolf supports rows of data coming from assets in which fields may be missing. When the asset sends data but a field on its schema is missing, the last-known value is inserted into the database, with a flag to say it was "assumed".)
#If you just want the most recent value for the light level, and don't care if it was assumed lightLevel = thisRow.value.lightlevel #If you want more details about when the value for the light level was received if (thisRow.actual.lightlevel is not null) { lightLevel = thisRow.actual.lightlevel lightLevelTS = thisRow.timestamp lightLevelWasAssumed = false } else { lightLevel = thisRow.lastKnown.lightlevel lightLevelTS = thisRow.lastKnownAt.lightlevel lightLevelWasAssumed = false }
Data pools are "big picture" views of data, and can display data that has been processed and aggregated for multiple assets, or for multiple data pools. Data pools are in a hierarchy. For example, a Location can have a data pool gathering data from Areas, or within it an Area can have a data pool gathering data from individual Assets.
The data processor processes each data pool when all of its "source" data pools have presented data, or one of its source data pools has presented data more than once. (You can think of them as "child", or "subordinate" data pools.) The source refers either to data coming from an assets under the current data pool, or from the assets in the data pool.
When this occurs, the data processor runs, and a program may contain the following variables.
Variable | Meaning |
source.total |
The total number of sources |
source.numKnown |
The number of sources that are transmitting data |
source.numAssumed |
The number of sources that did not transmit data this time |
source.values |
An array of arrays containing all of the data from the sources. Note this will include assumed data if one or more of the sources missed a transmission |
#Get the highest/lowest/average/total values maxLightLevel = max(source.values.lightlevel) minLightLevel = min(source.values.lightlevel) totalLightLevel = sum(source.values.lightlevel) averageLightLevel = mean(source.values.lightlevel) //N.b. the sources are arrays of data, so you can loop through them maxLightLevel = 0 minLightLevel = Infinity totalLightLevel = 0 for (lightLevel in source.values.lightlevel) { if (maxLightLevel < lightLevel) { maxLightLevel = lightLevel } if (minLightLevel > lightLevel) { minLightLevel = lightLevel } totalLightLevel += lightLevel } averageLightLevel = totalLightLevel / count(source.values.lightlevel)
Both asset and data pool schemas have the following variables.
Variable | Meaning |
metadata |
An array containing the asset or data pool's metadata |
schema |
An array containing the schema's metadata |
thisRow.timestamp |
The timestamp of the data being processed. All timestamps are in milliseconds since the epoch time; i.e. the number of milliseconds that have past since the 1st of January 1970 UTC. |
prevRow.timestamp |
The timestamp of the previous data received |
prevRow.value |
An array containing all of the incoming data/assumed values/calculated values from the previous row |
activationsThisTime = thisRow.value.activations - prevRow.value.activations activationsPerSecond = activationsThisTime * 1000 / (thisRow.timestamp - prevRow.timestamp)
There are several functions available that refer to data nodes in any hierarchy of data pools and assets:
Function | Use |
getMetadata(name[, nodeId]) |
This function gets the value of the metadata field in name . Defaults to the current node (data pool or asset), but gets that of nodeId if specified. (This function specifically gets the value of name from the node itself, not from a parent node). |
getInheritedMetadata(name[, nodeId]) |
Gets the value of the metadata field in name that is inherited from a parent node (or any higher level parent). Defaults to the current node (data pool or asset), but gets that of nodeId if specified. Use this rather than getMetadata in situations where name may be set on the node itself or inherited from a parent. |
setValue(key, value) |
Call this function to set the value of a calculated field. It won't work on any other type of field (i.e. you can't use it to change the value of incoming data). |
getHistoricValue(key, timestamp) |
This function retrieves a historic value. Enter a key and a timestamp, and the function will tell you what that value was at that time. (The timestamp does not need to be an exact match to a datapoint; you'll receive an assumed value if it does not perfectly match.) As a shortcut you can also enter a description from the above function, e.g.: getHistoricValue('light_level', '2 days ago'). |
getTimestamp(description[, timestamp]) |
Given a description such as:
Note that any times you specify (e.g. 3pm) will be according to the default timezone set in the site settings, and not the user's time zone. If you type something relative then you can specify another timestamp to match against - otherwise if not specified it will default to thisRow.timestamp. |
print(var1, var2, ...) |
This can be used while testing your Phi code to check the values of variables and add them to the debug-output. This function only functions while testing your code; the data-processor ignores it. |
activationsThisWeek = thisRow.value.activations - getHistoricValue('activations', '1 week ago') setValue('activations_this_week', activationsSinceLastWeek)
Phi is based on a subset of the Twig language.
Any basic expression, filter or operator from Twig will also work in Phi, for example:
n = -12 print(n|abs) #12 print(1 is odd) #true print(1 is even) #false colour = 'Red' t = "my car is #{colour}" print(t) #"my car is Red" print(t|lower) #"my car is red" print(t|upper) #"MY CAR IS RED" print(t|capitalize) #"My car is red" print(t|title) #"My Car Is Red" r = t|replace({car: 'van'}) print(r) #"my van is Red" x = 1
y = 2 t = "#{x} plus #{y} is #{x + y}" print(t) #"1 plus 2 is 3" n = 1.234e7 print(n|number_format) #"12,340,000" numbers = [4, 5, 6] print(numbers|join(',')) #"4,5,6" fruit = "apple,banana,cherry" print(fruit|split(',')) #["apple", "banana", "cherry"] t = ' Hello world ' print(t starts with 'H') #false print(t ends with 'd') #false t = t|trim print(t) #"Hello world" print(t starts with 'H') #true print(t ends with 'd') #true print(t matches '/hello/') #0 print(t matches '/Hello/') #1 print(t matches '/hello/i') #1 print('Hello' in t) #true print('Hi' in t) #false
Note that Twig's environment functions such as constant()
, include()
, parent()
and source()
do not work in Phi.