diff --git a/.gitignore b/.gitignore index c2658d7..397e836 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules/ +/node_modules +/test/data diff --git a/README.md b/README.md index 56eadae..70f28b8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,11 @@ var gpio4 = gpio.export(4, { // read or write to the header right away. Place your logic in this ready // function to guarantee everything will get fired properly ready: function() { - } + }, + + // To be able to test gpio-dependent code, you can set the gpio base path. + // This way you can modify the gpio value in your test code. + gpioBasePath: '/sys/class/gpio/' }); ``` diff --git a/lib/gpio.js b/lib/gpio.js index 20f24f5..cf32ccd 100644 --- a/lib/gpio.js +++ b/lib/gpio.js @@ -1,93 +1,109 @@ -var fs = require('fs'); -var util = require('util'); -var path = require('path'); -var EventEmitter = require('events').EventEmitter; -var exists = fs.exists || path.exists; - -var gpiopath = '/sys/class/gpio/'; - -var logError = function(e) { if(e) console.log(e.code, e.action, e.path); }; -var logMessage = function() { if (exports.logging) console.log.apply(console, arguments); }; - -var _write = function(str, file, fn, override) { - if(typeof fn !== "function") fn = logError; - fs.writeFile(file, str, function(err) { - if(err && !override) { - err.path = file; - err.action = 'write'; - logError(err); - } else { - if(typeof fn === "function") fn(); - } - }); +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const EventEmitter = require('events').EventEmitter; + +const defaultGpioBasePath = '/sys/class/gpio/'; + +function logError(e) { + if (e) console.log(e.code, e.action, e.path); +} + +function logMessage() { + if (exports.logging) console.log.apply(console, arguments); +} + +var _write = function (str, file, fn, override) { + if (typeof fn !== "function") fn = logError; + fs.writeFile(file, str, function (err) { + if (err && !override) { + err.path = file; + err.action = 'write'; + logError(err); + } else { + if (typeof fn === "function") fn(); + } + }); }; -var _read = function(file, fn) { - fs.readFile(file, "utf-8", function(err, data) { - if(err) { - err.path = file; - err.action = 'read'; - logError(err); - } else { - if(typeof fn === "function") fn(data); - else logMessage("value: ", data); - } - }); +var _read = function (file, fn) { + fs.readFile(file, "utf-8", function (err, data) { + if (err) { + err.path = file; + err.action = 'read'; + logError(err); + } else { + if (typeof fn === "function") fn(data); + else logMessage("value: ", data); + } + }); }; -var _unexport = function(number, fn) { - _write(number, gpiopath + 'unexport', function(err) { - if(err) return logError(err); - if(typeof fn === 'function') fn(); - }, 1); +var _unexport = function (_gpioBasePath, number, fn) { + _write(number, path.join(_gpioBasePath, 'unexport'), function (err) { + if (err) return logError(err); + if (typeof fn === 'function') fn(); + }, 1); }; -var _export = function(n, fn) { - if(exists(gpiopath + 'gpio'+n)) { - // already exported, unexport and export again - logMessage('Header already exported'); - _unexport(n, function() { _export(n, fn); }); - } else { - logMessage('Exporting gpio' + n); - _write(n, gpiopath + 'export', function(err) { - // if there's an error when exporting, unexport and repeat - if(err) _unexport(n, function() { _export(n, fn); }); - else if(typeof fn === 'function') fn(); - }, 1); - } +var _export = function (_gpioBasePath, n, fn) { + if (fs.existsSync(path.join(_gpioBasePath, 'gpio' + n))) { + // already exported, unexport and export again + logMessage('Header already exported'); + _unexport(_gpioBasePath, n, function () { + _export(_gpioBasePath, n, fn); + }); + } else { + logMessage('Exporting gpio' + n); + _write(n, path.join(_gpioBasePath, 'export'), function (err) { + // if there's an error when exporting, unexport and repeat + if (err) _unexport(_gpioBasePath, n, function () { + _export(_gpioBasePath, n, fn); + }); + else if (typeof fn === 'function') fn(); + }, 1); + } }; -var _testwrite = function(file, fn) { - fs.open(file, 'w', function(err, fd) { - if (err) { - fn(false, err); - return; - } - fs.close(fd, function(err){ - fn(true, null); - }); - }); +var _testwrite = function (file, fn) { + fs.open(file, 'w', function (err, fd) { + if (err) { + fn(false, err); + return; + } + fs.close(fd, function (err) { + fn(true, null); + }); + }); }; // fs.watch doesn't get fired because the file never // gets 'accessed' when setting header via hardware // manually watching value changes -var FileWatcher = function(path, interval, fn) { - if(typeof fn === 'undefined') { - fn = interval; - interval = 100; - } - if(typeof interval !== 'number') return false; - if(typeof fn !== 'function') return false; - - var value; - var readTimer = setInterval(function() { - _read(path, function(val) { - if(value !== val) { - if(typeof value !== 'undefined') fn(val); - value = val; - } - }); - }, interval); - - this.stop = function() { clearInterval(readTimer); }; +var FileWatcher = function (path, interval, fn, readyCallback) { + if (typeof fn === 'undefined') { + fn = interval; + interval = 100; + } + if (typeof interval !== 'number') return false; + if (typeof fn !== 'function') return false; + + var value; + function watchForChange() { + _read(path, function (val) { + if (value !== val) { + if (typeof value === 'undefined') { + readyCallback(); + } else { + fn(val); + } + value = val; + } + }); + } + watchForChange(); + var readTimer = setInterval(watchForChange, interval); + + this.stop = function () { + clearInterval(readTimer); + }; }; @@ -97,6 +113,7 @@ var GPIO = function(headerNum, opts) { var self = this; var dir = opts.direction; var interval = opts.interval; + this.gpioBasePath = opts.gpioBasePath || defaultGpioBasePath; if(typeof interval !== 'number') interval = 100; this.interval = interval; @@ -104,22 +121,22 @@ var GPIO = function(headerNum, opts) { this.value = 0; this.PATH = {}; - this.PATH.PIN = gpiopath + 'gpio' + headerNum + '/'; - this.PATH.VALUE = this.PATH.PIN + 'value'; - this.PATH.DIRECTION = this.PATH.PIN + 'direction'; + this.PATH.PIN = path.join(this.gpioBasePath, 'gpio' + headerNum); + this.PATH.VALUE = path.join(this.PATH.PIN, 'value'); + this.PATH.DIRECTION = path.join(this.PATH.PIN, 'direction'); this.export(function() { var onSuccess = function() { self.setDirection(dir, function () { - if ( dir === "out") { - if(typeof opts.ready === 'function') opts.ready.call(self); - } else { - self.value = undefined; - self._get(function(val) { - self.value = val; - if (typeof opts.ready === 'function') opts.ready.call(self); - }); - } + if (dir === "out") { + if (typeof opts.ready === 'function') opts.ready.call(self); + } else { + self.value = undefined; + self._get(function(val) { + self.value = val; + if (typeof opts.ready === 'function') opts.ready.call(self); + }); + } }); }; var attempts = 0; @@ -149,107 +166,117 @@ util.inherits(GPIO, EventEmitter); /** * Export and unexport gpio#, takes callback which fires when operation is completed */ -GPIO.prototype.export = function(fn) { _export(this.headerNum, fn); }; -GPIO.prototype.unexport = function(fn) { - if(this.valueWatcher) this.valueWatcher.stop(); - _unexport(this.headerNum, fn); +GPIO.prototype.export = function (fn) { + _export(this.gpioBasePath, this.headerNum, fn); +}; +GPIO.prototype.unexport = function (fn) { + if (this.valueWatcher) this.valueWatcher.stop(); + _unexport(this.gpioBasePath, this.headerNum, fn); }; /** * Sets direction, default is "out" */ -GPIO.prototype.setDirection = function(dir, fn) { - var self = this, path = this.PATH.DIRECTION; - if(typeof dir !== "string" || dir !== "in") dir = "out"; - this.direction = dir; - - logMessage('Setting direction "' + dir + '" on gpio' + this.headerNum); - - function watch () { - if(dir === 'in') { - if (!self.valueWatcher) { - // watch for value changes only for direction "in" - // since we manually trigger event for "out" direction when setting value - self.valueWatcher = new FileWatcher(self.PATH.VALUE, self.interval, function(val) { - val = parseInt(val, 10); - self.value = val; - self.emit("valueChange", val); - self.emit("change", val); - }); - } - } else { - // if direction is "out", try to clear the valueWatcher - if(self.valueWatcher) { - self.valueWatcher.stop(); - self.valueWatcher = null; - } - } - } - _read(path, function(currDir) { - var changedDirection = false; - if(currDir.indexOf(dir) !== -1) { - logMessage('Current direction is already ' + dir); - logMessage('Attempting to set direction anyway.'); - } else { - changedDirection = true; - } - _write(dir, path, function() { - watch(); - - if(typeof fn === 'function') fn(); - if (changedDirection) { - self.emit('directionChange', dir); - } - }, 1); - - }); +GPIO.prototype.setDirection = function (dir, fn) { + var self = this, path = this.PATH.DIRECTION; + if (typeof dir !== "string" || dir !== "in") dir = "out"; + this.direction = dir; + + logMessage('Setting direction "' + dir + '" on gpio' + this.headerNum); + + function watch(watchFinished) { + if (dir === 'in') { + if (!self.valueWatcher) { + // watch for value changes only for direction "in" + // since we manually trigger event for "out" direction when setting value + self.valueWatcher = new FileWatcher(self.PATH.VALUE, self.interval, function (val) { + val = parseInt(val, 10); + self.value = val; + self.emit("valueChange", val); + self.emit("change", val); + }, watchFinished); + } else { + watchFinished(); + } + } else { + // if direction is "out", try to clear the valueWatcher + if (self.valueWatcher) { + self.valueWatcher.stop(); + self.valueWatcher = null; + } + watchFinished(); + } + } + + _read(path, function (currDir) { + var changedDirection = false; + if (currDir.indexOf(dir) !== -1) { + logMessage('Current direction is already ' + dir); + logMessage('Attempting to set direction anyway.'); + } else { + changedDirection = true; + } + _write(dir, path, function () { + watch(function() { + if (typeof fn === 'function') fn(); + if (changedDirection) { + self.emit('directionChange', dir); + } + }); + }, 1); + + }); }; /** * Internal getter, stores value */ -GPIO.prototype._get = function(fn) { - var self = this, currVal = this.value; - - if(this.direction === 'out') return currVal; - - _read(this.PATH.VALUE, function(val) { - val = parseInt(val, 10); - if(val !== currVal) { - self.value = val; - if(typeof fn === "function") fn.call(this, self.value); - } - }); +GPIO.prototype._get = function (fn) { + var self = this, currVal = this.value; + + if (this.direction === 'out') return currVal; + + _read(this.PATH.VALUE, function (val) { + val = parseInt(val, 10); + if (val !== currVal) { + self.value = val; + if (typeof fn === "function") fn.call(this, self.value); + } + }); }; /** * Sets the value. If v is specified as 0 or '0', reset will be called */ -GPIO.prototype.set = function(v, fn) { - var self = this; - var callback = typeof v === 'function' ? v : fn; - if (typeof v === "boolean" || ) v = v ? 1 : 0; - if (typeof v !== "number" || v !== 0) v = 1; - - // if direction is out, just emit change event since we can reliably predict - // if the value has changed; we don't have to rely on watching a file - if(this.direction === 'out') { - if(this.value !== v) { - _write(v, this.PATH.VALUE, function() { - self.value = v; - self.emit('valueChange', v); - self.emit('change', v); - if(typeof callback === 'function') callback(self.value, true); - }); - } else { - if(typeof callback === 'function') callback(this.value, false); - } - } +GPIO.prototype.set = function (v, fn) { + var self = this; + var callback = typeof v === 'function' ? v : fn; + if (typeof v === "boolean") v = v ? 1 : 0; + if (typeof v !== "number" || v !== 0) v = 1; + + // if direction is out, just emit change event since we can reliably predict + // if the value has changed; we don't have to rely on watching a file + if (this.direction === 'out') { + if (this.value !== v) { + _write(v, this.PATH.VALUE, function () { + self.value = v; + self.emit('valueChange', v); + self.emit('change', v); + if (typeof callback === 'function') callback(self.value, true); + }); + } else { + if (typeof callback === 'function') callback(this.value, false); + } + } +}; +GPIO.prototype.reset = function (fn) { + this.set(0, fn); }; -GPIO.prototype.reset = function(fn) { this.set(0, fn); }; exports.logging = false; -exports.export = function(headerNum, direction) { return new GPIO(headerNum, direction); }; +exports.export = function (headerNum, direction) { + return new GPIO(headerNum, direction); +}; exports.unexport = _unexport; diff --git a/package.json b/package.json index 454628e..6649214 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,28 @@ { - "name": "gpio", - "version": "0.2.7", - "author": { - "name": "Dominick Pham", - "email": "dominick@dph.am", - "url": "http://dph.am" - }, - "description": "Talk to your Raspberry PI's general purpose inputs and outputs", - "keywords": ["gpio", "raspberry", "pi"], - "main": "./lib/gpio.js", - "repository": "git://github.com/EnotionZ/GpiO.git", - "devDependencies": { - "sinon": "*" - }, - "licenses": [ - { - "type": "MIT", - "url": "https://raw.github.com/EnotionZ/GpiO/master/LICENSE" - } - ] + "name": "gpio", + "version": "0.4.0", + "author": { + "name": "Sebastian Alkemade", + "email": "sa@linetco.com" + }, + "description": "Talk to your Raspberry PI's general purpose inputs and outputs", + "keywords": [ + "gpio", + "raspberry", + "pi" + ], + "main": "./lib/gpio.js", + "repository": "git://github.com/sa-linetco/GpiO.git", + "devDependencies": { + "assert": "^1.4.1", + "fs-extra": "^2.0.0", + "mocha": "^5.2.0", + "sinon": "^6.0.0" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.github.com/sa-linetco/GpiO/master/LICENSE" + } + ] } - diff --git a/test/tests.js b/test/tests.js index 4d3b02d..8006906 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,135 +1,151 @@ -var fs = require('fs'); +var fs = require('fs-extra'); var assert = require('assert'); var sinon = require('sinon'); + +var path = require('path'); + var gpio = require('../lib/gpio'); function read(file, fn) { - fs.readFile(file, "utf-8", function(err, data) { - if(!err && typeof fn === "function") fn(data); - }); + fs.readFile(file, "utf-8", function (err, data) { + if (!err && typeof fn === "function") fn(data); + }); } // remove whitespace function rmws(str) { - return str.replace(/\s+/g, ''); + return str.replace(/\s+/g, ''); } -describe('GPIO', function() { - - var gpio4; - - before(function(done) { - gpio4 = gpio.export(4, { - direction: 'out', - ready: done - }); - }); - - after(function() { - gpio4.unexport(); - }); - - describe('Header Direction Out', function() { - - describe('initializing', function() { - it('should open specified header', function(done) { - read('/sys/class/gpio/gpio4/direction', function(val) { - assert.equal(rmws(val), 'out'); - done(); - }); - }); - }); - - describe('#set', function() { - it('should set header value to high', function(done) { - gpio4.set(function() { - read('/sys/class/gpio/gpio4/value', function(val) { - assert.equal(rmws(val), '1'); - done(); - }); - }); - }); - }); - - describe('#reset', function() { - it('should set header value to low', function(done) { - gpio4.reset(function() { - read('/sys/class/gpio/gpio4/value', function(val) { - assert.equal(rmws(val), '0'); - done(); - }); - }); - }); - }); - - describe('#on :change', function() { - it('should fire callback when value changes', function(done) { - var callback = sinon.spy(); - gpio4.on('change', callback); - - // set, then reset - gpio4.set(function() { gpio4.reset(); }); - - // set and reset is async, wait some time before running assertions - setTimeout(function() { - assert.ok(callback.calledTwice); - done(); - gpio4.removeListener('change', callback); - }, 10); - }); - }); - }); - - // For these tests, make sure header 4 is connected to header 25 - // header 25 is exported with direction "out" and header 4 is used - // to simulate a hardware interrupt - describe('Header Direction In', function() { - - var gpio25; - - before(function(done) { - gpio25 = gpio.export(25, { - direction: 'in', - ready: done - }); - }); - after(function() { - gpio25.unexport(); - }); - - describe('#on :change', function() { - it('should respond to hardware set', function(done) { - var callback = sinon.spy(); - gpio25.on('change', callback); - - // wait a little before setting - setTimeout(function() { gpio4.set(); }, 500); - - // filewatcher has default interval of 100ms - setTimeout(function() { - assert.equal(gpio25.value, 1); - assert.ok(callback.calledOnce); - gpio25.removeListener('change', callback); - done(); - }, 600); - }); - - it('should respond to hardware reset', function(done) { - var callback = sinon.spy(); - gpio25.on('change', callback); - - // wait a little before setting - setTimeout(function() { gpio4.reset(); }, 500); - - // filewatcher has default interval of 100ms - setTimeout(function() { - assert.equal(gpio25.value, 0); - assert.ok(callback.calledOnce); - gpio25.removeListener('change', callback); - done(); - }, 600); - }); - }); - }); - +const GPIO_BASE_PATH = path.join(__dirname, 'data'); +const GPIO4_BASE_PATH = path.join(GPIO_BASE_PATH, 'gpio4'); + +describe('GPIO', function () { + + var gpio4; + + before(function (done) { + gpio4 = gpio.export(4, { + direction: 'out', + ready: done, + gpioBasePath: GPIO_BASE_PATH + }); + fs.ensureFileSync(path.join(GPIO4_BASE_PATH, 'value')); + fs.ensureFileSync(path.join(GPIO4_BASE_PATH, 'direction')); + }); + + after(function () { + gpio4.unexport(); + fs.removeSync(GPIO_BASE_PATH); + }); + + describe('Header Direction Out', function () { + + describe('initializing', function () { + it('should open specified header', function (done) { + read(path.join(GPIO4_BASE_PATH, 'direction'), function (val) { + assert.equal(rmws(val), 'out'); + done(); + }); + }); + }); + + describe('#set', function () { + it('should set header value to high', function (done) { + gpio4.set(function () { + read(path.join(GPIO4_BASE_PATH, 'value'), function (val) { + assert.equal(rmws(val), '1'); + done(); + }); + }); + }); + }); + + describe('#reset', function () { + it('should set header value to low', function (done) { + gpio4.reset(function () { + read(path.join(GPIO4_BASE_PATH, 'value'), function (val) { + assert.equal(rmws(val), '0'); + done(); + }); + }); + }); + }); + + describe('#on :change', function () { + it('should fire callback when value changes', function (done) { + var callback = sinon.spy(); + gpio4.on('change', callback); + + // set, then reset + gpio4.set(function () { + gpio4.reset(); + }); + + // set and reset is async, wait some time before running assertions + setTimeout(function () { + assert.ok(callback.calledTwice); + done(); + gpio4.removeListener('change', callback); + }, 10); + }); + }); + }); + + // For these tests, make sure header 4 is connected to header 25 + // header 25 is exported with direction "out" and header 4 is used + // to simulate a hardware interrupt + describe('Header Direction In', function () { + + var gpio25; + + before(function (done) { + gpio25 = gpio.export(25, { + direction: 'in', + ready: done + }); + }); + after(function () { + gpio25.unexport(); + }); + + describe('#on :change', function () { + it('should respond to hardware set', function (done) { + var callback = sinon.spy(); + gpio25.on('change', callback); + + // wait a little before setting + setTimeout(function () { + gpio4.set(); + }, 500); + + // filewatcher has default interval of 100ms + setTimeout(function () { + assert.equal(gpio25.value, 1); + assert.ok(callback.calledOnce); + gpio25.removeListener('change', callback); + done(); + }, 600); + }); + + it('should respond to hardware reset', function (done) { + var callback = sinon.spy(); + gpio25.on('change', callback); + + // wait a little before setting + setTimeout(function () { + gpio4.reset(); + }, 500); + + // filewatcher has default interval of 100ms + setTimeout(function () { + assert.equal(gpio25.value, 0); + assert.ok(callback.calledOnce); + gpio25.removeListener('change', callback); + done(); + }, 600); + }); + }); + }); + });