node-red-contrib-digital-factory
Version:
Node-Red nodes for Supply Chain Wizard's Digital Factory Platform
451 lines (392 loc) • 20.1 kB
JavaScript
module.exports = function(RED) {
// Main function called by Node-RED
function scwDigitalFactoryNode(config) {
RED.nodes.createNode(this,config);
let node = this;
var flowContext = this.context().flow;
var localTwin = {};
// Retrieve the config node
node.hub = RED.nodes.getNode(config.hub);
node.portal = RED.nodes.getNode(config.portal);
const statusEnum = {
idle: { color: "gray", text: "Idle" },
disconnected: { color: "red", text: "Disconnected" },
connected: { color: "green", text: "Connected" },
sending: { color: "blue", text: "Sending message" },
sent: { color: "blue", text: "Sent message" },
received: { color: "yellow", text: "Received" },
error: { color: "red", text: "Error" },
twinUpdated: { color: "blue", text: "Twin Updated"},
uploadError: { color: "gray", text: "Upload Error" },
uploaded: { color: "blue", text: "File Uploaded" },
twinReceived : { color: "blue", text: "Desired Props Received" }
};
var setStatus = function (status) {
node.status({ fill: status.color, shape: "dot", text: status.text });
}
//setStatus(statusEnum.connected);
let Protocol = require('azure-iot-device-mqtt').Mqtt;
let Client = require('azure-iot-device').Client;
let Message = require('azure-iot-device').Message;
if(!node.hub || !node.hub.deviceId || !node.hub.credentials.deviceKey) {return}
let deviceId = ""
if (node.hub.deviceId && node.hub.deviceIdType) {
let msg = {}
RED.util.evaluateNodeProperty(node.hub.deviceId,node.hub.deviceIdType,node,msg,(err,ivalue) => {
if (err) {
node.error(err)
} else {
deviceId = ivalue
}
});
}
let deviceKey = ""
if (node.hub.deviceKey && node.hub.deviceKeyType) {
let msg = {}
RED.util.evaluateNodeProperty(node.hub.deviceKey,node.hub.deviceKeyType,node,msg,(err,ivalue) => {
if (err) {
node.error(err)
} else {
deviceKey = ivalue
}
});
}
if(!deviceId || !deviceKey) {return}
let deviceConnectionString = "HostName=scw.azure-devices.net;DeviceId="+deviceId+";SharedAccessKey="+deviceKey
let client = Client.fromConnectionString(deviceConnectionString, Protocol);
function connectCallback (err) {
if (err) {
node.error('Could not connect: ' + err.message);
client.close();
client = undefined;
//client.open(connectCallback);
return
}
setStatus(statusEnum.connected)
node.log('Connected to IoT Hub');
// Create device Twin
try {client.getTwin(function(err, twin) {
if (err) {
node.error('could not get twin');
} else {
flowContext.set("twinProps", twin.properties)
//flowContext.set("twin", twin)
localTwin = twin.properties
twin.on('properties.desired', function(delta) {
setStatus(statusEnum.twinReceived);
let msgToSend;
msgToSend = {topic : "twinUpdate", payload: delta}
node.send(msgToSend)
Object.keys(delta).forEach(function(key) {
if (delta[key] === null && localTwin.desired[key]) {
// If our patch contains a null value, but we have a record of
// this module, then this is a delete operation.
delete localTwin[key];
} else if (delta[key]) {
if (localTwin.desired[key]) {
// Our patch contains a module, and we've seen this before.
// Must be an update operation.
// Store the complete object instead of just the delta
localTwin.desired[key] = twin.properties.desired[key];
} else {
// Our patch contains a module, but we've never seen this
// before. Must be an add operation.
// Store the complete object instead of just the delta
//console.log(twin.properties.desired)
localTwin.desired[key] = twin.properties.desired[key];
}
}
});
// flowContext.set("twinProps", localTwin)
});
}
})
} catch (error) {node.error(error)}
try {client.onDeviceMethod('customMethod', onCustomMethod);} catch (error) {node.error(error)};
try {client.onDeviceMethod('systemMethod', onSystemMethod);} catch (error) {node.error(error)};
function onCustomMethod(request, response) {
node.status({ fill: "blue", shape: "dot", text: "Received " + request.methodName });
flowContext.set(request.methodName + "_" + request.requestId, {request: request, response: response})
let msgToSend;
msgToSend = {topic : request.methodName, messageId : request.requestId, payload: request.payload}
if(request.payload != null && typeof request.payload !== "undefined") {msgToSend.action = request.payload.action}
node.send(msgToSend, false)
}
function backup(response, filename, includeLogs) {
try {
let stream = require('stream')
let archiver = require("archiver")
let streamifier = require("streamifier")
let deviceDate = new Date();
if (filename && filename != "") {
filename = "backup_app-" + filename + ".zip"
} else {
filename = "backup_app-" + deviceId + "-" + deviceDate.getUTCFullYear() + "-" +
(deviceDate.getUTCMonth() + 1) + "-" + deviceDate.getUTCDate() +
"-" + deviceDate.getUTCHours() + "-" + deviceDate.getUTCMinutes() +
"-" + deviceDate.getUTCSeconds() + ".zip"
}
class WritableBufferStream extends stream.Writable {
constructor(options) {
super(options);
this._chunks = [];
}
_write (chunk, enc, callback) {
this._chunks.push(chunk);
return callback(null);
}
_destroy(err, callback) {
this._chunks = null;
return callback(null);
}
toBuffer() {
return Buffer.concat(this._chunks);
}
}
let writeStream = new WritableBufferStream();
// wait for the writing to finish
writeStream.on('finish', () => {
// console log pdf as bas64 string
let myBuffer = writeStream.toBuffer()
let myStream = streamifier.createReadStream(myBuffer);
client.uploadToBlob(filename, myStream, myBuffer.byteLength, function (err) {
if (err) {response.send(500,'Error uploading file: ' + err.toString())
} else {
response.send(200,'File uploaded : ' + filename)
}
});
});
let archive = archiver('zip', {
zlib: { level: 9 } // Sets the compression level.
});
archive.pipe(writeStream);
let workingDir = RED.settings.userDir;
if (includeLogs) {
archive.glob('!(node_modules|.npm)',{
cwd: workingDir,
dot:true
});
archive.glob('!(node_modules|.npm)/**',{
cwd: workingDir,
dot:true
});
} else {
archive.glob('!(node_modules|.npm|logs)',{
cwd: workingDir,
dot:true
});
archive.glob('!(node_modules|.npm|logs)/**',{
cwd: workingDir,
dot:true
});
}
archive.finalize();
} catch (error) {
console.log(error)
}
}
function onSystemMethod(request, response) {
node.status({ fill: "blue", shape: "dot", text: "Received " + request.methodName });
if(request.payload === null || typeof request.payload == "undefined") {response.send(500, 'Payload Required'); return}
let os = require("os")
let isWin = (os.platform() === 'win32');
switch (request.payload.action) {
case "screenshot":
if (!isWin) {response.send(500, 'Only supported on Windows systems'); return;}
let nircmd = require('nircmd');
let deviceDate = new Date();
let filename = request.payload.filename
if (filename && filename != "") {
filename = "screenshot-" + filename + ".jpg"
} else {
filename = "screenshot-" + deviceId + "-" + deviceDate.getUTCFullYear() + "-" +
(deviceDate.getUTCMonth() + 1) + "-" + deviceDate.getUTCDate() +
"-" + deviceDate.getUTCHours() + "-" + deviceDate.getUTCMinutes() +
"-" + deviceDate.getUTCSeconds() + ".jpg"
}
nircmd('savescreenshot ' + filename).then(() => {
let fs = require('fs');
fs.stat(filename, function (err, stats) {
const rr = fs.createReadStream(filename);
client.uploadToBlob(filename, rr, stats.size, function (err) {
if (err) {response.send(500,'Error uploading file: ' + err.toString());
fs.unlink(filename, (err) => {
if (err) {
console.error(err)
return
}
//file removed
})
} else {
response.send(200,'File uploaded : ' + filename)
fs.unlink(filename, (err) => {
if (err) {
console.error(err)
return
}
//file removed
})
}
});
});
});
break;
case "restart_app":
try {
let { spawnSync } = require( 'child_process' );
let cmd = spawnSync( 'pm2', [ 'restart', 'node-red' ] );
response.send(200,JSON.stringify({stderr: cmd.stderr.toString(), stdout: cmd.stdout.toString()}));
} catch (error) {
console.log(error)
}
break;
case "shutdown_app":
try {
let { spawnSync } = require( 'child_process' );
let cmd = spawnSync( 'pm2', [ 'stop', 'node-red' ] );
response.send(200,JSON.stringify({stderr: cmd.stderr.toString(), stdout: cmd.stdout.toString()}));
} catch (error) {
console.log(error)
}
break;
case "npm":
try {
let { spawnSync } = require( 'child_process' );
let cmd = spawnSync( 'npm', request.payload.args );
response.send(200,JSON.stringify({stderr: cmd.stderr.toString(), stdout: cmd.stdout.toString()}));
} catch (error) {
console.log(error)
}
break;
case "backup":
backup(response, null, request.payload.includeLogs)
break;
case "ping":
response.send(200, 'Pong');
break;
default:
response.send(500, 'Action not supported');
break;
}
}
client.on('disconnect', function () {
if (client) {
try {
node.warn('Disconnecting from Azure IoT Hub');
client.removeAllListeners();
client = null;
setStatus(statusEnum.disconnected);
} catch (error) {
node.error("Disconnect : " + JSON.stringify(error))
}
}
});
client.on('error', function (err) {
node.error("Client Error : " + JSON.stringify(err.message));
});
client.on('message', function (msg) {
// We received a message
// node.log('Message received from Azure IoT Hub\n Id: ' + msg.messageId + '\n Payload: ' + msg.data);
let receivedMessage = {}
try {
receivedMessage.payload = JSON.parse(msg.data.toString())
} catch (error) {
receivedMessage.payload = msg.data.toString()
}
receivedMessage.topic = "c2d"
receivedMessage.messageId = "-"
node.send(receivedMessage);
client.complete(msg, function () {});
});
}
client.open(connectCallback);
node.on('input', function(msg, send, done) {
if (!client) {
try {
node.log('Reconnecting to IoT Hub');
client = Client.fromConnectionString(deviceConnectionString, Protocol);
client.open(connectCallback);
} catch (error) {
node.error("case 1 " + JSON.stringify(error))
done(error)
}
}
switch (msg.topic) {
case "twinUpdate":
// create a patch to send to the hub
let patch = msg.payload;
// send the patch
let localTwin = flowContext.get("twinProps")
localTwin.reported.update(patch, function(err) {
if (err) done(err);
setStatus(statusEnum.twinUpdated)
});
break;
case "telemetry":
let data = JSON.stringify(msg.payload);
let message = new Message(data);
client.sendEvent(message, function (err) {
if (err) {
done(err)
} else {
setStatus(statusEnum.sent)
done();}
});
break
case "fileUpload":
let streamifier = require('streamifier');
let path = require("path");
let filename = path.basename(msg.filename);
let payload = msg.payload
if (!filename) {done("filename must be provided")}
if (!payload) {done("may.payload must be provided")}
let myStream = streamifier.createReadStream(msg.payload);
client.uploadToBlob(filename, myStream, msg.payload.byteLength, function (err) {
if (err) {
setStatus(statusEnum.uploadError); done('Error uploading file: ' + err.toString());
} else {
setStatus(statusEnum.uploaded)
done();
}
});
break
default:
done("Unknown Topic Received");
break;
}
});
node.on('close', function(removed, done) {
if (removed) {
// This node has been deleted
node.log('Disconnecting from IoT Hub');
client.removeAllListeners();
client.close( function() { } );
client = null;
}
else {
//node restarted
try {
node.log('Disconnecting from IoT Hub');
client.removeAllListeners();
client.close( function() { } );
node.log('Reconnecting to IoT Hub');
client = Client.fromConnectionString(deviceConnectionString, Protocol);
client.open(connectCallback);
} catch (error) {
node.log(error)
}
}
if (done) {done();}
});
}
// Registration of the node into Node-RED
RED.nodes.registerType("digitalfactory",scwDigitalFactoryNode,
{
defaults: {
name: {value:""},
hub: {value:"", type:"DF Device Hub"},
portal: {value: "", type:"DF Portal"},
}
}
);
}