merge and refactor error handling in nodes

This commit is contained in:
Andreas Martens 2021-01-29 16:09:17 +00:00
commit 399733c645
68 changed files with 1629 additions and 676 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ puball.sh
setenv.sh
/.project
package-lock.json
social/xmpp/92-xmpp.old
*.tgz

View File

@ -19,4 +19,6 @@ If set to return an integer it can include both the low and high values.
If set to return a floating point value it will be from the low value, up to, but
**not** including the high value. `min <= n < max` - so selecting 1 to 6 will return values 1 <= n < 6 .
You can dynamically pass in the 'From' and 'To' values to the node using msg.to and/or msg.from. **NOTE:** hard coded values in the node **always take precedence**.
**Note:** This returns numbers - objects of type **number**.

View File

@ -1,7 +1,16 @@
<script type="text/html" data-help-name="random">
<p>Generates a random number between a low and high value.</p>
<p>Generates a random number between a low and high value. Defaults to 1 to 10.</p>
<p>If left blank <code>from</code> and <code>to</code> can be set dynamically as below.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>from <span class="property-type">number</span></dt>
<dd>containing the low value to be used.</dd>
<dt>to <span class="property-type">number</span></dt>
<dd>containing the high value to be used.</dd>
</dl>
<h3>Details</h3>
<p>If set to return an integer it can <i>include</i> both the low and high values.
<code>min <= n <= max</code></p>
<code>min <= n <= max</code></p>
<p>If set to return a floating point value it will be from the low value, up to, but
not including the high value. <code>min <= n < max</code></p>
not including the high value. <code>min <= n < max</code></p>
</script>

View File

@ -1,7 +1,16 @@
<script type="text/html" data-help-name="random">
<p>最小値と最大値との間の乱数を生成します。</p>
<p>最小値と最大値との間の乱数を生成します。デフォルトは1から10です。</p>
<p><code>最小</code><code>最大</code> を空にした場合は、以下の様に動的に値を設定できます。</p>
<h3>入力</h3>
<dl class="message-properties">
<dt>from <span class="property-type">数値</span></dt>
<dd>使用する最小値を含みます。</dd>
<dt>to <span class="property-type">数値</span></dt>
<dd>使用する最大値を含みます。</dd>
</dl>
<h3>詳細</h3>
<p>整数値を返すように設定した場合は、乱数には最大値と最小値の両方が<i>含まれます</i>
<code>min <= n <= max</code></p>
<code>min <= n <= max</code></p>
<p>浮動小数点値を返すように設定した場合、乱数は最小値から最大値未満の値を含み、最大値は含まれません。
<code>min <= n < max</code></p>
<code>min <= n < max</code></p>
</script>

View File

@ -1,6 +1,6 @@
{
"name" : "node-red-node-random",
"version" : "0.2.0",
"version" : "0.3.1",
"description" : "A Node-RED node that when triggered generates a random number between two values.",
"dependencies" : {
},
@ -19,5 +19,8 @@
"name": "Dave Conway-Jones",
"email": "ceejay@vnet.ibm.com",
"url": "http://nodered.org"
}
},
"contributors": [
{"name": "@zenofmud"}
]
}

View File

@ -31,8 +31,8 @@
color:"#E2D96E",
defaults: {
name: {value:""},
low: {value:"1"},
high: {value:"10"},
low: {value: 1,validate:function(v) { return !isNaN(v) || v.length === 0;} },
high: {value: 10,validate:function(v) { return !isNaN(v) || v.length === 0;} },
inte: {value:"true"},
property: {value:"payload",required:true}
},

View File

@ -3,21 +3,73 @@ module.exports = function(RED) {
"use strict";
function RandomNode(n) {
RED.nodes.createNode(this,n);
this.low = Number(n.low || 1);
this.high = Number(n.high || 10);
this.low = n.low
this.high = n.high
this.inte = n.inte || false;
this.property = n.property||"payload";
var node = this;
var tmp = {};
this.on("input", function(msg) {
var value;
if (node.inte == "true" || node.inte === true) {
value = Math.round(Math.random() * (node.high - node.low + 1) + node.low - 0.5);
tmp.low = 1 // set this as the default low value
tmp.low_e = ""
if (node.low) { // if the the node has a value use it
tmp.low = Number(node.low);
} else if ('from' in msg) { // else see if a 'from' is in the msg
if (Number(msg.from)) { // if it is, and is a number, use it
tmp.low = Number(msg.from);
} else { // otherwise setup NaN error
tmp.low = NaN;
tmp.low_e = " From: " + msg.from; // setup to show bad incoming msg.from
}
}
else {
value = Math.random() * (node.high - node.low) + node.low;
tmp.high = 10 // set this as the default high value
tmp.high_e = "";
if (node.high) { // if the the node has a value use it
tmp.high = Number(node.high);
} else if ('to' in msg) { // else see if a 'to' is in the msg
if (Number(msg.to)) { // if it is, and is a number, use it
tmp.high = Number(msg.to);
} else { // otherwise setup NaN error
tmp.high = NaN
tmp.high_e = " To: " + msg.to // setup to show bad incoming msg.to
}
}
// if tmp.low or high are not numbers, send an error msg with bad values
if ( (isNaN(tmp.low)) || (isNaN(tmp.high)) ) {
this.error("Random: one of the input values is not a number. " + tmp.low_e + tmp.high_e);
} else {
// at this point we have valid values so now to generate the random number!
// flip the values if low > high so random will work
var value = 0;
if (tmp.low > tmp.high) {
value = tmp.low
tmp.low = tmp.high
tmp.high = value
}
// if returning an integer, do a math.ceil() on the low value and a
// Math.floor()high value before generate the random number. This must be
// done to insure the rounding doesn't round up if using something like 4.7
// which would end up with 5
if ( (node.inte == "true") || (node.inte === true) ) {
tmp.low = Math.ceil(tmp.low);
tmp.high = Math.floor(tmp.high);
// use this to round integers
value = Math.round(Math.random() * (tmp.high - tmp.low + 1) + tmp.low - 0.5);
} else {
// use this to round floats
value = (Math.random() * (tmp.high - tmp.low)) + tmp.low;
}
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
});
}
RED.nodes.registerType("random",RandomNode);

View File

@ -3,7 +3,7 @@
"version" : "0.3.1",
"description" : "A Node-RED node to talk to an Arduino running firmata",
"dependencies" : {
"firmata" : "^2.0.0"
"firmata" : "^2.3.0"
},
"repository" : {
"type":"git",

View File

@ -4,7 +4,6 @@ module.exports = function(RED) {
var execSync = require('child_process').execSync;
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var fs = require('fs');
var testCommand = __dirname+'/testgpio.py'
var gpioCommand = __dirname+'/nrgpio';
@ -271,16 +270,21 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
var node = this;
if (allOK === true) {
var doConnect = function() {
node.child = spawn(gpioCommand+".py", ["kbd","0"]);
node.status({fill:"green",shape:"dot",text:"rpi-gpio.status.ok"});
node.child.stdout.on('data', function (data) {
var b = data.toString().trim().split(",");
var act = "up";
if (b[1] === "1") { act = "down"; }
if (b[1] === "2") { act = "repeat"; }
node.send({ topic:"pi/key", payload:Number(b[0]), action:act });
var d = data.toString().trim().split("\n");
for (var i = 0; i < d.length; i++) {
if (d[i] !== '') {
var b = d[i].trim().split(",");
var act = "up";
if (b[1] === "1") { act = "down"; }
if (b[1] === "2") { act = "repeat"; }
node.send({ topic:"pi/key", payload:Number(b[0]), action:act });
}
}
});
node.child.stderr.on('data', function (data) {
@ -295,7 +299,10 @@ module.exports = function(RED) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
node.finished();
}
else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); }
else {
node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"});
setTimeout(function() { doConnect(); },2000)
}
});
node.child.on('error', function (err) {
@ -303,6 +310,10 @@ module.exports = function(RED) {
else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); }
else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); }
});
}
if (allOK === true) {
doConnect();
node.on("close", function(done) {
node.status({});

View File

@ -1,6 +1,6 @@
{
"name": "node-red-node-pi-gpio",
"version": "1.2.0",
"version": "1.2.3",
"description": "The basic Node-RED node for Pi GPIO",
"dependencies" : {
},

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-blink1",
"version" : "0.0.17",
"version" : "0.0.18",
"description" : "A Node-RED node to control a Thingm Blink(1)",
"dependencies" : {
"node-blink1" : "0.2.2"
"node-blink1" : "0.5.1"
},
"repository" : {
"type":"git",

View File

@ -1,5 +1,5 @@
<script type="text/x-red" data-template-name="blinkstick">
<script type="text/html" data-template-name="blinkstick">
<div class="form-row">
<div class="form-row" id="node-input-row-payload">
<label for="node-input-serial">Serial</label>
@ -55,7 +55,7 @@
<div class="form-tips">Expects a msg.payload with either hex "#rrggbb" or decimal "red,green,blue" string, or HTML color name.</div>
</script>
<script type="text/x-red" data-help-name="blinkstick">
<script type="text/html" data-help-name="blinkstick">
<p><i><a href="http://www.blinkstick.com" target="_new">BlinkStick</a></i> output node. Expects a <code>msg.payload</code> with one of:</p>
<ul>
<li>A hex string <b>"#rrggbb"</b> triple</li>

View File

@ -0,0 +1,50 @@
<!--
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/html" data-template-name="blinkstick">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-tips">Expects a msg.payload with either hex #rrggbb or decimal red,green,blue.</div>
</script>
<script type="text/html" data-help-name="blinkstick">
<p>BlinkStick output node. Expects a <b>msg.payload</b> with either a hex string #rrggbb triple or red,green,blue as three 0-255 values.
It can also accept <i><a href="http://www.w3schools.com/html/html_colornames.asp" target="_new">standard HTML colour</a></i> names</p>
<p><b>NOTE:</b> currently only works with a single BlinkStick. (As it uses the findFirst() function to attach).</p>
<p>For more info see the <i><a href="http://blinkstick.com/" target="_new">BlinkStick website</a></i> or the <i><a href="https://github.com/arvydas/blinkstick-node" target="_new">node module</a></i> documentation.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('blinkstick',{
category: 'output',
color:"GoldenRod",
defaults: {
name: {value:""}
},
inputs:1,
outputs:0,
icon: "light.png",
align: "right",
label: function() {
return this.name||"blinkstick";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

View File

@ -0,0 +1,64 @@
/**
* 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 blinkstick = require("blinkstick");
Object.size = function(obj) {
var size = 0;
for (var key in obj) { if (obj.hasOwnProperty(key)) { size++; } }
return size;
};
function BlinkStick(n) {
RED.nodes.createNode(this,n);
var p1 = /^\#[A-Fa-f0-9]{6}$/
var p2 = /[0-9]+,[0-9]+,[0-9]+/
this.led = blinkstick.findFirst(); // maybe try findAll() (one day)
var node = this;
this.on("input", function(msg) {
if (msg != null) {
if (Object.size(node.led) !== 0) {
try {
if (p2.test(msg.payload)) {
var rgb = msg.payload.split(",");
node.led.setColor(parseInt(rgb[0])&255, parseInt(rgb[1])&255, parseInt(rgb[2])&255);
}
else {
node.led.setColor(msg.payload.toLowerCase().replace(/\s+/g,''));
}
}
catch (err) {
node.warn("BlinkStick missing ?");
node.led = blinkstick.findFirst();
}
}
else {
//node.warn("No BlinkStick found");
node.led = blinkstick.findFirst();
}
}
});
if (Object.size(node.led) === 0) {
node.error("No BlinkStick found");
}
}
RED.nodes.registerType("blinkstick",BlinkStick);
}

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-blinkstick",
"version" : "0.1.16",
"version" : "0.1.7",
"description" : "A Node-RED node to control a Blinkstick",
"dependencies" : {
"blinkstick" : "1.1.3"
"blinkstick" : "1.2.0"
},
"repository" : {
"type":"git",

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-pi-mcp3008",
"version" : "0.2.1",
"version" : "0.3.0",
"description" : "A Node-RED node to read from the MCP3008 Analogue to Digital Converter",
"dependencies" : {
"mcp-spi-adc": "^2.0.6"
"mcp-spi-adc": "^3.1.0"
},
"repository" : {
"type":"git",

View File

@ -2,16 +2,23 @@
module.exports = function(RED) {
"use strict";
var fs = require('fs');
var allOK = false;
var mcpadc;
// unlikely if not on a Pi
try {
var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString();
if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); }
if (cpuinfo.indexOf(": BCM") === -1) {
RED.log.warn("Info : mcp3xxx : Not running on a Pi - Ignoring node");
}
else {
mcpadc = require('mcp-spi-adc');
allOK = true;
}
}
catch(err) {
throw "Info : "+RED._("rpi-gpio.errors.ignorenode");
RED.log.warn("Info : mcp3xxx : Not running on a Pi - Ignoring node");
}
var mcpadc = require('mcp-spi-adc');
var mcp3xxx = [];
function PiMcpNode(n) {
@ -26,49 +33,54 @@ module.exports = function(RED) {
var opt = { speedHz:20000, deviceNumber:node.dnum, busNumber:node.bus };
var chans = parseInt(this.dev.substr(3));
try {
fs.statSync("/dev/spidev"+node.bus+"."+node.dnum);
if (mcp3xxx.length === 0) {
for (var i=0; i<chans; i++) {
if (node.dev === "3002") { mcp3xxx.push(mcpadc.openMcp3002(i, opt, cb)); }
if (node.dev === "3004") { mcp3xxx.push(mcpadc.openMcp3004(i, opt, cb)); }
if (node.dev === "3008") { mcp3xxx.push(mcpadc.openMcp3008(i, opt, cb)); }
if (node.dev === "3202") { mcp3xxx.push(mcpadc.openMcp3202(i, opt, cb)); }
if (node.dev === "3204") { mcp3xxx.push(mcpadc.openMcp3204(i, opt, cb)); }
if (node.dev === "3208") { mcp3xxx.push(mcpadc.openMcp3208(i, opt, cb)); }
if (node.dev === "3304") { mcp3xxx.push(mcpadc.openMcp3304(i, opt, cb)); }
if (allOK === true) {
try {
fs.statSync("/dev/spidev"+node.bus+"."+node.dnum);
if (mcp3xxx.length === 0) {
for (var i=0; i<chans; i++) {
if (node.dev === "3002") { mcp3xxx.push(mcpadc.openMcp3002(i, opt, cb)); }
if (node.dev === "3004") { mcp3xxx.push(mcpadc.openMcp3004(i, opt, cb)); }
if (node.dev === "3008") { mcp3xxx.push(mcpadc.openMcp3008(i, opt, cb)); }
if (node.dev === "3202") { mcp3xxx.push(mcpadc.openMcp3202(i, opt, cb)); }
if (node.dev === "3204") { mcp3xxx.push(mcpadc.openMcp3204(i, opt, cb)); }
if (node.dev === "3208") { mcp3xxx.push(mcpadc.openMcp3208(i, opt, cb)); }
if (node.dev === "3304") { mcp3xxx.push(mcpadc.openMcp3304(i, opt, cb)); }
}
}
node.on("input", function(msg) {
var pin = null;
if (node.pin === "M") {
var pay = parseInt(msg.payload.toString());
if ((pay >= 0) && (pay < chans)) { pin = pay; }
else { node.warn("Payload needs to select channel 0 to "+(chans-1)); }
}
else { pin = parseInt(node.pin); }
if (pin !== null) {
mcp3xxx[pin].read(function (err, reading) {
if (err) { node.warn("Read error: "+err); }
else { node.send({payload:reading.rawValue, topic:"adc/"+pin}); }
});
}
});
}
node.on("input", function(msg) {
var pin = null;
if (node.pin === "M") {
var pay = parseInt(msg.payload.toString());
if ((pay >= 0) && (pay < chans)) { pin = pay; }
else { node.warn("Payload needs to select channel 0 to "+(chans-1)); }
}
else { pin = parseInt(node.pin); }
if (pin !== null) {
mcp3xxx[pin].read(function (err, reading) {
if (err) { node.warn("Read error: "+err); }
else { node.send({payload:reading.rawValue, topic:"adc/"+pin}); }
});
catch(err) {
node.error("Error : Can't find SPI device - is SPI enabled in raspi-config ?");
}
node.on("close", function(done) {
if (mcp3xxx.length !== 0) {
var j=0;
for (var i=0; i<chans; i++) {
mcp3xxx[i].close(function() { j += 1; if (j === chans) {done()} });
}
mcp3xxx = [];
}
else { done(); }
});
}
catch(err) {
node.error("Error : Can't find SPI device - is SPI enabled in raspi-config ?");
else {
node.status({text:"node inactive."})
}
node.on("close", function(done) {
if (mcp3xxx.length !== 0) {
var j=0;
for (var i=0; i<chans; i++) {
mcp3xxx[i].close(function() { j += 1; if (j === chans) {done()} });
}
mcp3xxx = [];
}
else { done(); }
});
}
RED.nodes.registerType("pimcp3008",PiMcpNode);

View File

@ -47,6 +47,8 @@ MODE = sys.argv[3]
LED_BRIGHTNESS = min(255,int(max(0,float(sys.argv[4])) * 255 / 100))
if (sys.argv[5].lower() != "true"):
LED_GAMMA = range(256)
LED_CHANNEL = int(sys.argv[6])
LED_PIN = int(sys.argv[7])
def getRGBfromI(RGBint):
blue = RGBint & 255

View File

@ -1,8 +1,15 @@
<script type="text/x-red" data-template-name="rpi-neopixels">
<script type="text/html" data-template-name="rpi-neopixels">
<div class="form-row">
<label for="node-input-pixels"><i class="fa fa-sun-o"></i> LEDs</label>
<input type="text" id="node-input-pixels" placeholder="number" style="width:60px;"> in the string
<span style="margin-left:50px;">Pin Number </span>
<select id="node-input-gpio" style="width:50px; margin-left:5px;">
<option value="18">12</option>
<option value="12">32</option>
<option value="13">33</option>
<option value="19">35</option>
</select>
</div>
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-cogs"></i> Mode</label>
@ -46,9 +53,11 @@
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-tips"><b>Note</b>: pins 12 and 32 are on channel 0, and 33 and 35 are on channel 1.
You can only use one pin from each channel.</div>
</script>
<script type="text/x-red" data-help-name="rpi-neopixels">
<script type="text/html" data-help-name="rpi-neopixels">
<p>Raspberry Pi node to drive a string of neopixel or ws2812 LEDs.</p>
<p>Defaults to a bar chart style mode using configured foreground and background colours.
It can also display a needle (single pixel) type gauge.</p>
@ -65,8 +74,10 @@
with a CSV string <i>x,y,r,g,b</i>
<p>By default, gamma correction is enabled but it can disabled which can be useful for working with low brightness levels</p>
<p><code>msg.brightness</code> can be used to dynamically set brightness level</p>
<p>The pixels data line should be connected to Pi physical pin 12 - GPIO 18. <i>Note:</i>
this may conflict with audio playback.</p>
<p>The pixels data line should be connected to Pi physical pin 12, 32, 33 or 35. <b>Note:</b>
pins 12 and 32 are on the same channel, as are 33 and 35. If you want connect two neopixels then use pins
from different channels.</p>
<p>Note: this node may also conflict with audio playback.</p>
<p align="right"><a href="http://flows.nodered.org/node/node-red-node-pi-neopixel#usage">More info&nbsp;&nbsp;</a></p>
</script>
@ -76,6 +87,7 @@
color:"#c6dbef",
defaults: {
name: { value:"" },
gpio: { value:18 },
pixels: { value:"", required:true, validate:RED.validators.number() },
bgnd: { value:"" },
fgnd: { value:"" },

View File

@ -40,6 +40,9 @@ module.exports = function(RED) {
this.rgb = n.rgb || "rgb";
this.gamma = n.gamma;
if (this.gamma === undefined) { this.gamma = true; }
this.gpio = n.gpio || 18;
this.channel = 0;
if (this.gpio == 13 || this.gpio == 19) { this.channel = 1; }
this.brightness = Number(n.brightness || 100);
this.wipe = Number(n.wipe || 40);
if (this.wipe < 0) { this.wipe = 0; }
@ -114,7 +117,7 @@ module.exports = function(RED) {
}
if (allOK === true) {
node.child = spawn(piCommand, [node.pixels, node.wipe, node.mode, node.brightness, node.gamma]);
node.child = spawn(piCommand, [node.pixels, node.wipe, node.mode, node.brightness, node.gamma, node.channel, node.gpio]);
node.status({fill:"green",shape:"dot",text:"ok"});
node.on("input", inputlistener);

View File

@ -1,6 +1,6 @@
{
"name" : "node-red-node-pi-neopixel",
"version" : "0.0.25",
"version" : "0.1.1",
"description" : "A Node-RED node to output to a neopixel (ws2812) string of LEDS from a Raspberry Pi.",
"dependencies" : {
},

View File

@ -1,6 +1,6 @@
{
"name": "node-red-node-pi-gpiod",
"version": "0.1.0",
"version": "0.2.0",
"description": "A node-red node for PiGPIOd",
"dependencies" : {
"js-pigpio": "*"

View File

@ -1,5 +1,5 @@
<script type="text/x-red" data-template-name="pi-gpiod in">
<script type="text/html" data-template-name="pi-gpiod in">
<style>
.pinTable {
width: 300px;
@ -168,10 +168,10 @@
their other use before using as GPIO.</div>
</script>
<script type="text/x-red" data-help-name="pi-gpiod in">
<script type="text/html" data-help-name="pi-gpiod in">
<p>Raspberry Pi input node. Generates a <code>msg.payload</code> with either a
0 or 1 depending on the state of the input pin.
Requires the <a href="http://abyz.co.uk/rpi/pigpio/pigpiod.html" target="_new">pi-gpiod</a>
Requires the <a href="http://abyz.me.uk/rpi/pigpio/index.html" target="_new">pi-gpiod</a>
daemon to be running on the host computer in order to work.</p>
<p><b>Outputs</b>
<ul>
@ -233,7 +233,7 @@
});
</script>
<script type="text/x-red" data-template-name="pi-gpiod out">
<script type="text/html" data-template-name="pi-gpiod out">
<style>
.pinTable {
width: 300px;
@ -399,6 +399,28 @@
<option value="1" data-i18n="pi-gpiod.initpin1"></option>
</select>
</div>
<div class="form-row" id="node-set-freq">
<label for="node-input-freq"> <span data-i18n="rpi-gpio.label.freq"></span></label>
Frequency: <select id="node-input-freq" style="width:80px;">
<option value="8000">8000</option>
<option value="4000">4000</option>
<option value="2000">2000</option>
<option value="1600">1600</option>
<option value="1000">1000</option>
<option value="800">800</option>
<option value="500">500</option>
<option value="400">400</option>
<option value="320">320</option>
<option value="250">250</option>
<option value="200">200</option>
<option value="160">160</option>
<option value="80">80</option>
<option value="50">50</option>
<option value="40">40</option>
<option value="20">20</option>
<option value="10">10</option>
</select> Hz
</div>
<div class="form-row">
<label for="node-input-host"><i class="fa fa-globe"></i> <span data-i18n="pi-gpiod.label.host"></label>
<input type="text" id="node-input-host" placeholder="localhost" style="width:250px;">
@ -415,9 +437,9 @@
their other use before using as GPIO.</div>
</script>
<script type="text/x-red" data-help-name="pi-gpiod out">
<script type="text/html" data-help-name="pi-gpiod out">
<p>Raspberry Pi output node. Can be used in Digital, PWM or Servo modes. Requires the
<a href="http://abyz.co.uk/rpi/pigpio/pigpiod.html" target="_new">pi-gpiod</a> daemon to be running in order to work.</p>
<a href="http://abyz.me.uk/rpi/pigpio/index.html" target="_new">pi-gpiod</a> daemon to be running in order to work.</p>
<p><b>Inputs</b>
<ul>
<li><code>msg.payload</code> - <i>number | string</i> - 0,1 (Digital), 0-100 (PWM, Servo)</li>
@ -450,7 +472,8 @@
level: { value:"0" },
out: { value:"out" },
sermin: { value:"1000" },
sermax: { value:"2000" }
sermax: { value:"2000" },
freq: { value:"800" }
},
inputs:1,
outputs:0,
@ -478,29 +501,32 @@
var hidestate = function () {
if ($("#node-input-out").val() === "pwm") {
$('#node-set-freq').show();
$('#node-set-tick').hide();
$('#node-set-state').hide();
$('#node-set-minimax').hide();
$('#node-input-set').prop('checked', false);
$("#dig-tip").hide();
$("#pwm-tip").show();
$("#ser-tip").hide();
$('#dig-tip').hide();
$('#pwm-tip').show();
$('#ser-tip').hide();
}
else if ($("#node-input-out").val() === "ser") {
$('#node-set-freq').hide();
$('#node-set-tick').hide();
$('#node-set-state').hide();
$('#node-set-minimax').show();
$('#node-input-set').prop('checked', false);
$("#dig-tip").hide();
$("#pwm-tip").hide();
$("#ser-tip").show();
$('#dig-tip').hide();
$('#pwm-tip').hide();
$('#ser-tip').show();
}
else {
$('#node-set-freq').hide();
$('#node-set-tick').show();
$('#node-set-minimax').hide();
$("#dig-tip").show();
$("#pwm-tip").hide();
$("#ser-tip").hide();
$('#dig-tip').show();
$('#pwm-tip').hide();
$('#ser-tip').hide();
}
};
$("#node-input-out").change(function () { hidestate(); });

View File

@ -86,6 +86,7 @@ module.exports = function(RED) {
this.set = n.set || false;
this.level = parseInt(n.level || 0);
this.out = n.out || "out";
this.freq = parseInt(n.freq) || 800;
this.sermin = Number(n.sermin)/100;
this.sermax = Number(n.sermax)/100;
if (this.sermin > this.sermax) {
@ -120,6 +121,7 @@ module.exports = function(RED) {
PiGPIO.write(node.pin, out);
}
if (node.out === "pwm") {
PiGPIO.set_PWM_frequency(node.pin, node.freq);
PiGPIO.set_PWM_dutycycle(node.pin, parseInt(out * 2.55));
}
if (node.out === "ser") {

View File

@ -0,0 +1,12 @@
{
"sensehat": {
"label": {
"outputs": "Outputs",
"motionEvents": "Motion events",
"motionEventsExamples": "accelerometer, gyroscope, magnetometer, compass",
"environmentEvents": "Environment events",
"environmentEventsExamples": "temperature, humidity, pressure",
"joystickEvents": "Joystick events"
}
}
}

View File

@ -0,0 +1,12 @@
{
"sensehat": {
"label": {
"outputs": "出力",
"motionEvents": "モーションイベント",
"motionEventsExamples": "加速度計、ジャイロスコープ、磁力計、方位計",
"environmentEvents": "環境イベント",
"environmentEventsExamples": "温度、湿度、気圧",
"joystickEvents": "ジョイスティックイベント"
}
}
}

View File

@ -1,30 +1,30 @@
<script type="text/x-red" data-template-name="rpi-sensehat in">
<div class="form-row">
<label><i class="fa fa-arrow-right"></i> Outputs</label>
<label style="width: auto" for="node-input-motion"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-motion"> Motion events</label>
<div style="padding-left: 125px; margin-top: -5px; color: #bbb;">accelerometer, gyroscope, magnetometer, compass</div>
<label><i class="fa fa-arrow-right"></i> <span data-i18n="sensehat.label.outputs"></label>
<label style="width: auto" for="node-input-motion"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-motion"> <span data-i18n="sensehat.label.motionEvents"></label>
<div style="padding-left: 125px; margin-top: -5px; color: #bbb;" data-i18n="sensehat.label.motionEventsExamples"></div>
</div>
<div class="form-row">
<label></label>
<label style="width: auto" for="node-input-env"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-env"> Environment events</label>
<div style="padding-left: 125px; margin-top: -5px; color: #bbb;">temperature, humidity, pressure</div>
<label style="width: auto" for="node-input-env"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-env"> <span data-i18n="sensehat.label.environmentEvents"></label>
<div style="padding-left: 125px; margin-top: -5px; color: #bbb;" data-i18n="sensehat.label.environmentEventsExamples"></div>
</div>
<div class="form-row">
<label></label>
<label style="width: auto" for="node-input-stick"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-stick"> Joystick events</label>
<label style="width: auto" for="node-input-stick"><input style="vertical-align: top; width: auto; margin-right: 5px;" type="checkbox" id="node-input-stick"> <span data-i18n="sensehat.label.joystickEvents"></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">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>
<script type="text/x-red" data-template-name="rpi-sensehat out">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>

View File

@ -4,7 +4,7 @@ module.exports = function(RED) {
var spawn = require("child_process").spawn;
var plat = require("os").platform();
function doPing(node, host, arrayMode){
function doPing(node, host, arrayMode) {
const defTimeout = 5000;
var ex, hostOptions, commandLineOptions;
if (typeof host === "string") {
@ -25,30 +25,27 @@ module.exports = function(RED) {
if (arrayMode) {
msg.ping = hostOptions
}
if (plat == "linux" || plat == "android") {
if (plat == "linux" || plat == "android") {
commandLineOptions = ["-n", "-w", timeoutS, "-c", "1"]
} else if (plat.match(/^win/)) {
} else if (plat.match(/^win/)) {
commandLineOptions = ["-n", "1", "-w", hostOptions.timeout]
} else if (plat == "darwin" || plat == "freebsd") {
} else if (plat == "darwin" || plat == "freebsd") {
commandLineOptions = ["-n", "-t", timeoutS, "-c", "1"]
} else {
node.error("Sorry - your platform - "+plat+" - is not recognised.", msg);
} else {
node.error("Sorry - your platform - "+plat+" - is not recognised.", msg);
return; //dont pass go - just return!
}
//spawn with timeout in case of os issue
ex = spawn("ping", [...commandLineOptions, hostOptions.host]);
ex = spawn("ping", [...commandLineOptions, hostOptions.host]);
//monitor every spawned process & SIGINT if too long
var spawnTout = setTimeout(() => {
node.log(`ping - Host '${hostOptions.host}' process timeout - sending SIGINT`)
try {
if (ex && ex.pid) {
ex.removeAllListeners();
ex.kill("SIGINT");
}
}
catch(e) { console.warn(e) }
if (ex && ex.pid) { ex.kill("SIGINT"); }
}
catch(e) {console.warn(e); }
}, hostOptions.timeout+1000); //add 1s for grace
var res = false;
@ -56,11 +53,9 @@ module.exports = function(RED) {
var fail = false;
//var regex = /from.*time.(.*)ms/;
var regex = /=.*[<|=]([0-9]*).*TTL|ttl..*=([0-9\.]*)/;
if (ex && ex.hasOwnProperty("stdout")) {
ex.stdout.on("data", function (data) {
line += data.toString();
});
}
ex.stdout.on("data", function (data) {
line += data.toString();
});
ex.on("exit", function (err) {
clearTimeout(spawnTout);
});
@ -99,7 +94,7 @@ module.exports = function(RED) {
function generatePingList(str) {
return (str + "").split(",").map((e) => (e + "").trim()).filter((e) => e != "");
}
function clearPingInterval(){
function clearPingInterval() {
if (node.tout) { clearInterval(node.tout); }
}

View File

@ -1,6 +1,6 @@
{
"name" : "node-red-node-ping",
"version" : "0.2.1",
"version" : "0.2.2",
"description" : "A Node-RED node to ping a remote server, for use as a keep-alive check.",
"dependencies" : {
},

View File

@ -5,6 +5,7 @@ module.exports = function(RED) {
var events = require("events");
var serialp = require("serialport");
var bufMaxSize = 32768; // Max serial buffer size, for inputs...
const serialReconnectTime = settings.serialReconnectTime || 15000;
// TODO: 'serialPool' should be encapsulated in SerialPortNode
@ -350,7 +351,7 @@ module.exports = function(RED) {
}
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
}, serialReconnectTime);
}
});
obj.serial.on('error', function(err) {
@ -359,7 +360,7 @@ module.exports = function(RED) {
if (obj.tout) { clearTimeout(obj.tout); }
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
}, serialReconnectTime);
});
obj.serial.on('close', function() {
if (!obj._closing) {
@ -371,7 +372,7 @@ module.exports = function(RED) {
if (obj.tout) { clearTimeout(obj.tout); }
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
}, serialReconnectTime);
}
});
obj.serial.on('open',function() {

View File

@ -1,5 +1,9 @@
{
"serial": {
"status": {
"waiting": "waiting",
"timeout": "timeout"
},
"label": {
"serialport": "シリアルポート",
"settings": "設定",
@ -11,8 +15,13 @@
"split": "入力の分割方法",
"deliver": "分割後の配信データ",
"output": "出力",
"request": "リクエスト",
"responsetimeout": "デフォルトの応答タイムアウト",
"ms": "ミリ秒",
"serial": "serial",
"none": "なし"
"none": "なし",
"start": "オプションで開始文字",
"startor": "を待ちます。"
},
"placeholder": {
"serialport": "例: /dev/ttyUSB0/"
@ -25,13 +34,14 @@
"space": "スペース"
},
"linestates": {
"none": "auto",
"high": "High",
"low": "Low"
"none": "自動",
"high": "",
"low": ""
},
"split": {
"character": "文字列で区切る",
"timeout": "タイムアウト後で区切る",
"silent": "一定の待ち時間後に区切る",
"lengths": "一定の文字数で区切る"
},
"output": {
@ -40,19 +50,24 @@
},
"addsplit": "出力メッセージに分割文字を追加する",
"tip": {
"responsetimeout": "Tip: デフォルトの応答タイムアウトは msg.timeout の設定で上書きすることができます。",
"split": "Tip: \"区切り\" 文字は、入力を別々のメッセージに分割するために使用され、シリアルポートに送信されるすべてのメッセージに追加することもできます。",
"timeout": "Tip: タイムアウトモードでのタイムアウトは最初の文字が到着したときから始まります。"
"silent": "Tip: In line-silent mode timeout is restarted upon arrival of any character (i.e. inter-byte timeout).",
"timeout": "Tip: タイムアウトモードでのタイムアウトは最初の文字が到着したときから始まります。",
"count": "Tip: カウントモードでは msg.count 設定は、構成された値よりも小さいときに限り、構成されたカウントを上書きすることができます。",
"waitfor": "Tip: オプションです。すべてのデータを受信するには、空白のままにします。文字($)・エスケープコード(\\n)・16進コード(0x02)を受け入れることができます。" ,
"addchar": "Tip: この文字は、シリアルポートに送信されるすべてのメッセージに追加されます。通常は \\r や \\n です。"
},
"onopen": "serial port __port__ opened at __baud__ baud __config__",
"onopen": "シリアルポート __port__ が __baud__ ボー __config__ で開かれました",
"errors": {
"missing-conf": "missing serial config",
"serial-port": "serial port",
"error": "serial port __port__ error: __error__",
"unexpected-close": "serial port __port__ closed unexpectedly",
"disconnected": "serial port __port__ disconnected",
"closed": "serial port __port__ closed",
"missing-conf": "シリアル設定がありません。",
"serial-port": "シリアルポート",
"error": "シリアルポート __port__ エラー: __error__",
"unexpected-close": "シリアルポート __port__ が予期せず閉じられました",
"disconnected": "シリアルポート __port__ 切断",
"closed": "シリアルポート __port__ 閉じられました",
"list": "ポートのリスト化に失敗しました。手動で入力してください。",
"badbaudrate": "ボーレートが不正です"
"badbaudrate": "ボーレートが不正です"
}
}
}

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-serialport",
"version" : "0.11.0",
"version" : "0.11.1",
"description" : "Node-RED nodes to talk to serial ports",
"dependencies" : {
"serialport" : "^9.0.1"
"serialport" : "^9.0.2"
},
"repository" : {
"type":"git",

View File

@ -40,22 +40,22 @@
"grunt-lint-inline": "^1.0.0",
"grunt-simple-mocha": "^0.4.1",
"imap": "^0.8.19",
"mailparser": "^3.0.0",
"mailparser": "~3.0.1",
"markdown-it": "^11.0.0",
"mocha": "~6.2.3",
"msgpack-lite": "^0.1.26",
"multilang-sentiment": "^1.2.0",
"ngeohash": "^0.6.3",
"node-red": "^1.1.3",
"node-red": "~1.2.6",
"node-red-node-test-helper": "~0.2.5",
"nodemailer": "^6.4.10",
"nodemailer": "~6.4.16",
"poplib": "^0.1.7",
"proxyquire": "^2.1.3",
"pushbullet": "^2.4.0",
"sentiment": "^2.1.0",
"should": "^13.2.3",
"sinon": "~7.5.0",
"smtp-server": "^3.7.0",
"smtp-server": "~3.8.0",
"supertest": "^4.0.2",
"when": "^3.7.8"
},

View File

@ -94,6 +94,8 @@ module.exports = function(RED) {
sendopts.inReplyTo = msg.inReplyTo;
sendopts.replyTo = msg.replyTo;
sendopts.references = msg.references;
sendopts.headers = msg.headers;
sendopts.priority = msg.priority;
}
sendopts.subject = msg.topic || msg.title || "Message from Node-RED"; // subject line
if (msg.hasOwnProperty("envelope")) { sendopts.envelope = msg.envelope; }
@ -118,7 +120,18 @@ module.exports = function(RED) {
var payload = RED.util.ensureString(msg.payload);
sendopts.text = payload; // plaintext body
if (/<[a-z][\s\S]*>/i.test(payload)) { sendopts.html = payload; } // html body
if (msg.attachments) { sendopts.attachments = msg.attachments; } // add attachments
if (msg.attachments && Array.isArray(msg.attachments)) {
sendopts.attachments = msg.attachments;
for (var a=0; a < sendopts.attachments.length; a++) {
if (sendopts.attachments[a].hasOwnProperty("content")) {
if (typeof sendopts.attachments[a].content !== "string" && !Buffer.isBuffer(sendopts.attachments[a].content)) {
node.error(RED._("email.errors.invalidattachment"),msg);
node.status({fill:"red",shape:"ring",text:"email.status.sendfail"});
return;
}
}
}
}
}
smtpTransport.sendMail(sendopts, function(error, info) {
if (error) {

View File

@ -2,7 +2,8 @@
<p>Sends the <code>msg.payload</code> as an email, with a subject of <code>msg.topic</code>.</p>
<p>The default message recipient can be configured in the node, if it is left
blank it should be set using the <code>msg.to</code> property of the incoming message. If left blank
you can also specify any or all of: <code>msg.cc</code>, <code>msg.bcc</code>, <code>msg.replyTo</code>, <code>msg.inReplyTo</code>, <code>msg.references</code> properties.</p>
you can also specify any or all of: <code>msg.cc</code>, <code>msg.bcc</code>, <code>msg.replyTo</code>,
<code>msg.inReplyTo</code>, <code>msg.references</code>, <code>msg.headers</code>, or <code>msg.priority</code> properties.</p>
<p>You may optionally set <code>msg.from</code> in the payload which will override the <code>userid</code>
default value.</p>
<h3>GMail users</h3>

View File

@ -60,7 +60,8 @@
"fetchfail": "Failed to fetch folder: __folder__",
"parsefail": "Failed to parse message",
"messageerror": "Fetch message error: __error__",
"refreshtoolarge": "Refresh interval too large. Limiting to 2147483 seconds"
"refreshtoolarge": "Refresh interval too large. Limiting to 2147483 seconds",
"invalidattachment": "Invalid attachment content. Must be String or buffer"
}
}
}

View File

@ -2,7 +2,7 @@
<script type="text/html" data-help-name="e-mail">
<p><code>msg.payload</code>をemailとして送信します。件名は<code>msg.topic</code>で指定します。</p>
<p>メッセージの受信者のデフォルトはノードに設定できます。空のままとした場合は、入力メッセージの<code>msg.to</code>を設定します。<code>msg.cc</code><code>msg.bcc</code><code>msg.replyTo</code><code>msg.inReplyTo</code><code>msg.references</code>プロパティを設定することもできます。</p>
<p>メッセージの受信者のデフォルトはノードに設定できます。空のままとした場合は、入力メッセージの<code>msg.to</code>を設定します。<code>msg.cc</code><code>msg.bcc</code><code>msg.replyTo</code><code>msg.inReplyTo</code><code>msg.references</code>, <code>msg.headers</code>, <code>msg.priority</code>プロパティを設定することもできます。</p>
<p><code>msg.from</code>を指定すると、<code>ユーザID</code>のデフォルト値を上書きできます。</p>
<p>ペイロードはHTML形式とすることも可能です。</p>
<p>ペイロードにバイナリバッファを指定すると、添付ファイルに変換します。ファイル名は<code>msg.filename</code>に指定、メッセージ本体は<code>msg.description</code>に指定することができます。</p>

View File

@ -1,13 +1,13 @@
{
"name": "node-red-node-email",
"version": "1.8.1",
"version": "1.8.3",
"description": "Node-RED nodes to send and receive simple emails.",
"dependencies": {
"imap": "^0.8.19",
"poplib": "^0.1.7",
"mailparser": "^3.0.0",
"nodemailer": "~6.4.10",
"smtp-server": "^3.7.0"
"mailparser": "^3.0.1",
"nodemailer": "~6.4.17",
"smtp-server": "^3.8.0"
},
"repository": {
"type": "git",

View File

@ -1,7 +1,7 @@
<!-- PUSHBULLET CONFIG -->
<script type="text/x-red" data-template-name="pushbullet-config">
<script type="text/html" data-template-name="pushbullet-config">
<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">
@ -30,7 +30,7 @@
<!-- PUSHBULLET OUT -->
<script type="text/x-red" data-template-name="pushbullet">
<script type="text/html" data-template-name="pushbullet">
<div class="form-row">
<label for="node-input-config"><i class="fa fa-user"></i> Config</label>
<input type="text" id="node-input-config">
@ -72,12 +72,9 @@
<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="pushbullet-migration-info" style="display: none;">
<p><i class="fa fa-random"></i> Configuration node has been migrated, please click Ok and then re-deploy flow to save settings.</p>
</div>
</script>
<script type="text/x-red" data-help-name="pushbullet">
<script type="text/html" data-help-name="pushbullet">
<p>Uses PushBullet to push <code>msg.payload</code> to a device that has the PushBullet app installed.</p>
<p>Optionally uses <code>msg.topic</code> to set the title, if not already set in the properties.</p>
<p>Optionally uses <code>msg.pushtype</code> to set the type of the push, if not already set in the properties.</p>
@ -122,7 +119,6 @@
});
}
}
try {
getName(this.credentials.deviceid);
}
@ -131,7 +127,6 @@
getName(data.deviceid);
});
}
return this.name||this.devicename||this.title||"pushbullet";
},
labelStyle: function() {
@ -139,9 +134,6 @@
},
oneditsave: function() {
this.devicename = undefined;
if(this.migrationData) {
$.ajax('pushbullet/'+this.id+'/migrate?save=true');
}
},
oneditprepare: function() {
var node = this, ddConfig = $('#node-input-config'), ddDevice = $('#node-input-deviceid'), ddPushtype = $('#node-input-pushtype');
@ -181,7 +173,6 @@
addCurrent = false;
}
}
if(currentDevice) {
if(addCurrent && currentDevice !== "_msg_") {
ddDevice.append('<option value="'+currentDevice+'">'+currentDevice+'</option>');
@ -191,75 +182,13 @@
});
}
}
function checkMigration(cb) {
$.getJSON('pushbullet/'+node.id+'/migrate', function(data) {
var showMigration = false;
node.migrationData = data.config;
if(data.migrated) {
if(data.config) {
var configId = data.config;
var configType = 'pushbullet-config';
var configTypeDef = RED.nodes.getType(configType);
RED.nodes.add({
type: configType,
id: configId,
name: "Imported",
users: [node.id],
label: configTypeDef.label,
_def: configTypeDef,
});
}
if(node.credentials) {
if(node.credentials.pushkey) {
if(ddConfig.find('option[value="'+data.config+'"]').length === 0) {
ddConfig.append('<option value="'+data.config+'">Imported</option>');
}
ddConfig.val(data.config);
showMigration = true;
}
else {
ddConfig.val("_ADD_");
}
if(node.credentials.deviceid) {
ddDevice.append('<option value="'+node.credentials.deviceid+'">Imported</option>');
ddDevice.val(node.credentials.deviceid);
showMigration = true;
}
else {
ddDevice.val("");
}
}
}
if(showMigration) {
ddPushtype.val("note");
node.dirty = true;
$('#pushbullet-migration-info').show();
}
if(cb) {
cb(showMigration);
}
});
}
checkMigration(function(migrated) {
if(!migrated) {
ddConfig.change(function() {
if(ddConfig.val() && ddConfig.val() !== "_ADD_") {
updateDeviceList();
}
});
}
});
}
});
</script>
<!-- PUSHBULLET IN -->
<script type="text/x-red" data-template-name="pushbullet in">
<script type="text/html" data-template-name="pushbullet in">
<div class="form-row">
<label for="node-input-config"><i class="fa fa-user"></i> Config</label>
<input type="text" id="node-input-config" placeholder="Node-RED">
@ -277,7 +206,7 @@
</div>
</script>
<script type="text/x-red" data-help-name="pushbullet in">
<script type="text/html" data-help-name="pushbullet in">
<p>Receives Pushbullets from all devices. Messages contain the following data:</p>
<p><code>msg.pushtype</code>: type of message</p>
<p><code>msg.topic</code>: topic information from the push</p>

View File

@ -34,23 +34,11 @@ module.exports = function(RED) {
});
PushbulletConfig.prototype.initialise = function() {
if (this.initialised) {
return;
}
if (this.initialised) { return; }
this.emitter = new EventEmitter();
this.initialised = true;
var self = this;
// sort migration from old node
var apikey;
if (this.n._migrate) {
apikey = this.n._apikey;
this.credentials = {apikey:apikey};
}
else if (this.credentials) {
apikey = this.credentials.apikey;
}
var apikey = this.credentials.apikey;
if (apikey) {
try {
@ -254,84 +242,23 @@ module.exports = function(RED) {
this._inputNodes.push(handler);
};
function migrateOldSettings(n) {
if (n.config === undefined) {
var newid, config, apikey, deviceid, pushkeys;
try {
pushkeys = RED.settings.pushbullet || require(process.env.NODE_RED_HOME+"/../pushkey.js");
}
catch(err) { }
var cred = RED.nodes.getCredentials(n.id);
// get old apikey
if (cred && cred.hasOwnProperty("pushkey")) {
apikey = cred.pushkey;
}
else if (pushkeys) {
apikey = pushkeys.pushbullet;
}
// get old device
if (cred && cred.hasOwnProperty("deviceid")) {
deviceid = cred.deviceid;
}
else if (pushkeys) {
deviceid = pushkeys.deviceid;
}
if (apikey) {
newid = (1+Math.random()*4294967295).toString(16);
config = new PushbulletConfig({
id: newid,
type: 'pushbullet-config',
name: n.name,
_migrate: true,
_apikey: apikey,
});
}
if (!(apikey || deviceid)) {
return false;
}
// override configuration properties to compatible migrated ones
n.pushtype = "note";
n.deviceid = deviceid;
return {
deviceid: deviceid,
apikey: apikey,
config: config,
id: newid
};
}
return false;
}
function PushbulletOut(n) {
RED.nodes.createNode(this, n);
var self = this;
this.migrated = migrateOldSettings(n);
this.title = n.title;
this.chan = n.chan;
this.pushtype = n.pushtype;
this.pusher = null;
var configNode;
if (this.migrated) {
this.warn('Settings migrated from previous version of Pushbullet Node, please edit node to update settings.');
this.status({fill: 'yellow', shape: 'ring', text: 'Node migrated'});
this.deviceid = this.migrated.deviceid;
configNode = this.migrated.config;
}
else {
this.status({});
configNode = RED.nodes.getNode(n.config);
try {
this.deviceid = this.credentials.deviceid;
}
catch(err) { }
this.status({});
configNode = RED.nodes.getNode(n.config);
try {
this.deviceid = this.credentials.deviceid;
}
catch(err) { }
if (configNode) {
configNode.initialise();
@ -462,27 +389,6 @@ module.exports = function(RED) {
}
};
RED.httpAdmin.get('/pushbullet/:id/migrate', RED.auth.needsPermission('pushbullet.read'), function(req, res) {
var node = RED.nodes.getNode(req.params.id);
if (node && node.migrated) {
if (req.query.save) {
var promise;
if (node.migrated.apikey) {
promise = RED.nodes.addCredentials(node.migrated.id, {apikey: node.migrated.apikey});
}
if (node.migrated.deviceid) {
when(promise).then(function() {
RED.nodes.addCredentials(req.params.id, {deviceid: node.migrated.deviceid});
});
}
}
res.send(JSON.stringify({migrated: true, config: node.migrated.id}));
}
else {
res.send("{}");
}
});
RED.httpAdmin.get('/pushbullet/:id/devices', RED.auth.needsPermission('pushbullet.read'), function(req, res) {
var config = RED.nodes.getNode(req.params.id);
var cred = RED.nodes.getCredentials(req.params.id);

View File

@ -642,7 +642,12 @@ module.exports = function(RED) {
node.status({});
} else {
node.status({fill:"red",shape:"ring",text:"twitter.status.failed"});
node.error(result.body.errors[0].message,msg);
if ('error' in result.body && typeof result.body.error === 'string') {
node.error(result.body.error,msg);
} else {
node.error(result.body.errors[0].message,msg);
}
}
}).catch(function(err) {
node.status({fill:"red",shape:"ring",text:"twitter.status.failed"});

View File

@ -1,6 +1,6 @@
{
"name": "node-red-node-twitter",
"version": "1.1.6",
"version": "1.1.7",
"description": "A Node-RED node to talk to Twitter",
"dependencies": {
"twitter-ng": "0.6.2",

View File

@ -109,7 +109,7 @@
</script>
<script type="text/html" data-template-name="xmpp-server">
<div class="form-row node-input-server">
<div class="form-row">
<label for="node-config-input-server"><i class="fa fa-bookmark"></i> Server</label>
<input class="input-append-left" type="text" id="node-config-input-server" placeholder="blah.im" style="width: 40%;" >
<label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> Port</label>

View File

@ -12,13 +12,13 @@ module.exports = function(RED) {
this.username = n.user.split('@')[0];
// The user may elect to just specify the jid in the settings,
// in which case extract the server from the jid and default the port
if("undefined" === typeof n.server || n.server === ""){
if ("undefined" === typeof n.server || n.server === "") {
this.server = n.user.split('@')[1];
}
else{
this.server = n.server;
}
if("undefined" === typeof n.port || n.port === ""){
if ("undefined" === typeof n.port || n.port === "") {
this.port = 5222;
}
else{
@ -33,7 +33,7 @@ module.exports = function(RED) {
// The basic xmpp client object, this will be referred to as "xmpp" in the nodes.
// note we're not actually connecting here.
var proto = "xmpp";
if(this.port === 5223){
if (this.port === 5223) {
proto = "xmpps";
}
if (RED.settings.verbose || LOGITALL) {
@ -55,7 +55,7 @@ module.exports = function(RED) {
// function for a node to tell us it has us as config
this.register = function(xmppThat) {
if (RED.settings.verbose || LOGITALL) {that.log("registering "+xmppThat.id);}
if (RED.settings.verbose || LOGITALL) {that.log("registering "+xmppThat.id); }
that.users[xmppThat.id] = xmppThat;
// So we could start the connection here, but we already have the logic in the nodes that takes care of that.
// if (Object.keys(that.users).length === 1) {
@ -65,7 +65,7 @@ module.exports = function(RED) {
// function for a node to tell us it's not using us anymore
this.deregister = function(xmppThat,done) {
if (RED.settings.verbose || LOGITALL) {that.log("deregistering "+xmppThat.id);}
if (RED.settings.verbose || LOGITALL) {that.log("deregistering "+xmppThat.id); }
delete that.users[xmppThat.id];
if (that.closing) {
return done();
@ -84,8 +84,8 @@ module.exports = function(RED) {
this.lastUsed = undefined;
// function for a node to tell us it has just sent a message to our server
// so we know which node to blame if it all goes Pete Tong
this.used = function(xmppThat){
if (RED.settings.verbose || LOGITALL) {that.log(xmppThat.id+" sent a message to the xmpp server");}
this.used = function(xmppThat) {
if (RED.settings.verbose || LOGITALL) {that.log(xmppThat.id+" sent a message to the xmpp server"); }
that.lastUsed = xmppThat;
}
@ -101,20 +101,20 @@ module.exports = function(RED) {
that.log(stanza);
}
var err = stanza.getChild('error');
if(err){
if (err) {
var textObj = err.getChild('text');
var text = "node-red:common.status.error";
if("undefined" !== typeof textObj){
if ("undefined" !== typeof textObj) {
text = textObj.getText();
}
else{
textObj = err.getChild('code');
if("undefined" !== typeof textObj){
if ("undefined" !== typeof textObj) {
text = textObj.getText();
}
}
if (RED.settings.verbose || LOGITALL) {that.log("Culprit: "+that.lastUsed);}
if("undefined" !== typeof that.lastUsed){
if (RED.settings.verbose || LOGITALL) {that.log("Culprit: "+that.lastUsed); }
if ("undefined" !== typeof that.lastUsed) {
that.lastUsed.status({fill:"red",shape:"ring",text:text});
that.lastUsed.warn(text);
}
@ -122,20 +122,20 @@ module.exports = function(RED) {
that.log("We did wrong: "+text);
that.log(stanza);
}
// maybe throw the message or summit
//that.error(text);
}
}
}
else if(stanza.is('presence')){
if(['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1){
if (RED.settings.verbose || LOGITALL) {that.log("got a subscription based message");}
switch(stanza.attrs.type){
else if (stanza.is('presence')) {
if (['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1) {
if (RED.settings.verbose || LOGITALL) {that.log("got a subscription based message"); }
switch(stanza.attrs.type) {
case 'subscribe':
// they're asking for permission let's just say yes
var response = xml('presence',
{type:'subscribed', to:stanza.attrs.from});
{type:'subscribed', to:stanza.attrs.from});
// if an error comes back we can't really blame anyone else
that.used(that);
that.client.send(response);
@ -145,25 +145,26 @@ module.exports = function(RED) {
}
}
}
else if(stanza.is('iq')){
if (RED.settings.verbose || LOGITALL) {that.log("got an iq query");}
if(stanza.attrs.type === 'error'){
if (RED.settings.verbose || LOGITALL) {that.log("oh noes, it's an error");}
if(stanza.attrs.id === that.lastUsed.id){
else if (stanza.is('iq')) {
if (RED.settings.verbose || LOGITALL) {that.log("got an iq query"); }
if (stanza.attrs.type === 'error') {
if (RED.settings.verbose || LOGITALL) {that.log("oh noes, it's an error"); }
if (stanza.attrs.id === that.lastUsed.id) {
that.lastUsed.status({fill:"red", shape:"ring", text:stanza.getChild('error')});
that.lastUsed.warn(stanza.getChild('error'));
}
}
else if(stanza.attrs.type === 'result'){
// To-Do check for 'bind' result with our current jid
else if (stanza.attrs.type === 'result') {
// AM To-Do check for 'bind' result with our current jid
var query = stanza.getChild('query');
if (RED.settings.verbose || LOGITALL) {that.log("result!");}
if (RED.settings.verbose || LOGITALL) {that.log(query);}
if (RED.settings.verbose || LOGITALL) {that.log("result!"); }
if (RED.settings.verbose || LOGITALL) {that.log(query); }
}
}
}
});
// We shouldn't have any errors here that the input/output nodes can't handle
// if you need to see everything though; uncomment this block
// this.client.on('error', err => {
@ -176,18 +177,19 @@ module.exports = function(RED) {
// provide some presence so people can see we're online
that.connected = true;
await that.client.send(xml('presence'));
if (RED.settings.verbose || LOGITALL) {that.log('connected as '+that.username+' to ' +that.server+':'+that.port);}
// await that.client.send(xml('presence', {type: 'available'},xml('status', {}, 'available')));
if (RED.settings.verbose || LOGITALL) {that.log('connected as '+that.username+' to ' +that.server+':'+that.port); }
});
// if the connection has gone away, not sure why!
this.client.on('offline', () => {
that.connected = false;
if (RED.settings.verbose || LOGITALL) {that.log('connection closed');}
if (RED.settings.verbose || LOGITALL) {that.log('connection closed'); }
});
// gets called when the node is destroyed, e.g. if N-R is being stopped.
this.on("close", async done => {
if(that.client.connected){
if (that.client.connected) {
await that.client.send(xml('presence', {type: 'unavailable'}));
try{
if (RED.settings.verbose || LOGITALL) {
@ -195,10 +197,10 @@ module.exports = function(RED) {
}
await that.client.stop().then(that.log("XMPP client stopped")).catch(error=>{that.warn("Got an error whilst closing xmpp session: "+error)});
}
catch(e){
catch(e) {
that.warn(e);
}
}
}
done();
});
}
@ -223,6 +225,51 @@ module.exports = function(RED) {
xmpp.send(stanza);
}
// separated out since we want the same functionality from both in and out nodes
function errorHandler(node, err){
if (!node.quiet) {
node.quiet = true;
// if the error has a "stanza" then we've probably done something wrong and the
// server is unhappy with us
if (err.hasOwnProperty("stanza")) {
if (err.stanza.name === 'stream:error') { node.error("stream:error - bad login id/pwd ?",err); }
else { node.error(err.stanza.name,err); }
node.status({fill:"red",shape:"ring",text:"bad login"});
}
// The error might be a string
else if (err == "TimeoutError") {
// OK, this happens with OpenFire, suppress it.
node.status({fill:"grey",shape:"dot",text:"opening"});
node.log("Timed out! ",err);
// node.status({fill:"red",shape:"ring",text:"XMPP timeout"});
}
else if (err === "XMPP authentication failure") {
node.error(err,err);
node.status({fill:"red",shape:"ring",text:"XMPP authentication failure"});
}
// or it might have a name that tells us what's wrong
else if (err.name === "SASLError") {
node.error("Authorization error! "+err.condition,err);
node.status({fill:"red",shape:"ring",text:"XMPP authorization failure"});
}
// or it might have the errno set.
else if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err);
node.status({fill:"red",shape:"ring",text:"timeout"});
}
else if (err.errno === "ENOTFOUND") {
node.error("Server doesn't exist "+xmpp.options.service,err);
node.status({fill:"red",shape:"ring",text:"bad address"});
}
// nothing we've seen before!
else {
node.error("Unknown error: "+err,err);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
}
}
}
function XmppInNode(n) {
RED.nodes.createNode(this,n);
@ -239,7 +286,7 @@ module.exports = function(RED) {
var node = this;
var xmpp = this.serverConfig.client;
/* connection states
online: We are connected
offline: disconnected and will not autoretry
@ -294,51 +341,17 @@ module.exports = function(RED) {
node.status({fill:"grey",shape:"dot",text:"disconnecting"});
});
// we'll not add a offline catcher, as the error catcher should populate the status for us
// Should we listen on other's status (chatstate) or a chatroom state (groupbuddy)?
xmpp.on('error', err => {
if (RED.settings.verbose || LOGITALL) { node.log("XMPP Error: "+err); }
if(!node.quiet) {
node.quiet = true;
if (err.hasOwnProperty("stanza")) {
if (err.stanza.name === 'stream:error') { node.error("stream:error - bad login id/pwd ?",err); }
else { node.error(err.stanza.name,err); }
node.status({fill:"red",shape:"ring",text:"bad login"});
}
else {
if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err);
node.status({fill:"red",shape:"ring",text:"timeout"});
}
if (err.errno === "ENOTFOUND") {
node.error("Server doesn't exist "+xmpp.options.service,err);
node.status({fill:"red",shape:"ring",text:"bad address"});
}
else if (err === "XMPP authentication failure") {
node.error("Authentication failure! "+err,err);
node.status({fill:"red",shape:"ring",text:"XMPP authentication failure"});
}
else if (err.name === "SASLError") {
node.error("Authorization error! "+err.condition,err);
node.status({fill:"red",shape:"ring",text:"XMPP authorization failure"});
}
else if (err == "TimeoutError") {
// Suppress it!
node.warn("Timed out! ");
node.status({fill:"grey",shape:"dot",text:"opening"});
//node.status({fill:"red",shape:"ring",text:"XMPP timeout"});
}
else {
node.error(err,err);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
}
}
}
errorHandler(node, err);
});
// Meat of it, a stanza object contains chat messages (and other things)
xmpp.on('stanza', async (stanza) =>{
if (RED.settings.verbose || LOGITALL) {node.log(stanza);}
// node.log("Received stanza");
if (RED.settings.verbose || LOGITALL) {node.log(stanza); }
if (stanza.is('message')) {
if (stanza.attrs.type == 'chat') {
var body = stanza.getChild('body');
@ -349,18 +362,19 @@ module.exports = function(RED) {
msg.topic = stanza.attrs.from
}
else { msg.topic = ids[0]; }
// if (RED.settings.verbose || LOGITALL) {node.log("Received a message from "+stanza.attrs.from); }
if (!node.join && ((node.from === "") || (node.from === stanza.attrs.to))) {
node.send([msg,null]);
}
}
}
else if(stanza.attrs.type == 'groupchat'){
else if (stanza.attrs.type == 'groupchat') {
const parts = stanza.attrs.from.split("/");
var conference = parts[0];
var from = parts[1];
var body = stanza.getChild('body');
var payload = "";
if("undefined" !== typeof body){
if ("undefined" !== typeof body) {
payload = body.getText();
}
var msg = { topic:from, payload:payload, room:conference };
@ -371,23 +385,23 @@ module.exports = function(RED) {
}
}
}
else if(stanza.is('presence')){
if(['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1){
else if (stanza.is('presence')) {
if (['subscribe','subscribed','unsubscribe','unsubscribed'].indexOf(stanza.attrs.type) > -1) {
// this isn't for us, let the config node deal with it.
}
else{
var statusText="";
if(stanza.attrs.type === 'unavailable'){
if (stanza.attrs.type === 'unavailable') {
// the user might not exist, but the server doesn't tell us that!
statusText = "offline";
}
var status = stanza.getChild('status');
if("undefined" !== typeof status){
if ("undefined" !== typeof status) {
statusText = status.getText();
}
// right, do we care if there's no status?
if(statusText !== ""){
if (statusText !== "") {
var from = stanza.attrs.from;
var state = stanza.attrs.show;
var msg = {topic:from, payload: {presence:state, status:statusText} };
@ -403,16 +417,20 @@ module.exports = function(RED) {
}
});
// xmpp.on('subscribe', from => {
// xmpp.acceptSubscription(from);
// });
//register with config
this.serverConfig.register(this);
// Now actually make the connection
try {
if(xmpp.status === "online"){
if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
}
else{
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if(xmpp.status === "offline"){
if (xmpp.status === "offline") {
if (RED.settings.verbose || LOGITALL) {
node.log("starting xmpp client");
}
@ -505,49 +523,18 @@ module.exports = function(RED) {
xmpp.on('error', function(err) {
if (RED.settings.verbose || LOGITALL) { node.log(err); }
if(!node.quiet) {
node.quiet = true;
if (err.hasOwnProperty("stanza")) {
if (err.stanza.name === 'stream:error') { node.error("stream:error - bad login id/pwd ?",err); }
else { node.error(err.stanza.name,err); }
node.status({fill:"red",shape:"ring",text:"bad login"});
}
else {
if (err.errno === "ETIMEDOUT") {
node.error("Timeout connecting to server",err);
node.status({fill:"red",shape:"ring",text:"timeout"});
}
else if (err.errno === "ENOTFOUND") {
node.error("Server doesn't exist "+xmpp.options.service,err);
node.status({fill:"red",shape:"ring",text:"bad address"});
}
else if (err === "XMPP authentication failure") {
node.error(err,err);
node.status({fill:"red",shape:"ring",text:"XMPP authentication failure"});
}
else if (err == "TimeoutError") {
// OK, this happens with OpenFire, suppress it.
node.status({fill:"grey",shape:"dot",text:"opening"});
node.log("Timed out! ",err);
// node.status({fill:"red",shape:"ring",text:"XMPP timeout"});
}
else {
node.error("Unknown error: "+err,err);
node.status({fill:"red",shape:"ring",text:"node-red:common.status.error"});
}
}
}
errorHandler(node, err)
});
//register with config
this.serverConfig.register(this);
// Now actually make the connection
if(xmpp.status === "online"){
if (xmpp.status === "online") {
node.status({fill:"green",shape:"dot",text:"online"});
}
else{
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
if(xmpp.status === "offline"){
if (xmpp.status === "offline") {
xmpp.start().catch(error => {
node.error("Bad xmpp configuration; service: "+xmpp.options.service+" jid: "+node.serverConfig.jid);
node.warn(error);
@ -562,31 +549,31 @@ module.exports = function(RED) {
if (msg.presence) {
if (['away', 'dnd', 'xa', 'chat'].indexOf(msg.presence) > -1 ) {
var stanza = xml('presence',
{"show":msg.presence},
xml('status',{},msg.payload));
{"show":msg.presence},
xml('status',{},msg.payload));
node.serverConfig.used(node);
xmpp.send(stanza);
}
else { node.warn("Can't set presence - invalid value: "+msg.presence); }
}
else if(msg.command){
if(msg.command === "subscribe"){
else if (msg.command) {
if (msg.command === "subscribe") {
var stanza = xml('presence',
{type:'subscribe', to: msg.payload});
{type:'subscribe', to: msg.payload});
node.serverConfig.used(node);
xmpp.send(stanza);
}
else if(msg.command === "get"){
else if (msg.command === "get") {
var to = node.to || msg.topic || "";
var stanza = xml('iq',
{type:'get', id:node.id, to: to},
xml('query', 'http://jabber.org/protocol/muc#admin',
xml('item',{affiliation:msg.payload})));
{type:'get', id:node.id, to: to},
xml('query', 'http://jabber.org/protocol/muc#admin',
xml('item',{affiliation:msg.payload})));
node.serverConfig.used(node);
if (RED.settings.verbose || LOGITALL) {node.log("sending stanza "+stanza.toString());}
if (RED.settings.verbose || LOGITALL) {node.log("sending stanza "+stanza.toString()); }
xmpp.send(stanza);
}
}
else {
var to = node.to || msg.topic || "";
@ -624,7 +611,7 @@ module.exports = function(RED) {
});
node.on("close", function(removed, done) {
if (RED.settings.verbose || LOGITALL) {node.log("Closing");}
if (RED.settings.verbose || LOGITALL) {node.log("Closing"); }
node.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
node.serverConfig.deregister(node, done);
});

View File

@ -1,6 +1,6 @@
{
"name": "node-red-node-xmpp",
"version": "0.3.0",
"version": "0.3.1",
"description": "A Node-RED node to talk to an XMPP server",
"dependencies": {
"@xmpp/client": "^0.11.1"

View File

@ -69,7 +69,7 @@ module.exports = function(RED) {
this.multi = n.multi || false;
this.operation = n.operation;
this.mongoConfig = RED.nodes.getNode(this.mongodb);
this.status({fill:"grey",shape:"ring",text:RED._("mongodbstatus.connecting")});
this.status({fill:"grey",shape:"ring",text:RED._("mongodb.status.connecting")});
var node = this;
var noerror = true;
@ -83,7 +83,7 @@ module.exports = function(RED) {
}
else {
node.status({fill:"green",shape:"dot",text:RED._("mongodb.status.connected")});
node.clientDb = client.db();
node.client = client;
var db = client.db();
//console.log( db);
noerror = true;
@ -185,7 +185,7 @@ module.exports = function(RED) {
node.on("close", function() {
node.status({});
if (node.tout) { clearTimeout(node.tout); }
if (node.clientDb) { node.clientDb.close(); }
if (node.client) { node.client.close(); }
});
}
RED.nodes.registerType("mongodb out",MongoOutNode);
@ -212,7 +212,7 @@ module.exports = function(RED) {
}
else {
node.status({fill:"green",shape:"dot",text:RED._("mongodb.status.connected")});
node.clientDb = client.db();
node.client = client;
var db = client.db();
noerror = true;
var coll;
@ -274,13 +274,13 @@ module.exports = function(RED) {
}
else if (node.operation === "aggregate") {
msg.payload = (Array.isArray(msg.payload)) ? msg.payload : [];
coll.aggregate(msg.payload, function(err, result) {
coll.aggregate(msg.payload, function(err, cursor) {
if (err) {
node.error(err);
}
else {
cursor.toArray(function(cursorError, cursorDocs) {
console.log(cursorDocs);
//console.log(cursorDocs);
if (cursorError) {
node.error(cursorError);
}
@ -303,7 +303,7 @@ module.exports = function(RED) {
node.on("close", function() {
node.status({});
if (node.tout) { clearTimeout(node.tout); }
if (node.clientDb) { node.clientDb.close(); }
if (node.client) { node.client.close(); }
});
}
RED.nodes.registerType("mongodb in",MongoInNode);

View File

@ -19,7 +19,7 @@ Run the following command in your Node-RED user directory - typically `~/.node-r
```
npm install node-red-node-mongodb
```
Note that this package requires a MongoDB client package at least version 3.6.1 - if you have an older (version 2) client,
Note that this package requires a MongoDB client package at least version 3.6.3 - if you have an older (version 2) client,
you may need to remove that before installing this
```
npm remove mongodb
@ -29,10 +29,10 @@ you may need to remove that before installing this
Usage
-----
Nodes to save and retrieve data in a MongoDB instance - the database server can be local (mongodb//:localhost:27017), remote (mongodb://hostname.network:27017),
replica-set or cluster (mongodb://hostnameA.network:27017,hostnameB.network:27017), and DNS seedlist cluster (mongodb+srv://clustername.network).
Nodes to save and retrieve data in a MongoDB instance - the database server can be local (mongodb//:localhost:27017), remote (mongodb://hostname.network:27017),
replica-set or cluster (mongodb://hostnameA.network:27017,hostnameB.network:27017), and DNS seedlist cluster (mongodb+srv://clustername.network).
Reference [MongoDB docs](https://docs.mongodb.com/manual/reference/connection-string/) to see which connection method (host or clustered) to use for your MongoDB instance.
Reference [MongoDB docs](https://docs.mongodb.com/manual/reference/connection-string/) to see which connection method (host or clustered) to use for your MongoDB instance.
### Input

View File

@ -0,0 +1,38 @@
{
"mongodb": {
"label": {
"host": "ホスト",
"topology":"接続トポロジ",
"connectOptions":"接続オプション",
"port": "ポート",
"database": "データベース",
"username": "ユーザ",
"password": "パスワード",
"server": "サーバ",
"collection": "コレクション",
"operation": "操作",
"onlystore": "msg.payloadオブジェクトのみ保存",
"createnew": "一致しなければ新しいドキュメントを作成",
"updateall": "合致する全ドキュメントを更新"
},
"operation": {
"save": "save",
"insert": "insert",
"update": "update",
"remove": "remove",
"find": "find",
"count": "count",
"aggregate": "aggregate"
},
"status": {
"connecting": "接続中",
"connected": "接続しました",
"error": "エラー"
},
"tip": "<b> Tip:</b> コレクションが設定されていない場合、 <code>msg.collection</code>がコレクション名として使われます。",
"errors": {
"nocollection": "コレクションが定義されていません",
"missingconfig": "mongodbの設定がみつかりません"
}
}
}

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-mongodb",
"version" : "0.2.2",
"description" : "Node-RED nodes to talk to an Mongo database",
"version" : "0.2.5",
"description" : "Node-RED nodes to talk to a Mongo database",
"dependencies" : {
"mongodb" : "^3.6.2"
"mongodb" : "^3.6.3"
},
"repository" : {
"type":"git",

View File

@ -0,0 +1,28 @@
<script type="text/html" data-help-name="sqlite">
<p>Allows access to a SQLite database.</p>
<p>SQL Query sets how the query is passed to the node.</p>
<p>SQL Query <i>Via msg.topic</i> and <i>Fixed Statement</i> uses the <b>db.all</b> operation against the configured database. This does allow INSERTS, UPDATES and DELETES.
By its very nature it is SQL injection... so <i>be careful out there...</i></p>
<p>SQL Type <i>Prepared Statement</i> also uses <b>db.all</b> but sanitizes parameters passed, eliminating the possibility of SQL injection.</p>
<p>SQL Type <i>Batch without response</i> uses <b>db.exec</b> which runs all SQL statements in the provided string. No result rows are returned.</p>
<p>When using <i>Via msg.topic</i> or <i>Batch without response</i> <code>msg.topic</code> must hold the <i>query</i> for the database.</p>
<p>When using Normal or Prepared Statement, the <i>query</i> must be entered in the node config.</p>
<p>Pass in the parameters as an object in <code>msg.params</code> for Prepared Statement. Ex:<br />
<code>msg.params = {<br />
&nbsp;&nbsp;&nbsp;&nbsp;$id:1,<br />
&nbsp;&nbsp;&nbsp;&nbsp;$name:"John Doe"<br />
}</code><br />
Parameter object names must match parameters set up in the Prepared Statement. If you get the error <code>SQLITE_RANGE: bind or column index out of range</code>
be sure to include $ on the parameter object key.<br />
The SQL query for the example above could be: <code>insert into user_table (user_id, user) VALUES ($id, $name);</code></p>
<p>Using any SQL Query, the result is returned in <code>msg.payload</code></p>
<p>Typically the returned payload will be an array of the result rows, (or an error).</p>
<p>You can load SQLite extensions by inputting a <code>msg.extension</code> property containing the full
path and filename.</p>
<p>The reconnect timeout in milliseconds can be changed by adding a line to <b>settings.js</b>
<pre>sqliteReconnectTime: 20000,</pre></p>
</script>
<script type="text/html" data-help-name="sqlitedb">
<p>The default directory for the database file is the user's home directory through which the Node-RED process was started. You can specify absolute path to change it.</p>
</script>

View File

@ -0,0 +1,20 @@
{
"sqlite": {
"label": {
"database": "Database",
"sqlQuery": "SQL Query",
"viaMsgTopic": "Via msg.topic",
"fixedStatement": "Fixed Statement",
"preparedStatement": "Prepared Statement",
"batchWithoutResponse": "Batch without response",
"sqlStatement": "SQL Statement",
"mode": "Mode",
"readWriteCreate": "Read-Write-Create",
"readWrite": "Read-Write",
"readOnly": "Read-Only"
},
"tips": {
"memoryDb": "<b>Note</b>: Setting the database name to <code>:memory:</code> will create a non-persistant in memory database."
}
}
}

View File

@ -0,0 +1,26 @@
<script type="text/html" data-help-name="sqlite">
<p>SQLiteデータベースにアクセスする機能を提供します。</p>
<p>SQLクエリには、本ードへどの様にクエリを渡すかを設定します。</p>
<p><i>msg.topic経由</i><i>固定文</i> のSQLクエリは、設定したデータベースに対して <b>db.all</b> 操作を実行します。これによって、INSERTSとUPDATES、DELETESを利用できます。性質上、SQLインジェクションに<i>注意してください</i></p>
<p><i>事前定義文</i> のSQLタイプも <b>db.all</b> を使用しますが、渡されたパラメータを無害化することで、SQLインジェクションが生じる可能性を排除できます。</p>
<p><i>一括(応答なし)</i> のSQLタイプは、提供された文字列内にある全てのSQLステートメントを実行する <b>db.exec</b> を使用します。結果の行は返されません。</p>
<p><i>msg.topic経由</i> または <i>一括(応答なし)</i> を用いる時、 <code>msg.topic</code> には、データベースに問い合わせるための <i>クエリ</i> が格納されている必要があります。</p>
<p>通常の方法や事前定義文を用いる時、 <i>クエリ</i> はノード設定に入力される必要があります。</p>
<p>事前定義文を用いるためには <code>msg.params</code> をオブジェクトとしてパラメータに渡します。例:<br />
<code>msg.params = {<br />
&nbsp;&nbsp;&nbsp;&nbsp;$id:1,<br />
&nbsp;&nbsp;&nbsp;&nbsp;$name:"John Doe"<br />
}</code><br />
パラメータのオブジェクト名は、事前定義文に設定したパラメータと一致させる必要があります。
もし <code>SQLITE_RANGE: bind or column index out of range</code> というエラーが発生した場合は、バラメータのオブジェクトのキーに$を含めてください。<br />
上の例で用いるSQLクエリは、次の様になります: <code>insert into user_table (user_id, user) VALUES ($id, $name);</code></p>
<p>SQLクエリを使用すると、 <code>msg.payload</code> に結果が返されます。</p>
<p>通常、返されるペイロードは、結果の行から成る配列(またはエラー)になります。</p>
<p>フルパスやファイル名を含む <code>msg.extension</code> プロパティを用いることで、SQLiteの拡張を読み込むこともできます。</p>
<p>ミリ秒単位の再接続タイムアウトは、<b>settings.js</b> に下記の設定を追加することで、変更できます。
<pre>sqliteReconnectTime: 20000,</pre></p>
</script>
<script type="text/html" data-help-name="sqlitedb">
<p>データベースファイルのデフォルトディレクトリは、Node-REDプロセスを開始したユーザのホームディレクトリです。これは絶対パスを用いることで変更できます。</p>
</script>

View File

@ -0,0 +1,20 @@
{
"sqlite": {
"label": {
"database": "データベース",
"sqlQuery": "SQLクエリ",
"viaMsgTopic": "msg.topic経由",
"fixedStatement": "固定文",
"preparedStatement": "事前定義文",
"batchWithoutResponse": "一括(応答なし)",
"sqlStatement": "SQL文",
"mode": "モード",
"readWriteCreate": "読み取り-書き込み-作成",
"readWrite": "読み取り-書き込み",
"readOnly": "読み取りのみ"
},
"tips": {
"memoryDb": "<b>注釈</b>: データベース名に <code>:memory:</code> を設定すると、非永続的なメモリデータベースを作成します。"
}
}
}

View File

@ -1,19 +1,17 @@
<script type="text/html" data-template-name="sqlitedb">
<div class="form-row">
<label for="node-config-input-db"><i class="fa fa-database"></i> Database</label>
<label for="node-config-input-db"><i class="fa fa-database"></i> <span data-i18n="sqlite.label.database"></label>
<input type="text" id="node-config-input-db" placeholder="/tmp/sqlite">
</div>
<div class="form-row">
<label for="node-config-input-mode">Mode</label>
<label for="node-config-input-mode" data-i18n="sqlite.label.mode"></label>
<select id="node-config-input-mode" style="width:70%">
<option value="RWC">Read-Write-Create</option>
<option value="RW">Read-Write</option>
<option value="RO">Read-Only</option>
<option value="RWC" data-i18n="sqlite.label.readWriteCreate"></option>
<option value="RW" data-i18n="sqlite.label.readWrite"></option>
<option value="RO" data-i18n="sqlite.label.readOnly"></option>
</select>
</div>
<div class="form-tips"><b>Note</b>: Setting the database name to <code>:memory:</code>
will create a non-persistant in memory database.</div>
<div class="form-tips" data-i18n="[html]sqlite.tips.memoryDb"></div>
</script>
<script type="text/javascript">
@ -29,27 +27,26 @@
});
</script>
<script type="text/html" data-template-name="sqlite">
<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">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
<div class="form-row">
<label for="node-input-mydb"><i class="fa fa-database"></i> Database</label>
<label for="node-input-mydb"><i class="fa fa-database"></i> <span data-i18n="sqlite.label.database"></label>
<input type="text" id="node-input-mydb">
</div>
<div class="form-row">
<label for=""><i class="fa fa-code"></i> SQL Query</label>
<label for=""><i class="fa fa-code"></i> <span data-i18n="sqlite.label.sqlQuery"></label>
<select id="node-input-sqlquery">
<option value="msg.topic">Via msg.topic</option>
<option value="fixed">Fixed Statement</option>
<option value="prepared">Prepared Statement</option>
<option value="batch">Batch without response</option>
<option value="msg.topic" data-i18n="sqlite.label.viaMsgTopic"></option>
<option value="fixed" data-i18n="sqlite.label.fixedStatement"></option>
<option value="prepared" data-i18n="sqlite.label.preparedStatement"></option>
<option value="batch" data-i18n="sqlite.label.batchWithoutResponse"></option>
</select>
</div>
<div class="form-row" style="margin-bottom: 0px;">
<label for="" style="width: unset;" id="node-input-sqllabel"><i class="fa fa-code"></i> SQL Statement</label>
<label for="" style="width: unset;" id="node-input-sqllabel"><i class="fa fa-code"></i> <span data-i18n="sqlite.label.sqlStatement"></label>
</div>
<div>
<input type="hidden" id="node-input-sql" autofocus="autofocus">
@ -59,35 +56,6 @@
</div>
</script>
<script type="text/html" data-help-name="sqlite">
<p>Allows access to a Sqlite database.</p>
<p>SQL Query sets how the query is passed to the node.</p>
<p>SQL Query <i>Via msg.topic</i> and <i>Fixed Statement</i> uses the <b>db.all</b> operation against the configured database. This does allow INSERTS, UPDATES and DELETES.
By its very nature it is SQL injection... so <i>be careful out there...</i></p>
<p>SQL Type <i>Prepared Statement</i> also uses <b>db.all</b> but sanitizes parameters passed, eliminating the possibility of SQL injection.</p>
<p>SQL Type <i>Batch without response</i> uses <b>db.exec</b> which runs all SQL statements in the provided string. No result rows are returned.</p>
<p>When using <i>Via msg.topic</i> or <i>Batch without response</i> <code>msg.topic</code> must hold the <i>query</i> for the database.</p>
<p>When using Normal or Prepared the <i>query</i> must be entered in the node config.</p>
<p>Pass in the parameters as an object in <code>msg.params</code> for Prepared. Ex:<br />
<code>msg.params = {<br />
&nbsp;&nbsp;&nbsp;&nbsp;$id:1,<br />
&nbsp;&nbsp;&nbsp;&nbsp;$name:"John Doe"<br />
}</code><br />
Parameter object names must match parameters set up in the Prepared Statement. If you get the error <code>SQLITE_RANGE: bind or column index out of range</code>
be sure to include $ on the parameter object key.<br />
The sql query for the example above could be: <code>insert into user_table (user_id, user) VALUES ($id, $name);</code></p>
<p>Using any SQL Query, the result is returned in <code>msg.payload</code></p>
<p>Typically the returned payload will be an array of the result rows, (or an error).</p>
<p>You can load sqlite extensions by inputting a <code>msg.extension</code> property containing the full
path and filename.</p>
<p>The reconnect timeout in milliseconds can be changed by adding a line to <b>settings.js</b>
<pre>sqliteReconnectTime: 20000,</pre></p>
</script>
<script type="text/html" data-help-name="sqlitedb">
<p>The default directory for the database file is the user's home directory through which the NR process was started. You can specify absolute path to change it.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('sqlite',{
category: 'storage-input',

View File

@ -15,74 +15,257 @@ describe('random node', function() {
helper.stopServer(done);
});
});
// ============================================================
it("should be loaded with correct defaults", function(done) {
var flow = [{"id":"n1", "type":"random", "name":"random1", "wires":[[]]}];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
//console.log(n1);
n1.should.have.property("low", 1);
n1.should.have.property("high", 10);
n1.should.have.property("inte", false);
done();
});
});
it('should output an integer between -3 and 3', function(done) {
var flow = [{"id":"n1", "type":"random", low:-3, high:3, inte:true, wires:[["n2"]] },
it ("Test i1 (integer) - DEFAULT no overrides defaults to 1 and 10", function(done) {
var flow = [{id:"n1", type:"random", low: "" , high:"" , inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload");
msg.payload.should.be.approximately(0,3);
msg.payload.toString().indexOf(".").should.equal(-1);
done();
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(1,10);
msg.payload.toString().indexOf(".").should.equal(-1); // see if it's really an integer and not a float...
done();
}
catch(err) { done(err); }
});
n1.emit("input", {payload:"a"});
n1.emit("input", {"test":"Test i1"});
});
});
it('should output an float between 20 and 30', function(done) {
var flow = [{"id":"n1", "type":"random", low:20, high:30, inte:false, wires:[["n2"]] },
it ("Test f1 (float) - DEFAULT no overrides defaults to 1 and 10", function(done) {
var flow = [{id:"n1", type:"random", low:"" , high:"" , inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload");
msg.payload.should.be.approximately(25,5);
msg.payload.toString().indexOf(".").should.not.equal(-1);
done();
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(1.0,9.999);
//msg.payload.toString().indexOf(".").should.not.equal(-1);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {payload:"a"});
n1.emit("input", {"test":"Test f-1"});
});
});
it('should output an integer between -3 and 3 on chosen property - foo', function(done) {
var flow = [{"id":"n1", "type":"random", property:"foo", low:-3, high:3, inte:true, wires:[["n2"]] },
// ============================================================
it ("Test i2 (integer) - FLIP node From = 3 To = -3", function(done) {
var flow = [{id:"n1", type:"random", low: 3, high: -3, inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("foo");
msg.foo.should.be.approximately(0,3);
msg.foo.toString().indexOf(".").should.equal(-1);
done();
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(-3,3);
msg.payload.toString().indexOf(".").should.equal(-1); // slightly dumb test to see if it really is an integer and not a float...
done();
}
catch(err) { done(err); }
});
n1.emit("input", {payload:"a"});
n1.emit("input", {"test":"Test i2"});
});
});
it ("Test f2 (float) - FLIP node From = 3 To = -3", function(done) {
var flow = [{id:"n1", type:"random", low: 3, high: -3, inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(-3.0,3.0);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test f2"});
});
});
// ============================================================
it ("Test i3 (integer) - values in msg From = 2 To = '5', node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(2,5);
msg.payload.toString().indexOf(".").should.equal(-1); // slightly dumb test to see if it really is an integer and not a float...
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test i3", "from":2, "to":'5'});
});
});
it ("Test f3 (float) - values in msg From = 2 To = '5', node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(2.0,5.0);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test f3", "from":2, "to":'5'});
});
});
// ============================================================
it ("Test i4 (integer) - value in msg From = 2, node From = 5 To = '' - node overides From = 5 To defaults to 10", function(done) {
var flow = [{id:"n1", type:"random", low: 5, high:"", inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(5,10);
msg.payload.toString().indexOf(".").should.equal(-1);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test i4", "from": 2});
});
});
it ("Test f4 (float) - value in msg From = 2, node From = 5 To = '' - node wins 'To' defaults to 10", function(done) {
var flow = [{id:"n1", type:"random", low: 5, high:"", inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(5.0,10.0);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test f4", "from": 2});
});
});
// ============================================================
it ("Test i5 (integer) - msg From = '6' To = '9' node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(6,9);
msg.payload.toString().indexOf(".").should.equal(-1); // slightly dumb test to see if it really is an integer and not a float...
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test i5", "from": '6', "to": '9'});
});
});
it ("Test f5 (float) - msg From = '6' To = '9' node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(6.0,9.0);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test f5", "from": '6', "to": '9'});
});
});
// ============================================================
it ("Test i6 (integer) - msg From = 2.4 To = '7.3' node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:true, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(2,7);
msg.payload.toString().indexOf(".").should.equal(-1); // slightly dumb test to see if it really is an integer and not a float...
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test i6", "from": 2.4, "to": '7.3'});
});
});
it ("Test f6 (float) - msg From = 2.4 To = '7.3' node no entries", function(done) {
var flow = [{id:"n1", type:"random", low: "", high: "", inte:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
//console.log(msg);
msg.should.have.a.property("payload");
msg.payload.should.be.within(2.4,7.3);
done();
}
catch(err) { done(err); }
});
n1.emit("input", {"test":"Test f6", "from": 2.4, "to": '7.3'});
});
});
// ============================================================
});

View File

@ -1,9 +1,12 @@
var should = require("should");
// var should = require("should");
var sinon = require('sinon');
//var fs = require("fs");
var fs = require("fs");
var helper = require("node-red-node-test-helper");
var exifNode = require('../../../utility/exif/94-exif.js');
// var exif = require('exif');
var path = require("path");
var image = fs.readFileSync(path.join(__dirname,"test.jpeg"));
describe('exif node', function() {
"use strict";
@ -19,45 +22,47 @@ describe('exif node', function() {
});
it('extracts location data from Exif data of JPEG', function(done) {
var exif = require('exif');
var ExifImage = exif.ExifImage;
//var ExifImage = exif.ExifImage;
// the jpg file is a single black dot but it was originally a photo taken at IBM Hursley
//console.log(process.cwd());
//var data = fs.readFileSync("test/utility/exif/exif_test_image.jpg", null); // extracting genuine exif data to be fed back as the result of the stubbed ExifImage constructor
//var data = fs.readFileSync("exif_test_image.jpg", null); // extracting genuine exif data to be fed back as the result of the stubbed ExifImage constructor
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
var flow = [{id:"exifNode1", type:"exif", mode:"normal", property:"payload", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
var gpsmsg = { gps: { GPSLatitudeRef: 'N',
GPSLatitude: [ 50, 57, 22.4697 ],
GPSLongitudeRef: 'W',
GPSLongitude: [ 1, 22, 1.2467 ],
GPSAltitudeRef: 0,
GPSAltitude: 50,
GPSTimeStamp: [ 7, 32, 2 ],
GPSImgDirectionRef: 'M',
GPSImgDirection: 267,
GPSProcessingMethod: 'ASCII\u0000\u0000\u0000FUSED',
GPSDateStamp: '2014:06:10' }
};
var spy = sinon.stub(exif, 'ExifImage').callsFake(function(arg1,arg2) { arg2(null,gpsmsg); });
// var gpsmsg = { gps: { GPSLatitudeRef: 'N',
// GPSLatitude: [ 50, 57, 22.4697 ],
// GPSLongitudeRef: 'W',
// GPSLongitude: [ 1, 22, 1.2467 ],
// GPSAltitudeRef: 0,
// GPSAltitude: 50,
// GPSTimeStamp: [ 7, 32, 2 ],
// GPSImgDirectionRef: 'M',
// GPSImgDirection: 267,
// GPSProcessingMethod: 'ASCII\u0000\u0000\u0000FUSED',
// GPSDateStamp: '2014:06:10' }
// };
// var stub = sinon.stub(exif, 'ExifImage').callsFake(function(arg1,arg2) {
// console.log("DING",arg1,arg2);
// arg2(null,gpsmsg);
// });
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
msg.location.lat.should.equal(50.95624); // this data is stored in the jpg file
msg.location.lon.should.equal(-1.36701);
exif.ExifImage.restore();
// exif.ExifImage.restore();
msg.location.lat.should.equal(51.04365); // this data is stored in the jpg file
msg.location.lon.should.equal(-1.31525);
done();
});
exifNode1.receive({payload:new Buffer.from("hello")});
exifNode1.receive({payload:image});
});
});
it('extracts location data in Southern and Eastern hemispheres', function(done) {
it.skip('extracts location data in Southern and Eastern hemispheres', function(done) {
var exif = require('exif');
var ExifImage = exif.ExifImage;
// the jpg file is a single black dot but it was originally a photo taken at IBM Hursley
@ -65,7 +70,7 @@ describe('exif node', function() {
//var data = fs.readFileSync("test/utility/exif/exif_test_image.jpg", null); // extracting genuine exif data to be fed back as the result of the stubbed ExifImage constructor
//var data = fs.readFileSync("exif_test_image.jpg", null); // extracting genuine exif data to be fed back as the result of the stubbed ExifImage constructor
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
var gpsmsg = { gps: { GPSLatitudeRef: 'S',
GPSLatitude: [ 50, 57, 22.4697 ],
@ -77,16 +82,18 @@ describe('exif node', function() {
GPSProcessingMethod: 'ASCII\u0000\u0000\u0000FUSED',
GPSDateStamp: '2014:06:10' }
};
var spy = sinon.stub(exif, 'ExifImage').callsFake(function(arg1,arg2) { arg2(null,gpsmsg); });
var spy = sinon.stub(exif, 'ExifImage').callsFake(function(arg1,arg2) {
arg2(null,gpsmsg);
});
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
exif.ExifImage.restore();
msg.location.lat.should.equal(-50.95624); // this data is stored in the jpg file
msg.location.lon.should.equal(1.36701);
exif.ExifImage.restore();
done();
});
@ -99,9 +106,8 @@ describe('exif node', function() {
var ExifImage = exif.ExifImage;
// this time just use a buffer that isn't an jpeg image
var data = new Buffer.from("hello");
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
@ -125,9 +131,8 @@ describe('exif node', function() {
var exif = require('exif');
var ExifImage = exif.ExifImage;
var data = "hello";
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
@ -151,9 +156,8 @@ describe('exif node', function() {
var exif = require('exif');
var ExifImage = exif.ExifImage;
var data = new Buffer.from("hello");
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
var flow = [{id:"exifNode1", type:"exif", property:"payload", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
@ -165,7 +169,7 @@ describe('exif node', function() {
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("No payload received, ");
logEvents[0][0].msg.toString().should.startWith("No input received, ");
done();
},150);
@ -173,13 +177,12 @@ describe('exif node', function() {
});
});
it('should report if bad latitude', function(done) {
it.skip('should report if bad latitude', function(done) {
var exif = require('exif');
var ExifImage = exif.ExifImage;
var data = new Buffer.from("hello");
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
var gpsmsg = { gps: { GPSLatitudeRef: 'N',
GPSLatitude: [ 50, 57 ],
@ -189,23 +192,22 @@ describe('exif node', function() {
GPSAltitude: 50,
GPSTimeStamp: [ 7, 32, 2 ] }
};
var spy = sinon.stub(exif, 'ExifImage').callsFake(
function(arg1,arg2){
arg2(null,gpsmsg);
});
var spy = sinon.stub(exif, 'ExifImage').callsFake( function(arg1,arg2){
arg2(null,gpsmsg);
});
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
var helperNode1 = helper.getNode("helperNode1");
setTimeout(function() {
exif.ExifImage.restore();
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "exif";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("Invalid latitude data,");
exif.ExifImage.restore();
done();
},150);
@ -213,13 +215,13 @@ describe('exif node', function() {
});
});
it('should report if bad longitude', function(done) {
it.skip('should report if bad longitude', function(done) {
var exif = require('exif');
var ExifImage = exif.ExifImage;
var data = new Buffer.from("hello");
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
var gpsmsg = { gps: { GPSLatitudeRef: 'N',
GPSLatitude: [ 50, 57, 1.3 ],
@ -229,23 +231,22 @@ describe('exif node', function() {
GPSAltitude: 50,
GPSTimeStamp: [ 7, 32, 2 ] }
};
var spy = sinon.stub(exif, 'ExifImage').callsFake(
function(arg1,arg2){
arg2(null,gpsmsg);
});
var spy = sinon.stub(exif, 'ExifImage').callsFake( function(arg1,arg2){
arg2(null,gpsmsg);
});
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
var helperNode1 = helper.getNode("helperNode1");
setTimeout(function() {
exif.ExifImage.restore();
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "exif";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("Invalid longitude data,");
exif.ExifImage.restore();
done();
},150);
@ -253,13 +254,13 @@ describe('exif node', function() {
});
});
it('should report if unsure about location', function(done) {
it.skip('should report if unsure about location', function(done) {
var exif = require('exif');
var ExifImage = exif.ExifImage;
var data = new Buffer.from("hello");
var eD;
var flow = [{id:"exifNode1", type:"exif", wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
{id:"helperNode1", type:"helper"}];
var gpsmsg = { gps: { GPSLatitudeRef: 'N',
GPSLatitude: [ 50, 57, 1.3 ],
@ -267,30 +268,26 @@ describe('exif node', function() {
GPSAltitude: 50,
GPSTimeStamp: [ 7, 32, 2 ] }
};
var spy = sinon.stub(exif, 'ExifImage').callsFake(
function(arg1,arg2){
arg2(null,gpsmsg);
});
var spy = sinon.stub(exif, 'ExifImage').callsFake( function(arg1,arg2) {
arg2(null,gpsmsg);
});
helper.load(exifNode, flow, function() {
var exifNode1 = helper.getNode("exifNode1");
var helperNode1 = helper.getNode("helperNode1");
setTimeout(function() {
exif.ExifImage.restore();
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "exif";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("The location of this image cannot be determined safely");
exif.ExifImage.restore();
done();
},150);
exifNode1.receive({payload:data});
});
});
});

BIN
test/utility/exif/test.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,9 +1,9 @@
{
"name" : "node-red-node-timeswitch",
"version" : "0.0.7",
"version" : "0.0.8",
"description" : "A Node-RED node to provide a simple timeswitch to schedule daily on/off events.",
"dependencies" : {
"suncalc": "1.6.0"
"suncalc": "^1.8.0"
},
"repository" : {
"type":"git",

View File

@ -1,5 +1,5 @@
<script type="text/x-red" data-template-name="timeswitch">
<script type="text/html" data-template-name="timeswitch">
<div class="form-row">
<label for="node-input-starttime"><i class="fa fa-clock-o"></i> Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;On</label>
<select id="node-input-starttime" style="width:24% !important">
@ -99,8 +99,8 @@
<option value="1395">23:15</option>
<option value="1410">23:30</option>
<option value="1425">23:45</option>
<option value="5000">Dawn</option>
<option value="6000">Dusk</option>
<option value="5000">Sunrise</option>
<option value="6000">Sunset</option>
</select>
<div style="display:inline-block; width:12%; text-align:right;">Off</div>
<select id="node-input-endtime" style="width:24% !important">
@ -200,8 +200,8 @@
<option value="1395">23:15</option>
<option value="1410">23:30</option>
<option value="1425">23:45</option>
<option value="5000">Dawn</option>
<option value="6000">Dusk</option>
<option value="5000">Sunrise</option>
<option value="6000">Sunset</option>
<option value="10001">Start + 1 min</option>
<option value="10002">Start + 2 mins</option>
<option value="10005">Start + 5 mins</option>
@ -222,9 +222,9 @@
</div>
<div class="form-row" id="offsetrow">
<label for="node-input-dawnoff">Offset&nbsp;:&nbsp;Dawn</label>
<label for="node-input-dawnoff">Offset&nbsp;:&nbsp;Sunrise</label>
<input type="text" id="node-input-dawnoff" placeholder="0" style="width:22%">
<div style="display:inline-block; width:12%; text-align:right;">Dusk</div>
<div style="display:inline-block; width:12%; text-align:right;">Sunset</div>
<input type="text" id="node-input-duskoff" placeholder="0" style="width:22%">
</div>
@ -266,11 +266,11 @@
</div>
</script>
<script type="text/x-red" data-help-name="timeswitch">
<script type="text/html" data-help-name="timeswitch">
<p>Timeswitch node to schedule daily on/off events.</p>
<p>Sets <code>msg.payload</code> to 1 at on time, and 0 at off time.</p>
<p>Also allows the use of dawn and dusk.</p>
<p>Dawn and dusk times can be offset both positively (+ve) for minutes later
<p>Also allows the use of sunrise and sunset.</p>
<p>Sunrise and sunset times can be offset both positively (+ve) for minutes later
or negatively (-ve) for minutes earlier.</p>
<p>The output emits a <code>msg.payload</code> of <i>1</i> or <i>0</i> every minute depending on
whether the current time is during the selected on time or off time.</p>

View File

@ -0,0 +1,13 @@
Copyright 2020 OpenJS Foundation and other contributors, https://openjsf.org/
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.

View File

@ -0,0 +1,74 @@
node-red-node-annotate-image
==================
A <a href="http://nodered.org" target="_new">Node-RED</a> node that can annotate
a JPEG image.
The node is currently limited to drawing rectangles and circles over the image.
That can be used, for example, to annotate an image with bounding boxes of features
detected in the image by a TensorFlow node.
Install
-------
Either use the Edit Menu - Manage Palette option to install, or run the following command in your Node-RED user directory - typically `~/.node-red`
npm install node-red-node-annotate-image
Usage
-----
The JPEG image should be passed to the node as a Buffer object under `msg.payload`.
The annotations are provided in <code>msg.annotations</code> and are applied in order.
Each annotation is an object with the following properties:
- `type` (*string*) : the type of the annotation - `rect` or `circle`
- `x`/`y` (*number*) : the top-left corner of a `rect` annotation, or the center of a `circle` annotation.
- `w`/`h` (*number*) : the width and height of a `rect` annotation
- `r` (*number*) : the radius of a `circle` annotation
- `bbox` (*array*) : this can be used instead of `x`, `y`, `w`, `h` and `r`.
It should be an array of four values giving the bounding box of the annotation:
`[x, y, w, h]`. If this property is set and `type` is not set, it will default to `rect`.
- `label` (*string*) : an optional piece of text to label the annotation with
- `stroke` (*string*) : the line color of the annotation. Default: `#ffC000`
- `lineWidth` (*number*) : the stroke width used to draw the annotation. Default: `5`
- `fontSize` (*number*) : the font size to use for the label. Default: `24`
- `fontColor` (*string*) : the color of the font to use for the label. Default: `#ffC000`
- `labelLocation` (*string*) : The location to place the label. `top` or `bottom`.
If this propery is not set it will default to `automatic` and make the best guess based on location.
Examples
--------
```javascript
msg.annotations = [ {
type: "rect",
x: 10, y: 10, w: 50, h: 50,
label: "hello"
}]
```
```javascript
msg.annotations = [
{
type: "circle",
x: 50, y: 50, r: 20,
lineWidth: 10
},
{
type: "rect",
x: 30, y: 30, w: 40, h: 40,
stroke: "blue"
}
]
```
```javascript
msg.annotations = [ {
type: "rect",
bbox: [ 10, 10, 50, 50]
}]
```

Binary file not shown.

View File

@ -0,0 +1,166 @@
<script type="text/html" data-template-name="annotate-image">
<div class="form-row">
<span id="node-input-row-stroke">
<label for="node-input-stroke">Line Color</label>
</span>
<label style="margin-left: 20px" for="node-input-lineWidth">Line Width</label>
<input style="width: 50px" type="text" id="node-input-lineWidth">
</div>
<div class="form-row">
<span id="node-input-row-fontColor">
<label for="node-input-fontColor">Font Color</label>
</span>
<label style="margin-left: 20px" for="node-input-fontSize">Font Size</label>
<input style="width: 50px" type="text" id="node-input-fontSize">
</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/html" data-help-name="annotate-image">
<p>A node that can annotate JPEG images with simple shapes and labels.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload<span class="property-type">Buffer</span></dt>
<dd>A Buffer containing a JPEG image. Support for PNG will come soon.</dd>
<dt>annotations<span class="property-type">Array</span></dt>
<dd>An array of annotations to apply to the image. See below for details
of the annotation format.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>payload<span class="property-type">Buffer</span></dt>
<dd>The image with any annotations applied.</dd>
</dl>
<h3>Details</h3>
<p>The annotations provided in <code>msg.annotations</code> are applied in order.
Each annotation is an object with the following properties:</p>
<dl class="message-properties">
<dt>type<span class="property-type">string</span></dt>
<dd><ul>
<li><code>"rect"</code> - draws a rectangle</li>
<li><code>"circle"</code> - draws a circle</li>
</dd>
<dt>x,y <span class="property-type">number</span></dt>
<dd>The top-left corner of a <code>rect</code> annotation, or the center of a <code>circle</code> annotation.</dd>
<dt>w,h <span class="property-type">number</span></dt>
<dd>The width and height of a <code>rect</code> annotation.</dd>
<dt>r <span class="property-type">number</span></dt>
<dd>The radius of a <code>circle</code> annotation.</dd>
<dt>bbox <span class="property-type">array</span></dt>
<dd>This can be used instead of <code>x</code>,<code>y</code>,<code>w</code>,<code>h</code> and <code>r</code>. It should
be an array of four values giving the bounding box of the annotation: <code>[x, y, w, h]</code>.<br>
If this property is set and <code>type</code> is not set, it will default to <code>rect</code>.</dd>
<dt>label <span class="property-type">string</span></dt>
<dd>An optional piece of text to label the annotation with</dd>
<dt>stroke <span class="property-type">string</span></dt>
<dd>The line color of the annotation. Default: <code>"#ffC000"</code></dd>
<dt>lineWidth <span class="property-type">number</span></dt>
<dd>The stroke width used to draw the annotation. Default: <code>5</code></dd>
<dt>fontSize <span class="property-type">number</span></dt>
<dd>The font size to use for the label. Default: <code>24</code></dd>
<dt>fontColor <span class="property-type">string</span></dt>
<dd>The color of the font to use for the label. Default: <code>"#ffC000"</code></dd>
<dt>labelLocation <span class="property-type">string</span></dt>
<dd>The location to place the label. Can be set to <code>top</code> or <code>bottom</code>.
Default: <code>"automatic"</code>.</dd>
</dl>
<h3>Examples</h3>
<pre> msg.annotations = [ {
type: "rect",
x: 10, y: 10, w: 50, h: 50,
label: "hello"
}]</pre>
<pre> msg.annotations = [ {
type: "circle",
x: 50, y: 50, r: 20
}]</pre>
<pre> msg.annotations = [ {
type: "rect",
bbox: [ 10, 10, 50, 50]
}]</pre>
</script>
<script type="text/javascript">
(function() {
// Lifted from @node-red/editor-client/.../group.js
// Need to make this a default built-in palette so we don't have to copy
// it around.
var colorPalette = [
"#ff0000",
"#ffC000",
"#ffff00",
"#92d04f",
"#0070c0",
"#001f60",
"#6f2fa0",
"#000000",
"#777777"
]
var colorSteps = 3;
var colorCount = colorPalette.length;
for (var i=0,len=colorPalette.length*colorSteps;i<len;i++) {
var ci = i%colorCount;
var j = Math.floor(i/colorCount)+1;
var c = colorPalette[ci];
var r = parseInt(c.substring(1, 3), 16);
var g = parseInt(c.substring(3, 5), 16);
var b = parseInt(c.substring(5, 7), 16);
var dr = (255-r)/(colorSteps+((ci===colorCount-1) ?0:1));
var dg = (255-g)/(colorSteps+((ci===colorCount-1) ?0:1));
var db = (255-b)/(colorSteps+((ci===colorCount-1) ?0:1));
r = Math.min(255,Math.floor(r+j*dr));
g = Math.min(255,Math.floor(g+j*dg));
b = Math.min(255,Math.floor(b+j*db));
var s = ((r<<16) + (g<<8) + b).toString(16);
colorPalette.push('#'+'000000'.slice(0, 6-s.length)+s);
}
RED.nodes.registerType('annotate-image',{
category: 'utility',
color:"#f1c2f0",
defaults: {
name: {value:""},
fill: {value:""},
stroke: {value:"#ffC000"},
lineWidth: {value:5},
fontSize: {value: 24},
fontColor: {value: "#ffC000"}
},
inputs:1,
outputs:1,
icon: "font-awesome/fa-object-group",
label: function() {
return this.name||"annotate image";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
RED.colorPicker.create({
id:"node-input-stroke",
value: this.stroke || "#ffC000",
palette: colorPalette,
cellPerRow: colorCount,
cellWidth: 16,
cellHeight: 16,
cellMargin: 3
}).appendTo("#node-input-row-stroke");
RED.colorPicker.create({
id:"node-input-fontColor",
value: this.fontColor || "#ffC000",
palette: colorPalette,
cellPerRow: colorCount,
cellWidth: 16,
cellHeight: 16,
cellMargin: 3
}).appendTo("#node-input-row-fontColor");
}
});
})();
</script>

View File

@ -0,0 +1,192 @@
module.exports = function(RED) {
"use strict";
const pureimage = require("pureimage");
const Readable = require("stream").Readable;
const Writable = require("stream").Writable;
const path = require("path");
let fontLoaded = false;
function loadFont() {
if (!fontLoaded) {
const fnt = pureimage.registerFont(path.join(__dirname,'./SourceSansPro-Regular.ttf'),'Source Sans Pro');
fnt.load();
fontLoaded = true;
}
}
function AnnotateNode(n) {
RED.nodes.createNode(this,n);
var node = this;
const defaultFill = n.fill || "";
const defaultStroke = n.stroke || "#ffC000";
const defaultLineWidth = parseInt(n.lineWidth) || 5;
const defaultFontSize = n.fontSize || 24;
const defaultFontColor = n.fontColor || "#ffC000";
loadFont();
this.on("input", function(msg) {
if (Buffer.isBuffer(msg.payload)) {
if (msg.payload[0] !== 0xFF || msg.payload[1] !== 0xD8) {
node.error("Not a JPEG image",msg);
} else if (Array.isArray(msg.annotations) && msg.annotations.length > 0) {
const stream = new Readable();
stream.push(msg.payload);
stream.push(null);
pureimage.decodeJPEGFromStream(stream).then(img => {
const c = pureimage.make(img.width, img.height);
const ctx = c.getContext('2d');
ctx.drawImage(img,0,0,img.width,img.height);
ctx.lineJoin = 'bevel';
msg.annotations.forEach(function(annotation) {
ctx.fillStyle = annotation.fill || defaultFill;
ctx.strokeStyle = annotation.stroke || defaultStroke;
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
ctx.lineJoin = 'bevel';
let x,y,r,w,h;
if (!annotation.type && annotation.bbox) {
annotation.type = 'rect';
}
switch(annotation.type) {
case 'rect':
if (annotation.bbox) {
x = annotation.bbox[0]
y = annotation.bbox[1]
w = annotation.bbox[2]
h = annotation.bbox[3]
} else {
x = annotation.x
y = annotation.y
w = annotation.w
h = annotation.h
}
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
ctx.beginPath();
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
ctx.rect(x,y,w,h);
ctx.closePath();
ctx.stroke();
if (annotation.label) {
ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`;
ctx.fillStyle = annotation.fontColor || defaultFontColor;
ctx.textBaseline = "top";
ctx.textAlign = "left";
//set offset value so txt is above or below image
if (annotation.labelLocation) {
if (annotation.labelLocation === "top") {
y = y - (20+((defaultLineWidth*0.5)+(Number(defaultFontSize))));
if (y < 0)
{
y = 0;
}
}
else if (annotation.labelLocation === "bottom") {
y = y + (10+h+(((defaultLineWidth*0.5)+(Number(defaultFontSize)))));
ctx.textBaseline = "bottom";
}
}
//if not user defined make best guess for top or bottom based on location
else {
//not enought room above imagebox, put label on the bottom
if (y < 0 + (20+((defaultLineWidth*0.5)+(Number(defaultFontSize))))) {
y = y + (10+h+(((defaultLineWidth*0.5)+(Number(defaultFontSize)))));
ctx.textBaseline = "bottom";
}
//else put the label on the top
else {
y = y - (20+((defaultLineWidth*0.5)+(Number(defaultFontSize))));
if (y < 0) {
y = 0;
}
}
}
ctx.fillText(annotation.label, x,y);
}
break;
case 'circle':
if (annotation.bbox) {
x = annotation.bbox[0] + annotation.bbox[2]/2
y = annotation.bbox[1] + annotation.bbox[3]/2
r = Math.min(annotation.bbox[2],annotation.bbox[3])/2;
} else {
x = annotation.x
y = annotation.y
r = annotation.r;
}
ctx.beginPath();
ctx.lineWidth = annotation.lineWidth || defaultLineWidth;
ctx.arc(x,y,r,0,Math.PI*2);
ctx.closePath();
ctx.stroke();
if (annotation.label) {
ctx.font = `${annotation.fontSize || defaultFontSize}pt 'Source Sans Pro'`;
ctx.fillStyle = annotation.fontColor || defaultFontColor;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText(annotation.label, x+2,y)
}
break;
}
});
const bufferOutput = getWritableBuffer();
pureimage.encodeJPEGToStream(c,bufferOutput.stream,90).then(() => {
msg.payload = bufferOutput.getBuffer();
node.send(msg);
})
}).catch(err => {
node.error(err,msg);
})
} else {
// No annotations to make - send the message on
node.send(msg);
}
} else {
node.error("Payload not a Buffer",msg)
}
return msg;
});
}
RED.nodes.registerType("annotate-image",AnnotateNode);
function getWritableBuffer() {
var currentSize = 0;
var buffer = null;
const stream = new Writable({
write(chunk, encoding, callback) {
if (!buffer) {
buffer = Buffer.from(chunk);
} else {
var newBuffer = Buffer.allocUnsafe(currentSize + chunk.length);
buffer.copy(newBuffer);
chunk.copy(newBuffer,currentSize);
buffer = newBuffer;
}
currentSize += chunk.length
callback();
}
});
return {
stream: stream,
getBuffer: function() {
return buffer;
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"name": "node-red-node-annotate-image",
"version": "0.1.1",
"description": "A Node-RED node that can annotate an image",
"dependencies": {
"pureimage": "^0.2.5"
},
"repository": {
"type": "git",
"url": "https://github.com/node-red/node-red-nodes/tree/master/utility/iamge-annotate"
},
"license": "Apache-2.0",
"keywords": [
"node-red"
],
"node-red": {
"nodes": {
"annotate": "annotate.js"
}
},
"contributors": [
{
"name": "Nick O'Leary"
}
]
}

View File

@ -1,17 +1,31 @@
<script type="text/x-red" data-template-name="exif">
<script type="text/html" data-template-name="exif">
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-dot-circle-o"></i> Mode</label>
<select style="width:70%" id="node-input-mode">
<option value="normal">Standard node</option>
<option value="worldmap">Use with Worldmap-in node</option>
</select>
</div>
<div class="form-row" id="node-exif-prop-select">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</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="exif">
<script type="text/html" data-help-name="exif">
<p>Extract <a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format">Exif</a> information from JPEG images.</p>
<p>This node expects an incoming JPEG image buffer. If Exif data is present,
<p>This node expects an incoming JPEG image buffer in the selected property. If Exif data is present,
it extracts the data into the <code>msg.exif</code> object.</p>
<p>The node then adds location data as <code>msg.location</code>, should the Exif data carry this information.
<code>msg.payload</code> remains the original, unmodified image buffer. </p>
This also includes an icon, bearing and field of view arc suitable for use in the worldmap node.
The selected input property retains the original, unmodified image buffer.</p>
<p>If configured to use the worldmap in node then the existing image payload will be replaced by the location
object so that it can be fed back to the map directly.</p>
</script>
<script type="text/javascript">
@ -19,7 +33,9 @@
category: 'utility',
color:"#f1c2f0",
defaults: {
name: {value:""}
name: {value:""},
mode: {value:"normal"},
property: {value:"payload",required:true}
},
inputs:1,
outputs:1,
@ -29,6 +45,17 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) { $("#node-input-property").val("payload"); }
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-mode").change(function() {
if ($("#node-input-mode").val() === "worldmap") {
$("#node-exif-prop-select").hide();
}
else { $("#node-exif-prop-select").show(); }
});
}
});
</script>

View File

@ -1,7 +1,6 @@
module.exports = function(RED) {
"use strict";
var ExifImage = require('exif').ExifImage;
function convertDegreesMinutesSecondsToDecimals(degrees, minutes, seconds) {
var result;
@ -11,7 +10,11 @@ module.exports = function(RED) {
function ExifNode(n) {
RED.nodes.createNode(this,n);
this.mode = n.mode || "normal";
if (this.mode === "worldmap") { this.property = "payload.content"; }
else { this.property = n.property || "payload"; }
var node = this;
var ExifImage = require('exif').ExifImage;
/***
* Extracts GPS location information from Exif data. If enough information is
@ -20,7 +23,7 @@ module.exports = function(RED) {
* Assumes that the msg object will always have exifData available as msg.exif.
* Assume that the GPS data saved into Exif provides a valid value
*/
function addMsgLocationDataFromExifGPSData(msg) {
function addMsgLocationDataFromExifGPSData(msg,val) {
var gpsData = msg.exif.gps; // declaring variable purely to make checks more readable
if (gpsData.GPSAltitude) {
/* istanbul ignore else */
@ -31,14 +34,12 @@ module.exports = function(RED) {
// The data provided in Exif is in degrees, minutes, seconds, this is to be converted into a single floating point degree
if (gpsData.GPSLatitude.length === 3) { // OK to convert latitude
if (gpsData.GPSLongitude.length === 3) { // OK to convert longitude
var latitude = convertDegreesMinutesSecondsToDecimals(gpsData.GPSLatitude[0], gpsData.GPSLatitude[1], gpsData.GPSLatitude[2]);
latitude = Math.round(latitude * 100000)/100000; // 5dp is approx 1m resolution...
// (N)orth means positive latitude, (S)outh means negative latitude
if (gpsData.GPSLatitudeRef.toString() === 'S' || gpsData.GPSLatitudeRef.toString() === 's') {
latitude = latitude * -1;
}
var longitude = convertDegreesMinutesSecondsToDecimals(gpsData.GPSLongitude[0], gpsData.GPSLongitude[1], gpsData.GPSLongitude[2]);
longitude = Math.round(longitude * 100000)/100000; // 5dp is approx 1m resolution...
// (E)ast means positive longitude, (W)est means negative longitude
@ -49,7 +50,6 @@ module.exports = function(RED) {
if (!msg.location) { msg.location = {}; }
msg.location.lat = latitude;
msg.location.lon = longitude;
return;
}
else {
node.log("Invalid longitude data, no location information has been added to the message.");
@ -62,21 +62,49 @@ module.exports = function(RED) {
else {
node.log("The location of this image cannot be determined safely so no location information has been added to the message.");
}
msg.location.arc = {
ranges: [500,1000,2000],
pan: gpsData.GPSImgDirection,
fov: (2 * Math.atan(36 / (2 * msg.exif.exif.FocalLengthIn35mmFormat)) * 180 / Math.PI),
color: '#910000'
}
msg.location.icon = "fa-camera";
var na;
if (val.hasOwnProperty("name")) { na = val.name; }
else if (msg.hasOwnProperty("filename")) { na = msg.filename.split('/').pop(); }
else { na = msg.exif.image.Make+"_"+msg.exif.image.ModifyDate; }
msg.location.name = na;
msg.location.layer = "Images";
msg.location.popup = '<img width="280" src="data:image/jpeg;base64,'+val.toString("base64")+'"/>'
}
this.on("input", function(msg) {
if (node.mode === "worldmap" && Buffer.isBuffer(msg.payload)) { node.property = "payload"; }
else if (node.mode === "worldmap" && (msg.payload.action !== "file" || msg.payload.type.indexOf("image") === -1)) { return; } // in case worldmap-in not filtered.
try {
if (msg.payload) {
if (Buffer.isBuffer(msg.payload)) {
new ExifImage({ image : msg.payload }, function (error, exifData) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string") { // it must be a base64 encoded inline image type
if (value.indexOf('data:image') !== -1) {
value = new Buffer.from(value.replace(/^data:image\/[a-z]+;base64,/, ""), 'base64');
}
}
if (Buffer.isBuffer(value)) { // or a proper jpg buffer
new ExifImage({ image:value }, function (error, exifData) {
if (error) {
node.log(error.toString());
if (node.mode !== "worldmap") {
node.log(error.toString());
}
else {
msg.location = {name:msg.payload.name, lat:msg.payload.lat, lon:msg.payload.lon, layer:"Images", icon:"fa-camera", draggable:true};
msg.location.popup = '<img width="280" src="data:image\/png;base64,'+msg.payload.content.toString('base64')+'"/><br/>';
}
}
else {
if (exifData) {
msg.exif = exifData;
if ((exifData.hasOwnProperty("gps")) && (Object.keys(exifData.gps).length !== 0)) {
addMsgLocationDataFromExifGPSData(msg);
addMsgLocationDataFromExifGPSData(msg,value);
}
//else { node.log("The incoming image did not contain Exif GPS data."); }
}
@ -84,6 +112,10 @@ module.exports = function(RED) {
node.warn("The incoming image did not contain any Exif data, nothing to do.");
}
}
if (node.mode === "worldmap") {
msg.payload = msg.location;
delete msg.location;
}
node.send(msg);
});
}
@ -93,7 +125,7 @@ module.exports = function(RED) {
}
}
else {
node.error("No payload received, the Exif node cannot proceed, no messages sent.",msg);
node.warn("No input received, the Exif node cannot proceed, no messages sent.",msg);
return;
}
}

View File

@ -1,23 +1,30 @@
{
"name" : "node-red-node-exif",
"version" : "0.0.7",
"description" : "A Node-RED node that extracts Exif information from JPEG image buffers.",
"dependencies" : {
"exif": "0.4.0"
"name": "node-red-node-exif",
"version": "0.2.1",
"description": "A Node-RED node that extracts Exif information from JPEG image buffers.",
"dependencies": {
"exif": "^0.6.0"
},
"repository" : {
"type":"git",
"url":"https://github.com/node-red/node-red-nodes/tree/master/utility/exif"
"repository": {
"type": "git",
"url": "https://github.com/node-red/node-red-nodes/tree/master/utility/exif"
},
"license": "Apache-2.0",
"keywords": [ "node-red", "exif"],
"node-red" : {
"nodes" : {
"keywords": [
"node-red",
"exif"
],
"node-red": {
"nodes": {
"exif": "94-exif.js"
}
},
"contributors": [
{"name": "Dave Conway-Jones"},
{"name": "Zoltan Balogh"}
{
"name": "Dave Conway-Jones"
},
{
"name": "Zoltan Balogh"
}
]
}