Custom algorithms (current)

Custom algorithms (v7.1 / current)

While Blank-it's out-of-the-box motion detection algorithms are sufficient for most environments, we also allow IT administrators to design their own algorithms in order to customize how Blank-it will blank the screen in response to input from the various sensors.

These algorithms are built in to the configuration file, and executed by the Blank-it software. Custom algorithms can contain arbitrary code.

Custom algorithm versions

This document describes the custom algorithm version v7.1. This is the currently recommended version, and should be used for all implementations of Blank-it using v7.25.13871 onwards.

The previous custom algorithm version is v7.0, which will work on all versions of Blank-it from v7.0.0 onwards. This previous version is no longer recommended for use, except in implementations unable to upgrade to Blank-it v7.25.13871 or later. The documentation for this older version is available here.

Programming language

Custom algorithms are written in JavaScript / ECMAScript. Most features from ES6 onwards are available.

Defining a custom algorithm

Custom algorithms must implement the following function:

function run(sensorData, appState) {
    // ...
}

The function has two parameters:

  • sensorData provides access to the sensor data from the currently connected sensors (GPS / Accelerometer / Gyrometer / OBD-II / BL-205 / BL-360 / etc.)
  • appState provides access to the current app state (sensor state, motion state, etc.). This state can be updated by the function in order to control how Blank-it should respond to the sensor data.

Here is a very simple example, that will set the motion state in response to the speed from GPS being above a certain threshold:

function run(sensorData, appState) {
    appState.sensorType = "GPS";
    appState.sensorDescription = `State: ${sensorData.gpsState} / Speed: ${Math.round(sensorData.gpsSpeed.milesPerHour)}mph`
    appState.sensorState = sensorData.gpsState;
    appState.motionState = (sensorData.gpsState == sensorState.Active && sensorData.gpsSpeed.milesPerHour > 5)
        ? motionState.moving
        : motionState.stationary;
}

Built-in objects and functions

Most of the standard Javascript built-in objects and functions are available to use when building your custom algorithm.

Here is a link to the standard built-in objects and functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects (opens in a new tab)

Custom algorithm classes and structures

The sensorData object is mapped to the following C# class in the Blank-it app:

class SensorData
{
    public string GpsSource { get; set; }
    public SensorState GpsState { get; set; }
    public SensorDataQuality GpsQuality { get; set; }
    public Speed GpsSpeed { get; set; }
    public GeoCoordinate GpsLocation { get; set; }
 
    public string AccelerometerSource { get; set; }
    public SensorState AccelerometerState { get; set; }
    public double AccelerometerX { get; set; }
    public double AccelerometerY { get; set; }
    public double AccelerometerZ { get; set; }
 
    public string GyrometerSource { get; set; }
    public SensorState GyrometerState { get; set; }
    public double GyrometerX { get; set; }
    public double GyrometerY { get; set; }
    public double GyrometerZ { get; set; }
 
    public string Obd2Source { get; set; }
    public SensorState Obd2State { get; set; }
    public Speed Obd2Speed { get; set; }
 
    public string BL205Source { get; set; }
    public SensorState BL205State { get; set; }
    public double BL205Vibration { get; set; }
    public double BL205Motion { get; set; }
 
    public string BL360Source { get; set; }
    public SensorState BL360State { get; set; }
    public SensorDataQuality BL360Quality { get; set; }
    public string BL360Version { get; set; }
    public bool? BL360IsMoving { get; set; }
    public Speed? BL360Speed { get; set; }
    public int? BL360PointCount { get; set; }
}

The appState structure is mapped to the following C# class in the Blank-it app:

class AppState
{
    public MotionState MotionState { get; set; }
    public SensorState SensorState { get; set; }
    public string SensorType { get; set; }
    public string SensorDescription { get; set; }
}

The follow structures are also useful to be aware of:

enum MotionState
{
    Unknown,
    Moving,
    Stationary,
}
 
enum SensorState
{
    Unknown,
    SearchingForSensor,
    WaitingForData,
    Active,
    Disabled,
}
 
public enum SensorDataQuality
{
    None,
    Low,
    Medium,
    High,
}
 
struct Speed
{
    public double MetresPerSecond { get; }
    public double KilometresPerHour { get; }
    public double MilesPerHour { get; }
    public double Knots { get; }
}
 
struct GeoCoordinate
{
    public double Latitude { get; }
    public double Longitude { get; }
}

Custom algorithm additional functions

A number of other functions are made available:

  • log(message, arguments) writes a log message.
  • error(message, arguments) writes an error message.
  • formatSpeed(speed) formats the specified Speed value as a localized string, using miles-per-hour in imperial locales and kilometres-per-hour in metric locales.
  • algorithmName(name) sets the algorithm name for display in the Blank-it UI.
  • requireVersion(version) ensures that the algorithm can only run in a specific version (or later) of Blank-it.

Examples

A basic algorithm that checks for GPS speed over a certain threshold:

function run(sensorData, appState) {
    appState.sensorType = "GPS";
    appState.sensorDescription = `State: ${sensorData.gpsState} / Speed: ${Math.round(sensorData.gpsSpeed.milesPerHour)}mph`
    appState.sensorState = sensorData.gpsState;
    appState.motionState = (sensorData.gpsState == sensorState.Active && sensorData.gpsSpeed.milesPerHour > 5)
        ? motionState.moving
        : motionState.stationary;
}

A more advanced algorithm that looks at GPS speed and also a sliding window of BL-360 speed:

// Set the algorithm name, and any Blank-it version restrictions:
algorithmName('QuickStart (BL-360 + GPS)');
requireVersion('7.25.13871'); // v7.25.13871 or above.
 
// These constants can be edited to customize how your algorithm works:
const bl360WindowSize = 25; // Sliding window size for BL-360 values. We run 20 times per second, so this is one second worth of data.
const bl360WindowSuccessRequired = 13; // Number of successes required in the BL-360 sliding window before we blank the screen.
const bl360SpeedThreshold = 0.01; // BL-360 speed threshold in metres per second. Roughly equivalent to 0.02 mph or 0.03 km/h.
const gpsSpeedThreshold = 1.4; // GPS speed threshold in metres per second. Roughly equivalent to 3 mph or 5 km/h.
 
// Define a sliding window class to keep track of the BL-360 values:
 
class SlidingWindow {
    constructor(size) {
        this.size = size;
        this.queue = [];
    }
 
    add(value) {
        if (this.queue.length >= this.size) {
            this.queue.shift();
        }
        this.queue.push(value);
    }
 
    clear() {
        this.queue.length = 0;
    }
 
    movingCount() {
        return this.queue.filter(x => x === MotionState.Moving).length;
    }
 
    stationaryCount() {
        return this.queue.filter(x => x === MotionState.Stationary).length;
    }
}
 
// Define some helper function to combine sensor states and motion states:
 
function combineSensorStates(states) {
    if (states.some(x => x === SensorState.Active)) {
        return SensorState.Active;
    }
 
    if (states.some(x => x === SensorState.WaitingForData)) {
        return SensorState.WaitingForData;
    }
 
    if (states.some(x => x === SensorState.SearchingForSensor)) {
        return SensorState.SearchingForSensor;
    }
 
    return SensorState.Unknown;
}
 
function combineMotionStates(states) {
    if (states.some(x => x === MotionState.Moving)) {
        return MotionState.Moving;
    }
 
    if (states.some(x => x === MotionState.Stationary)) {
        return MotionState.Stationary;
    }
 
    return MotionState.Unknown;
}
 
// Set up any variables that need to persist between runs:
const bl360Window = new SlidingWindow(bl360WindowSize);
 
// The most important part of the code is the 'run' function.
// This function is called periodically by the Blank-it app to check the sensor data and update the app state.
function run(sensorData, appState) {
 
    // Handle the BL-360 sensor data.
    let bl360MotionState = MotionState.Unknown;
    let bl360MovingCount = 0;
    const hasBL360 = sensorData.BL360State == SensorState.Active && sensorData.BL360Speed != null;
    if (hasBL360) {
        const absoluteSpeed = Math.abs(sensorData.BL360Speed.MetresPerSecond);
        const motionStateForThisReading = (absoluteSpeed > bl360SpeedThreshold)
            ? MotionState.Moving
            : MotionState.Stationary;
        bl360Window.add(motionStateForThisReading);
        bl360MovingCount = bl360Window.movingCount();
        bl360MotionState = bl360MovingCount >= bl360WindowSuccessRequired
            ? MotionState.Moving
            : MotionState.Stationary;
    } 
 
    // Handle the GPS sensor data.
    let gpsMotionState = MotionState.Unknown;
    const hasGps = sensorData.GpsState == SensorState.Active && sensorData.GpsSpeed != null;
    if (hasGps) {
        const absoluteSpeed = Math.abs(sensorData.GpsSpeed.MetresPerSecond);
        gpsMotionState = (absoluteSpeed > gpsSpeedThreshold) ? MotionState.Moving : MotionState.Stationary;
    }
 
    // Update the app state based on the sensor data.
    appState.SensorState = combineSensorStates([sensorData.BL360State, sensorData.GpsState]);
    appState.MotionState = combineMotionStates([bl360MotionState, gpsMotionState]);
 
    // Finally, we can set the sensor type and description.
    // This isn't required for app functionality, but it can help the user understand what the app is doing.
    appState.SensorType = (hasBL360 && hasGps)
        ? 'QuickStart (BL-360 + GPS)'
        : (hasBL360) ? 'QuickStart (BL-360)'
        : (hasGps) ? 'QuickStart (GPS)'
        : 'QuickStart (no data)';
    appState.SensorDescription = (hasBL360 && hasGps)
        ? `BL-360: ${Math.round((bl360MovingCount / bl360WindowSuccessRequired) * 100)}% | GPS: ${formatSpeed(sensorData.GpsSpeed)}`
        : (hasBL360) ? `BL-360: ${Math.round((bl360MovingCount / bl360WindowSuccessRequired) * 100)}%`
        : (hasGps) ? `GPS: ${formatSpeed(sensorData.GpsSpeed)}`
        : 'No sensor data available';
}