diff options
author | 2017-02-15 10:37:53 -0500 | |
---|---|---|
committer | 2017-02-15 10:40:37 -0500 | |
commit | 324ee36fe31763e507b422ab0a88e4230045e205 (patch) | |
tree | d0b04520f6657601c918ce63fd27575977624187 /dgbuilder/core_nodes | |
parent | f0c97e8db427481e28c0a16b789bc73801b35e47 (diff) |
Initial commit for OpenECOMP SDN-C OA&M
Change-Id: I7ab579fd0d206bf356f36d52dcdf4f71f1fa2680
Signed-off-by: Timoney, Daniel (dt5972) <dtimoney@att.com>
Former-commit-id: 2a9f0edd09581f907e62ec4689b5ac94dd5382ba
Diffstat (limited to 'dgbuilder/core_nodes')
78 files changed, 10557 insertions, 0 deletions
diff --git a/dgbuilder/core_nodes/analysis/72-sentiment.html b/dgbuilder/core_nodes/analysis/72-sentiment.html new file mode 100644 index 00000000..c33b873b --- /dev/null +++ b/dgbuilder/core_nodes/analysis/72-sentiment.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="sentiment"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="sentiment"> + <p>Analyses the <b>msg.payload</b> and adds a <b>msg.sentiment</b> object that contains the resulting AFINN-111 sentiment score as <b>msg.sentiment.score</b>.</p> + <p>A score greater than zero is positive and less than zero is negative.</p> + <p>The score typically ranges from -5 to +5, but can go higher and lower.</p> + <p>An object of word score overrides can be supplied as <b>msg.overrides</b>.</p> + <p>See <a href="https://github.com/thisandagain/sentiment/blob/master/README.md" target="_new">the Sentiment docs here</a>.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('sentiment',{ + category: 'analysis-function', + color:"#E6E0F8", + defaults: { + name: {value:""}, + }, + inputs:1, + outputs:1, + icon: "arrow-in.png", + label: function() { + return this.name||"sentiment"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/analysis/72-sentiment.js b/dgbuilder/core_nodes/analysis/72-sentiment.js new file mode 100644 index 00000000..747e079c --- /dev/null +++ b/dgbuilder/core_nodes/analysis/72-sentiment.js @@ -0,0 +1,33 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var sentiment = require('sentiment'); + + function SentimentNode(n) { + RED.nodes.createNode(this,n); + var node = this; + + this.on("input", function(msg) { + sentiment(msg.payload, msg.overrides || null, function (err, result) { + msg.sentiment = result; + node.send(msg); + }); + }); + } + RED.nodes.registerType("sentiment",SentimentNode); +} diff --git a/dgbuilder/core_nodes/core/20-inject.html b/dgbuilder/core_nodes/core/20-inject.html new file mode 100644 index 00000000..38aa6efe --- /dev/null +++ b/dgbuilder/core_nodes/core/20-inject.html @@ -0,0 +1,437 @@ +<!--
+ 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.
+-->
+
+<script type="text/x-red" data-template-name="inject">
+ <div class="form-row node-input-payload">
+ <label for="node-input-payloadType"><i class="fa fa-envelope"></i> Payload</label>
+ <select id="node-input-payloadType" style="width:125px !important">
+ <option value="date">timestamp</option>
+ <option value="none">blank</option>
+ <option value="string">string</option>
+ </select>
+ </div>
+
+ <div class="form-row" id="node-input-row-payload">
+ <label for="node-input-payload"></label>
+ <input type="text" id="node-input-payload" placeholder="payload">
+ </div>
+
+ <div class="form-row">
+ <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
+ <input type="text" id="node-input-topic" placeholder="topic">
+ </div>
+
+ <div class="form-row">
+ <label for=""><i class="fa fa-repeat"></i> Repeat</label>
+ <select id="inject-time-type-select" style="width: 288px"><option value="none">none</option><option value="interval">interval</option><option value="interval-time">interval between times</option><option value="time">at a specific time</option></select>
+ <input type="hidden" id="node-input-repeat" placeholder="payload">
+ <input type="hidden" id="node-input-crontab" placeholder="payload">
+ </div>
+
+ <div class="form-row inject-time-row hidden" id="inject-time-row-interval">
+ every <input id="inject-time-interval-count" class="inject-time-count" value="1"></input>
+ <select style="width: 100px" id="inject-time-interval-units"><option value="s">seconds</option><option value="m">minutes</option><option value="h">hours</option></select><br/>
+ <!-- on <select disabled id="inject-time-interval-days" class="inject-time-days"></select> -->
+ </div>
+
+ <div class="form-row inject-time-row hidden" id="inject-time-row-interval-time">
+ at every <select style="width: 90px" id="inject-time-interval-time-units" class="inject-time-int-count" value="1">
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="10">10</option>
+ <option value="12">12</option>
+ <option value="15">15</option>
+ <option value="20">20</option>
+ <option value="30">30</option>
+ <option value="0">60</option>
+ </select> minutes<br/>
+ between <select id="inject-time-interval-time-start" class="inject-time-times"></select>
+ and <select id="inject-time-interval-time-end" class="inject-time-times"></select><br/>
+ on <select id="inject-time-interval-time-days" class="inject-time-days"></select>
+ </div>
+
+ <div class="form-row inject-time-row hidden" id="inject-time-row-time">
+ at <input id="inject-time-time" value="12:00"></input><br/>
+ on <select id="inject-time-time-days" class="inject-time-days"></select>
+ </div>
+
+ <div class="form-row" id="node-once">
+ <label> </label>
+ <input type="checkbox" id="node-input-once" style="display: inline-block; width: auto; vertical-align: top;">
+ <label for="node-input-once" style="width: 70%;">Fire once at start ?</label>
+ </div>
+
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+ <input type="text" id="node-input-name" placeholder="name">
+ </div>
+
+ <div class="form-tips"><b>Note:</b> "interval between times" and "at a specific time" will use cron.<br/>See info box for details.</div>
+</script>
+<style>
+ .inject-time-row {
+ padding-left: 110px;
+ }
+ .inject-time-row select {
+ margin: 3px 0;
+ }
+ .inject-time-days {
+ width: 262px;
+ }
+ .inject-time-times {
+ width: 90px;
+ }
+ .inject-time-row > .ui-spinner {
+ height: 28px;
+ margin: 3px 0;
+ border-color: rgb(204, 204, 204);
+ }
+ #inject-time-time {
+ margin-top: 3px;
+ width: 75px;
+ }
+ .inject-time-count {
+ width: 40px !important;
+ }
+</style>
+<script type="text/x-red" data-help-name="inject">
+ <p>Pressing the button on the left side of the node allows a message on a topic to be injected into the flow. This is mainly for test purposes.</p>
+ <p>If no payload is specified the payload is set to the current time in millisecs since 1970. This allows subsequent functions to perform time based actions.</p>
+ <p>The repeat function does what it says on the tin and continuously sends the payload every x seconds.</p>
+ <p>The Fire once at start option actually waits 50mS before firing to give other nodes a chance to instantiate properly.</p>
+ <p><b>Note: </b>"Interval between times" and "at a specific time" will use cron. This means that 20 minutes will be at the next hour, 20 minutes past and 40 minutes past - not in 20 minutes time.
+ If you want every 20 minutes from now - use the basic "interval" option.</p>
+</script>
+
+<script type="text/javascript">
+ RED.nodes.registerType('inject',{
+ category: 'input',
+ color:"#a6bbcf",
+ defaults: {
+ name: {value:""},
+ topic: {value:""},
+ payload: {value:""},
+ payloadType: {value:"date"},
+ repeat: {value:""},
+ crontab: {value:""},
+ once: {value:false}
+ },
+ inputs:0,
+ outputs:1,
+ icon: "inject.png",
+ label: function() {
+ if (this.payloadType === "string") {
+ return this.name||this.topic||this.payload||"inject";
+ }
+ else { return this.name||this.topic||"inject"; }
+ },
+ labelStyle: function() {
+ return this.name?"node_label_italic":"";
+ },
+ oneditprepare: function() {
+ $("#inject-time-type-select").change(function() {
+ $("#node-input-crontab").val('');
+ var id = $("#inject-time-type-select option:selected").val();
+ $(".inject-time-row").hide();
+ $("#inject-time-row-"+id).show();
+ if ((id == "none") || (id == "interval")) {
+ $("#node-once").show();
+ }
+ else {
+ $("#node-once").hide();
+ $("#node-input-once").prop('checked', false);
+ }
+ });
+
+ var days = [
+ {v:"*",t:"every day"},
+ {v:"1-5",t:"Mondays to Fridays"},
+ {v:"0,6",t:"Saturdays and Sundays"},
+ {v:"1",t:"Mondays"},
+ {v:"2",t:"Tuesdays"},
+ {v:"3",t:"Wednesdays"},
+ {v:"4",t:"Thursdays"},
+ {v:"5",t:"Fridays"},
+ {v:"6",t:"Saturdays"},
+ {v:"0",t:"Sundays"}
+ ];
+
+ $(".inject-time-days").each(function() {
+ for (var d in days) {
+ $(this).append($("<option></option>").val(days[d].v).text(days[d].t));
+ }
+ });
+
+ $(".inject-time-times").each(function() {
+ for (var i=0;i<24;i++) {
+ var l = (i<10?"0":"")+i+":00";
+ $(this).append($("<option></option>").val(i).text(l));
+ }
+ });
+
+ $(".inject-time-count").spinner({
+ //max:60,
+ min:1
+ });
+
+ $("#inject-time-interval-units").change(function() {
+ var units = $("#inject-time-interval-units option:selected").val();
+ //$("#inject-time-interval-days").prop("disabled",(units == "s")?"disabled":false);
+ //$(".inject-time-count").spinner("option","max",(units == "h")?24:60);
+ });
+
+ $.widget( "ui.injecttimespinner", $.ui.spinner, {
+ options: {
+ // seconds
+ step: 60 * 1000,
+ // hours
+ page: 60
+ },
+ _parse: function( value ) {
+ if ( typeof value === "string" ) {
+ // already a timestamp
+ if ( Number( value ) == value ) {
+ return Number( value );
+ }
+ var p = value.split(":");
+ var offset = new Date().getTimezoneOffset();
+ return (((Number(p[0])+1)*60)+Number(p[1])+offset)*60*1000;
+ }
+ return value;
+ },
+ _format: function( value ) {
+ var d = new Date(value);
+ var h = d.getHours();
+ var m = d.getMinutes();
+ return ((h < 10)?"0":"")+h+":"+((m < 10)?"0":"")+m;
+ }
+ });
+
+ $("#inject-time-time").injecttimespinner();
+
+ var repeattype = "none";
+ if (this.repeat != "" && this.repeat != 0) {
+ repeattype = "interval";
+ var r = "s";
+ var c = this.repeat;
+ if (this.repeat % 60 === 0) { r = "m"; c = c/60; }
+ if (this.repeat % 1440 === 0) { r = "h"; c = c/24; }
+ $("#inject-time-interval-count").val(c);
+ $("#inject-time-interval-units").val(r);
+ //$("#inject-time-interval-units option").filter(function() {return $(this).val() == "s";}).attr('selected',true);
+ $("#inject-time-interval-days").prop("disabled","disabled");
+ } else if (this.crontab) {
+ var cronparts = this.crontab.split(" ");
+ var days = cronparts[4];
+ if (!isNaN(cronparts[0]) && !isNaN(cronparts[1])) {
+ repeattype = "time";
+ // Fixed time
+ var time = cronparts[1]+":"+cronparts[0];
+ $("#inject-time-time").val(time);
+ $("#inject-time-type-select option").filter(function() {return $(this).val() == "s";}).attr('selected',true);
+ $("#inject-time-time-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
+ }
+ //else if (cronparts[0] == "0") {
+ // // interval - hours
+ // var hours = cronparts[1].slice(2);
+ // repeattype = "interval";
+ // $("#inject-time-interval-days").prop("disabled",false);
+ // $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
+ // $("#inject-time-interval-count").val(hours)
+ // $("#inject-time-interval-units option").filter(function() {return $(this).val() == "h";}).attr('selected',true);
+ //} else if (cronparts[1] == "*") {
+ // // interval - minutes
+ // var minutes = cronparts[0].slice(2);
+ // repeattype = "interval";
+ // $("#inject-time-interval-days").prop("disabled",false);
+ // $("#inject-time-interval-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
+ // $("#inject-time-interval-count").val(minutes)
+ // $("#inject-time-interval-units option").filter(function() {return $(this).val() == "m";}).attr('selected',true);
+ //}
+ else {
+ repeattype = "interval-time";
+ // interval - time period
+ var minutes = cronparts[0].slice(2);
+ if (minutes === "") { minutes = "0"; }
+ $("#inject-time-interval-time-units").val(minutes);
+ $("#inject-time-interval-time-days option").filter(function() {return $(this).val() == days;}).attr('selected',true);
+ var time = cronparts[1];
+ var timeparts = time.split(",");
+ var start;
+ var end;
+ if (timeparts.length == 1) {
+ // 0 or 0-10
+ var hours = timeparts[0].split("-");
+ if (hours.length == 1) {
+ if (hours[0] === "") {
+ start = "0";
+ end = "0";
+ }
+ else {
+ start = hours[0];
+ end = Number(hours[0])+1;
+ }
+ } else {
+ start = hours[0];
+ end = (Number(hours[1])+1)%24;
+ }
+ } else {
+ // 23,0 or 17-23,0-10 or 23,0-2 or 17-23,0
+ var startparts = timeparts[0].split("-");
+ start = startparts[0];
+
+ var endparts = timeparts[1].split("-");
+ if (endparts.length == 1) {
+ end = Number(endparts[0])+1;
+ } else {
+ end = Number(endparts[1])+1;
+ }
+ }
+ $("#inject-time-interval-time-start option").filter(function() {return $(this).val() == start;}).attr('selected',true);
+ $("#inject-time-interval-time-end option").filter(function() {return $(this).val() == end;}).attr('selected',true);
+
+ }
+ } else {
+ $("#inject-time-type-select option").filter(function() {return $(this).val() == "none";}).attr('selected',true);
+ }
+
+ $(".inject-time-row").hide();
+ $("#inject-time-type-select option").filter(function() {return $(this).val() == repeattype;}).attr('selected',true);
+ $("#inject-time-row-"+repeattype).show();
+
+ if (this.payloadType == null) {
+ if (this.payload == "") {
+ this.payloadType = "date";
+ } else {
+ this.payloadType = "string";
+ }
+ }
+
+ $("#node-input-payloadType").change(function() {
+ var id = $("#node-input-payloadType option:selected").val();
+ if (id == "string") {
+ $("#node-input-row-payload").show();
+ } else {
+ $("#node-input-row-payload").hide();
+ }
+ });
+ $("#node-input-payloadType").val(this.payloadType);
+ $("#node-input-payloadType").change();
+ $("#inject-time-type-select").change();
+
+ },
+ oneditsave: function() {
+ var repeat = "";
+ var crontab = "";
+ var type = $("#inject-time-type-select option:selected").val();
+ if (type == "none") {
+ // nothing
+ } else if (type == "interval") {
+ var count = $("#inject-time-interval-count").val();
+ var units = $("#inject-time-interval-units option:selected").val();
+ var days = $("#inject-time-interval-days option:selected").val();
+ if (units == "s") {
+ repeat = count;
+ } else {
+ if (units == "m") {
+ //crontab = "*/"+count+" * * * "+days;
+ repeat = count * 60;
+ } else if (units == "h") {
+ //crontab = "0 */"+count+" * * "+days;
+ repeat = count * 60 * 24;
+ }
+ }
+ } else if (type == "interval-time") {
+ var count = $("#inject-time-interval-time-units").val();
+ var startTime = Number($("#inject-time-interval-time-start option:selected").val());
+ var endTime = Number($("#inject-time-interval-time-end option:selected").val());
+ var days = $("#inject-time-interval-time-days option:selected").val();
+ var timerange = "*";
+ if (startTime == endTime) {
+ //TODO: invalid
+ repeat = "";
+ crontab = "";
+ } else if (endTime == 0) {
+ timerange = startTime+"-23";
+ } else if (startTime+1 < endTime) {
+ timerange = startTime+"-"+(endTime-1);
+ } else if (startTime+1 == endTime) {
+ timerange = startTime;
+ } else {
+ var startpart = "";
+ var endpart = "";
+ if (startTime == 23) {
+ startpart = "23";
+ } else {
+ startpart = startTime+"-23";
+ }
+ if (endTime == 1) {
+ endpart = "0";
+ } else {
+ endpart = "0-"+(endTime-1);
+ }
+ timerange = startpart+","+endpart;
+ }
+ repeat = "";
+ if (count === "0") {
+ crontab = count+" "+timerange+" * * "+days;
+ }
+ else {
+ crontab = "*/"+count+" "+timerange+" * * "+days;
+ }
+ } else if (type == "time") {
+ var time = $("#inject-time-time").val();
+ var days = $("#inject-time-time-days option:selected").val();
+ var parts = time.split(":");
+ repeat = "";
+ crontab = parts[1]+" "+parts[0]+" * * "+days;
+ }
+
+ $("#node-input-repeat").val(repeat);
+ $("#node-input-crontab").val(crontab);
+
+ },
+ button: {
+ onclick: function() {
+ var label = (this.name||this.payload).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
+ if (this.payloadType === "date") { label = "timestamp"; }
+ if (this.payloadType === "none") { label = "blank"; }
+ d3.xhr("inject/"+this.id).post(function(err,resp) {
+ if (err) {
+ if (err.status == 404) {
+ RED.notify("<strong>Error</strong>: inject node not deployed","error");
+ } else if (err.status == 500) {
+ RED.notify("<strong>Error</strong>: inject failed, see log for details.","error");
+ } else if (err.status == 0) {
+ RED.notify("<strong>Error</strong>: no response from server","error");
+ } else {
+ RED.notify("<strong>Error</strong>: unexpected error: ("+err.status+")"+err.response,"error");
+ }
+ } else if (resp.status == 200) {
+ RED.notify("Successfully injected: "+label,"success");
+ } else {
+ RED.notify("<strong>Error</strong>: unexpected response: ("+resp.status+") "+resp.response,"error");
+ }
+ });
+ }
+ }
+ });
+
+</script>
diff --git a/dgbuilder/core_nodes/core/20-inject.js b/dgbuilder/core_nodes/core/20-inject.js new file mode 100644 index 00000000..dff0fb65 --- /dev/null +++ b/dgbuilder/core_nodes/core/20-inject.js @@ -0,0 +1,97 @@ +/** + * Copyright 2013, 2014 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. + **/ + +module.exports = function(RED) { + var cron = require("cron"); + + function InjectNode(n) { + RED.nodes.createNode(this,n); + this.topic = n.topic; + this.payload = n.payload; + this.payloadType = n.payloadType; + this.repeat = n.repeat; + this.crontab = n.crontab; + this.once = n.once; + var node = this; + this.interval_id = null; + this.cronjob = null; + + if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) { + this.repeat = this.repeat * 1000; + this.log("repeat = "+this.repeat); + this.interval_id = setInterval( function() { + node.emit("input",{}); + }, this.repeat ); + } else if (this.crontab) { + if (cron) { + this.log("crontab = "+this.crontab); + this.cronjob = new cron.CronJob(this.crontab, + function() { + node.emit("input",{}); + }, + null,true); + } else { + this.error("'cron' module not found"); + } + } + + if (this.once) { + setTimeout( function(){ node.emit("input",{}); }, 100); + } + + this.on("input",function(msg) { + var msg = {topic:this.topic}; + if ( (this.payloadType == null && this.payload == "") || this.payloadType == "date") { + msg.payload = Date.now(); + } else if (this.payloadType == null || this.payloadType == "string") { + msg.payload = this.payload; + } else { + msg.payload = ""; + } + this.send(msg); + msg = null; + }); + } + + RED.nodes.registerType("inject",InjectNode); + + InjectNode.prototype.close = function() { + if (this.interval_id != null) { + clearInterval(this.interval_id); + this.log("inject: repeat stopped"); + } else if (this.cronjob != null) { + this.cronjob.stop(); + this.log("inject: cronjob stopped"); + delete this.cronjob; + } + } + + RED.httpAdmin.post("/inject/:id", function(req,res) { + var node = RED.nodes.getNode(req.params.id); + if (node != null) { + try { + node.receive(); + res.send(200); + } catch(err) { + res.send(500); + node.error("Inject failed:"+err); + console.log(err.stack); + } + } else { + res.send(404); + } + }); +} diff --git a/dgbuilder/core_nodes/core/58-debug.html b/dgbuilder/core_nodes/core/58-debug.html new file mode 100644 index 00000000..04aa5078 --- /dev/null +++ b/dgbuilder/core_nodes/core/58-debug.html @@ -0,0 +1,248 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="debug"> + <div class="form-row"> + <label for="node-input-complete"><i class="fa fa-list"></i> Output</label> + <select type="text" id="node-input-complete" style="display: inline-block; width: 250px; vertical-align: top;"> + <option value="false">payload only</option> + <option value="true">complete msg object</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-console"><i class="fa fa-random"></i> to</label> + <select type="text" id="node-input-console" style="display: inline-block; width: 250px; vertical-align: top;"> + <option value="false">debug tab</option> + <option value="true">debug tab and console</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="debug"> + <p>The Debug node can be connected to the output of any node. It will display the timestamp, <b>msg.topic</b> and <b>msg.payload</b> fields of any messages it receives in the debug tab of the sidebar. + <br/>The sidebar can be accessed under the options drop-down in the top right corner.</p> + <p>The button to the right of the node will toggle it's output on and off so you can de-clutter the debug window.</p> + <p>If the payload is an object it will be stringified first for display and indicate that by saying "(Object) ".</p> + <p>If the payload is a buffer it will be stringified first for display and indicate that by saying "(Buffer) ".</p> + <p>Selecting any particular message will highlight (in red) the debug node that reported it. This is useful if you wire up multiple debug nodes.</p> + <p>Optionally can show the complete msg object - but the screen can get messy.</p> + <p>In addition any calls to node.warn or node.error will appear here.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('debug',{ + category: 'output', + defaults: { + name: {value:""}, + active: {value:true}, + console: {value:"false"}, + complete: {value:"false"} + }, + label: function() { + return this.name||"debug"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + color:"#87a980", + inputs:1, + outputs:0, + icon: "debug.png", + align: "right", + button: { + toggle: "active", + onclick: function() { + var label = this.name||"debug"; + d3.xhr("debug/"+this.id+"/"+(this.active?"enable":"disable")).post(function(err,resp) { + if (err) { + if (err.status == 404) { + RED.notify("<strong>Error</strong>: debug node not deployed","error"); + } else if (err.status == 0) { + RED.notify("<strong>Error</strong>: no response from server","error"); + } else { + RED.notify("<strong>Error</strong>: unexpected error: ("+err.status+")"+err.response,"error"); + } + } else if (resp.status == 200) { + RED.notify("Successfully activated: "+label,"success"); + } else if (resp.status == 201) { + RED.notify("Successfully deactivated: "+label,"success"); + } else { + RED.notify("<strong>Error</strong>: unexpected response: ("+resp.status+") "+resp.response,"error"); + } + }); + } + }, + onpaletteadd: function() { + var content = document.createElement("div"); + content.id = "tab-debug"; + + var toolbar = document.createElement("div"); + toolbar.id = "debug-toolbar"; + content.appendChild(toolbar); + + toolbar.innerHTML = '<div class="btn-group pull-right"><a id="debug-tab-clear" title="clear log" class="btn btn-mini" href="#"><i class="fa fa-trash"></i></a></div> '; + + var messages = document.createElement("div"); + messages.id = "debug-content"; + content.appendChild(messages); + + RED.sidebar.addTab("debug",content); + + function getTimestamp() { + var d = new Date(); + return d.toLocaleString(); + } + + var sbc = document.getElementById("debug-content"); + + var messageCount = 0; + var that = this; + RED._debug = function(msg) { + that.handleDebugMessage("",{ + name:"debug", + msg:msg + }); + } + + this.handleDebugMessage = function(t,o) { + var msg = document.createElement("div"); + msg.onmouseover = function() { + msg.style.borderRightColor = "#999"; + var n = RED.nodes.node(o.id); + if (n) { + n.highlighted = true; + n.dirty = true; + } + RED.view.redraw(); + }; + msg.onmouseout = function() { + msg.style.borderRightColor = ""; + var n = RED.nodes.node(o.id); + if (n) { + n.highlighted = false; + n.dirty = true; + } + RED.view.redraw(); + }; + msg.onclick = function() { + var node = RED.nodes.node(o.id); + if (node) { + RED.view.showWorkspace(node.z); + } + + }; + var name = (o.name?o.name:o.id).toString().replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); + var topic = (o.topic||"").toString().replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); + var payload = (o.msg||"").toString().replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); + msg.className = 'debug-message'+(o.level?(' debug-message-level-'+o.level):''); + msg.innerHTML = '<span class="debug-message-date">'+getTimestamp()+'</span>'+ + '<span class="debug-message-name">['+name+']</span>'+ + (o.topic?'<span class="debug-message-topic">'+topic+'</span>':'')+ + '<span class="debug-message-payload">'+payload+'</span>'; + var atBottom = (sbc.scrollHeight-messages.offsetHeight-sbc.scrollTop) < 5; + messageCount++; + $(messages).append(msg); + + if (messageCount > 200) { + $("#debug-content .debug-message:first").remove(); + messageCount--; + } + if (atBottom) { + $(sbc).scrollTop(sbc.scrollHeight); + } + }; + RED.comms.subscribe("debug",this.handleDebugMessage); + + $("#debug-tab-clear").click(function() { + $(".debug-message").remove(); + messageCount = 0; + RED.nodes.eachNode(function(node) { + node.highlighted = false; + node.dirty = true; + }); + RED.view.redraw(); + }); + }, + onpaletteremove: function() { + RED.comms.unsubscribe("debug",this.handleDebugMessage); + RED.sidebar.removeTab("debug"); + delete RED._debug; + } + }); +</script> + +<style> + #debug-content { + position: absolute; + top: 30px; + bottom: 0px; + left:0px; + right: 0px; + overflow-y: scroll; + } + #debug-toolbar { + padding: 3px 10px; + height: 24px; + background: #f3f3f3; + } + .debug-message { + cursor: pointer; + border-bottom: 1px solid #eee; + border-left: 8px solid #eee; + border-right: 8px solid #eee; + padding: 2px; + } + .debug-message-date { + background: #fff; + font-size: 9px; + color: #aaa; + padding: 1px 5px 1px 1px; + } + .debug-message-topic { + display: block; + background: #fff; + padding: 1px 5px; + font-size: 9px; + color: #a66; + } + .debug-message-name { + background: #fff; + padding: 1px 5px; + font-size: 9px; + color: #aac; + } + .debug-message-payload { + display: block; + padding: 2px; + background: #fff; + } + .debug-message-level-log { + border-left-color: #eee; + border-right-color: #eee; + } + .debug-message-level-warn { + border-left-color: #ffdf9d; + border-right-color: #ffdf9d; + } + .debug-message-level-error { + border-left-color: #f99; + border-right-color: #f99; + } +</style> diff --git a/dgbuilder/core_nodes/core/58-debug.js b/dgbuilder/core_nodes/core/58-debug.js new file mode 100644 index 00000000..7436bf2c --- /dev/null +++ b/dgbuilder/core_nodes/core/58-debug.js @@ -0,0 +1,114 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + var util = require("util"); + var events = require("events"); + var debuglength = RED.settings.debugMaxLength||1000; + var useColors = false; + // util.inspect.styles.boolean = "red"; + + function DebugNode(n) { + RED.nodes.createNode(this,n); + this.name = n.name; + this.complete = n.complete; + this.console = n.console; + this.active = (n.active == null)||n.active; + var node = this; + + this.on("input",function(msg) { + if (this.complete == "true") { // debug complete msg object + if (this.console == "true") { + node.log("\n"+util.inspect(msg, {colors:useColors, depth:10})); + } + if (this.active) { + sendDebug({id:this.id,name:this.name,topic:msg.topic,msg:msg,_path:msg._path}); + } + } else { // debug just the msg.payload + if (this.console == "true") { + if (typeof msg.payload === "string") { + node.log((msg.payload.indexOf("\n") != -1 ? "\n" : "") + msg.payload); + } + else if (typeof msg.payload === "object") { node.log("\n"+util.inspect(msg.payload, {colors:useColors, depth:10})); } + else { node.log(util.inspect(msg.payload, {colors:useColors})); } + } + if (this.active) { + sendDebug({id:this.id,name:this.name,topic:msg.topic,msg:msg.payload,_path:msg._path}); + } + } + }); + } + + RED.nodes.registerType("debug",DebugNode); + + function sendDebug(msg) { + if (msg.msg instanceof Error) { + msg.msg = msg.msg.toString(); + } else if (msg.msg instanceof Buffer) { + msg.msg = "(Buffer) "+msg.msg.toString('hex'); + } else if (typeof msg.msg === 'object') { + var seen = []; + var ty = "(Object) "; + if (util.isArray(msg.msg)) { ty = "(Array) "; } + msg.msg = ty + JSON.stringify(msg.msg, function(key, value) { + if (typeof value === 'object' && value !== null) { + if (seen.indexOf(value) !== -1) { return "[circular]"; } + seen.push(value); + } + return value; + }," "); + seen = null; + } else if (typeof msg.msg === "boolean") { + msg.msg = "(boolean) "+msg.msg.toString(); + } else if (msg.msg === 0) { + msg.msg = "0"; + } else if (msg.msg == null) { + msg.msg = "(undefined)"; + } + + if (msg.msg.length > debuglength) { + msg.msg = msg.msg.substr(0,debuglength) +" ...."; + } + + RED.comms.publish("debug",msg); + } + + DebugNode.logHandler = new events.EventEmitter(); + DebugNode.logHandler.on("log",function(msg) { + if (msg.level == "warn" || msg.level == "error") { + sendDebug(msg); + } + }); + RED.log.addHandler(DebugNode.logHandler); + + RED.httpAdmin.post("/debug/:id/:state", function(req,res) { + var node = RED.nodes.getNode(req.params.id); + var state = req.params.state; + if (node != null) { + if (state === "enable") { + node.active = true; + res.send(200); + } else if (state === "disable") { + node.active = false; + res.send(201); + } else { + res.send(404); + } + } else { + res.send(404); + } + }); +} diff --git a/dgbuilder/core_nodes/core/75-exec.html b/dgbuilder/core_nodes/core/75-exec.html new file mode 100644 index 00000000..567a34c1 --- /dev/null +++ b/dgbuilder/core_nodes/core/75-exec.html @@ -0,0 +1,68 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="exec"> + <div class="form-row"> + <label for="node-input-command"><i class="fa fa-file"></i> Command</label> + <input type="text" id="node-input-command" placeholder="command"> + </div> + <div class="form-row"> + <label for="node-input-append"><i class="fa fa-list"></i> Append</label> + <input type="text" id="node-input-append" placeholder="extra input"> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-useSpawn" placeholder="spawn" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-useSpawn" style="width: 70%;">Use spawn() instead of exec() ?</label> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: <i>spawn</i> expects only one command word - and appended args to be comma separated.</div> +</script> + +<script type="text/x-red" data-help-name="exec"> + <p>Calls out to a system command.<br/></p> + <p>Provides 3 outputs... stdout, stderr, and return code.</p> + <p>By default uses exec() which calls the command, blocks while waiting for completion, and then returns the complete result in one go, along with any errors.</p> + <p>Optionally can use spawn() instead, which returns output from stdout and stderr as the command runs (ie one line at a time). On completion it then returns a return code (on the 3rd output).</p> + <p>Spawn only expect one command word, with all extra parameters to be comma separated and passed as the append.</p> + <p>The optional append gets added to the command after the <b>msg.payload</b> - so you can do things like pipe the result to another command.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('exec',{ + category: 'advanced-function', + color:"darksalmon", + defaults: { + command: {value:"",required:true}, + append: {value:""}, + useSpawn: {value:""}, + name: {value:""} + }, + inputs:1, + outputs:3, + icon: "arrow-in.png", + align: "right", + label: function() { + return this.name||this.command; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/75-exec.js b/dgbuilder/core_nodes/core/75-exec.js new file mode 100644 index 00000000..a07b1401 --- /dev/null +++ b/dgbuilder/core_nodes/core/75-exec.js @@ -0,0 +1,84 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var spawn = require('child_process').spawn; + var exec = require('child_process').exec; + + function ExecNode(n) { + RED.nodes.createNode(this,n); + this.cmd = n.command.trim(); + this.append = n.append.trim() || ""; + this.useSpawn = n.useSpawn; + + var node = this; + this.on("input", function(msg) { + node.status({fill:"blue",shape:"dot"}); + if (this.useSpawn === true) { + // make the extra args into an array + // then prepend with the msg.payload + if (typeof(msg.payload !== "string")) { msg.payload = msg.payload.toString(); } + var arg = []; + if (node.append.length > 0) { arg = node.append.split(","); } + if (msg.payload.trim() !== "") { arg.unshift(msg.payload); } + node.log(node.cmd+" ["+arg+"]"); + if (node.cmd.indexOf(" ") == -1) { + var ex = spawn(node.cmd,arg); + ex.stdout.on('data', function (data) { + //console.log('[exec] stdout: ' + data); + msg.payload = data.toString(); + node.send([msg,null,null]); + }); + ex.stderr.on('data', function (data) { + //console.log('[exec] stderr: ' + data); + msg.payload = data.toString(); + node.send([null,msg,null]); + }); + ex.on('close', function (code) { + //console.log('[exec] result: ' + code); + msg.payload = code; + node.status({}); + node.send([null,null,msg]); + }); + ex.on('error', function (code) { + node.warn(code); + }); + } + else { node.error("Spawn command must be just the command - no spaces or extra parameters"); } + } + else { + var cl = node.cmd+" "+msg.payload+" "+node.append; + node.log(cl); + var child = exec(cl, function (error, stdout, stderr) { + msg.payload = stdout; + var msg2 = {payload:stderr}; + var msg3 = null; + //console.log('[exec] stdout: ' + stdout); + //console.log('[exec] stderr: ' + stderr); + if (error !== null) { + msg3 = {payload:error}; + //console.log('[exec] error: ' + error); + } + node.status({}); + node.send([msg,msg2,msg3]); + }); + } + }); + } + + RED.nodes.registerType("exec",ExecNode); +} diff --git a/dgbuilder/core_nodes/core/80-function.html b/dgbuilder/core_nodes/core/80-function.html new file mode 100644 index 00000000..442c391d --- /dev/null +++ b/dgbuilder/core_nodes/core/80-function.html @@ -0,0 +1,110 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="function"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-row"> + <label for="node-input-func"><i class="fa fa-wrench"></i> Function</label> + <input type="hidden" id="node-input-func" autofocus="autofocus"> + <div style="height: 250px;" class="node-text-editor" id="node-input-func-editor" ></div> + </div> + <div class="form-row"> + <label for="node-input-outputs"><i class="fa fa-random"></i> Outputs</label> + <input id="node-input-outputs" style="width: 60px; height: 1.7em;" value="1"> + </div> + <div class="form-tips">See the Info tab for help writing functions.</div> +</script> + +<script type="text/x-red" data-help-name="function"> + <p>A function block where you can write code to do more interesting things.</p> + <p>The message is passed in as a JavaScript object called <code>msg</code>.</p> + <p>By convention it will have a <code>msg.payload</code> property containing + the body of the message.</p> + <p>The function should return the messages it wants to pass on to the next nodes + in the flow. It can return:</p> + <ul> + <li>a single message object - passed to nodes connected to the first output</li> + <li>an array of message objects - passed to nodes connected to the corresponding outputs</li> + </ul> + <p>If any element of the array is itself an array of messages, multiple + messages are sent to the corresponding output.</p> + <p>If null is returned, either by itself or as an element of the array, no + message is passed on.</p> + <p>See the <a target="_new" href="http://nodered.org/docs/writing-functions.html">online documentation</a> for more help.</p> + +</script> + +<script type="text/javascript"> + RED.nodes.registerType('function',{ + color:"#fdd0a2", + category: 'function', + defaults: { + name: {value:""}, + func: {value:"\nreturn msg;"}, + outputs: {value:1} + }, + inputs:1, + outputs:1, + icon: "function.png", + label: function() { + return this.name; + }, + oneditprepare: function() { + $( "#node-input-outputs" ).spinner({ + min:1 + }); + + function functionDialogResize(ev,ui) { + $("#node-input-func-editor").css("height",(ui.size.height-275)+"px"); + }; + + $( "#dialog" ).on("dialogresize", functionDialogResize); + $( "#dialog" ).one("dialogopen", function(ev) { + var size = $( "#dialog" ).dialog('option','sizeCache-function'); + if (size) { + functionDialogResize(null,{size:size}); + } + }); + $( "#dialog" ).one("dialogclose", function(ev,ui) { + var height = $( "#dialog" ).dialog('option','height'); + $( "#dialog" ).off("dialogresize",functionDialogResize); + }); + var that = this; + require(["orion/editor/edit"], function(edit) { + that.editor = edit({ + parent:document.getElementById('node-input-func-editor'), + lang:"js", + contents: $("#node-input-func").val() + }); + RED.library.create({ + url:"functions", // where to get the data from + type:"function", // the type of object the library is for + editor:that.editor, // the field name the main text body goes to + fields:['name','outputs'] + }); + $("#node-input-name").focus(); + + }); + }, + oneditsave: function() { + $("#node-input-func").val(this.editor.getText()) + delete this.editor; + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/80-function.js b/dgbuilder/core_nodes/core/80-function.js new file mode 100644 index 00000000..e1413a7a --- /dev/null +++ b/dgbuilder/core_nodes/core/80-function.js @@ -0,0 +1,79 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var vm = require("vm"); + + function FunctionNode(n) { + RED.nodes.createNode(this,n); + this.name = n.name; + this.func = n.func; + var functionText = "var results = null; results = (function(msg){"+this.func+"\n})(msg);"; + this.topic = n.topic; + var sandbox = { + console:console, + util:util, + Buffer:Buffer, + context: { + global:RED.settings.functionGlobalContext || {} + } + }; + var context = vm.createContext(sandbox); + try { + this.script = vm.createScript(functionText); + this.on("input", function(msg) { + try { + var start = process.hrtime(); + context.msg = msg; + this.script.runInContext(context); + var results = context.results; + if (results == null) { + results = []; + } else if (results.length == null) { + results = [results]; + } + if (msg._topic) { + for (var m in results) { + if (results[m]) { + if (util.isArray(results[m])) { + for (var n=0; n < results[m].length; n++) { + results[m][n]._topic = msg._topic; + } + } else { + results[m]._topic = msg._topic; + } + } + } + } + this.send(results); + var duration = process.hrtime(start); + if (process.env.NODE_RED_FUNCTION_TIME) { + this.status({fill:"yellow",shape:"dot",text:""+Math.floor((duration[0]* 1e9 + duration[1])/10000)/100}); + } + } catch(err) { + this.error(err.toString()); + } + }); + } catch(err) { + this.error(err); + } + } + + RED.nodes.registerType("function",FunctionNode); + RED.library.register("functions"); +} diff --git a/dgbuilder/core_nodes/core/80-template.html b/dgbuilder/core_nodes/core/80-template.html new file mode 100644 index 00000000..dc014d37 --- /dev/null +++ b/dgbuilder/core_nodes/core/80-template.html @@ -0,0 +1,102 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="template"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-row"> + <label for="node-input-template"><i class="fa fa-file-code-o"></i> Template</label> + <input type="hidden" id="node-input-template" autofocus="autofocus"> + <div style="height: 250px;" class="node-text-editor" id="node-input-template-editor" ></div> + </div> + <div class="form-row"> + <label for="node-input-field"><i class="fa fa-edit"></i> Property</label> + msg.<input type="text" id="node-input-field" placeholder="payload" style="width: 64%;"> + </div> +</script> + +<script type="text/x-red" data-help-name="template"> + <p>Creates a new message 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"> + RED.nodes.registerType('template',{ + color:"rgb(243, 181, 103)", + category: 'function', + defaults: { + name: {value:""}, + field: {value:"payload"}, + template: {value:"This is the payload: {{payload}}!"}, + }, + inputs:1, + outputs:1, + icon: "template.png", + label: function() { + return this.name; + }, + oneditprepare: function() { + + function templateDialogResize(ev,ui) { + $("#node-input-template-editor").css("height",(ui.size.height-200)+"px"); + }; + + $( "#dialog" ).on("dialogresize", templateDialogResize); + $( "#dialog" ).one("dialogopen", function(ev) { + var size = $( "#dialog" ).dialog('option','sizeCache-template'); + if (size) { + templateDialogResize(null,{size:size}); + } + }); + $( "#dialog" ).one("dialogclose", function(ev,ui) { + var height = $( "#dialog" ).dialog('option','height'); + $( "#dialog" ).off("dialogresize",templateDialogResize); + }); + + var that = this; + require(["orion/editor/edit"], function(edit) { + that.editor = edit({ + parent:document.getElementById('node-input-template-editor'), + lang:"html", + contents: $("#node-input-template").val() + }); + RED.library.create({ + url:"templates", // where to get the data from + type:"template", // the type of object the library is for + editor:that.editor, // the field name the main text body goes to + fields:['name','field'] + }); + $("#node-input-name").focus(); + }); + }, + oneditsave: function() { + $("#node-input-template").val(this.editor.getText()) + delete this.editor; + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/80-template.js b/dgbuilder/core_nodes/core/80-template.js new file mode 100644 index 00000000..7c84142d --- /dev/null +++ b/dgbuilder/core_nodes/core/80-template.js @@ -0,0 +1,61 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var mustache = require("mustache"); + + function TemplateNode(n) { + RED.nodes.createNode(this,n); + this.name = n.name; + this.field = n.field || "payload"; + this.template = n.template; + var node = this; + + var b = node.field.split("."); + var i = 0; + var m = null; + var rec = function(obj) { + i += 1; + if ((i < b.length) && (typeof obj[b[i-1]] === "object")) { + rec(obj[b[i-1]]); // not there yet - carry on digging + } + else { + if (i === b.length) { // we've finished so assign the value + obj[b[i-1]] = mustache.render(node.template,m); + node.send(m); + } + else { + obj[b[i-1]] = {}; // needs to be a new object so create it + rec(obj[b[i-1]]); // and carry on digging + } + } + } + + node.on("input", function(msg) { + try { + m = msg; + i = 0; + rec(msg); + } catch(err) { + node.error(err.message); + } + }); + } + + RED.nodes.registerType("template",TemplateNode); + RED.library.register("templates"); +} diff --git a/dgbuilder/core_nodes/core/89-delay.html b/dgbuilder/core_nodes/core/89-delay.html new file mode 100644 index 00000000..dcb0a5b9 --- /dev/null +++ b/dgbuilder/core_nodes/core/89-delay.html @@ -0,0 +1,167 @@ +<!-- + 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. +--> + +<!-- First, the content of the edit dialog is defined. --> +<script type="text/x-red" data-template-name="delay"> + + <div class="form-row"> + <label for="node-input-pauseType"><i class="fa fa-tasks"></i> Action</label> + <select id="node-input-pauseType" style="width:270px !important"> + <option value="delay">Delay message</option> + <option value="rate">Limit rate to</option> + <option value="random">Random delay</option> + </select> + </div> + <div id="delay-details" class="form-row"> + <label for="node-input-timeout"><i class="fa fa-clock-o"></i> For</label> + <input type="text" id="node-input-timeout" placeholder="Time" style="direction:rtl; width:50px !important"> + <select id="node-input-timeoutUnits" style="width:200px !important"> + <option value="milliseconds">Milliseconds</option> + <option value="seconds">Seconds</option> + <option value="minutes">Minutes</option> + <option value="hours">Hours</option> + <option value="days">Days</option> + </select> + </div> + + <div id="rate-details" class="form-row"> + <label for="node-input-rate"><i class="fa fa-clock-o"></i> To</label> + <input type="text" id="node-input-rate" placeholder="1" style="direction:rtl; width:30px !important"> + <label for="node-input-reateUnits">msg(s) per</label> + <select id="node-input-rateUnits" style="width:140px !important"> + <option value="second">Second</option> + <option value="minute">Minute</option> + <option value="hour">Hour</option> + <option value="day">Day</option> + </select> + <br/> + <input style="margin: 20px 0 20px 100px; width: 30px;" type="checkbox" id="node-input-drop"><label style="width: 250px;" for="node-input-drop">drop intermediate messages</label> + </div> + + <div id="random-details" class="form-row"> + <label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> Between</label> + <input type="text" id="node-input-randomFirst" placeholder="" style="directon:rtl; width:30px !important"> + <label for="node-input-randomLast" style="width:20px"> & </label> + <input type="text" id="node-input-randomLast" placeholder="" style="directon:rtl; width:30px !important"> + <select id="node-input-randomUnits" style="width:140px !important"> + <option value="milliseconds">Milliseconds</option> + <option value="seconds">Seconds</option> + <option value="minutes">Minutes</option> + <option value="hours">Hours</option> + <option value="days">Days</option> + </select> + </div> + + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + +</script> + +<!-- Next, some simple help text is provided for the node. --> +<script type="text/x-red" data-help-name="delay"> + <p>Introduces a delay into a flow or rate limts messges</p> + <p>Default delay is 5 seconds and rate limit of 1 msg/second, but both can be configured</p> +</script> + +<!-- Finally, the node type is registered along with all of its properties --> +<script type="text/javascript"> + RED.nodes.registerType('delay',{ + category: 'function', // the palette category + color:"#E6E0F8", + defaults: { // defines the editable properties of the node + name: {value:""}, // along with default values. + pauseType: {value:"delay", required:true}, + timeout: {value:"5", required:true, validate:RED.validators.number()}, + timeoutUnits: {value:"seconds"}, + rate: {value:"1", required:true, validate:RED.validators.number()}, + rateUnits: {value: "second"}, + randomFirst: {value:"1", required:true, validate:RED.validators.number()}, + randomLast: {value:"5", required:true, validate:RED.validators.number()}, + randomUnits: {value: "seconds"}, + drop: {value:false} + }, + inputs:1, // set the number of inputs - only 0 or 1 + outputs:1, // set the number of outputs - 0 to n + icon: "timer.png", // set the icon (held in public/icons) + label: function() { // sets the default label contents + if (this.pauseType == "delay") { + var units = this.timeoutUnits ? this.timeoutUnits.charAt(0) : "s"; + if (this.timeoutUnits == "milliseconds") { units = "ms"; } + return this.name||"delay "+this.timeout+" " + units; + } else if (this.pauseType == "rate") { + var units = this.rateUnits ? this.rateUnits.charAt(0) : "s"; + return this.name||"limit "+this.rate+" msg/"+ units; + } else if (this.pauseType == "random") { + return this.name || "random"; + } + return "foo"; + }, + labelStyle: function() { // sets the class to apply to the label + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $( "#node-input-timeout" ).spinner({min:1,max:60}); + $( "#node-input-rate" ).spinner({min:1}); + + $( "#node-input-randomFirst" ).spinner({min:0}); + $( "#node-input-randomLast" ).spinner({min:1}); + + if (this.pauseType == "delay") { + $("#delay-details").show(); + $("#rate-details").hide(); + $("#random-details").hide(); + } else if (this.pauseType == "rate") { + $("#delay-details").hide(); + $("#rate-details").show(); + $("#random-details").hide(); + } else if (this.pauseType == "random") { + $("#delay-details").hide(); + $("#rate-details").hide(); + $("#random-details").show(); + } + + if (!this.timeoutUnits) { + $("#node-input-timeoutUnits option").filter(function() { + return $(this).val() == 'seconds'; + }).attr('selected', true); + } + + if (!this.randomUnits) { + $("#node-input-randomUnits option").filter(function() { + return $(this).val() == 'seconds'; + }).attr('selected', true); + } + + $("#node-input-pauseType").on("change",function() { + if (this.value == "delay") { + $("#delay-details").show(); + $("#rate-details").hide(); + $("#random-details").hide(); + } else if (this.value == "rate") { + $("#delay-details").hide(); + $("#rate-details").show(); + $("#random-details").hide(); + } else if (this.value == "random") { + $("#delay-details").hide(); + $("#rate-details").hide(); + $("#random-details").show(); + } + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/89-delay.js b/dgbuilder/core_nodes/core/89-delay.js new file mode 100644 index 00000000..3c4e1c01 --- /dev/null +++ b/dgbuilder/core_nodes/core/89-delay.js @@ -0,0 +1,171 @@ +/** + * Copyright 2013, 2014 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. + **/ + +//Simple node to introduce a pause into a flow +module.exports = function(RED) { + "use strict"; + + var MILLIS_TO_NANOS = 1000000; + var SECONDS_TO_NANOS = 1000000000; + + function random(n) { + var wait = n.randomFirst + (n.diff * Math.random()); + if (n.buffer.length > 0) { + n.send(n.buffer.pop()); + n.randomID = setTimeout(function() {random(n);},wait); + } else { + n.randomID = -1; + } + } + + function DelayNode(n) { + RED.nodes.createNode(this,n); + + this.pauseType = n.pauseType; + this.timeoutUnits = n.timeoutUnits; + this.randomUnits = n.randomUnits; + this.rateUnits = n.rateUnits; + + if (n.timeoutUnits === "milliseconds") { + this.timeout = n.timeout; + } else if (n.timeoutUnits === "seconds") { + this.timeout = n.timeout * 1000; + } else if (n.timeoutUnits === "minutes") { + this.timeout = n.timeout * (60 * 1000); + } else if (n.timeoutUnits === "hours") { + this.timeout = n.timeout * (60 * 60 * 1000); + } else if (n.timeoutUnits === "days") { + this.timeout = n.timeout * (24 * 60 * 60 * 1000); + } + + if (n.rateUnits === "second") { + this.rate = 1000/n.rate; + } else if (n.rateUnits === "minute") { + this.rate = (60 * 1000)/n.rate; + } else if (n.rateUnits === "hour") { + this.rate = (60 * 60 * 1000)/n.rate; + } else if (n.rateUnits === "day") { + this.rate = (24 * 60 * 60 * 1000)/n.rate; + } + + if (n.randomUnits === "milliseconds") { + this.randomFirst = n.randomFirst; + this.randomLast = n.randomLast; + } else if (n.randomUnits === "seconds") { + this.randomFirst = n.randomFirst * 1000; + this.randomLast = n.randomLast * 1000; + } else if (n.randomUnits === "minutes") { + this.randomFirst = n.randomFirst * (60 * 1000); + this.randomLast = n.randomLast * (60 * 1000); + } else if (n.randomUnits === "hours") { + this.randomFirst = n.randomFirst * (60 * 60 * 1000); + this.randomLast = n.randomLast * (60 * 60 * 1000); + } else if (n.randomUnits === "days") { + this.randomFirst = n.randomFirst * (24 * 60 * 60 * 1000); + this.randomLast = n.randomLast * (24 * 60 * 60 * 1000); + } + + this.diff = this.randomLast - this.randomFirst; + this.name = n.name; + this.idList = []; + this.buffer = []; + this.intervalID = -1; + this.randomID = -1; + this.lastSent; + this.drop = n.drop; + var node = this; + + if (this.pauseType === "delay") { + this.on("input", function(msg) { + var id; + id = setTimeout(function(){ + node.idList.splice(node.idList.indexOf(id),1); + node.send(msg); + }, node.timeout); + this.idList.push(id); + }); + + this.on("close", function() { + for (var i=0; i<this.idList.length; i++ ) { + clearTimeout(this.idList[i]); + } + this.idList = []; + }); + + } else if (this.pauseType === "rate") { + this.on("input", function(msg) { + if (!node.drop) { + if ( node.intervalID !== -1) { + node.buffer.push(msg); + if (node.buffer.length > 0) { + node.status({text:node.buffer.length}); + } + if (node.buffer.length > 1000) { + node.warn(this.name + " buffer exceeded 1000 messages"); + } + } else { + node.send(msg); + node.intervalID = setInterval(function() { + if (node.buffer.length === 0) { + clearInterval(node.intervalID); + node.intervalID = -1; + node.status({text:""}); + } + + if (node.buffer.length > 0) { + node.send(node.buffer.shift()); + node.status({text:node.buffer.length}); + } + },node.rate); + } + } else { + var timeSinceLast; + if (node.lastSent) { + timeSinceLast = process.hrtime(node.lastSent); + } + if (!node.lastSent) { // ensuring that we always send the first message + node.lastSent = process.hrtime(); + node.send(msg); + } else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { + node.lastSent = process.hrtime(); + node.send(msg); + } + } + }); + + this.on("close", function() { + clearInterval(this.intervalID); + this.buffer = []; + }); + + } else if (this.pauseType === "random") { + this.on("input",function(msg){ + node.buffer.push(msg); + if (node.randomID === -1) { + var wait = node.randomFirst + (node.diff * Math.random()); + node.randomID = setTimeout(function() {random(node);},wait); + } + }); + + this.on("close", function (){ + if (this.randomID !== -1) { + clearTimeout(this.randomID); + } + }); + } + } + RED.nodes.registerType("delay",DelayNode); +} diff --git a/dgbuilder/core_nodes/core/89-trigger.html b/dgbuilder/core_nodes/core/89-trigger.html new file mode 100644 index 00000000..f3ec530d --- /dev/null +++ b/dgbuilder/core_nodes/core/89-trigger.html @@ -0,0 +1,130 @@ +<!-- + Copyright 2014 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. +--> + +<script type="text/x-red" data-template-name="trigger"> + <div class="form-row"> + <label for="node-input-op1type"><i class="fa fa-arrow-up"></i> Output</label> + <select id="node-input-op1type" style="width:73% !important"> + <option value="val">the value below</option> + <option value="pay">the existing payload</option> + <option value="nul">nothing (no output)</option> + </select> + </div> + <div class="form-row" id="node-op1"> + <label for="node-input-op1"> </label> + <input type="text" id="node-input-op1"> + </div> + <div class="form-row"> + <label for="node-input-duration"><i class="fa fa-clock-o"></i> then wait</label> + <input type="text" id="node-input-duration" placeholder="250" style="direction:rtl; width:70px !important"> + <select id="node-input-units" style="width:140px !important"> + <option value="ms">Milliseconds</option> + <option value="s">Seconds</option> + <option value="min">Minutes</option> + <option value="hr">Hours</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-op2type"><i class="fa fa-arrow-down"></i> output</label> + <select id="node-input-op2type" style="width:73% !important"> + <option value="val">the value below</option> + <option value="pay">the existing payload</option> + <option value="nul">nothing (no output)</option> + </select> + </div> + <div class="form-row" id="node-op2"> + <label for="node-input-op2"> </label> + <input type="text" id="node-input-op2"> + </div> + <div class="form-row"> + <label for="node-input-extend"><i class="fa fa-repeat"></i> and</label> + <select id="node-input-extend" style="width:73% !important"> + <option value="false">don't extend the timer if retriggered</option> + <option value="true">extend the timer if retriggered</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <!-- <div class="form-tips">Tip: Outputs can be values, null, {{templated}} or msg.payload<br/> --> + <div class="form-tips">Setting the timeout to 0 sets an infinite timeout = single shot.</div> + <script> + { + $("#node-input-op1type").change(function() { + if ($("#node-input-op1type").val() == "val") { $("#node-op1").show(); } + else { $("#node-op1").hide(); } + }); + $("#node-input-op2type").change(function() { + if ($("#node-input-op2type").val() == "val") { $("#node-op2").show(); } + else { $("#node-op2").hide(); } + }); + } + </script> +</script> + +<script type="text/x-red" data-help-name="trigger"> + <p>Creates two messages on the output separated by a timeout whenever ANY <b>msg</b> arrives on the input.</p> + <p>For example, this can be used to toggle a Raspberry PI GPIO pin on and off.</p> + <p>The two output states can be specified as can the duration of the timer. + Either output can be set to a value, or templated from the inbound + <b>msg</b> using mustache syntax. <pre>The payload is {{payload}}</pre></p> + <p>If the payload is an object then setting the output to <i>existing payload</i> will pass the complete payload object through.</p> + <p>Optionally the timer can be extended by being retriggered... or not.</p> + <p>By setting the first output to <i>nothing</i>, and selecting extend timer - a watchdog timer can be created. + No output will happen as long as repeated inputs occur within the timeout period.</p> + <p>Setting the timer to 0 creates an "infinite" timeout - the first output will happen but the second + never will, and neither can the first be retriggered - so a true one shot.</p> + <p>If a <b>msg.reset</b> property is present any timeout currently in progress + will be cleared and the second output will not happen.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('trigger',{ + category: 'function', + color:"#E6E0F8", + defaults: { + op1: {value:"1"}, + op2: {value:"0"}, + op1type: {value:""}, + op2type: {value:""}, + duration: {value:"250",required:true,validate:RED.validators.number()}, + extend: {value:"false"}, + units: {value: "ms"}, + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "trigger.png", + label: function() { + if (this.duration > 0) { + return this.name||"trigger "+this.duration+this.units; + } + else { + return this.name||"trigger once ∞"; + } + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $( "#node-input-duration" ).spinner({ + min:1, + increment:25 + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/89-trigger.js b/dgbuilder/core_nodes/core/89-trigger.js new file mode 100644 index 00000000..d86a2130 --- /dev/null +++ b/dgbuilder/core_nodes/core/89-trigger.js @@ -0,0 +1,91 @@ +/** + * Copyright 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var mustache = require("mustache"); + function TriggerNode(n) { + RED.nodes.createNode(this,n); + this.op1 = n.op1 || "1"; + this.op2 = n.op2 || "0"; + this.op1type = n.op1type || "val"; + this.op2type = n.op2type || "val"; + this.extend = n.extend || false; + this.units = n.units || "ms"; + this.duration = n.duration || 250; + if (this.duration <= 0) { this.duration = 0; } + else { + if (this.units == "s") { this.duration = this.duration * 1000; } + if (this.units == "min") { this.duration = this.duration * 1000 * 60; } + if (this.units == "hr") { this.duration = this.duration * 1000 *60 * 60; } + } + this.op1Templated = this.op1.indexOf("{{") != -1; + this.op2Templated = this.op2.indexOf("{{") != -1; + if (!isNaN(this.op1)) { this.op1 = Number(this.op1); } + if (!isNaN(this.op2)) { this.op2 = Number(this.op2); } + if (this.op1 == "true") { this.op1 = true; } + if (this.op2 == "true") { this.op1 = true; } + if (this.op1 == "false") { this.op2 = false; } + if (this.op2 == "false") { this.op2 = false; } + if (this.op1 == "null") { this.op1 = null; } + if (this.op2 == "null") { this.op1 = null; } + try { this.op1 = JSON.parse(this.op1); } + catch(e) { this.op1 = this.op1; } + try { this.op2 = JSON.parse(this.op2); } + catch(e) { this.op2 = this.op2; } + + var node = this; + var tout = null; + var m2; + this.on("input", function(msg) { + if (msg.hasOwnProperty("reset")) { + clearTimeout(tout); + tout = null; + } + else { + if (!tout) { + if (node.op2type === "pay") { m2 = msg.payload; } + else if (node.op2Templated) { m2 = mustache.render(node.op2,msg); } + else { m2 = node.op2; } + if (node.op1type === "pay") { } + else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } + else { msg.payload = node.op1; } + if (node.op1type !== "nul") { node.send(msg); } + if (node.duration === 0) { tout = "infinite"; } + else { + tout = setTimeout(function() { + msg.payload = m2; + if (node.op2type !== "nul") { node.send(msg); } + tout = null; + },node.duration); + } + } + else if ((node.extend == "true") && (node.duration > 0)) { + clearTimeout(tout); + tout = setTimeout(function() { + msg.payload = m2; + if (node.op2type !== "nul") { node.send(msg); } + tout = null; + },node.duration); + } + } + }); + this.on("close", function() { + if (tout) { clearTimeout(tout); } + }); + } + RED.nodes.registerType("trigger",TriggerNode); +} diff --git a/dgbuilder/core_nodes/core/90-comment.html b/dgbuilder/core_nodes/core/90-comment.html new file mode 100644 index 00000000..3638fdaa --- /dev/null +++ b/dgbuilder/core_nodes/core/90-comment.html @@ -0,0 +1,86 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="comment"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-comment"></i> Comment</label> + <input type="text" id="node-input-name" placeholder="Comment"> + </div> + <div class="form-row"> + <label for="node-input-info" style="width: 100% !important;"><i class="fa fa-comments"></i> More</label> + <input type="hidden" id="node-input-info" autofocus="autofocus"> + <div style="height: 250px;" class="node-text-editor" id="node-input-info-editor" ></div> + </div> + <div class="form-tips">Tip: this isn't meant for "War and Peace" - but useful notes can be kept here.</div> +</script> + +<script type="text/x-red" data-help-name="comment"> + <p>Simple comment block.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('comment',{ + category: 'function', + color:"#ffffff", + defaults: { + name: {value:""}, + info: {value:""} + }, + inputs:0, + outputs:0, + icon: "comment.png", + label: function() { + return this.name||""; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $( "#node-input-outputs" ).spinner({ + min:1 + }); + function functionDialogResize(ev,ui) { + $("#node-input-info-editor").css("height",(ui.size.height-235)+"px"); + }; + $( "#dialog" ).on("dialogresize", functionDialogResize); + $( "#dialog" ).one("dialogopen", function(ev) { + var size = $( "#dialog" ).dialog('option','sizeCache-function'); + if (size) { + functionDialogResize(null,{size:size}); + } + }); + $( "#dialog" ).one("dialogclose", function(ev,ui) { + var height = $( "#dialog" ).dialog('option','height'); + $( "#dialog" ).off("dialogresize",functionDialogResize); + }); + var that = this; + require(["orion/editor/edit"], function(edit) { + that.editor = edit({ + parent:document.getElementById('node-input-info-editor'), + lang:"text", + showLinesRuler:false, + showFoldingRuler:false, + contents: $("#node-input-info").val() + }); + $("#node-input-name").focus(); + }); + }, + oneditsave: function() { + $("#node-input-info").val(this.editor.getText()); + delete this.editor; + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/90-comment.js b/dgbuilder/core_nodes/core/90-comment.js new file mode 100644 index 00000000..ef5f0800 --- /dev/null +++ b/dgbuilder/core_nodes/core/90-comment.js @@ -0,0 +1,23 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + function CommentNode(n) { + RED.nodes.createNode(this,n); + } + RED.nodes.registerType("comment",CommentNode); +} diff --git a/dgbuilder/core_nodes/core/98-unknown.html b/dgbuilder/core_nodes/core/98-unknown.html new file mode 100644 index 00000000..19a4ad59 --- /dev/null +++ b/dgbuilder/core_nodes/core/98-unknown.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="unknown"> + <div class="form-tips"><p>This node is a type unknown to your installation of Node-RED.</p> + <p><i>If you deploy with the node in this state, it will lose all of its configuration.</i></p> + <p>See the Info side bar for more help</p></div> +</script> + +<script type="text/x-red" data-help-name="unknown"> + <p>This node is a type unknown to your installation of Node-RED.</p> + <p><i>If you deploy with the node in this state, it will lose all of its configuration.</i></p> + <p>It is possible this node type is already installed, but is missing a dependency. Check the Node-RED start-up log for + any error messages associated with the missing node type. Use <b>npm install <module></b> to install any missing modules + and restart Node-RED and reimport the nodes.</p> + <p>Otherwise, you should contact the author of the flow to obtain a copy of the missing node type.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('unknown',{ + category: 'unknown', + color:"#fff0f0", + defaults: { + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "", + label: function() { + return "("+this.name+")"||"unknown"; + }, + labelStyle: function() { + return "node_label_unknown"; + } + }); +</script> diff --git a/dgbuilder/core_nodes/core/98-unknown.js b/dgbuilder/core_nodes/core/98-unknown.js new file mode 100644 index 00000000..ed4716b8 --- /dev/null +++ b/dgbuilder/core_nodes/core/98-unknown.js @@ -0,0 +1,23 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + function UnknownNode(n) { + RED.nodes.createNode(this,n); + } + RED.nodes.registerType("unknown",UnknownNode); +} diff --git a/dgbuilder/core_nodes/deprecated/61-imap.html b/dgbuilder/core_nodes/deprecated/61-imap.html new file mode 100644 index 00000000..9702cd64 --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/61-imap.html @@ -0,0 +1,56 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="imap"> + <div class="form-row node-input-repeat"> + <label for="node-input-repeat"><i class="fa fa-repeat"></i>Repeat (S)</label> + <input type="text" id="node-input-repeat" placeholder="300"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="imap"> + <p>Repeatedly gets a <b>single email</b> from an IMAP server and forwards on as a msg if not already seen.</p> + <p>The subject is loaded into <b>msg.topic</b> and <b>msg.payload</b> is the plain text body. + If there is text/html then that is returned in <b>msg.html</b>. <b>msg.from</b> and <b>msg.date</b> are also set if you need them.</p> + <p>Uses the imap module - you also need to pre-configure your email settings in a file emailkeys.js as per below.</p> + <p><pre>module.exports = { service: "Gmail", user: "blahblah@gmail.com", pass: "password", server: "imap.gmail.com", port: "993" }</pre></p> + <p>This <b>must</b> be located in the directory <b>above</b> node-red.</p> + <p><b>Note:</b> this node <i>only</i> gets the most recent single email from the inbox, so set the repeat (polling) time appropriately.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('imap',{ + category: 'deprecated', + color:"#c7e9c0", + defaults: { + repeat: {value:"300",required:true}, + name: {value:""} + }, + inputs:0, + outputs:1, + icon: "envelope.png", + label: function() { + return this.name||"IMAP"; + }, + labelStyle: function() { + return (this.name||!this.topic)?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/deprecated/61-imap.js b/dgbuilder/core_nodes/deprecated/61-imap.js new file mode 100644 index 00000000..aa2b4beb --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/61-imap.js @@ -0,0 +1,139 @@ +/** + * 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(process.env.NODE_RED_HOME+"/red/red"); +var Imap = require('imap'); +var util = require('util'); + +try { + var emailkey = RED.settings.email || require(process.env.NODE_RED_HOME+"/../emailkeys.js"); +} catch (err) { + //util.log("[61-imap.js] Info : No Email credentials found."); +} + +if (emailkey) { + var imap = new Imap({ + user: emailkey.user, + password: emailkey.pass, + host: emailkey.server||"imap.gmail.com", + port: emailkey.port||"993", + tls: true, + tlsOptions: { rejectUnauthorized: false } + }); + + function openInbox(cb) { + imap.openBox('INBOX', true, cb); + } +} + +function ImapNode(n) { + RED.nodes.createNode(this,n); + this.warn("This node has been deprecated and will be deleted in a future release. Please update your flow to use the 'e-mail in' node."); + this.name = n.name; + this.repeat = n.repeat * 1000 || 300000; + var node = this; + this.interval_id = null; + var oldmail = {}; + + if (!isNaN(this.repeat) && this.repeat > 0) { + node.log("repeat = "+this.repeat); + this.interval_id = setInterval( function() { + node.emit("input",{}); + }, this.repeat ); + } + + this.on("input", function(msg) { + if (imap) { + imap.once('ready', function() { + var pay = {}; + openInbox(function(err, box) { + if (box.messages.total > 0) { + var f = imap.seq.fetch(box.messages.total + ':*', { bodies: ['HEADER.FIELDS (FROM SUBJECT DATE)','TEXT'] }); + f.on('message', function(msg, seqno) { + node.log('message: #'+ seqno); + var prefix = '(#' + seqno + ') '; + msg.on('body', function(stream, info) { + var buffer = ''; + stream.on('data', function(chunk) { + buffer += chunk.toString('utf8'); + }); + stream.on('end', function() { + if (info.which !== 'TEXT') { + pay.from = Imap.parseHeader(buffer).from[0]; + pay.topic = Imap.parseHeader(buffer).subject[0]; + pay.date = Imap.parseHeader(buffer).date[0]; + } else { + var parts = buffer.split("Content-Type"); + for (var p in parts) { + if (parts[p].indexOf("text/plain") >= 0) { + pay.payload = parts[p].split("\n").slice(1,-2).join("\n").trim(); + } + if (parts[p].indexOf("text/html") >= 0) { + pay.html = parts[p].split("\n").slice(1,-2).join("\n").trim(); + } + } + //pay.body = buffer; + } + }); + }); + msg.on('end', function() { + //node.log('Finished: '+prefix); + }); + }); + f.on('error', function(err) { + node.warn('fetch error: ' + err); + }); + f.on('end', function() { + if (JSON.stringify(pay) !== oldmail) { + node.send(pay); + oldmail = JSON.stringify(pay); + node.log('sent new message: '+pay.topic); + } + else { node.log('duplicate not sent: '+pay.topic); } + imap.end(); + }); + } + else { + // node.log("you have achieved inbox zero"); + imap.end(); + } + }); + }); + imap.connect(); + } + else { node.warn("No Email credentials found. See info panel."); } + }); + + if (imap) { + imap.on('error', function(err) { + util.log(err); + }); + } + + this.on("error", function(err) { + node.log("error: ",err); + }); + + this.on("close", function() { + if (this.interval_id != null) { + clearInterval(this.interval_id); + } + if (imap) { imap.destroy(); } + }); + + node.emit("input",{}); +} +RED.nodes.registerType("imap",ImapNode); diff --git a/dgbuilder/core_nodes/deprecated/73-parsexml.html b/dgbuilder/core_nodes/deprecated/73-parsexml.html new file mode 100644 index 00000000..b6fc16fe --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/73-parsexml.html @@ -0,0 +1,53 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="xml2js"> + <!-- <div class="form-row"> + <label>Use Console</label> + <input type="checkbox" id="node-input-useEyes" placeholder="Name" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-useEyes" style="width: 70%;">Debug output in console ?</label> + </div> --> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <!-- <div class="form-tips">Uses xml2js to process xml into javascript object.</div> --> +</script> + +<script type="text/x-red" data-help-name="xml2js"> + <p>A function that parses the <b>msg.payload</b> using the xml2js library. Places the result in the payload.</p> + <p>See <a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md" target="_new">the xml2js docs <i>here</i></a> for more information.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('xml2js',{ + category: 'deprecated', + color:"#E6E0F8", + defaults: { + //useEyes: {value:false}, + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "arrow-in.png", + label: function() { + return this.name||"xml2json"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/deprecated/73-parsexml.js b/dgbuilder/core_nodes/deprecated/73-parsexml.js new file mode 100644 index 00000000..92850cb6 --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/73-parsexml.js @@ -0,0 +1,47 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var parseString = require('xml2js').parseString; + var useColors = true; + //util.inspect.styles.boolean = "red"; + + function Xml2jsNode(n) { + RED.nodes.createNode(this,n); + this.warn("This node has been deprecated and will be deleted in a future release. Please update your flow to use the 'xml' node."); + this.useEyes = n.useEyes||false; + var node = this; + this.on("input", function(msg) { + try { + parseString(msg.payload, {strict:true,async:true}, function (err, result) { + //parseString(msg.payload, {strict:false,async:true}, function (err, result) { + if (err) { node.error(err); } + else { + msg.payload = result; + node.send(msg); + if (node.useEyes == true) { + node.log("\n"+util.inspect(msg, {colors:useColors, depth:10})); + } + } + }); + } + catch(e) { util.log("[73-parsexml.js] "+e); } + }); + } + RED.nodes.registerType("xml2js",Xml2jsNode); +} diff --git a/dgbuilder/core_nodes/deprecated/74-js2xml.html b/dgbuilder/core_nodes/deprecated/74-js2xml.html new file mode 100644 index 00000000..f614579b --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/74-js2xml.html @@ -0,0 +1,51 @@ +<!--
+ 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.
+-->
+
+<script type="text/x-red" data-template-name="json2xml">
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-list"></i> XML Root</label>
+ <input type="text" id="node-input-root" placeholder="object"></input>
+ </div>
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+ <input type="text" id="node-input-name" placeholder="Name"></input>
+ </div>
+</script>
+
+<script type="text/x-red" data-help-name="json2xml">
+ <p>A function that parses the <b>msg.payload</b> using the js2xmlparser library. Places the result back in <b>msg.payload</b>.</p>
+ <p>See the <a href="https://github.com/michaelkourlas/node-js2xmlparser" target="_new">js2xmlparser docs</a> for more information.</p>
+</script>
+
+<script type="text/javascript">
+ RED.nodes.registerType('json2xml',{
+ category: 'deprecated',
+ color:"#E6E0F8",
+ defaults: {
+ name: {value:""},
+ root: {value:"object"},
+ },
+ inputs:1,
+ outputs:1,
+ icon: "arrow-in.png",
+ label: function() {
+ return this.name||"json2xml";
+ },
+ labelStyle: function() {
+ return this.name?"node_label_italic":"";
+ }
+ });
+</script>
diff --git a/dgbuilder/core_nodes/deprecated/74-js2xml.js b/dgbuilder/core_nodes/deprecated/74-js2xml.js new file mode 100644 index 00000000..164bafad --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/74-js2xml.js @@ -0,0 +1,39 @@ +/**
+ * 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.
+ **/
+
+module.exports = function(RED) {
+ "use strict";
+ var js2xmlparser = require("js2xmlparser");
+
+ function Js2XmlNode(n) {
+ RED.nodes.createNode(this,n);
+ this.warn("This node has been deprecated and will be deleted in a future release. Please update your flow to use the 'xml' node.");
+ this.root = n.root;
+ var node = this;
+
+ this.on("input", function(msg) {
+ try {
+ var root = node.root || typeof msg.payload;
+ if (typeof msg.payload !== "object") { msg.payload = '"'+msg.payload+'"'; }
+ console.log(root, typeof msg.payload,msg.payload);
+ msg.payload = js2xmlparser(root, msg.payload);
+ node.send(msg);
+ }
+ catch(e) { console.log(e); }
+ });
+ }
+ RED.nodes.registerType("json2xml",Js2XmlNode);
+}
diff --git a/dgbuilder/core_nodes/deprecated/90-httpget.html b/dgbuilder/core_nodes/deprecated/90-httpget.html new file mode 100644 index 00000000..b1f2e080 --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/90-httpget.html @@ -0,0 +1,61 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="httpget"> + <div class="form-tips"><b>Deprecated</b>: please use the <i>http request</i> node.</div> + <br> + <div class="form-row"> + <label for="node-input-baseurl"><i class="fa fa-tasks"></i> Base URL</label> + <input type="text" id="node-input-baseurl" placeholder="http(s)://url"> + </div> + <div class="form-row"> + <label for="node-input-append"><i class="fa fa-tasks"></i> Append</label> + <input type="text" id="node-input-append" placeholder=""> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">The <b>Base URL</b> gets prepended to whatever payload is passed in. Leave blank if you pass in a full url.<br/>The append gets added to the end after any payload.<br/>The output Topic is the same as the input Topic.</div> +</script> + +<script type="text/x-red" data-help-name="httpget"> + <p>Performs an HTTP or HTTPS GET and returns the fetched page.</p> + <p>The return code is placed in <b>msg.rc</b>, and the full text of the result is in <b>msg.payload</b>.</p> + <p>The <b>msg.payload</b> is added to the base url, and then the optional append is added after.</p> + <p>This is mostly suitable for small pages as large results will need a lot of parsing....</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('httpget',{ + category: 'deprecated', + color:"rgb(231, 231, 174)", + defaults: { + name: {value:""}, + baseurl: {value:""}, + append: {value:""} + }, + inputs:1, + outputs:1, + icon: "white-globe.png", + label: function() { + return this.name||this.baseurl; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/deprecated/90-httpget.js b/dgbuilder/core_nodes/deprecated/90-httpget.js new file mode 100644 index 00000000..63e16b93 --- /dev/null +++ b/dgbuilder/core_nodes/deprecated/90-httpget.js @@ -0,0 +1,53 @@ +/** + * 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(process.env.NODE_RED_HOME+"/red/red"); + +function HttpGet(n) { + RED.nodes.createNode(this,n); + this.warn("This node has been deprecated and will be deleted in a future release. Please update your flow to use the 'http request' node."); + this.baseurl = n.baseurl || ""; + this.append = n.append || ""; + var node = this; + if (this.baseurl.substring(0,5) === "https") { var http = require("https"); } + else { var http = require("http"); } + this.on("input", function(msg) { + msg._payload = msg.payload; + //util.log("[httpget] "+this.baseurl+msg.payload+this.append); + http.get(this.baseurl+msg.payload+this.append, function(res) { + node.log("Http response: " + res.statusCode); + msg.rc = res.statusCode; + msg.payload = ""; + if ((msg.rc != 200) && (msg.rc != 404)) { + node.send(msg); + } + res.setEncoding('utf8'); + res.on('data', function(chunk) { + msg.payload += chunk; + }); + res.on('end', function() { + node.send(msg); + }); + }).on('error', function(e) { + //node.error(e); + msg.rc = 503; + msg.payload = e; + node.send(msg); + }); + }); +} + +RED.nodes.registerType("httpget",HttpGet); diff --git a/dgbuilder/core_nodes/hardware/35-arduino.html b/dgbuilder/core_nodes/hardware/35-arduino.html new file mode 100644 index 00000000..17f02892 --- /dev/null +++ b/dgbuilder/core_nodes/hardware/35-arduino.html @@ -0,0 +1,171 @@ +<!-- + Copyright 2013,2014 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. +--> +<script type="text/x-red" data-template-name="arduino in"> + <div class="form-row"> + <label for="node-input-arduino"><i class="fa fa-tasks"></i> Arduino</label> + <input type="text" id="node-input-arduino"> + </div> + <div class="form-row"> + <label for="node-input-pin"><i class="fa fa-circle"></i> Pin</label> + <input type="text" id="node-input-pin" placeholder="2"> + </div> + <div class="form-row"> + <label for="node-input-state"><i class="fa fa-wrench"></i> Type</label> + <select type="text" id="node-input-state" style="width: 150px;"> + <option value="INPUT">Digital pin</option> + <option value="ANALOG">Analogue pin</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips"><b>Note:</b> You cannot use the same pin for both output and input.</div> +</script> + +<script type="text/x-red" data-help-name="arduino in"> + <p>Arduino input node. Connects to local Arduino and monitors the selected pin for changes. Uses <a href="http://firmata.org/" target="_new"><i>Firmata</i>.</a></p> + <p>The Arduino must be loaded with the Standard Firmata sketch available in the Arduino examples.</p> + <p>You can select either Digital or Analogue input. Outputs the value read as <b>msg.payload</b> and the pin number as <b>msg.topic</b>.</p> + <p>It only outputs on a change of value - fine for digital inputs, but you can get a lot of data from analogue pins which you must then handle.</p> + <p>You can set the sample rate in ms from 20 to 65535.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('arduino in',{ + category: 'Arduino', + color:"#3fadb5", + defaults: { + name: {value:""}, + pin: {value:"",required:true}, + state: {value:"INPUT",required:true}, + arduino: {type:"arduino-board"} + }, + inputs:0, + outputs:1, + icon: "arduino.png", + label: function() { + var a = ""; + if (this.state == "ANALOG") a = "A"; + return this.name||"Pin: "+a+this.pin; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + +<script type="text/x-red" data-template-name="arduino out"> + <div class="form-row"> + <label for="node-input-arduino"><i class="fa fa-tasks"></i> Arduino</label> + <input type="text" id="node-input-arduino"> + </div> + <div class="form-row"> + <label for="node-input-pin"><i class="fa fa-circle"></i> Pin</label> + <input type="text" id="node-input-pin" placeholder="13"> + </div> + <div class="form-row"> + <label for="node-input-state"><i class="fa fa-wrench"></i> Type</label> + <select type="text" id="node-input-state" style="width: 200px;"> + <option value="OUTPUT">Digital (0/1)</option> + <option value="PWM">Analogue (0-255)</option> + <option value="SERVO">Servo (0-180)</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips"><b>Note:</b> You cannot use the same pin for both output and input.</div> +</script> + +<script type="text/x-red" data-help-name="arduino out"> + <p>Arduino output node. Connects to local Arduino and writes to the selected digital pin. Uses <a href="http://firmata.org/" target="_new"><i>Firmata</i>.</a></p> + <p>The Arduino must be loaded with the Standard Firmata sketch available in the Arduino examples.</p> + <p>You can select Digital, Analogue (PWM) or Servo type outputs. Expects a numeric value in <b>msg.payload</b>. The pin number is set in the properties panel.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('arduino out',{ + category: 'Arduino', + color:"#3fadb5", + defaults: { + name: {value:""}, + pin: {value:"",required:true}, + state: {value:"",required:true}, + arduino: {type:"arduino-board"} + }, + inputs:1, + outputs:0, + icon: "arduino.png", + align: "right", + label: function() { + return this.name||"Pin: "+this.pin; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + + +<script type="text/x-red" data-template-name="arduino-board"> + <div class="form-row"> + <label for="node-config-input-device"><i class="fa fa-random"></i> Port</label> + <input type="text" id="node-config-input-device" style="width:60%;" placeholder="e.g. /dev/ttyUSB0 COM1"/> + <a id="node-config-lookup-serial" class="btn"><i id="node-config-lookup-serial-icon" class="fa fa-search"></i></a> + </div> + <div class="form-tips"><b>Tip:</b> Use search to try to auto-detect serial port.</div> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('arduino-board',{ + category: 'config', + defaults: { + device: {value:"",required:true} + }, + label: function() { + return this.device||"arduino"; + }, + oneditprepare: function() { + try { + $("#node-config-input-device").autocomplete( "destroy" ); + } catch(err) { } + $("#node-config-lookup-serial").click(function() { + $("#node-config-lookup-serial-icon").removeClass('fa-search'); + $("#node-config-lookup-serial-icon").addClass('spinner'); + $("#node-config-lookup-serial").addClass('disabled'); + + $.getJSON('arduinoports',function(data) { + $("#node-config-lookup-serial-icon").addClass('fa-search'); + $("#node-config-lookup-serial-icon").removeClass('spinner'); + $("#node-config-lookup-serial").removeClass('disabled'); + var ports = []; + $.each(data, function(i, port){ + ports.push(port); + }); + $("#node-config-input-device").autocomplete({ + source:ports, + minLength:0, + close: function( event, ui ) { + $("#node-config-input-device").autocomplete( "destroy" ); + } + }).autocomplete("search",""); + }); + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/hardware/35-arduino.js b/dgbuilder/core_nodes/hardware/35-arduino.js new file mode 100644 index 00000000..795e9907 --- /dev/null +++ b/dgbuilder/core_nodes/hardware/35-arduino.js @@ -0,0 +1,160 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var ArduinoFirmata = require('arduino-firmata'); + var fs = require('fs'); + var plat = require('os').platform(); + var portlist = ArduinoFirmata.list(function (err, ports) { + portlist = ports; + }); + + // The Board Definition - this opens (and closes) the connection + function ArduinoNode(n) { + RED.nodes.createNode(this,n); + this.device = n.device || null; + this.repeat = n.repeat||25; + //node.log("opening connection "+this.device); + var node = this; + node.board = new ArduinoFirmata(); + if (portlist.indexOf(node.device) === -1) { + node.warn("Device "+node.device+" not found"); + } + else { + node.board.connect(node.device); + } + + node.board.on('boardReady', function(){ + node.log("version "+node.board.boardVersion); + }); + + node.on('close', function() { + if (node.board) { + try { + node.board.close(function() { + node.log("port closed"); + }); + } catch(e) { } + } + }); + } + RED.nodes.registerType("arduino-board",ArduinoNode); + + + // The Input Node + function DuinoNodeIn(n) { + RED.nodes.createNode(this,n); + this.buttonState = -1; + this.pin = n.pin; + this.state = n.state; + this.arduino = n.arduino; + this.serverConfig = RED.nodes.getNode(this.arduino); + if (typeof this.serverConfig === "object") { + this.board = this.serverConfig.board; + //this.repeat = this.serverConfig.repeat; + var node = this; + node.status({fill:"red",shape:"ring",text:"connecting"}); + + node.board.on('connect', function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + //console.log("i",node.state,node.pin); + if (node.state == "ANALOG") { + node.board.on('analogChange', function(e) { + if (e.pin == node.pin) { + var msg = {payload:e.value, topic:"A"+e.pin}; + node.send(msg); + } + }); + + } + else { + node.board.pinMode(node.pin, ArduinoFirmata.INPUT); + node.board.on('digitalChange', function(e) { + if (e.pin == node.pin) { + var msg = {payload:e.value, topic:e.pin}; + node.send(msg); + } + }); + } + }); + } + else { + util.log("[Firmata-arduino] port not configured"); + } + } + RED.nodes.registerType("arduino in",DuinoNodeIn); + + + // The Output Node + function DuinoNodeOut(n) { + RED.nodes.createNode(this,n); + this.buttonState = -1; + this.pin = n.pin; + this.state = n.state; + this.arduino = n.arduino; + this.serverConfig = RED.nodes.getNode(this.arduino); + if (typeof this.serverConfig === "object") { + this.board = this.serverConfig.board; + var node = this; + node.status({fill:"red",shape:"ring",text:"connecting"}); + + node.board.on('connect', function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + //console.log("o",node.state,node.pin); + node.board.pinMode(node.pin, node.state); + node.on("input", function(msg) { + if (node.state == "OUTPUT") { + if ((msg.payload == true)||(msg.payload == 1)||(msg.payload.toString().toLowerCase() == "on")) { + node.board.digitalWrite(node.pin, true); + } + if ((msg.payload == false)||(msg.payload == 0)||(msg.payload.toString().toLowerCase() == "off")) { + node.board.digitalWrite(node.pin, false); + } + } + if (node.state == "PWM") { + msg.payload = msg.payload * 1; + if ((msg.payload >= 0) && (msg.payload <= 255)) { + //console.log(msg.payload, node.pin); + node.board.servoWrite(node.pin, msg.payload); + } + } + if (node.state == "SERVO") { + msg.payload = msg.payload * 1; + if ((msg.payload >= 0) && (msg.payload <= 180)) { + //console.log(msg.payload, node.pin); + node.board.servoWrite(node.pin, msg.payload); + } + } + }); + }); + } + else { + util.log("[Firmata-arduino] port not configured"); + } + } + RED.nodes.registerType("arduino out",DuinoNodeOut); + + RED.httpAdmin.get("/arduinoports",function(req,res) { + ArduinoFirmata.list(function (err, ports) { + //console.log(JSON.stringify(ports)); + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write(JSON.stringify(ports)); + res.end(); + }); + }); +} diff --git a/dgbuilder/core_nodes/hardware/36-rpi-gpio.html b/dgbuilder/core_nodes/hardware/36-rpi-gpio.html new file mode 100644 index 00000000..9e705c2c --- /dev/null +++ b/dgbuilder/core_nodes/hardware/36-rpi-gpio.html @@ -0,0 +1,182 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="rpi-gpio in"> + <div class="form-row"> + <label for="node-input-pin"><i class="fa fa-circle"></i> GPIO Pin</label> + <select type="text" id="node-input-pin" style="width: 200px;"> + <option value="-" disabled>select pin </option> + <option value="3">3 - SDA1 </option> + <option value="5">5 - SCL1 </option> + <option value="7">7 - GPIO7</option> + <option value="8">8 - TxD </option> + <option value="10">10 - RxD </option> + <option value="11">11 - GPIO0</option> + <option value="12">12 - GPIO1</option> + <option value="13">13 - GPIO2</option> + <option value="15">15 - GPIO3</option> + <option value="16">16 - GPIO4</option> + <option value="18">18 - GPIO5</option> + <option value="19">19 - MOSI </option> + <option value="21">21 - MISO </option> + <option value="22">22 - GPIO6</option> + <option value="23">23 - SCLK </option> + <option value="24">24 - CE0 </option> + <option value="26">26 - CE1 </option> + </select> + <span id="pitype"></span> + </div> + <div class="form-row"> + <label for="node-input-intype"><i class="fa fa-long-arrow-up"></i> Resistor?</label> + <select type="text" id="node-input-intype" style="width: 150px;"> + <option value="tri">none</option> + <option value="up">pullup</option> + <option value="down">pulldown</option> + <!--<option value="tri">tristate</option>--> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: Only Digital I/O is supported - input must be 0 or 1.</div> +</script> + +<script type="text/x-red" data-help-name="rpi-gpio in"> + <p>Raspberry Pi input node. Generates a <b>msg.payload</b> with either a 0 or 1 depending on the state of the input pin. Requires the gpio command to work.</p> + <p>You may also enable the input pullup resitor or the pulldown resistor.</p> + <p>The <b>msg.topic</b> is set to <i>pi/{the pin number}</i></p> + <p><b>Note:</b> we are using the actual physical pin numbers on connector P1 as they are easier to locate.</p> + <p><b>Note:</b> This node currently polls the pin every 250mS. This is not ideal as it loads the cpu, and will be rewritten shortly to try to use interrupts.</p> + +</script> + +<script type="text/javascript"> + RED.nodes.registerType('rpi-gpio in',{ + category: 'advanced-input', + color:"#c6dbef", + defaults: { + name: { value:"" }, + intype: { value: "in" }, + pin: { value:"",required:true,validate:RED.validators.number() }, + }, + inputs:0, + outputs:1, + icon: "rpi.png", + label: function() { + return this.name||"Pin: "+this.pin ; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $.getJSON('rpi-gpio/'+this.id,function(data) { + $('#pitype').text(data.type); + if (data.type === "Model B+") { + $('#node-input-pin').append($("<option></option>").attr("value",27).text("27 - SDA0")); + $('#node-input-pin').append($("<option></option>").attr("value",28).text("28 - SCL0")); + $('#node-input-pin').append($("<option></option>").attr("value",29).text("29 - GPIO21")); + $('#node-input-pin').append($("<option></option>").attr("value",31).text("31 - GPIO22")); + $('#node-input-pin').append($("<option></option>").attr("value",32).text("32 - GPIO26")); + $('#node-input-pin').append($("<option></option>").attr("value",33).text("33 - GPIO23")); + $('#node-input-pin').append($("<option></option>").attr("value",35).text("35 - GPIO24")); + $('#node-input-pin').append($("<option></option>").attr("value",36).text("36 - GPIO27")); + $('#node-input-pin').append($("<option></option>").attr("value",37).text("37 - GPIO25")); + $('#node-input-pin').append($("<option></option>").attr("value",38).text("38 - GPIO28")); + $('#node-input-pin').append($("<option></option>").attr("value",40).text("40 - GPIO29")); + } + }); + } + }); +</script> + + +<script type="text/x-red" data-template-name="rpi-gpio out"> + <div class="form-row"> + <label for="node-input-pin"><i class="fa fa-circle"></i> GPIO Pin</label> + <select type="text" id="node-input-pin" style="width: 200px;"> + <option value="-">select pin </option> + <option value="3">3 - SDA1 </option> + <option value="5">5 - SCL1 </option> + <option value="7">7 - GPIO7</option> + <option value="8">8 - TxD </option> + <option value="10">10 - RxD </option> + <option value="11">11 - GPIO0</option> + <option value="12">12 - GPIO1</option> + <option value="13">13 - GPIO2</option> + <option value="15">15 - GPIO3</option> + <option value="16">16 - GPIO4</option> + <option value="18">18 - GPIO5</option> + <option value="19">19 - MOSI </option> + <option value="21">21 - MISO </option> + <option value="22">22 - GPIO6</option> + <option value="23">23 - SCLK </option> + <option value="24">24 - CE0 </option> + <option value="26">26 - CE1 </option> + </select> + <span id="pitype"></span> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: Only Digital I/O is supported - input must be 0 or 1.</div> +</script> + +<script type="text/x-red" data-help-name="rpi-gpio out"> + <p>Raspberry Pi output node. Expects a <b>msg.payload</b> with either a 0 or 1 (or true or false). Requires the gpio command to work.</p> + <p>Will set the selected physical pin high or low depending on the value passed in.</p> + <p><b>Note:</b> we are using the actual physical pin numbers on connector P1 as they are easier to locate.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('rpi-gpio out',{ + category: 'advanced-output', + color:"#c6dbef", + defaults: { + name: { value:"" }, + pin: { value:"",required:true,validate:RED.validators.number() }, + }, + inputs:1, + outputs:0, + icon: "rpi.png", + align: "right", + label: function() { + return this.name||"Pin: "+this.pin; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $.getJSON('rpi-gpio/'+this.id,function(data) { + $('#pitype').text(data.type); + if (data.type === "Model B+") { + $('#node-input-pin').append($("<option></option>").attr("value",27).text("27 - SDA0")); + $('#node-input-pin').append($("<option></option>").attr("value",28).text("28 - SCL0")); + $('#node-input-pin').append($("<option></option>").attr("value",29).text("29 - GPIO21")); + $('#node-input-pin').append($("<option></option>").attr("value",31).text("31 - GPIO22")); + $('#node-input-pin').append($("<option></option>").attr("value",32).text("32 - GPIO26")); + $('#node-input-pin').append($("<option></option>").attr("value",33).text("33 - GPIO23")); + $('#node-input-pin').append($("<option></option>").attr("value",35).text("35 - GPIO24")); + $('#node-input-pin').append($("<option></option>").attr("value",36).text("36 - GPIO27")); + $('#node-input-pin').append($("<option></option>").attr("value",37).text("37 - GPIO25")); + $('#node-input-pin').append($("<option></option>").attr("value",38).text("38 - GPIO28")); + $('#node-input-pin').append($("<option></option>").attr("value",40).text("40 - GPIO29")); + } + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/hardware/36-rpi-gpio.js b/dgbuilder/core_nodes/hardware/36-rpi-gpio.js new file mode 100644 index 00000000..93cbc4e0 --- /dev/null +++ b/dgbuilder/core_nodes/hardware/36-rpi-gpio.js @@ -0,0 +1,185 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var exec = require('child_process').exec; + var fs = require('fs'); + + var gpioCommand = '/usr/local/bin/gpio'; + + if (!fs.existsSync("/dev/ttyAMA0")) { // unlikely if not on a Pi + throw "Info : Ignoring Raspberry Pi specific node."; + } + + if (!fs.existsSync(gpioCommand)) { // gpio command not installed + throw "Info : Can't find Raspberry Pi wiringPi gpio command."; + } + + // Map physical P1 pins to Gordon's Wiring-Pi Pins (as they should be V1/V2 tolerant) + var pintable = { + // Physical : WiringPi + "11":"0", + "12":"1", + "13":"2", + "15":"3", + "16":"4", + "18":"5", + "22":"6", + "7":"7", + "3":"8", + "5":"9", + "24":"10", + "26":"11", + "19":"12", + "21":"13", + "23":"14", + "8":"15", + "10":"16", + "27":"30", + "28":"31", + "29":"21", + "31":"22", + "32":"26", + "33":"23", + "35":"24", + "36":"27", + "37":"25", + "38":"28", + "40":"29" + } + var tablepin = { + // WiringPi : Physical + "0":"11", + "1":"12", + "2":"13", + "3":"15", + "4":"16", + "5":"18", + "6":"22", + "7":"7", + "8":"3", + "9":"5", + "10":"24", + "11":"26", + "12":"19", + "13":"21", + "14":"23", + "15":"8", + "16":"10", + "30":"27", + "31":"28", + "21":"29", + "22":"31", + "26":"32", + "23":"33", + "24":"35", + "27":"36", + "25":"37", + "28":"38", + "29":"40" + } + + function GPIOInNode(n) { + RED.nodes.createNode(this,n); + this.buttonState = -1; + this.pin = pintable[n.pin]; + this.intype = n.intype; + var node = this; + + if (node.pin !== undefined) { + exec(gpioCommand+" mode "+node.pin+" "+node.intype, function(err,stdout,stderr) { + if (err) { node.error(err); } + else { + node._interval = setInterval( function() { + exec(gpioCommand+" read "+node.pin, function(err,stdout,stderr) { + if (err) { node.error(err); } + else { + if (node.buttonState !== Number(stdout)) { + var previousState = node.buttonState; + node.buttonState = Number(stdout); + if (previousState !== -1) { + var msg = {topic:"pi/"+tablepin[node.pin], payload:node.buttonState}; + node.send(msg); + } + } + } + }); + }, 250); + } + }); + } + else { + node.error("Invalid GPIO pin: "+node.pin); + } + + node.on("close", function() { + clearInterval(node._interval); + }); + } + + function GPIOOutNode(n) { + RED.nodes.createNode(this,n); + this.pin = pintable[n.pin]; + var node = this; + + if (node.pin !== undefined) { + process.nextTick(function() { + exec(gpioCommand+" mode "+node.pin+" out", function(err,stdout,stderr) { + if (err) { node.error(err); } + else { + node.on("input", function(msg) { + if (msg.payload === "true") { msg.payload = true; } + if (msg.payload === "false") { msg.payload = false; } + var out = Number(msg.payload); + if ((out === 0)|(out === 1)) { + exec(gpioCommand+" write "+node.pin+" "+out, function(err,stdout,stderr) { + if (err) { node.error(err); } + }); + } + else { node.warn("Invalid input - not 0 or 1"); } + }); + } + }); + }); + } + else { + node.error("Invalid GPIO pin: "+node.pin); + } + + node.on("close", function() { + exec(gpioCommand+" mode "+node.pin+" in"); + }); + } + + var pitype = { type:"" }; + exec(gpioCommand+" -v | grep Type", function(err,stdout,stderr) { + if (err) { + util.log('[36-rpi-gpio.js] Error: "'+gpioCommand+' -v" command failed for some reason.'); + } + else { + pitype = { type:(stdout.split(","))[0].split(": ")[1], rev:(stdout.split(","))[1].split(": ")[1] }; + } + }); + + RED.nodes.registerType("rpi-gpio in",GPIOInNode); + RED.nodes.registerType("rpi-gpio out",GPIOOutNode); + + RED.httpAdmin.get('/rpi-gpio/:id',function(req,res) { + res.send( JSON.stringify(pitype) ); + }); +} diff --git a/dgbuilder/core_nodes/io/10-mqtt.html b/dgbuilder/core_nodes/io/10-mqtt.html new file mode 100644 index 00000000..2ff5eb29 --- /dev/null +++ b/dgbuilder/core_nodes/io/10-mqtt.html @@ -0,0 +1,157 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="mqtt in"> + <div class="form-row"> + <label for="node-input-broker"><i class="fa fa-globe"></i> Broker</label> + <input type="text" id="node-input-broker"> + </div> + <div class="form-row"> + <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label> + <input type="text" id="node-input-topic" placeholder="Topic"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="mqtt in"> + <p>MQTT input node. Connects to a broker and subscribes to the specified topic. The topic may contain MQTT wildcards.</p> + <p>Outputs an object called <b>msg</b> containing <b>msg.topic, msg.payload, msg.qos</b> and <b>msg.retain</b>.</p> + <p><b>msg.payload</b> is a String.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('mqtt in',{ + category: 'input', + defaults: { + name: {value:""}, + topic: {value:"",required:true}, + broker: {type:"mqtt-broker", required:true} + }, + color:"#d8bfd8", + inputs:0, + outputs:1, + icon: "bridge.png", + label: function() { + return this.name||this.topic||"mqtt"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + +<script type="text/x-red" data-template-name="mqtt out"> + <div class="form-row"> + <label for="node-input-broker"><i class="fa fa-globe"></i> Broker</label> + <input type="text" id="node-input-broker"> + </div> + <div class="form-row"> + <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label> + <input type="text" id="node-input-topic" placeholder="Topic"> + </div> + <div class="form-row"> + <label for="node-input-qos"><i class="fa fa-empire"></i> QoS</label> + <select id="node-input-qos" style="width:125px !important"> + <option value=""></option> + <option value="0">0</option> + <option value="1">1</option> + <option value="2">2</option> + </select> + <i class="fa fa-history"></i> Retain <select id="node-input-retain" style="width:125px !important"> + <option value=""></option> + <option value="false">false</option> + <option value="true">true</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: Leave topic, qos or retain blank if you want to set them via msg properties.</div> +</script> + +<script type="text/x-red" data-help-name="mqtt out"> + <p>Connects to a MQTT broker and publishes <b>msg.payload</b> either to the <b>msg.topic</b> or to the topic specified in the edit window. The value in the edit window has precedence.</p> + <p>Likewise QoS and/or retain values in the edit panel will overwrite any <b>msg.qos</b> and <b>msg.retain</b> properties. If nothing is set they default to <i>0</i> and <i>false</i> respectively.</p> + <p>If <b>msg.payload</b> contains an object it will be stringified before being sent.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('mqtt out',{ + category: 'output', + defaults: { + name: {value:""}, + topic: {value:""}, + qos: {value:""}, + retain: {value:""}, + broker: {type:"mqtt-broker", required:true} + }, + color:"#d8bfd8", + inputs:1, + outputs:0, + icon: "bridge.png", + align: "right", + label: function() { + return this.name||this.topic||"mqtt"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + +<script type="text/x-red" data-template-name="mqtt-broker"> + <div class="form-row node-input-broker"> + <label for="node-config-input-broker"><i class="fa fa-globe"></i> Broker</label> + <input class="input-append-left" type="text" id="node-config-input-broker" placeholder="localhost" style="width: 40%;" > + <label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> Port</label> + <input type="text" id="node-config-input-port" placeholder="Port" style="width:45px"> + </div> + <div class="form-row"> + <label for="node-config-input-clientid"><i class="fa fa-tag"></i> Client ID</label> + <input type="text" id="node-config-input-clientid" placeholder="Leave blank for auto generated"> + </div> + <div class="form-row"> + <label for="node-config-input-user"><i class="fa fa-user"></i> Username</label> + <input type="text" id="node-config-input-user"> + </div> + <div class="form-row"> + <label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label> + <input type="password" id="node-config-input-password"> + </div> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('mqtt-broker',{ + category: 'config', + defaults: { + broker: {value:"",required:true}, + port: {value:1883,required:true,validate:RED.validators.number()}, + clientid: { value:"" } + }, + credentials: { + user: {type:"text"}, + password: {type: "password"} + }, + label: function() { + if (this.broker == "") { this.broker = "localhost"; } + return (this.clientid?this.clientid+"@":"")+this.broker+":"+this.port; + } + }); +</script> diff --git a/dgbuilder/core_nodes/io/10-mqtt.js b/dgbuilder/core_nodes/io/10-mqtt.js new file mode 100644 index 00000000..c8bc4901 --- /dev/null +++ b/dgbuilder/core_nodes/io/10-mqtt.js @@ -0,0 +1,119 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var connectionPool = require("./lib/mqttConnectionPool"); + + function MQTTBrokerNode(n) { + RED.nodes.createNode(this,n); + this.broker = n.broker; + this.port = n.port; + this.clientid = n.clientid; + if (this.credentials) { + this.username = this.credentials.user; + this.password = this.credentials.password; + } + } + RED.nodes.registerType("mqtt-broker",MQTTBrokerNode,{ + credentials: { + user: {type:"text"}, + password: {type: "password"} + } + }); + + function MQTTInNode(n) { + RED.nodes.createNode(this,n); + this.topic = n.topic; + this.broker = n.broker; + this.brokerConfig = RED.nodes.getNode(this.broker); + if (this.brokerConfig) { + this.status({fill:"red",shape:"ring",text:"disconnected"}); + this.client = connectionPool.get(this.brokerConfig.broker,this.brokerConfig.port,this.brokerConfig.clientid,this.brokerConfig.username,this.brokerConfig.password); + var node = this; + this.client.subscribe(this.topic,2,function(topic,payload,qos,retain) { + var msg = {topic:topic,payload:payload,qos:qos,retain:retain}; + if ((node.brokerConfig.broker == "localhost")||(node.brokerConfig.broker == "127.0.0.1")) { + msg._topic = topic; + } + node.send(msg); + }); + this.client.on("connectionlost",function() { + node.status({fill:"red",shape:"ring",text:"disconnected"}); + }); + this.client.on("connect",function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + this.client.connect(); + } else { + this.error("missing broker configuration"); + } + this.on('close', function() { + if (this.client) { + this.client.disconnect(); + } + }); + } + RED.nodes.registerType("mqtt in",MQTTInNode); + + function MQTTOutNode(n) { + RED.nodes.createNode(this,n); + this.topic = n.topic; + this.qos = n.qos || null; + this.retain = n.retain; + this.broker = n.broker; + this.brokerConfig = RED.nodes.getNode(this.broker); + + if (this.brokerConfig) { + this.status({fill:"red",shape:"ring",text:"disconnected"},true); + this.client = connectionPool.get(this.brokerConfig.broker,this.brokerConfig.port,this.brokerConfig.clientid,this.brokerConfig.username,this.brokerConfig.password); + var node = this; + this.on("input",function(msg) { + if (msg.qos) { + msg.qos = parseInt(msg.qos); + if ((msg.qos !== 0) && (msg.qos !== 1) && (msg.qos !== 2)) { + msg.qos = null; + } + } + msg.qos = Number(node.qos || msg.qos || 0); + msg.retain = node.retain || msg.retain || false; + msg.retain = ((msg.retain === true) || (msg.retain === "true")) || false; + if (node.topic) { + msg.topic = node.topic; + } + if ((msg.hasOwnProperty("topic")) && (typeof msg.topic === "string") && (msg.topic !== "")) { // topic must exist + this.client.publish(msg); // send the message + } + else { node.warn("Invalid topic specified"); } + }); + this.client.on("connectionlost",function() { + node.status({fill:"red",shape:"ring",text:"disconnected"}); + }); + this.client.on("connect",function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + this.client.connect(); + } else { + this.error("missing broker configuration"); + } + this.on('close', function() { + if (this.client) { + this.client.disconnect(); + } + }); + } + RED.nodes.registerType("mqtt out",MQTTOutNode); +} diff --git a/dgbuilder/core_nodes/io/21-httpin.html b/dgbuilder/core_nodes/io/21-httpin.html new file mode 100644 index 00000000..059b8596 --- /dev/null +++ b/dgbuilder/core_nodes/io/21-httpin.html @@ -0,0 +1,254 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="http in"> + <div class="form-row"> + <label for="node-input-method"><i class="fa fa-tasks"></i> Method</label> + <select type="text" id="node-input-method" style="width:72%;"> + <option value="get">GET</option> + <option value="post">POST</option> + <option value="put">PUT</option> + <option value="delete">DELETE</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-url"><i class="fa fa-globe"></i> url</label> + <input type="text" id="node-input-url" placeholder="/url"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div id="node-input-tip" class="form-tips">The url will be relative to <code><span id="node-input-path"></span></code>.</div> +</script> + +<script type="text/x-red" data-help-name="http in"> + <p>Provides an input node for http requests, allowing the creation of simple web services.</p> + <p>The resulting message has the following properties: + <ul> + <li>msg.req : <a href="http://expressjs.com/api.html#req">http request</a></li> + <li>msg.res : <a href="http://expressjs.com/api.html#res">http response</a></li> + </ul> + </p> + <p>For POST/PUT requests, the body is available under <code>msg.req.body</code>. This + uses the <a href="http://expressjs.com/api.html#bodyParser">Express bodyParser middleware</a> to parse the content to a JSON object. + </p> + <p> + By default, this expects the body of the request to be url encoded: + <pre>foo=bar&this=that</pre> + </p> + <p> + To send JSON encoded data to the node, the content-type header of the request must be set to + <code>application/json</code>. + </p> + <p> + <b>Note: </b>This node does not send any response to the http request. This should be done with + a subsequent HTTP Response node, or Function node. + In the case of a Function node, the <a href="http://expressjs.com/api.html#res">Express response documentation</a> + describes how this should be done. For example: + <pre>msg.res.send(200, 'Thanks for the request ');<br/>return msg;</pre> + </p> + +</script> + +<script type="text/x-red" data-template-name="http response"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">The messages sent to this node <b>must</b> originate from an <i>http input</i> node</div> +</script> + +<script type="text/x-red" data-help-name="http response"> + <p>Sends responses back to http requests received from an HTTP Input node.</p> + <p>The response can be customised using the following message properties:</p> + <ul> + <li><code>payload</code> is sent as the body of the response</li> + <li><code>statusCode</code>, if set, is used as the response status code (default: 200)</li> + <li><code>headers</code>, if set, should be an object containing field/value + pairs to be added as response headers.</li> + </ul> +</script> + +<script type="text/x-red" data-template-name="http request"> + <div class="form-row"> + <label for="node-input-method"><i class="fa fa-tasks"></i> Method</label> + <select type="text" id="node-input-method" style="width:72%;"> + <option value="GET">GET</option> + <option value="POST">POST</option> + <option value="PUT">PUT</option> + <option value="DELETE">DELETE</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-url"><i class="fa fa-globe"></i> URL</label> + <input type="text" id="node-input-url" placeholder="http://"> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-useAuth" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-useAuth" style="width: 70%;">Use basic authentication?</label> + </div> + <div class="form-row node-input-useAuth-row"> + <label for="node-input-user"><i class="fa fa-user"></i> Username</label> + <input type="text" id="node-input-user"> + </div> + <div class="form-row node-input-useAuth-row"> + <label for="node-input-password"><i class="fa fa-lock"></i> Password</label> + <input type="password" id="node-input-password"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="http request"> + <p>Provides a node for making http requests.</p> + <p>The URL and HTTP method can be configured in the node, but also + overridden by the incoming message: + <ul> + <li><code>url</code>, if set, is used as the url of the request. Must start with http: or https:</li> + <li><code>method</code>, if set, is used as the HTTP method of the request. + Must be one of <code>GET</code>, <code>PUT</code>, <code>POST</code> or <code>DELETE</code> (default: GET)</li> + <li><code>headers</code>, if set, should be an object containing field/value + pairs to be added as request headers</li> + <li><code>payload</code> is sent as the body of the request</li> + </ul> + <p>When configured within the node, the URL property can contain <a href="http://mustache.github.io/mustache.5.html" target="_new">mustache-style</a> tags. These allow the + url to be constructed using values of the incoming message. For example, if the url is set to + <code>example.com/{{topic}}</code>, it will have the value of <code>msg.topic</code> automatically inserted.</p> + <p> + The output message contains the following properties: + <ul> + <li><code>payload</code> is the body of the response</li> + <li><code>statusCode</code> is the status code of the response, or the error code if the request could not be completed</li> + <li><code>headers</code> is an object containing the response headers</li> + </ul> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('http in',{ + category: 'input', + color:"rgb(231, 231, 174)", + defaults: { + name: {value:""}, + url: {value:"",required:true}, + method: {value:"get",required:true} + }, + inputs:0, + outputs:1, + icon: "white-globe.png", + label: function() { + if (this.name) { + return this.name; + } else if (this.url) { + var root = RED.settings.httpNodeRoot; + if (root.slice(-1) != "/") { + root = root+"/"; + } + if (this.url.charAt(0) == "/") { + root += this.url.slice(1); + } else { + root += this.url; + } + return "["+this.method+"] "+root; + } else { + return "http"; + } + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + var root = RED.settings.httpNodeRoot; + if (root.slice(-1) == "/") { + root = root.slice(0,-1); + } + if (root == "") { + $("#node-input-tip").hide(); + } else { + $("#node-input-path").html(root); + $("#node-input-tip").show(); + } + //document.getElementById("node-config-wsdocpath").innerHTML= + } + + }); + + RED.nodes.registerType('http response',{ + category: 'output', + color:"rgb(231, 231, 174)", + defaults: { + name: {value:""} + }, + inputs:1, + outputs:0, + align: "right", + icon: "white-globe.png", + label: function() { + return this.name||"http"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); + + RED.nodes.registerType('http request',{ + category: 'function', + color:"rgb(231, 231, 174)", + defaults: { + name: {value:""}, + method:{value:"GET"}, + url:{value:""}, + //user -> credentials + //pass -> credentials + }, + credentials: { + user: {type:"text"}, + password: {type: "password"} + }, + inputs:1, + outputs:1, + align: "right", + icon: "white-globe.png", + label: function() { + return this.name||"http request"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + if (this.credentials.user || this.credentials.has_password) { + $('#node-input-useAuth').prop('checked', true); + $(".node-input-useAuth-row").show(); + } else { + $('#node-input-useAuth').prop('checked', false); + $(".node-input-useAuth-row").hide(); + } + + $("#node-input-useAuth").change(function() { + if ($(this).is(":checked")) { + $(".node-input-useAuth-row").show(); + } else { + $(".node-input-useAuth-row").hide(); + $('#node-input-user').val(''); + $('#node-input-password').val(''); + } + }); + }, + }); +</script> diff --git a/dgbuilder/core_nodes/io/21-httpin.js b/dgbuilder/core_nodes/io/21-httpin.js new file mode 100644 index 00000000..877ccc09 --- /dev/null +++ b/dgbuilder/core_nodes/io/21-httpin.js @@ -0,0 +1,241 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var http = require("follow-redirects").http; + var https = require("follow-redirects").https; + var urllib = require("url"); + var express = require("express"); + var getBody = require('raw-body'); + var mustache = require("mustache"); + var querystring = require("querystring"); + + var cors = require('cors'); + var jsonParser = express.json(); + var urlencParser = express.urlencoded(); + + function rawBodyParser(req, res, next) { + if (req._body) { return next(); } + req.body = ""; + req._body = true; + getBody(req, { + limit: '1mb', + length: req.headers['content-length'], + encoding: 'utf8' + }, function (err, buf) { + if (err) { return next(err); } + req.body = buf; + next(); + }); + } + + + function HTTPIn(n) { + RED.nodes.createNode(this,n); + if (RED.settings.httpNodeRoot !== false) { + + this.url = n.url; + this.method = n.method; + + var node = this; + + this.errorHandler = function(err,req,res,next) { + node.warn(err); + res.send(500); + }; + + this.callback = function(req,res) { + if (node.method == "post") { + node.send({req:req,res:res,payload:req.body}); + } else if (node.method == "get") { + node.send({req:req,res:res,payload:req.query}); + } else { + node.send({req:req,res:res}); + } + } + + var corsHandler = function(req,res,next) { next(); } + + if (RED.settings.httpNodeCors) { + corsHandler = cors(RED.settings.httpNodeCors); + RED.httpNode.options(this.url,corsHandler); + } + + if (this.method == "get") { + RED.httpNode.get(this.url,corsHandler,this.callback,this.errorHandler); + } else if (this.method == "post") { + RED.httpNode.post(this.url,corsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler); + } else if (this.method == "put") { + RED.httpNode.put(this.url,corsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler); + } else if (this.method == "delete") { + RED.httpNode.delete(this.url,corsHandler,this.callback,this.errorHandler); + } + + this.on("close",function() { + var routes = RED.httpNode.routes[this.method]; + for (var i = 0; i<routes.length; i++) { + if (routes[i].path == this.url) { + routes.splice(i,1); + //break; + } + } + if (RED.settings.httpNodeCors) { + var route = RED.httpNode.route['options']; + for (var j = 0; j<route.length; j++) { + if (route[j].path == this.url) { + route.splice(j,1); + //break; + } + } + } + }); + } else { + this.warn("Cannot create http-in node when httpNodeRoot set to false"); + } + } + 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) { + msg.res.set(msg.headers); + } + var statusCode = msg.statusCode || 200; + if (typeof msg.payload == "object" && !Buffer.isBuffer(msg.payload)) { + msg.res.jsonp(statusCode,msg.payload); + } else { + if (msg.res.get('content-length') == null) { + var len; + if (msg.payload == null) { + len = 0; + } else if (typeof msg.payload == "number") { + len = Buffer.byteLength(""+msg.payload); + } else { + len = Buffer.byteLength(msg.payload); + } + msg.res.set('content-length', len); + } + msg.res.send(statusCode,msg.payload); + } + } else { + node.warn("No response object"); + } + }); + } + RED.nodes.registerType("http response",HTTPOut); + + function HTTPRequest(n) { + RED.nodes.createNode(this,n); + var nodeUrl = n.url; + var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1; + var nodeMethod = n.method || "GET"; + var node = this; + this.on("input",function(msg) { + node.status({fill:"blue",shape:"dot",text:"requesting"}); + var url; + if (msg.url) { + url = msg.url; + } else if (isTemplatedUrl) { + url = mustache.render(nodeUrl,msg); + } else { + url = nodeUrl; + } + // url must start http:// or https:// so assume http:// if not set + if (!((url.indexOf("http://")===0) || (url.indexOf("https://")===0))) { + url = "http://"+url; + } + + var method = (msg.method||nodeMethod).toUpperCase(); + //node.log(method+" : "+url); + var opts = urllib.parse(url); + opts.method = method; + opts.headers = {}; + if (msg.headers) { + for (var v in msg.headers) { + if (msg.headers.hasOwnProperty(v)) { + var name = v.toLowerCase(); + if (name !== "content-type" && name !== "content-length") { + // only normalise the known headers used later in this + // function. Otherwise leave them alone. + name = v; + } + opts.headers[name] = msg.headers[v]; + } + } + } + if (this.credentials && this.credentials.user) { + opts.auth = this.credentials.user+":"+(this.credentials.password||""); + } + var payload = null; + + if (msg.payload && (method == "POST" || method == "PUT") ) { + if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) { + payload = msg.payload; + } else if (typeof msg.payload == "number") { + payload = msg.payload+""; + } else { + if (opts.headers['content-type'] == 'application/x-www-form-urlencoded') { + payload = querystring.stringify(msg.payload); + } else { + payload = JSON.stringify(msg.payload); + if (opts.headers['content-type'] == null) { + opts.headers['content-type'] = "application/json"; + } + } + } + if (opts.headers['content-length'] == null) { + opts.headers['content-length'] = Buffer.byteLength(payload); + } + } + + var req = ((/^https/.test(url))?https:http).request(opts,function(res) { + res.setEncoding('utf8'); + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.payload = ""; + res.on('data',function(chunk) { + msg.payload += chunk; + }); + res.on('end',function() { + node.send(msg); + node.status({}); + }); + }); + req.on('error',function(err) { + msg.payload = err.toString() + " : " + url; + msg.statusCode = err.code; + node.send(msg); + node.status({fill:"red",shape:"ring",text:err.code}); + }); + if (payload) { + req.write(payload); + } + req.end(); + }); + } + + RED.nodes.registerType("http request",HTTPRequest,{ + credentials: { + user: {type:"text"}, + password: {type: "password"} + } + }); +} diff --git a/dgbuilder/core_nodes/io/22-websocket.html b/dgbuilder/core_nodes/io/22-websocket.html new file mode 100644 index 00000000..ff6ed742 --- /dev/null +++ b/dgbuilder/core_nodes/io/22-websocket.html @@ -0,0 +1,163 @@ +<!--
+ 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.
+-->
+
+<!-- WebSocket Input Node -->
+<script type="text/x-red" data-template-name="websocket in">
+ <div class="form-row">
+ <label for="node-input-server"><i class="fa fa-bookmark"></i> Path</label>
+ <input type="text" id="node-input-server">
+ </div>
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+ <input type="text" id="node-input-name" placeholder="Name">
+ </div>
+</script>
+
+<script type="text/x-red" data-help-name="websocket in">
+ <p>WebSocket input node.</p>
+ <p>By default, the data received from the WebSocket will be in <b>msg.payload</b>.
+ The listener can be configured to expect a properly formed JSON string, in which
+ case it will parse the JSON and send on the resulting object as the entire message.</p>
+</script>
+
+<script type="text/javascript">
+ RED.nodes.registerType('websocket in',{
+ category: 'input',
+ defaults: {
+ name: {value:""},
+ server: {type:"websocket-listener"}
+ },
+ color:"rgb(215, 215, 160)",
+ inputs:0,
+ outputs:1,
+ icon: "white-globe.png",
+ label: function() {
+ var wsNode = RED.nodes.node(this.server);
+ return this.name||(wsNode?"[ws] "+wsNode.label():"websocket");
+ },
+ labelStyle: function() {
+ return this.name?"node_label_italic":"";
+ }
+ });
+</script>
+
+<!-- WebSocket out Node -->
+<script type="text/x-red" data-template-name="websocket out">
+ <div class="form-row">
+ <label for="node-input-server"><i class="fa fa-bookmark"></i> Path</label>
+ <input type="text" id="node-input-server">
+ </div>
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+ <input type="text" id="node-input-name" placeholder="Name">
+ </div>
+</script>
+
+<script type="text/x-red" data-help-name="websocket out">
+ <p>WebSocket out node.</p>
+ <p>By default, <b>msg.payload</b> will be sent over the WebSocket. The listener
+ can be configured to encode the entire message object as a JSON string and send that
+ over the WebSocket.</p>
+
+ <p>If the message arriving at this node started at a WebSocket In node, the message
+ will be sent back to the client that triggered the flow. Otherwise, the message
+ will be broadcast to all connected clients.</p>
+ <p>If you want to broadcast a message that started at a WebSocket In node, you
+ should delete the <b>msg._session</b> property within the flow</p>.
+</script>
+
+<script type="text/javascript">
+ RED.nodes.registerType('websocket out',{
+ category: 'output',
+ defaults: {
+ name: {value:""},
+ server: {type:"websocket-listener", required:true}
+ },
+ color:"rgb(215, 215, 160)",
+ inputs:1,
+ outputs:0,
+ icon: "white-globe.png",
+ align: "right",
+ label: function() {
+ var wsNode = RED.nodes.node(this.server);
+ return this.name||(wsNode?"[ws] "+wsNode.label():"websocket");
+ },
+ labelStyle: function() {
+ return this.name?"node_label_italic":"";
+ }
+ });
+</script>
+
+<!-- WebSocket Server configuration node -->
+<script type="text/x-red" data-template-name="websocket-listener">
+ <div class="form-row">
+ <label for="node-config-input-path"><i class="fa fa-bookmark"></i> Path</label>
+ <input type="text" id="node-config-input-path" placeholder="/ws/example">
+ </div>
+ <div class="form-row">
+ <label for="node-config-input-wholemsg"> </label>
+ <select type="text" id="node-config-input-wholemsg" style="width: 70%;">
+ <option value="false">Send/Receive payload</option>
+ <option value="true">Send/Receive entire message</option>
+ </select>
+ </div>
+ <div class="form-tips">
+ Be default, <code>payload</code> will contain the data to be sent over, or received from a websocket.
+ The listener can be configured to send or receive the entire message object as a JSON formatted string.
+ <p id="node-config-ws-tip">This path will be relative to <code><span id="node-config-ws-path"></span></code>.</p>
+ </div>
+</script>
+
+<script type="text/x-red" data-help-name="websocket-listener">
+ <p>This configuration node creates a WebSocket Server using the specified path</p>
+</script>
+
+<script type="text/javascript">
+ RED.nodes.registerType('websocket-listener',{
+ category: 'config',
+ defaults: {
+ path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/) },
+ wholemsg: {value:"false"}
+ },
+ inputs:0,
+ outputs:0,
+ label: function() {
+ var root = RED.settings.httpNodeRoot;
+ if (root.slice(-1) != "/") {
+ root = root+"/";
+ }
+ if (this.path.charAt(0) == "/") {
+ root += this.path.slice(1);
+ } else {
+ root += this.path;
+ }
+ return root;
+ },
+ oneditprepare: function() {
+ var root = RED.settings.httpNodeRoot;
+ if (root.slice(-1) == "/") {
+ root = root.slice(0,-1);
+ }
+ if (root == "") {
+ $("#node-config-ws-tip").hide();
+ } else {
+ $("#node-config-ws-path").html(root);
+ $("#node-config-ws-tip").show();
+ }
+ //document.getElementById("node-config-wsdocpath").innerHTML=
+ }
+ });
+</script>
diff --git a/dgbuilder/core_nodes/io/22-websocket.js b/dgbuilder/core_nodes/io/22-websocket.js new file mode 100644 index 00000000..72eda502 --- /dev/null +++ b/dgbuilder/core_nodes/io/22-websocket.js @@ -0,0 +1,185 @@ +/**
+ * 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.
+ **/
+
+module.exports = function(RED) {
+ "use strict";
+ var ws = require("ws"),
+ inspect = require("sys").inspect;
+
+ // A node red node that sets up a local websocket server
+ function WebSocketListenerNode(n) {
+ // Create a RED node
+ RED.nodes.createNode(this,n);
+
+ var node = this;
+
+ // Store local copies of the node configuration (as defined in the .html)
+ node.path = n.path;
+ node.wholemsg = (n.wholemsg === "true");
+
+ node._inputNodes = []; // collection of nodes that want to receive events
+
+ var path = RED.settings.httpNodeRoot || "/";
+ path = path + (path.slice(-1) == "/" ? "":"/") + (node.path.charAt(0) == "/" ? node.path.substring(1) : node.path);
+
+ // Workaround https://github.com/einaros/ws/pull/253
+ // Listen for 'newListener' events from RED.server
+ node._serverListeners = {};
+
+ var storeListener = function(/*String*/event,/*function*/listener){
+ if(event == "error" || event == "upgrade" || event == "listening"){
+ node._serverListeners[event] = listener;
+ }
+ }
+
+ node._clients = {};
+
+ RED.server.addListener('newListener',storeListener);
+
+ // Create a WebSocket Server
+ node.server = new ws.Server({server:RED.server,path:path});
+
+ // Workaround https://github.com/einaros/ws/pull/253
+ // Stop listening for new listener events
+ RED.server.removeListener('newListener',storeListener);
+
+ node.server.on('connection', function(socket){
+ var id = (1+Math.random()*4294967295).toString(16);
+ node._clients[id] = socket;
+ socket.on('close',function() {
+ delete node._clients[id];
+ });
+ socket.on('message',function(data,flags){
+ node.handleEvent(id,socket,'message',data,flags);
+ });
+ socket.on('error', function(err) {
+ node.warn("An error occured on the ws connection: "+inspect(err));
+ });
+ });
+
+ node.on("close", function() {
+ // Workaround https://github.com/einaros/ws/pull/253
+ // Remove listeners from RED.server
+ var listener = null;
+ for(var event in node._serverListeners) {
+ if (node._serverListeners.hasOwnProperty(event)) {
+ listener = node._serverListeners[event];
+ if(typeof listener === "function"){
+ RED.server.removeListener(event,listener);
+ }
+ }
+ }
+ node._serverListeners = {};
+ node.server.close();
+ node._inputNodes = [];
+ });
+ }
+ RED.nodes.registerType("websocket-listener",WebSocketListenerNode);
+
+ WebSocketListenerNode.prototype.registerInputNode = function(/*Node*/handler){
+ this._inputNodes.push(handler);
+ }
+
+ WebSocketListenerNode.prototype.handleEvent = function(id,/*socket*/socket,/*String*/event,/*Object*/data,/*Object*/flags){
+ var msg;
+ if (this.wholemsg) {
+ try {
+ msg = JSON.parse(data);
+ }
+ catch(err) {
+ msg = { payload:data };
+ }
+ } else {
+ msg = {
+ payload:data
+ };
+ }
+ msg._session = {type:"websocket",id:id};
+
+ for (var i = 0; i < this._inputNodes.length; i++) {
+ this._inputNodes[i].send(msg);
+ }
+ }
+
+ WebSocketListenerNode.prototype.broadcast = function(data){
+ try {
+ for (var i = 0; i < this.server.clients.length; i++) {
+ this.server.clients[i].send(data);
+ }
+ }
+ catch(e) { // swallow any errors
+ this.warn("ws:"+i+" : "+e);
+ }
+ }
+
+ WebSocketListenerNode.prototype.send = function(id,data) {
+ var session = this._clients[id];
+ if (session) {
+ try {
+ session.send(data);
+ }
+ catch(e) { // swallow any errors
+ }
+ }
+ }
+
+ function WebSocketInNode(n) {
+ RED.nodes.createNode(this,n);
+ this.server = n.server;
+ var node = this;
+ this.serverConfig = RED.nodes.getNode(this.server);
+ if (this.serverConfig) {
+ this.serverConfig.registerInputNode(this);
+ } else {
+ this.error("Missing server configuration");
+ }
+ }
+ RED.nodes.registerType("websocket in",WebSocketInNode);
+
+ function WebSocketOutNode(n) {
+ RED.nodes.createNode(this,n);
+ var node = this;
+ this.server = n.server;
+ this.serverConfig = RED.nodes.getNode(this.server);
+ if (!this.serverConfig) {
+ this.error("Missing server configuration");
+ }
+ this.on("input", function(msg) {
+ var payload;
+ if (this.serverConfig.wholemsg) {
+ delete msg._session;
+ payload = JSON.stringify(msg);
+ } else {
+ if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string.
+ payload = RED.util.ensureString(msg.payload);
+ }
+ else {
+ payload = msg.payload;
+ }
+ }
+ if (msg._session && msg._session.type == "websocket") {
+ node.serverConfig.send(msg._session.id,payload);
+ } else {
+ node.serverConfig.broadcast(payload,function(error){
+ if (!!error) {
+ node.warn("An error occurred while sending:" + inspect(error));
+ }
+ });
+ }
+ });
+ }
+ RED.nodes.registerType("websocket out",WebSocketOutNode);
+}
diff --git a/dgbuilder/core_nodes/io/23-watch.html b/dgbuilder/core_nodes/io/23-watch.html new file mode 100644 index 00000000..8bf22be5 --- /dev/null +++ b/dgbuilder/core_nodes/io/23-watch.html @@ -0,0 +1,57 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="watch"> + <div class="form-row node-input-filename"> + <label for="node-input-files"><i class="fa fa-file"></i> File(s)</label> + <input type="text" id="node-input-files" placeholder="File(s) or Directory"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div id="node-input-tip" class="form-tips">On Windows you must use double slashes \\ in any directory names.</div> +</script> + +<script type="text/x-red" data-help-name="watch"> + <p>Watches a directory or file for any changes.</p> + <p>You can enter a list of comma separated directories or files if you like. You will need to put " around any that have spaces in.</p> + <p>On Windows you must use double slashes \\ in any directory names.</p> + <p>The full filename of the file that actually changed is put into <b>msg.payload</b>, while a stringified version of the watched criteria is returned in <b>msg.topic</b>.</p> + <p><b>msg.file</b> contains just the short filename of the file that changed.</p> + <p>Of course in Linux, <i>everything</i> could be a file and thus watched...</p> + <p><b>Note: </b>The directory or file must exist in order to be watched. If the file or directory gets deleted it may no longer be monitored even if it gets re-created.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('watch',{ + category: 'advanced-input', + defaults: { + name: {value:""}, + files: {value:"",required:true} + }, + color:"BurlyWood", + inputs:0, + outputs:1, + icon: "watch.png", + label: function() { + return this.name||this.files; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/io/23-watch.js b/dgbuilder/core_nodes/io/23-watch.js new file mode 100644 index 00000000..8a17f5ac --- /dev/null +++ b/dgbuilder/core_nodes/io/23-watch.js @@ -0,0 +1,51 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var Notify = require("fs.notify"); + var fs = require("fs"); + var sep = require("path").sep; + + function WatchNode(n) { + RED.nodes.createNode(this,n); + + this.files = n.files.split(","); + for (var f =0; f < this.files.length; f++) { + this.files[f] = this.files[f].trim(); + } + this.p = (this.files.length == 1) ? this.files[0] : JSON.stringify(this.files); + var node = this; + + var notifications = new Notify(node.files); + notifications.on('change', function (file, event, path) { + try { + if (fs.statSync(path).isDirectory()) { path = path + sep + file; } + } catch(e) { } + var msg = { payload: path, topic: node.p, file: file }; + node.send(msg); + }); + + notifications.on('error', function (error, path) { + node.warn(error); + }); + + this.close = function() { + notifications.close(); + } + } + RED.nodes.registerType("watch",WatchNode); +} diff --git a/dgbuilder/core_nodes/io/25-serial.html b/dgbuilder/core_nodes/io/25-serial.html new file mode 100644 index 00000000..225e4dc3 --- /dev/null +++ b/dgbuilder/core_nodes/io/25-serial.html @@ -0,0 +1,265 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="serial in"> + <div class="form-row node-input-serial"> + <label for="node-input-serial"><i class="fa fa-random"></i> Serial Port</label> + <input type="text" id="node-input-serial"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="serial in"> + <p>Reads data from a local serial port.</p> + <p>Can either <ul><li>wait for a "split" character (default \n). Also accepts hex notation (0x0a).</li> + <li>Wait for a timeout in milliseconds for the first character received</li> + <li>Wait to fill a fixed sized buffer</li></ul></p> + <p>It then outputs <b>msg.payload</b> as either a UTF8 ascii string or a binary Buffer object.</p> + <p>If no split character is specified, or a timeout or buffer size of 0, then a stream of single characters is sent - again either as ascii chars or size 1 binary buffers.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('serial in',{ + category: 'input', + defaults: { + name: {name:""}, + serial: {type:"serial-port",required:true} + }, + color:"BurlyWood", + inputs:0, + outputs:1, + icon: "serial.png", + label: function() { + var serialNode = RED.nodes.node(this.serial); + return this.name||(serialNode?serialNode.label().split(":")[0]:"serial"); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + +<script type="text/x-red" data-template-name="serial out"> + <div class="form-row node-input-serial"> + <label for="node-input-serial"><i class="fa fa-random"></i> Serial Port</label> + <input type="text" id="node-input-serial"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="serial out"> + <p>Provides a connection to an outbound serial port.</p> + <p>Only the <b>msg.payload</b> is sent.</p> + <p>Optionally the new line character used to split the input can be appended to every message sent out to the serial port.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('serial out',{ + category: 'output', + defaults: { + name: {name:""}, + serial: {type:"serial-port",required:true} + }, + color:"BurlyWood", + inputs:1, + outputs:0, + icon: "serial.png", + align: "right", + label: function() { + var serialNode = RED.nodes.node(this.serial); + return this.name||(serialNode?serialNode.label().split(":")[0]:"serial"); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + + +<script type="text/x-red" data-template-name="serial-port"> + <div class="form-row"> + <label for="node-config-input-serialport"><i class="fa fa-random"></i> Serial Port</label> + <input type="text" id="node-config-input-serialport" style="width:60%;" placeholder="/dev/ttyUSB0"/> + <a id="node-config-lookup-serial" class="btn"><i id="node-config-lookup-serial-icon" class="fa fa-search"></i></a> + </div> + <div class="form-row"> + <table><tr> + <td width = "102px"><i class="fa fa-wrench"></i> Settings</td> + <td width = "100px">Baud Rate</td> + <td width = "80px">Data Bits</td> + <td width = "80px">Parity</td> + <td width = "80px">Stop Bits</td> + </tr><tr><td> </td> + <td> + <select type="text" id="node-config-input-serialbaud" style="width: 100px;"> + <option value="115200">115200</option> + <option value="57600">57600</option> + <option value="38400">38400</option> + <option value="19200">19200</option> + <option value="9600">9600</option> + <option value="4800">4800</option> + <option value="2400">2400</option> + <option value="1800">1800</option> + <option value="1200">1200</option> + <option value="600">600</option> + <option value="300">300</option> + <option value="200">200</option> + <option value="150">150</option> + <option value="134">134</option> + <option value="110">110</option> + <option value="75">75</option> + <option value="50">50</option> + </select> + </td><td> + <select type="text" id="node-config-input-databits" style="width: 80px;"> + <option value="8">8</option> + <option value="7">7</option> + <option value="6">6</option> + <option value="5">5</option> + </select> + </td><td> + <select type="text" id="node-config-input-parity" style="width: 80px;"> + <option value="none">None</option> + <option value="even">Even</option> + <option value="mark">Mark</option> + <option value="odd">Odd</option> + <option value="space">Space</option> + </select> + </td><td> + <select type="text" id="node-config-input-stopbits" style="width: 80px;"> + <option value="2">2</option> + <option value="1">1</option> + </select> + </td> + </tr></table><br/> + + <div class="form-row"> + <label for="node-config-input-out"><i class="fa fa-cut"></i> Split input</label> + <select type="text" id="node-config-input-out" style="width:52%;"> + <option value="char">when character received is</option> + <option value="time">after a fixed timeout of</option> + <option value="count">a fixed number of characters</option> + </select> + <input type="text" id="node-config-input-newline" style="width:50px;"> + <span id="node-units"></span> + </div> + + <div class="form-row"> + <label for="node-config-input-bin"><i class="fa fa-sign-in"></i> and deliver</label> + <select type="text" id="node-config-input-bin" style="width: 77%;"> + <option value="false">ascii strings</option> + <option value="bin">binary buffers</option> + </select> + </div> + <br/> + <div class="form-row" id="node-config-addchar"> + <label for="node-config-input-addchar"><i class="fa fa-sign-out"></i> On output</label> + <select type="text" id="node-config-input-addchar" style="width: 77%;"> + <option value="false">don't add 'split' character to output messages</option> + <option value="true">add 'split' character to output messages</option> + </select> + </div> + <div class="form-tips" id="tip-split">Tip: the "Split on" character is used to split the input into separate messages. It can also be added to every message sent out to the serial port.</div> + <div class="form-tips" id="tip-bin" hidden>Tip: In timeout mode timeout starts from arrival of first character.</div> + <script> + var previous = null; + $("#node-config-input-out").on('focus', function () { previous = this.value; }).change(function() { + if (previous == null) { previous = $("#node-config-input-out").val(); } + if ($("#node-config-input-out").val() == "char") { + if (previous != "char") { $("#node-config-input-newline").val("\\n"); } + $("#node-units").text(""); + $("#node-config-addchar").show(); + $("#tip-split").show(); + $("#tip-bin").hide(); + } + else if ($("#node-config-input-out").val() == "time") { + if (previous != "time") { $("#node-config-input-newline").val("0"); } + $("#node-units").text("ms"); + $("#node-config-addchar").hide(); + $("#node-config-input-addchar").val("false"); + $("#tip-split").hide(); + $("#tip-bin").show(); + } + else { + if (previous != "count") { $("#node-config-input-newline").val("12"); } + $("#node-units").text("chars"); + $("#node-config-addchar").hide(); + $("#node-config-input-addchar").val("false"); + $("#tip-split").hide(); + $("#tip-bin").hide(); + } + }); + + </script> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('serial-port',{ + category: 'config', + defaults: { + //name: {value:""}, + serialport: {value:"",required:true}, + serialbaud: {value:57600,required:true}, + databits: {value:8,required:true}, + parity: {value:"none",required:true}, + stopbits: {value:1,required:true}, + newline: {value:"\\n"}, + bin: {value:""}, + out: {value:""}, + addchar: {value:false} + }, + label: function() { + this.serialbaud = this.serialbaud || 57600; + this.databits = this.databits || 8; + this.parity = this.parity || 'none'; + this.stopbits = this.stopbits || 1; + return this.serialport+":"+this.serialbaud+"-"+this.databits+this.parity.charAt(0).toUpperCase()+this.stopbits; + }, + oneditprepare: function() { + try { + $("#node-config-input-serialport").autocomplete( "destroy" ); + } catch(err) { + } + $("#node-config-lookup-serial").click(function() { + //$("#node-config-lookup-serial-icon").removeClass('fa fa-search'); + //$("#node-config-lookup-serial-icon").addClass('fa fa-spinner'); + $("#node-config-lookup-serial").addClass('disabled'); + $.getJSON('serialports',function(data) { + //$("#node-config-lookup-serial-icon").addClass('fa fa-search'); + //$("#node-config-lookup-serial-icon").removeClass('fa fa-spinner'); + $("#node-config-lookup-serial").removeClass('disabled'); + var ports = []; + $.each(data, function(i, port){ + ports.push(port.comName); + }); + $("#node-config-input-serialport").autocomplete({ + source:ports, + minLength:0, + close: function( event, ui ) { + $("#node-config-input-serialport").autocomplete( "destroy" ); + } + }).autocomplete("search",""); + }); + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/io/25-serial.js b/dgbuilder/core_nodes/io/25-serial.js new file mode 100644 index 00000000..96e4aca6 --- /dev/null +++ b/dgbuilder/core_nodes/io/25-serial.js @@ -0,0 +1,310 @@ +/** +* Copyright 2013,2014 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. +**/ + +module.exports = function(RED) { + "use strict"; + var settings = RED.settings; + var events = require("events"); + var util = require("util"); + var serialp = require("serialport"); + var bufMaxSize = 32768; // Max serial buffer size, for inputs... + + // TODO: 'serialPool' should be encapsulated in SerialPortNode + + function SerialPortNode(n) { + RED.nodes.createNode(this,n); + this.serialport = n.serialport; + this.newline = n.newline; + this.addchar = n.addchar || "false"; + this.serialbaud = parseInt(n.serialbaud) || 57600; + this.databits = parseInt(n.databits) || 8; + this.parity = n.parity || "none"; + this.stopbits = parseInt(n.stopbits) || 1; + this.bin = n.bin || "false"; + this.out = n.out || "char"; + } + RED.nodes.registerType("serial-port",SerialPortNode); + + function SerialOutNode(n) { + RED.nodes.createNode(this,n); + this.serial = n.serial; + this.serialConfig = RED.nodes.getNode(this.serial); + + if (this.serialConfig) { + var node = this; + node.port = serialPool.get(this.serialConfig.serialport, + this.serialConfig.serialbaud, + this.serialConfig.databits, + this.serialConfig.parity, + this.serialConfig.stopbits, + this.serialConfig.newline); + node.addCh = ""; + if (node.serialConfig.addchar == "true") { + node.addCh = this.serialConfig.newline.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0"); + } + node.on("input",function(msg) { + var payload = msg.payload; + if (!Buffer.isBuffer(payload)) { + if (typeof payload === "object") { + payload = JSON.stringify(payload); + } else { + payload = payload.toString(); + } + payload += node.addCh; + } else if (node.addCh !== "") { + payload = Buffer.concat([payload,new Buffer(node.addCh)]); + } + node.port.write(payload,function(err,res) { + if (err) { + node.error(err); + } + }); + }); + node.port.on('ready', function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + node.port.on('closed', function() { + node.status({fill:"red",shape:"ring",text:"not connected"}); + }); + } else { + this.error("missing serial config"); + } + + this.on("close", function(done) { + if (this.serialConfig) { + serialPool.close(this.serialConfig.serialport,done); + } else { + done(); + } + }); + } + RED.nodes.registerType("serial out",SerialOutNode); + + + function SerialInNode(n) { + RED.nodes.createNode(this,n); + this.serial = n.serial; + this.serialConfig = RED.nodes.getNode(this.serial); + + if (this.serialConfig) { + var node = this; + node.tout = null; + var buf; + if (node.serialConfig.out != "count") { buf = new Buffer(bufMaxSize); } + else { buf = new Buffer(Number(node.serialConfig.newline)); } + var i = 0; + node.status({fill:"grey",shape:"dot",text:"unknown"}); + node.port = serialPool.get(this.serialConfig.serialport, + this.serialConfig.serialbaud, + this.serialConfig.databits, + this.serialConfig.parity, + this.serialConfig.stopbits, + this.serialConfig.newline + ); + + var splitc; + if (node.serialConfig.newline.substr(0,2) == "0x") { + splitc = new Buffer([parseInt(node.serialConfig.newline)]); + } else { + splitc = new Buffer(node.serialConfig.newline.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0")); + } + + this.port.on('data', function(msg) { + // single char buffer + if ((node.serialConfig.newline === 0)||(node.serialConfig.newline === "")) { + if (node.serialConfig.bin !== "bin") { node.send({"payload": String.fromCharCode(msg)}); } + else { node.send({"payload": new Buffer([msg])}); } + } + else { + // do the timer thing + if (node.serialConfig.out === "time") { + if (node.tout) { + i += 1; + buf[i] = msg; + } + else { + node.tout = setTimeout(function () { + node.tout = null; + var m = new Buffer(i+1); + buf.copy(m,0,0,i+1); + if (node.serialConfig.bin !== "bin") { m = m.toString(); } + node.send({"payload": m}); + m = null; + }, node.serialConfig.newline); + i = 0; + buf[0] = msg; + } + } + // count bytes into a buffer... + else if (node.serialConfig.out === "count") { + buf[i] = msg; + i += 1; + if ( i >= parseInt(node.serialConfig.newline)) { + var m = new Buffer(i); + buf.copy(m,0,0,i); + if (node.serialConfig.bin !== "bin") { m = m.toString(); } + node.send({"payload":m}); + m = null; + i = 0; + } + } + // look to match char... + else if (node.serialConfig.out === "char") { + buf[i] = msg; + i += 1; + if ((msg === splitc[0]) || (i === bufMaxSize)) { + var m = new Buffer(i); + buf.copy(m,0,0,i); + if (node.serialConfig.bin !== "bin") { m = m.toString(); } + node.send({"payload":m}); + m = null; + i = 0; + } + } + else { console.log("Should never get here"); } + } + }); + this.port.on('ready', function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + this.port.on('closed', function() { + node.status({fill:"red",shape:"ring",text:"not connected"}); + }); + } else { + this.error("missing serial config"); + } + + this.on("close", function(done) { + if (this.serialConfig) { + serialPool.close(this.serialConfig.serialport,done); + } else { + done(); + } + }); + } + RED.nodes.registerType("serial in",SerialInNode); + + + var serialPool = function() { + var connections = {}; + return { + get:function(port,baud,databits,parity,stopbits,newline,callback) { + var id = port; + if (!connections[id]) { + connections[id] = function() { + var obj = { + _emitter: new events.EventEmitter(), + serial: null, + _closing: false, + tout: null, + on: function(a,b) { this._emitter.on(a,b); }, + close: function(cb) { this.serial.close(cb); }, + write: function(m,cb) { this.serial.write(m,cb); }, + } + //newline = newline.replace("\\n","\n").replace("\\r","\r"); + var setupSerial = function() { + //if (newline == "") { + obj.serial = new serialp.SerialPort(port,{ + baudrate: baud, + databits: databits, + parity: parity, + stopbits: stopbits, + parser: serialp.parsers.raw + },true, function(err, results) { if (err) { obj.serial.emit('error',err); } }); + //} + //else { + // obj.serial = new serialp.SerialPort(port,{ + // baudrate: baud, + // databits: databits, + // parity: parity, + // stopbits: stopbits, + // parser: serialp.parsers.readline(newline) + // },true, function(err, results) { if (err) obj.serial.emit('error',err); }); + //} + obj.serial.on('error', function(err) { + util.log("[serial] serial port "+port+" error "+err); + obj._emitter.emit('closed'); + obj.tout = setTimeout(function() { + setupSerial(); + }, settings.serialReconnectTime); + }); + obj.serial.on('close', function() { + if (!obj._closing) { + util.log("[serial] serial port "+port+" closed unexpectedly"); + obj._emitter.emit('closed'); + obj.tout = setTimeout(function() { + setupSerial(); + }, settings.serialReconnectTime); + } + }); + obj.serial.on('open',function() { + util.log("[serial] serial port "+port+" opened at "+baud+" baud "+databits+""+parity.charAt(0).toUpperCase()+stopbits); + if (obj.tout) { clearTimeout(obj.tout); } + //obj.serial.flush(); + obj._emitter.emit('ready'); + }); + obj.serial.on('data',function(d) { + //console.log(Buffer.isBuffer(d),d.length,d); + //if (typeof d !== "string") { + // //d = d.toString(); + for (var z=0; z<d.length; z++) { + obj._emitter.emit('data',d[z]); + } + //} + //else { + // obj._emitter.emit('data',d); + //} + }); + obj.serial.on("disconnect",function() { + util.log("[serial] serial port "+port+" gone away"); + }); + } + setupSerial(); + return obj; + }(); + } + return connections[id]; + }, + close: function(port,done) { + if (connections[port]) { + if (connections[port].tout != null) { + clearTimeout(connections[port].tout); + } + connections[port]._closing = true; + try { + connections[port].close(function() { + util.log("[serial] serial port closed"); + done(); + }); + } + catch(err) { } + delete connections[port]; + } else { + done(); + } + } + } + }(); + + RED.httpAdmin.get("/serialports",function(req,res) { + serialp.list(function (err, ports) { + //console.log(JSON.stringify(ports)); + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write(JSON.stringify(ports)); + res.end(); + }); + }); +} diff --git a/dgbuilder/core_nodes/io/31-tcpin.html b/dgbuilder/core_nodes/io/31-tcpin.html new file mode 100644 index 00000000..c8cec599 --- /dev/null +++ b/dgbuilder/core_nodes/io/31-tcpin.html @@ -0,0 +1,299 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="tcp in"> + <div class="form-row"> + <label for="node-input-server"><i class="fa fa-dot-circle-o"></i> Type</label> + <select id="node-input-server" style="width:120px; margin-right:5px;"> + <option value="server">Listen on</option> + <option value="client">Connect to</option> + </select> + port <input type="text" id="node-input-port" style="width: 50px"> + </div> + <div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;"> + at host <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;"> + </div> + + <div class="form-row"> + <label><i class="fa fa-sign-out"></i> Output</label> + a + <select id="node-input-datamode" style="width:110px;"> + <option value="stream">stream of</option> + <option value="single">single</option> + </select> + <select id="node-input-datatype" style="width:140px;"> + <option value="buffer">Buffer</option> + <option value="utf8">String</option> + <option value="base64">Base64 String</option> + </select> + payload<span id="node-input-datamode-plural">s</span> + </div> + + <div id="node-row-newline" class="form-row hidden" style="padding-left: 110px;"> + delimited by <input type="text" id="node-input-newline" style="width: 110px;"> + </div> + + <div class="form-row"> + <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label> + <input type="text" id="node-input-topic" placeholder="Topic"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="tcp in"> + <p>Provides a choice of tcp inputs. Can either connect to a remote tcp port, + or accept incoming connections.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('tcp in',{ + category: 'input', + color:"Silver", + defaults: { + server: {value:"server",required:true}, + host: {value:"",validate:function(v) { return (this.server == "server")||v.length > 0;} }, + port: {value:"",required:true,validate:RED.validators.number()}, + datamode:{value:"stream"}, + datatype:{value:"buffer"}, + newline:{value:""}, + topic: {value:""}, + name: {value:""}, + base64: {/*deprecated*/ value:false,required:true} + }, + inputs:0, + outputs:1, + icon: "bridge-dash.png", + label: function() { + return this.name || "tcp:"+(this.host?this.host+":":"")+this.port; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + var updateOptions = function() { + var sockettype = $("#node-input-server option:selected").val(); + if (sockettype == "client") { + $("#node-input-host-row").show(); + } else { + $("#node-input-host-row").hide(); + } + var datamode = $("#node-input-datamode option:selected").val(); + var datatype = $("#node-input-datatype option:selected").val(); + if (datamode == "stream") { + $("#node-input-datamode-plural").show(); + if (datatype == "utf8") { + $("#node-row-newline").show(); + } else { + $("#node-row-newline").hide(); + } + } else { + $("#node-input-datamode-plural").hide(); + $("#node-row-newline").hide(); + } + }; + updateOptions(); + $("#node-input-server").change(updateOptions); + $("#node-input-datatype").change(updateOptions); + $("#node-input-datamode").change(updateOptions); + } + }); +</script> + + +<script type="text/x-red" data-template-name="tcp out"> + <div class="form-row"> + <label for="node-input-beserver"><i class="fa fa-dot-circle-o"></i> Type</label> + <select id="node-input-beserver" style="width:150px; margin-right:5px;"> + <option value="server">Listen on</option> + <option value="client">Connect to</option> + <option value="reply">Reply to TCP</option> + </select> + <span id="node-input-port-row">port <input type="text" id="node-input-port" style="width: 50px"></span> + </div> + + <div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;"> + at host <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;"> + </div> + + <div class="form-row hidden" id="node-input-end-row"> + <label> </label> + <input type="checkbox" id="node-input-end" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-end" style="width: 70%;">Close connection after each message is sent ?</label> + </div> + + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-base64" placeholder="base64" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-base64" style="width: 70%;">Decode Base64 message ?</label> + </div> + + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + + <div class="form-tips hidden" id="fin-tip"> + <b>Note:</b> Closing the connection after each message is generally not a good thing - but is useful to indicate an end-of-file for example. + </div> + <div class="form-tips hidden" id="fin-tip2"> + <b>Note:</b> Closing the connection after each message is generally not a good thing - but is useful to indicate an end-of-file for example. The receiving client will need to reconnect. + </div> +</script> + +<script type="text/x-red" data-help-name="tcp out"> + <p>Provides a choice of tcp outputs. Can either connect to a remote tcp port, + accept incoming connections, or reply to messages received from a TCP In node.</p> + <p>Only <b>msg.payload</b> is sent.</p> + <p>If <b>msg.payload</b> is a string containing a Base64 encoding of binary + data, the Base64 decoding option will cause it to be converted back to binary + before being sent.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('tcp out',{ + category: 'output', + color:"Silver", + defaults: { + host: {value:"",validate:function(v) { return (this.beserver != "client")||v.length > 0;} }, + port: {value:"",validate:function(v) { return (this.beserver == "reply")||RED.validators.number()(v) } }, + beserver: {value:"client",required:true}, + base64: {value:false,required:true}, + end: {value:false,required:true}, + name: {value:""} + }, + inputs:1, + outputs:0, + icon: "bridge-dash.png", + align: "right", + label: function() { + return this.name || "tcp:"+(this.host?this.host+":":"")+this.port; + }, + labelStyle: function() { + return (this.name)?"node_label_italic":""; + }, + oneditprepare: function() { + var updateOptions = function() { + var sockettype = $("#node-input-beserver option:selected").val(); + if (sockettype == "reply") { + $("#node-input-port-row").hide(); + $("#node-input-host-row").hide(); + $("#node-input-end-row").hide(); + } else { + $("#node-input-port-row").show(); + $("#node-input-end-row").show(); + } + + if (sockettype == "client") { + $("#node-input-host-row").show(); + $("#fin-tip").show(); + } else { + $("#node-input-host-row").hide(); + $("#fin-tip").hide(); + } + + if (sockettype == "server") { + $("#fin-tip2").show(); + } + else { + $("#fin-tip2").hide(); + } + + }; + updateOptions(); + $("#node-input-beserver").change(updateOptions); + } + }); +</script> + + +<script type="text/x-red" data-template-name="tcp request"> + <div class="form-row"> + <label for="node-input-server"><i class="fa fa-globe"></i> Server</label> + <input type="text" id="node-input-server" placeholder="ip.address" style="width:50%"> + port <input type="text" id="node-input-port" style="width:50px"> + </div> + <div class="form-row"> + <label for="node-input-out"><i class="fa fa-sign-out"></i> Return</label> + <select type="text" id="node-input-out" style="width:52%;"> + <option value="time">after a fixed timeout of</option> + <option value="char">when character received is</option> + <option value="count">a fixed number of characters</option> + <option value="sit">never. Keep connection open</option> + </select> + <input type="text" id="node-input-splitc" style="width:50px;"> + <span id="node-units"></span> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips"><b>Tip:</b> outputs a binary <b>Buffer</b>, so you may want to .toString() it.</div> + <script> + var previous = null; + $("#node-input-out").on('focus', function () { previous = this.value; }).change(function() { + if (previous == null) { previous = $("#node-input-out").val(); } + if ($("#node-input-out").val() == "char") { + if (previous != "char") $("#node-input-splitc").val("\\n"); + $("#node-units").text(""); + } + else if ($("#node-input-out").val() == "time") { + if (previous != "time") $("#node-input-splitc").val("0"); + $("#node-units").text("ms"); + } + else if ($("#node-input-out").val() == "count") { + if (previous != "count") $("#node-input-splitc").val("12"); + $("#node-units").text("chars"); + } + else { + if (previous != "sit") $("#node-input-splitc").val("0"); + $("#node-units").text(""); + } + }); +</script> + +<script type="text/x-red" data-help-name="tcp request"> + <p>A simple TCP request node - sends the <b>msg.payload</b> to a server tcp port and expects a response.</p> + <p>Connects, sends the "request", reads the "response". It can either count a number of + returned characters into a fixed buffer, match a specified character before returning, + wait a fixed timeout from first reply and then return, or just sit and wait for data.</p> + <p>The response will be output in <b>msg.payload</b> as a buffer, so you may want to .toString() it.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('tcp request',{ + category: 'function', + color:"Silver", + defaults: { + server: {value:"",required:true}, + port: {value:"",required:true,validate:RED.validators.number()}, + out: {value:"time",required:true}, + splitc: {value:"0",required:true}, + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "bridge-dash.png", + label: function() { + return this.name || "tcp:"+(this.server?this.server+":":"")+this.port; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/io/31-tcpin.js b/dgbuilder/core_nodes/io/31-tcpin.js new file mode 100644 index 00000000..2e4e5e7b --- /dev/null +++ b/dgbuilder/core_nodes/io/31-tcpin.js @@ -0,0 +1,472 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var reconnectTime = RED.settings.socketReconnectTime||10000; + var socketTimeout = RED.settings.socketTimeout||null; + var net = require('net'); + + var connectionPool = {}; + + function TcpIn(n) { + RED.nodes.createNode(this,n); + this.host = n.host; + this.port = n.port * 1; + this.topic = n.topic; + this.stream = (!n.datamode||n.datamode=='stream'); /* stream,single*/ + this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */ + this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r"); + this.base64 = n.base64; + this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server"); + this.closing = false; + var node = this; + var count = 0; + + if (!node.server) { + var buffer = null; + var client; + var reconnectTimeout; + var end = false; + var setupTcpClient = function() { + node.log("connecting to "+node.host+":"+node.port); + node.status({fill:"grey",shape:"dot",text:"connecting"}); + var id = (1+Math.random()*4294967295).toString(16); + client = net.connect(node.port, node.host, function() { + buffer = (node.datatype == 'buffer')? new Buffer(0):""; + node.log("connected to "+node.host+":"+node.port); + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + connectionPool[id] = client; + + client.on('data', function (data) { + if (node.datatype != 'buffer') { + data = data.toString(node.datatype); + } + if (node.stream) { + if ((node.datatype) === "utf8" && node.newline != "") { + buffer = buffer+data; + var parts = buffer.split(node.newline); + for (var i = 0;i<parts.length-1;i+=1) { + var msg = {topic:node.topic, payload:parts[i]}; + msg._session = {type:"tcp",id:id}; + node.send(msg); + } + buffer = parts[parts.length-1]; + } else { + var msg = {topic:node.topic, payload:data}; + msg._session = {type:"tcp",id:id}; + node.send(msg); + } + } else { + if ((typeof data) === "string") { + buffer = buffer+data; + } else { + buffer = Buffer.concat([buffer,data],buffer.length+data.length); + } + } + }); + client.on('end', function() { + if (!node.stream || (node.datatype == "utf8" && node.newline != "" && buffer.length > 0)) { + var msg = {topic:node.topic,payload:buffer}; + msg._session = {type:"tcp",id:id}; + if (buffer.length !== 0) { + end = true; // only ask for fast re-connect if we actually got something + node.send(msg); + } + buffer = null; + } + }); + client.on('close', function() { + delete connectionPool[id]; + node.status({fill:"red",shape:"ring",text:"disconnected"}); + if (!node.closing) { + if (end) { // if we were asked to close then try to reconnect once very quick. + end = false; + reconnectTimeout = setTimeout(setupTcpClient, 20); + } + else { + node.log("connection lost to "+node.host+":"+node.port); + reconnectTimeout = setTimeout(setupTcpClient, reconnectTime); + } + } + }); + client.on('error', function(err) { + node.log(err); + }); + } + setupTcpClient(); + + this.on('close', function() { + this.closing = true; + client.end(); + clearTimeout(reconnectTimeout); + }); + } else { + var server = net.createServer(function (socket) { + if (socketTimeout !== null) { socket.setTimeout(socketTimeout); } + var id = (1+Math.random()*4294967295).toString(16); + connectionPool[id] = socket; + node.status({text:++count+" connections"}); + + var buffer = (node.datatype == 'buffer')? new Buffer(0):""; + socket.on('data', function (data) { + if (node.datatype != 'buffer') { + data = data.toString(node.datatype); + } + if (node.stream) { + if ((typeof data) === "string" && node.newline != "") { + buffer = buffer+data; + var parts = buffer.split(node.newline); + for (var i = 0;i<parts.length-1;i+=1) { + var msg = {topic:node.topic, payload:parts[i],ip:socket.remoteAddress,port:socket.remotePort}; + msg._session = {type:"tcp",id:id}; + node.send(msg); + } + buffer = parts[parts.length-1]; + } else { + var msg = {topic:node.topic, payload:data}; + msg._session = {type:"tcp",id:id}; + node.send(msg); + } + } else { + if ((typeof data) === "string") { + buffer = buffer+data; + } else { + buffer = Buffer.concat([buffer,data],buffer.length+data.length); + } + } + }); + socket.on('end', function() { + if (!node.stream || (node.datatype === "utf8" && node.newline !== "")) { + if (buffer.length > 0) { + var msg = {topic:node.topic,payload:buffer}; + msg._session = {type:"tcp",id:id}; + node.send(msg); + } + buffer = null; + } + }); + socket.on('timeout', function() { + node.log('timeout closed socket port '+node.port); + socket.end(); + }); + socket.on('close', function() { + delete connectionPool[id]; + node.status({text:--count+" connections"}); + }); + socket.on('error',function(err) { + node.log(err); + }); + }); + server.on('error', function(err) { + if (err) { + node.error('unable to listen on port '+node.port+' : '+err); + } + }); + server.listen(node.port, function(err) { + if (err) { + node.error('unable to listen on port '+node.port+' : '+err); + } else { + node.log('listening on port '+node.port); + + node.on('close', function() { + node.closing = true; + server.close(); + node.log('stopped listening on port '+node.port); + }); + } + }); + } + + } + RED.nodes.registerType("tcp in",TcpIn); + + function TcpOut(n) { + RED.nodes.createNode(this,n); + this.host = n.host; + this.port = n.port * 1; + this.base64 = n.base64; + this.doend = n.end || false; + this.beserver = n.beserver; + this.name = n.name; + this.closing = false; + var node = this; + + if (!node.beserver||node.beserver=="client") { + var reconnectTimeout; + var client = null; + var connected = false; + var end = false; + + var setupTcpClient = function() { + node.log("connecting to "+node.host+":"+node.port); + node.status({fill:"grey",shape:"dot",text:"connecting"}); + client = net.connect(node.port, node.host, function() { + connected = true; + node.log("connected to "+node.host+":"+node.port); + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + client.on('error', function (err) { + node.log('error : '+err); + }); + client.on('end', function (err) { + }); + client.on('close', function() { + node.status({fill:"red",shape:"ring",text:"disconnected"}); + connected = false; + client.destroy(); + if (!node.closing) { + if (end) { + end = false; + reconnectTimeout = setTimeout(setupTcpClient,20); + } + else { + node.log("connection lost to "+node.host+":"+node.port); + reconnectTimeout = setTimeout(setupTcpClient,reconnectTime); + } + } + }); + } + setupTcpClient(); + + node.on("input", function(msg) { + if (connected && msg.payload != null) { + if (Buffer.isBuffer(msg.payload)) { + client.write(msg.payload); + } else if (typeof msg.payload === "string" && node.base64) { + client.write(new Buffer(msg.payload,'base64')); + } else { + client.write(new Buffer(""+msg.payload)); + } + if (node.doend === true) { + end = true; + client.end(); + } + } + }); + + node.on("close", function() { + this.closing = true; + client.end(); + clearTimeout(reconnectTimeout); + }); + + } else if (node.beserver == "reply") { + node.on("input",function(msg) { + if (msg._session && msg._session.type == "tcp") { + var client = connectionPool[msg._session.id]; + if (client) { + if (Buffer.isBuffer(msg.payload)) { + client.write(msg.payload); + } else if (typeof msg.payload === "string" && node.base64) { + client.write(new Buffer(msg.payload,'base64')); + } else { + client.write(new Buffer(""+msg.payload)); + } + } + } + }); + } else { + var connectedSockets = []; + node.status({text:"0 connections"}); + var server = net.createServer(function (socket) { + if (socketTimeout !== null) { socket.setTimeout(socketTimeout); } + var remoteDetails = socket.remoteAddress+":"+socket.remotePort; + node.log("connection from "+remoteDetails); + connectedSockets.push(socket); + node.status({text:connectedSockets.length+" connections"}); + socket.on('timeout', function() { + node.log('timeout closed socket port '+node.port); + socket.end(); + }); + socket.on('close',function() { + node.log("connection closed from "+remoteDetails); + connectedSockets.splice(connectedSockets.indexOf(socket),1); + node.status({text:connectedSockets.length+" connections"}); + }); + socket.on('error',function() { + node.log("socket error from "+remoteDetails); + connectedSockets.splice(connectedSockets.indexOf(socket),1); + node.status({text:connectedSockets.length+" connections"}); + }); + }); + + node.on("input", function(msg) { + if (msg.payload != null) { + var buffer; + if (Buffer.isBuffer(msg.payload)) { + buffer = msg.payload; + } else if (typeof msg.payload === "string" && node.base64) { + buffer = new Buffer(msg.payload,'base64'); + } else { + buffer = new Buffer(""+msg.payload); + } + for (var i = 0; i<connectedSockets.length;i+=1) { + if (node.doend === true) { connectedSockets[i].end(buffer); } + else { connectedSockets[i].write(buffer); } + } + } + }); + + server.on('error', function(err) { + if (err) { + node.error('unable to listen on port '+node.port+' : '+err); + } + }); + + server.listen(node.port, function(err) { + if (err) { + node.error('unable to listen on port '+node.port+' : '+err); + } else { + node.log('listening on port '+node.port); + node.on('close', function() { + server.close(); + node.log('stopped listening on port '+node.port); + }); + } + }); + } + } + RED.nodes.registerType("tcp out",TcpOut); + + function TcpGet(n) { + RED.nodes.createNode(this,n); + this.server = n.server; + this.port = Number(n.port); + this.out = n.out; + this.splitc = n.splitc; + + if (this.out != "char") { this.splitc = Number(this.splitc); } + else { this.splitc.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0"); } + + var buf; + if (this.out == "count") { buf = new Buffer(this.splitc); } + else { buf = new Buffer(32768); } // set it to 32k... hopefully big enough for most.... but only hopefully + + this.connected = false; + var node = this; + var client; + + this.on("input", function(msg) { + var i = 0; + if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) { + msg.payload = msg.payload.toString(); + } + if (!node.connected) { + client = net.Socket(); + client.setTimeout(socketTimeout); + node.status({}); + client.connect(node.port, node.server, function() { + //node.log('client connected'); + node.status({fill:"green",shape:"dot",text:"connected"}); + node.connected = true; + client.write(msg.payload); + }); + + client.on('data', function(data) { + //node.log("data:"+ data.length+":"+ data); + if (node.splitc === 0) { + node.send({"payload": data}); + } + else if (node.out === "sit") { // if we are staying connected just send the buffer + node.send({"payload": data}); + } + else { + for (var j = 0; j < data.length; j++ ) { + if (node.out === "time") { + // do the timer thing + if (node.tout) { + i += 1; + buf[i] = data[j]; + } + else { + node.tout = setTimeout(function () { + node.tout = null; + var m = new Buffer(i+1); + buf.copy(m,0,0,i+1); + node.send({"payload": m}); + client.end(); + m = null; + }, node.splitc); + i = 0; + buf[0] = data[j]; + } + } + // count bytes into a buffer... + else if (node.out == "count") { + buf[i] = data[j]; + i += 1; + if ( i >= node.serialConfig.count) { + node.send({"payload": buf}); + client.end(); + i = 0; + } + } + // look for a char + else { + buf[i] = data[j]; + i += 1; + if (data[j] == node.splitc) { + var m = new Buffer(i); + buf.copy(m,0,0,i); + node.send({"payload": m}); + client.end(); + m = null; + i = 0; + } + } + } + } + }); + + client.on('end', function() { + //node.log('client disconnected'); + node.connected = false; + node.status({}); + client = null; + }); + + client.on('error', function() { + node.log('connect failed'); + node.status({fill:"red",shape:"ring",text:"error"}); + if (client) { client.end(); } + }); + + client.on('timeout',function() { + node.log('connect timeout'); + if (client) { + client.end(); + setTimeout(function() { + client.connect(node.port, node.server, function() { + //node.log('client connected'); + node.connected = true; + client.write(msg.payload); + }); + },reconnectTime); + } + }); + } + else { client.write(msg.payload); } + }); + + this.on("close", function() { + if (client) { buf = null; client.end(); } + }); + + } + RED.nodes.registerType("tcp request",TcpGet); +} diff --git a/dgbuilder/core_nodes/io/32-udp.html b/dgbuilder/core_nodes/io/32-udp.html new file mode 100644 index 00000000..1c2eed57 --- /dev/null +++ b/dgbuilder/core_nodes/io/32-udp.html @@ -0,0 +1,212 @@ +<!-- + 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. +--> + +<!-- The Input Node --> +<script type="text/x-red" data-template-name="udp in"> + <div class="form-row"> + <label for="node-input-port"><i class="fa fa-sign-in"></i> Listen</label> + on port <input type="text" id="node-input-port" placeholder="Port" style="width: 45px"> + for <select id="node-input-multicast" style='width:40%'> + <option value="false">udp messages</option> + <option value="true">multicast messages</option> + </select> + </div> + <div class="form-row node-input-group"> + <label for="node-input-group"><i class="fa fa-list"></i> Group</label> + <input type="text" id="node-input-group" placeholder="225.0.18.83"> + </div> + <div class="form-row node-input-iface"> + <label for="node-input-iface"><i class="fa fa-random"></i> Interface</label> + <input type="text" id="node-input-iface" placeholder="(optional) ip address of eth0"> + </div> + <div class="form-row"> + <label for="node-input-datatype"><i class="fa fa-sign-out"></i> Output</label> + <select id="node-input-datatype" style="width: 70%;"> + <option value="buffer">a Buffer</option> + <option value="utf8">a String</option> + <option value="base64">a Base64 encoded string</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: Make sure your firewall will allow the data in.</div> + <script> + $("#node-input-multicast").change(function() { + var id = $("#node-input-multicast option:selected").val(); + if (id == "false") { + $(".node-input-group").hide(); + $(".node-input-iface").hide(); + } + else { + $(".node-input-group").show(); + $(".node-input-iface").show(); + } + }); + </script> +</script> + +<script type="text/x-red" data-help-name="udp in"> + <p>A udp input node, that produces a <b>msg.payload</b> containing a <i>BUFFER</i>, string, or base64 encoded string. Supports multicast.</p> + <p>It also provides <b>msg.ip</b> and <b>msg.port</b> to the ip address and port from which the message was received.</p> + <p>On some systems you may need to be root to use ports below 1024 and/or broadcast.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('udp in',{ + category: 'input', + color:"Silver", + defaults: { + name: {value:""}, + iface: {value:""}, + port: {value:"",required:true,validate:RED.validators.number()}, + datatype: {value:"buffer",required:true}, + multicast: {value:"false"}, + group: {value:"",validate:function(v) { return (this.multicast !== "true")||v.length > 0;} } + }, + inputs:0, + outputs:1, + icon: "bridge-dash.png", + label: function() { + if (this.multicast=="false") { + return this.name||"udp "+this.port; + } + else return this.name||"udp "+(this.group+":"+this.port); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> + + +<!-- The Output Node --> +<script type="text/x-red" data-template-name="udp out"> + <div class="form-row"> + <label for="node-input-port"><i class="fa fa-envelope"></i> Send a</label> + <select id="node-input-multicast" style='width:40%'> + <option value="false">udp message</option> + <option value="broad">broadcast message</option> + <option value="multi">multicast message</option> + </select> + to port <input type="text" id="node-input-port" placeholder="port" style="width: 70px"> + </div> + <div class="form-row node-input-addr"> + <label for="node-input-addr" id="node-input-addr-label"><i class="fa fa-list"></i> Address</label> + <input type="text" id="node-input-addr" placeholder="destination ip" style="width: 70%;"> + </div> + <div class="form-row node-input-iface"> + <label for="node-input-iface"><i class="fa fa-random"></i> Interface</label> + <input type="text" id="node-input-iface" placeholder="(optional) ip address of eth0"> + </div> + <div class="form-row"> + <label for="node-input-outport-type"> </label> + <select id="node-input-outport-type"> + <option id="node-input-outport-type-random" value="random">use random local port</option> + <option value="fixed">bind to local port</option> + </select> + <input type="text" id="node-input-outport" style="width: 70px;" placeholder="port"> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-base64" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-base64" style="width: 70%;">Decode Base64 encoded payload ?</label> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: leave address and port blank if you want to set using <b>msg.ip</b> and <b>msg.port</b>.</div> + <script> + $("#node-input-multicast").change(function() { + var id = $("#node-input-multicast option:selected").val(); + if (id !== "multi") { + $(".node-input-iface").hide(); + $("#node-input-addr-label").html('<i class="fa fa-list"></i> Address'); + $("#node-input-addr")[0].placeholder = 'destination ip'; + } + else { + $(".node-input-iface").show(); + $("#node-input-addr-label").html('<i class="fa fa-list"></i> Group'); + $("#node-input-addr")[0].placeholder = '225.0.18.83'; + } + if (id === "broad") { + $("#node-input-addr")[0].placeholder = '255.255.255.255'; + } + }); + </script> +</script> + +<script type="text/x-red" data-help-name="udp out"> + <p>This node sends <b>msg.payload</b> to the designated udp host and port. Supports multicast.</p> + <p>You may also use <b>msg.ip</b> and <b>msg.port</b> to set the destination values.<br/><b>Note</b>: the statically configured values have precedence.</p> + <p>If you select broadcast either set the address to the local broadcast ip address, or maybe try 255.255.255.255, which is the global broadcast address.</p> + <p>On some systems you may need to be root to use ports below 1024 and/or broadcast.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('udp out',{ + category: 'output', + color:"Silver", + defaults: { + name: {value:""}, + addr: {value:""}, + iface: {value:""}, + port: {value:""}, + outport: {value:""}, + base64: {value:false,required:true}, + multicast: {value:"false"} + }, + inputs:1, + outputs:0, + icon: "bridge-dash.png", + align: "right", + label: function() { + return this.name||"udp "+(this.addr+":"+this.port); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + var type = this.outport==""?"random":"fixed"; + $("#node-input-outport-type option").filter(function() { + return $(this).val() == type; + }).attr('selected',true); + + $("#node-input-outport-type").change(function() { + var type = $(this).children("option:selected").val(); + if (type == "random") { + $("#node-input-outport").val("").hide(); + } else { + $("#node-input-outport").show(); + } + }); + + $("#node-input-outport-type").change(); + + $("#node-input-multicast").change(function() { + var type = $(this).children("option:selected").val(); + if (type == "false") { + $("#node-input-outport-type-random").html("bind to random local port"); + } else { + $("#node-input-outport-type-random").html("bind to target port"); + } + }); + $("#node-input-multicast").change(); + } + }); +</script> diff --git a/dgbuilder/core_nodes/io/32-udp.js b/dgbuilder/core_nodes/io/32-udp.js new file mode 100644 index 00000000..a7968e3a --- /dev/null +++ b/dgbuilder/core_nodes/io/32-udp.js @@ -0,0 +1,171 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var dgram = require('dgram'); + + // The Input Node + function UDPin(n) { + RED.nodes.createNode(this,n); + this.group = n.group; + this.port = n.port; + 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) { + if ((err.code == "EACCES") && (node.port < 1024)) { + node.error("UDP access error, you may need root access for ports below 1024"); + } else { + node.error("UDP error : "+err.code); + } + 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, ip:remote.address, port: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); + try { + server.setMulticastTTL(128); + server.addMembership(node.group,node.iface); + node.log("udp multicast group "+node.group); + } catch (e) { + if (e.errno == "EINVAL") { + node.error("Bad Multicast Address"); + } else if (e.errno == "ENODEV") { + node.error("Must be ip address of the required interface"); + } else { + node.error("Error :"+e.errno); + } + } + } + }); + + node.on("close", function() { + try { + server.close(); + node.log('udp listener stopped'); + } catch (err) { + node.error(err); + } + }); + + server.bind(node.port,node.iface); + } + 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.outport = n.outport||""; + 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 + + if (node.multicast != "false") { + if (node.outport == "") { node.outport = node.port; } + sock.bind(node.outport, function() { // have to bind before you can enable broadcast... + sock.setBroadcast(true); // turn on broadcast + if (node.multicast == "multi") { + try { + sock.setMulticastTTL(128); + sock.addMembership(node.addr,node.iface); // Add to the multicast group + node.log('udp multicast ready : '+node.outport+' -> '+node.addr+":"+node.port); + } catch (e) { + if (e.errno == "EINVAL") { + node.error("Bad Multicast Address"); + } else if (e.errno == "ENODEV") { + node.error("Must be ip address of the required interface"); + } else { + node.error("Error :"+e.errno); + } + } + } else { + node.log('udp broadcast ready : '+node.outport+' -> '+node.addr+":"+node.port); + } + }); + } else if (node.outport != "") { + sock.bind(node.outport); + node.log('udp ready : '+node.outport+' -> '+node.addr+":"+node.port); + } else { + node.log('udp ready : '+node.addr+":"+node.port); + } + + node.on("input", function(msg) { + if (msg.payload != null) { + var add = node.addr || msg.ip || ""; + var por = node.port || msg.port || 0; + if (add == "") { + node.warn("udp: ip address not set"); + } else if (por == 0) { + node.warn("udp: port not set"); + } else if (isNaN(por) || (por < 1) || (por > 65535)) { + node.warn("udp: port number not valid"); + } else { + var message; + if (node.base64) { + message = new Buffer(msg.payload, 'base64'); + } else if (msg.payload instanceof Buffer) { + message = msg.payload; + } else { + message = new Buffer(""+msg.payload); + } + sock.send(message, 0, message.length, por, add, function(err, bytes) { + if (err) { + node.error("udp : "+err); + } + message = null; + }); + } + } + }); + + node.on("close", function() { + try { + sock.close(); + node.log('udp output stopped'); + } catch (err) { + node.error(err); + } + }); + } + RED.nodes.registerType("udp out",UDPout); +} diff --git a/dgbuilder/core_nodes/io/lib/mqtt.js b/dgbuilder/core_nodes/io/lib/mqtt.js new file mode 100644 index 00000000..141a8889 --- /dev/null +++ b/dgbuilder/core_nodes/io/lib/mqtt.js @@ -0,0 +1,254 @@ +/** + * 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 util = require("util"); +var mqtt = require("mqtt"); +var events = require("events"); +//var inspect = require("sys").inspect; + +//var Client = module.exports.Client = function( + +var port = 1883; +var host = "localhost"; + +function MQTTClient(port,host) { + this.port = port||1883; + this.host = host||"localhost"; + this.messageId = 1; + this.pendingSubscriptions = {}; + this.inboundMessages = {}; + this.lastOutbound = (new Date()).getTime(); + this.lastInbound = (new Date()).getTime(); + this.connected = false; + + this._nextMessageId = function() { + this.messageId += 1; + if (this.messageId > 0xFFFF) { + this.messageId = 1; + } + return this.messageId; + } + events.EventEmitter.call(this); +} +util.inherits(MQTTClient, events.EventEmitter); + +MQTTClient.prototype.connect = function(options) { + 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); + } + }); + 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) { + var self = this; + if (self.connected) { + var options = { + subscriptions:[{topic:topic,qos:qos}], + messageId: self._nextMessageId() + }; + this.pendingSubscriptions[options.messageId] = topic; + this.lastOutbound = (new Date()).getTime() + self.client.subscribe(options); + } +} +MQTTClient.prototype.unsubscribe = function(topic) { + var self = this; + if (self.connected) { + var options = { + topic:topic, + messageId: self._nextMessageId() + }; + this.pendingSubscriptions[options.messageId] = topic; + this.lastOutbound = (new Date()).getTime() + self.client.unsubscribe(options); + } +} + +MQTTClient.prototype.publish = function(topic,payload,qos,retain) { + var self = this; + if (self.connected) { + + if (!Buffer.isBuffer(payload)) { + if (typeof payload === "object") { + payload = JSON.stringify(payload); + } else if (typeof payload !== "string") { + payload = ""+payload; + } + } + var options = { + topic: topic, + payload: payload, + qos: qos||0, + retain:retain||false + }; + if (options.qos != 0) { + options.messageId = self._nextMessageId(); + } + this.lastOutbound = (new Date()).getTime() + self.client.publish(options); + } +} + +MQTTClient.prototype.disconnect = function() { + var self = this; + if (this.connected) { + this.connected = false; + try { + this.client.disconnect(); + } catch(err) { + } + } +} +MQTTClient.prototype.isConnected = function() { + return this.connected; +} +module.exports.createClient = function(port,host) { + var mqtt_client = new MQTTClient(port,host); + return mqtt_client; +} + diff --git a/dgbuilder/core_nodes/io/lib/mqttConnectionPool.js b/dgbuilder/core_nodes/io/lib/mqttConnectionPool.js new file mode 100644 index 00000000..d15f0fc7 --- /dev/null +++ b/dgbuilder/core_nodes/io/lib/mqttConnectionPool.js @@ -0,0 +1,128 @@ +/** + * 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 util = require("util"); +var mqtt = require("./mqtt"); +var settings = require(process.env.NODE_RED_HOME+"/red/red").settings; + +var connections = {}; + +function matchTopic(ts,t) { + if (ts == "#") { + return true; + } + var re = new RegExp("^"+ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$"); + return re.test(t); +} + +module.exports = { + get: function(broker,port,clientid,username,password,will) { + var id = "["+(username||"")+":"+(password||"")+"]["+(clientid||"")+"]@"+broker+":"+port; + if (!connections[id]) { + 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); + options.username = username; + options.password = password; + options.will = will; + var queue = []; + var subscriptions = []; + var connecting = false; + var obj = { + _instances: 0, + publish: function(msg) { + if (client.isConnected()) { + client.publish(msg.topic,msg.payload,msg.qos,msg.retain); + } else { + if (!connecting) { + connecting = true; + client.connect(options); + } + queue.push(msg); + } + }, + subscribe: function(topic,qos,callback) { + subscriptions.push({topic:topic,qos:qos,callback:callback}); + client.on('message',function(mtopic,mpayload,mqos,mretain) { + if (matchTopic(topic,mtopic)) { + callback(mtopic,mpayload,mqos,mretain); + } + }); + if (client.isConnected()) { + client.subscribe(topic,qos); + } + }, + on: function(a,b){ + client.on(a,b); + }, + once: function(a,b){ + client.once(a,b); + }, + connect: function() { + if (client && !client.isConnected() && !connecting) { + connecting = true; + client.connect(options); + } + }, + disconnect: function() { + this._instances -= 1; + if (this._instances == 0) { + client.disconnect(); + client = null; + delete connections[id]; + } + } + }; + 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; + var qos = subscriptions[s].qos; + var callback = subscriptions[s].callback; + client.subscribe(topic,qos); + } + //console.log("connected - publishing",queue.length,"messages"); + while(queue.length) { + var msg = queue.shift(); + //console.log(msg); + client.publish(msg.topic,msg.payload,msg.qos,msg.retain); + } + } + }); + client.on('connectionlost', function(err) { + util.log('[mqtt] ['+uid+'] connection lost to broker tcp://'+broker+':'+port); + connecting = false; + setTimeout(function() { + obj.connect(); + }, settings.mqttReconnectTime||5000); + }); + client.on('disconnect', function() { + connecting = false; + util.log('[mqtt] ['+uid+'] disconnected from broker tcp://'+broker+':'+port); + }); + + return obj + }(); + } + connections[id]._instances += 1; + return connections[id]; + } +}; diff --git a/dgbuilder/core_nodes/logic/10-switch.html b/dgbuilder/core_nodes/logic/10-switch.html new file mode 100644 index 00000000..4e02f446 --- /dev/null +++ b/dgbuilder/core_nodes/logic/10-switch.html @@ -0,0 +1,198 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="switch"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-row" style="padding-top:10px;"> + If msg.<input type="text" id="node-input-property" style="width: 200px;"/> + </div> + <div class="form-row"> + <div id="node-input-rule-container-div" style="border-radius: 5px; height: 310px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;"> + <ol id="node-input-rule-container" style=" list-style-type:none; margin: 0;"> + </ol> + </div> + <a href="#" class="btn btn-mini" id="node-input-add-rule" style="margin-top: 4px;"><i class="fa fa-plus"></i> Add</a> + </div> + <div> + <select id="node-input-checkall" style="width:100%; margin-right:5px;"> + <option value="true">checking all rules</option> + <option value="false">stopping after first match</option> + </select> + </div> +</script> + +<script type="text/x-red" data-help-name="switch"> + <p>A simple function node to route messages based on its properties.</p> + <p>When a message arrives, the selected property is evaluated against each + of the defined rules. The message is then sent to the output of <i>all</i> + rules that pass.</p> + <p>Note: the <i>otherwise</i> rule applies as a "not any of" the rules preceding it.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('switch', { + color: "#E2D96E", + category: 'function', + defaults: { + name: {value:""}, + property: {value:"payload", required:true}, + rules: {value:[{t:"eq", v:""}]}, + checkall: {value:"true", required:true}, + outputs: {value:1} + }, + inputs: 1, + outputs: 1, + icon: "switch.png", + label: function() { + return this.name||"switch"; + }, + oneditprepare: function() { + + var operators = [ + {v:"eq",t:"=="}, + {v:"neq",t:"!="}, + {v:"lt",t:"<"}, + {v:"lte",t:"<="}, + {v:"gt",t:">"}, + {v:"gte",t:">="}, + {v:"btwn",t:"is between"}, + {v:"cont",t:"contains"}, + {v:"regex",t:"matches regex"}, + {v:"true",t:"is true"}, + {v:"false",t:"is false"}, + {v:"null",t:"is null"}, + {v:"nnull",t:"is not null"}, + {v:"else",t:"otherwise"} + ]; + + function generateRule(i,rule) { + var container = $('<li/>',{style:"margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"}); + var row = $('<div/>').appendTo(container); + var row2 = $('<div/>',{style:"padding-top: 5px; text-align: right;"}).appendTo(container); + + var selectField = $('<select/>',{style:"width:120px; margin-left: 5px; text-align: center;"}).appendTo(row); + for (var d in operators) { + selectField.append($("<option></option>").val(operators[d].v).text(operators[d].t)); + } + + var valueField = $('<input/>',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px; width: 145px;"}).appendTo(row); + var btwnField = $('<span/>').appendTo(row); + var btwnValueField = $('<input/>',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px; width: 50px;"}).appendTo(btwnField); + btwnField.append(" and "); + var btwnValue2Field = $('<input/>',{class:"node-input-rule-btwn-value2",type:"text",style:"width: 50px;margin-left:2px;"}).appendTo(btwnField); + + var finalspan = $('<span/>',{style:"float: right; margin-top: 3px;margin-right: 10px;"}).appendTo(row); + finalspan.append(' send to <span class="node-input-rule-index">'+i+'</span> '); + + selectField.change(function() { + var type = selectField.children("option:selected").val(); + if (type.length < 4) { + selectField.css({"width":"60px"}); + } else if (type === "regex") { + selectField.css({"width":"147px"}); + } else { + selectField.css({"width":"120px"}); + } + if (type === "btwn") { + valueField.hide(); + btwnField.show(); + } else { + btwnField.hide(); + if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { + valueField.hide(); + } else { + valueField.show(); + } + } + }); + + var deleteButton = $('<a/>',{href:"#",class:"btn btn-mini", style:"margin-left: 5px;"}).appendTo(finalspan); + $('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton); + + deleteButton.click(function() { + container.css({"background":"#fee"}); + container.fadeOut(300, function() { + $(this).remove(); + $("#node-input-rule-container").children().each(function(i) { + $(this).find(".node-input-rule-index").html(i+1); + }); + + }); + }); + + $("#node-input-rule-container").append(container); + + selectField.find("option").filter(function() {return $(this).val() == rule.t;}).attr('selected',true); + if (rule.t == "btwn") { + btwnValueField.val(rule.v); + btwnValue2Field.val(rule.v2); + } else if (typeof rule.v != "undefined") { + valueField.val(rule.v); + } + selectField.change(); + } + + $("#node-input-add-rule").click(function() { + generateRule($("#node-input-rule-container").children().length+1,{t:"",v:"",v2:""}); + $("#node-input-rule-container-div").scrollTop($("#node-input-rule-container-div").get(0).scrollHeight); + }); + + for (var i=0;i<this.rules.length;i++) { + var rule = this.rules[i]; + generateRule(i+1,rule); + } + + function switchDialogResize(ev,ui) { + $("#node-input-rule-container-div").css("height",(ui.size.height-260)+"px"); + }; + + $( "#dialog" ).on("dialogresize", switchDialogResize); + $( "#dialog" ).one("dialogopen", function(ev) { + var size = $( "#dialog" ).dialog('option','sizeCache-switch'); + if (size) { + switchDialogResize(null,{size:size}); + } + }); + $( "#dialog" ).one("dialogclose", function(ev,ui) { + $( "#dialog" ).off("dialogresize",switchDialogResize); + }); + }, + oneditsave: function() { + var rules = $("#node-input-rule-container").children(); + var ruleset; + var node = this; + node.rules= []; + rules.each(function(i) { + var rule = $(this); + var type = rule.find("select option:selected").val(); + var r = {t:type}; + if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else")) { + if (type === "btwn") { + r.v = rule.find(".node-input-rule-btwn-value").val(); + r.v2 = rule.find(".node-input-rule-btwn-value2").val(); + } else { + r.v = rule.find(".node-input-rule-value").val(); + } + } + node.rules.push(r); + }); + node.outputs = node.rules.length; + } + }); +</script> diff --git a/dgbuilder/core_nodes/logic/10-switch.js b/dgbuilder/core_nodes/logic/10-switch.js new file mode 100644 index 00000000..8bcb8571 --- /dev/null +++ b/dgbuilder/core_nodes/logic/10-switch.js @@ -0,0 +1,78 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var operators = { + 'eq': function(a, b) { return a == b; }, + 'neq': function(a, b) { return a != b; }, + 'lt': function(a, b) { return a < b; }, + 'lte': function(a, b) { return a <= b; }, + 'gt': function(a, b) { return a > b; }, + 'gte': function(a, b) { return a >= b; }, + 'btwn': function(a, b, c) { return a >= b && a <= c; }, + 'cont': function(a, b) { return (a + "").indexOf(b) != -1; }, + 'regex': function(a, b) { return (a + "").match(new RegExp(b)); }, + 'true': function(a) { return a === true; }, + 'false': function(a) { return a === false; }, + 'null': function(a) { return typeof a == "undefined"; }, + 'nnull': function(a) { return typeof a != "undefined"; }, + 'else': function(a) { return a === true; } + }; + + function SwitchNode(n) { + RED.nodes.createNode(this, n); + this.rules = n.rules; + this.property = n.property; + this.checkall = n.checkall || "true"; + var propertyParts = n.property.split("."); + var node = this; + + for (var i=0; i<this.rules.length; i+=1) { + var rule = this.rules[i]; + if (!isNaN(Number(rule.v))) { + rule.v = Number(rule.v); + rule.v2 = Number(rule.v2); + } + } + + this.on('input', function (msg) { + var onward = []; + try { + var prop = propertyParts.reduce(function (obj, i) { + return obj[i] + }, msg); + var elseflag = true; + for (var i=0; i<node.rules.length; i+=1) { + var rule = node.rules[i]; + var test = prop; + if (rule.t == "else") { test = elseflag; elseflag = true; } + if (operators[rule.t](test,rule.v, rule.v2)) { + onward.push(msg); + elseflag = false; + if (node.checkall == "false") { break; } + } else { + onward.push(null); + } + } + this.send(onward); + } catch(err) { + node.warn(err); + } + }); + } + RED.nodes.registerType("switch", SwitchNode); +} diff --git a/dgbuilder/core_nodes/logic/15-change.html b/dgbuilder/core_nodes/logic/15-change.html new file mode 100644 index 00000000..702dd807 --- /dev/null +++ b/dgbuilder/core_nodes/logic/15-change.html @@ -0,0 +1,139 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="change"> + <div> + <select id="node-input-action" style="width:95%; margin-right:5px;"> + <option value="replace">Set the value of the message property</option> + <option value="change">Search/replace the value of the message property</option> + <option value="delete">Delete the message property</option> + </select> + </div> + <div class="form-row" style="padding-top:10px;" id="node-prop1-row"> + <label for="node-input-property">called</label> msg.<input type="text" id="node-input-property" style="width: 63%;"/> + </div> + <div class="form-row" id="node-from-row"> + <label for="node-input-from" id="node-input-f"></label> + <input type="text" id="node-input-from" placeholder="this"/> + </div> + <div class="form-row" id="node-to-row"> + <label for="node-input-to" id="node-input-t"></label> + <input type="text" id="node-input-to" placeholder="that"/> + </div> + <div class="form-row" id="node-reg-row"> + <label> </label> + <input type="checkbox" id="node-input-reg" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-reg" style="width: 70%;">Use regular expressions ?</label> + </div> + <div class="form-tips" id="node-tip"></div> + <br/> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="change"> + <p>A simple function node to change, replace, add or delete properties of a message.</p> + <p>When a message arrives, the selected property is modified by the defined rules. + The message is then sent to the output.</p> + <p><b>Note:</b> Replace only operates on <b>strings</b>. Anything else will be passed straight through.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('change', { + color: "#E2D96E", + category: 'function', + defaults: { + action: {value:"replace",required:true}, + property: {value:"payload",required:true}, + from: {value:"",validate: function(v) { + if (this.action == "change" && this.reg) { + try { + var re = new RegExp(this.from, "g"); + return true; + } catch(err) { + return false; + } + } + return true; + }}, + to: {value:""}, + reg: {value:false}, + name: {value:""} + }, + inputs: 1, + outputs: 1, + icon: "swap.png", + label: function() { + if (this.name) { + return this.name; + } + if (this.action == "replace") { + return "set msg."+this.property; + } else { + return this.action+" msg."+this.property + } + }, + labelStyle: function() { + return this.name ? "node_label_italic" : ""; + }, + oneditprepare: function() { + if (this.reg === null) { $("#node-input-reg").prop('checked', true); } + $("#node-input-action").change( function() { + var a = $("#node-input-action").val(); + if (a === "replace") { + $("#node-input-todo").html("called"); + //$("#node-input-f").html("name"); + $("#node-input-t").html("to"); + $("#node-from-row").hide(); + $("#node-to-row").show(); + $("#node-reg-row").hide(); + $("#node-tip").show(); + $("#node-tip").html("Tip: expects a new property name and either a fixed value OR the full name of another message property eg: msg.sentiment.score"); + } + if (a === "delete") { + $("#node-input-todo").html("called"); + //$("#node-input-f").html("called"); + //$("#node-input-t").html("to"); + $("#node-from-row").hide(); + $("#node-to-row").hide(); + $("#node-reg-row").hide(); + $("#node-tip").hide(); + } + if (a === "change") { + $("#node-input-todo").html("called"); + $("#node-input-f").html("Search for"); + $("#node-input-t").html("replace with"); + $("#node-from-row").show(); + $("#node-to-row").show(); + $("#node-reg-row").show(); + $("#node-tip").show(); + $("#node-tip").html("Tip: only works on string properties. If regular expressions are used, the <i>replace with</i> field can contain capture results, eg $1."); + } + //if (a === "replace") { + // $("#node-input-todo").html("called"); + // //$("#node-input-f").html("with"); + // $("#node-input-t").html("with"); + // $("#node-from-row").hide(); + // $("#node-to-row").show(); + // $("#node-tip").html("Tip: accepts either a fixed value OR the full name of another msg.property eg: msg.sentiment.score"); + //} + }); + $("#node-input-action").change(); + } + }); +</script> diff --git a/dgbuilder/core_nodes/logic/15-change.js b/dgbuilder/core_nodes/logic/15-change.js new file mode 100644 index 00000000..b7ef62e1 --- /dev/null +++ b/dgbuilder/core_nodes/logic/15-change.js @@ -0,0 +1,74 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + function ChangeNode(n) { + RED.nodes.createNode(this, n); + this.action = n.action; + this.property = n.property || ""; + this.from = n.from || " "; + this.to = n.to || " "; + this.reg = (n.reg === null || n.reg); + var node = this; + if (node.reg === false) { + this.from = this.from.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + } + var makeNew = function( stem, path, value ) { + var lastPart = (arguments.length === 3) ? path.pop() : false; + for (var i = 0; i < path.length; i++) { + stem = stem[path[i]] = stem[path[i]] || {}; + } + if (lastPart) { stem = stem[lastPart] = value; } + return stem; + }; + + this.on('input', function (msg) { + if (node.action == "change") { + try { + node.re = new RegExp(this.from, "g"); + } catch (e) { + node.error(e.message); + } + if (typeof msg[node.property] === "string") { + msg[node.property] = (msg[node.property]).replace(node.re, node.to); + } + } + //else if (node.action == "replace") { + //if (node.to.indexOf("msg.") == 0) { + //msg[node.property] = eval(node.to); + //} + //else { + //msg[node.property] = node.to; + //} + //} + else if (node.action == "replace") { + if (node.to.indexOf("msg.") === 0) { + makeNew( msg, node.property.split("."), eval(node.to) ); + } + else { + makeNew( msg, node.property.split("."), node.to ); + } + //makeNew( msg, node.property.split("."), node.to ); + } + else if (node.action == "delete") { + delete(msg[node.property]); + } + node.send(msg); + }); + } + RED.nodes.registerType("change", ChangeNode); +} diff --git a/dgbuilder/core_nodes/logic/16-range.html b/dgbuilder/core_nodes/logic/16-range.html new file mode 100644 index 00000000..5f87128f --- /dev/null +++ b/dgbuilder/core_nodes/logic/16-range.html @@ -0,0 +1,81 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="range"> + <div class="form-row"> + <label for="node-input-action"><i class="fa fa-dot-circle-o"></i> Action</label> + <select id="node-input-action" style="width:70%; margin-right:5px;"> + <option value="scale">Scale msg.payload</option> + <option value="clamp">Scale and limit to the target range</option> + <option value="roll">Scale and wrap within the target range</option> + </select> + </div> + <br/> + <div class="form-row"><i class="fa fa-sign-in"></i> Map the input range:</div> + <div class="form-row"><label></label> + from: <input type="text" id="node-input-minin" placeholder="e.g. 0" style="width:100px;"/> + to: <input type="text" id="node-input-maxin" placeholder="e.g. 99" style="width:100px;"/> + </div> + <div class="form-row"><i class="fa fa-sign-out"></i> to the result range:</div> + <div class="form-row"><label></label> + from: <input type="text" id="node-input-minout" placeholder="e.g. 0" style="width:100px;"/> + to: <input type="text" id="node-input-maxout" placeholder="e.g. 255" style="width:100px;"/> + </div> + <br/> + <div class="form-row"><label></label> + <input type="checkbox" id="node-input-round" style="display: inline-block; width: auto; vertical-align: top;"> + <label style="width: auto;" for="node-input-round">Round result to the nearest integer?</label></input> + </div> + <br/> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips" id="node-tip">Tip: This node ONLY works with numbers.</div> +</script> + +<script type="text/x-red" data-help-name="range"> + <p>A simple function node to remap numeric input values to another scale.</p> + <p>Currently only does a linear scaling.</p> + <p><b>Note:</b> This only operates on <b>numbers</b>. Anything else will try to be made into a number and rejected if that fails.</p> + <p><i>Scale and limit to target range</i> means that the result will never be outside the range specified within the result range.</p> + <p><i>Scale and wrap within the target range</i> means that the result will essentially be a "modulo-style" wrap-around within the result range.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('range', { + color: "#E2D96E", + category: 'function', + defaults: { + minin: {value:"",required:true,validate:RED.validators.number()}, + maxin: {value:"",required:true,validate:RED.validators.number()}, + minout: {value:"",required:true,validate:RED.validators.number()}, + maxout: {value:"",required:true,validate:RED.validators.number()}, + action: {value:"scale"}, + round: {value:false}, + name: {value:""} + }, + inputs: 1, + outputs: 1, + icon: "range.png", + label: function() { + return this.name || "range"; + }, + labelStyle: function() { + return this.name ? "node_label_italic" : ""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/logic/16-range.js b/dgbuilder/core_nodes/logic/16-range.js new file mode 100644 index 00000000..ec39342a --- /dev/null +++ b/dgbuilder/core_nodes/logic/16-range.js @@ -0,0 +1,48 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + function RangeNode(n) { + RED.nodes.createNode(this, n); + this.action = n.action; + this.round = n.round || false; + this.minin = Number(n.minin); + this.maxin = Number(n.maxin); + this.minout = Number(n.minout); + this.maxout = Number(n.maxout); + var node = this; + + this.on('input', function (msg) { + var n = Number(msg.payload); + if (!isNaN(n)) { + if (node.action == "clamp") { + if (n < node.minin) { n = node.minin; } + if (n > node.maxin) { n = node.maxin; } + } + if (node.action == "roll") { + if (n >= node.maxin) { n = (n - node.minin) % (node.maxin - node.minin) + node.minin; } + if (n < node.minin) { n = (n - node.minin) % (node.maxin - node.minin) + node.maxin; } + } + msg.payload = ((n - node.minin) / (node.maxin - node.minin) * (node.maxout - node.minout)) + node.minout; + if (node.round) { msg.payload = Math.round(msg.payload); } + node.send(msg); + } + else { node.log("Not a number: "+msg.payload); } + }); + } + RED.nodes.registerType("range", RangeNode); +} diff --git a/dgbuilder/core_nodes/parsers/70-CSV.html b/dgbuilder/core_nodes/parsers/70-CSV.html new file mode 100644 index 00000000..5632246f --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-CSV.html @@ -0,0 +1,123 @@ + +<!-- + Copyright 2014 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. +--> + +<script type="text/x-red" data-template-name="csv"> + <div class="form-row"> + <label for="node-input-temp"><i class="fa fa-list"></i> Columns</label> + <input type="text" id="node-input-temp" placeholder="comma-separated column names"> + </div> + <div class="form-row"> + <label for="node-input-select-sep"><i class="fa fa-text-width"></i> Separator</label> + <select style="width: 150px" id="node-input-select-sep"> + <option value=",">comma</option> + <option value="\t">tab</option> + <option value=" ">space</option> + <option value=";">semicolon</option> + <option value=":">colon</option> + <option value="">other...</option> + </select> + <input style="width: 40px;" type="text" id="node-input-sep" pattern="."> + </div> + + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <hr align="middle"/> + <div class="form-row"> + <label style="width: 100%;"><i class="fa fa-gears"></i> CSV-to-Object options</label> + <label style="margin-left: 10px; margin-right: -10px;"><i class="fa fa-sign-in"></i> Input</label> + <input style="width: 30px" type="checkbox" id="node-input-hdrin"><label style="width: auto;" for="node-input-hdrin">first row contains column names</span> + </div> + <div class="form-row"> + <label style="margin-left: 10px; margin-right: -10px;"><i class="fa fa-sign-out"></i> Output</label> + <select type="text" id="node-input-multi" style="width: 250px;"> + <option value="one">a message per row</option> + <option value="mult">a single message [array]</option> + </select> + </div> + <hr align="middle"/> + <div class="form-row"> + <label style="width: 100%;"><i class="fa fa-gears"></i> Object-to-CSV options</label> + <label style="margin-left: 10px; margin-right: -10px;"><i class="fa fa-sign-in"></i> Output</label> + <input style="width: 30px" type="checkbox" id="node-input-hdrout"><label style="width: auto;" for="node-input-hdrout">include column name row</span> + </div> + <div class="form-row"> + <label style="margin-left: 10px; margin-right: -10px;"><i class="fa fa-align-left"></i> Newline</label> + <select style="width: 150px" id="node-input-ret"> + <option value='\n'>Linux (\n)</option> + <option value='\r'>Mac (\r)</option> + <option value='\r\n'>Windows (\r\n)</option> + </select> + </div> +</script> + +<script type="text/x-red" data-help-name="csv"> + <p>A function that parses the <b>msg.payload</b> to convert csv to/from a javascript object. + Places the result in the payload.</p> + <p>If the input is a string it tries to parse it as CSV and creates a javascript object.</p> + <p>If the input is a javascript object it tries to build a CSV string.</p> + <p>The columns template should contain an ordered list of column headers. For csv input these become the property names. + For csv output these specify the properties to extract from the object and the order for the csv.</p> + <p><b>Note:</b> the columns should always be specified comma separated - even if another separator is chosen for the data.</p> + </script> + +<script type="text/javascript"> + RED.nodes.registerType('csv',{ + category: 'function', + color:"#DEBD5C", + defaults: { + name: {value:""}, + sep: {value:',',required:true,validate:RED.validators.regex(/^.{1,2}$/)}, + //quo: {value:'"',required:true}, + hdrin: {value:""}, + hdrout: {value:""}, + multi: {value:"one",required:true}, + ret: {value:'\\n'}, + temp: {value:""} + }, + inputs:1, + outputs:1, + icon: "arrow-in.png", + label: function() { + return this.name||"csv"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + if (this.sep == "," || this.sep == "\\t" || this.sep == ";" || this.sep == ":" || this.sep == " ") { + $("#node-input-select-sep").val(this.sep); + $("#node-input-sep").hide(); + } else { + $("#node-input-select-sep").val(""); + $("#node-input-sep").val(this.sep); + $("#node-input-sep").show(); + } + $("#node-input-select-sep").change(function() { + var v = $("#node-input-select-sep option:selected").val(); + $("#node-input-sep").val(v); + if (v == "") { + $("#node-input-sep").val(""); + $("#node-input-sep").show().focus(); + } else { + $("#node-input-sep").hide(); + } + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/parsers/70-CSV.js b/dgbuilder/core_nodes/parsers/70-CSV.js new file mode 100644 index 00000000..56aa7c72 --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-CSV.js @@ -0,0 +1,157 @@ +/** + * Copyright 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + function CSVNode(n) { + RED.nodes.createNode(this,n); + this.template = n.temp.split(","); + this.sep = (n.sep || ',').replace("\\t","\t").replace("\\n","\n").replace("\\r","\r"); + this.quo = '"'; + this.ret = (n.ret || "\n").replace("\\n","\n").replace("\\r","\r"); + this.winflag = (this.ret === "\r\n"); + this.lineend = "\n"; + this.multi = n.multi || "one"; + this.hdrin = n.hdrin || false; + this.hdrout = n.hdrout || false; + this.goodtmpl = true; + var node = this; + + // pass in an array of column names to be trimed, de-quoted and retrimed + var clean = function(col) { + for (var t = 0; t < col.length; t++) { + col[t] = col[t].trim(); // remove leading and trailing whitespace + if (col[t].charAt(0) === '"' && col[t].charAt(col[t].length -1) === '"') { + // remove leading and trailing quotes (if they exist) - and remove whitepace again. + col[t] = col[t].substr(1,col[t].length -2).trim(); + } + } + if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; } + else { node.goodtmpl = true; } + return col; + } + node.template = clean(node.template); + + this.on("input", function(msg) { + if (msg.hasOwnProperty("payload")) { + if (typeof msg.payload == "object") { // convert object to CSV string + try { + var ou = ""; + if (node.hdrout) { + ou += node.template.join(node.sep) + node.ret; + } + if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; } + for (var s = 0; s < msg.payload.length; s++) { + for (var t=0; t < node.template.length; t++) { + + // aaargh - resorting to eval here - but fairly contained front and back. + var p = RED.util.ensureString(eval("msg.payload[s]."+node.template[t])); + + if (p === "undefined") { p = ""; } + if (p.indexOf(node.sep) != -1) { // add quotes if any "commas" + ou += node.quo + p + node.quo + node.sep; + } + else if (p.indexOf(node.quo) != -1) { // add double quotes if any quotes + p = p.replace(/"/g, '""'); + ou += node.quo + p + node.quo + node.sep; + } + else { ou += p + node.sep; } // otherwise just add + } + ou = ou.slice(0,-1) + node.ret; // remove final "comma" and add "newline" + } + node.send({payload:ou}); + } + catch(e) { node.log(e); } + } + else if (typeof msg.payload == "string") { // convert CSV string to object + try { + var f = true; // flag to indicate if inside or outside a pair of quotes true = outside. + var j = 0; // pointer into array of template items + var k = [""]; // array of data for each of the template items + var o = {}; // output object to build up + var a = []; // output array is needed for multiline option + var first = true; // is this the first line + var tmp = ""; + + // For now we are just going to assume that any \r or \n means an end of line... + // got to be a weird csv that has singleton \r \n in it for another reason... + + // Now process the whole file/line + for (var i = 0; i < msg.payload.length; i++) { + if ((node.hdrin === true) && first) { // if the template is in the first line + if ((msg.payload[i] === "\n")||(msg.payload[i] === "\r")) { // look for first line break + node.template = clean(tmp.split(node.sep)); + first = false; + } + else { tmp += msg.payload[i]; } + } + else { + if (msg.payload[i] === node.quo) { // if it's a quote toggle inside or outside + f = !f; + if (msg.payload[i-1] === node.quo) { k[j] += '\"'; } // if it's a quotequote then it's actually a quote + } + else if ((msg.payload[i] === node.sep) && f) { // if we are outside of quote (ie valid separator + if (!node.goodtmpl) { node.template[j] = "col"+(j+1); } + if ( node.template[j] && (node.template[j] !== "") && (k[j] !== "" ) ) { + if (!isNaN(Number(k[j]))) { k[j] = Number(k[j]); } + o[node.template[j]] = k[j]; + } + j += 1; + k[j] = ""; + } + else if (f && ((msg.payload[i] === "\n") || (msg.payload[i] === "\r"))) { // handle multiple lines + //console.log(j,k,o,k[j]); + if ( node.template[j] && (node.template[j] !== "") && (k[j] !== "") ) { + if (!isNaN(Number(k[j]))) { k[j] = Number(k[j]); } + else { k[j].replace(/\r$/,''); } + o[node.template[j]] = k[j]; + } + if (JSON.stringify(o) !== "{}") { // don't send empty objects + if (node.multi === "one") { node.send({payload:o}); } // either send + else { a.push(o); } // or add to the array + } + j = 0; + k = [""]; + o = {}; + } + else { // just add to the part of the message + k[j] += msg.payload[i]; + } + } + } + // Finished so finalize and send anything left + //console.log(j,k,o,k[j]); + if (!node.goodtmpl) { node.template[j] = "col"+(j+1); } + if ( node.template[j] && (node.template[j] !== "") && (k[j] !== "") ) { + if (!isNaN(Number(k[j]))) { k[j] = Number(k[j]); } + else { k[j].replace(/\r$/,''); } + o[node.template[j]] = k[j]; + } + msg.payload = o; + if (JSON.stringify(o) !== "{}") { // don't send empty objects + if (node.multi === "one") { node.send({payload:o}); } // either send + else { a.push(o); } // or add to the aray + } + if (node.multi !== "one") { node.send({payload:a}); } // finally send the array + } + catch(e) { node.log(e); } + } + else { node.log("This node only handles csv strings or js objects."); } + } + }); + } + RED.nodes.registerType("csv",CSVNode); +} diff --git a/dgbuilder/core_nodes/parsers/70-HTML.html b/dgbuilder/core_nodes/parsers/70-HTML.html new file mode 100644 index 00000000..2b73b49c --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-HTML.html @@ -0,0 +1,73 @@ +<!-- + Copyright 2014 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. +--> + +<script type="text/x-red" data-template-name="html"> + <div class="form-row"> + <label for="node-input-tag"><i class="fa fa-filter"></i> Select</label> + <input type="text" id="node-input-tag" placeholder="h1"> + </div> + <div class="form-row"> + <label for="node-input-ret"><i class="fa fa-sign-out"></i> Output</label> + <select id="node-input-ret" style="width:73% !important"> + <option value="html">the html content of the elements</option> + <option value="text">only the text content of the elements</option> + <!-- <option value="attr">an object of any attributes</option> --> + <!-- <option value="val">return the value from a form element</option> --> + </select> + </div> + <div class="form-row"> + <label for="node-input-as"> </label> + <select id="node-input-as" style="width:73% !important"> + <option value="single">as a single message containing an array</option> + <option value="multi">as multiple messages, one for each element</option> + </select> + </div> + <br/> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: The <b>Select</b> value is a <a href="http://api.jquery.com/category/selectors/" target="_new"><i><u>jQuery</u></i></a> style selector.</div> +</script> + +<script type="text/x-red" data-help-name="html"> + <p>Extracts elements from an html document held in <b>msg.payload</b> using a selector.</p> + <p>The selector uses the <a href="http://api.jquery.com/category/selectors/" target="_new">jQuery syntax</a>.</p> + <p>The result is either a single message with a payload containing an array of the matched elements, or multiple + messages that each contain a matched element.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('html',{ + category: 'function', + color:"#DEBD5C", + defaults: { + tag: {value:""}, + ret: {value:"html"}, + as: {value:"single"}, + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "jq.png", + label: function() { + return this.name||this.tag||"html"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/parsers/70-HTML.js b/dgbuilder/core_nodes/parsers/70-HTML.js new file mode 100644 index 00000000..7a9450f3 --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-HTML.js @@ -0,0 +1,60 @@ +/** + * Copyright 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var cheerio = require('cheerio'); + + function CheerioNode(n) { + RED.nodes.createNode(this,n); + this.tag = n.tag || "h1"; + this.ret = n.ret || "html"; + this.as = n.as || "single"; + var node = this; + this.on("input", function(msg) { + try { + var $ = cheerio.load(msg.payload); + var pay = []; + $(node.tag).each(function() { + if (node.as === "multi") { + var pay2 = null; + if (node.ret === "html") { pay2 = $(this).html(); } + if (node.ret === "text") { pay2 = $(this).text(); } + //if (node.ret === "attr") { pay2 = $(this)[0]["attribs"]; } + //if (node.ret === "val") { pay2 = $(this).val(); } + if (pay2) { + msg.payload = pay2; + node.send(msg); + } + } + if (node.as === "single") { + if (node.ret === "html") { pay.push( $(this).html() ); } + if (node.ret === "text") { pay.push( $(this).text() ); } + //if (node.ret === "attr") { pay.push( $(this)[0]["attribs"] ); } + //if (node.ret === "val") { pay.push( $(this).val() ); } + } + }); + if ((node.as === "single") && (pay.length !== 0)) { + msg.payload = pay; + node.send(msg); + } + } catch (error) { + node.log('Error: '+error.message); + } + }); + } + RED.nodes.registerType("html",CheerioNode); +} diff --git a/dgbuilder/core_nodes/parsers/70-JSON.html b/dgbuilder/core_nodes/parsers/70-JSON.html new file mode 100644 index 00000000..864974ab --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-JSON.html @@ -0,0 +1,47 @@ +<!-- + Copyright 2014 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. +--> + +<script type="text/x-red" data-template-name="json"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="json"> + <p>A function that parses the <b>msg.payload</b> to convert a JSON string to/from a javascript object. Places the result back into the payload.</p> + <p>If the input is a JSON string it tries to parse it to a javascript object.</p> + <p>If the input is a javascript object it creates a JSON string.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('json',{ + category: 'function', + color:"#DEBD5C", + defaults: { + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "arrow-in.png", + label: function() { + return this.name||"json"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/parsers/70-JSON.js b/dgbuilder/core_nodes/parsers/70-JSON.js new file mode 100644 index 00000000..e216bd4f --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-JSON.js @@ -0,0 +1,46 @@ +/** + * Copyright 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + + function JSONNode(n) { + RED.nodes.createNode(this,n); + var node = this; + this.on("input", function(msg) { + if (msg.hasOwnProperty("payload")) { + if (typeof msg.payload === "string") { + try { + msg.payload = JSON.parse(msg.payload); + node.send(msg); + } + catch(e) { node.log(e+ "\n"+msg.payload); } + } + else if (typeof msg.payload === "object") { + if (!Buffer.isBuffer(msg.payload) ) { + if (!util.isArray(msg.payload)) { + msg.payload = JSON.stringify(msg.payload); + node.send(msg); + } + } + } + else { node.log("dropped: "+msg.payload); } + } + }); + } + RED.nodes.registerType("json",JSONNode); +} diff --git a/dgbuilder/core_nodes/parsers/70-XML.html b/dgbuilder/core_nodes/parsers/70-XML.html new file mode 100644 index 00000000..8b0a2843 --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-XML.html @@ -0,0 +1,48 @@ +<!-- + Copyright 2014 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. +--> + +<script type="text/x-red" data-template-name="xml"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="xml"> + <p>A function that parses the <b>msg.payload</b> to convert xml to/from a javascript object. Places the result in the payload.</p> + <p>If the input is a string it tries to parse it as XML and creates a javascript object.</p> + <p>If the input is a javascript object it tries to build an XML string.</p> + <p>See <a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md" target="_new">the xml2js docs <i>here</i></a> for more information.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('xml',{ + category: 'function', + color:"#DEBD5C", + defaults: { + name: {value:""} + }, + inputs:1, + outputs:1, + icon: "arrow-in.png", + label: function() { + return this.name||"xml"; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/parsers/70-XML.js b/dgbuilder/core_nodes/parsers/70-XML.js new file mode 100644 index 00000000..931de7f5 --- /dev/null +++ b/dgbuilder/core_nodes/parsers/70-XML.js @@ -0,0 +1,46 @@ +/** + * Copyright 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var xml2js = require('xml2js'); + var parseString = xml2js.parseString; + var builder = new xml2js.Builder({renderOpts:{pretty:false}}); + + function XMLNode(n) { + RED.nodes.createNode(this,n); + var node = this; + this.on("input", function(msg) { + if (msg.hasOwnProperty("payload")) { + if (typeof msg.payload == "object") { + msg.payload = builder.buildObject(msg.payload); + node.send(msg); + } + else if (typeof msg.payload == "string") { + parseString(msg.payload, {strict:true,async:true}, function (err, result) { + if (err) { node.error(err); } + else { + msg.payload = result; + node.send(msg); + } + }); + } + else { node.log("This node only handles xml strings or js objects."); } + } + }); + } + RED.nodes.registerType("xml",XMLNode); +} diff --git a/dgbuilder/core_nodes/social/27-twitter.html b/dgbuilder/core_nodes/social/27-twitter.html new file mode 100644 index 00000000..99d45213 --- /dev/null +++ b/dgbuilder/core_nodes/social/27-twitter.html @@ -0,0 +1,223 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="twitter-credentials"> + <div class="form-row" id="node-config-twitter-row"></div> + <input type="hidden" id="node-config-input-screen_name"> +</script> + +<script type="text/javascript"> +(function() { + var twitterConfigNodeId = null; + var twitterConfigNodeIntervalId = null; + + function showTwitterAuthStart() { + var pathname = document.location.pathname; + if (pathname.slice(-1) != "/") { + pathname += "/"; + } + var callback = encodeURIComponent(location.protocol+"//"+location.hostname+":"+location.port+pathname+"twitter-credentials/"+twitterConfigNodeId+"/auth/callback"); + $("#node-config-dialog-ok").button("disable"); + $("#node-config-twitter-row").html('<div style="text-align: center; margin-top: 20px; "><a class="btn" id="node-config-twitter-start" href="twitter-credentials/'+twitterConfigNodeId+'/auth?callback='+callback+'" target="_blank">Click here to authenticate with Twitter.</a></div>'); + $("#node-config-twitter-start").click(function() { + twitterConfigNodeIntervalId = window.setTimeout(pollTwitterCredentials,2000); + }); + } + function updateTwitterScreenName(sn) { + $("#node-config-input-screen_name").val(sn); + $("#node-config-twitter-row").html('<label><i class="fa fa-user"></i> Twitter ID</label><span class="input-xlarge uneditable-input">'+sn+'</span>'); + } + function pollTwitterCredentials(e) { + $.getJSON('credentials/twitter-credentials/'+twitterConfigNodeId,function(data) { + if (data.screen_name) { + updateTwitterScreenName(data.screen_name); + twitterConfigNodeIntervalId = null; + $("#node-config-dialog-ok").button("enable"); + } else { + twitterConfigNodeIntervalId = window.setTimeout(pollTwitterCredentials,2000); + } + }) + } + RED.nodes.registerType('twitter-credentials',{ + category: 'config', + defaults: { + screen_name: {value:""} + }, + credentials: { + screen_name: {type:"text"}, + access_token: {type: "password"}, + access_token_secret: {type:"password"} + }, + + label: function() { + return this.screen_name; + }, + exportable: false, + oneditprepare: function() { + twitterConfigNodeId = this.id; + if (!this.screen_name || this.screen_name == "") { + showTwitterAuthStart(); + } else { + if (this.credentials.screen_name) { + updateTwitterScreenName(this.credentials.screen_name); + } else { + showTwitterAuthStart(); + } + } + }, + oneditsave: function() { + if (twitterConfigNodeIntervalId) { + window.clearTimeout(twitterConfigNodeIntervalId); + } + }, + oneditcancel: function(adding) { + if (twitterConfigNodeIntervalId) { + window.clearTimeout(twitterConfigNodeIntervalId); + } + } + }); +})(); +</script> + +<script type="text/x-red" data-template-name="twitter in"> + <div class="form-row"> + <label for="node-input-twitter"><i class="fa fa-user"></i> Log in as</label> + <input type="text" id="node-input-twitter"> + </div> + <div class="form-row"> + <label for="node-input-user"><i class="fa fa-search"></i> Search</label> + <select type="text" id="node-input-user" style="display: inline-block; vertical-align: middle; width:60%;"> + <option value="false">all public tweets</option> + <option value="true">the tweets of who you follow</option> + <option value="user">the tweets of specific users</option> + <option value="dm">your direct messages</option> + </select> + </div> + <div class="form-row" id="node-input-tags-row"> + <label for="node-input-tags"><i class="fa fa-tags"></i> <span id="node-input-tags-label">for</span></label> + <input type="text" id="node-input-tags" placeholder="comma-separated words, @ids, #tags"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">Tip: Use commas without spaces between multiple search terms. Comma = OR, Space = AND. + <br/>The Twitter API WILL NOT deliver 100% of all tweets. + <br/>Tweets of who you follow will include their retweets and favourites.</div> +</script> + +<script type="text/x-red" data-help-name="twitter in"> + <p>Twitter input node. Can be used to search either: + <ul><li>the public or a user's stream for tweets containing the configured search term</li> + <li>all tweets by specific users</li> + <li>direct messages received by the authenticated user</li> + </ul></p> + <p>Use space for <i>and</i> and comma , for <i>or</i> when searching for multiple terms.</p> + <p>Sets the <b>msg.topic</b> to <i>tweets/</i> and then appends the senders screen name.</p> + <p>Sets <b>msg.location</b> to the tweeters location if known.</p> + <p>Sets <b>msg.tweet</b> to the full tweet object as documented by <a href="https://dev.twitter.com/docs/platform-objects/tweets">Twitter</a>. + <p><b>Note:</b> when set to a specific user's tweets, or your direct messages, the node is subject to + Twitter's API rate limiting. If you deploy the flows multiple times within a 15 minute window, you may + exceed the limit and will see errors from the node. These errors will clear when the current 15 minute window + passes.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('twitter in',{ + category: 'social-input', + color:"#C0DEED", + defaults: { + twitter: {type:"twitter-credentials",required:true}, + tags: {value:"",validate:function(v) { return this.user == "dm" || v.length > 0;}}, + user: {value:"false",required:true}, + name: {value:""}, + topic: {value:"tweets"} + }, + inputs:0, + outputs:1, + icon: "twitter.png", + label: function() { + if (this.name) { + return this.name; + } + if (this.user == "dm") { + var user = RED.nodes.node(this.twitter); + return (user?user.label()+" ":"")+"DMs"; + } else if (this.user == "user") { + return this.tags+" tweets"; + } + return this.tags; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + $("#node-input-user").change(function() { + var type = $("#node-input-user option:selected").val(); + if (type == "user") { + $("#node-input-tags-row").show(); + $("#node-input-tags-label").html("User"); + $("#node-input-tags").attr("placeholder","comma-separated @twitter handles"); + } else if (type == "dm") { + $("#node-input-tags-row").hide(); + } else { + $("#node-input-tags-row").show(); + $("#node-input-tags-label").html("for"); + $("#node-input-tags").attr("placeholder","comma-separated words, @ids, #hashtags"); + } + + }); + $("#node-input-user").change(); + + } + }); +</script> + + +<script type="text/x-red" data-template-name="twitter out"> + <div class="form-row"> + <label for="node-input-twitter"><i class="fa fa-user"></i> Twitter</label> + <input type="text" id="node-input-twitter"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="twitter out"> + <p>Twitter out node. Tweets the <b>msg.payload</b>.</p> + <p>If <b>msg.media</b> exists and is a Buffer object, this node will treat it + as an image and attach it to the tweet.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('twitter out',{ + category: 'social-output', + color:"#C0DEED", + defaults: { + twitter: {type:"twitter-credentials",required:true}, + name: {value:"Tweet"} + }, + inputs:1, + outputs:0, + icon: "twitter.png", + align: "right", + label: function() { + return this.name; + } + }); +</script> diff --git a/dgbuilder/core_nodes/social/27-twitter.js b/dgbuilder/core_nodes/social/27-twitter.js new file mode 100644 index 00000000..5cacd9eb --- /dev/null +++ b/dgbuilder/core_nodes/social/27-twitter.js @@ -0,0 +1,347 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var ntwitter = require('twitter-ng'); + var OAuth= require('oauth').OAuth; + var request = require('request'); + + function TwitterNode(n) { + RED.nodes.createNode(this,n); + this.screen_name = n.screen_name; + } + RED.nodes.registerType("twitter-credentials",TwitterNode,{ + credentials: { + screen_name: {type:"text"}, + access_token: {type: "password"}, + access_token_secret: {type:"password"} + } + }); + + function TwitterInNode(n) { + RED.nodes.createNode(this,n); + this.active = true; + this.user = n.user; + //this.tags = n.tags.replace(/ /g,''); + this.tags = n.tags; + this.twitter = n.twitter; + this.topic = n.topic||"tweets"; + this.twitterConfig = RED.nodes.getNode(this.twitter); + var credentials = RED.nodes.getCredentials(this.twitter); + + if (credentials && credentials.screen_name == this.twitterConfig.screen_name) { + var twit = new ntwitter({ + consumer_key: "OKjYEd1ef2bfFolV25G5nQ", + consumer_secret: "meRsltCktVMUI8gmggpXett7WBLd1k0qidYazoML6g", + access_token_key: credentials.access_token, + access_token_secret: credentials.access_token_secret + }); + + + //setInterval(function() { + // twit.get("/application/rate_limit_status.json",null,function(err,cb) { + // console.log("direct_messages:",cb["resources"]["direct_messages"]); + // }); + // + //},10000); + + var node = this; + if (this.user === "user") { + node.poll_ids = []; + node.since_ids = {}; + var users = node.tags.split(","); + for (var i=0;i<users.length;i++) { + var user = users[i].replace(" ",""); + twit.getUserTimeline({ + screen_name:user, + trim_user:0, + count:1 + },function() { + var u = user+""; + return function(err,cb) { + if (err) { + node.error(err); + return; + } + if (cb[0]) { + node.since_ids[u] = cb[0].id_str; + } else { + node.since_ids[u] = '0'; + } + node.poll_ids.push(setInterval(function() { + twit.getUserTimeline({ + screen_name:u, + trim_user:0, + since_id:node.since_ids[u] + },function(err,cb) { + if (cb) { + for (var t=cb.length-1;t>=0;t-=1) { + var tweet = cb[t]; + var where = tweet.user.location||""; + var la = tweet.lang || tweet.user.lang; + //console.log(tweet.user.location,"=>",tweet.user.screen_name,"=>",pay); + var msg = { topic:node.topic+"/"+tweet.user.screen_name, payload:tweet.text, location:where, lang:la, tweet:tweet }; + node.send(msg); + if (t == 0) { + node.since_ids[u] = tweet.id_str; + } + } + } + if (err) { + node.error(err); + } + }); + },60000)); + } + }()); + } + } else if (this.user === "dm") { + node.poll_ids = []; + twit.getDirectMessages({ + screen_name:node.twitterConfig.screen_name, + trim_user:0, + count:1 + },function(err,cb) { + if (err) { + node.error(err); + return; + } + if (cb[0]) { + node.since_id = cb[0].id_str; + } else { + node.since_id = '0'; + } + node.poll_ids.push(setInterval(function() { + twit.getDirectMessages({ + screen_name:node.twitterConfig.screen_name, + trim_user:0, + since_id:node.since_id + },function(err,cb) { + if (cb) { + for (var t=cb.length-1;t>=0;t-=1) { + var tweet = cb[t]; + var msg = { topic:node.topic+"/"+tweet.sender.screen_name, payload:tweet.text, tweet:tweet }; + node.send(msg); + if (t == 0) { + node.since_id = tweet.id_str; + } + } + } + if (err) { + node.error(err); + } + }); + },120000)); + }); + + } else if (this.tags !== "") { + try { + var thing = 'statuses/filter'; + if (this.user === "true") { thing = 'user'; } + var st = { track: [node.tags] }; + var bits = node.tags.split(","); + if ((bits.length > 0) && (bits.length % 4 == 0)) { + if ((Number(bits[0]) < Number(bits[2])) && (Number(bits[1]) < Number(bits[3]))) { + st = { locations: node.tags }; + } + else { + node.log("possible bad geo area format. Should be lower-left lon, lat, upper-right lon, lat"); + } + } + + var setupStream = function() { + if (node.active) { + twit.stream(thing, st, function(stream) { + //console.log(st); + //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) { + //console.log(tweet.user); + if (tweet.user !== undefined) { + var where = tweet.user.location||""; + var la = tweet.lang || tweet.user.lang; + //console.log(tweet.user.location,"=>",tweet.user.screen_name,"=>",pay); + var msg = { topic:node.topic+"/"+tweet.user.screen_name, payload:tweet.text, location:where, lang:la, tweet:tweet }; + node.send(msg); + } + }); + stream.on('limit', function(tweet) { + node.log("tweet rate limit hit"); + }); + stream.on('error', function(tweet,rc) { + if (rc == 420) { + node.warn("Twitter rate limit hit"); + } else { + node.warn("Stream error:"+tweet.toString()+" ("+rc+")"); + } + setTimeout(setupStream,10000); + }); + stream.on('destroy', function (response) { + if (this.active) { + node.warn("twitter ended unexpectedly"); + setTimeout(setupStream,10000); + } + }); + }); + } + } + setupStream(); + } + catch (err) { + node.error(err); + } + } else { + this.error("Invalid tag property"); + } + } else { + this.error("missing twitter credentials"); + } + + this.on('close', function() { + if (this.stream) { + this.active = false; + this.stream.destroy(); + } + if (this.poll_ids) { + for (var i=0;i<this.poll_ids.length;i++) { + clearInterval(this.poll_ids[i]); + } + } + }); + } + RED.nodes.registerType("twitter in",TwitterInNode); + + + function TwitterOutNode(n) { + RED.nodes.createNode(this,n); + this.topic = n.topic; + this.twitter = n.twitter; + this.twitterConfig = RED.nodes.getNode(this.twitter); + var credentials = RED.nodes.getCredentials(this.twitter); + var node = this; + + if (credentials && credentials.screen_name == this.twitterConfig.screen_name) { + var twit = new ntwitter({ + consumer_key: "OKjYEd1ef2bfFolV25G5nQ", + consumer_secret: "meRsltCktVMUI8gmggpXett7WBLd1k0qidYazoML6g", + access_token_key: credentials.access_token, + access_token_secret: credentials.access_token_secret + }); + node.on("input", function(msg) { + node.status({fill:"blue",shape:"dot",text:"tweeting"}); + + if (msg.payload.length > 140) { + msg.payload = msg.payload.slice(0,139); + node.warn("Tweet greater than 140 : truncated"); + } + + if (msg.media && Buffer.isBuffer(msg.media)) { + var apiUrl = "https://api.twitter.com/1.1/statuses/update_with_media.json"; + var signedUrl = oa.signUrl(apiUrl, + credentials.access_token, + credentials.access_token_secret, + "POST"); + + var r = request.post(signedUrl,function(err,httpResponse,body) { + if (err) { + node.error(err.toString()); + node.status({fill:"red",shape:"ring",text:"failed"}); + } else { + var response = JSON.parse(body); + if (body.errors) { + var errorList = body.errors.map(function(er) { return er.code+": "+er.message }).join(", "); + node.error("tweet failed: "+errorList); + node.status({fill:"red",shape:"ring",text:"failed"}); + } else { + node.status({}); + } + } + }); + var form = r.form(); + form.append("status",msg.payload); + form.append("media[]",msg.media,{filename:"image"}); + + } else { + twit.updateStatus(msg.payload, function (err, data) { + if (err) { + node.status({fill:"red",shape:"ring",text:"failed"}); + node.error(err); + } + node.status({}); + }); + } + }); + } + } + RED.nodes.registerType("twitter out",TwitterOutNode); + + var oa = new OAuth( + "https://api.twitter.com/oauth/request_token", + "https://api.twitter.com/oauth/access_token", + "OKjYEd1ef2bfFolV25G5nQ", + "meRsltCktVMUI8gmggpXett7WBLd1k0qidYazoML6g", + "1.0", + null, + "HMAC-SHA1" + ); + + RED.httpAdmin.get('/twitter-credentials/:id/auth', function(req, res){ + var credentials = {}; + oa.getOAuthRequestToken({ + oauth_callback: req.query.callback + },function(error, oauth_token, oauth_token_secret, results){ + if (error) { + var resp = '<h2>Oh no!</h2>'+ + '<p>Something went wrong with the authentication process. The following error was returned:<p>'+ + '<p><b>'+error.statusCode+'</b>: '+error.data+'</p>'+ + '<p>One known cause of this type of failure is if the clock is wrong on system running Node-RED.'; + res.send(resp) + } else { + credentials.oauth_token = oauth_token; + credentials.oauth_token_secret = oauth_token_secret; + res.redirect('https://twitter.com/oauth/authorize?oauth_token='+oauth_token) + RED.nodes.addCredentials(req.params.id,credentials); + } + }); + }); + + RED.httpAdmin.get('/twitter-credentials/:id/auth/callback', function(req, res, next){ + var credentials = RED.nodes.getCredentials(req.params.id); + credentials.oauth_verifier = req.query.oauth_verifier; + + oa.getOAuthAccessToken( + credentials.oauth_token, + credentials.token_secret, + credentials.oauth_verifier, + function(error, oauth_access_token, oauth_access_token_secret, results){ + if (error){ + console.log(error); + res.send("yeah something broke."); + } else { + credentials = {}; + credentials.access_token = oauth_access_token; + credentials.access_token_secret = oauth_access_token_secret; + credentials.screen_name = "@"+results.screen_name; + RED.nodes.addCredentials(req.params.id,credentials); + res.send("<html><head></head><body>Authorised - you can close this window and return to Node-RED</body></html>"); + } + } + ); + }); +} diff --git a/dgbuilder/core_nodes/social/32-feedparse.html b/dgbuilder/core_nodes/social/32-feedparse.html new file mode 100644 index 00000000..a7954277 --- /dev/null +++ b/dgbuilder/core_nodes/social/32-feedparse.html @@ -0,0 +1,57 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="feedparse"> + <div class="form-row"> + <label for="node-input-url"><i class="fa fa-globe"></i> Feed url</label> + <input type="text" id="node-input-url"> + </div> + <div class="form-row"> + <label for="node-input-interval"><i class="fa fa-repeat"></i> Repeat <span style="font-size: 0.9em;">(M)</span></label> + <input type="text" id="node-input-interval" placeholder="minutes"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <!-- <div class="form-tips"></div> --> +</script> + +<script type="text/x-red" data-help-name="feedparse"> + <p>Monitors an RSS/atom feed for new entries.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('feedparse',{ + category: 'advanced-input', + color:"#C0DEED", + defaults: { + name: {value:""}, + url: {value:"", required:true}, + interval: { value:15, required: true,validate:RED.validators.number()} + }, + inputs:0, + outputs:1, + icon: "feed.png", + label: function() { + return this.name||this.url; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); + +</script> diff --git a/dgbuilder/core_nodes/social/32-feedparse.js b/dgbuilder/core_nodes/social/32-feedparse.js new file mode 100644 index 00000000..97630e7d --- /dev/null +++ b/dgbuilder/core_nodes/social/32-feedparse.js @@ -0,0 +1,71 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var FeedParser = require("feedparser"); + var request = require("request"); + + function FeedParseNode(n) { + RED.nodes.createNode(this,n); + this.url = n.url; + this.interval = (parseInt(n.interval)||15)*60000; + var node = this; + this.interval_id = null; + this.seen = {}; + if (this.url !== "") { + var getFeed = function() { + request(node.url,function(err) { + if (err) node.error(err); + }) + .pipe(new FeedParser({feedurl:node.url})) + .on('error', function(error) { + node.error(error); + }) + .on('meta', function (meta) {}) + .on('readable', function () { + var stream = this, article; + while (article = stream.read()) { + if (!(article.guid in node.seen) || ( node.seen[article.guid] != 0 && node.seen[article.guid] != article.date.getTime())) { + node.seen[article.guid] = article.date?article.date.getTime():0; + var msg = { + topic:article.origlink||article.link, + payload: article.description, + article: article + }; + node.send(msg); + } + } + }) + .on('end', function () { + }); + }; + this.interval_id = setInterval(getFeed,node.interval); + getFeed(); + + } else { + this.error("Invalid url"); + } + } + + RED.nodes.registerType("feedparse",FeedParseNode); + + FeedParseNode.prototype.close = function() { + if (this.interval_id != null) { + clearInterval(this.interval_id); + } + } +} diff --git a/dgbuilder/core_nodes/social/61-email.html b/dgbuilder/core_nodes/social/61-email.html new file mode 100644 index 00000000..37397083 --- /dev/null +++ b/dgbuilder/core_nodes/social/61-email.html @@ -0,0 +1,189 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="e-mail"> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-envelope"></i> To</label> + <input type="text" id="node-input-name" placeholder="email@address.com"> + </div> + <!-- <div class="form-row"> + <label for="node-input-pin"><i class="fa fa-asterisk"></i> Service</label> + <select type="text" id="node-input-pin" style="width: 150px;"> + <option value="-" disabled> </option> + <option value="DynectEmail">DynectEmail</option> + <option value="Gmail">Gmail</option> + <option value="hot.ee">hot.ee</option> + <option value="Hotmail">Hotmail</option> + <option value="iCloud">iCloud</option> + <option value="mail.ee">mail.ee</option> + <option value="Mail.Ru">Mail.Ru</option> + <option value="Mailgun">Mailgun</option> + <option value="Mailjet">Mailjet</option> + <option value="Mandrill">Mandrill</option> + <option value="Postmark">Postmark</option> + <option value="QQ">QQ</option> + <option value="QQex">QQex</option> + <option value="SendGrid">SendGrid</option> + <option value="SendCloud">SendCloud</option> + <option value="SES">SES</option> + <option value="Yahoo">Yahoo</option> + <option value="yandex">yandex</option> + <option value="Zoho">Zoho</option> + </select> + </div> --> + <div class="form-row"> + <label for="node-input-server"><i class="fa fa-globe"></i> Server</label> + <input type="text" id="node-input-server" placeholder="smtp.gmail.com"> + </div> + <div class="form-row"> + <label for="node-input-port"><i class="fa fa-random"></i> Port</label> + <input type="text" id="node-input-port" placeholder="465"> + </div> + <div class="form-row"> + <label for="node-input-userid"><i class="fa fa-user"></i> Userid</label> + <input type="text" id="node-input-userid"> + </div> + <div class="form-row"> + <label for="node-input-password"><i class="fa fa-lock"></i> Password</label> + <input type="password" id="node-input-password"> + </div> + <br/> + <div class="form-row"> + <label for="node-input-dname"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-dname" placeholder="Name"> + </div> + <div class="form-tips" id="node-tip"><b>Note:</b> Copied credentials from global emailkeys.js file.</div> +</script> + +<script type="text/x-red" data-help-name="e-mail"> + <p>Sends the <b>msg.payload</b> as an email, with a subject of <b>msg.topic</b>.</p> + <!-- <p>It sends the message to the configured recipient <i>only</i>.</p> --> + <p>You may dynamically overide the default recipient by setting a <b>msg.to</b> property.</p> + <!-- <p><b>msg.topic</b> is used to set the subject of the email, and <b>msg.payload</b> is the body text.</p> --> +</script> + +<script type="text/javascript"> +(function() { + RED.nodes.registerType('e-mail',{ + category: 'social-output', + color:"#c7e9c0", + defaults: { + server: {value:"smtp.gmail.com",required:true}, + port: {value:"465",required:true}, + name: {value:"",required:true}, + dname: {value:""} + }, + credentials: { + userid: {type:"text"}, + password: {type: "password"}, + global: { type:"boolean"} + }, + + inputs:1, + outputs:0, + icon: "envelope.png", + align: "right", + label: function() { + return this.dname||this.name||"email"; + }, + labelStyle: function() { + return (this.dname||!this.topic)?"node_label_italic":""; + }, + oneditprepare: function() { + if (this.credentials.global) { + $('#node-tip').show(); + } else { + $('#node-tip').hide(); + }; + } + }); +})(); +</script> + + +<script type="text/x-red" data-template-name="e-mail in"> + <div class="form-row node-input-repeat"> + <label for="node-input-repeat"><i class="fa fa-repeat"></i> Check Repeat (S)</label> + <input type="text" id="node-input-repeat" placeholder="300"> + </div> + <div class="form-row"> + <label for="node-input-server"><i class="fa fa-globe"></i> Server</label> + <input type="text" id="node-input-server" placeholder="imap.gmail.com"> + </div> + <div class="form-row"> + <label for="node-input-port"><i class="fa fa-random"></i> Port</label> + <input type="text" id="node-input-port" placeholder="993"> + </div> + <div class="form-row"> + <label for="node-input-userid"><i class="fa fa-user"></i> Userid</label> + <input type="text" id="node-input-userid"> + </div> + <div class="form-row"> + <label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label> + <input type="password" id="node-input-password"> + </div> + <br/> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips" id="node-tip"><b>Note:</b> Copied credentials from global emailkeys.js file.</div> + <div id="node-input-tip" class="form-tips">Tip: <b>ONLY</b> retrieves the single most recent email.</div> +</script> + +<script type="text/x-red" data-help-name="e-mail in"> + <p>Repeatedly gets a <b>single email</b> from an IMAP server and forwards on as a msg if not already seen.</p> + <p>The subject is loaded into <b>msg.topic</b> and <b>msg.payload</b> is the plain text body. + If there is text/html then that is returned in <b>msg.html</b>. <b>msg.from</b> and <b>msg.date</b> are also set if you need them.</p> + <p>Uses the imap module.</p> + <p><b>Note:</b> this node <i>only</i> gets the most recent single email from the inbox, so set the repeat (polling) time appropriately.</p> +</script> + +<script type="text/javascript"> +(function() { + RED.nodes.registerType('e-mail in',{ + category: 'social-input', + color:"#c7e9c0", + defaults: { + repeat: {value:"300",required:true}, + server: {value:"imap.gmail.com",required:true}, + port: {value:"993",required:true}, + name: {value:""} + }, + credentials: { + userid: {type:"text"}, + password: {type: "password"}, + global: { type:"boolean"} + }, + inputs:0, + outputs:1, + icon: "envelope.png", + label: function() { + return this.name||"email"; + }, + labelStyle: function() { + return (this.name||!this.topic)?"node_label_italic":""; + }, + oneditprepare: function() { + if (this.credentials.global) { + $('#node-tip').show(); + } else { + $('#node-tip').hide(); + }; + } + }); +})(); +</script> diff --git a/dgbuilder/core_nodes/social/61-email.js b/dgbuilder/core_nodes/social/61-email.js new file mode 100644 index 00000000..7d0f8cb9 --- /dev/null +++ b/dgbuilder/core_nodes/social/61-email.js @@ -0,0 +1,246 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var nodemailer = require("nodemailer"); + var Imap = require('imap'); + + //console.log(nodemailer.Transport.transports.SMTP.wellKnownHosts); + + try { + var globalkeys = RED.settings.email || require(process.env.NODE_RED_HOME+"/../emailkeys.js"); + } catch(err) { + } + + function EmailNode(n) { + RED.nodes.createNode(this,n); + this.topic = n.topic; + this.name = n.name; + this.outserver = n.server; + this.outport = n.port; + var flag = false; + if (this.credentials && this.credentials.hasOwnProperty("userid")) { + this.userid = this.credentials.userid; + } else { + if (globalkeys) { + this.userid = globalkeys.user; + flag = true; + } else { + this.error("No e-mail userid set"); + } + } + if (this.credentials && this.credentials.hasOwnProperty("password")) { + this.password = this.credentials.password; + } else { + if (globalkeys) { + this.password = globalkeys.pass; + flag = true; + } else { + this.error("No e-mail password set"); + } + } + if (flag) { + RED.nodes.addCredentials(n.id,{userid:this.userid, password:this.password, global:true}); + } + var node = this; + + var smtpTransport = nodemailer.createTransport({ + host: node.outserver, + port: node.outport, + secure: true, + auth: { + user: node.userid, + pass: node.password + } + }); + + this.on("input", function(msg) { + if (smtpTransport) { + node.status({fill:"blue",shape:"dot",text:"sending"}); + var payload = RED.util.ensureString(msg.payload); + smtpTransport.sendMail({ + from: node.userid, // sender address + to: msg.to || node.name, // comma separated list of addressees + subject: msg.topic, // subject line + text: payload // plaintext body + }, function(error, info) { + if (error) { + node.error(error); + node.status({fill:"red",shape:"ring",text:"send failed"}); + } else { + node.log("Message sent: " + info.response); + node.status({}); + } + }); + } + else { node.warn("No Email credentials found. See info panel."); } + }); + } + RED.nodes.registerType("e-mail",EmailNode,{ + credentials: { + userid: {type:"text"}, + password: {type: "password"}, + global: { type:"boolean"} + } + }); + + function EmailInNode(n) { + RED.nodes.createNode(this,n); + this.name = n.name; + this.repeat = n.repeat * 1000 || 300000; + this.inserver = n.server || globalkeys.server || "imap.gmail.com"; + this.inport = n.port || globalkeys.port || "993"; + var flag = false; + + if (this.credentials && this.credentials.hasOwnProperty("userid")) { + this.userid = this.credentials.userid; + } else { + if (globalkeys) { + this.userid = globalkeys.user; + flag = true; + } else { + this.error("No e-mail userid set"); + } + } + if (this.credentials && this.credentials.hasOwnProperty("password")) { + this.password = this.credentials.password; + } else { + if (globalkeys) { + this.password = globalkeys.pass; + flag = true; + } else { + this.error("No e-mail password set"); + } + } + if (flag) { + RED.nodes.addCredentials(n.id,{userid:this.userid, password:this.password, global:true}); + } + + var node = this; + this.interval_id = null; + var oldmail = {}; + + var imap = new Imap({ + user: node.userid, + password: node.password, + host: node.inserver, + port: node.inport, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + connTimeout: node.repeat, + authTimeout: node.repeat + }); + + if (!isNaN(this.repeat) && this.repeat > 0) { + node.log("repeat = "+this.repeat); + this.interval_id = setInterval( function() { + node.emit("input",{}); + }, this.repeat ); + } + + this.on("input", function(msg) { + imap.once('ready', function() { + node.status({fill:"blue",shape:"dot",text:"fetching"}); + var pay = {}; + imap.openBox('INBOX', true, function(err, box) { + if (box.messages.total > 0) { + var f = imap.seq.fetch(box.messages.total + ':*', { bodies: ['HEADER.FIELDS (FROM SUBJECT DATE)','TEXT'] }); + f.on('message', function(msg, seqno) { + node.log('message: #'+ seqno); + var prefix = '(#' + seqno + ') '; + msg.on('body', function(stream, info) { + var buffer = ''; + stream.on('data', function(chunk) { + buffer += chunk.toString('utf8'); + }); + stream.on('end', function() { + if (info.which !== 'TEXT') { + pay.from = Imap.parseHeader(buffer).from[0]; + pay.topic = Imap.parseHeader(buffer).subject[0]; + pay.date = Imap.parseHeader(buffer).date[0]; + } else { + var parts = buffer.split("Content-Type"); + for (var p = 0; p < parts.length; p++) { + if (parts[p].indexOf("text/plain") >= 0) { + pay.payload = parts[p].split("\n").slice(1,-2).join("\n").trim(); + } + if (parts[p].indexOf("text/html") >= 0) { + pay.html = parts[p].split("\n").slice(1,-2).join("\n").trim(); + } + } + //pay.body = buffer; + } + }); + }); + msg.on('end', function() { + //node.log('Finished: '+prefix); + }); + }); + f.on('error', function(err) { + node.warn('fetch error: ' + err); + node.status({fill:"red",shape:"ring",text:"fetch error"}); + }); + f.on('end', function() { + if (JSON.stringify(pay) !== oldmail) { + node.send(pay); + oldmail = JSON.stringify(pay); + node.log('received new email: '+pay.topic); + } + else { node.log('duplicate not sent: '+pay.topic); } + //node.status({fill:"green",shape:"dot",text:"ok"}); + node.status({}); + imap.end(); + }); + } + else { + node.log("you have achieved inbox zero"); + //node.status({fill:"green",shape:"dot",text:"ok"}); + node.status({}); + imap.end(); + } + }); + }); + node.status({fill:"grey",shape:"dot",text:"connecting"}); + imap.connect(); + }); + + imap.on('error', function(err) { + node.log(err); + node.status({fill:"red",shape:"ring",text:"connect error"}); + }); + + this.on("error", function(err) { + node.log("error: ",err); + }); + + this.on("close", function() { + if (this.interval_id != null) { + clearInterval(this.interval_id); + } + if (imap) { imap.destroy(); } + }); + + node.emit("input",{}); + } + RED.nodes.registerType("e-mail in",EmailInNode,{ + credentials: { + userid: {type:"text"}, + password: {type: "password"}, + global: { type:"boolean"} + } + }); +} diff --git a/dgbuilder/core_nodes/social/91-irc.html b/dgbuilder/core_nodes/social/91-irc.html new file mode 100644 index 00000000..11112374 --- /dev/null +++ b/dgbuilder/core_nodes/social/91-irc.html @@ -0,0 +1,206 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="irc in"> + <div class="form-row"> + <label for="node-input-ircserver"><i class="fa fa-globe"></i> IRC Server</label> + <input type="text" id="node-input-ircserver"> + </div> + <div class="form-row"> + <label for="node-input-channel"><i class="fa fa-random"></i> Channel</label> + <input type="text" id="node-input-channel" placeholder="#nodered"> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">The channel to join must start with a # (as per normal irc rules...)<br/> + You may join multiple channels by comma separating a list - #chan1,#chan2,etc.</div> +</script> + +<script type="text/x-red" data-help-name="irc in"> + <p>Connects to a channel on an IRC server.</p> + <p>You may join multiple channels by comma separating a list - #chan1,#chan2,#etc.</p> + <p>Any messages on that channel will appear on the <code>msg.payload</code> at the output, + while <code>msg.topic</code> will contain who it is from. + <code>msg.to</code> contains either the name of the channel or PRIV in the case of a pm.</p> + <p>The second output provides a <code>msg.payload</code> that has any status messages such as joins, parts, kicks etc.</p> + <p>The type of the status message is set as <code>msg.payload.type</code>.</p> + <p>The possible status types are: <br /> + <table border="1" cellpadding="1" cellspacing="1"> + <thead> + <tr> + <th scope="col">Type</th> + <th scope="col">Description</th> + </tr> + </thead> + <tbody> + <tr> + <td>message</td> + <td>message is sent into the channel</td> + </tr> + <tr> + <td>pm</td> + <td>private message to the bot</td> + </tr> + <tr> + <td>join</td> + <td>a user joined the channel (also triggered when the bot joins a channel)</td> + </tr> + <tr> + <td>invite</td> + <td>the bot is being invited to a channel</td> + </tr> + <tr> + <td>part</td> + <td>a user leaves a channel</td> + </tr> + <tr> + <td>quit</td> + <td>a user quits a channel</td> + </tr> + <tr> + <td>kick</td> + <td>a user is kicked from a channel</td> + </tr> + <tr> + <td>names</td> + <td>retrieves the list of users when the bot joins a channel</td> + </tr> + </tbody> +</table> +</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('irc in',{ + category: 'social-input', + defaults: { + name: {value:""}, + ircserver: {type:"irc-server", required:true}, + channel: {value:"",required:true,validate:RED.validators.regex(/^#/)} + }, + color:"Silver", + inputs:0, + outputs:2, + icon: "hash.png", + label: function() { + var ircNode = RED.nodes.node(this.ircserver); + return this.name || (ircNode ? ircNode.label() : "irc"); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + if ((this.ircserver !== undefined) && (this.ircserver !== "")) { + this.channel = this.channel || RED.nodes.node(this.ircserver).channel; + $("#node-input-channel").val(this.channel); + } + else { this.channel = this.channel; } + $("#node-input-channel").val(this.channel); + } + }); +</script> + + +<script type="text/x-red" data-template-name="irc out"> + <div class="form-row"> + <label for="node-input-ircserver"><i class="fa fa-globe"></i> IRC Server</label> + <input type="text" id="node-input-ircserver"> + </div> + <div class="form-row"> + <label for="node-input-channel"><i class="fa fa-random"></i> Channel</label> + <input type="text" id="node-input-channel" placeholder="#nodered"> + </div> + <div class="form-row"> + <label for="node-input-sendObject"><i class="fa fa-arrows"></i> Action</label> + <select type="text" id="node-input-sendObject" style="display: inline-block; vertical-align: middle; width:70%;"> + <option value="pay">Send payload to channel(s)</option> + <option value="true">Use msg.topic to set nickname or channel(s)</option> + <option value="false">Send complete msg object to channel(s)</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips">The channel to join must start with a # (as per normal irc rules...)<br/> + Sending the complete object will stringify the whole msg object before sending.</div> +</script> + +<script type="text/x-red" data-help-name="irc out"> + <p>Sends messages to a channel on an IRC server</p> + <p>You can send just the <code>msg.payload</code>, or the complete <code>msg</code> object to the selected channel, + or you can select to use <code>msg.topic</code> to send the <code>msg.payload</code> to a specific user (private message) or channel.</p> + <p>If multiple output channels are listed (eg. #chan1,#chan2), then the message will be sent to all of them.</p> + <p><b>Note:</b> you can only send to channels you have previously joined so they MUST be specified in the node - even if you then decide to use a subset in msg.topic</p> + <p>You may send RAW commands using <code>msg.raw</code> - This must contain an array of parameters - eg. <pre>["privmsg","#nodered","Hello world"]</pre></p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('irc out',{ + category: 'social-output', + defaults: { + name: {value:""}, + sendObject: {value:"pay", required:true}, + ircserver: {type:"irc-server", required:true}, + channel: {value:"",required:true,validate:RED.validators.regex(/^#/)} + }, + color:"Silver", + inputs:1, + outputs:0, + icon: "hash.png", + align: "right", + label: function() { + return this.name || (this.ircserver ? RED.nodes.node(this.ircserver).label() : "irc"); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + if ((this.ircserver !== undefined) && (this.ircserver !== "")) { + this.channel = this.channel || RED.nodes.node(this.ircserver).channel; + $("#node-input-channel").val(this.channel); + } + else { this.channel = this.channel; } + } + }); +</script> + + +<script type="text/x-red" data-template-name="irc-server"> + <div class="form-row"> + <label for="node-config-input-server"><i class="fa fa-globe"></i> IRC Server</label> + <input type="text" id="node-config-input-server" placeholder="irc.freenode.net"> + </div> + <div class="form-row"> + <label for="node-config-input-nickname"><i class="fa fa-user"></i> Nickname</label> + <input type="text" id="node-config-input-nickname" placeholder="joe123"> + </div> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('irc-server',{ + category: 'config', + defaults: { + server: {value:"",required:true}, + nickname: {value:"",required:true} + }, + label: function() { + return this.server; + } + }); +</script> diff --git a/dgbuilder/core_nodes/social/91-irc.js b/dgbuilder/core_nodes/social/91-irc.js new file mode 100644 index 00000000..c520e44f --- /dev/null +++ b/dgbuilder/core_nodes/social/91-irc.js @@ -0,0 +1,237 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var irc = require("irc"); + + // The Server Definition - this opens (and closes) the connection + function IRCServerNode(n) { + RED.nodes.createNode(this,n); + this.server = n.server; + this.channel = n.channel; + this.nickname = n.nickname; + this.lastseen = 0; + this.ircclient = null; + this.on("close", function() { + if (this.ircclient != null) { + this.ircclient.removeAllListeners(); + this.ircclient.disconnect(); + } + }); + } + RED.nodes.registerType("irc-server",IRCServerNode); + + + // The Input Node + function IrcInNode(n) { + RED.nodes.createNode(this,n); + this.ircserver = n.ircserver; + this.serverConfig = RED.nodes.getNode(this.ircserver); + this.channel = n.channel || this.serverConfig.channel; + var node = this; + if (node.serverConfig.ircclient === null) { + node.log("Connecting to "+node.serverConfig.server); + node.status({fill:"grey",shape:"dot",text:"connecting"}); + node.serverConfig.ircclient = new irc.Client(node.serverConfig.server, node.serverConfig.nickname,{autoConnect:false,retryDelay:20000}); + node.serverConfig.ircclient.setMaxListeners(0); + node.serverConfig.ircclient.addListener('error', function(message) { + node.log(JSON.stringify(message)); + }); + node.serverConfig.ircclient.addListener('netError', function(message) { + node.log(JSON.stringify("NET "+message)); + node.serverConfig.lastseen = Date.now(); + }); + node.serverConfig.ircclient.addListener('connect', function() { + node.serverConfig.lastseen = Date.now(); + }); + node.serverConfig.ircclient.addListener('ping', function(server) { + node.serverConfig.lastseen = Date.now(); + //node.log("PING "+JSON.stringify(server)); + }); + node.recon = setInterval( function() { + //console.log("CHK ",(Date.now()-node.serverConfig.lastseen)/1000); + if ((Date.now()-node.serverConfig.lastseen) > 300000) { // if more than 5 mins since last seen + node.ircclient.send.apply(node.ircclient,["TIME"]); // request time to check link + } + if ((Date.now()-node.serverConfig.lastseen) > 400000) { // If more than 6.5 mins + node.serverConfig.ircclient.disconnect(); + node.serverConfig.ircclient.connect(); + node.log("reconnect"); // then retry + } + node.ircclient.send.apply(node.ircclient,["TIME"]); // request time to check link + }, 60000); // check every 1 min + } + else { node.status({text:""}); } + node.ircclient = node.serverConfig.ircclient; + + node.ircclient.addListener('registered', function(message) { + //node.log(node.ircclient.nick+" ONLINE"); + node.status({fill:"yellow",shape:"dot",text:"connected"}); + node.ircclient.join( node.channel, function(data) { + // node.log(data+" JOINED "+node.channel); + node.status({fill:"green",shape:"dot",text:"joined"}); + }); + }); + node.ircclient.addListener('message', function (from, to, message) { + //node.log(from + ' => ' + to + ' : ' + message); + if (~node.channel.toLowerCase().indexOf(to.toLowerCase())) { + var msg = { "topic":from, "from":from, "to":to, "payload":message }; + node.send([msg,null]); + } + //else { console.log(node.channel,to); } + }); + node.ircclient.addListener('pm', function(from, message) { + //node.log("PM => "+from + ': ' + message); + var msg = { "topic":from, "from":from, "to":"PRIV", "payload":message }; + node.send([msg,null]); + }); + node.ircclient.addListener('join', function(channel, who) { + var msg = { "payload": { "type":"join", "who":who, "channel":channel } }; + node.send([null,msg]); + //node.log(who+' has joined '+channel); + }); + node.ircclient.addListener('invite', function(channel, from, message) { + var msg = { "payload": { "type":"invite", "who":from, "channel":channel, "message":message } }; + node.send([null,msg]); + //node.log(from+' sent invite to '+channel+': '+message); + }); + node.ircclient.addListener('part', function(channel, who, reason) { + var msg = { "payload": { "type":"part", "who":who, "channel":channel, "reason":reason } }; + node.send([null,msg]); + //node.log(who+' has left '+channel+': '+reason); + }); + node.ircclient.addListener('quit', function(nick, reason, channels, message) { + var msg = { "payload": { "type":"quit", "who":nick, "channel":channels, "reason":reason } }; + node.send([null,msg]); + //node.log(nick+' has quit '+channels+': '+reason); + }); + node.ircclient.addListener('kick', function(channel, who, by, reason) { + var msg = { "payload": { "type":"kick", "who":who, "channel":channel, "by":by, "reason":reason } }; + node.send([null,msg]); + //node.log(who+' was kicked from '+channel+' by '+by+': '+reason); + }); + node.ircclient.addListener('names', function (channel, nicks) { + var msg = { "payload": { "type": "names", "channel": channel, "names": nicks} }; + node.send([null, msg]); + }); + node.ircclient.addListener('raw', function (message) { // any message means we are alive + node.serverConfig.lastseen = Date.now(); + }); + node.on("close", function() { + node.ircclient.removeAllListeners(); + if (node.recon) { clearInterval(node.recon); } + }); + } + RED.nodes.registerType("irc in",IrcInNode); + + + // The Output Node + function IrcOutNode(n) { + RED.nodes.createNode(this,n); + this.sendFlag = n.sendObject; + this.ircserver = n.ircserver; + this.serverConfig = RED.nodes.getNode(this.ircserver); + this.channel = n.channel || this.serverConfig.channel; + var node = this; + if (node.serverConfig.ircclient === null) { + node.log("connecting to "+node.serverConfig.server); + node.status({fill:"grey",shape:"dot",text:"connecting"}); + node.serverConfig.ircclient = new irc.Client(node.serverConfig.server, node.serverConfig.nickname,{autoConnect:false,retryDelay:20000}); + node.serverConfig.ircclient.setMaxListeners(0); + node.serverConfig.ircclient.addListener('error', function(message) { + node.log(JSON.stringify(message)); + }); + node.serverConfig.ircclient.addListener('netError', function(message) { + node.log(JSON.stringify("NET "+message)); + node.serverConfig.lastseen = Date.now(); + }); + node.serverConfig.ircclient.addListener('connect', function() { + node.serverConfig.lastseen = Date.now(); + }); + node.serverConfig.ircclient.addListener('ping', function(server) { + node.serverConfig.lastseen = Date.now(); + //node.log("PING "+JSON.stringify(server)); + }); + node.serverConfig.ircclient.addListener('raw', function (message) { // any message received means we are alive + if (message.commandType === "reply") { node.serverConfig.lastseen = Date.now(); } + }); + node.recon = setInterval( function() { + //console.log("CHK ",(Date.now()-node.serverConfig.lastseen)/1000); + if ((Date.now()-node.serverConfig.lastseen) > 300000) { // if more than 5 mins since last seen + node.ircclient.send.apply(node.ircclient,["TIME"]); // request time to check link + } + if ((Date.now()-node.serverConfig.lastseen) > 400000) { // If more than 6.5 mins + node.serverConfig.ircclient.disconnect(); + node.serverConfig.ircclient.connect(); + node.log("reconnect"); // then retry + } + node.ircclient.send.apply(node.ircclient,["TIME"]); // request time to check link + }, 60000); // check every 1 min + node.serverConfig.ircclient.connect(); + } + else { node.status({text:""}); } + node.ircclient = node.serverConfig.ircclient; + + node.ircclient.addListener('registered', function(message) { + node.log(node.ircclient.nick+" ONLINE"); + node.status({fill:"yellow",shape:"dot",text:"connected"}); + node.ircclient.join( node.channel, function(data) { + //node.log(data+" JOINED "+node.channel); + node.status({fill:"green",shape:"dot",text:"joined"}); + }); + }); + + node.on("input", function(msg) { + if (Object.prototype.toString.call( msg.raw ) === '[object Array]') { + node.log("RAW command:"+msg.raw); + node.ircclient.send.apply(node.ircclient,msg.raw); + //var m = msg.raw; + //for (var i = 0; i < 10; i++) { + //if (typeof m[i] !== "string") { m[i] = ""; } + //m[i] = m[i].replace(/"/g, ""); + //} + //node.log("RAW command:"+m); + //node.ircclient.send(m[0],m[1],m[2],m[3],m[4],m[5],m[6],m[7],m[8],m[9]); + } + else { + if (msg._topic) { delete msg._topic; } + var ch = node.channel.split(","); // split on , so we can send to multiple + if (node.sendFlag == "true") { // override channels with msg.topic + if ((msg.hasOwnProperty('topic'))&&(typeof msg.topic === "string")) { + ch = msg.topic.split(","); // split on , so we can send to multiple + } + else { node.warn("msg.topic not set"); } + } + for (var c = 0; c < ch.length; c++) { + if (node.sendFlag == "false") { // send whole message object to each channel + node.ircclient.say(ch[c], JSON.stringify(msg)); + } + else { // send just the payload to each channel + if (typeof msg.payload === "object") { msg.payload = JSON.stringify(msg.payload); } + node.ircclient.say(ch[c], msg.payload); + } + } + } + }); + + node.on("close", function() { + node.ircclient.removeAllListeners(); + if (node.recon) { clearInterval(node.recon); } + }); + } + RED.nodes.registerType("irc out",IrcOutNode); +} diff --git a/dgbuilder/core_nodes/storage/28-tail.html b/dgbuilder/core_nodes/storage/28-tail.html new file mode 100644 index 00000000..c094d064 --- /dev/null +++ b/dgbuilder/core_nodes/storage/28-tail.html @@ -0,0 +1,58 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="tail"> + <div class="form-row node-input-filename"> + <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label> + <input type="text" id="node-input-filename" placeholder="Filename"> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-split" placeholder="Name" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-split" style="width: 70%;">Split lines if we see \n ?</label> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <!-- <div class="form-tips">WON'T work on Windows.</div> --> +</script> + +<script type="text/x-red" data-help-name="tail"> + <p>Tails (watches for things to be added) to the configured file. (Linux/Mac ONLY)</p> + <p>This won't work on Windows filesystems, as it relies on the tail -F command.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('tail',{ + category: 'storage-input', + defaults: { + name: {value:""}, + split: {value:false}, + filename: {value:"",required:true} + }, + color:"BurlyWood", + inputs:0, + outputs:1, + icon: "file.png", + label: function() { + return this.name||this.filename; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); +</script> diff --git a/dgbuilder/core_nodes/storage/28-tail.js b/dgbuilder/core_nodes/storage/28-tail.js new file mode 100644 index 00000000..89c7a639 --- /dev/null +++ b/dgbuilder/core_nodes/storage/28-tail.js @@ -0,0 +1,69 @@ +/** + * Copyright 2013, 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var spawn = require('child_process').spawn; + var plat = require('os').platform(); + + if (plat.match(/^win/)) { + throw "Info : Currently not supported on Windows."; + } + + function TailNode(n) { + RED.nodes.createNode(this,n); + + this.filename = n.filename; + this.split = n.split; + var node = this; + + var err = ""; + // TODO: rewrite to use node-tail + var tail = spawn("tail", ["-F", "-n", "0", this.filename]); + tail.stdout.on("data", function (data) { + if (node.split) { + // TODO: allow customisation of the line break - as we do elsewhere + var strings = data.toString().split("\n"); + for (var s in strings) { + //TODO: should we really filter blanks? Is that expected? + if (strings[s] !== "") { + node.send({ + topic: node.filename, + payload: strings[s] + }); + } + } + } + else { + var msg = { + topic:node.filename, + payload: data.toString() + }; + node.send(msg); + } + }); + + tail.stderr.on("data", function(data) { + node.warn(data.toString()); + }); + + this.on("close", function() { + if (tail) { tail.kill(); } + }); + } + + RED.nodes.registerType("tail",TailNode); +} diff --git a/dgbuilder/core_nodes/storage/50-file.html b/dgbuilder/core_nodes/storage/50-file.html new file mode 100644 index 00000000..5113a17d --- /dev/null +++ b/dgbuilder/core_nodes/storage/50-file.html @@ -0,0 +1,110 @@ +<!-- + Copyright 2013, 2014 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. +--> + +<script type="text/x-red" data-template-name="file"> + <div class="form-row node-input-filename"> + <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label> + <input type="text" id="node-input-filename" placeholder="Filename"> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-appendNewline" placeholder="Name" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-appendNewline" style="width: 70%;">Append newline ?</label> + </div> + <div class="form-row"> + <label> </label> + <input type="checkbox" id="node-input-overwriteFile" placeholder="Name" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-overwriteFile" style="width: 70%;">Overwrite complete file ?</label> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="file"> + <p>Writes <b>msg.payload</b> to the file specified, e.g. to create a log.</p> + <p>The filename can be overridden by the <b>msg.filename</b> property of the incoming message.</p> + <p>A newline is added to every message. But this can be turned off if required, for example, to allow binary files to be written.</p> + <p>The default behaviour is to append to the file. This can be changed to overwrite the file each time, for example if you want to output a "static" web page or report.</p> + <p>If a <b>msg.delete</b> property exists then the file will be deleted instead.</p> +</script> + +<script type="text/x-red" data-template-name="file in"> + <div class="form-row"> + <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label> + <input type="text" id="node-input-filename" placeholder="Filename"> + </div> + <div class="form-row"> + <label for="node-input-format"><i class="fa fa-sign-out"></i> Output as</label> + <select id="node-input-format"> + <option value="utf8">a utf8 string</option> + <option value="">a Buffer</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/x-red" data-help-name="file in"> + <p>Reads the specified file and sends the content as <b>msg.payload</b>, and the filename as <b>msg.filename</b>.</p> + <p>The filename can be overridden by the <b>msg.filename</b> property of the incoming message.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('file',{ + category: 'storage-output', + defaults: { + name: {value:""}, + filename: {value:""}, + appendNewline: {value:true}, + overwriteFile: {value:false} + }, + color:"BurlyWood", + inputs:1, + outputs:0, + icon: "file.png", + align: "right", + label: function() { + return this.name||this.filename; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); + + RED.nodes.registerType('file in',{ + category: 'storage-input', + defaults: { + name: {value:""}, + filename: {value:""}, + format: {value:"utf8"}, + }, + color:"BurlyWood", + inputs:1, + outputs:1, + icon: "file.png", + label: function() { + return this.name||this.filename; + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + } + }); + +</script> diff --git a/dgbuilder/core_nodes/storage/50-file.js b/dgbuilder/core_nodes/storage/50-file.js new file mode 100644 index 00000000..d6cc4410 --- /dev/null +++ b/dgbuilder/core_nodes/storage/50-file.js @@ -0,0 +1,93 @@ +/** + * Copyright 2013, 2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var fs = require("fs"); + + function FileNode(n) { + RED.nodes.createNode(this,n); + this.filename = n.filename || ""; + this.appendNewline = n.appendNewline; + this.overwriteFile = n.overwriteFile; + var node = this; + this.on("input",function(msg) { + var filename = msg.filename || this.filename; + if (filename === "") { + node.warn('No filename specified'); + } else if (msg.hasOwnProperty('delete')) { + fs.unlink(filename, function (err) { + if (err) { node.warn('Failed to delete file : '+err); } + //console.log('Deleted file",filename); + }); + } else if (typeof msg.payload != "undefined") { + var data = msg.payload; + if (typeof data === "object") { + if (!Buffer.isBuffer(data)) { + data = JSON.stringify(data); + } + } + if (typeof data === "boolean") { data = data.toString(); } + if ((this.appendNewline)&&(!Buffer.isBuffer(data))) { data += "\n"; } + if (this.overwriteFile) { + // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while + fs.writeFile(filename, data, "binary", function (err) { + if (err) { node.warn('Failed to write to file : '+err); } + //console.log('Message written to file',filename); + }); + } + else { + // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while + fs.appendFile(filename, data, "binary", function (err) { + if (err) { node.warn('Failed to append to file : '+err); } + //console.log('Message appended to file',filename); + }); + } + } + }); + } + RED.nodes.registerType("file",FileNode); + + function FileInNode(n) { + RED.nodes.createNode(this,n); + + this.filename = n.filename || ""; + this.format = n.format; + var node = this; + var options = {}; + if (this.format) { + options['encoding'] = this.format; + } + this.on("input",function(msg) { + var filename = msg.filename || this.filename; + if (filename === "") { + node.warn('No filename specified'); + } else { + fs.readFile(filename,options,function(err,data) { + if (err) { + node.warn(err); + msg.error = err; + } else { + msg.filename = filename; + msg.payload = data; + } + node.send(msg); + }); + } + }); + } + RED.nodes.registerType("file in",FileInNode); +} diff --git a/dgbuilder/core_nodes/storage/65-redisout.html b/dgbuilder/core_nodes/storage/65-redisout.html new file mode 100644 index 00000000..9000dfd6 --- /dev/null +++ b/dgbuilder/core_nodes/storage/65-redisout.html @@ -0,0 +1,105 @@ +<!-- + 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. +--> + +<script type="text/x-red" data-template-name="redis out"> + <div class="form-row node-input-hostname"> + <label for="node-input-hostname"><i class="fa fa-bookmark"></i> Host</label> + <input class="input-append-left" type="text" id="node-input-hostname" placeholder="127.0.0.1" style="width: 40%;" ><button id="node-input-hostname-lookup" class="btn input-append-right"><span class="caret"></span></button> + <label for="node-input-port" style="margin-left: 10px; width: 35px; "> Port</label> + <input type="text" id="node-input-port" placeholder="6379" style="width:45px"> + </div> + <div class="form-row"> + <label for="node-input-key"><i class="fa fa-key"></i> Key</label> + <input type="text" id="node-input-key" placeholder="Redis Key"> + </div> + <div class="form-row"> + <label for="node-input-type"><i class="fa fa-th"></i> Type</label> + <select type="text" id="node-input-structtype" style="width: 150px;"> + <option value="string">String</option> + <option value="hash">Hash</option> + <option value="set">Set</option> + <option value="list">List</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips"> + If key is blank, the topic will be used as the key.<br> + If type is hash, payload should be field=value. + </div> +</script> + +<script type="text/x-red" data-help-name="redis out"> + <p>A Redis output node. Options include Hash, Set, List and String.</p> + <p>To run this you need a local Redis server running. For details see <a href="http://redis.io/" target="_new">the Redis site</a>.</p> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('redis out',{ + category: 'storage-output', + color:"#ffaaaa", + defaults: { + hostname: { value:"127.0.0.1",required:true}, + port: { value: 6379,required:true}, + name: {value:""}, + key: {value:""}, + structtype: {value:"",required:true} + }, + inputs:1, + outputs:0, + icon: "redis.png", + align: "right", + label: function() { + return this.name||this.key+" ("+this.structtype+")"; + }, + oneditprepare: function() { + var availableServers = []; + var matchedServers = {}; + RED.nodes.eachNode(function(node) { + if (node.type == "redis out" && node.hostname && node.port && !matchedServers[node.hostname+":"+node.port]) { + var label = node.hostname+":"+node.port; + matchedServers[label] = true; + availableServers.push({ + label:label, + value:node.hostname, + port:node.port + }); + } + }); + $( "#node-input-hostname" ).autocomplete({ + minLength: 0, + source: availableServers, + select: function( event, ui ) { + $("#node-input-port").val(ui.item.port); + } + }); + var tt = this; + tt._acOpen = false; + $( "#node-input-hostname" ).on( "autocompleteclose", function( event, ui ) { tt._acOpen = false;} ); + $( "#node-input-hostname-lookup" ).click(function(e) { + if (tt._acOpen) { + $( "#node-input-hostname" ).autocomplete( "close"); + } else { + $( "#node-input-hostname" ).autocomplete( "search", "" ); + } + tt._acOpen = !tt._acOpen; + e.preventDefault(); + }); + } + }); +</script> diff --git a/dgbuilder/core_nodes/storage/65-redisout.js b/dgbuilder/core_nodes/storage/65-redisout.js new file mode 100644 index 00000000..907e2a55 --- /dev/null +++ b/dgbuilder/core_nodes/storage/65-redisout.js @@ -0,0 +1,107 @@ +/** + * 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. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var redis = require("redis"); + + var hashFieldRE = /^([^=]+)=(.*)$/; + + var redisConnectionPool = function() { + var connections = {}; + var obj = { + get: function(host,port) { + var id = host+":"+port; + if (!connections[id]) { + connections[id] = redis.createClient(port,host); + connections[id].on("error",function(err) { + util.log("[redis] "+err); + }); + connections[id].on("connect",function() { + util.log("[redis] connected to "+host+":"+port); + }); + connections[id]._id = id; + connections[id]._nodeCount = 0; + } + connections[id]._nodeCount += 1; + return connections[id]; + }, + close: function(connection) { + connection._nodeCount -= 1; + if (connection._nodeCount === 0) { + if (connection) { + clearTimeout(connection.retry_timer); + connection.end(); + } + delete connections[connection._id]; + } + } + }; + return obj; + }(); + + + function RedisOutNode(n) { + RED.nodes.createNode(this,n); + this.port = n.port||"6379"; + this.hostname = n.hostname||"127.0.0.1"; + this.key = n.key; + this.structtype = n.structtype; + + this.client = redisConnectionPool.get(this.hostname,this.port); + + if (this.client.connected) { + this.status({fill:"green",shape:"dot",text:"connected"}); + } else { + this.status({fill:"red",shape:"ring",text:"disconnected"},true); + } + + var node = this; + this.client.on("end", function() { + node.status({fill:"red",shape:"ring",text:"disconnected"}); + }); + this.client.on("connect", function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + + this.on("input", function(msg) { + var k = this.key || msg.topic; + if (k) { + if (this.structtype == "string") { + this.client.set(k,RED.util.ensureString(msg.payload)); + } else if (this.structtype == "hash") { + var r = hashFieldRE.exec(msg.payload); + if (r) { + this.client.hset(k,r[1],r[2]); + } else { + this.warn("Invalid payload for redis hash"); + } + } else if (this.structtype == "set") { + this.client.sadd(k,msg.payload); + } else if (this.structtype == "list") { + this.client.rpush(k,msg.payload); + } + } else { + this.warn("No key or topic set"); + } + }); + this.on("close", function() { + redisConnectionPool.close(node.client); + }); + } + RED.nodes.registerType("redis out",RedisOutNode); +} diff --git a/dgbuilder/core_nodes/storage/66-mongodb.html b/dgbuilder/core_nodes/storage/66-mongodb.html new file mode 100644 index 00000000..81c56389 --- /dev/null +++ b/dgbuilder/core_nodes/storage/66-mongodb.html @@ -0,0 +1,231 @@ +<!-- + Copyright 2013,2014 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. +--> + +<script type="text/x-red" data-template-name="mongodb"> + <div class="form-row"> + <label for="node-config-input-hostname"><i class="fa fa-bookmark"></i> Host</label> + <input class="input-append-left" type="text" id="node-config-input-hostname" placeholder="localhost" style="width: 40%;" > + <label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> Port</label> + <input type="text" id="node-config-input-port" placeholder="27017" style="width:45px"> + </div> + <div class="form-row"> + <label for="node-config-input-db"><i class="fa fa-database"></i> Database</label> + <input type="text" id="node-config-input-db" placeholder="test"> + </div> + <div class="form-row"> + <label for="node-config-input-user"><i class="fa fa-user"></i> Username</label> + <input type="text" id="node-config-input-user"> + </div> + <div class="form-row"> + <label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label> + <input type="password" id="node-config-input-password"> + </div> + <div class="form-row"> + <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-config-input-name" placeholder="Name"> + </div> +</script> + +<script type="text/javascript"> + RED.nodes.registerType('mongodb', { + category: 'config', + color: "rgb(218, 196, 180)", + defaults: { + hostname: {value: "127.0.0.1", required: true}, + port: {value: 27017, required: true}, + db: {value: "", required: true}, + name: {value: ""} + }, + credentials: { + user: {type: "text"}, + password: {type: "password"} + }, + label: function() { + return this.name || this.hostname + ":" + this.port + "/" + this.db; + } + }); +</script> + + +<script type="text/x-red" data-template-name="mongodb out"> + <div class="form-row"> + <label for="node-input-mongodb"><i class="fa fa-bookmark"></i> Server</label> + <input type="text" id="node-input-mongodb"> + </div> + <div class="form-row"> + <label for="node-input-collection"><i class="fa fa-briefcase"></i> Collection</label> + <input type="text" id="node-input-collection" placeholder="collection"> + </div> + <div class="form-row"> + <label for="node-input-operation"><i class="fa fa-wrench"></i> Operation</label> + <select type="text" id="node-input-operation" style="display: inline-block; vertical-align: top;"> + <option value="store">save</option> + <option value="insert">insert</option> + <option value="update">update</option> + <option value="delete">remove</option> + </select> + </div> + <div class="form-row node-input-payonly"> + <label> </label> + <input type="checkbox" id="node-input-payonly" placeholder="Only" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-payonly" style="width: 70%;">Only store msg.payload object</label> + </div> + <div class="form-row node-input-upsert"> + <label> </label> + <input type="checkbox" id="node-input-upsert" placeholder="Only" style="display: inline-block; width: auto; vertical-align: top;"> + <label for="node-input-upsert" style="width: 70%;">Create a new document if no match found</label> + </div> + <div class="form-row node-input-multi"> + <label> </label> + <input type="checkbox" id="node-input-multi" placeholder="Only" style="display: inline-block; width: auto; vertical-align: top;;"> + <label for="node-input-multi" style="width: 70%;">Update all matching documents</label> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips" id="node-warning" style="display: none"><b> Tip:</b> If no collection is set, ensure <b>msg.collection</b> will contain the collection name + </div> +</script> + +<script type="text/x-red" data-help-name="mongodb out"> + <p>A simple MongoDB output node. Can save, insert, update and remove objects from a chosen collection.</p> + <p>Save will update an existing object or insert a new object if one does not already exist.</p> + <p>Insert will insert a new object.</p> + <p>Save and insert either store <b>msg</b> or <b>msg.payload</b>.</p> + <p>Update will modify an existing object or objects. The query to select objects to update uses <b>msg.query</b> and the update to the element uses <b>msg.payload</b>.</p> + <p>Update can add a object if it does not exist or update multiple objects.</p> + <p>Remove will remove objects that match the query passed in on <b>msg.payload</b>. A blank query will delete <i>all of the objects</i> in the collection.</p> + <p>You can either set the collection method in the node config or on <b>msg.collection</b>. Setting it in the node will override <b>msg.collection</b>.</p> + <p>By default MongoDB creates an <i>_id</i> property as the primary key - so repeated injections of the same <b>msg</b> will result in many database entries.</p> + <p>If this is NOT the desired behaviour - ie. you want repeated entries to overwrite, then you must set the <b>msg._id</b> property to be a constant by the use of a previous function node.</p> + <p>This could be a unique constant or you could create one based on some other msg property.</p> + <p>Currently we do not limit or cap the collection size at all... this may well change.</p> +</script> + +<script type="text/javascript"> + + function oneditprepare() { + $("#node-input-operation").change(function () { + var id = $("#node-input-operation option:selected").val(); + + if (id === "update") { + $(".node-input-payonly").hide(); + $(".node-input-upsert, .node-input-multi").show(); + } else if (id === "delete") { + $(".node-input-payonly, .node-input-upsert, .node-input-multi").hide(); + } else { + $(".node-input-payonly").show(); + $(".node-input-upsert, .node-input-multi").hide(); + } + }); + + $("#node-input-collection").change(function () { + if($("#node-input-collection").val() === "") { + $("#node-warning").show(); + } else { + $("#node-warning").hide(); + } + }); + } + + RED.nodes.registerType('mongodb out', { + category: 'storage-output', + color: "rgb(218, 196, 180)", + defaults: { + mongodb: {type: "mongodb", required: true}, + name: {value: ""}, + collection: {value: ""}, + payonly: {value: false}, + upsert: {value: false}, + multi: {value: false}, + operation: {value: "store"} + }, + inputs: 1, + outputs: 0, + icon: "mongodb.png", + align: "right", + label: function() { + var mongoNode = RED.nodes.node(this.mongodb); + return this.name || (mongoNode ? mongoNode.label() + " " + this.collection: "mongodb"); + }, + labelStyle: function() { + return this.name ? "node_label_italic" : ""; + }, + oneditprepare: oneditprepare + }); +</script> + + +<script type="text/x-red" data-template-name="mongodb in"> + <div class="form-row"> + <label for="node-input-mongodb"><i class="fa fa-bookmark"></i> Server</label> + <input type="text" id="node-input-mongodb"> + </div> + <div class="form-row"> + <label for="node-input-collection"><i class="fa fa-briefcase"></i> Collection</label> + <input type="text" id="node-input-collection" placeholder="collection"> + </div> + <div class="form-row"> + <label for="node-input-operation"><i class="fa fa-wrench"></i> Operation</label> + <select type="text" id="node-input-operation" style="display: inline-block; vertical-align: top;"> + <option value="find">find</option> + <option value="count">count</option> + <option value="aggregate">aggregate</option> + </select> + </div> + <div class="form-row"> + <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> + <input type="text" id="node-input-name" placeholder="Name"> + </div> + <div class="form-tips" id="node-warning" style="display: none"><b> Tip:</b> If no collection is set, ensure <b>msg.collection</b> will contain the collection name + </div> +</script> + +<script type="text/x-red" data-help-name="mongodb in"> + <p>Calls a MongoDB collection method based on the selected operator.</p> + <p>Find queries a collection using the <b>msg.payload</b> as the query statement as per the .find() function. Optionally, you may also (via a function) set a <b>msg.projection</b> object to constrain the returned fields, a <b>msg.sort</b> object and a <b>msg.limit</b> object.</p> + <p>Count returns a count of the number of documents in a collection or matching a query using the <b>msg.payload</b> as the query statement.</p> + <p>Aggregate provides access to the aggregation pipeline using the <b>msg.payload</b> as the pipeline array.</p> + <p>You can override the collection the method is performed on by setting <b>msg.collection</b> to the desired collection name.</p> + <p>See the <a href="http://docs.mongodb.org/manual/reference/method/db.collection.find/" target="new"><i>MongoDB collection methods docs</i></a> for examples.</p> + <p>The result is returned in <b>msg.payload</b>.</p> +</script> + +<script type="text/javascript"> + + RED.nodes.registerType('mongodb in', { + category: 'storage-input', + color: "rgb(218, 196, 180)", + defaults: { + mongodb: {type: "mongodb", required: true}, + name: {value: ""}, + collection: {value: ""}, + operation: {value: "find"} + }, + inputs: 1, + outputs: 1, + icon: "mongodb.png", + label: function() { + var mongoNode = RED.nodes.node(this.mongodb); + return this.name || (mongoNode ? mongoNode.label() + " " + this.collection: "mongodb"); + }, + labelStyle: function() { + return this.name ? "node_label_italic" : ""; + }, + oneditprepare: oneditprepare + }); +</script> diff --git a/dgbuilder/core_nodes/storage/66-mongodb.js b/dgbuilder/core_nodes/storage/66-mongodb.js new file mode 100644 index 00000000..3a71407c --- /dev/null +++ b/dgbuilder/core_nodes/storage/66-mongodb.js @@ -0,0 +1,233 @@ +/** + * Copyright 2013,2014 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. + **/ + +module.exports = function(RED) { + "use strict"; + var mongo = require('mongodb'); + var MongoClient = mongo.MongoClient; + + function MongoNode(n) { + RED.nodes.createNode(this,n); + this.hostname = n.hostname; + this.port = n.port; + this.db = n.db; + this.name = n.name; + + var url = "mongodb://"; + if (this.credentials && this.credentials.user && this.credentials.password) { + url += this.credentials.user+":"+this.credentials.password+"@"; + } + url += this.hostname+":"+this.port+"/"+this.db; + + this.url = url; + } + + RED.nodes.registerType("mongodb",MongoNode,{ + credentials: { + user: {type:"text"}, + password: {type: "password"} + } + }); + + function ensureValidSelectorObject(selector) { + if (selector != null && (typeof selector != 'object' || Buffer.isBuffer(selector))) { + return {}; + } + return selector; + } + + + function MongoOutNode(n) { + RED.nodes.createNode(this,n); + this.collection = n.collection; + this.mongodb = n.mongodb; + this.payonly = n.payonly || false; + this.upsert = n.upsert || false; + this.multi = n.multi || false; + this.operation = n.operation; + this.mongoConfig = RED.nodes.getNode(this.mongodb); + + if (this.mongoConfig) { + var node = this; + MongoClient.connect(this.mongoConfig.url, function(err, db) { + if (err) { + node.error(err); + } else { + node.clientDb = db; + var coll; + if (node.collection) { + coll = db.collection(node.collection); + } + node.on("input",function(msg) { + if (!coll) { + if (msg.collection) { + coll = db.collection(msg.collection); + } else { + node.error("No collection defined"); + return; + } + } + delete msg._topic; + delete msg.collection; + if (node.operation === "store") { + if (node.payonly) { + if (typeof msg.payload !== "object") { + msg.payload = {"payload": msg.payload}; + } + coll.save(msg.payload,function(err, item) { + if (err) { + node.error(err); + } + }); + } else { + coll.save(msg,function(err, item) { + if (err) { + node.error(err); + } + }); + } + } else if (node.operation === "insert") { + if (node.payonly) { + if (typeof msg.payload !== "object") { + msg.payload = {"payload": msg.payload}; + } + coll.insert(msg.payload, function(err, item) { + if (err) { + node.error(err); + } + }); + } else { + coll.insert(msg, function(err,item) { + if (err) { + node.error(err); + } + }); + } + } else if (node.operation === "update") { + if (typeof msg.payload !== "object") { + msg.payload = {"payload": msg.payload}; + } + var query = msg.query || {}; + var payload = msg.payload || {}; + var options = { + upsert: node.upsert, + multi: node.multi + }; + + coll.update(query, payload, options, function(err, item) { + if (err) { + node.error(err + " " + payload); + } + }); + } else if (node.operation === "delete") { + coll.remove(msg.payload, function(err, items) { + if (err) { + node.error(err); + } + }); + } + }); + } + }); + } else { + this.error("missing mongodb configuration"); + } + + this.on("close", function() { + if (this.clientDb) { + this.clientDb.close(); + } + }); + } + RED.nodes.registerType("mongodb out",MongoOutNode); + + function MongoInNode(n) { + RED.nodes.createNode(this,n); + this.collection = n.collection; + this.mongodb = n.mongodb; + this.operation = n.operation || "find"; + this.mongoConfig = RED.nodes.getNode(this.mongodb); + + if (this.mongoConfig) { + var node = this; + MongoClient.connect(this.mongoConfig.url, function(err,db) { + if (err) { + node.error(err); + } else { + node.clientDb = db; + var coll; + if (node.collection) { + coll = db.collection(node.collection); + } + node.on("input", function(msg) { + if (!coll) { + if (msg.collection) { + coll = db.collection(msg.collection); + } else { + node.error("No collection defined"); + return; + } + } + if (node.operation === "find") { + msg.projection = msg.projection || {}; + var selector = ensureValidSelectorObject(msg.payload); + coll.find(selector,msg.projection).sort(msg.sort).limit(msg.limit).toArray(function(err, items) { + if (err) { + node.error(err); + } else { + msg.payload = items; + delete msg.projection; + delete msg.sort; + delete msg.limit; + node.send(msg); + } + }); + } else if (node.operation === "count") { + var selector = ensureValidSelectorObject(msg.payload); + coll.count(selector, function(err, count) { + if (err) { + node.error(err); + } else { + msg.payload = count; + node.send(msg); + } + }); + } else if (node.operation === "aggregate") { + msg.payload = (msg.payload instanceof Array) ? msg.payload : []; + coll.aggregate(msg.payload, function(err, result) { + if (err) { + node.error(err); + } else { + msg.payload = result; + node.send(msg); + } + }); + } + }); + } + }); + } else { + this.error("missing mongodb configuration"); + } + + this.on("close", function() { + if (this.clientDb) { + this.clientDb.close(); + } + }); + } + RED.nodes.registerType("mongodb in",MongoInNode); +} |