Phi: a real-time programming language for connected devices

Assetwolf supports Phi, a simple language for running calculations and small programs in real-time.

Why do I need Phi?

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:

  • compare a latest value with others in that device's row of data
  • compare the value with data from other devices nearby
  • compare the value with previous data from that device
  • perform scientific calculations on any of the above data (such as sine, cosine, mean, standard deviation, etc.)
  • perform simple program logic (such as for loops, and if...else logic).

The Phi programming language allows those things to be done.

How do I get see the Phi interface?

You can see the Phi programming interface when editing a Schemas in an Assetwolf portal.

The Schemas page specifies:

  • meta data fields (i.e. static data fields)
  • for an Asset definition, incoming data fields; or for a Data Pool definition, source data fields
  • the calculated data fields
  • how the calculations are performed (this where Phi comes in).

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.

What does Phi code look like?

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

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) {

#Looping through an array
numbers = [2, 3, 5, 7]
for (p in numbers) {

#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
	if (!isPrime) {
	primes[] = i

How does Phi code refer to IoT data?

There are four sources of IoT data:

  • meta data (about an asset, or a schema itself)
  • incoming data from "this asset" (for asset schemas only)
  • data from assets underneath the current data pool in the hierarchy (for data pool schemas only)
  • previous data that occurred for "this asset" or "this data pool" at some time in the past.

Here are the variables in more detail.

Phi variables in more detail

Here are all available variables.

For assets only

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

For data pools only

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 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)

For both assets and data pools

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)


Advanced usage

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.