From 3baeaa96aad19247d45be45bdedd2bca5f574efc Mon Sep 17 00:00:00 2001 From: Rick Waldron Date: Sun, 31 May 2015 19:01:42 -0400 Subject: [PATCH] Button: refactor to controller design. --- lib/button.js | 194 +++++++++++++++++++++++++--------------- test/button.js | 233 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 314 insertions(+), 113 deletions(-) diff --git a/lib/button.js b/lib/button.js index 0f7c5a655..ad1378cfc 100644 --- a/lib/button.js +++ b/lib/button.js @@ -12,6 +12,35 @@ var priv = new Map(), }; +var Controllers = { + DEFAULT: { + initialize: { + value: function(opts, dataHandler) { + + if (Pins.isFirmata(this) && typeof opts.pinValue === "string" && opts.pinValue[0] === "A") { + opts.pinValue = this.io.analogPins[+opts.pinValue.slice(1)]; + } + + this.pin = +opts.pinValue; + + this.io.pinMode(this.pin, this.io.MODES.INPUT); + + // Enable the pullup resistor after setting pin mode + if (this.pullup) { + this.io.digitalWrite(this.pin, this.io.HIGH); + } + + this.io.digitalRead(this.pin, dataHandler); + } + }, + toBoolean: { + value: function(raw) { + return raw === this.downValue; + } + } + } +}; + /** * Button * @constructor @@ -28,9 +57,19 @@ var priv = new Map(), */ function Button(opts) { - var timeout; + if (!(this instanceof Button)) { + return new Button(opts); + } + var pinValue; - var isFirmata; + var raw; + var invert = false; + var downValue = 1; + var upValue = 0; + var controller = null; + var state = { + timeout: null + }; // Create a 5 ms debounce boundary on event triggers // this avoids button events firing on @@ -41,97 +80,51 @@ function Button(opts) { }, this); }, 7); - if (!(this instanceof Button)) { - return new Button(opts); - } - pinValue = typeof opts === "object" ? opts.pin : opts; Board.Component.call( this, opts = Board.Options(opts) ); - isFirmata = Pins.isFirmata(this); + opts.pinValue = pinValue; - if (isFirmata && typeof pinValue === "string" && pinValue[0] === "A") { - pinValue = this.io.analogPins[+pinValue.slice(1)]; + if (opts.controller && typeof opts.controller === "string") { + controller = Controllers[opts.controller.toUpperCase()]; + } else { + controller = opts.controller; } - pinValue = +pinValue; - - // Set the pin to INPUT mode - this.mode = this.io.MODES.INPUT; - - // Option to enable the built-in pullup resistor - this.isPullup = opts.isPullup || false; - - if (isFirmata && !Number.isNaN(pinValue)) { - this.pin = pinValue; + if (controller == null) { + controller = Controllers.DEFAULT; } - this.io.pinMode(this.pin, this.mode); + Object.defineProperties(this, controller); - // Enable the pullup resistor after setting pin mode - if (this.isPullup) { - this.io.digitalWrite(this.pin, this.io.HIGH); - } + // `holdtime` is used by a timeout to determine + // if the button has been released within a specified + // time frame, in milliseconds. + this.holdtime = opts.holdtime || 500; + + // `opts.isPullup` is included as part of an effort to + // phase out "isFoo" options properties + this.pullup = opts.pullup || opts.isPullup || false; // Turns out some button circuits will send // 0 for up and 1 for down, and some the inverse, // so we can invert our function with this option. // Default to invert in pullup mode, but use opts.invert // if explicitly defined (even if false) - this.invert = typeof opts.invert !== "undefined" ? - opts.invert : (this.isPullup || false); + invert = typeof opts.invert !== "undefined" ? + opts.invert : (this.pullup || false); - this.downValue = this.invert ? 0 : 1; - this.upValue = this.invert ? 1 : 0; - - // Button instance properties - this.holdtime = opts && opts.holdtime || 500; + if (invert) { + downValue = downValue ^ 1; + upValue = upValue ^ 1; + } // Create a "state" entry for privately // storing the state of the button - priv.set(this, { - isDown: false - }); - - // Analog Read event loop - this.io.digitalRead(this.pin, function(data) { - var err = null; - - // data = upValue, this.isDown = true - // indicates that the button has been released - // after previously being pressed - if (data === this.upValue && this.isDown) { - if (timeout) { - clearTimeout(timeout); - } - priv.get(this).isDown = false; - - trigger.call(this, "up"); - } - - // data = downValue, this.isDown = false - // indicates that the button has been pressed - // after previously being released - if (data === this.downValue && !this.isDown) { - - // Update private data - priv.get(this).isDown = true; - - // Call debounced event trigger for given "key" - // This will trigger all event aliases assigned - // to "key" - trigger.call(this, "down" /* key */ ); - - timeout = setTimeout(function() { - if (this.isDown) { - this.emit("hold", err); - } - }.bind(this), this.holdtime); - } - }.bind(this)); + priv.set(this, state); Object.defineProperties(this, { value: { @@ -139,12 +132,69 @@ function Button(opts) { return Number(this.isDown); } }, + invert: { + get: function() { + return invert; + }, + set: function(value) { + invert = value; + downValue = invert ? 0 : 1; + upValue = invert ? 1 : 0; + } + }, + downValue: { + get: function() { + return downValue; + }, + set: function(value) { + downValue = value; + upValue = value ^ 1; + invert = value ? true : false; + } + }, + upValue: { + get: function() { + return upValue; + }, + set: function(value) { + upValue = value; + downValue = value ^ 1; + invert = value ? true : false; + } + }, isDown: { get: function() { - return priv.get(this).isDown; + return this.toBoolean(raw); } } }); + + if (typeof this.initialize === "function") { + this.initialize(opts, function(data) { + var err = null; + + // Update the raw data value, which + // is used by isDown = toBoolean() + raw = data; + + if (!this.isDown) { + if (state.timeout) { + clearTimeout(state.timeout); + } + trigger.call(this, "up"); + } + + if (this.isDown) { + trigger.call(this, "down"); + + state.timeout = setTimeout(function() { + if (this.isDown) { + this.emit("hold", err); + } + }.bind(this), this.holdtime); + } + }.bind(this)); + } } util.inherits(Button, events.EventEmitter); diff --git a/test/button.js b/test/button.js index 266c19b13..2bb0892d6 100644 --- a/test/button.js +++ b/test/button.js @@ -13,32 +13,32 @@ var board = new five.Board({ io.emit("ready"); +var proto = []; +var instance = [{ + name: "pullup" +}, { + name: "invert" +}, { + name: "downValue" +}, { + name: "upValue" +}, { + name: "holdtime" +}, { + name: "isDown" +}, { + name: "value" +}]; + + exports["Button, Digital Pin"] = { setUp: function(done) { - this.digitalRead = sinon.spy(board.io, "digitalRead"); + this.digitalRead = sinon.spy(MockFirmata.prototype, "digitalRead"); this.button = new Button({ pin: 8, board: board }); - this.proto = []; - - this.instance = [{ - name: "isPullup" - }, { - name: "invert" - }, { - name: "downValue" - }, { - name: "upValue" - }, { - name: "holdtime" - }, { - name: "isDown" - }, { - name: "value" - }]; - done(); }, @@ -48,13 +48,13 @@ exports["Button, Digital Pin"] = { }, shape: function(test) { - test.expect(this.proto.length + this.instance.length); + test.expect(proto.length + instance.length); - this.proto.forEach(function(method) { + proto.forEach(function(method) { test.equal(typeof this.button[method.name], "function"); }, this); - this.instance.forEach(function(property) { + instance.forEach(function(property) { test.notEqual(typeof this.button[property.name], "undefined"); }, this); @@ -102,36 +102,21 @@ exports["Button, Digital Pin"] = { clock.restore(); test.done(); }); + this.button.holdtime = 10; callback(this.button.downValue); - clock.tick(500); + clock.tick(11); callback(this.button.upValue); }, }; exports["Button, Analog Pin"] = { setUp: function(done) { - this.digitalRead = sinon.spy(board.io, "digitalRead"); + this.digitalRead = sinon.spy(MockFirmata.prototype, "digitalRead"); this.button = new Button({ pin: "A0", board: board }); - this.proto = []; - - this.instance = [{ - name: "isPullup" - }, { - name: "invert" - }, { - name: "downValue" - }, { - name: "upValue" - }, { - name: "holdtime" - }, { - name: "isDown" - }]; - done(); }, @@ -184,8 +169,174 @@ exports["Button, Analog Pin"] = { clock.restore(); test.done(); }); + + this.button.holdtime = 10; + callback(this.button.downValue); + clock.tick(11); + callback(this.button.upValue); + }, +}; + +exports["Button, Value Inversion"] = { + setUp: function(done) { + this.digitalRead = sinon.spy(MockFirmata.prototype, "digitalRead"); + this.button = new Button({ + pin: 8, + board: board + }); + + + done(); + }, + + tearDown: function(done) { + this.digitalRead.restore(); + done(); + }, + + initialInversion: function(test) { + test.expect(6); + + this.button = new Button({ + pin: 8, + invert: true, + board: board + }); + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + this.button.downValue = 1; + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + this.button.upValue = 1; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + test.done(); + }, + + pullupInversion: function(test) { + test.expect(6); + + this.button = new Button({ + pin: 8, + pullup: true, + board: board + }); + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + this.button.downValue = 1; + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + this.button.upValue = 1; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + test.done(); + }, + + inlineInversion: function(test) { + test.expect(14); + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + this.button.upValue = 1; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + this.button.upValue = 0; + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + this.button.downValue = 0; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + this.button.downValue = 1; + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + this.button.invert = true; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + this.button.invert = false; + + test.equal(this.button.downValue, 1); + test.equal(this.button.upValue, 0); + + test.done(); + }, + + downInversion: function(test) { + + var callback = this.digitalRead.args[0][1]; + test.expect(3); + + //fake timers dont play nice with __.debounce + this.button.on("down", function() { + + test.ok(true); + test.done(); + }); + + this.button.downValue = 0; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + callback(this.button.downValue); + }, + + upInversion: function(test) { + + var callback = this.digitalRead.args[0][1]; + test.expect(3); + + this.button.on("up", function() { + test.ok(true); + test.done(); + }); + + this.button.upValue = 1; + + test.equal(this.button.downValue, 0); + test.equal(this.button.upValue, 1); + + callback(this.button.upValue); + }, + + holdInversion: function(test) { + var clock = sinon.useFakeTimers(); + var callback = this.digitalRead.args[0][1]; + test.expect(1); + + //fake timers dont play nice with __.debounce + this.button.on("hold", function() { + test.ok(true); + clock.restore(); + test.done(); + }); + + this.button.holdtime = 10; + this.button.downValue = 0; callback(this.button.downValue); - clock.tick(500); + clock.tick(11); callback(this.button.upValue); }, };