Advanced IT info

Advanced IT information

This section provides information to IT teams who are supporting Blank-it within their organization.

File system

Blank-it stores files in three main directories.

  • The baseline configuration directory. This directory should be read-only. It stores some baseline configuration file that should never be able to be edited by unprivileged users.
  • The shared data directory. Users should have read and write access. It stores several transient data files and configuration files.
  • The logs directory. Users should have read and write access. It stores the application log files.

By default, these directories are as follows:

DirectoryDirectory pathPermissions
Baseline configurationC:\ProgramData\Blank-itRead-only
Shared dataC:\Users\Public\Documents\Blank-it\DataRead + Write
LogsC:\Users\Public\Documents\Blank-it\LogsRead + Write

Firewall configuration

The Blank-it software will occasionally make web requests to the Blank-it servers. It is recommended that an organization allows the following traffic through its firewall:

HostnamePortComments
gateway.blank-it.com443Required for general operation of the Blank-it software.
portal.blank-it.com443Required to allow administrators to use the built-in configuration manager.

Distributing a configuration to a fleet of PCs

In order to accommodate the widest variety of organizational software deployment tools, Blank-it uses the simplest possible configuration deployment strategy. To roll out a new Blank-it configuration, all an organization has to do is overwrite the existing Blank-it configuration file with a new Blank-it configuration file. This can be done while the Blank-it software is running; Blank-it will continue to run as it was, the new configuration will be applied next time the Blank-it software starts.

Advanced installation options

Blank-it has a number of advanced installation options that can be used by passing custom arguments to msiexec during the installation process.

ArgumentDescription
BASE_DIRThe path to use as the baseline configuration directory (described above).
SHARED_DIRThe path to use as the shared configuration directory (described above).
LOGS_DIRThe path to use as the logs directory (described above).
CONFIG_PATHThe path of a config file to be automatically deployed to the relevant locations.
KEEP_LOGS(Uninstall only) If set to true, retain the log files when uninstalling.

In addition, Blank-it can be installed in 'quiet' mode by passing the /qn flag to msiexec.

Here is an example installation command that will install Blank-it in quiet mode, with a custom directory structure, and automatic deployment of a configuration file:

msiexec.exe /i blankit.msi /qn BASE_DIR="C:\BL_BASE" SHARED_DIR="C:\BL_SHARED" LOGS_DIR="C:\BL_LOGS" CONFIG_PATH="config.bl6"

Here is an example uninstallation command that will uninstall Blank-it in quiet mode, but retain the log files.

msiexec.exe /x blankit.msi /qn KEEP_LOGS=TRUE

Custom algorithms

Blank-it supports custom algorithms, allowing IT administrators 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 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; }
}

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,
}
 
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();
        }
    }
}

Algorithm encoding

Once an algorithm has been designed, it must be encoded as UTF-8 Base64 or UTF-8 Base32 (Crockman) before being saved to the Blank-it Portal.

The custom algorithms generated by the Blank-it Assistant tool will automatically be encoded in this manner. Likewise, algorithms created or saved via the Blank-it Assistant tool's Algorithm Editor will also automatically be encoded in this manner.