Source: index.js

/**
 * RPi GPIO mock framework.
 * @module gpio-mock
 * @see module:gpio-mock
 * @author Oscar Djupfeldt
 */

/* jshint node: true */
'use strict';

let fs = require('fs');
let cept = require('cept');
let rimraf = require('rimraf');
let mkdirp = require('mkdirp');
let ds18b20 = require('./ds18b20');

var mockGPIOPath;

var ofs = {};

var stoppers = [];

let sysGPIOPath = '/sys/class/gpio';

var mockGPIOPath = './sys/class/gpio';

let replacePath = function(path) {
  if (path && typeof path !== 'string') {
    return path;
  }
  if (path && typeof path === 'string' && path.startsWith(sysGPIOPath)) {
    path = path.replace(sysGPIOPath, mockGPIOPath);
  } else if (path && typeof path === 'string' && path.startsWith(ds18b20.sysPath)) {
    path = path.replace(ds18b20.sysPath, ds18b20.mockPath);
  }
  return path;
};

let updatePaths = function(mockLocation, callback) {
  var prefix = mockLocation;
  if (mockLocation.endsWith('/')) {
    prefix = mockLocation.substring(0, mockLocation.length - 1);
  }
  mockGPIOPath = prefix + sysGPIOPath;
  createDirectories(mockGPIOPath, function() {
    ds18b20.mockPath = prefix + ds18b20.sysPath;
    createDirectories(ds18b20.mockPath, callback);
  });
};

let createDirectories = function(path, callback) {
  fs.stat(path, function (err, stats) {
    // Path does not exist
    if (err) {
      mkdirp(path, function(err) {
        callback(err);
      });
    }
    // Path is not directory
    else if (!stats.isDirectory()) {
      callback(new Error(path + ' exists and is not a directory!'));
    } else {
      callback();
    }
  });
};

let checkExport = function() {
  ofs.readFile('./sys/class/gpio/export', 'utf8', function(err, data) {
    if (!err && data && typeof parseInt(data) === 'number' && !ofs.existsSync('./sys/class/gpio/gpio' + data)) {
      fs.mkdirSync('./sys/class/gpio/gpio' + data);
      ofs.writeFileSync('./sys/class/gpio/gpio' + data + '/direction', 'in');
      ofs.writeFileSync('./sys/class/gpio/gpio' + data + '/value', '0');
      ofs.writeFileSync('./sys/class/gpio/export', '');
    }
  });
};

let checkUnexport = function() {
  ofs.readFile('./sys/class/gpio/unexport', 'utf8', function(err, data) {
    if (!err && data && typeof parseInt(data) === 'number' && ofs.existsSync('./sys/class/gpio/gpio' + data)) {
      rimraf('./sys/class/gpio/gpio' + data, function(err, data) {
        ofs.writeFileSync('./sys/class/gpio/unexport', '');
      });
    }
  });
};

let startWatcher = function() {
  setTimeout(function () {
    checkExport();
    checkUnexport();
    ds18b20.sensorFunction();
    ds18b20.sensorStatic();
    startWatcher();
  }, 500);
};

let mock = function() {
  let copy = function(index) {
    if (typeof fs[index] === 'function') {
      ofs[index] = fs[index];
    }
  };
  let replace = function(index) {
    if (typeof fs[index] === 'function') {
      ofs[index] = fs[index];
      stoppers.push(
        cept(fs, index, function() {
          var args = [];
          for (var i in arguments) {
            if (typeof arguments[i] === 'string') {
              args.push(replacePath(arguments[i]));
            } else {
              args.push(arguments[i]);
            }
          }
          return ofs[index].apply(this, args);
        })
      );
    }
  };

  for (var index in fs) {
    copy(index);
    replace(index);
  }

  ds18b20.mock();
};

/**
 * Initializes the framework. Calling this function will override any call to fs where the path starts with either
 * /sys/class/gpio or /sys/bus/w1/devices. By default the overrides are ./sys/class/gpio and ./sys/bus/w1/devices.
 *
 * @function start
 * @param {string} mockLocation If a value is passed, this is prepended to the real path to form the new mock path
 * @param {Function} callback Called when start is finished
 */
let start = function(mockLocation, callback) {
  if (typeof mockLocation === 'function') {
    callback = mockLocation;
    mockLocation = undefined;
  }
  if (!mockLocation) {
    mockLocation = '.';
  }
  updatePaths(mockLocation, function(err) {
    if (!err) {
      mock();
      startWatcher();
      callback();
    } else {
      console.error(err);
      callback(err);
    }
  });
};

/**
 * Stops the framework. Calling this function will reset fs to default.
 *
 * @function stop
 */
let stop = function() {
  for (var i in stoppers) {
    stoppers[i]();
  }
  stoppers = [];
  ds18b20.stop();
};

/**
 * Adds a new simulated DS18B20 digital thermometer.
 *
 * @function addDS18B20
 * @param {string} id The id of the thermometer, must be unique
 * @param {object} sensor Thermometer description
 * @param {string} sensor.behavior Defines how the thermometer behaves. Should be either 'static', 'external' or
 * 'function'. 'static' continuously resets the w1_slave file to indicate the temperature provided in sensor.temperature.
 * 'external' sets the initial value to the temperature provided in sensor.temperature, but makes no further changes to
 *  w1_slave, allowing for external programs to change the value.
 *  'function' executes the function provided in sensor.temperature continuously.
 * @param {string} sensor.temperature Defines the initial or static temperature, or a function returning the temperature.
 * Values should be 1000 * degrees celcius, no decimals. If sensor.behavior is set to 'function' a function must be
 * provided here, it should return a string representing the temperature, following the same pattern as if set to a fixed
 * value.
 */
let addDS18B20 = function(id, sensor, callback) {
  ds18b20.addDS18B20(id, sensor, callback);
};

let setDS18B20 = function(id, sensor, callback) {
  ds18b20.setDS18B20(id, sensor, callback);
};

module.exports = {
  start: start,
  stop: stop,
  addDS18B20: addDS18B20,
  setDS18B20: setDS18B20,
  ofs: ofs
};