Custom algorithms (legacy)

Custom algorithms (legacy)

NOTE: This document describes the older (outdated) version of custom algorithms. Click here for the currently recommended documentation.

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 the C# programming language (opens in a new tab)

Algorithm structure

All algorithms must implement the following interface:

interface ISensorAlgorithm
{
    public SensorAlgorithmOutput Apply(SensorData sensorData);
}

They take the following structure as an input:

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; }
}

They return the following structure as an output:

class SensorAlgorithmOutput
{
    public MotionState MotionState { get; }
    public SensorState SensorState { get; }
    public string SensorType { get; }
    public string SensorDescription { get; }
 
    public SensorAlgorithmOutput(MotionState motionState, SensorState sensorState, string sensorType, string sensorDescription)
    {
        MotionState = motionState;
        SensorState = sensorState;
        SensorType = sensorType;
        SensorDescription = sensorDescription;
    }
}

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; }
}

Thus, the basic structure of a custom algorithm is as per the following (very simple) example:

class CustomAlgorithm : ISensorAlgorithm
{
    public SensorAlgorithmOutput Apply(SensorData sensorData)
    {
        var sensorState = sensorData.GpsState;
        var motionState = MotionState.Stationary;
        if (sensorData.GpsState is SensorState.Active && sensorData.GpsSpeed.KilometresPerHour > 5)
        {
            motionState = MotionState.Moving;
        }
 
        return new SensorAlgorithmOutput(motionState, sensorState, "GPS", description);
    }
}

Here is a more complex example:

class CustomAlgorithm : ISensorAlgorithm
{
    private static bool IsMetric { get; } = RegionInfo.CurrentRegion.IsMetric;
 
    private readonly SlidingWindow _accelerometerDeltaSlidingWindow = new SlidingWindow(10);
 
    public SensorAlgorithmOutput Apply(SensorData sensorData)
    {
        var sources = new List<string>();
        var descriptions = new List<string>();
 
        var gpsMotionState = MotionState.Unknown;
        var obdMotionState = MotionState.Unknown;
        var bld205MotionState = MotionState.Unknown;
        var accelerometerMotionState = MotionState.Unknown;
 
        if (sensorData.GpsState is SensorState.Active)
        {
            gpsMotionState = sensorData.GpsSpeed.KilometresPerHour > 5
                ? MotionState.Moving
                : MotionState.Stationary;
 
            sources.Add("GPS");
            descriptions.Add($"{sensorData.GpsSource} ({GetSpeedDescription(sensorData.GpsSpeed)})");
        }
 
        if (sensorData.Obd2State is SensorState.Active)
        {
            obdMotionState = sensorData.Obd2Speed.KilometresPerHour > 5
                ? MotionState.Moving
                : MotionState.Stationary;
 
            sources.Add("OBD-II");
            descriptions.Add($"{sensorData.Obd2Source} ({GetSpeedDescription(sensorData.Obd2Speed)})");
        }
 
        if (sensorData.BL205State is SensorState.Active)
        {
            bld205MotionState = sensorData.BL205Vibration > 50
                ? MotionState.Moving
                : MotionState.Stationary;
 
            sources.Add("BL205");
            descriptions.Add($"{sensorData.BL205Source} ({sensorData.BL205Vibration})");
        }
 
        if (sensorData.AccelerometerState is SensorState.Active)
        {
            var rms = MathHelper.RootMeanSquare(sensorData.AccelerometerX, sensorData.AccelerometerY, sensorData.AccelerometerZ);
            var rmsMg = rms * 1000;
 
            _accelerometerDeltaSlidingWindow.Add(rmsMg);
 
            var rmsDelta = _accelerometerDeltaSlidingWindow.Delta();
 
            accelerometerMotionState = rmsDelta > 80
                ? MotionState.Moving
                : MotionState.Stationary;
 
            sources.Add("Accelerometer");
            descriptions.Add($"{sensorData.AccelerometerSource} ({rmsDelta:N0}mg)");
        }
 
        var type = sources.Any()
          ? $"Custom sensor (sources: {string.Join(" | ", sources)})"
          : "Custom sensor (no sources present)";
 
        var description = descriptions.Any()
          ? string.Join(" | ", descriptions)
          : "No data available";
 
        var combinedSensorState = SensorStateHelper.CombineSensorStates(
            sensorData.BL205State,
            sensorData.GpsState,
            sensorData.Obd2State,
            sensorData.AccelerometerState);
 
        var combinedMotionState = MotionStateHelper.CombineMotionStatesByPriority(
            gpsMotionState,
            obdMotionState,
            bld205MotionState,
            accelerometerMotionState);
 
        return new SensorAlgorithmOutput(combinedMotionState, combinedSensorState, type, description);
    }
 
    private static string GetSpeedDescription(Speed speed)
    {
        return IsMetric
            ? $"{speed.KilometresPerHour:N2} km/h"
            : $"{speed.MilesPerHour:N2} mph";
    }
 
    private sealed class SlidingWindow
    {
        private readonly int _size;
        private readonly Queue<double> _queue;
 
        public SlidingWindow(int size)
        {
            _size = size;
            _queue = new Queue<double>(_size);
        }
 
        public void Add(double value)
        {
            if (_queue.Count >= _size)
            {
                _queue.Dequeue();
            }
 
            _queue.Enqueue(value);
        }
 
        public double Delta()
        {
            return _queue.Max() - _queue.Min();
        }
    }
}