UNPKG

vytronics.hmi

Version:

Vytronics HMI server. Core components Vytronics HMI - The 100% Free, Open-Source, SCADA/HMI Initiative

484 lines (406 loc) 15.3 kB
/* 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/>. */ //This module implements the Tags database singleton // //Events emitted //============================== //tagdb.tagChanged(id, changeData) - Emitted everytime a tag changes. //Note that when a client subscribes to a tag change the server will (if the //client desires) refresh with the current tag data. This event is only for //actual changes. // // id = ID of the tag that changed // data = Change data object containing one property (key) for each data // that has changed. For example if only the value has changed: // { value: <new value } // If the value and another app defined field such as"stateText" has changed: // { value: <new value>, stateText:<some text> } // var vyutil = require("./vyutil"); var events = require("events"); var db = require("./db"); var log = require('log4js').getLogger('tagdb'); log.setLevel(vyutil.getenv('VYTRONICS_TAGDB_LOG_LEVEL', 'warn')); module.exports.version = '0.0.0'; //TagDB can emit events var emitter = new events.EventEmitter(); var globals = {}; var tags = {}; var TAG_TYPES = { DISCRETE: 'discrete', ANALOG: 'analog', UNTYPED: 'untyped', OBJECT: 'object' }; //Load tags from json file var load = function (json) { if ( undefined === json ) { return; } for( var tagid in json ) { if( json.hasOwnProperty(tagid ) ) { //sys.* is reserved if (/^sys\..*/.exec(tagid)){ log.error('tagdb load sys.* is a reserved tag id pattern.'); continue; } var tag_config = json[tagid]; //If no tag create function supplied then simply create a tag from json if (!tag_config.create) { tags[tagid] = new Tag(tagid,json[tagid]); } //Else - Execute the creation function which should spit out either a single tag_config literal // { tagid: "some_id", // config: { } // } //or an array of literals like the above else if ( vyutil.isFunction(tag_config.create) ){ tag_config = tag_config.create(tagid, tag_config.config); tag_config = Array.isArray(tag_config) ? tag_config: [tag_config]; tag_config.forEach( function (tag_info){ if ((!tag_info) || (!tag_info.tagid)) { log.error('tagid:' + tagid + ' create error. Invalid config info. Must return {tagid:xxx, config:xxx} config:', tag_info); } else { tags[tag_info.tagid] = new Tag(tag_info.tagid, tag_info.config); } }); } else { log.error('tagid:' + tagid + ' create error. Invalid config:', tag_config); } } } emitter.emit('loaded'); }; //TODO - exposed this for sysdriver to create system tags //Need more work to make it general and ensure other useages //properly link up to drivers and get post load processing //Also need to do more validation such as duplicate tags etc. module.exports.create_tag = function(tagid, config) { var tag = new Tag(tagid, config); tags[tagid] = tag; } var start = function() { //Kick off any periodic calculations //TODO - how about a stop function getTags().forEach( function(tagid) { var tag = getTag(tagid); if (tag.calc) { //If this tag is a periodic calc if(tag.calc.intervalID) { //If already has an active timer clearInterval(tag.calc.intervalID); } setInterval( function() { try { var val = tag.calc.func.call(tag); tag.setValue(val); } catch(err){ //TODO - log this log.error("Tag:"+tag.id+" calcVal error:" + err.message, err.stack); } }, tag.calc.interval); } }); }; var getTags = function() { var tagIds = []; for( var tid in tags ) { if ( tags.hasOwnProperty(tid) ) { tagIds.push(tid); } } return tagIds; }; /* If tagid begins with "regex:" then return array of matches, otherwise returns single match */ var getTag = function (tagid) { var tag = tags[tagid]; if (tag) return tag; return undefined; }; var getTagsRegex = function (tagid_regex){ var matches = []; Object.getOwnPropertyNames(tags).forEach( function(tagid){ if (tagid.match(tagid_regex)) { matches.push(tags[tagid]); } }); return matches; }; var getTagInfo = function (tagid_regex, props){ var matches = []; Object.getOwnPropertyNames(tags).forEach( function(tagid){ if (tagid.match(tagid_regex)) { //If no specific properties requested then just send back tagids array if ( !props){ matches.push(tagid); } else { var tag = tags[tagid]; var tag_props = { tagid: tagid }; props.forEach(function (prop){ if ( vyutil.isDefined(tag[prop]) ){ tag_props[prop] = tag[prop]; } }); matches.push(tag_props); } } }); return matches; }; module.exports.get_tag_types = function (){ //make immutable by returning a copy return TAG_TYPES.slice(); } module.exports.load = load; module.exports.start = start; module.exports.on = function (type, listener){ emitter.on(type, listener); }, module.exports.getTags = getTags; module.exports.getTag = getTag; module.exports.getTagInfo = getTagInfo; module.exports.getTagsRegex = getTagsRegex; //Ask driver to write a value to this tagid. This is typically called from a client //GUI and value may need to be coerced according to tag.value_info to expected telemetry //value // module.exports.write_tag_request = function (tagid, value) { var tag = getTag(tagid); //Coerce value if necessary to telemetry value value = tag.coerce_value(value); console.log('###tagdb write_tag tagid:' + tagid + ' value:' + value); db.driverdb.write_item(tag.driverinfo, value); return true; } ////////////////Private Tag class. Nobody should be constructing outside this module //Construct a Tag object from json //Subscribe to driver IO and kick off any periodic calc loops function Tag(tagid, json) { this.id = tagid; this.value = json.defaultValue; //Assign to mem driver if no driver info this.driverinfo = json.driverinfo || { id:'mem', item:tagid}; //The value_info object gives tags their personality. Provides the information for converting raw //telemetered values to the desired engineering values (or discrete states) and visa versa. Also //associcates alarm priority as desired. If no convert object defined then no conversion is performed //from or to telemetry value. May change in the future to use default value as a hint. // // For discrete tags... // value_info: { // type: "discrete", // map: [ { value: 0, state: "NORMAL", almprior: 0 }, // { value: 1, state: "ALARM", almprior: 3 } ], // comment: "can put a string here for GUIs" // } // // For analog tags... // value_info: { // type: "analog", // to_daq: !!js/function "function(value) { return value*10 - 3; }", // from_daq: !!js/function "function(volts) { return (volts + 3) / 10; }", // min: -30, // max: 100, // //Need something for alarms TODO // comment: "can put a string here for guis" // } // // // TODO - validate this.value_info = json.value_info; //Set up any periodic calcs if( json.calcVal !== undefined ) { this.calc ={ interval:json.calcVal.interval, //TODO - test isNaN func:json.calcVal.func }; } } //Set the tag value and send notifications. Converts from raw telemetry to HMI value //If tag.value_info object is defined then this info is used to value_info // to/from raw telemetry value. Otherwise try to coerce to number. // // value is the raw telemetry value // // Precondition - tag.value_info has been validated when DB was loaded // Tag.prototype.setValue = function(value) { //TODO - should function check for no change in value //and if so exit with no action? if ( ! vyutil.isDefined(this.value_info) || (this.value_info.type === TAG_TYPES.UNTYPED)) { //Attempt to coerce to number var tagval = +value; tagval = isNaN(tagval) ? value : tagval; this.value = tagval; } else if (this.value_info.type === TAG_TYPES.OBJECT) { log.warn('TODO - implement analog tag types.'); //Coerce JSON strings to objects? this.value = value; } else if (this.value_info.type === TAG_TYPES.DISCRETE){ var map = this.value_info.map; var mapval = undefined; for (var i=0; i<map.length; i++){ if (map[i].value === value){ mapval = map[i].state; break; } } if ( ! vyutil.isDefined(mapval) ){ log.error('attempt to set invalid value [' + value + '] for tag ' + this.id); return; } else { this.value = mapval; } } else if (this.value_info.type === TAG_TYPES.ANALOG){ value = +value; if (isNaN(value)) return; //TODO log error? var from_daq = this.value_info.from_daq; if ( from_daq ) { //If conversion is defined, do it value = from_daq(value); //TODO - test min and max if limits are defined this.value = value; } } var data = { //TODO - pass other properties such as quality, alarm priority etc. id: this.id, value: this.value }; emitter.emit("tagChanged", this.id, data); }; //Coerce to and return raw telemetry value // Tag.prototype.coerce_value = function (value){ if ( ! vyutil.isDefined(this.value_info) || (this.value_info.type === TAG_TYPES.UNTYPED)) { //Attempt to coerce to number var tagval = +value; tagval = isNaN(tagval) ? value : tagval; return tagval; } else if (this.value_info.type === TAG_TYPES.OBJECT) { //TODO - this is wrong. Driver should expect a JSON string or plain string //The logic below would be needed for in memory tags. Think about it. //If string try to coerce from JSON if (vyutil.isString(value)){ try { return JSON.parse(value); } catch(err) { //Let value just be the string return value; } } else { return value; } } else if (this.value_info.type === TAG_TYPES.DISCRETE){ //Reverse lookup from state string to value var map = this.value_info.map; var telemval = undefined; for (var i=0; i<map.length; i++){ if (map[i].state === value){ telemval = map[i].value; break; } } if ( ! vyutil.isDefined(telemval) ){ log.error('attempt to coerce invalid value [' + value + '] for tag ' + this.id); return undefined; } else { return telemval; } } else if (this.value_info.type === TAG_TYPES.ANALOG){ value = +value; if (isNaN(value)) return; //TODO log error? var to_daq = this.value_info.to_daq; if ( to_daq ) { //If conversion is defined, do it value = to_daq(value); //TODO - test min and max if limits are defined return value; } } else { log.warn('tag.coerce_value programmer error tag:' + this.id + '?'); } }; /* Return a safe copy of value_info for tag For discretes: { type: "discrete", states: [] - array of valid state strings comment: string for GUIs } For analog: { type: "analog", min: min reasonability limit max: max reasonability limit comment: string for GUIs } For object type: { type: "object", comment: string for GUIs } For tags with no value_info object: { type: 'untyped', comment: - will be a system generated string "Untyped. Attempt will be made too coerce telemetry values to numbers." } */ Tag.prototype.get_value_info = function (){ if ( ! vyutil.isDefined(this.value_info) ) { return { type: TAG_TYPES.UNTYPED, comment: "Untyped tag. Attempt will be made to coerce telemetry values to numbers." }; } else if (this.value_info.type === TAG_TYPES.UNTYPED){ return { type: TAG_TYPES.UNTYPED, comment: this.value_info.comment || "Untyped tag. Attempt will be made to coerce telemetry values to numbers." }; } else if (this.value_info.type === TAG_TYPES.DISCRETE){ var map = this.value_info.map; var states = []; this.value_info.map.forEach( function (mapi){ states.push(mapi.state); }); return { comment: this.value_info.comment || "discrete tag type", states: states }; } else if (this.value_info.type === TAG_TYPES.ANALOG){ return { min: this.value_info.min, max: this.value_info.max, num_digits: this.value_info.num_digits, comment: this.value_info.comment || "analog tag type" }; } else if (this.value_info.type === TAG_TYPES.OBJECT){ return { comment: this.value_info.comment || "object tag type" }; } };