node-red-contrib-serialport-rs485
Version:
Node-RED nodes to talk to serial ports with RS485/Autoswitch RTS
365 lines (345 loc) • 16.4 kB
JavaScript
module.exports = function(RED) {
"use strict";
var settings = RED.settings;
var events = require("events");
var serialp = require("serialport");
var bufMaxSize = 32768; // Max serial buffer size, for inputs...
// TODO: 'serialPool' should be encapsulated in SerialPortNode
function SerialPortNode(n) {
RED.nodes.createNode(this,n);
this.serialport = n.serialport;
this.newline = n.newline;
this.addchar = n.addchar || "false";
this.serialbaud = parseInt(n.serialbaud) || 57600;
this.databits = parseInt(n.databits) || 8;
this.parity = n.parity || "none";
this.stopbits = parseInt(n.stopbits) || 1;
this.bin = n.bin || "false";
this.out = n.out || "char";
this.flowcontrol = n.flowcontrol || "none";
}
RED.nodes.registerType("RS485 serial-port",SerialPortNode);
function SerialOutNode(n) {
RED.nodes.createNode(this,n);
this.serial = n.serial;
this.serialConfig = RED.nodes.getNode(this.serial);
if (this.serialConfig) {
var node = this;
node.port = serialPool.get(this.serialConfig.serialport,
this.serialConfig.serialbaud,
this.serialConfig.databits,
this.serialConfig.parity,
this.serialConfig.stopbits,
this.serialConfig.newline,
this.serialConfig.flowcontrol);
node.addCh = "";
if (node.serialConfig.addchar == "true" || node.serialConfig.addchar === true) {
node.addCh = this.serialConfig.newline.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0"); // jshint ignore:line
}
node.on("input",function(msg) {
if (msg.hasOwnProperty("payload")) {
var payload = msg.payload;
if (!Buffer.isBuffer(payload)) {
if (typeof payload === "object") {
payload = JSON.stringify(payload);
} else {
payload = payload.toString();
}
payload += node.addCh;
} else if (node.addCh !== "") {
payload = Buffer.concat([payload,new Buffer(node.addCh)]);
}
node.port.write(payload,function(err,res) {
if (err) {
var errmsg = err.toString().replace("Serialport","Serialport "+node.port.serial.path);
node.error(errmsg,msg);
}
});
}
});
node.port.on('ready', function() {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
});
node.port.on('closed', function() {
node.status({fill:"red",shape:"ring",text:"node-red:common.status.not-connected"});
});
} else {
this.error(RED._("serial.errors.missing-conf"));
}
this.on("close", function(done) {
if (this.serialConfig) {
serialPool.close(this.serialConfig.serialport,done);
} else {
done();
}
});
}
RED.nodes.registerType("RS485 serial out",SerialOutNode);
function SerialInNode(n) {
RED.nodes.createNode(this,n);
this.serial = n.serial;
this.serialConfig = RED.nodes.getNode(this.serial);
if (this.serialConfig) {
var node = this;
node.tout = null;
var buf;
if (node.serialConfig.out != "count") { buf = new Buffer(bufMaxSize); }
else { buf = new Buffer(Number(node.serialConfig.newline)); }
var i = 0;
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.not-connected"});
node.port = serialPool.get(this.serialConfig.serialport,
this.serialConfig.serialbaud,
this.serialConfig.databits,
this.serialConfig.parity,
this.serialConfig.stopbits,
this.serialConfig.newline,
this.serialConfig.flowcontrol
);
var splitc;
if (node.serialConfig.newline.substr(0,2) == "0x") {
splitc = new Buffer([parseInt(node.serialConfig.newline)]);
} else {
splitc = new Buffer(node.serialConfig.newline.replace("\\n","\n").replace("\\r","\r").replace("\\t","\t").replace("\\e","\e").replace("\\f","\f").replace("\\0","\0")); // jshint ignore:line
}
this.port.on('data', function(msg) {
// single char buffer
if ((node.serialConfig.newline === 0)||(node.serialConfig.newline === "")) {
if (node.serialConfig.bin !== "bin") { node.send({"payload": String.fromCharCode(msg)}); }
else { node.send({"payload": new Buffer([msg])}); }
}
else {
// do the timer thing
if (node.serialConfig.out === "time") {
if (node.tout) {
i += 1;
buf[i] = msg;
}
else {
node.tout = setTimeout(function () {
node.tout = null;
var m = new Buffer(i+1);
buf.copy(m,0,0,i+1);
if (node.serialConfig.bin !== "bin") { m = m.toString(); }
node.send({"payload": m});
m = null;
}, node.serialConfig.newline);
i = 0;
buf[0] = msg;
}
}
// count bytes into a buffer...
else if (node.serialConfig.out === "count") {
buf[i] = msg;
i += 1;
if ( i >= parseInt(node.serialConfig.newline)) {
var m = new Buffer(i);
buf.copy(m,0,0,i);
if (node.serialConfig.bin !== "bin") { m = m.toString(); }
node.send({"payload":m});
m = null;
i = 0;
}
}
// look to match char...
else if (node.serialConfig.out === "char") {
buf[i] = msg;
i += 1;
if ((msg === splitc[0]) || (i === bufMaxSize)) {
var n = new Buffer(i);
buf.copy(n,0,0,i);
if (node.serialConfig.bin !== "bin") { n = n.toString(); }
node.send({"payload":n});
n = null;
i = 0;
}
}
}
});
this.port.on('ready', function() {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
});
this.port.on('closed', function() {
node.status({fill:"red",shape:"ring",text:"node-red:common.status.not-connected"});
});
} else {
this.error(RED._("serial.errors.missing-conf"));
}
this.on("close", function(done) {
if (this.serialConfig) {
serialPool.close(this.serialConfig.serialport,done);
} else {
done();
}
});
}
RED.nodes.registerType("RS485 serial in",SerialInNode);
var serialPool = (function() {
var connections = {};
return {
get:function(port,baud,databits,parity,stopbits,newline,flowcontrol,callback) {
var id = port;
if (!connections[id]) {
connections[id] = (function() {
var obj = {
_emitter: new events.EventEmitter(),
serial: null,
_closing: false,
tout: null,
on: function(a,b) { this._emitter.on(a,b); },
close: function(cb) { this.serial.close(cb); },
beginSend:function(cb){
var comport=this.serial;
switch(flowcontrol){
case "rts-on":
this.debug_log('Switch RTS-ON');
comport.set({"rts": true},cb);
break;
case "rts-off":
this.debug_log('Switch RTS-OFF');
comport.set({"rts": false},cb);
break;
case "dtr-on":
this.debug_log('Switch dtr-ON');
comport.set({"dtr": true},cb);
break;
case "dtr-off":
this.debug_log('Switch dtr-OFF');
comport.set({"dtr": false},cb);
break;
default:
if(cb)
cb();
}
},
endSend:function(cb){
var comport=this.serial;
switch(flowcontrol){
case "rts-on":
this.debug_log('Switch RTS-OFF');
comport.set({"rts": false},cb);
break;
case "rts-off":
this.debug_log('Switch RTS-ON');
comport.set({"rts": true},cb);
break;
case "dtr-on":
this.debug_log('Switch DTR-OFF');
comport.set({"dtr": false},cb);
break;
case "dtr-off":
this.debug_log('Switch DTR-ON');
comport.set({"dtr": true},cb);
break;
default:
if(cb)
cb();
}
},
debug_log:function(title,data){
console.log('[' + new Date().toJSON() + '] ',title,data);
},
write: function(m,cb) {
var me=this;
var lom=m.length;
var msPerByte = 11 / baud;
var msWait = lom*msPerByte*1000;
var comport=this.serial;
me.beginSend(function(){
me.debug_log('SEND',m);
comport.write(m,function(){
me.debug_log('WAIT '+msWait+' msec');
setTimeout(function(){
me.endSend(cb);
},msWait);
});
});
},
}
//newline = newline.replace("\\n","\n").replace("\\r","\r");
var olderr = "";
var setupSerial = function() {
obj.serial = new serialp(port,{
baudrate: baud,
databits: databits,
parity: parity,
stopbits: stopbits,
parser: serialp.parsers.raw,
autoOpen: true,
rtscts:false
}, function(err, results) {
if (err) {
if (err.toString() !== olderr) {
olderr = err.toString();
RED.log.error(RED._("serial.errors.error",{port:port,error:olderr}));
}
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
}
});
obj.serial.on('error', function(err) {
RED.log.error(RED._("serial.errors.error",{port:port,error:err.toString()}));
obj._emitter.emit('closed');
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
});
obj.serial.on('close', function() {
if (!obj._closing) {
RED.log.error(RED._("serial.errors.unexpected-close",{port:port}));
obj._emitter.emit('closed');
obj.tout = setTimeout(function() {
setupSerial();
}, settings.serialReconnectTime);
}
});
obj.serial.on('open',function() {
obj.endSend();
olderr = "";
RED.log.info(RED._("serial.onopen",{port:port,baud:baud,config: databits+""+parity.charAt(0).toUpperCase()+stopbits}));
if (obj.tout) { clearTimeout(obj.tout); }
//obj.serial.flush();
obj._emitter.emit('ready');
});
obj.serial.on('data',function(d) {
obj.debug_log('REC',d);
for (var z=0; z<d.length; z++) {
obj._emitter.emit('data',d[z]);
}
});
obj.serial.on("disconnect",function() {
RED.log.error(RED._("serial.errors.disconnected",{port:port}));
});
}
setupSerial();
return obj;
}());
}
return connections[id];
},
close: function(port,done) {
if (connections[port]) {
if (connections[port].tout != null) {
clearTimeout(connections[port].tout);
}
connections[port]._closing = true;
try {
connections[port].close(function() {
RED.log.info(RED._("serial.errors.closed",{port:port}));
done();
});
}
catch(err) { }
delete connections[port];
} else {
done();
}
}
}
}());
RED.httpAdmin.get("/serialports", RED.auth.needsPermission('serial.read'), function(req,res) {
serialp.list(function (err, ports) {
res.json(ports);
});
});
}