From 5107b59b98e9e1c64efcfdc315bad650f2f3d9f3 Mon Sep 17 00:00:00 2001 From: Ovis Maximus Date: Sat, 26 Dec 2015 11:58:48 +0100 Subject: [PATCH 1/3] implementation of remote controllable GPIO pins --- .gitignore | 2 + .travis.yml | 10 +++++ CHANGELOG.md | 1 + README.md | 29 +++++++++++++-- app.js | 36 ++++++++++++++++-- example_configs/config.json | 18 ++++++++- lib/gpio.js | 66 +++++++++++++++++++++++++++++++++ lib/macros.js | 5 +++ package.json | 3 +- static/css/app.less | 21 ++++++++++- static/css/compiled/app.css | 12 +++++- static/js/app/core.js | 50 +++++++++++++++++++++++-- static/js/compiled/app.js | 2 +- templates/index.swig | 7 ++++ test/fixtures/config.json | 20 +++++++++- test/lib/gpio.js | 74 +++++++++++++++++++++++++++++++++++++ test/lib/macros.js | 16 +++++++- test/lib/wiring-pi-mock.js | 20 ++++++++++ test/lirc_web.js | 39 ++++++++++++++++++- 19 files changed, 411 insertions(+), 20 deletions(-) create mode 100644 lib/gpio.js create mode 100644 test/lib/gpio.js create mode 100644 test/lib/wiring-pi-mock.js diff --git a/.gitignore b/.gitignore index 9f9d0a8..1c88d31 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ node_modules/ .settings/ *.swp npm-debug.log +server.cert +server.key diff --git a/.travis.yml b/.travis.yml index 971e9e6..32ca878 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,16 @@ language: node_js +sudo: required node_js: - "node" +env: + - CXX=g++-4.9 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.9 + - gcc-4.9 script: - npm run lint-js - npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b99a21..7596396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. As of `v0.1.0`, this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +* Adds support for gpio controlled devices (thanks @OvisMaximus) ## [0.2.4] - 2016-01-13 diff --git a/README.md b/README.md index d3aad5b..ad8a935 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You'll need to have [LIRC](http://lirc.org) installed and configured on your mac npm install -g lirc_web lirc_web -Note that you may need to run the `npm install` command with `sudo`. +Note that you probably have to run the `npm install -g` command with `sudo`. ### Viewing @@ -36,11 +36,12 @@ You may place this configuration file in one of two locations and `lirc_web` wil These are the available configuration options: 1. ``repeaters`` - buttons that repeatedly send their commands while pressed. A common example are the volume buttons on most remote controls. While you hold the volume buttons down, the remote will repeatedly send the volume command to your device. -2. ``macros`` - a collection of commands that should be executed one after another. This allows you to automate actions like "Play Xbox 360" or "Listen to music via AirPlay". Each step in a macro is described in the format ``[ "REMOTE", "COMMAND" ]``, where ``REMOTE`` and ``COMMAND`` are defined by what you have programmed into LIRC. You can add delays between steps of macros in the format of ``[ "delay", 500 ]``. Note that the delay is measured in milliseconds so 1000 milliseconds = 1 second. +2. ``macros`` - a collection of commands that should be executed one after another. This allows you to automate actions like "Play Xbox 360" or "Listen to music via AirPlay". Each step in a macro is described in the format ``[ "REMOTE", "COMMAND" ]``, where ``REMOTE`` and ``COMMAND`` are defined by what you have programmed into LIRC. You can add delays between steps of macros in the format of ``[ "delay", 500 ]``. Note that the delay is measured in milliseconds so 1000 milliseconds = 1 second. You can switch GPIO pins in the format of ``[ "gpio", "Receiver", 0 ] `` to set the gpio pin named "Receiver" to off. 3. ``commandLabels`` - a way to rename commands that LIRC understands (``KEY_POWER``, ``KEY_VOLUMEUP``) with labels that humans prefer (``Power``, ``Volume Up``). 4. ``remoteLabels`` - a way to rename the remotes that LIRC understands (``XBOX360``) with labels that humans prefer (``Xbox 360``). 5. ``blacklists`` - a way to hide unused commands from your remotes. 6. ``server`` - server configuration settings (ports, [SSL](http://serverfault.com/a/366374)). +7. ``gpios`` - configure gpio pins that drive some hardware like a relais switching the power supply of a device. #### Example config.json: @@ -62,6 +63,9 @@ These are the available configuration options: }, "macros": { "Play Xbox 360": [ + [ "gpio", "TV", 1], + [ "gpio", "Receiver", 1], + [ "gpio", "Xbox", 1], [ "SonyTV", "Power" ], [ "delay", 500 ], [ "SonyTV", "Xbox360" ], @@ -71,10 +75,16 @@ These are the available configuration options: [ "Xbox360", "Power" ] ], "Listen to Music": [ + [ "gpio", "Receiver", 1], [ "Yamaha", "Power" ], [ "delay", 500 ], [ "Yamaha", "AirPlay" ] - ] + ], + "all off": [ + [ "gpio", "TV", 0], + [ "gpio", "Receiver", 0], + [ "gpio", "Xbox", 0] + ], }, "commandLabels": { "Yamaha": { @@ -92,7 +102,15 @@ These are the available configuration options: "AUX2", "AUX3" ] - } + }, + "gpios": [ + {"name": "Receiver", + "pin": 19}, + {"name": "TV", + "pin": 16}, + {"name": "Xbox", + "pin": 13} + ] } Please see the `example_configs/` directory. @@ -107,10 +125,12 @@ API endpoints: * ``GET`` ``/remotes.json`` - Returns all known remotes and commands * ``GET`` ``/remotes/:remote.json`` - Returns all known commands for remote ``:remote`` * ``GET`` ``/macros.json`` - Returns all known macros +* ``GET`` ``/gpios.json`` - Returns all configured GPIO pins * ``POST`` ``/remotes/:remote/:command`` - Send ``:command`` to ``:remote`` one time * ``POST`` ``/remotes/:remote/:command/send_start`` - Begin sending ``:command`` * ``POST`` ``/remotes/:remote/:command/send_stop`` - Stop sending ``:command`` * ``POST`` ``/macros/:macro`` - Send all commands for ``:macro`` one time +* ``POST`` ``/gpios/:gpio-pin`` - Change the state of the pin ## Development @@ -130,6 +150,7 @@ Install GruntJS (build environment): npm install -g grunt-init grunt server +Note that you probably have to run the `npm install -g` command with `sudo`. **You may need to reload your shell before continuing so the Grunt binares are detected.** diff --git a/app.js b/app.js index 7765cfc..407c570 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ var swig = require('swig'); var labels = require('./lib/labels'); var https = require('https'); var fs = require('fs'); +var gpio = require('./lib/gpio'); var macros = require('./lib/macros'); // Precompile templates @@ -94,6 +95,7 @@ function refineRemotes(myRemotes) { if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') { lircNode.remotes = require(__dirname + '/test/fixtures/remotes.json'); config = require(__dirname + '/test/fixtures/config.json'); + gpio.overrideWiringPi(require('./test/lib/wiring-pi-mock')); } else { _init(); } @@ -110,6 +112,7 @@ app.get('/', function (req, res) { remotes: refinedRemotes, macros: config.macros, repeaters: config.repeaters, + gpios: config.gpios, labelForRemote: labelFor.remote, labelForCommand: labelFor.command, })); @@ -135,6 +138,20 @@ app.get('/remotes/:remote.json', function (req, res) { } }); +function respondWithGpioState(res) { + if (config.gpios) { + gpio.updatePinStates(); + res.json(config.gpios); + } else { + res.send(404); + } +} + +// List all gpio switches in JSON format +app.get('/gpios.json', function (req, res) { + respondWithGpioState(res); +}); + // List all macros in JSON format app.get('/macros.json', function (req, res) { res.json(config.macros); @@ -149,7 +166,6 @@ app.get('/macros/:macro.json', function (req, res) { } }); - // Send :remote/:command one time app.post('/remotes/:remote/:command', function (req, res) { lircNode.irsend.send_once(req.params.remote, req.params.command, function () {}); @@ -171,19 +187,33 @@ app.post('/remotes/:remote/:command/send_stop', function (req, res) { res.sendStatus(200); }); +// toggle /gpios/:gpio_pin +app.post('/gpios/:gpio_pin', function (req, res) { + var newValue = gpio.togglePin(req.params.gpio_pin); + res.setHeader('Cache-Control', 'no-cache'); + res.json(newValue); + res.end(); +}); + + // Execute a macro (a collection of commands to one or more remotes) app.post('/macros/:macro', function (req, res) { - // If the macro exists, execute it if (config.macros && config.macros[req.params.macro]) { macros.exec(config.macros[req.params.macro], lircNode); res.setHeader('Cache-Control', 'no-cache'); - res.sendStatus(200); + if (config.gpios) { + respondWithGpioState(res); + } else { + res.sendStatus(200); + } } else { res.setHeader('Cache-Control', 'no-cache'); res.sendStatus(404); } }); +gpio.init(config.gpios); + // Listen (http) if (config.server && config.server.port) { port = config.server.port; diff --git a/example_configs/config.json b/example_configs/config.json index 22b108e..9515b65 100644 --- a/example_configs/config.json +++ b/example_configs/config.json @@ -14,11 +14,19 @@ }, "macros": { "Xbox360": [ + [ "gpio", "TV", 1], + [ "gpio", "Receiver", 1], + [ "gpio", "Xbox", 1], [ "SonyTV", "Power" ], [ "SonyTV", "Xbox360" ], [ "Yamaha", "Power" ], [ "Yamaha", "Xbox360" ], [ "Xbox360", "Power" ] + ], + "all off": [ + [ "gpio", "TV", 0], + [ "gpio", "Receiver", 0], + [ "gpio", "Xbox", 0] ] }, "commandLabels": { @@ -36,5 +44,13 @@ "AUX2", "AUX3" ] - } + }, + "gpios": [ + {"name": "Receiver", + "pin": 19}, + {"name": "TV", + "pin": 16}, + {"name": "Xbox", + "pin": 13} + ] } diff --git a/lib/gpio.js b/lib/gpio.js new file mode 100644 index 0000000..242f1fe --- /dev/null +++ b/lib/gpio.js @@ -0,0 +1,66 @@ +var wpi = require('wiring-pi'); +var gpios = null; + +function updatePinStates() { + var i; + var gpio; + if (gpios) { + for (i = 0; i < gpios.length; i++) { + gpio = gpios[i]; + gpio.state = wpi.digitalRead(gpio.pin); + } + } +} + +function togglePin(pinId) { + var numericPinId = parseInt(pinId, 10); + var currentState = wpi.digitalRead(numericPinId); + var newState = currentState > 0 ? 0 : 1; + wpi.digitalWrite(numericPinId, newState); + return { + 'pin': numericPinId, + 'state': newState, + }; +} + +function findElement(array, propertyName, propertyValue) { + var i; + for (i = 0; i < array.length; i++) { + if (array[i][propertyName] === propertyValue) { + return array[i]; + } + } +} + +function getPinIdByName(pinName) { + var gpioPin = findElement(gpios, 'name', pinName); + return gpioPin.pin; +} + +function setPin(pinName, newState) { + var numericPinId = getPinIdByName(pinName); + wpi.digitalWrite(numericPinId, newState); +} + +function init(configuration) { + if (configuration) { + gpios = configuration; + wpi.setup('gpio'); + updatePinStates(); + } +} + +function overrideWiringPi(wpiReplacement) { + var origWpi = wpi; + wpi = wpiReplacement; + return origWpi; +} + +module.exports = { + init: init, + setPin: setPin, + overrideWiringPi: overrideWiringPi, + updatePinStates: updatePinStates, + togglePin: togglePin, +}; +// vim: expandtab:tabstop=8:softtabstop=2:shiftwidth=2 diff --git a/lib/macros.js b/lib/macros.js index 5fd977a..f3b3b53 100644 --- a/lib/macros.js +++ b/lib/macros.js @@ -1,3 +1,5 @@ +var gpio = require('./gpio'); + function exec(macro, lircNode, iter) { var i = iter || 0; @@ -13,6 +15,9 @@ function exec(macro, lircNode, iter) { setTimeout(function () { exec(macro, lircNode, i); }, command[1]); + } else if (command[0] === 'gpio') { + gpio.setPin(command[1], command[2]); + exec(macro, lircNode, i); } else { // By default, wait 100msec before calling next command lircNode.irsend.send_once(command[0], command[1], function () { diff --git a/package.json b/package.json index 7182ebb..26da2b5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "lirc_node": "0.0.4", "lodash": "^3.10.1", "morgan": "^1.6.1", - "swig": "^1.4.2" + "swig": "^1.4.2", + "wiring-pi": "2.1.0" }, "devDependencies": { "eslint": "^1.10.0", diff --git a/static/css/app.less b/static/css/app.less index 99a069d..3a8df1f 100644 --- a/static/css/app.less +++ b/static/css/app.less @@ -6,6 +6,9 @@ @colorPeterRiver: #3498DB; @colorGreenSea: #16A085; @colorTurqoise: #1ABC9C; +@colorGreyBtn: #cccccc; +@colorRedBtn: #d3898a; +@colorGreenBtn: #9bcc9c; html, body { min-height: 100%; @@ -76,6 +79,18 @@ h1 { background: #5dade2 !important; } +.gpio-link { + background: @colorRedBtn !important; +} + +.gpio-link.switch_on { + background: @colorGreenBtn !important; +} + +.gpio-link.active { + background: @colorGreyBtn !important; +} + .btn-wide { padding-left: 0; padding-right: 0; @@ -89,11 +104,13 @@ h1 { } .remotes-nav, -.macros-nav { +.macros-nav, +.gpios-nav { } .remotes-nav li, -.macros-nav li { +.macros-nav li, +.gpios-nav li { margin: 20px 0; } diff --git a/static/css/compiled/app.css b/static/css/compiled/app.css index 9f66da9..b70265f 100644 --- a/static/css/compiled/app.css +++ b/static/css/compiled/app.css @@ -54,6 +54,15 @@ h1.is-remote { .remote-link.active { background: #5dade2 !important; } +.gpio-link { + background: #d3898a !important; +} +.gpio-link.switch_on { + background: #9bcc9c !important; +} +.gpio-link.active { + background: #cccccc !important; +} .btn-wide { padding-left: 0; padding-right: 0; @@ -65,7 +74,8 @@ h1.is-remote { position: relative; } .remotes-nav li, -.macros-nav li { +.macros-nav li, +.gpios-nav li { margin: 20px 0; } .back { diff --git a/static/js/app/core.js b/static/js/app/core.js index f899419..77371f1 100644 --- a/static/js/app/core.js +++ b/static/js/app/core.js @@ -62,21 +62,63 @@ $(function() { $.ajax({ type: "POST", url: $(this).attr('href'), - success: function(data) {}, + success: function(data) { initializeSwitchStates(data); }, + error: function(xhr, type) {} + }); + }); + + // gpio-link buttons adopt color depending on the gpio state + $('.gpio-link').on('click', function(evt) { + evt.preventDefault(); + var visualElement = $(this); + $.ajax({ + type: "POST", + url: "/gpios/"+$(this).attr('pin'), + success: function(data) { updateGpioButton(visualElement, data); }, error: function(xhr, type) {} }); }); + function updateGpioButton(visualElement, gpioData) { + console.log(JSON.stringify(gpioData, null, 2)); + if (gpioData.state==0) { + visualElement.removeClass('switch_on'); + } else { + visualElement.addClass('switch_on'); + } + } + + $(document).ready(function() { + $.ajax({ + type: "GET", + url: "/gpios.json" , + success: initializeSwitchStates, + error: function(xhr, type) {} + }); + }); + + function initializeSwitchStates(gpios) { + console.log(JSON.stringify(gpios)); + var i, gpio; + for (i=0; i>>0,f=0;if("function"!=typeof b)throw new TypeError;if(0==e&&1==arguments.length)throw new TypeError;if(arguments.length>=2)c=arguments[1];else for(;;){if(f in d){c=d[f++];break}if(++f>=e)throw new TypeError}for(;e>f;)f in d&&(c=b.call(a,c,d[f],f,d)),f++;return c})}();var Zepto=function(){function a(a){return null==a?String(a):W[X.call(a)]||"object"}function b(b){return"function"==a(b)}function c(a){return null!=a&&a==a.window}function d(a){return null!=a&&a.nodeType==a.DOCUMENT_NODE}function e(b){return"object"==a(b)}function f(a){return e(a)&&!c(a)&&a.__proto__==Object.prototype}function g(a){return a instanceof Array}function h(a){return"number"==typeof a.length}function i(a){return E.call(a,function(a){return null!=a})}function j(a){return a.length>0?y.fn.concat.apply([],a):a}function k(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function l(a){return a in H?H[a]:H[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function m(a,b){return"number"!=typeof b||J[k(a)]?b:b+"px"}function n(a){var b,c;return G[a]||(b=F.createElement(a),F.body.appendChild(b),c=I(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),"none"==c&&(c="block"),G[a]=c),G[a]}function o(a){return"children"in a?D.call(a.children):y.map(a.childNodes,function(a){return 1==a.nodeType?a:void 0})}function p(a,b,c){for(x in b)c&&(f(b[x])||g(b[x]))?(f(b[x])&&!f(a[x])&&(a[x]={}),g(b[x])&&!g(a[x])&&(a[x]=[]),p(a[x],b[x],c)):b[x]!==w&&(a[x]=b[x])}function q(a,b){return b===w?y(a):y(a).filter(b)}function r(a,c,d,e){return b(c)?c.call(a,d,e):c}function s(a,b,c){null==c?a.removeAttribute(b):a.setAttribute(b,c)}function t(a,b){var c=a.className,d=c&&c.baseVal!==w;return b===w?d?c.baseVal:c:void(d?c.baseVal=b:a.className=b)}function u(a){var b;try{return a?"true"==a||("false"==a?!1:"null"==a?null:isNaN(b=Number(a))?/^[\[\{]/.test(a)?y.parseJSON(a):a:b):a}catch(c){return a}}function v(a,b){b(a);for(var c in a.childNodes)v(a.childNodes[c],b)}var w,x,y,z,A,B,C=[],D=C.slice,E=C.filter,F=window.document,G={},H={},I=F.defaultView.getComputedStyle,J={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},K=/^\s*<(\w+|!)[^>]*>/,L=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,M=/^(?:body|html)$/i,N=["val","css","html","text","data","width","height","offset"],O=["after","prepend","before","append"],P=F.createElement("table"),Q=F.createElement("tr"),R={tr:F.createElement("tbody"),tbody:P,thead:P,tfoot:P,td:Q,th:Q,"*":F.createElement("div")},S=/complete|loaded|interactive/,T=/^\.([\w-]+)$/,U=/^#([\w-]*)$/,V=/^[\w-]+$/,W={},X=W.toString,Y={},Z=F.createElement("div");return Y.matches=function(a,b){if(!a||1!==a.nodeType)return!1;var c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector;if(c)return c.call(a,b);var d,e=a.parentNode,f=!e;return f&&(e=Z).appendChild(a),d=~Y.qsa(e,b).indexOf(a),f&&Z.removeChild(a),d},A=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},B=function(a){return E.call(a,function(b,c){return a.indexOf(b)==c})},Y.fragment=function(a,b,c){a.replace&&(a=a.replace(L,"<$1>")),b===w&&(b=K.test(a)&&RegExp.$1),b in R||(b="*");var d,e,g=R[b];return g.innerHTML=""+a,e=y.each(D.call(g.childNodes),function(){g.removeChild(this)}),f(c)&&(d=y(e),y.each(c,function(a,b){N.indexOf(a)>-1?d[a](b):d.attr(a,b)})),e},Y.Z=function(a,b){return a=a||[],a.__proto__=y.fn,a.selector=b||"",a},Y.isZ=function(a){return a instanceof Y.Z},Y.init=function(a,c){if(!a)return Y.Z();if(b(a))return y(F).ready(a);if(Y.isZ(a))return a;var d;if(g(a))d=i(a);else if(e(a))d=[f(a)?y.extend({},a):a],a=null;else if(K.test(a))d=Y.fragment(a.trim(),RegExp.$1,c),a=null;else{if(c!==w)return y(c).find(a);d=Y.qsa(F,a)}return Y.Z(d,a)},y=function(a,b){return Y.init(a,b)},y.extend=function(a){var b,c=D.call(arguments,1);return"boolean"==typeof a&&(b=a,a=c.shift()),c.forEach(function(c){p(a,c,b)}),a},Y.qsa=function(a,b){var c;return d(a)&&U.test(b)?(c=a.getElementById(RegExp.$1))?[c]:[]:1!==a.nodeType&&9!==a.nodeType?[]:D.call(T.test(b)?a.getElementsByClassName(RegExp.$1):V.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},y.contains=function(a,b){return a!==b&&a.contains(b)},y.type=a,y.isFunction=b,y.isWindow=c,y.isArray=g,y.isPlainObject=f,y.isEmptyObject=function(a){var b;for(b in a)return!1;return!0},y.inArray=function(a,b,c){return C.indexOf.call(b,a,c)},y.camelCase=A,y.trim=function(a){return a.trim()},y.uuid=0,y.support={},y.expr={},y.map=function(a,b){var c,d,e,f=[];if(h(a))for(d=0;d=0?a:a+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(a){return C.every.call(this,function(b,c){return a.call(b,c,b)!==!1}),this},filter:function(a){return b(a)?this.not(this.not(a)):y(E.call(this,function(b){return Y.matches(b,a)}))},add:function(a,b){return y(B(this.concat(y(a,b))))},is:function(a){return this.length>0&&Y.matches(this[0],a)},not:function(a){var c=[];if(b(a)&&a.call!==w)this.each(function(b){a.call(this,b)||c.push(this)});else{var d="string"==typeof a?this.filter(a):h(a)&&b(a.item)?D.call(a):y(a);this.forEach(function(a){d.indexOf(a)<0&&c.push(a)})}return y(c)},has:function(a){return this.filter(function(){return e(a)?y.contains(this,a):y(this).find(a).size()})},eq:function(a){return-1===a?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!e(a)?a:y(a)},last:function(){var a=this[this.length-1];return a&&!e(a)?a:y(a)},find:function(a){var b,c=this;return b="object"==typeof a?y(a).filter(function(){var a=this;return C.some.call(c,function(b){return y.contains(b,a)})}):1==this.length?y(Y.qsa(this[0],a)):this.map(function(){return Y.qsa(this,a)})},closest:function(a,b){var c=this[0],e=!1;for("object"==typeof a&&(e=y(a));c&&!(e?e.indexOf(c)>=0:Y.matches(c,a));)c=c!==b&&!d(c)&&c.parentNode;return y(c)},parents:function(a){for(var b=[],c=this;c.length>0;)c=y.map(c,function(a){return(a=a.parentNode)&&!d(a)&&b.indexOf(a)<0?(b.push(a),a):void 0});return q(b,a)},parent:function(a){return q(B(this.pluck("parentNode")),a)},children:function(a){return q(this.map(function(){return o(this)}),a)},contents:function(){return this.map(function(){return D.call(this.childNodes)})},siblings:function(a){return q(this.map(function(a,b){return E.call(o(b.parentNode),function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return y.map(this,function(b){return b[a]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=null),"none"==I(this,"").getPropertyValue("display")&&(this.style.display=n(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){var c=b(a);if(this[0]&&!c)var d=y(a).get(0),e=d.parentNode||this.length>1;return this.each(function(b){y(this).wrapAll(c?a.call(this,b):e?d.cloneNode(!0):d)})},wrapAll:function(a){if(this[0]){y(this[0]).before(a=y(a));for(var b;(b=a.children()).length;)a=b.first();y(a).append(this)}return this},wrapInner:function(a){var c=b(a);return this.each(function(b){var d=y(this),e=d.contents(),f=c?a.call(this,b):a;e.length?e.wrapAll(f):d.append(f)})},unwrap:function(){return this.parent().each(function(){y(this).replaceWith(y(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(a){return this.each(function(){var b=y(this);(a===w?"none"==b.css("display"):a)?b.show():b.hide()})},prev:function(a){return y(this.pluck("previousElementSibling")).filter(a||"*")},next:function(a){return y(this.pluck("nextElementSibling")).filter(a||"*")},html:function(a){return a===w?this.length>0?this[0].innerHTML:null:this.each(function(b){var c=this.innerHTML;y(this).empty().append(r(this,a,b,c))})},text:function(a){return a===w?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=a})},attr:function(a,b){var c;return"string"==typeof a&&b===w?0==this.length||1!==this[0].nodeType?w:"value"==a&&"INPUT"==this[0].nodeName?this.val():!(c=this[0].getAttribute(a))&&a in this[0]?this[0][a]:c:this.each(function(c){if(1===this.nodeType)if(e(a))for(x in a)s(this,x,a[x]);else s(this,a,r(this,b,c,this.getAttribute(a)))})},removeAttr:function(a){return this.each(function(){1===this.nodeType&&s(this,a)})},prop:function(a,b){return b===w?this[0]&&this[0][a]:this.each(function(c){this[a]=r(this,b,c,this[a])})},data:function(a,b){var c=this.attr("data-"+k(a),b);return null!==c?u(c):w},val:function(a){return a===w?this[0]&&(this[0].multiple?y(this[0]).find("option").filter(function(a){return this.selected}).pluck("value"):this[0].value):this.each(function(b){this.value=r(this,a,b,this.value)})},offset:function(a){if(a)return this.each(function(b){var c=y(this),d=r(this,a,b,c.offset()),e=c.offsetParent().offset(),f={top:d.top-e.top,left:d.left-e.left};"static"==c.css("position")&&(f.position="relative"),c.css(f)});if(0==this.length)return null;var b=this[0].getBoundingClientRect();return{left:b.left+window.pageXOffset,top:b.top+window.pageYOffset,width:Math.round(b.width),height:Math.round(b.height)}},css:function(b,c){if(arguments.length<2&&"string"==typeof b)return this[0]&&(this[0].style[A(b)]||I(this[0],"").getPropertyValue(b));var d="";if("string"==a(b))c||0===c?d=k(b)+":"+m(b,c):this.each(function(){this.style.removeProperty(k(b))});else for(x in b)b[x]||0===b[x]?d+=k(x)+":"+m(x,b[x])+";":this.each(function(){this.style.removeProperty(k(x))});return this.each(function(){this.style.cssText+=";"+d})},index:function(a){return a?this.indexOf(y(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return C.some.call(this,function(a){return this.test(t(a))},l(a))},addClass:function(a){return this.each(function(b){z=[];var c=t(this),d=r(this,a,b,c);d.split(/\s+/g).forEach(function(a){y(this).hasClass(a)||z.push(a)},this),z.length&&t(this,c+(c?" ":"")+z.join(" "))})},removeClass:function(a){return this.each(function(b){return a===w?t(this,""):(z=t(this),r(this,a,b,z).split(/\s+/g).forEach(function(a){z=z.replace(l(a)," ")}),t(this,z.trim()),void 0)})},toggleClass:function(a,b){return this.each(function(c){var d=y(this),e=r(this,a,c,t(this));e.split(/\s+/g).forEach(function(a){(b===w?!d.hasClass(a):b)?d.addClass(a):d.removeClass(a)})})},scrollTop:function(){return this.length?"scrollTop"in this[0]?this[0].scrollTop:this[0].scrollY:void 0},position:function(){if(this.length){var a=this[0],b=this.offsetParent(),c=this.offset(),d=M.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(y(a).css("margin-top"))||0,c.left-=parseFloat(y(a).css("margin-left"))||0,d.top+=parseFloat(y(b[0]).css("border-top-width"))||0,d.left+=parseFloat(y(b[0]).css("border-left-width"))||0,{top:c.top-d.top,left:c.left-d.left}}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||F.body;a&&!M.test(a.nodeName)&&"static"==y(a).css("position");)a=a.offsetParent;return a})}},y.fn.detach=y.fn.remove,["width","height"].forEach(function(a){y.fn[a]=function(b){var e,f=this[0],g=a.replace(/./,function(a){return a[0].toUpperCase()});return b===w?c(f)?f["inner"+g]:d(f)?f.documentElement["offset"+g]:(e=this.offset())&&e[a]:this.each(function(c){f=y(this),f.css(a,r(this,b,c,f[a]()))})}}),O.forEach(function(b,c){var d=c%2;y.fn[b]=function(){var b,e,f=y.map(arguments,function(c){return b=a(c),"object"==b||"array"==b||null==c?c:Y.fragment(c)}),g=this.length>1;return f.length<1?this:this.each(function(a,b){e=d?b:b.parentNode,b=0==c?b.nextSibling:1==c?b.firstChild:2==c?b:null,f.forEach(function(a){if(g)a=a.cloneNode(!0);else if(!e)return y(a).remove();v(e.insertBefore(a,b),function(a){null!=a.nodeName&&"SCRIPT"===a.nodeName.toUpperCase()&&(!a.type||"text/javascript"===a.type)&&!a.src&&window.eval.call(window,a.innerHTML)})})})},y.fn[d?b+"To":"insert"+(c?"Before":"After")]=function(a){return y(a)[b](this),this}}),Y.Z.prototype=y.fn,Y.uniq=B,Y.deserializeValue=u,y.zepto=Y,y}();window.Zepto=Zepto,"$"in window||(window.$=Zepto),function(a){function b(a){var b=this.os={},c=this.browser={},d=a.match(/WebKit\/([\d.]+)/),e=a.match(/(Android)\s+([\d.]+)/),f=a.match(/(iPad).*OS\s([\d_]+)/),g=!f&&a.match(/(iPhone\sOS)\s([\d_]+)/),h=a.match(/(webOS|hpwOS)[\s\/]([\d.]+)/),i=h&&a.match(/TouchPad/),j=a.match(/Kindle\/([\d.]+)/),k=a.match(/Silk\/([\d._]+)/),l=a.match(/(BlackBerry).*Version\/([\d.]+)/),m=a.match(/(BB10).*Version\/([\d.]+)/),n=a.match(/(RIM\sTablet\sOS)\s([\d.]+)/),o=a.match(/PlayBook/),p=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),q=a.match(/Firefox\/([\d.]+)/);(c.webkit=!!d)&&(c.version=d[1]),e&&(b.android=!0,b.version=e[2]),g&&(b.ios=b.iphone=!0,b.version=g[2].replace(/_/g,".")),f&&(b.ios=b.ipad=!0,b.version=f[2].replace(/_/g,".")),h&&(b.webos=!0,b.version=h[2]),i&&(b.touchpad=!0),l&&(b.blackberry=!0,b.version=l[2]),m&&(b.bb10=!0,b.version=m[2]),n&&(b.rimtabletos=!0,b.version=n[2]),o&&(c.playbook=!0),j&&(b.kindle=!0,b.version=j[1]),k&&(c.silk=!0,c.version=k[1]),!k&&b.android&&a.match(/Kindle Fire/)&&(c.silk=!0),p&&(c.chrome=!0,c.version=p[1]),q&&(c.firefox=!0,c.version=q[1]),b.tablet=!!(f||o||e&&!a.match(/Mobile/)||q&&a.match(/Tablet/)),b.phone=!b.tablet&&!!(e||g||h||l||m||p&&a.match(/Android/)||p&&a.match(/CriOS\/([\d.]+)/)||q&&a.match(/Mobile/))}b.call(a,navigator.userAgent),a.__detect=b}(Zepto),function(a){function b(a){return a._zid||(a._zid=n++)}function c(a,c,f,g){if(c=d(c),c.ns)var h=e(c.ns);return(m[b(a)]||[]).filter(function(a){return a&&(!c.e||a.e==c.e)&&(!c.ns||h.test(a.ns))&&(!f||b(a.fn)===b(f))&&(!g||a.sel==g)})}function d(a){var b=(""+a).split(".");return{e:b[0],ns:b.slice(1).sort().join(" ")}}function e(a){return new RegExp("(?:^| )"+a.replace(" "," .* ?")+"(?: |$)")}function f(b,c,d){"string"!=a.type(b)?a.each(b,d):b.split(/\s/).forEach(function(a){d(a,c)})}function g(a,b){return a.del&&("focus"==a.e||"blur"==a.e)||!!b}function h(a){return p[a]||a}function i(c,e,i,j,k,l){var n=b(c),o=m[n]||(m[n]=[]);f(e,i,function(b,e){var f=d(b);f.fn=e,f.sel=j,f.e in p&&(e=function(b){var c=b.relatedTarget;return!c||c!==this&&!a.contains(this,c)?f.fn.apply(this,arguments):void 0}),f.del=k&&k(e,b);var i=f.del||e;f.proxy=function(a){var b=i.apply(c,[a].concat(a.data));return b===!1&&(a.preventDefault(),a.stopPropagation()),b},f.i=o.length,o.push(f),c.addEventListener(h(f.e),f.proxy,g(f,l))})}function j(a,d,e,i,j){var k=b(a);f(d||"",e,function(b,d){c(a,b,d,i).forEach(function(b){delete m[k][b.i],a.removeEventListener(h(b.e),b.proxy,g(b,j))})})}function k(b){var c,d={originalEvent:b};for(c in b)!s.test(c)&&void 0!==b[c]&&(d[c]=b[c]);return a.each(t,function(a,c){d[a]=function(){return this[c]=q,b[a].apply(b,arguments)},d[c]=r}),d}function l(a){if(!("defaultPrevented"in a)){a.defaultPrevented=!1;var b=a.preventDefault;a.preventDefault=function(){this.defaultPrevented=!0,b.call(this)}}}var m=(a.zepto.qsa,{}),n=1,o={},p={mouseenter:"mouseover",mouseleave:"mouseout"};o.click=o.mousedown=o.mouseup=o.mousemove="MouseEvents",a.event={add:i,remove:j},a.proxy=function(c,d){if(a.isFunction(c)){var e=function(){return c.apply(d,arguments)};return e._zid=b(c),e}if("string"==typeof d)return a.proxy(c[d],c);throw new TypeError("expected function")},a.fn.bind=function(a,b){return this.each(function(){i(this,a,b)})},a.fn.unbind=function(a,b){return this.each(function(){j(this,a,b)})},a.fn.one=function(a,b){return this.each(function(c,d){i(this,a,b,null,function(a,b){return function(){var c=a.apply(d,arguments);return j(d,b,a),c}})})};var q=function(){return!0},r=function(){return!1},s=/^([A-Z]|layer[XY]$)/,t={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};a.fn.delegate=function(b,c,d){return this.each(function(e,f){i(f,c,d,b,function(c){return function(d){var e,g=a(d.target).closest(b,f).get(0);return g?(e=a.extend(k(d),{currentTarget:g,liveFired:f}),c.apply(g,[e].concat([].slice.call(arguments,1)))):void 0}})})},a.fn.undelegate=function(a,b,c){return this.each(function(){j(this,b,c,a)})},a.fn.live=function(b,c){return a(document.body).delegate(this.selector,b,c),this},a.fn.die=function(b,c){return a(document.body).undelegate(this.selector,b,c),this},a.fn.on=function(b,c,d){return!c||a.isFunction(c)?this.bind(b,c||d):this.delegate(c,b,d)},a.fn.off=function(b,c,d){return!c||a.isFunction(c)?this.unbind(b,c||d):this.undelegate(c,b,d)},a.fn.trigger=function(b,c){return("string"==typeof b||a.isPlainObject(b))&&(b=a.Event(b)),l(b),b.data=c,this.each(function(){"dispatchEvent"in this&&this.dispatchEvent(b)})},a.fn.triggerHandler=function(b,d){var e,f;return this.each(function(g,h){e=k("string"==typeof b?a.Event(b):b),e.data=d,e.target=h,a.each(c(h,b.type||b),function(a,b){return f=b.proxy(e),e.isImmediatePropagationStopped()?!1:void 0})}),f},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.trigger(b)}}),["focus","blur"].forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.each(function(){try{this[b]()}catch(a){}}),this}}),a.Event=function(a,b){"string"!=typeof a&&(b=a,a=b.type);var c=document.createEvent(o[a]||"Events"),d=!0;if(b)for(var e in b)"bubbles"==e?d=!!b[e]:c[e]=b[e];return c.initEvent(a,d,!0,null,null,null,null,null,null,null,null,null,null,null,null),c.isDefaultPrevented=function(){return this.defaultPrevented},c}}(Zepto),function(a){function b(b,c,d){var e=a.Event(c);return a(b).trigger(e,d),!e.defaultPrevented}function c(a,c,d,e){return a.global?b(c||s,d,e):void 0}function d(b){b.global&&0===a.active++&&c(b,null,"ajaxStart")}function e(b){b.global&&!--a.active&&c(b,null,"ajaxStop")}function f(a,b){var d=b.context;return b.beforeSend.call(d,a,b)===!1||c(b,d,"ajaxBeforeSend",[a,b])===!1?!1:void c(b,d,"ajaxSend",[a,b])}function g(a,b,d){var e=d.context,f="success";d.success.call(e,a,f,b),c(d,e,"ajaxSuccess",[b,d,a]),i(f,b,d)}function h(a,b,d,e){var f=e.context;e.error.call(f,d,b,a),c(e,f,"ajaxError",[d,e,a]),i(b,d,e)}function i(a,b,d){var f=d.context;d.complete.call(f,b,a),c(d,f,"ajaxComplete",[b,d]),e(d)}function j(){}function k(a){return a&&(a=a.split(";",2)[0]),a&&(a==x?"html":a==w?"json":u.test(a)?"script":v.test(a)&&"xml")||"text"}function l(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function m(b){b.processData&&b.data&&"string"!=a.type(b.data)&&(b.data=a.param(b.data,b.traditional)),b.data&&(!b.type||"GET"==b.type.toUpperCase())&&(b.url=l(b.url,b.data))}function n(b,c,d,e){var f=!a.isFunction(c);return{url:b,data:f?c:void 0,success:f?a.isFunction(d)?d:void 0:c,dataType:f?e||d:d}}function o(b,c,d,e){var f,g=a.isArray(c);a.each(c,function(c,h){f=a.type(h),e&&(c=d?e:e+"["+(g?"":c)+"]"),!e&&g?b.add(h.name,h.value):"array"==f||!d&&"object"==f?o(b,h,d,c):b.add(c,h)})}var p,q,r=0,s=window.document,t=/)<[^<]*)*<\/script>/gi,u=/^(?:text|application)\/javascript/i,v=/^(?:text|application)\/xml/i,w="application/json",x="text/html",y=/^\s*$/;a.active=0,a.ajaxJSONP=function(b){if("type"in b){var c,d="jsonp"+ ++r,e=s.createElement("script"),i=function(){clearTimeout(c),a(e).remove(),delete window[d]},k=function(a){i(),a&&"timeout"!=a||(window[d]=j),h(null,a||"abort",l,b)},l={abort:k};return f(l,b)===!1?(k("abort"),!1):(window[d]=function(a){i(),g(a,l,b)},e.onerror=function(){k("error")},e.src=b.url.replace(/=\?/,"="+d),a("head").append(e),b.timeout>0&&(c=setTimeout(function(){k("timeout")},b.timeout)),l)}return a.ajax(b)},a.ajaxSettings={type:"GET",beforeSend:j,success:j,error:j,complete:j,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:w,xml:"application/xml, text/xml",html:x,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},a.ajax=function(b){var c=a.extend({},b||{});for(p in a.ajaxSettings)void 0===c[p]&&(c[p]=a.ajaxSettings[p]);d(c),c.crossDomain||(c.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(c.url)&&RegExp.$2!=window.location.host),c.url||(c.url=window.location.toString()),m(c),c.cache===!1&&(c.url=l(c.url,"_="+Date.now()));var e=c.dataType,i=/=\?/.test(c.url);if("jsonp"==e||i)return i||(c.url=l(c.url,"callback=?")),a.ajaxJSONP(c);var n,o=c.accepts[e],r={},s=/^([\w-]+:)\/\//.test(c.url)?RegExp.$1:window.location.protocol,t=c.xhr();c.crossDomain||(r["X-Requested-With"]="XMLHttpRequest"),o&&(r.Accept=o,o.indexOf(",")>-1&&(o=o.split(",",2)[0]),t.overrideMimeType&&t.overrideMimeType(o)),(c.contentType||c.contentType!==!1&&c.data&&"GET"!=c.type.toUpperCase())&&(r["Content-Type"]=c.contentType||"application/x-www-form-urlencoded"),c.headers=a.extend(r,c.headers||{}),t.onreadystatechange=function(){if(4==t.readyState){t.onreadystatechange=j,clearTimeout(n);var b,d=!1;if(t.status>=200&&t.status<300||304==t.status||0==t.status&&"file:"==s){e=e||k(t.getResponseHeader("content-type")),b=t.responseText;try{"script"==e?(1,eval)(b):"xml"==e?b=t.responseXML:"json"==e&&(b=y.test(b)?null:a.parseJSON(b))}catch(f){d=f}d?h(d,"parsererror",t,c):g(b,t,c)}else h(null,t.status?"error":"abort",t,c)}};var u="async"in c?c.async:!0;t.open(c.type,c.url,u);for(q in c.headers)t.setRequestHeader(q,c.headers[q]);return f(t,c)===!1?(t.abort(),!1):(c.timeout>0&&(n=setTimeout(function(){t.onreadystatechange=j,t.abort(),h(null,"timeout",t,c)},c.timeout)),t.send(c.data?c.data:null),t)},a.get=function(b,c,d,e){return a.ajax(n.apply(null,arguments))},a.post=function(b,c,d,e){var f=n.apply(null,arguments);return f.type="POST",a.ajax(f)},a.getJSON=function(b,c,d){var e=n.apply(null,arguments);return e.dataType="json",a.ajax(e)},a.fn.load=function(b,c,d){if(!this.length)return this;var e,f=this,g=b.split(/\s/),h=n(b,c,d),i=h.success;return g.length>1&&(h.url=g[0],e=g[1]),h.success=function(b){f.html(e?a("
").html(b.replace(t,"")).find(e):b),i&&i.apply(f,arguments)},a.ajax(h),this};var z=encodeURIComponent;a.param=function(a,b){var c=[];return c.add=function(a,b){this.push(z(a)+"="+z(b))},o(c,a,b),c.join("&").replace(/%20/g,"+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b,c=[];return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){b=a(this);var d=b.attr("type");"fieldset"!=this.nodeName.toLowerCase()&&!this.disabled&&"submit"!=d&&"reset"!=d&&"button"!=d&&("radio"!=d&&"checkbox"!=d||this.checked)&&c.push({name:b.attr("name"),value:b.val()})}),c},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a,b){function c(a){return d(a.replace(/([a-z])([A-Z])/,"$1-$2"))}function d(a){return a.toLowerCase()}function e(a){return f?f+a:d(a)}var f,g,h,i,j,k,l,m,n="",o={Webkit:"webkit",Moz:"",O:"o",ms:"MS"},p=window.document,q=p.createElement("div"),r=/^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,s={};a.each(o,function(a,c){return q.style[a+"TransitionProperty"]!==b?(n="-"+d(a)+"-",f=c,!1):void 0}),g=n+"transform",s[h=n+"transition-property"]=s[i=n+"transition-duration"]=s[j=n+"transition-timing-function"]=s[k=n+"animation-name"]=s[l=n+"animation-duration"]=s[m=n+"animation-timing-function"]="",a.fx={off:f===b&&q.style.transitionProperty===b,speeds:{_default:400,fast:200,slow:600},cssPrefix:n,transitionEnd:e("TransitionEnd"),animationEnd:e("AnimationEnd")},a.fn.animate=function(b,c,d,e){return a.isPlainObject(c)&&(d=c.easing,e=c.complete,c=c.duration),c&&(c=("number"==typeof c?c:a.fx.speeds[c]||a.fx.speeds._default)/1e3),this.anim(b,c,d,e)},a.fn.anim=function(d,e,f,n){var o,p,q,t={},u="",v=this,w=a.fx.transitionEnd;if(e===b&&(e=.4),a.fx.off&&(e=0),"string"==typeof d)t[k]=d,t[l]=e+"s",t[m]=f||"linear",w=a.fx.animationEnd;else{p=[];for(o in d)r.test(o)?u+=o+"("+d[o]+") ":(t[o]=d[o],p.push(c(o)));u&&(t[g]=u,p.push(g)),e>0&&"object"==typeof d&&(t[h]=p.join(", "),t[i]=e+"s",t[j]=f||"linear")}return q=function(b){if("undefined"!=typeof b){if(b.target!==b.currentTarget)return;a(b.target).unbind(w,q)}a(this).css(s),n&&n.call(this)},e>0&&this.bind(w,q),this.size()&&this.get(0).clientLeft,this.css(t),0>=e&&setTimeout(function(){v.each(function(){q.call(this)})},0),this},q=null}(Zepto),function(a){function b(a){return"tagName"in a?a:a.parentNode}function c(a,b,c,d){var e=Math.abs(a-b),f=Math.abs(c-d);return e>=f?a-b>0?"Left":"Right":c-d>0?"Up":"Down"}function d(){j=null,k.last&&(k.el.trigger("longTap"),k={})}function e(){j&&clearTimeout(j),j=null}function f(){g&&clearTimeout(g),h&&clearTimeout(h),i&&clearTimeout(i),j&&clearTimeout(j),g=h=i=j=null,k={}}var g,h,i,j,k={},l=750;a(document).ready(function(){var m,n;a(document.body).bind("touchstart",function(c){m=Date.now(),n=m-(k.last||m),k.el=a(b(c.touches[0].target)),g&&clearTimeout(g),k.x1=c.touches[0].pageX,k.y1=c.touches[0].pageY,n>0&&250>=n&&(k.isDoubleTap=!0),k.last=m,j=setTimeout(d,l)}).bind("touchmove",function(a){e(),k.x2=a.touches[0].pageX,k.y2=a.touches[0].pageY,Math.abs(k.x1-k.x2)>10&&a.preventDefault()}).bind("touchend",function(b){e(),k.x2&&Math.abs(k.x1-k.x2)>30||k.y2&&Math.abs(k.y1-k.y2)>30?i=setTimeout(function(){k.el.trigger("swipe"),k.el.trigger("swipe"+c(k.x1,k.x2,k.y1,k.y2)),k={}},0):"last"in k&&(h=setTimeout(function(){var b=a.Event("tap");b.cancelTouch=f,k.el.trigger(b),k.isDoubleTap?(k.el.trigger("doubleTap"),k={}):g=setTimeout(function(){g=null,k.el.trigger("singleTap"),k={}},250)},0))}).bind("touchcancel",f),a(window).bind("scroll",f)}),["swipe","swipeLeft","swipeRight","swipeUp","swipeDown","doubleTap","tap","singleTap","longTap"].forEach(function(b){a.fn[b]=function(a){return this.bind(b,a)}})}(Zepto),FastClick.prototype.deviceIsAndroid=navigator.userAgent.indexOf("Android")>0,FastClick.prototype.deviceIsIOS=/iP(ad|hone|od)/.test(navigator.userAgent),FastClick.prototype.deviceIsIOS4=FastClick.prototype.deviceIsIOS&&/OS 4_\d(_\d)?/.test(navigator.userAgent),FastClick.prototype.deviceIsIOSWithBadTarget=FastClick.prototype.deviceIsIOS&&/OS ([6-9]|\d{2})_\d/.test(navigator.userAgent),FastClick.prototype.needsClick=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"button":case"input":return this.deviceIsIOS&&"file"===a.type?!0:a.disabled;case"label":case"video":return!0;default:return/\bneedsclick\b/.test(a.className)}},FastClick.prototype.needsFocus=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"textarea":case"select":return!0;case"input":switch(a.type){case"button":case"checkbox":case"file":case"image":case"radio":case"submit":return!1}return!a.disabled&&!a.readOnly;default:return/\bneedsfocus\b/.test(a.className)}},FastClick.prototype.sendClick=function(a,b){"use strict";var c,d;document.activeElement&&document.activeElement!==a&&document.activeElement.blur(),d=b.changedTouches[0],c=document.createEvent("MouseEvents"),c.initMouseEvent("click",!0,!0,window,1,d.screenX,d.screenY,d.clientX,d.clientY,!1,!1,!1,!1,0,null),c.forwardedTouchEvent=!0,a.dispatchEvent(c)},FastClick.prototype.focus=function(a){"use strict";var b;this.deviceIsIOS&&a.setSelectionRange?(b=a.value.length,a.setSelectionRange(b,b)):a.focus()},FastClick.prototype.updateScrollParent=function(a){"use strict";var b,c;if(b=a.fastClickScrollParent,!b||!b.contains(a)){c=a;do{if(c.scrollHeight>c.offsetHeight){b=c,a.fastClickScrollParent=c;break}c=c.parentElement}while(c)}b&&(b.fastClickLastScrollTop=b.scrollTop)},FastClick.prototype.getTargetElementFromEventTarget=function(a){"use strict";return a.nodeType===Node.TEXT_NODE?a.parentNode:a},FastClick.prototype.onTouchStart=function(a){"use strict";var b,c,d;if(b=this.getTargetElementFromEventTarget(a.target),c=a.targetTouches[0],this.deviceIsIOS){if(d=window.getSelection(),d.rangeCount&&!d.isCollapsed)return!0;if(!this.deviceIsIOS4){if(c.identifier===this.lastTouchIdentifier)return a.preventDefault(),!1;this.lastTouchIdentifier=c.identifier,this.updateScrollParent(b)}}return this.trackingClick=!0,this.trackingClickStart=a.timeStamp,this.targetElement=b,this.touchStartX=c.pageX,this.touchStartY=c.pageY,a.timeStamp-this.lastClickTime<200&&a.preventDefault(),!0},FastClick.prototype.touchHasMoved=function(a){"use strict";var b=a.changedTouches[0];return Math.abs(b.pageX-this.touchStartX)>10||Math.abs(b.pageY-this.touchStartY)>10?!0:!1},FastClick.prototype.findControl=function(a){"use strict";return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")},FastClick.prototype.onTouchEnd=function(a){"use strict";var b,c,d,e,f,g=this.targetElement;if(this.touchHasMoved(a)&&(this.trackingClick=!1,this.targetElement=null),!this.trackingClick)return!0;if(a.timeStamp-this.lastClickTime<200)return this.cancelNextClick=!0,!0;if(this.lastClickTime=a.timeStamp,c=this.trackingClickStart,this.trackingClick=!1,this.trackingClickStart=0, -this.deviceIsIOSWithBadTarget&&(f=a.changedTouches[0],g=document.elementFromPoint(f.pageX-window.pageXOffset,f.pageY-window.pageYOffset)),d=g.tagName.toLowerCase(),"label"===d){if(b=this.findControl(g)){if(this.focus(g),this.deviceIsAndroid)return!1;g=b}}else if(this.needsFocus(g))return a.timeStamp-c>100||this.deviceIsIOS&&window.top!==window&&"input"===d?(this.targetElement=null,!1):(this.focus(g),this.deviceIsIOS4&&"select"===d||(this.targetElement=null,a.preventDefault()),!1);return this.deviceIsIOS&&!this.deviceIsIOS4&&(e=g.fastClickScrollParent,e&&e.fastClickLastScrollTop!==e.scrollTop)?!0:(this.needsClick(g)||(a.preventDefault(),this.sendClick(g,a)),!1)},FastClick.prototype.onTouchCancel=function(){"use strict";this.trackingClick=!1,this.targetElement=null},FastClick.prototype.onMouse=function(a){"use strict";return this.targetElement?a.forwardedTouchEvent?!0:a.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(a.stopImmediatePropagation?a.stopImmediatePropagation():a.propagationStopped=!0,a.stopPropagation(),a.preventDefault(),!1):!0:!0},FastClick.prototype.onClick=function(a){"use strict";var b;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===a.target.type&&0===a.detail?!0:(b=this.onMouse(a),b||(this.targetElement=null),b)},FastClick.prototype.destroy=function(){"use strict";var a=this.layer;this.deviceIsAndroid&&(a.removeEventListener("mouseover",this.onMouse,!0),a.removeEventListener("mousedown",this.onMouse,!0),a.removeEventListener("mouseup",this.onMouse,!0)),a.removeEventListener("click",this.onClick,!0),a.removeEventListener("touchstart",this.onTouchStart,!1),a.removeEventListener("touchend",this.onTouchEnd,!1),a.removeEventListener("touchcancel",this.onTouchCancel,!1)},FastClick.attach=function(a){"use strict";return new FastClick(a)},"undefined"!=typeof define&&define.amd&&define(function(){"use strict";return FastClick}),"undefined"!=typeof module&&module.exports&&(module.exports=FastClick.attach,module.exports.FastClick=FastClick);var OSUR={util:{}};OSUR.util.hasTouchEvents=function(){var a;return("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)&&(a=!0),a},$(function(){$(".command-once").on("click",function(a){$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),$(".command-repeater").on("mousedown touchstart",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_start",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!0)}),$(".command-repeater").on("mouseup touchend touchleave touchcancel",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_stop",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!1)}),$(window).on("mouseup touchend touchleave touchcancel",function(a){$(".command-repeater[data-active=true]").trigger("mouseup")}),$(".macro-link").on("click",function(a){a.preventDefault(),$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),OSUR.util.hasTouchEvents()?($("body").addClass("has-touch"),$(".command-link, .remote-link").on("touchstart",function(a){$(this).addClass("active")}),$(".command-link, .remote-link").on("touchend touchleave touchcancel",function(a){$(this).removeClass("active")}),$("body").on("touchcancel",function(a){$(".command-link").removeClass("active")})):$("body").addClass("no-touch"),$(".back").on("click",function(a){$(".remote.active").removeClass("active"),$(".remotes-nav").removeClass("hidden"),$(".macros-nav").removeClass("hidden"),$(".back").addClass("hidden"),$("#title").html($("#title").attr("data-text")),$("#titlebar").removeClass("is-remote")}),$(".remotes-nav a").on("click",function(a){a.preventDefault();var b=$(this).attr("href");$(".remotes-nav").addClass("hidden"),$(".macros-nav").addClass("hidden"),$(b).addClass("active"),$(".back").removeClass("hidden"),$("#title").html($(this).html()),$("#titlebar").addClass("is-remote")}),OSUR.fastClick=new FastClick(document.body)}); \ No newline at end of file +this.deviceIsIOSWithBadTarget&&(f=a.changedTouches[0],g=document.elementFromPoint(f.pageX-window.pageXOffset,f.pageY-window.pageYOffset)),d=g.tagName.toLowerCase(),"label"===d){if(b=this.findControl(g)){if(this.focus(g),this.deviceIsAndroid)return!1;g=b}}else if(this.needsFocus(g))return a.timeStamp-c>100||this.deviceIsIOS&&window.top!==window&&"input"===d?(this.targetElement=null,!1):(this.focus(g),this.deviceIsIOS4&&"select"===d||(this.targetElement=null,a.preventDefault()),!1);return this.deviceIsIOS&&!this.deviceIsIOS4&&(e=g.fastClickScrollParent,e&&e.fastClickLastScrollTop!==e.scrollTop)?!0:(this.needsClick(g)||(a.preventDefault(),this.sendClick(g,a)),!1)},FastClick.prototype.onTouchCancel=function(){"use strict";this.trackingClick=!1,this.targetElement=null},FastClick.prototype.onMouse=function(a){"use strict";return this.targetElement?a.forwardedTouchEvent?!0:a.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(a.stopImmediatePropagation?a.stopImmediatePropagation():a.propagationStopped=!0,a.stopPropagation(),a.preventDefault(),!1):!0:!0},FastClick.prototype.onClick=function(a){"use strict";var b;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===a.target.type&&0===a.detail?!0:(b=this.onMouse(a),b||(this.targetElement=null),b)},FastClick.prototype.destroy=function(){"use strict";var a=this.layer;this.deviceIsAndroid&&(a.removeEventListener("mouseover",this.onMouse,!0),a.removeEventListener("mousedown",this.onMouse,!0),a.removeEventListener("mouseup",this.onMouse,!0)),a.removeEventListener("click",this.onClick,!0),a.removeEventListener("touchstart",this.onTouchStart,!1),a.removeEventListener("touchend",this.onTouchEnd,!1),a.removeEventListener("touchcancel",this.onTouchCancel,!1)},FastClick.attach=function(a){"use strict";return new FastClick(a)},"undefined"!=typeof define&&define.amd&&define(function(){"use strict";return FastClick}),"undefined"!=typeof module&&module.exports&&(module.exports=FastClick.attach,module.exports.FastClick=FastClick);var OSUR={util:{}};OSUR.util.hasTouchEvents=function(){var a;return("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)&&(a=!0),a},$(function(){function a(a,b){console.log(JSON.stringify(b,null,2)),0==b.state?a.removeClass("switch_on"):a.addClass("switch_on")}function b(b){console.log(JSON.stringify(b));var c,d;for(c=0;c +
+ +
    + {% for io in gpios %} +
  • + {% endfor %} +
diff --git a/test/fixtures/config.json b/test/fixtures/config.json index 18a1ba6..8bac125 100644 --- a/test/fixtures/config.json +++ b/test/fixtures/config.json @@ -7,6 +7,9 @@ }, "macros": { "Play Xbox 360": [ + [ "gpio", "TV", 1], + [ "gpio", "Receiver", 1], + [ "gpio", "Xbox", 1], [ "SonyTV", "Power" ], [ "SonyTV", "Xbox360" ], [ "Yamaha", "Power" ], @@ -14,13 +17,20 @@ [ "Xbox360", "Power" ] ], "Listen to Music / Jams": [ + [ "gpio", "Receiver", 1], [ "Yamaha", "Power" ], [ "Yamaha", "AirPlay" ] ], + "all off": [ + [ "gpio", "TV", 0], + [ "gpio", "Receiver", 0], + [ "gpio", "Xbox", 0] + ], "Macro With Delay": [ [ "delay", 500 ], [ "Yamaha", "Power" ] ] + }, "commandLabels": { "LircNamespace": { @@ -42,5 +52,13 @@ "S7", "S8" ] - } + }, + "gpios": [ + {"name": "Receiver", + "pin": 19}, + {"name": "TV", + "pin": 16}, + {"name": "Xbox", + "pin": 13} + ] } diff --git a/test/lib/gpio.js b/test/lib/gpio.js new file mode 100644 index 0000000..ce55aad --- /dev/null +++ b/test/lib/gpio.js @@ -0,0 +1,74 @@ +var gpio = require('../../lib/gpio'); +var assert = require('assert'); +var gpioProbe = require('./wiring-pi-mock.js'); + +var config = [ + { 'name': 'a', 'pin': 47, 'state': 0 }, + { 'name': 'b', 'pin': 11, 'state': 0 }]; + +describe('gpio', function () { + var realWpi; + before(function () { + gpioProbe.initPinsWith(0); + realWpi = gpio.overrideWiringPi(gpioProbe); + gpio.init(config); + }); + + after(function () { + gpio.overrideWiringPi(realWpi); + }); + + describe('updatePinStates', function () { + it('should update all pin states', function () { + gpioProbe.initPinsWith(1); + gpio.updatePinStates(); + assert.deepEqual( + config, + [{ 'name': 'a', 'pin': 47, 'state': 1 }, + { 'name': 'b', 'pin': 11, 'state': 1 }], + 'states are not updated properly'); + }); + }); + + describe('togglePin', function () { + it('should change active pin to inactive', function () { + var res; + gpioProbe.initPinsWith(1); + res = gpio.togglePin(47); + assert.deepEqual( + res, + { 'pin': 47, 'state': 0 }, + 'pin has not changed its state'); + }); + + it('should change inactive pin to active', function () { + var res; + gpioProbe.initPinsWith(0); + res = gpio.togglePin(47); + assert.deepEqual( + res, + { 'pin': 47, 'state': 1 }, + 'pin has not changed its state'); + }); + }); + + describe('setPin', function () { + it('should set a pin by name to the given value', function () { + gpioProbe.initPinsWith(0); + gpio.setPin('a', 1); + assert.equal(gpioProbe.emulatedPins[47], 1); + gpio.setPin('a', 1); + assert.equal(gpioProbe.emulatedPins[47], 1); + gpio.setPin('a', 0); + assert.equal(gpioProbe.emulatedPins[47], 0); + }); + }); + + describe('init', function () { + it('should initialize the wiring_pi library to use gpio address schema', function () { + assert.equal(gpioProbe.schema, 'gpio'); + }); + }); +}); + +// vim: expandtab:tabstop=8:softtabstop=2:shiftwidth=2 diff --git a/test/lib/macros.js b/test/lib/macros.js index 9dd6784..96a6257 100644 --- a/test/lib/macros.js +++ b/test/lib/macros.js @@ -3,7 +3,21 @@ var assert = require('assert'); var sinon = require('sinon'); // config fixture -var config = require('../fixtures/config.json'); +var config = { + 'macros': { + 'Play Xbox 360': [ + ['SonyTV', 'Power'], + ['SonyTV', 'Xbox360'], + ['Yamaha', 'Power'], + ['Yamaha', 'Xbox360'], + ['Xbox360', 'Power'], + ], + 'Macro With Delay': [ + ['delay', 500], + ['Yamaha', 'Power'], + ], + }, +}; describe('macros', function () { var lircNode; diff --git a/test/lib/wiring-pi-mock.js b/test/lib/wiring-pi-mock.js new file mode 100644 index 0000000..76e9371 --- /dev/null +++ b/test/lib/wiring-pi-mock.js @@ -0,0 +1,20 @@ +module.exports = { + emulatedPins: new Array(50), + schema: null, + digitalRead: function (pin) { + return this.emulatedPins[pin]; + }, + digitalWrite: function (pin, state) { + this.emulatedPins[pin] = state; + return this.emulatedPins[pin]; + }, + setup: function (schema) { + this.schema = schema; + }, + initPinsWith: function (value) { + var i; + for (i = 0; i < this.emulatedPins.length; i++) { + this.emulatedPins[i] = value; + } + }, +}; diff --git a/test/lirc_web.js b/test/lirc_web.js index ac8cfd4..bcf1d70 100644 --- a/test/lirc_web.js +++ b/test/lirc_web.js @@ -4,8 +4,14 @@ var request = require('supertest'); var jsdom = require('jsdom'); var fs = require('fs'); var jquery = fs.readFileSync('node_modules/jquery/dist/jquery.js', 'utf-8'); +var configFixture = require(__dirname + '/fixtures/config.json'); +var gpioMock = require('./lib/wiring-pi-mock'); +var gpio = require('../lib/gpio'); describe('lirc_web', function () { + before(function () { + gpio.init(configFixture.gpios); + }); describe('routes', function () { // Root route it('should have an index route "/"', function (done) { @@ -25,6 +31,10 @@ describe('lirc_web', function () { assert(request(app).get('/remotes/Xbox360.json').expect(200, done)); }); + it('should return 404 for unknown remote', function (done) { + assert(request(app).get('/remotes/DOES_NOT_EXIST.json').expect(404, done)); + }); + it('should have GET route for JSON list of commands for macro', function (done) { assert(request(app).get('/macros/Play%20Xbox%20360.json').expect(200, done)); }); @@ -33,8 +43,8 @@ describe('lirc_web', function () { assert(request(app).get('/macros/Listen%20to%20Music%20%2F%20Jams.json').expect(200, done)); }); - it('should return 404 for unknown remote', function (done) { - assert(request(app).get('/remotes/DOES_NOT_EXIST.json').expect(404, done)); + it('should have GET route for JSON list of gpio pins', function (done) { + assert(request(app).get('/gpios.json').expect(200, done)); }); // Sending commands @@ -54,6 +64,22 @@ describe('lirc_web', function () { it('should have POST route for sending a macro', function (done) { assert(request(app).post('/macros/Play%20Xbox%20360').expect(200, done)); }); + + // Sending GPIO change requests + it('should have POST route for gpio toggle', function (done) { + var before = 0; + gpioMock.initPinsWith(before); + request(app) + .post('/gpios/26') + .set('Accept', 'application/json') + .expect(200, function (err, result) { + var pinDescription = JSON.parse(result.text); + assert(pinDescription.pin, 26, 'response contains requested pin number'); + assert(pinDescription.state, 1, 'response contains requested pin state'); + assert.equal(gpioMock.emulatedPins[26], 1, 'pin state has been changed'); + done(); + }); + }); }); describe('index action', function () { @@ -100,6 +126,15 @@ describe('lirc_web', function () { assert(elem.textContent.match(/LircNamespace/) === null); }); }); + + it('should contain all gpio buttons', function () { + var gpioButtons = $('button.gpio-link'); + assert.equal(gpioButtons.length, configFixture.gpios.length); + gpioButtons.each(function (idx, elem) { + var pin = $(elem).attr('pin'); + assert.equal(pin, configFixture.gpios[idx].pin); + }); + }); }); describe('json api', function () { From 1ab66801ae2503580c2299c9480659de9ca9c6d8 Mon Sep 17 00:00:00 2001 From: Ovis Date: Sat, 16 Jan 2016 11:50:11 +0100 Subject: [PATCH 2/3] improved log output on used configuration --- CHANGELOG.md | 1 + app.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7596396..436acaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ As of `v0.1.0`, this project adheres to [Semantic Versioning](http://semver.org/ ## Unreleased * Adds support for gpio controlled devices (thanks @OvisMaximus) +* Improves log output on used configuration (thanks @OvisMaximus) ## [0.2.4] - 2016-01-13 diff --git a/app.js b/app.js index 407c570..0f5c80b 100644 --- a/app.js +++ b/app.js @@ -42,20 +42,27 @@ app.use(compress()); app.use(express.static(__dirname + '/static')); function _init() { - var home = process.env.HOME; + var searchPaths = []; + + function configure(configFileName) { + searchPaths.push(configFileName); + config = require(configFileName); + console.log('Open Source Universal Remote is configured by ' + configFileName); + } lircNode.init(); // Config file is optional try { try { - config = require(__dirname + '/config.json'); + configure(__dirname + '/config.json'); } catch (e) { - config = require(home + '/.lirc_web_config.json'); + configure(process.env.HOME + '/.lirc_web_config.json'); } } catch (e) { console.log('DEBUG:', e); console.log('WARNING: Cannot find config.json!'); + console.log('DEBUG: tried: ' + JSON.stringify(searchPaths)); } } From 150a36cf03377d908f11d8f4fdfa9023585d4022 Mon Sep 17 00:00:00 2001 From: Ovis Maximus Date: Sun, 17 Jan 2016 11:23:45 +0100 Subject: [PATCH 3/3] adopted changed lint configuration --- app.js | 14 +++++++++++--- lib/gpio.js | 4 ++-- lib/labels.js | 4 +++- test/lib/gpio.js | 12 ++++++------ test/lib/macros.js | 2 +- test/lirc_web.js | 23 ++++++++++++++--------- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/app.js b/app.js index 0f5c80b..df7bbc7 100644 --- a/app.js +++ b/app.js @@ -23,6 +23,8 @@ var app = module.exports = express(); // lirc_web configuration var config = {}; +var hasServerPortConfig = false; +var hasSSLConfig = false; // Server & SSL options var port = 3000; @@ -64,6 +66,11 @@ function _init() { console.log('WARNING: Cannot find config.json!'); console.log('DEBUG: tried: ' + JSON.stringify(searchPaths)); } + + hasServerPortConfig = config.server && config.server.port; + hasSSLConfig = + config.server && config.server.ssl && config.server.ssl_cert + && config.server.ssl_key && config.server.ssl_port; } function refineRemotes(myRemotes) { @@ -222,7 +229,7 @@ app.post('/macros/:macro', function (req, res) { gpio.init(config.gpios); // Listen (http) -if (config.server && config.server.port) { +if (hasServerPortConfig) { port = config.server.port; } // only start server, when called as application @@ -232,7 +239,7 @@ if (!module.parent) { } // Listen (https) -if (config.server && config.server.ssl && config.server.ssl_cert && config.server.ssl_key && config.server.ssl_port) { +if (hasSSLConfig) { sslOptions = { key: fs.readFileSync(config.server.ssl_key), cert: fs.readFileSync(config.server.ssl_cert), @@ -240,5 +247,6 @@ if (config.server && config.server.ssl && config.server.ssl_cert && config.serve https.createServer(sslOptions, app).listen(config.server.ssl_port); - console.log('Open Source Universal Remote UI + API has started on port ' + config.server.ssl_port + ' (https).'); + console.log('Open Source Universal Remote UI + API has started on port ' + + config.server.ssl_port + ' (https).'); } diff --git a/lib/gpio.js b/lib/gpio.js index 242f1fe..1fe3f93 100644 --- a/lib/gpio.js +++ b/lib/gpio.js @@ -18,8 +18,8 @@ function togglePin(pinId) { var newState = currentState > 0 ? 0 : 1; wpi.digitalWrite(numericPinId, newState); return { - 'pin': numericPinId, - 'state': newState, + pin: numericPinId, + state: newState, }; } diff --git a/lib/labels.js b/lib/labels.js index ae5efcf..8a55279 100644 --- a/lib/labels.js +++ b/lib/labels.js @@ -1,6 +1,8 @@ module.exports = function Labels(remoteLabels, commandLabels) { function getCommandLabel(remote, command) { - return commandLabels && commandLabels[remote] && commandLabels[remote][command] ? commandLabels[remote][command] : command; + return commandLabels && commandLabels[remote] + && commandLabels[remote][command] + ? commandLabels[remote][command] : command; } function getRemoteLabel(remote) { diff --git a/test/lib/gpio.js b/test/lib/gpio.js index ce55aad..4c44ff9 100644 --- a/test/lib/gpio.js +++ b/test/lib/gpio.js @@ -3,8 +3,8 @@ var assert = require('assert'); var gpioProbe = require('./wiring-pi-mock.js'); var config = [ - { 'name': 'a', 'pin': 47, 'state': 0 }, - { 'name': 'b', 'pin': 11, 'state': 0 }]; + { name: 'a', pin: 47, state: 0 }, + { name: 'b', pin: 11, state: 0 }]; describe('gpio', function () { var realWpi; @@ -24,8 +24,8 @@ describe('gpio', function () { gpio.updatePinStates(); assert.deepEqual( config, - [{ 'name': 'a', 'pin': 47, 'state': 1 }, - { 'name': 'b', 'pin': 11, 'state': 1 }], + [{ name: 'a', pin: 47, state: 1 }, + { name: 'b', pin: 11, state: 1 }], 'states are not updated properly'); }); }); @@ -37,7 +37,7 @@ describe('gpio', function () { res = gpio.togglePin(47); assert.deepEqual( res, - { 'pin': 47, 'state': 0 }, + { pin: 47, state: 0 }, 'pin has not changed its state'); }); @@ -47,7 +47,7 @@ describe('gpio', function () { res = gpio.togglePin(47); assert.deepEqual( res, - { 'pin': 47, 'state': 1 }, + { pin: 47, state: 1 }, 'pin has not changed its state'); }); }); diff --git a/test/lib/macros.js b/test/lib/macros.js index 96a6257..385107f 100644 --- a/test/lib/macros.js +++ b/test/lib/macros.js @@ -4,7 +4,7 @@ var sinon = require('sinon'); // config fixture var config = { - 'macros': { + macros: { 'Play Xbox 360': [ ['SonyTV', 'Power'], ['SonyTV', 'Xbox360'], diff --git a/test/lirc_web.js b/test/lirc_web.js index bcf1d70..abe8a94 100644 --- a/test/lirc_web.js +++ b/test/lirc_web.js @@ -109,7 +109,8 @@ describe('lirc_web', function () { assert(response.headers['content-type'].match(/html/)); }); - it('should return an HTML document in which all button elements of class command-link have an href of the form /remotes/:remote/:command', function () { + it('should return an HTML document in which all button elements of class command-link have an ' + + 'href of the form /remotes/:remote/:command', function () { assert.equal(error, null); $('button.command-link').each(function (idx, elem) { @@ -150,17 +151,21 @@ describe('lirc_web', function () { SonyTV: ['Power', 'VolumeUp', 'VolumeDown', 'ChannelUp', 'ChannelDown'], Xbox360: XBOX_COMMANDS, LightControl: LIGHT_COMMANDS, - LircNamespace: ['KEY_POWER', 'KEY_VOLUMEUP', 'KEY_VOLUMEDOWN', 'KEY_CHANNELUP', 'KEY_CHANNELDOWN'], + LircNamespace: + ['KEY_POWER', 'KEY_VOLUMEUP', 'KEY_VOLUMEDOWN', 'KEY_CHANNELUP', 'KEY_CHANNELDOWN'], }; - it('should return a list of all remotes (and commands) when /remotes.json is accessed', function (done) { - request(app) - .get('/remotes.json') - .set('Accept', 'application/json') - .expect(200, REFINED_REMOTES, done); - }); + it('should return a list of all remotes (and commands) when /remotes.json is accessed', + function (done) { + request(app) + .get('/remotes.json') + .set('Accept', 'application/json') + .expect(200, REFINED_REMOTES, done); + } + ); - it('should return a list of all commands for a remote when /remotes/:remote.json is accessed', function (done) { + it('should return a list of all commands for a remote when /remotes/:remote.json is ' + + 'accessed', function (done) { request(app) .get('/remotes/Xbox360.json') .set('Accept', 'application/json')