vytronics.hmi
Version:
Vytronics HMI server. Core components Vytronics HMI - The 100% Free, Open-Source, SCADA/HMI Initiative
295 lines (232 loc) • 9.28 kB
JavaScript
/*
Copyright 2014 Charles Weissman
This file is part of "Vytroncs HMI, the 100% Free, Open-Source SCADA/HMI Initiative"
herein referred to as "Vytronics HMI".
Vytronics HMI is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Vytronics HMI is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Vytronics HMI. If not, see <http://www.gnu.org/licenses/>.
*/
var util = require("util");
var vyutil = require('./vyutil');
var events = require("events");
var db = require("./db");
var log = require("log4js").getLogger('clientdb');
log.setLevel(vyutil.getenv('VYTRONICS_CLIENTDB_LOG_LEVEL', 'warn'));
exports.version = '0.0.0';
var clients = [];
//Listen for tag changes
db.tagdb.on('tagChanged', function(id, changes) {
var data = {
id: id,
value: changes.value
//TODO - custom message for each subscription based on
// fields wanted
};
clients.forEach(function (client) {
//Socket connected clients do quick and dirty. They only need
//single emit if any match
if ( client.is_inprocess !== true ) {
if(client.containsSubscription(id)) {
//network clients never have callback, use null
client.tagChangeCallback(null, id, data);
}
}
else { //This is an in process client. Need to check every subscription
//since each may have its own callback
var matches = [];
client.subscriptions.forEach( function (sub){
if (sub.regex) {
if (id.match(sub.regex)){
matches.push(sub);
}
}
else if (sub.tagid === id){
matches.push(sub);
}
});
matches.forEach( function (match){
client.tagChangeCallback(match.callback,
match.regex? match.regex.toString(): match.tagid,
data);
});
}
});
});
var createClient = function(emitter) {
var name;
var is_inprocess = false;
//If null or string emitter then this is an in process client. Create an emitter
//for it
if (!emitter) {
name = db.GUID(); //Just give it a random name
}
if ( "string" === typeof(emitter) ){
//TODO - dont allow identical names
name = emitter;
emitter = new events.EventEmitter();
is_inprocess = true;
}
else {
name = db.GUID();
}
emitter.on('disconnect', function () {
clients.forEach( function(client) {
if(client.emitter === emitter) {
//TODO - need to clean any client stuff up?
log.info("client disconnected:" + client.guid);
clients.splice(clients.indexOf(client), 1);
}
});
});
var client = new Client(emitter,name, is_inprocess);
clients.push(client);
return client;
};
function Client(emitter,guid, is_inprocess) {
//TODO - add username etc.
this.guid = guid,
this.emitter = emitter;
this.subscriptions = [];
this.is_inprocess = is_inprocess; //TODO - be able to detect by emitter type?
log.info("client logged on. GUID:" + guid);
var self = this;
emitter.on('app_call', function(funcName, call_data, callback) {
var call_err = 0;
var result = 0;
try {
result = db.rpcdb.invoke(funcName, call_data);
} catch(err) {
log.error('app_call:' + err.message,err,err.stack);
call_err = err.message;
}
if(!callback) return;
//The args have to be JSON strigifyable. Not sure there is a
//test for this so any issues will result in unexpected transmission
//without any warning. User functions need have the precondition that
//the only return strigifyable results
callback(result, call_err); //can this have more than one param?
});
emitter.on('subscribeTag', function(tagid, ackfunc) {
log.debug('clientdb.subscribeTag tagid:' + tagid);
//TODO - refractor this to return an object { result:, err: } where
//err is just a string instead of array?
var result = self.subscribeTag(tagid);
if(ackfunc) {
ackfunc(result);
}
});
emitter.on('unsubscribeTag', function(tagid, guid, ackfunc) {
//TODO - refractor this to return an object { result:, err: } where
//err is just a string instead of array
var result = self.unsubscribeTag(tagid, guid);
if(ackfunc) {
ackfunc(result);
}
});
};
//Subscribe to a tagid
//If callback is supplied then call it on each change. This would be for in-process clients
//Otherwise the changes will be emitted to network client
//
Client.prototype.subscribeTag = function(tagid, callback) {
var guid = db.GUID(); //create unique serial code so client can delete
var tag = db.tagdb.getTag(tagid);
if (!tag) {
log.warn("client:" + this.guid +" subscribeTag tagid:"+tagid+" no matches?");
return undefined;
}
//Subscribe and also immediately emit current data
//Send on very next tick to refresh client. If we send now it will be received
//before client gets the acknowledge/guid
var self = this;
process.nextTick( function (){
var data = {
id: tag.id,
guid: guid, //serial number for this subscription
value: tag.value
//TODO - custom message for each subscription based on
// fields wanted
};
self.tagChangeCallback(callback, tag.id, data);
});
this.subscriptions.push( {tagid:tagid, guid:guid, callback: callback} );
return guid; //Ack the subscription.
};
//Subscribe via regex match of tagid
//If callback is supplied then call it on each change. This would be for in-process clients
//Otherwise the changes will be emitted to network client
//
Client.prototype.subscribeTagRegex = function (tagid_regex, callback){
var guid = db.GUID(); //create unique serial code so client can delete
var matches = db.tagdb.getTagsRegex(tagid_regex);
if ( 0 === matches.length ) {
log.warn("client:" + this.guid +" subscribeTagRegex tagid:"+tagid_regex.toString()+" no matches?");
return undefined;
}
this.subscriptions.push( {tagid:tagid_regex.toString, guid:guid, regex: tagid_regex, callback: callback} );
//Subscribe and also immediately emit current data
//Send on very next tick to refresh client. If we send now it will be received
//before client gets the acknowledge/guid
var self = this;
var regex_str = tagid_regex.toString(); //use id of regex string
process.nextTick( function (){
matches.forEach( function(tag){
var data = {
id: tag.id,
guid: guid, //serial number for this subscription
value: tag.value
//TODO - custom message for each subscription based on
// fields wanted
};
self.tagChangeCallback(callback, regex_str, data);
});
});
return guid; //Ack the subscription.
};
Client.prototype.tagChangeCallback = function (callback, id, data){
if (!callback){
this.emitter.emit('tagChanged', id , data);
}
else {
try {
callback(id, data);
}
catch (err){
log.error('client guid:' + this.guid + ' tagid:' + id + ' tagchange callback exception:' + err.message);
}
}
}
Client.prototype.unsubscribeTag = function(tagid, guid) {
var self = this;
var remove = [];
var keep = [];
self.subscriptions.forEach( function(sub) {
if ( sub.guid === guid ) {
remove.push(sub);
}
else {
keep.push(sub);
}
}, self);
self.subscriptions = keep;
//TODO - real return code. Return blank guid if any errors
//Ack the unsubscribe. TODO return undefined if any error?
return guid;
};
Client.prototype.containsSubscription = function(tagid) {
for(var i=0; i<this.subscriptions.length; i++){
var sub = this.subscriptions[i];
//If subscription is a regex then execute it, otherwise do straight compare
var match = sub.regex? tagid.match(sub.regex): (sub.tagid === tagid);
if (match) return true;
}
return false;
};
exports.createClient = createClient;