Custom algorithms

Custom algorithms

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.

Programming language

Custom algorithms are written in JavaScript.

Existing custom algorithms from older versions of the software may be written in C#, but this is no longer recommended. The documentation for these legacy algorithms is available here.

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('25.5'); // v25.5.x 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';
}