From b761904424fa99300a0259cc6205ff4f2a91c7ba Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Tue, 22 May 2018 15:48:24 +0100 Subject: [PATCH 1/3] let Pi nodes be visible/editable on all platforms even where they are not physically available. --- nodes/core/hardware/36-rpi-gpio.js | 343 ++++++++++++++----------- nodes/core/locales/en-US/messages.json | 2 +- 2 files changed, 189 insertions(+), 156 deletions(-) diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index cc1a8ab2a..40c3d0f9b 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -6,35 +6,36 @@ module.exports = function(RED) { var fs = require('fs'); var gpioCommand = __dirname+'/nrgpio'; + var allOK = true; try { var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); - if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); } - } catch(err) { - throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); - } - - try { - fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian - // /usr/lib/python2.7/dist-packages/RPi/GPIO - } catch(err) { + if (cpuinfo.indexOf(": BCM") === -1) { + allOK = false; + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode")); + } try { - fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch - } - catch(err) { + fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian + // /usr/lib/python2.7/dist-packages/RPi/GPIO + } catch(err) { try { - fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot - } - catch(err) { - RED.log.warn(RED._("rpi-gpio.errors.libnotfound")); - throw "Warning : "+RED._("rpi-gpio.errors.libnotfound"); + fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch + } catch(err) { + try { + fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot + } catch(err) { + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound")); + allOK = false; + } } } - } - - if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) { - RED.log.error(RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand})); - throw "Error : "+RED._("rpi-gpio.errors.mustbeexecutable"); + if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) { + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand})); + allOK = false; + } + } catch(err) { + allOK = false; + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode")); } // the magic to make python print stuff immediately @@ -61,48 +62,62 @@ module.exports = function(RED) { } } - if (node.pin !== undefined) { - node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); - node.running = true; - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + if (allOK === true) { + if (node.pin !== undefined) { + node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); + node.running = true; + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - node.child.stdout.on('data', function (data) { - var d = data.toString().trim().split("\n"); - for (var i = 0; i < d.length; i++) { - if (d[i] === '') { return; } - if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { - node.send({ topic:"pi/"+node.pin, payload:Number(d[i]) }); + node.child.stdout.on('data', function (data) { + var d = data.toString().trim().split("\n"); + for (var i = 0; i < d.length; i++) { + if (d[i] === '') { return; } + if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { + node.send({ topic:"pi/"+node.pin, payload:Number(d[i]) }); + } + node.buttonState = d[i]; + node.status({fill:"green",shape:"dot",text:d[i]}); + if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } } - node.buttonState = d[i]; - node.status({fill:"green",shape:"dot",text:d[i]}); - if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } - } - }); + }); - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); - node.child.on('close', function (code) { - node.running = false; - node.child = null; - if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } - if (node.done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); + node.child.on('close', function (code) { + node.running = false; + node.child = null; + if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } + if (node.done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error",{error:err.errno})) } - }); + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error",{error:err.errno})) } + }); + } + else { + node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + } } else { - node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + if (node.read === true) { + var val; + if (node.intype == "up") { val = 1; } + if (node.intype == "down") { val = 0; } + setTimeout(function(){ + node.send({ topic:"pi/"+node.pin, payload:val }); + node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:val})}); + },250); + } } node.on("close", function(done) { @@ -155,20 +170,83 @@ module.exports = function(RED) { else { node.warn(RED._("rpi-gpio.errors.invalidinput")+": "+out); } } - if (node.pin !== undefined) { - if (node.set && (node.out === "out")) { - node.child = spawn(gpioCommand, [node.out,node.pin,node.level]); - node.status({fill:"green",shape:"dot",text:node.level}); - } else { - node.child = spawn(gpioCommand, [node.out,node.pin,node.freq]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - } - node.running = true; + if (allOK === true) { + if (node.pin !== undefined) { + if (node.set && (node.out === "out")) { + node.child = spawn(gpioCommand, [node.out,node.pin,node.level]); + node.status({fill:"green",shape:"dot",text:node.level}); + } else { + node.child = spawn(gpioCommand, [node.out,node.pin,node.freq]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + } + node.running = true; - node.on("input", inputlistener); + node.on("input", inputlistener); + + node.child.stdout.on('data', function (data) { + if (RED.settings.verbose) { node.log("out: "+data+" :"); } + }); + + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); + + node.child.on('close', function (code) { + node.child = null; + node.running = false; + if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } + if (node.done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); + + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } + }); + + } + else { + node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + } + } + else { + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + node.on("input", function(msg){ + node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:msg.payload.toString()})}); + }); + } + + node.on("close", function(done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + delete pinsInUse[node.pin]; + if (node.child != null) { + node.done = done; + node.child.stdin.write("close "+node.pin); + node.child.kill('SIGKILL'); + } + else { done(); } + }); + + } + RED.nodes.registerType("rpi-gpio out",GPIOOutNode); + + function PiMouseNode(n) { + RED.nodes.createNode(this,n); + this.butt = n.butt || 7; + var node = this; + + if (allOK === true) { + node.child = spawn(gpioCommand+".py", ["mouse",node.butt]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); node.child.stdout.on('data', function (data) { - if (RED.settings.verbose) { node.log("out: "+data+" :"); } + data = Number(data); + if (data !== 0) { node.send({ topic:"pi/mouse", button:data, payload:1 }); } + else { node.send({ topic:"pi/mouse", button:data, payload:0 }); } }); node.child.stderr.on('data', function (data) { @@ -192,69 +270,19 @@ module.exports = function(RED) { else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } }); + node.on("close", function(done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + if (node.child != null) { + node.done = done; + node.child.kill('SIGINT'); + node.child = null; + } + else { done(); } + }); } else { - node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); } - - node.on("close", function(done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - delete pinsInUse[node.pin]; - if (node.child != null) { - node.done = done; - node.child.stdin.write("close "+node.pin); - node.child.kill('SIGKILL'); - } - else { done(); } - }); - - } - RED.nodes.registerType("rpi-gpio out",GPIOOutNode); - - function PiMouseNode(n) { - RED.nodes.createNode(this,n); - this.butt = n.butt || 7; - var node = this; - - node.child = spawn(gpioCommand+".py", ["mouse",node.butt]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - - node.child.stdout.on('data', function (data) { - data = Number(data); - if (data === 1) { node.send({ topic:"pi/mouse", button:data, payload:1 }); } - else { node.send({ topic:"pi/mouse", button:data, payload:0 }); } - }); - - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); - - node.child.on('close', function (code) { - node.child = null; - node.running = false; - if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } - if (node.done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); - - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } - }); - - node.on("close", function(done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - if (node.child != null) { - node.done = done; - node.child.kill('SIGINT'); - node.child = null; - } - else { done(); } - }); } RED.nodes.registerType("rpi-mouse",PiMouseNode); @@ -262,39 +290,40 @@ module.exports = function(RED) { RED.nodes.createNode(this,n); var node = this; - node.child = spawn(gpioCommand+".py", ["kbd","0"]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + if (allOK === true) { + node.child = spawn(gpioCommand+".py", ["kbd","0"]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - node.child.stdout.on('data', function (data) { - var b = data.toString().trim().split(","); - var act = "up"; - if (b[1] === "1") { act = "down"; } - if (b[1] === "2") { act = "repeat"; } - node.send({ topic:"pi/key", payload:Number(b[0]), action:act }); - }); + node.child.stdout.on('data', function (data) { + var b = data.toString().trim().split(","); + var act = "up"; + if (b[1] === "1") { act = "down"; } + if (b[1] === "2") { act = "repeat"; } + node.send({ topic:"pi/key", payload:Number(b[0]), action:act }); + }); - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); - node.child.on('close', function (code) { - node.running = false; - node.child = null; - if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } - if (node.done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); + node.child.on('close', function (code) { + node.running = false; + node.child = null; + if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } + if (node.done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } - }); + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } + }); - node.on("close", function(done) { + node.on("close", function(done) { node.status({}); if (node.child != null) { node.done = done; @@ -303,6 +332,10 @@ module.exports = function(RED) { } else { done(); } }); + } + else { + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + } } RED.nodes.registerType("rpi-keyboard",PiKeyboardNode); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 74ec0bcd4..15d66c9dc 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -787,7 +787,7 @@ }, "errors": { "ignorenode": "Ignoring Raspberry Pi specific node", - "version": "Version command failed", + "version": "Failed to get version from Pi", "sawpitype": "Saw Pi Type", "libnotfound": "Cannot find Pi RPi.GPIO python library", "alreadyset": "GPIO pin __pin__ already set as type: __type__", From 7dd329b5ee589f9477e7a2e838e3cdbb066b24a9 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Tue, 22 May 2018 17:26:52 +0100 Subject: [PATCH 2/3] Add basic loading tests for GPIO nodes --- nodes/core/hardware/36-rpi-gpio.js | 26 +++--- nodes/core/locales/en-US/messages.json | 2 +- test/nodes/core/hardware/36-rpi-gpio_spec.js | 90 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 test/nodes/core/hardware/36-rpi-gpio_spec.js diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index 40c3d0f9b..0a21f578a 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -340,20 +340,22 @@ module.exports = function(RED) { RED.nodes.registerType("rpi-keyboard",PiKeyboardNode); var pitype = { type:"" }; - exec(gpioCommand+" info", function(err,stdout,stderr) { - if (err) { - RED.log.info(RED._("rpi-gpio.errors.version")); - } - else { - try { - var info = JSON.parse( stdout.trim().replace(/\'/g,"\"") ); - pitype.type = info["TYPE"]; + if (allOK === true) { + exec(gpioCommand+" info", function(err,stdout,stderr) { + if (err) { + RED.log.info(RED._("rpi-gpio.errors.version")); } - catch(e) { - RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim()); + else { + try { + var info = JSON.parse( stdout.trim().replace(/\'/g,"\"") ); + pitype.type = info["TYPE"]; + } + catch(e) { + RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim()); + } } - } - }); + }); + } RED.httpAdmin.get('/rpi-gpio/:id', RED.auth.needsPermission('rpi-gpio.read'), function(req,res) { res.json(pitype); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 15d66c9dc..d59f3d1bf 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -786,7 +786,7 @@ "na": "N/A : __value__" }, "errors": { - "ignorenode": "Ignoring Raspberry Pi specific node", + "ignorenode": "Raspberry Pi specific node set inactive", "version": "Failed to get version from Pi", "sawpitype": "Saw Pi Type", "libnotfound": "Cannot find Pi RPi.GPIO python library", diff --git a/test/nodes/core/hardware/36-rpi-gpio_spec.js b/test/nodes/core/hardware/36-rpi-gpio_spec.js new file mode 100644 index 000000000..3933154c0 --- /dev/null +++ b/test/nodes/core/hardware/36-rpi-gpio_spec.js @@ -0,0 +1,90 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var rpi = require("../../../../nodes/core/hardware/36-rpi-gpio.js"); +var helper = require("node-red-node-test-helper"); +var fs = require("fs"); + +describe('RPI GPIO Node', function() { + + before(function(done) { + helper.startServer(done); + }); + + after(function(done) { + helper.stopServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + var checkIgnore = function(done) { + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return ((evt[0].level == 30) && (evt[0].msg.indexOf("rpi-gpio")===0)); + }); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("rpi-gpio : rpi-gpio.errors.ignorenode"); + done(); + } catch(err) { + done(err); + } + },25); + } + + it('should load Input node', function(done) { + var flow = [{id:"n1", type:"rpi-gpio in", name:"rpi-gpio in" }]; + helper.load(rpi, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'rpi-gpio in'); + try { + var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); + if (cpuinfo.indexOf(": BCM") === 1) { + done(); // It's ON a PI ... should really do more tests ! + } else { + checkIgnore(done); + } + } + catch(e) { + checkIgnore(done); + } + }); + }); + + it('should load Output node', function(done) { + var flow = [{id:"n1", type:"rpi-gpio out", name:"rpi-gpio out" }]; + helper.load(rpi, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'rpi-gpio out'); + try { + var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); + if (cpuinfo.indexOf(": BCM") === 1) { + done(); // It's ON a PI ... should really do more tests ! + } else { + checkIgnore(done); + } + } + catch(e) { + checkIgnore(done); + } + }); + }); + +}); From 1d05b4c9814c656c2be9683d88eac4efae96eb41 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Wed, 23 May 2018 08:58:04 +0100 Subject: [PATCH 3/3] relax test spec slightly --- test/nodes/core/hardware/36-rpi-gpio_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/nodes/core/hardware/36-rpi-gpio_spec.js b/test/nodes/core/hardware/36-rpi-gpio_spec.js index 3933154c0..c11ea254c 100644 --- a/test/nodes/core/hardware/36-rpi-gpio_spec.js +++ b/test/nodes/core/hardware/36-rpi-gpio_spec.js @@ -39,7 +39,6 @@ describe('RPI GPIO Node', function() { var logEvents = helper.log().args.filter(function(evt) { return ((evt[0].level == 30) && (evt[0].msg.indexOf("rpi-gpio")===0)); }); - logEvents.should.have.length(1); logEvents[0][0].should.have.a.property('msg'); logEvents[0][0].msg.toString().should.startWith("rpi-gpio : rpi-gpio.errors.ignorenode"); done();