Compare commits

..

15 Commits
0.7.0 ... 0.7.2

Author SHA1 Message Date
Nick O'Leary
b2f4bc915e Bump 0.7.2 2014-04-26 22:43:29 +01:00
Nick O'Leary
13deef189d Add ws heartbeat to keep connection alive through firewall 2014-04-24 23:42:44 +01:00
Nick O'Leary
b5a8a7288b Tidy up ajax usage 2014-04-21 22:42:46 +01:00
Nick O'Leary
b6fd103b37 /nodes end-point should be text/html not json 2014-04-21 22:17:52 +01:00
Nick O'Leary
6a17a7d4c2 Add version information to log output 2014-04-21 21:55:28 +01:00
Nick O'Leary
c39f4f9738 Handle port-in-use error on start-up 2014-04-21 21:42:59 +01:00
Nick O'Leary
c20128b80f MQTT Client - missing null check 2014-04-21 21:14:03 +01:00
Nick O'Leary
0b7fa1ab5c Fix MQTT client reconnect logic 2014-04-21 20:40:56 +01:00
Nick O'Leary
775297d625 Fix library ui 2014-04-20 23:07:54 +01:00
Nick O'Leary
d00624f9e3 Tidy up REST interface
- Ensure application/json where appropriate
 - Use jquery api rather than d3
2014-04-20 22:35:38 +01:00
Nick O'Leary
d702caa5be Bump 0.7.1 2014-04-20 20:52:16 +01:00
Nick O'Leary
729036ec0b Fix HTTTP Request url template 2014-04-20 20:50:20 +01:00
Nick O'Leary
eee8f89146 Clear MQTT Connection watchdog on error 2014-04-19 22:19:06 +01:00
Nick O'Leary
4ae5f34d2e Make Template node help clearer 2014-04-18 15:33:29 +01:00
Nick O'Leary
18ae7108f5 Add Grunt-cli dependency and fix Travis 2014-04-16 23:28:02 +01:00
14 changed files with 263 additions and 208 deletions

View File

@@ -134,6 +134,9 @@
}
ws.onmessage = function(event) {
var o = JSON.parse(event.data);
if (o.heartbeat) {
return;
}
//console.log(msg);
var msg = document.createElement("div");
msg.onmouseover = function() {

View File

@@ -57,6 +57,26 @@ function DebugNode(n) {
});
}
var lastSentTime = (new Date()).getTime();
setInterval(function() {
var now = (new Date()).getTime();
if (now-lastSentTime > 15000) {
lastSentTime = now;
for (var i in DebugNode.activeConnections) {
var ws = DebugNode.activeConnections[i];
try {
var p = JSON.stringify({heartbeat:lastSentTime});
ws.send(p);
} catch(err) {
util.log("[debug] ws heartbeat error : "+err);
}
}
}
}, 15000);
RED.nodes.registerType("debug",DebugNode);
DebugNode.send = function(msg) {
@@ -85,7 +105,7 @@ DebugNode.send = function(msg) {
if (msg.msg.length > debuglength) {
msg.msg = msg.msg.substr(0,debuglength) +" ....";
}
for (var i in DebugNode.activeConnections) {
var ws = DebugNode.activeConnections[i];
try {
@@ -95,6 +115,7 @@ DebugNode.send = function(msg) {
util.log("[debug] ws error : "+err);
}
}
lastSentTime = (new Date()).getTime();
}
DebugNode.activeConnections = [];

View File

@@ -29,9 +29,18 @@
</script>
<script type="text/x-red" data-help-name="template">
<p>Creates new messages based on a template.</p>
<p>Very useful for creating boilerplate web pages, emails, tweets and so on.</p>
<p>Uses the <i><a href="http://mustache.github.io/mustache.5.html" target="_new">Mustache</a></i> format.</p>
<p>Creates a new payload based on the provided template.</p>
<p>This uses the <i><a href="http://mustache.github.io/mustache.5.html" target="_new">mustache</a></i> format.</p>
<p>For example, when a template of:
<pre>Hello {{name}}. Today is {{date}}</pre>
<p>receives a message containing:
<pre>{
name: "Fred",
date: "Monday"
payload: ...
}</pre>
<p>The resulting payload will be:
<pre>Hello Fred. Today is Monday</pre>
</script>
<script type="text/javascript">

View File

@@ -138,6 +138,7 @@ function HTTPRequest(n) {
var node = this;
var credentials = RED.nodes.getCredentials(n.id);
this.on("input",function(msg) {
var url;
if (msg.url) {
url = msg.url;
} else if (isTemplatedUrl) {
@@ -145,7 +146,6 @@ function HTTPRequest(n) {
} else {
url = nodeUrl;
}
var url = msg.url||nodeUrl;
var method = (msg.method||nodeMethod).toUpperCase();
var opts = urllib.parse(url);
opts.method = method;

View File

@@ -16,6 +16,7 @@
var util = require("util");
var mqtt = require("mqtt");
var events = require("events");
//var inspect = require("sys").inspect;
//var Client = module.exports.Client = function(
@@ -44,131 +45,143 @@ function MQTTClient(port,host) {
util.inherits(MQTTClient, events.EventEmitter);
MQTTClient.prototype.connect = function(options) {
var self = this;
options = options||{};
self.options = options;
self.options.keepalive = options.keepalive||15;
self.options.clean = self.options.clean||true;
self.options.protocolId = 'MQIsdp';
self.options.protocolVersion = 3;
self.client = mqtt.createConnection(this.port,this.host,function(err,client) {
if (err) {
self.connected = false;
self.connectionError = true;
self.emit('connectionlost',err);
return;
}
client.on('close',function(e) {
clearInterval(self.watchdog);
if (!self.connectionError) {
if (!this.connected) {
var self = this;
options = options||{};
self.options = options;
self.options.keepalive = options.keepalive||15;
self.options.clean = self.options.clean||true;
self.options.protocolId = 'MQIsdp';
self.options.protocolVersion = 3;
self.client = mqtt.createConnection(this.port,this.host,function(err,client) {
if (err) {
self.connected = false;
clearInterval(self.watchdog);
self.connectionError = true;
//util.log('[mqtt] ['+self.uid+'] connection error 1 : '+inspect(err));
self.emit('connectionlost',err);
return;
}
client.on('close',function(e) {
//util.log('[mqtt] ['+self.uid+'] on close');
clearInterval(self.watchdog);
if (!self.connectionError) {
if (self.connected) {
self.connected = false;
self.emit('connectionlost',e);
} else {
self.emit('disconnect');
}
}
});
client.on('error',function(e) {
//util.log('[mqtt] ['+self.uid+'] on error : '+inspect(e));
clearInterval(self.watchdog);
if (self.connected) {
self.connected = false;
self.emit('connectionlost',e);
} else {
self.emit('disconnect');
}
}
});
client.on('error',function(e) {
clearInterval(self.watchdog);
if (self.connected) {
self.connected = false;
self.emit('connectionlost',e);
}
});
client.on('connack',function(packet) {
if (packet.returnCode == 0) {
self.watchdog = setInterval(function(self) {
var now = (new Date()).getTime();
if (now - self.lastOutbound > self.options.keepalive*500 || now - self.lastInbound > self.options.keepalive*500) {
if (self.pingOutstanding) {
try {
self.client.disconnect();
} catch (err) {
}
} else {
self.lastOutbound = (new Date()).getTime();
self.lastInbound = (new Date()).getTime();
self.pingOutstanding = true;
self.client.pingreq();
}
}
},self.options.keepalive*500,self);
self.pingOutstanding = false;
self.lastInbound = (new Date()).getTime()
self.lastOutbound = (new Date()).getTime()
self.connected = true;
self.connectionError = false;
self.emit('connect');
} else {
self.connected = false;
self.emit('connectionlost');
}
});
client.on('suback',function(packet) {
self.lastInbound = (new Date()).getTime()
var topic = self.pendingSubscriptions[packet.messageId];
self.emit('subscribe',topic,packet.granted[0]);
delete self.pendingSubscriptions[packet.messageId];
});
client.on('unsuback',function(packet) {
self.lastInbound = (new Date()).getTime()
var topic = self.pendingSubscriptions[packet.messageId];
self.emit('unsubscribe',topic,packet.granted[0]);
delete self.pendingSubscriptions[packet.messageId];
});
client.on('publish',function(packet) {
self.lastInbound = (new Date()).getTime()
if (packet.qos < 2) {
var p = packet;
self.emit('message',p.topic,p.payload,p.qos,p.retain);
} else {
self.inboundMessages[packet.messageId] = packet;
this.lastOutbound = (new Date()).getTime()
self.client.pubrec(packet);
}
if (packet.qos == 1) {
this.lastOutbound = (new Date()).getTime()
self.client.puback(packet);
}
});
client.on('pubrel',function(packet) {
self.lastInbound = (new Date()).getTime()
var p = self.inboundMessages[packet.messageId];
if (p) {
self.emit('message',p.topic,p.payload,p.qos,p.retain);
delete self.inboundMessages[packet.messageId];
}
self.lastOutbound = (new Date()).getTime()
self.client.pubcomp(packet);
});
client.on('puback',function(packet) {
self.lastInbound = (new Date()).getTime()
// outbound qos-1 complete
});
client.on('pubrec',function(packet) {
self.lastInbound = (new Date()).getTime()
self.lastOutbound = (new Date()).getTime()
self.client.pubrel(packet);
});
client.on('pubcomp',function(packet) {
self.lastInbound = (new Date()).getTime()
// outbound qos-2 complete
});
client.on('pingresp',function(packet) {
self.lastInbound = (new Date()).getTime()
self.pingOutstanding = false;
});
this.lastOutbound = (new Date()).getTime()
this.connectionError = false;
client.connect(self.options);
});
});
client.on('connack',function(packet) {
if (packet.returnCode == 0) {
self.watchdog = setInterval(function(self) {
var now = (new Date()).getTime();
//util.log('[mqtt] ['+self.uid+'] watchdog '+inspect({connected:self.connected,connectionError:self.connectionError,pingOutstanding:self.pingOutstanding,now:now,lastOutbound:self.lastOutbound,lastInbound:self.lastInbound}));
if (now - self.lastOutbound > self.options.keepalive*500 || now - self.lastInbound > self.options.keepalive*500) {
if (self.pingOutstanding) {
//util.log('[mqtt] ['+self.uid+'] watchdog pingOustanding - disconnect');
try {
self.client.disconnect();
} catch (err) {
}
} else {
//util.log('[mqtt] ['+self.uid+'] watchdog pinging');
self.lastOutbound = (new Date()).getTime();
self.lastInbound = (new Date()).getTime();
self.pingOutstanding = true;
self.client.pingreq();
}
}
},self.options.keepalive*500,self);
self.pingOutstanding = false;
self.lastInbound = (new Date()).getTime()
self.lastOutbound = (new Date()).getTime()
self.connected = true;
self.connectionError = false;
self.emit('connect');
} else {
self.connected = false;
self.emit('connectionlost');
}
});
client.on('suback',function(packet) {
self.lastInbound = (new Date()).getTime()
var topic = self.pendingSubscriptions[packet.messageId];
self.emit('subscribe',topic,packet.granted[0]);
delete self.pendingSubscriptions[packet.messageId];
});
client.on('unsuback',function(packet) {
self.lastInbound = (new Date()).getTime()
var topic = self.pendingSubscriptions[packet.messageId];
self.emit('unsubscribe',topic,packet.granted[0]);
delete self.pendingSubscriptions[packet.messageId];
});
client.on('publish',function(packet) {
self.lastInbound = (new Date()).getTime()
if (packet.qos < 2) {
var p = packet;
self.emit('message',p.topic,p.payload,p.qos,p.retain);
} else {
self.inboundMessages[packet.messageId] = packet;
this.lastOutbound = (new Date()).getTime()
self.client.pubrec(packet);
}
if (packet.qos == 1) {
this.lastOutbound = (new Date()).getTime()
self.client.puback(packet);
}
});
client.on('pubrel',function(packet) {
self.lastInbound = (new Date()).getTime()
var p = self.inboundMessages[packet.messageId];
if (p) {
self.emit('message',p.topic,p.payload,p.qos,p.retain);
delete self.inboundMessages[packet.messageId];
}
self.lastOutbound = (new Date()).getTime()
self.client.pubcomp(packet);
});
client.on('puback',function(packet) {
self.lastInbound = (new Date()).getTime()
// outbound qos-1 complete
});
client.on('pubrec',function(packet) {
self.lastInbound = (new Date()).getTime()
self.lastOutbound = (new Date()).getTime()
self.client.pubrel(packet);
});
client.on('pubcomp',function(packet) {
self.lastInbound = (new Date()).getTime()
// outbound qos-2 complete
});
client.on('pingresp',function(packet) {
//util.log('[mqtt] ['+self.uid+'] received pingresp');
self.lastInbound = (new Date()).getTime()
self.pingOutstanding = false;
});
this.lastOutbound = (new Date()).getTime()
this.connectionError = false;
client.connect(self.options);
});
}
}
MQTTClient.prototype.subscribe = function(topic,qos) {

View File

@@ -34,6 +34,7 @@ module.exports = {
connections[id] = function() {
var uid = (1+Math.random()*4294967295).toString(16);
var client = mqtt.createClient(port,broker);
client.uid = uid;
client.setMaxListeners(0);
var options = {keepalive:15};
options.clientId = clientid || 'mqtt_' + (1+Math.random()*4294967295).toString(16);
@@ -73,7 +74,7 @@ module.exports = {
client.once(a,b);
},
connect: function() {
if (!client.isConnected() && !connecting) {
if (client && !client.isConnected() && !connecting) {
connecting = true;
client.connect(options);
}
@@ -90,7 +91,6 @@ module.exports = {
client.on('connect',function() {
if (client) {
util.log('[mqtt] ['+uid+'] connected to broker tcp://'+broker+':'+port);
connecting = false;
for (var s in subscriptions) {
var topic = subscriptions[s].topic;
@@ -108,13 +108,13 @@ module.exports = {
});
client.on('connectionlost', function(err) {
util.log('[mqtt] ['+uid+'] connection lost to broker tcp://'+broker+':'+port);
connecting = false;
setTimeout(function() {
if (client) {
client.connect(options);
}
obj.connect();
}, settings.mqttReconnectTime||5000);
});
client.on('disconnect', function() {
connecting = false;
util.log('[mqtt] ['+uid+'] disconnected from broker tcp://'+broker+':'+port);
});

View File

@@ -1,6 +1,6 @@
{
"name" : "node-red",
"version" : "0.7.0",
"version" : "0.7.2",
"description" : "A visual tool for wiring the Internet of Things",
"homepage" : "http://nodered.org",
"license" : "Apache",
@@ -11,7 +11,7 @@
"main" : "red/red.js",
"scripts" : {
"start": "node red.js",
"test": "grunt"
"test": "./node_modules/.bin/grunt"
},
"contributors": [
{"name": "Nick O'Leary"},
@@ -42,6 +42,7 @@
},
"devDependencies": {
"grunt": "0.4.4",
"grunt-cli": "0.1.13",
"grunt-simple-mocha": "0.4.0",
"mocha": "1.18.2",
"should": "3.3.1"

View File

@@ -246,7 +246,7 @@
<form class="form-horizontal">
<div class="form-row">
<ul id="node-dialog-library-breadcrumbs" class="breadcrumb">
<li class="active"><a href="#">Library</a> <span class="divider">/</span></li>
<li class="active"><a href="#">Library</a></li>
</ul>
</div>
<div class="form-row">

View File

@@ -80,30 +80,32 @@ var RED = function() {
$("#btn-icn-deploy").addClass('spinner');
RED.view.dirty(false);
d3.xhr("flows").header("Content-type", "application/json")
.post(JSON.stringify(nns),function(err,resp) {
$("#btn-icn-deploy").removeClass('spinner');
$("#btn-icn-deploy").addClass('icon-upload');
if (resp && resp.status == 204) {
RED.notify("Successfully deployed","success");
RED.nodes.eachNode(function(node) {
if (node.changed) {
node.dirty = true;
node.changed = false;
}
});
// Once deployed, cannot undo back to a clean state
RED.history.markAllDirty();
RED.view.redraw();
} else {
RED.view.dirty(true);
if (resp) {
RED.notify("<strong>Error</strong>: "+resp,"error");
} else {
RED.notify("<strong>Error</strong>: no response from server","error");
}
console.log(err,resp);
$.ajax({
url:"flows",
type: "POST",
data: JSON.stringify(nns),
contentType: "application/json; charset=utf-8"
}).done(function(data,textStatus,xhr) {
RED.notify("Successfully deployed","success");
RED.nodes.eachNode(function(node) {
if (node.changed) {
node.dirty = true;
node.changed = false;
}
});
// Once deployed, cannot undo back to a clean state
RED.history.markAllDirty();
RED.view.redraw();
}).fail(function(xhr,textStatus,err) {
RED.view.dirty(true);
if (xhr.responseText) {
RED.notify("<strong>Error</strong>: "+xhr.responseText,"error");
} else {
RED.notify("<strong>Error</strong>: no response from server","error");
}
}).always(function() {
$("#btn-icn-deploy").removeClass('spinner');
$("#btn-icn-deploy").addClass('icon-upload');
});
}
}
@@ -150,7 +152,7 @@ var RED = function() {
}
function loadFlows() {
d3.json("flows",function(nodes) {
$.getJSON("flows",function(nodes) {
RED.nodes.import(nodes);
RED.view.dirty(false);
RED.view.redraw();

View File

@@ -17,7 +17,7 @@ RED.library = function() {
function loadFlowLibrary() {
d3.json("library/flows",function(data) {
$.getJSON("library/flows",function(data) {
//console.log(data);
var buildMenu = function(data,root) {
@@ -84,7 +84,7 @@ RED.library = function() {
li.onclick = function () {
var dirName = v;
return function(e) {
var bcli = $('<li class="active"><a href="#">'+dirName+'</a> <span class="divider">/</span></li>');
var bcli = $('<li class="active"><span class="divider">/</span> <a href="#">'+dirName+'</a></li>');
$("a",bcli).click(function(e) {
$(this).parent().nextAll().remove();
$.getJSON("library/"+options.url+root+dirName,function(data) {
@@ -112,6 +112,7 @@ RED.library = function() {
$(".list-selected",ul).removeClass("list-selected");
$(this).addClass("list-selected");
$.get("library/"+options.url+root+item.fn, function(data) {
console.log(data);
selectedLibraryItem = item;
libraryEditor.setText(data);
});
@@ -239,12 +240,13 @@ RED.library = function() {
open: function(e) {
var form = $("form",this);
form.height(form.parent().height()-30);
$(".form-row:last-child",form).height(form.height()-60);
$("#node-select-library-text").height("100%");
$(".form-row:last-child",form).children().height(form.height()-60);
},
resize: function(e) {
var form = $("form",this);
form.height(form.parent().height()-30);
$(".form-row:last-child",form).height(form.height()-60);
$(".form-row:last-child",form).children().height(form.height()-60);
}
});

22
red.js
View File

@@ -44,6 +44,7 @@ nopt.invalidHandler = function(k,v,t) {
var parsedArgs = nopt(knownOpts,shortHands,process.argv,2)
if (parsedArgs.help) {
console.log("Node-RED v"+RED.version());
console.log("Usage: node red.js [-v] [-?] [--settings settings.js] [flows.json]");
console.log("");
console.log("Options:");
@@ -163,6 +164,16 @@ function getListenPath() {
RED.start().then(function() {
if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) {
server.on('error', function(err) {
if (err.errno === "EADDRINUSE") {
util.log('[red] Unable to listen on '+getListenPath());
util.log('[red] Error: port in use');
} else {
util.log('[red] Uncaught Exception:');
util.log(err.stack);
}
process.exit(1);
});
server.listen(settings.uiPort,settings.uiHost,function() {
if (settings.httpAdminRoot === false) {
util.log('[red] Admin UI disabled');
@@ -176,14 +187,9 @@ RED.start().then(function() {
process.on('uncaughtException',function(err) {
if (err.errno === "EADDRINUSE") {
util.log('[red] Unable to listen on '+getListenPath());
util.log('[red] Error: port in use');
} else {
util.log('[red] Uncaught Exception:');
util.log(err.stack);
}
process.exit(1);
util.log('[red] Uncaught Exception:');
util.log(err.stack);
process.exit(1);
});
process.on('SIGINT', function () {

View File

@@ -29,8 +29,7 @@ function init() {
});
req.on('end', function() {
storage.saveFlow(req.params[0],fullBody).then(function() {
res.writeHead(204, {'Content-Type': 'text/plain'});
res.end();
res.send(204);
}).otherwise(function(err) {
util.log("[red] Error loading flow '"+req.params[0]+"' : "+err);
res.send(500);
@@ -40,17 +39,14 @@ function init() {
redApp.get("/library/flows",function(req,res) {
storage.getAllFlows().then(function(flows) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(JSON.stringify(flows));
res.end();
res.json(flows);
});
});
redApp.get(new RegExp("/library/flows\/(.*)"), function(req,res) {
storage.getFlow(req.params[0]).then(function(data) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(data);
res.end();
res.set('Content-Type', 'application/json');
res.send(data);
}).otherwise(function(err) {
if (err) {
util.log("[red] Error loading flow '"+req.params[0]+"' : "+err);
@@ -67,13 +63,13 @@ function createLibrary(type) {
redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),function(req,res) {
var path = req.params[1]||"";
storage.getLibraryEntry(type,path).then(function(result) {
res.writeHead(200, {'Content-Type': 'text/plain'});
if (typeof result === "string") {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(result);
res.end();
} else {
res.write(JSON.stringify(result));
res.json(result);
}
res.end();
}).otherwise(function(err) {
if (err) {
util.log("[red] Error loading library entry '"+path+"' : "+err);

View File

@@ -18,6 +18,7 @@ var events = require("./events");
var server = require("./server");
var nodes = require("./nodes");
var library = require("./library");
var fs = require("fs");
var settings = null;
var path = require('path');
@@ -34,6 +35,15 @@ var RED = {
library.init();
return server.app;
},
version: function() {
var p = require(path.join(process.env.NODE_RED_HOME,"package.json"));
if (fs.existsSync(path.join(process.env.NODE_RED_HOME,".git"))) {
return p.version+".git";
} else {
return p.version;
}
},
start: server.start,
stop: server.stop,

View File

@@ -37,46 +37,38 @@ function createServer(_server,_settings) {
flowfile = settings.flowFile || 'flows_'+require('os').hostname()+'.json';
app.get("/nodes",function(req,res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(redNodes.getNodeConfigs());
res.end();
res.send(redNodes.getNodeConfigs());
});
app.get("/flows",function(req,res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(JSON.stringify(redNodes.getConfig()));
res.end();
res.json(redNodes.getConfig());
});
app.post("/flows",function(req,res) {
var fullBody = '';
req.on('data', function(chunk) {
fullBody += chunk.toString();
app.post("/flows",
express.json(),
function(req,res) {
var flows = req.body;
storage.saveFlows(flows).then(function() {
res.json(204);
redNodes.setConfig(flows);
}).otherwise(function(err) {
util.log("[red] Error saving flows : "+err);
res.send(500,err.message);
});
req.on('end', function() {
try {
var flows = JSON.parse(fullBody);
storage.saveFlows(flows).then(function() {
res.writeHead(204, {'Content-Type': 'text/plain'});
res.end();
redNodes.setConfig(flows);
}).otherwise(function(err) {
util.log("[red] Error saving flows : "+err);
res.send(500, err.message);
});
} catch(err) {
util.log("[red] Error saving flows : "+err);
res.send(400, "Invalid flow");
}
});
});
},
function(error,req,res,next) {
res.send(400,"Invalid Flow");
}
);
}
function start() {
var RED = require("./red");
var defer = when.defer();
storage.init(settings).then(function() {
console.log("\nWelcome to Node-RED\n===================\n");
util.log("[red] Version: "+RED.version());
util.log("[red] Loading palette nodes");
var nodeErrors = redNodes.load(settings);
if (nodeErrors.length > 0) {