ncd-red-industrial
Version:
351 lines (348 loc) • 8.71 kB
JavaScript
const events = require("events");
const Queue = require("promise-queue");
module.exports = class NcdSerialProXR{
constructor(connection){
this.interactive = {};
this.comm = connection;
this.buff = [];
this.queue = new Queue(1);
this.tout = false;
this.queueCBs = {fulfill: false, reject: false};
var obj = this;
this.comm.on('data', function(d){
if(obj.queueCBs.fulfill){
obj.buff.push(d);
var valid = obj.validate();
if(valid === true){
var fulfill = obj.queueCBs.fulfill;
obj.queueCBs = {fulfill: false, reject: false};
var payload = obj.buff.slice(2, -1);
obj.buff = [];
clearTimeout(obj.tout);
fulfill(payload);
}else if(valid !== false){
obj.buff = [];
var reject = obj.queueCBs.reject;
obj.queueCBs = {fulfill: false, reject: false};
clearTimeout(obj.tout);
reject({'Communication Error': valid});
}
}
});
this.comm.on('error', (err) => {
var reject = obj.queueCBs.reject;
obj.queueCBs = {fulfill: false, reject: false};
reject({'Communication error': err});
});
}
init(){
return this.send([254,33]);
}
validate(){
if(this.buff.length){
var len = this.buff.length;
if(this.buff[0] == 170){
if(len > 3 && this.buff[1]+3 == len){
var valid = this.buff[len-1] == ((this.buff.reduce((t,i) => t+i) - this.buff[len-1]) & 255);
if(!valid){
return this.buff;
}
return true;
}
}else{
return 'bad header';
}
}
return false;
}
wrapApi(payload){
var packet = [170, payload.length];
packet.push.apply(packet, payload);
packet.push(packet.reduce((t,i) => t+i)&255);
return Buffer.from(packet);
}
send(payload){
var obj = this;
var cmd = this.queue.add(() => {
return new Promise((fulfill, reject) => {
obj.queueCBs.reject = reject;
obj.queueCBs.fulfill = fulfill;
obj.tout = setTimeout(() => {
obj.buff = [];
obj.queueCBs = {fulfill: false, reject: false};
//TODO: we need a way to not do this until after the connection is available
reject({'Communication Error': 'Timeout'});
}, 3000);
var bytes = obj.wrapApi(payload);
obj.comm.write(bytes);
});
});
return new Promise((fulfill, reject) => {
cmd.then((res) => {
fulfill(res);
}).catch(reject);
});
}
relayStatus(r, b){
if(r > 8){
_r = r % 8;
b = (r - _r) / 8;
r = _r;
}
if(typeof b == 'undefined') b = 1;
return this.send([254, r+115, b]);
}
bankStatus(b){
return this.send([254,124,b]);
}
_relayOp(o,r){
var cmd = [254];
switch(o){
case 'toggle':
cmd.push(47);
cmd.push(r & 255);
cmd.push(r >> 8);
cmd.push(1);
break;
case 'on':
cmd.push(48);
cmd.push(r & 255);
cmd.push(r >> 8);
break;
case 'off':
cmd.push(47);
cmd.push(r & 255);
cmd.push(r >> 8);
break;
}
return this.send(cmd);
}
relayOp(o,r,b){
if(o == 'invert') o='toggle';
if(typeof b == 'undefined'){
return this._relayOp(o,parseInt(r)-1);
}else if(parseInt(b) == 0){
}else{
return this._relayOp(o,(8 * (parseInt(b)-1)) + (parseInt(r)-1));
}
}
bankOp(op, b, g){
if(g == 'all'){
var cmd = 0;
switch(op){
case 'reverse':
cmd+=1;
case 'invert':
cmd+=1;
case 'on':
cmd+=1;
case 'off':
cmd+=129;
}
return this.send([254,cmd,b]);
}
var val;
var o = op == 'on' ? 1 : 0;
switch(g){
case 'odd':
val = 85;
break;
case 'even':
val = 170;
break;
default:
if(!isNaN(g)){
val = 100 + (g-1) + (op == 'on' ? 8 : 0);
return this.send([254,val,parseInt(b)])
}
}
return this.send([254,140,val,parseInt(b)])
}
findTimer(r){
var promises = [];
for(var t=0;t<16;t++){
promises.push(this.send([254, 50, 130, t]));
}
return new Promise((fulfill, reject) => {
Promise.all(promises).then((r) => {
var found = r.some((status, t) => {
if(status[3] == r || status.slice(0,3).reduce((a,b) => a+b) == 0){
fulfill(t);
return true;
}
});
if(!found) reject(false);
}).catch(reject)
});
}
timerOp(op, r, d){
var obj = this;
return new Promise((fulfill, reject) => {
obj.findTimer(r).then((t) => {
if(op == 'in') t+=20;
obj.send([254,50,t+50,d.h,d.m,d.s,r]).then(fulfill).catch(reject);
}).catch(reject);
});
}
command(cmd){
var seconds = 0;
if(cmd.duration){
cmd.relay = parseInt(cmd.relay)-1;
if(typeof cmd.bank != 'undefined'){
cmd.relay += ((cmd.bank-1) * 8);
}
if(typeof cmd.duration != 'object'){
var duration = parseInt(cmd.duration);
cmd.duration = {
h: 0,
m: 0,
s: 0,
}
if(cmd.scale.indexOf('second') == 0){
if(duration > 255){
cmd.duration.s = 255;
duration = Math.ceil((duration - 255)/60);
cmd.scale = 'minutes';
}else{
cmd.duration.s = duration;
}
}
if(cmd.scale.indexOf('minute') == 0){
if(duration > 255){
cmd.duration.m = 255;
duration = Math.ceil((duration - 255)/60);
cmd.scale = 'hours';
}else{
cmd.duration.m = duration;
}
}
if(cmd.scale.indexOf('hour') == 0){
cmd.duration.h = duration & 255;
}
}
return this.timerOp(cmd.mtype, cmd.relay, cmd.duration);
//handle momentary
}else{
['off','activate','deactivate','toggle','on'].some((v) => {
if(v == 'activate') v = 'on';
if(v == 'deactivate') v = 'off';
if(cmd.op.indexOf(v) > -1) return cmd.op = v;
});
}
if(!cmd.relay || cmd.bank == 0 || cmd.banks){
if(typeof cmd.bank == 'undefined') cmd.bank = 0;
if(typeof cmd.group == 'undefined') cmd.group = cmd.relay ? cmd.relay : 'all';
return this.bankOp(cmd.op, cmd.bank, cmd.group);
}
return this.relayOp(cmd.op, cmd.relay, cmd.bank);
}
relayTalk(str){
var groups = ['get', 'relay', 'group', 'bank', 'banks', 'op', 'mtype', 'duration', 'scale'];
var m;
var regex = /(get)|(?:(?:relay|channel) ([0-9]+))|(all|odd|even)|bank ([0-9]+)|(all) banks|(off|activate|deactivate|toggle|on)|(for|in) ([0-9]+) ([^\s]+)/gmi;
var res = {};
while ((m = regex.exec(str.toLowerCase())) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
// The result can be accessed through the `m`-variable.
for(var i=1;i<groups.length+1;i++){
var g = groups[i-1];
if(g == 'op'){
if(typeof res.op == 'undefined') res.op = [];
res.op.push(m[i]);
}else{
if(typeof m[i] != 'undefined') res[g] = m[i];
}
}
}
if(res.get){
this.interactive = res;
return this.status();
}
return this.command(res);
}
bank(b){
this.interactive.bank = b;
return this;
}
channel(r){return this.relay(r);}
relay(r){
this.interactive.relay = r;
return this;
}
activate(){return this.on();}
on(){
this.interactive.op = 'on';
var cmd = this.interactive;
this.interactive = {};
return this.command(cmd);
}
deactivate(){return this.off();}
off(){
this.interactive.op = 'off';
var cmd = this.interactive;
this.interactive = {};
return this.command(cmd);
}
toggle(){
this.interactive.op = 'toggle';
var cmd = this.interactive;
this.interactive = {};
return this.command(cmd);
}
pulse(d, s){
this.interactive.mtype = 'in';
this.interactive.duration = d;
this.interactive.scale = typeof s == 'undefined' ? 'seconds' : s;
var cmd = this.interactive;
this.interactive = {};
return this.command(cmd);
}
timer(d, s){
this.interactive.mtype = 'for';
this.interactive.duration = d;
this.interactive.scale = typeof s == 'undefined' ? 'seconds' : s;
var cmd = this.interactive;
this.interactive = {};
return this.command(cmd);
}
status(){
if(typeof this.interactive.relay == 'undefined'){
var bank = typeof this.interactive.bank == 'undefined' ? 1 : parseInt(this.interactive.bank);
this.interactive = {};
return this.bankStatus(bank);
}
var ret = this.relayStatus(parseInt(this.interactive.relay), parseInt(this.interactive.bank));
this.interactive = {};
return ret;
}
//b stands for bits!
readAD(b){
var obj = this;
var cmd = this.interactive;
this.interactive = {};
return new Promise((fulfill, reject) => {
if(b == 10){
obj.send([254, 167]).then((r) => {
if(typeof cmd.relay == 'undefined'){
fulfill(r);
}else{
var i = (cmd.relay-1)*2;
var val = (r[i] << 8) | r[i+1];
fulfill(val);
}
}).catch(reject);
}else{
obj.send([254, 166]).then((r) => {
if(typeof cmd.relay == 'undefined'){
fulfill(r);
}else{
fulfill(r[cmd.relay-1]);
}
}).catch(reject);
}
});
}
}