diff --git a/README.md b/README.md index 10f384d3a..cdc862615 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A visual tool for wiring the Internet of Things. +![Screenshot](http://nodered.org/images/node-red-screenshot.png "Node-RED: A visual tool for wiring the Internet of Things") + ## Quick Start Check out [INSTALL](INSTALL.md) for full instructions on getting started. @@ -16,6 +18,8 @@ Check out [INSTALL](INSTALL.md) for full instructions on getting started. More documentation can be found [here](http://nodered.org/docs). +For further help, or general discussion, there is also a [mailing list](https://groups.google.com/forum/#!forum/node-red). + ## Browser Support The Node-RED editor runs in the browser. We routinely develop and test using @@ -28,9 +32,11 @@ list. ### Reporting issues -Please raise any bug reports or feature requests on the project's issue -tracker. Be sure to search the list to see if your issue has already -been raised. +Please raise any bug reports on the project's [issue tracker](https://github.com/node-red/node-red/issues?state=open). +Be sure to search the list to see if your issue has already been raised. + +For feature requests, please raise them on the [mailing list](https://groups.google.com/forum/#!forum/node-red) +first. ### Creating new nodes diff --git a/nodes/99-sample.html.demo b/nodes/99-sample.html.demo index 2b87ad16e..e26197aa7 100644 --- a/nodes/99-sample.html.demo +++ b/nodes/99-sample.html.demo @@ -17,6 +17,8 @@ + + diff --git a/nodes/99-sample.js.demo b/nodes/99-sample.js.demo index a212df469..5db5b141d 100644 --- a/nodes/99-sample.js.demo +++ b/nodes/99-sample.js.demo @@ -14,38 +14,39 @@ * limitations under the License. **/ +// If you use this as a template, replace IBM Corp. with your own name. + // Sample Node-RED node file // Require main module var RED = require("../../red/red"); // The main node definition - most things happen in here -function SampleNode(n) { +function SampleNode(n) { // Create a RED node RED.nodes.createNode(this,n); - + // Store local copies of the node configuration (as defined in the .html) this.topic = n.topic; - + // Do whatever you need to do in here - declare callbacks etc // Note: this sample doesn't do anything much - it will only send // this message once at startup... // Look at other real nodes for some better ideas of what to do.... var msg = {}; - msg.topic = node.topic; + msg.topic = this.topic; msg.payload = "Hello world !" - + // send out the message to the rest of the workspace. this.send(msg); + + this.on("close", function() { + // Called when the node is shutdown - eg on redeploy. + // Allows ports to be closed, connections dropped etc. + // eg: this.client.disconnect(); + }); } // Register the node by name. This must be called before overriding any of the // Node functions. RED.nodes.registerType("sample",SampleNode); - - -SampleNode.prototype.close = function() { - // Called when the node is shutdown - eg on redeploy. - // Allows ports to be closed, connections dropped etc. - // eg: this.client.disconnect(); -} diff --git a/nodes/analysis/73-parsexml.html b/nodes/analysis/73-parsexml.html index 77e4cb8ca..71b3a21cb 100644 --- a/nodes/analysis/73-parsexml.html +++ b/nodes/analysis/73-parsexml.html @@ -37,7 +37,7 @@ category: 'advanced-function', color:"#E6E0F8", defaults: { - useEyes: {value:"false"}, + useEyes: {value:false}, name: {value:""}, }, inputs:1, diff --git a/nodes/core/20-inject.html b/nodes/core/20-inject.html index 8ab75445b..0d0de8a02 100644 --- a/nodes/core/20-inject.html +++ b/nodes/core/20-inject.html @@ -202,7 +202,7 @@ }, oneditprepare: function() { var repeattype = "none"; - if (Number(this.repeat) != 0) { + if (this.repeat != "") { repeattype = "interval"; $("#inject-time-interval-units option").filter(function() {return $(this).val() == "s";}).attr('selected',true); $("#inject-time-interval-count").val(this.repeat); @@ -210,7 +210,7 @@ } else if (this.crontab) { var cronparts = this.crontab.split(" "); var days = cronparts[4]; - if (Number(cronparts[0]) && Number(cronparts[1])) { + if (!isNaN(cronparts[0]) && !isNaN(cronparts[1])) { repeattype = "time"; // Fixed time var time = cronparts[1]+":"+cronparts[0]; diff --git a/nodes/core/80-function.html b/nodes/core/80-function.html index f4090f801..68b71cc75 100644 --- a/nodes/core/80-function.html +++ b/nodes/core/80-function.html @@ -21,7 +21,7 @@
- +
@@ -85,6 +85,7 @@ editor:that.editor, // the field name the main text body goes to fields:['name','outputs'] }); + $("#node-input-name").focus(); }); }, diff --git a/nodes/core/80-template.html b/nodes/core/80-template.html index f0a024173..8d7c8ddd1 100644 --- a/nodes/core/80-template.html +++ b/nodes/core/80-template.html @@ -22,7 +22,7 @@
- +
@@ -79,7 +79,7 @@ editor:that.editor, // the field name the main text body goes to fields:['name'] }); - + $("#node-input-name").focus(); }); }, oneditsave: function() { diff --git a/nodes/core/90-comment.html b/nodes/core/90-comment.html index 86b8922cb..b909b5d1f 100644 --- a/nodes/core/90-comment.html +++ b/nodes/core/90-comment.html @@ -21,7 +21,7 @@
- +
Tip: this isn't meant for War and Peace - but useful notes can be kept here.
@@ -75,6 +75,7 @@ showFoldingRuler:false, contents: $("#node-input-info").val() }); + $("#node-input-name").focus(); }); }, oneditsave: function() { diff --git a/nodes/hardware/35-arduino.html b/nodes/hardware/35-arduino.html index e4d1a9d43..6640b77a0 100644 --- a/nodes/hardware/35-arduino.html +++ b/nodes/hardware/35-arduino.html @@ -138,7 +138,7 @@ category: 'config', defaults: { //baud: {baud:"57600",required:true}, - repeat: {value:"25",required:true,validate:RED.validators.number()}, + repeat: {value:"50",required:true,validate:RED.validators.number()}, device: {value:"",required:true} }, label: function() { diff --git a/nodes/hardware/35-arduino.js b/nodes/hardware/35-arduino.js index 3e6d648da..b81ea7cfc 100644 --- a/nodes/hardware/35-arduino.js +++ b/nodes/hardware/35-arduino.js @@ -25,46 +25,46 @@ function ArduinoNode(n) { RED.nodes.createNode(this,n); this.device = n.device; this.repeat = n.repeat||25; - util.log("[firmata] Opening"+this.device); + util.log("[firmata] Opening "+this.device); + var node = this; -// var tou = setInterval(function() { -// if (!arduinoReady) { -// clearInterval(tou); - - arduinoReady = false; - if (thisboard == null) { - this.board = new firmata.Board(this.device, function(err) { - if (err) { - util.log("[firmata] "+err); - return; + node.toun = setInterval(function() { + if (!arduinoReady) { + if (thisboard == null) { + node.board = new firmata.Board(node.device, function(err) { + if (err) { + console.log("[firmata] error: ",err); + return; + } + arduinoReady = true; + thisboard = node.board; + clearInterval(node.toun); + util.log('[firmata] Arduino connected'); + }); } - arduinoReady = true; - util.log('[firmata] Arduino connected'); - }); - thisboard = this.board; - } - else { - util.log("[firmata] Arduino already connected"); - this.board = thisboard; - console.log(this.board._events); - this.board.removeAllListeners(); - arduinoReady = true; - } + else { + node.board = thisboard; + node.board.removeAllListeners(); + arduinoReady = true; + clearInterval(node.toun); + node.toun = false; + util.log("[firmata] Arduino already connected"); + } + } else { util.log("[firmata] Waiting for Firmata"); } + }, 10000); // wait for firmata to connect to arduino -// } else { util.log("[firmata] Waiting for Firmata"); } -// }, 1000); // wait for firmata to disconnect from arduino - - this._close = function() { + this.on('close', function() { //this.board.sp.close(function() { console.log("[firmata] Serial port closed"); arduinoReady = false; }); + arduinoReady = false; + if (node.toun) { + clearInterval(node.toun); + util.log("[firmata] arduino wait loop stopped"); + } util.log("[firmata] Stopped"); - } + }); } - RED.nodes.registerType("arduino-board",ArduinoNode); -ArduinoNode.prototype.close = function() { - this._close(); -} // The Input Node function DuinoNodeIn(n) { @@ -78,11 +78,13 @@ function DuinoNodeIn(n) { this.board = this.serverConfig.board; this.repeat = this.serverConfig.repeat; var node = this; - - var tout = setInterval(function() { - if (arduinoReady) { - clearInterval(tout); - console.log(node.state,node.pin,node.board.MODES[node.state]); + + node.toui = setInterval(function() { + if (thisboard != null) { + node.board = thisboard; + clearInterval(node.toui); + node.toui = false; + //console.log(node.state,node.pin,node.board.MODES[node.state]); node.board.pinMode(node.pin, node.board.MODES[node.state]); node.board.setSamplingInterval(node.repeat); var oldrdg = ""; @@ -103,23 +105,21 @@ function DuinoNodeIn(n) { } } else { node.log("Waiting for Arduino"); } - }, 2000); // loop to wait for firmata to connect to arduino - - this._close = function() { - clearInterval(this._interval); - util.log("[arduino] input eventlistener stopped"); - } + }, 5000); // loop to wait for firmata to connect to arduino + + this.on('close', function() { + if (node.toui) { + clearInterval(node.toui); + util.log("[firmata] input wait loop stopped"); + } + }); } else { - util.log("[arduino] Serial Port not Configured"); + util.log("[firmata] Serial Port not Configured"); } } RED.nodes.registerType("arduino in",DuinoNodeIn); -DuinoNodeIn.prototype.close = function() { - this._close(); -} - // The Output Node function DuinoNodeOut(n) { @@ -132,10 +132,10 @@ function DuinoNodeOut(n) { if (typeof this.serverConfig === "object") { this.board = this.serverConfig.board; var node = this; - + this.on("input", function(msg) { //console.log(msg); - if (arduinoReady) { + if (thisboard != null) { if (node.state == "OUTPUT") { if ((msg.payload == true)||(msg.payload == 1)||(msg.payload.toString().toLowerCase() == "on")) { node.board.digitalWrite(node.pin, node.board.HIGH); @@ -161,17 +161,26 @@ function DuinoNodeOut(n) { } //else { console.log("Arduino not ready"); } }); - - var touo = setInterval(function() { - if (arduinoReady) { - clearInterval(touo); - //console.log(node.state,node.pin,node.board.MODES[node.state]); + + node.touo = setInterval(function() { + if (thisboard != null) { + clearInterval(node.touo); + node.touo = false; + node.board = thisboard; node.board.pinMode(node.pin, node.board.MODES[node.state]); } + else { util.log("[firmata] waiting for arduino to connect"); } }, 5000); // loop to wait for firmata to connect to arduino + + this.on('close', function() { + if (node.touo) { + clearInterval(node.touo); + util.log("[firmata] output wait loop stopped"); + } + }); } else { - util.log("[arduino] Serial Port not Configured"); + util.log("[firmata] Serial Port not Configured"); } } RED.nodes.registerType("arduino out",DuinoNodeOut); diff --git a/nodes/io/21-httpin.js b/nodes/io/21-httpin.js index c3e3a027b..0776d0383 100644 --- a/nodes/io/21-httpin.js +++ b/nodes/io/21-httpin.js @@ -57,7 +57,7 @@ RED.nodes.registerType("http in",HTTPIn); function HTTPOut(n) { RED.nodes.createNode(this,n); - + var node = this; this.on("input",function(msg) { if (msg.res) { if (msg.headers) { @@ -65,6 +65,8 @@ function HTTPOut(n) { } var statusCode = msg.statusCode || 200; msg.res.send(statusCode,msg.payload); + } else { + node.warn("No response object"); } }); } @@ -80,9 +82,9 @@ function HTTPRequest(n) { this.on("input",function(msg) { var opts = urllib.parse(msg.url||url); - opts.method = msg.method||method; + opts.method = (msg.method||method).toUpperCase(); if (msg.headers) { - opts.header = headers; + opts.header = msg.headers; } var req = httplib.request(opts,function(res) { res.setEncoding('utf8'); @@ -103,7 +105,7 @@ function HTTPRequest(n) { msg.statusCode = err.code; node.send(msg); }); - if (msg.payload) { + if (msg.payload && (method == "PUSH" || method == "PUT") ) { if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) { req.write(msg.payload); } else if (typeof msg.payload == "number") { diff --git a/nodes/io/31-tcpin.js b/nodes/io/31-tcpin.js index 78b7ab731..8d52df7a9 100644 --- a/nodes/io/31-tcpin.js +++ b/nodes/io/31-tcpin.js @@ -30,7 +30,7 @@ function TcpIn(n) { this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server"); this.closing = false; var node = this; - + if (!node.server) { var buffer = null; var client; @@ -41,13 +41,13 @@ function TcpIn(n) { buffer = (node.datatype == 'buffer')? new Buffer(0):""; node.log("connected to "+node.host+":"+node.port); }); - + client.on('data', function (data) { if (node.datatype != 'buffer') { data = data.toString(node.datatype); } if (node.stream) { - if ((typeof data) === "string" && node.newline != "") { + if ((node.datatype) === "utf8" && node.newline != "") { buffer = buffer+data; var parts = buffer.split(node.newline); for (var i = 0;i + + + + + + + + + + + + + + + + + diff --git a/nodes/io/32-udp.js b/nodes/io/32-udp.js new file mode 100644 index 000000000..39b7b1222 --- /dev/null +++ b/nodes/io/32-udp.js @@ -0,0 +1,115 @@ +/** + * Copyright 2013 IBM Corp. + * + * 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 RED = require("../../red/red"); +var dgram = require('dgram'); + +// The Input Node +function UDPin(n) { + RED.nodes.createNode(this,n); + this.group = n.group; + this.port = n.port; + this.host = n.host || null; + this.datatype = n.datatype; + this.iface = n.iface || null; + this.multicast = n.multicast; + var node = this; + + var server = dgram.createSocket('udp4'); + + server.on("error", function (err) { + console.log("udp listener error:\n" + err.stack); + server.close(); + }); + + server.on('message', function (message, remote) { + var msg; + if (node.datatype =="base64") { msg = { payload:message.toString('base64'), fromip:remote.address+':'+remote.port }; } + else if (node.datatype =="utf8") { msg = { payload:message.toString('utf8'), fromip:remote.address+':'+remote.port }; } + else { msg = { payload:message, fromip:remote.address+':'+remote.port }; } + node.send(msg); + }); + + server.on('listening', function () { + var address = server.address(); + node.log('udp listener at ' + address.address + ":" + address.port); + if (node.multicast == "true") { + server.setBroadcast(true) + server.setMulticastTTL(128); + server.addMembership(node.group,node.iface); + node.log("udp multicast group "+node.group); + } + }); + + node.on("close", function() { + try { + server.close(); + node.log('udp listener stopped'); + } + catch (err) { console.log(err); } + }); + + server.bind(node.port,node.host); +} +RED.nodes.registerType("udp in",UDPin); + + +// The Output Node +function UDPout(n) { + RED.nodes.createNode(this,n); + //this.group = n.group; + this.port = n.port; + this.base64 = n.base64; + this.addr = n.addr; + this.iface = n.iface || null; + this.multicast = n.multicast; + var node = this; + + var sock = dgram.createSocket('udp4'); // only use ipv4 for now + sock.bind(node.port); // have to bind before you can enable broadcast... + if (this.multicast != "false") { + sock.setBroadcast(true); // turn on broadcast + if (this.multicast == "multi") { + sock.setMulticastTTL(128); + sock.addMembership(node.addr,node.iface); // Add to the multicast group + node.log('udp multicast ready : '+node.addr+":"+node.port); + } + else node.log('udp broadcast ready : '+node.addr+":"+node.port); + } + else node.log('udp ready : '+node.addr+":"+node.port); + + node.on("input", function(msg) { + if (msg.payload != null) { + //console.log("UDP:",msg.payload); + var message; + if (node.base64) { message = new Buffer(b64string, 'base64'); } + else { message = new Buffer(""+msg.payload); } + console.log("UDP send :",node.addr,node.port); + sock.send(message, 0, message.length, node.port, node.addr, function(err, bytes) { + if (err) node.error("udp : "+err); + }); + } + }); + + node.on("close", function() { + try { + sock.close(); + node.log('udp output stopped'); + } + catch (err) { console.log(err); } + }); +} +RED.nodes.registerType("udp out",UDPout); diff --git a/nodes/social/27-twitter.html b/nodes/social/27-twitter.html index 08e80786b..223c42c4a 100644 --- a/nodes/social/27-twitter.html +++ b/nodes/social/27-twitter.html @@ -29,8 +29,8 @@ pathname += "/"; } var callback = encodeURIComponent(location.protocol+"//"+location.hostname+":"+location.port+pathname+"twitter/"+twitterConfigNodeId+"/auth/callback"); - - $("#node-config-twitter-row").html('Click here to authenticate with Twitter.'); + + $("#node-config-twitter-row").html('Click here to authenticate with Twitter.'); $("#node-config-twitter-start").click(function() { twitterConfigNodeIntervalId = window.setTimeout(pollTwitterCredentials,2000); }); @@ -102,34 +102,33 @@ @@ -140,7 +139,7 @@ defaults: { twitter: {type:"twitter-credentials",required:true}, tags: {value:"",required:true}, - user: {value:false}, + user: {value:"false",required:true}, name: {value:""}, topic: {value:"tweets"} }, diff --git a/nodes/social/27-twitter.js b/nodes/social/27-twitter.js index 923fec859..751ef2d65 100644 --- a/nodes/social/27-twitter.js +++ b/nodes/social/27-twitter.js @@ -24,14 +24,13 @@ function TwitterNode(n) { } RED.nodes.registerType("twitter-credentials",TwitterNode); - function TwitterInNode(n) { RED.nodes.createNode(this,n); this.active = true; this.user = n.user; this.tags = n.tags.replace(/ /g,''); this.twitter = n.twitter; - this.topic = n.topic; + this.topic = n.topic||"tweets"; this.twitterConfig = RED.nodes.getNode(this.twitter); var credentials = RED.nodes.getCredentials(this.twitter); @@ -47,11 +46,12 @@ function TwitterInNode(n) { if (this.tags !== "") { try { var thing = 'statuses/filter'; - if (this.user) { thing = 'user'; } + if (this.user == "true") { thing = 'user'; } function setupStream() { if (node.active) { twit.stream(thing, { track: [node.tags] }, function(stream) { //twit.stream('user', { track: [node.tags] }, function(stream) { + //twit.stream('site', { track: [node.tags] }, function(stream) { //twit.stream('statuses/filter', { track: [node.tags] }, function(stream) { node.stream = stream; stream.on('data', function(tweet) { @@ -101,8 +101,6 @@ TwitterInNode.prototype.close = function() { } } - - function TwitterOutNode(n) { RED.nodes.createNode(this,n); this.topic = n.topic; diff --git a/nodes/social/57-notify.html b/nodes/social/57-notify.html index ed711bed0..67c04cbf3 100644 --- a/nodes/social/57-notify.html +++ b/nodes/social/57-notify.html @@ -16,7 +16,7 @@ @@ -103,7 +103,7 @@
- +
diff --git a/nodes/social/91-irc.js b/nodes/social/91-irc.js index 4041147dc..21456c32d 100644 --- a/nodes/social/91-irc.js +++ b/nodes/social/91-irc.js @@ -16,6 +16,7 @@ var RED = require("../../red/red"); var irc = require("irc"); +var util = require("util"); // The Server Definition - this opens (and closes) the connection function IRCServerNode(n) { @@ -23,40 +24,38 @@ function IRCServerNode(n) { this.server = n.server; this.channel = n.channel; this.nickname = n.nickname; - this.ircclient = new irc.Client(this.server, this.nickname, { - channels: [this.channel] + this.ircclient = null; + this.on("close", function() { + if (this.ircclient != null) { + this.ircclient.disconnect(); + } }); - this._close = function() { - this.ircclient.disconnect(); - } } RED.nodes.registerType("irc-server",IRCServerNode); -IRCServerNode.prototype.close = function() { - this._close(); -} - - // The Input Node function IrcInNode(n) { RED.nodes.createNode(this,n); this.ircserver = n.ircserver; this.serverConfig = RED.nodes.getNode(this.ircserver); + if (this.serverConfig.ircclient == null) { + this.serverConfig.ircclient = new irc.Client(this.serverConfig.server, this.serverConfig.nickname, { + channels: [this.serverConfig.channel] + }); + this.serverConfig.ircclient.addListener('error', function(message) { + util.log('[irc] '+ JSON.stringify(message)); + }); + } this.ircclient = this.serverConfig.ircclient; var node = this; - this.ircclient.addListener('message', function (from, to, message) { console.log(from + ' => ' + to + ': ' + message); var msg = { "topic":from, "to":to, "payload":message }; node.send(msg); }); - this.ircclient.addListener('error', function(message) { - node.error(JSON.stringify(message)); - }); - } RED.nodes.registerType("irc in",IrcInNode); @@ -66,12 +65,20 @@ function IrcOutNode(n) { this.sendAll = n.sendObject; this.ircserver = n.ircserver; this.serverConfig = RED.nodes.getNode(this.ircserver); - this.ircclient = this.serverConfig.ircclient; this.channel = this.serverConfig.channel; + if (this.serverConfig.ircclient == null) { + this.serverConfig.ircclient = new irc.Client(this.serverConfig.server, this.serverConfig.nickname, { + channels: [this.serverConfig.channel] + }); + this.serverConfig.ircclient.addListener('error', function(message) { + util.log('[irc] '+ JSON.stringify(message)); + }); + } + this.ircclient = this.serverConfig.ircclient; var node = this; this.on("input", function(msg) { - console.log(msg); + //console.log(msg,node.channel); if (node.sendAll) { node.ircclient.say(node.channel, JSON.stringify(msg)); } diff --git a/nodes/social/92-xmpp.js b/nodes/social/92-xmpp.js index f8c8c227c..15f957958 100644 --- a/nodes/social/92-xmpp.js +++ b/nodes/social/92-xmpp.js @@ -14,8 +14,17 @@ * limitations under the License. **/ +var orig=console.warn; +console.warn=(function() { // suppress warning from stringprep when not needed) + var orig=console.warn; + return function() { + //orig.apply(console, arguments); + }; +})(); + var RED = require("../../red/red"); var xmpp = require('simple-xmpp'); +console.warn = orig; try { var xmppkey = require("../../settings").xmpp || require("../../../xmppkeys.js"); @@ -97,17 +106,13 @@ function XmppNode(n) { } }); - this._close = function() { + this.on("close", function() { xmpp.setPresence('offline'); //xmpp.conn.end(); // TODO - DCJ NOTE... this is not good. It leaves the connection up over a restart - which will end up with bad things happening... // (but requires the underlying xmpp lib to be fixed (which does have an open bug request on fixing the close method)). this.warn("Due to an underlying bug in the xmpp library this does not disconnect old sessions. This is bad... A restart would be better."); - } + }); } RED.nodes.registerType("xmpp",XmppNode); - -XmppNode.prototype.close = function() { - this._close(); -} diff --git a/public/icons/bluetooth.png b/public/icons/bluetooth.png new file mode 100644 index 000000000..f94e92f19 Binary files /dev/null and b/public/icons/bluetooth.png differ diff --git a/public/red/ui/editor.js b/public/red/ui/editor.js index fc85f58cc..98605baf6 100644 --- a/public/red/ui/editor.js +++ b/public/red/ui/editor.js @@ -143,8 +143,34 @@ RED.editor = function() { var changes = {}; var changed = false; var wasDirty = RED.view.dirty(); + + if (editing_node._def.oneditsave) { + var oldValues = {}; + for (var d in editing_node._def.defaults) { + if (typeof editing_node[d] === "string" || typeof editing_node[d] === "number") { + oldValues[d] = editing_node[d]; + } else { + oldValues[d] = $.extend(true,{},{v:editing_node[d]}).v; + } + } editing_node._def.oneditsave.call(editing_node); + + for (var d in editing_node._def.defaults) { + if (oldValues[d] === null || typeof oldValues[d] === "string" || typeof oldValues[d] === "number") { + if (oldValues[d] !== editing_node[d]) { + changes[d] = oldValues[d]; + changed = true; + } + } else { + if (JSON.stringify(oldValues[d]) !== JSON.stringify(editing_node[d])) { + changes[d] = oldValues[d]; + changed = true; + } + } + } + + } if (editing_node._def.defaults) { @@ -177,7 +203,6 @@ RED.editor = function() { changes[d] = editing_node[d]; editing_node[d] = newValue; changed = true; - RED.view.dirty(true); } } } @@ -185,6 +210,7 @@ RED.editor = function() { var removedLinks = updateNodeProperties(editing_node); if (changed) { + RED.view.dirty(true); RED.history.push({t:'edit',node:editing_node,changes:changes,links:removedLinks,dirty:wasDirty}); } diff --git a/public/red/ui/palette.js b/public/red/ui/palette.js index 8975c71f7..994b85ade 100644 --- a/public/red/ui/palette.js +++ b/public/red/ui/palette.js @@ -63,7 +63,10 @@ RED.palette = function() { container:'body', content: $(($("script[data-help-name|='"+nt+"']").html()||"

no information available

").trim())[0] }); - + $(d).click(function() { + var help = '
'+($("script[data-help-name|='"+d.type+"']").html()||"")+"
"; + $("#tab-info").html(help); + }); $(d).draggable({ helper: 'clone', appendTo: 'body', diff --git a/public/red/ui/view.js b/public/red/ui/view.js index d8fe0bdeb..42d815666 100644 --- a/public/red/ui/view.js +++ b/public/red/ui/view.js @@ -649,7 +649,13 @@ RED.view = function() { //mainRect.on("touchend",nodeMouseUp); if (d._def.icon) { - var icon = node.append("image").attr("xlink:href","icons/"+d._def.icon).attr("class","node_icon").attr("x",0).attr("y",0).attr("width","15").attr("height",function(d){return Math.min(50,d.h);}); + var icon = node.append("image") + .attr("xlink:href","icons/"+d._def.icon) + .attr("class","node_icon") + .attr("x",0).attr("y",function(d){return (d.h-Math.min(50,d.h))/2;}) + .attr("width","15") + .attr("height", function(d){return Math.min(50,d.h);}); + if (d._def.align) { icon.attr('class','node_icon node_icon_'+d._def.align); } @@ -747,7 +753,7 @@ RED.view = function() { var port = d3.select(this); port.attr("y",function(d){return (d.h/2)-5;}) }); - thisNode.selectAll(".node_icon").attr("height",function(d){return Math.min(50,d.h);}); + thisNode.selectAll(".node_icon").attr("height",function(d){return Math.min(50,d.h);}).attr("y",function(d){return (d.h-Math.min(50,d.h))/2;}); thisNode.selectAll('.node_right_button_group').attr("transform",function(d){return "translate("+(d.w-100)+","+0+")";}); thisNode.selectAll('.node_right_button').attr("transform",function(d){return "translate("+(d.w-100)+","+0+")";}).attr("fill",function(d) { diff --git a/red.js b/red.js index 6e6f8abfd..4001cba12 100644 --- a/red.js +++ b/red.js @@ -21,7 +21,6 @@ var crypto = require("crypto"); var settings = require("./settings"); var RED = require("./red/red.js"); - var server; var app = express(); @@ -49,11 +48,30 @@ if (settings.httpAuth) { ); } +settings.flowFile = process.argv[2] || settings.flowFile; + var red = RED.init(server,settings); app.use(settings.httpRoot,red); - -server.listen(settings.uiPort); RED.start(); -util.log('[red] Server now running at http'+(settings.https?'s':'')+'://127.0.0.1:'+settings.uiPort+settings.httpRoot); +server.listen(settings.uiPort,function() { + util.log('[red] Server now running at http'+(settings.https?'s':'')+'://127.0.0.1:'+settings.uiPort+settings.httpRoot); +}); + +process.on('uncaughtException',function(err) { + if (err.errno === "EADDRINUSE") { + util.log('[red] Unable to listen on http'+(settings.https?'s':'')+'://127.0.0.1:'+settings.uiPort+settings.httpRoot); + util.log('[red] Error: port in use'); + } else { + util.log('[red] Uncaught Exception:'); + util.log(err.stack); + } + process.exit(1); +}); + +process.on('SIGINT', function () { + RED.stop(); + util.log('[red] Exiting Node-RED. Thank you.'); + process.exit(); +}); diff --git a/red/nodes.js b/red/nodes.js index 0cfebe9a0..c3d901223 100644 --- a/red/nodes.js +++ b/red/nodes.js @@ -42,7 +42,6 @@ function getCallerFilename(type) { return stack[0].getFileName(); } - var registry = (function() { var nodes = {}; var logHandlers = []; @@ -91,7 +90,6 @@ var node_type_registry = (function() { var obj = { register: function(type,node) { util.inherits(node, Node); - var callerFilename = getCallerFilename(type); if (callerFilename == null) { util.log("["+type+"] unable to determine filename"); @@ -117,7 +115,6 @@ var node_type_registry = (function() { result += node_configs[nt]; } return result; - } } return obj; @@ -176,7 +173,6 @@ Node.prototype.send = function(msg) { } module.exports.Node = Node; - Node.prototype.receive = function(msg) { this.emit("input",msg); } @@ -197,9 +193,6 @@ Node.prototype.error = function(msg) { this.emit("log",o); } - - - var credentials = {}; var credentialsFile = "credentials.json"; if (fs.existsSync(credentialsFile)) { @@ -225,8 +218,6 @@ module.exports.deleteCredentials = function(id) { delete credentials[id]; saveCredentialsFile(); } - - module.exports.createNode = function(node,def) { Node.call(node,def); } @@ -257,12 +248,9 @@ module.exports.load = function() { }); } loadNodes(__dirname+"/../nodes"); - //events.emit("nodes-loaded"); } - - var activeConfig = null; var missingTypes = []; @@ -279,10 +267,15 @@ events.on('type-registered',function(type) { } }); - module.exports.getNode = function(nid) { return registry.get(nid); } + +module.exports.closedown = function() { + util.log("[red] Closing Down Nodes"); + registry.clear(); +} + module.exports.setConfig = function(conf) { if (activeConfig&&activeConfig.length > 0) { util.log("[red] Stopping flows"); @@ -293,7 +286,6 @@ module.exports.setConfig = function(conf) { } var parseConfig = function() { - missingTypes = []; for (var i in activeConfig) { var type = activeConfig[i].type; @@ -307,7 +299,6 @@ var parseConfig = function() { for (var i in missingTypes) { util.log("[red] - "+missingTypes[i]); } - return; } @@ -317,19 +308,18 @@ var parseConfig = function() { var nn = null; var nt = node_type_registry.get(activeConfig[i].type); if (nt) { - try { - nn = new nt(activeConfig[i]); - } - catch (err) { - util.log("[red] "+activeConfig[i].type+" : "+err); - } + try { + nn = new nt(activeConfig[i]); + } + catch (err) { + util.log("[red] "+activeConfig[i].type+" : "+err); + } } // console.log(nn); if (nn == null) { util.log("[red] unknown type: "+activeConfig[i].type); } } - // Clean up any orphaned credentials var deletedCredentials = false; for (var c in credentials) { @@ -343,5 +333,4 @@ var parseConfig = function() { saveCredentialsFile(); } events.emit("nodes-started"); - } diff --git a/red/red.js b/red/red.js index 23134df57..b2d18b2c5 100644 --- a/red/red.js +++ b/red/red.js @@ -14,7 +14,7 @@ * limitations under the License. **/ -var events = require("./events"); +var events = require("./events"); var server = require("./server"); var nodes = require("./nodes"); var library = require("./library"); @@ -24,23 +24,23 @@ var settings = null; var events = require("events"); var RED = { - + init: function(httpServer,userSettings) { settings = userSettings; server.init(httpServer,settings); library.init(); return server.app; }, - + start: server.start, - nodes: nodes, library: library, - events: events + events: events, + stop: nodes.closedown, }; RED.__defineGetter__("app", function() { return server.app }); RED.__defineGetter__("server", function() { return server.server }); RED.__defineGetter__("settings", function() { return settings }); -module.exports = RED; +module.exports = RED; diff --git a/red/server.js b/red/server.js index 38b9b013d..5361cb7af 100644 --- a/red/server.js +++ b/red/server.js @@ -18,9 +18,9 @@ var fs = require('fs'); var util = require('util'); var createUI = require("./ui"); var redNodes = require("./nodes"); -var host = require('os').hostname(); //TODO: relocated user dir -var rulesfile = process.argv[2] || 'flows_'+host+'.json'; + +var flowfile = ''; var app = null; var server = null; @@ -29,6 +29,8 @@ function createServer(_server,settings) { server = _server; app = createUI(settings); + flowfile = settings.flowFile || 'flows_'+require('os').hostname()+'.json'; + //TODO: relocated user dir fs.exists("lib/",function(exists) { if (!exists) { @@ -43,9 +45,9 @@ function createServer(_server,settings) { }); app.get("/flows",function(req,res) { - fs.exists(rulesfile, function (exists) { + fs.exists(flowfile, function (exists) { if (exists) { - res.sendfile(rulesfile); + res.sendfile(flowfile); } else { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write("[]"); @@ -62,7 +64,7 @@ function createServer(_server,settings) { req.on('end', function() { res.writeHead(204, {'Content-Type': 'text/plain'}); res.end(); - fs.writeFile(rulesfile, fullBody, function(err) { + fs.writeFile(flowfile, fullBody, function(err) { if(err) { util.log(err); } else { @@ -87,14 +89,14 @@ function start() { util.log("------------------------------------------"); - fs.exists(rulesfile, function (exists) { + fs.exists(flowfile, function (exists) { if (exists) { - util.log("[red] Loading flows : "+rulesfile); - fs.readFile(rulesfile,'utf8',function(err,data) { + util.log("[red] Loading flows : "+flowfile); + fs.readFile(flowfile,'utf8',function(err,data) { redNodes.setConfig(JSON.parse(data)); }); } else { - util.log("[red] Flows file not found : "+rulesfile); + util.log("[red] Flows file not found : "+flowfile); } }); } diff --git a/settings.js b/settings.js index e3e76d4a3..2dd19bb41 100644 --- a/settings.js +++ b/settings.js @@ -19,6 +19,9 @@ module.exports = { serialReconnectTime: 15000, debugMaxLength: 1000, + // The file containing the flows. If not set, it defaults to flows_.json + //flowFile: 'flows.json', + // You can protect the user interface with a userid and password by using the following property // the password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password') //httpAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"},