Phi: a real-time scripting language for IoT

Phi is a scripting language for internet of things devices. It combines the power and familiarity of PHP and its cousin Twig, running them in a safe environment, and supplementing them with features for handling IoT device data.

Running in near-real-time, Phi has features for recalling past data, aggregating data, and triggering alarms when conditions are met.

Data architecture

Data from devices in some location is fed to a matching architecture on AssetWolf:

Diagram of devices

It doesn't matter how the data is sent: it can be MQTT, http, or LoRaWAN; once inside AssetWolf, it is treated the same.

For each asset, Phi script is run:

Diagram of devices 2The Phi script runs as soon as data for the asset arrives.

There is an array called thisRow which contains the latest row of data, and an array called prevRow which contains the previous row (which is often useful for comparison). Not all fields are expected to be present every time, and when a field's data isn't sent in a transmission it's possible to get the most recently-sent value. See this page for more info.

Hierarchies

AssetWolf can model a hierarchy. A simple hierarchy could consist of assets being within data pools, and the data pools being within a location. For example, a hierarchy could have sensors within rooms, and there can be several rooms within each building:

Diagram of devices 3The principle applies, that in order for the Phi script of a data pool to run:

  • every one of its child assets has sent some data, or
  • one of its child assets has sent data twice once since the last run.

In the above example, every time the assets of Room 1 communicate (or one communicates twice), the Phi script of Room 1 gets run to process their data. When Rooms 1, 2 and 3 all process their data (or one is processed twice), the Phi script for Location A gets run to process the data coming up from the rooms.

Hierarchies can have any number of levels, though typically there are 1 or 2 levels, and seldom more than 5 levels.

We generally call assets and data pools nodes.

Because of this hierarchical structure, we talk of data bubbling up from the lower level nodes to the upper level nodes. This is useful because instead of just offering granular device-level data, AssetWolf can do much more with the aggregated data.

Asset types and schemas

Assets are likely to be of different kinds: for example, one type of asset may measure temperature and voltage, while another type of asset may count some kind of activity. We use the term schema to denote an asset type.

A schema for an asset type defines:

  • meta data fields for all assets of that type
  • incoming data fields from the assets
  • the Phi script that is executed in near-real-time for all assets of that type
  • calculated data fields (the values that are stored after Phi runs).

The same principle applies for a schema for a data pool type (e.g. room, zone, location):

  • meta data fields for all data pools of that type
  • source data fields from the level or levels below
  • the Phi script that is executed in near-real-time for all data pools of that type
  • calculated data fields.

In an AssetWolf portal, go to Set Up Things->Schemas to see all schemas. When viewing or editing a schema a portal superuser can see the above things and edit them, including the Phi script for that schema.

Metrics

Assetwolf supports metrics, which again can be calculated on any asset or data pool level, up to location level.

Metrics don't run in near-real-time, but are calculated at regular intervals, such as every month, every day, every hour or every 10 minutes. These are typically used for key performance information, billing or other statistics.

They are written in Phi and so share the same language and principles.

Alarms

AssetWolf supports alarms. It's possible to define a trigger that will raise an alarm, and a procedure for what should occur when the alarm begins and ends..

At their simplest, triggers can be defined as some measured value exceeding an allowed value; but when more complex conditions need to be detected, Phi script can be written so as to cause a trigger to fire.

Using Phi to trigger an alarm allows far more complex rules to be set up. Such rules may be based on historic values, the values of nearby assets or data pools, and other factors.

Alarms can be set for assets and for data pools. The fireTrigger() function is used to trigger an alarm.

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
dump(a)    #3

a += 1
dump(a)    #4

a++
dump(a)    #5

a = 2 * 3
dump(a)    #6

a = 8 - 1
dump(a)    #7

a = 2 ^ 3
dump(a)    #8

a = 21 % 12
dump(a)    #9

While writing Phi code, you can use the dump() 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) {
    dump(i)
}

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

#Looping through an object
colors = {red: 0xff0000, green: 0x00ff00, blue: 0x0000ff}
for (color, code in colors) {
    dump(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
}