@frangoteam/fuxa
Version:
Web-based Process Visualization (SCADA/HMI/Dashboard) software
743 lines (698 loc) • 29.7 kB
JavaScript
/**
* BACnet Client Driver
*/
;
var bacnet;
const utils = require('../../utils');
const deviceUtils = require('../device-utils');
function BACNETclient(_data, _logger, _events, _runtime) {
var runtime = _runtime;
var data = _data; // Current Device data { id, name, tags, enabled, ... }
var logger = _logger; // Logger
var working = false; // Working flag to manage overloading polling and connection
var connected = false; // Connected flag
var lastStatus = ''; // Last Device status
var events = _events; // Events to commit change to runtime
var client = null;
var varsValue = {}; // Signale to send to frontend { id, type, value }
var requestItemsMap = {}; // Map of request (JSON, CSV, XML, ...) {key: item path, value: tag}
var objectsMapToRead = {};
// var getProperty = null; // Function to ask property
var lastTimestampValue; // Last Timestamp of asked values
var overloading = 0; // Overloading counter to mange the break connection
var devices = {}; // Devices found { id, maxApdu, segmentation, vendorId }
/**
* Connect the client to BACnet device
*/
this.connect = function () {
return new Promise(function (resolve, reject) {
if (data.property) {
try {
if (_checkWorking(true)) {
logger.info(`'${data.name}' try to open BACnet`, true);
_connect().then( res => {
logger.info(`'${data.name}' connected!`, true);
_emitStatus('connect-ok');
connected = true;
resolve();
_checkWorking(false);
}, reason => {
logger.error(`'${data.name}' connect failed! ${reason}`);
_emitStatus('connect-error');
_clearVarsValue();
reject();
_checkWorking(false);
client.close();
client = null;
});
} else {
reject();
}
} catch (err) {
logger.error(`'${data.name}' try to connect error! ${err}`);
_emitStatus('connect-error');
_clearVarsValue();
reject();
_checkWorking(false);
}
} else {
logger.error(`'${data.name}' missing connection data!`);
_emitStatus('connect-failed');
_clearVarsValue();
reject();
}
});
}
/**
* Disconnect the BACnet client
*/
this.disconnect = function () {
return new Promise(function (resolve, reject) {
try {
if (client && connected) {
client.close();
client = null;
}
resolve(true);
} catch (err) {
if (err) {
logger.error(`'${data.name}' disconnect failure! ${err}`);
}
reject();
}
connected = false;
monitored = false;
_checkWorking(false);
_emitStatus('connect-off');
_clearVarsValue();
});
}
/**
* Browse Devices Objects, read the objects of gived device
*/
this.browse = function (node) {
return new Promise(function (resolve, reject) {
if (!node) {
_askName(Object.values(devices)).then(res => {
resolve(Object.values(devices));
});
} else if (node.id) {
if (_checkWorking(true)) {
try {
if (node.parent) { // BACnet object => read property
_checkWorking(false);
} else { // BACnet device => read object list
_readObjectList(node.id).then(result => {
resolve(result);
_checkWorking(false);
}, err => {
reject();
_checkWorking(false);
});
}
} catch (err) {
if (err) {
logger.error(`'${data.name}' browse failure! ${err}`);
}
reject();
_checkWorking(false);
}
}
} else {
reject();
_checkWorking(false);
}
});
}
// Next function wraps the API client.readPropertyMultiple call into a Promise
// and handles the callbacks with resolve and reject.
// https://stackoverflow.com/questions/5010288/how-to-make-a-function-wait-until-a-callback-has-been-called-using-node-js
this.apiFunctionWrapper = function (deviceAddress, requestArray) {
return new Promise((resolve, reject) => {
client.readPropertyMultiple(deviceAddress, requestArray, async (err, value) => {
if (err) {
logger.error(`'${data.name}' readPropertyMultiple error! ${err}`);
reject(err);
}
resolve(value);
});
});
}
/**
* Take the current Objects (Tags) value (only changed), Reset the change flag, Emit Tags value
* Save DAQ value
*/
this.polling = async function () {
if (_checkWorking(true)) {
for (var deviceId in objectsMapToRead) {
try {
const deviceAddress = _getDeviceAddress(devices[deviceId]);
//wrap the client.readPropertyMultiple in a promise so the callback can be awaited
const value = await this.apiFunctionWrapper(deviceAddress, objectsMapToRead[deviceId]);
if (!(value && value.values && value.values[0] && value.values[0].values)) {
logger.error(`'${data.name}' readPropertyMultiple error! unknow`);
} else if (value.values && value.values.length) {
let result = [];
value.values.forEach(bacData => {
if (bacData.objectId && bacData.values && bacData.values[0].id === bacnet.enum.PropertyIdentifier.PRESENT_VALUE) {
let address = _formatId(bacData.objectId.type, bacData.objectId.instance);
if (bacData.values[0].value && bacData.values[0].value.type === bacnet.enum.ApplicationTag.ERROR ||
bacData.values[0].value.length > 0 && bacData.values[0].value[0].type === bacnet.enum.ApplicationTag.ERROR ) {
logger.error(`'${data.name}' readPropertyMultiple error! errorClass: ${bacData.values[0].value[0].value.errorClass}, errorCode: ${bacData.values[0].value[0].value.errorCode}`);
} else {
result.push({
address: address,
rawValue: bacData.values[0].value[0].value,
type: bacData.objectId.type
});
}
}
});
if (result.length) {
let varsValueChanged = await _updateVarsValue(deviceId, result);
lastTimestampValue = new Date().getTime();
_emitValues(varsValue);
if (this.addDaq && !utils.isEmptyObject(varsValueChanged)) {
this.addDaq(varsValueChanged, data.name, data.id);
}
}
}
if (lastStatus !== 'connect-ok') {
_emitStatus('connect-ok');
}
} catch (err) {
if (err) {
logger.error(`'${data.name}' readPropertyMultiple error! ${err}`);
}
}
}
_checkWorking(false);
} else {
_emitStatus('connect-busy');
}
}
/**
* Load Objects (Tags) to read by polling
*/
this.load = function (_data) {
data = JSON.parse(JSON.stringify(_data));
try {
requestItemsMap = {};
var count = Object.keys(data.tags).length;
for (var id in data.tags) {
if (!requestItemsMap[data.tags[id].memaddress]) {
requestItemsMap[data.tags[id].memaddress] = {};
}
requestItemsMap[data.tags[id].memaddress][data.tags[id].address] = data.tags[id];
}
objectsMapToRead = {};
for (var deviceId in requestItemsMap) {
objectsMapToRead[deviceId] = [];
for (var tagId in requestItemsMap[deviceId]) {
let obj = _extractId(requestItemsMap[deviceId][tagId].address);
objectsMapToRead[deviceId].push({objectId: {type: obj.type, instance: obj.instance}, properties: [{id: bacnet.enum.PropertyIdentifier.PRESENT_VALUE}]});
}
}
logger.info(`'${data.name}' data loaded (${count})`, true);
} catch (err) {
logger.error(`'${data.name}' load error! ${err}`);
}
}
/**
* Return Objects (Tags) values array { id: <name>, value: <value>, type: <type> }
*/
this.getValues = function () {
return data.tags;
}
/**
* Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> }
*/
this.getValue = function (id) {
if (varsValue[id]) {
return {id: id, value: varsValue[id].value, ts: lastTimestampValue };
}
return null;
}
/**
* Return Device status Connected/Disconnected 'connect-off', 'connect-ok', 'connect-error'
*/
this.getStatus = function () {
return lastStatus;
}
/**
* Return Objects (Tag) property
*/
this.getTagProperty = function (id) {
if (data.tags[id]) {
return { id: id, name: data.tags[id].name, type: data.tags[id].type, format: data.tags[id].format};
} else {
return null;
}
}
/**
* Set Tag value, used to set value from frontend
*/
this.setValue = async function (tagId, value) {
if (data.tags[tagId]) {
var obj = _extractId(data.tags[tagId].address);
value = await deviceUtils.tagRawCalculator(value, data.tags[tagId], runtime);
_writeProperty(_getDeviceAddress(devices[data.tags[tagId].memaddress]), obj, value).then(result => {
logger.info(`'${data.name}' setValue(${tagId}, ${result})`, true, true);
}, reason => {
if (reason && reason.stack) {
logger.error(`'${data.name}' _writeProperty error! ${reason.stack}`);
} else {
logger.error(`'${data.name}' _writeProperty error! ${reason}`);
}
});
return true;
}
return false;
}
/**
* Is Connected true/false
*/
this.isConnected = function () {
return connected;
}
/**
* Set the callback to set value to DAQ
*/
this.bindAddDaq = function (fnc) {
this.addDaq = fnc; // Add the DAQ value to db history
}
this.addDaq = null; // Callback to add the DAQ value to db history
/**
* Return the timestamp of last read tag operation on polling
* @returns
*/
this.lastReadTimestamp = () => {
return lastTimestampValue;
}
/**
* Return the Daq settings of Tag
* @returns
*/
this.getTagDaqSettings = (tagId) => {
return data.tags[tagId] ? data.tags[tagId].daq : null;
}
/**
* Set Daq settings of Tag
* @returns
*/
this.setTagDaqSettings = (tagId, settings) => {
if (data.tags[tagId]) {
utils.mergeObjectsValues(data.tags[tagId].daq, settings);
}
}
/**
* Connect the client to device
* Listening after broadcast query
* @param {*} callback
*/
var _connect = function() {
return new Promise(function (resolve, reject) {
var ipInterface = data.property.address || '0.0.0.0';
if (data.property.address && data.property.address.indexOf(':') !== -1) {
ipInterface = data.property.address.substring(0, data.property.address.indexOf(':'));
var port = data.property.address.substring(data.property.address.indexOf(':') + 1);
}
var settings = {
interface: ipInterface,
port: parseInt(port) || 47808,
adpuTimeout: parseInt(data.property.adpuTimeout) || 6000
}
var tryExplicit = false;
if (data.property.broadcastAddress) {
settings['broadcastAddress'] = data.property.broadcastAddress || '0.0.0.255';
tryExplicit = settings['broadcastAddress'].indexOf('255') === -1;
}
if (utils.getNetworkInterfaces().indexOf(ipInterface) === -1) {
reject(`'${data.name}' selected interface don't exist!`);
return;
}
console.log(settings);
client = new bacnet(settings);
// let options = { maxSegments: bacnet.enum.MaxSegments.MAX_SEG2, maxAdpu: bacnet.enum.MaxAdpu.MAX_APDU1476 };
// client.deviceCommunicationControl(ipAddress, 0, bacnet.enum.EnableDisable.DISABLE, (err, value) => {
// console.log('value: ', value);
// });
let tdelay = setTimeout(() => {
reject('timeout');
}, 5000);
devices = {};
try {
client.on('iAm', (device) => {
if (device && device.payload && device.payload.deviceId && !devices[device.id]) {
if (tdelay) {
clearTimeout(tdelay);
}
device = {
...device.payload,
...device.header,
id: device.payload.deviceId,
name: 'DeviceId ' + device.payload.deviceId
};
devices[device.id] = device;
resolve();
}
});
client.whoIs();
if (tryExplicit) {
setTimeout(() => {
client.whoIs({deviceIPAddress: settings['broadcastAddress']});
}, 2000);
}
} catch (err) {
reject(err);
if (tdelay) {
clearTimeout(tdelay);
}
}
});
}
/**
* Ask all devices name
* @param {*} devices
*/
var _askName = function (devices) {
return new Promise(async function (resolve, reject) {
var readfnc = [];
if (devices.length) {
for (var index in devices) {
var device = devices[index];
try {
let rp = await _readProperty(_getDeviceAddress(device), { type: bacnet.enum.ObjectType.DEVICE, instance: device.deviceId}, bacnet.enum.PropertyIdentifier.OBJECT_NAME);
if (rp) {
readfnc.push(rp);
}
} catch (err) {
logger.error(`'${data.name}' _readProperty error! ${err}`);
}
}
Promise.all(readfnc).then(results => {
if (results) {
for (var index in results) {
var device = _getDevice(results[index].instance);
if (device) {
device.name = results[index].value;
device.class = _getObjectClass(device.type);
}
}
}
resolve();
}, reason => {
if (reason) {
if (reason.stack) {
logger.error(`'${data.name}' _askName error! ${reason.stack}`);
} else if (reason.message) {
logger.error(`'${data.name}' _askName error! ${reason.message}`);
}
} else {
logger.error(`'${data.name}' _askName error! ${reason}`);
}
reject();
});
} else {
reject();
}
});
}
/**
* Read Objects list of device, in Device Object
* @param {*} instance
*/
var _readObjectList = function(instance) {
return new Promise(function (resolve, reject) {
client.readProperty(_getDeviceAddress(devices[instance]), {type: bacnet.enum.ObjectType.DEVICE, instance: instance}, bacnet.enum.PropertyIdentifier.OBJECT_LIST, (err, value) => {
if (err) {
logger.error(`'${data.name}' _readObjectList error! ${err}`);
} else if (value && value.values && value.values.length) {
var objects = [];
var readfnc = [];
for (var index in value.values) {
var object = value.values[index].value;
object.parent = instance;
if (_isObjectToShow(object.type)) {
objects.push(object);
try {
readfnc.push(_readProperty(_getDeviceAddress(devices[instance]), { type: object.type, instance: object.instance}, bacnet.enum.PropertyIdentifier.OBJECT_NAME));
} catch (error) {
logger.error(`'${data.name}' _readObjectList error! ${error}`);
}
}
}
Promise.all(readfnc).then(results => {
if (results) {
for (var index in results) {
if (results[index]) {
var object = _getObject(objects, results[index].type, results[index].instance);
if (object) {
object.id = _formatId(object.type, object.instance);
object.name = results[index].value;
object.class = _getObjectClass(object.type);
}
}
}
}
resolve(objects);
}, reason => {
if (reason) {
if (reason.stack) {
logger.error(`'${data.name}' _readObjectList error! ${reason.stack}`);
} else if (reason.message) {
logger.error(`'${data.name}' _readObjectList error! ${reason.message}`);
}
} else {
logger.error(`'${data.name}' _readObjectList error! ${reason}`);
}
reject(reason);
});
}
});
});
}
/**
* Read Object property, Object are defined with type and instance
* @param {*} bacobj
* @param {*} property
*/
var _readProperty = function(address, bacobj, property) {
return new Promise(function (resolve, reject) {
client.readProperty(address, bacobj, property, (err, value) => {
if (err) {
resolve();
} else if (value && value.values && value.values[0] && value.values[0].value) {
resolve({ type: bacobj.type, instance: bacobj.instance, value: value.values[0].value });
} else {
resolve();
}
});
});
}
var _writeProperty = function(address, bacobj, value) {
return new Promise(function (resolve, reject) {
var tvalue = {type: bacnet.enum.ApplicationTag.NULL, value: value};
bacobj.type = parseInt(bacobj.type);
bacobj.instance = parseInt(bacobj.instance);
if (bacobj.type === bacnet.enum.ObjectType.ANALOG_INPUT ||
bacobj.type === bacnet.enum.ObjectType.ANALOG_OUTPUT ||
bacobj.type === bacnet.enum.ObjectType.ANALOG_VALUE) {
tvalue.type = bacnet.enum.ApplicationTag.REAL;
tvalue.value = parseFloat(value);
} else if (bacobj.type === bacnet.enum.ObjectType.BINARY_INPUT ||
bacobj.type === bacnet.enum.ObjectType.BINARY_OUTPUT ||
bacobj.type === bacnet.enum.ObjectType.BINARY_VALUE) {
tvalue.type = bacnet.enum.ApplicationTag.ENUMERATED;
tvalue.value = parseInt(value);
}
client.writeProperty(address, bacobj, bacnet.enum.PropertyIdentifier.PRESENT_VALUE, [tvalue], { priority: 16 }, (err, result) => {
if (err) {
reject(err);
console.error('value: ', err);
} else {
resolve();
}
});
});
}
/**
* Return the Device
* @param {*} deviceId
*/
var _getDevice = function (deviceId) {
for (var id in devices) {
if (devices[id].deviceId === deviceId) {
return devices[id];
}
}
return null;
}
/**
* Search and return the object
* @param {*} objs
* @param {*} type
* @param {*} instance
*/
var _getObject = function (objs, type, instance) {
for (var index in objs) {
if (objs[index] && objs[index].type === type && objs[index].instance === instance) {
return objs[index];
}
}
return null;
}
/**
* Return the object class text
* @param {*} type
*/
var _getObjectClass = function (type) {
if (type === bacnet.enum.ObjectType.DEVICE) {
return 'Device';
} else if (type === bacnet.enum.ObjectType.ANALOG_INPUT) {
return 'Variable';
} else if (type === bacnet.enum.ObjectType.ANALOG_OUTPUT) {
return 'Variable';
} else if (type === bacnet.enum.ObjectType.ANALOG_VALUE) {
return 'Variable';
} else if (type === bacnet.enum.ObjectType.BINARY_INPUT) {
return 'Variable';
} else if (type === bacnet.enum.ObjectType.BINARY_OUTPUT) {
return 'Variable';
} else if (type === bacnet.enum.ObjectType.BINARY_VALUE) {
return 'Variable';
} else {
return 'Object';
}
}
/**
* Return if is a object to shoe and config (only Analog and Digital)
* @param {*} type
*/
var _isObjectToShow = function (type) {
if (type === bacnet.enum.ObjectType.DEVICE) {
return false;
} else if (type === bacnet.enum.ObjectType.ANALOG_INPUT) {
return true;
} else if (type === bacnet.enum.ObjectType.ANALOG_OUTPUT) {
return true;
} else if (type === bacnet.enum.ObjectType.ANALOG_VALUE) {
return true;
} else if (type === bacnet.enum.ObjectType.BINARY_INPUT) {
return true;
} else if (type === bacnet.enum.ObjectType.BINARY_OUTPUT) {
return true;
} else if (type === bacnet.enum.ObjectType.BINARY_VALUE) {
return true;
} else {
return false;
}
}
/**
* Update the Tags values read
* @param {*} vars
*/
var _updateVarsValue = async (deviceId, vars) => {
const timestamp = new Date().getTime();
var someval = false;
var changed = {};
for (var index in vars) {
var address = vars[index].address;
if (requestItemsMap[deviceId][address]) {
var tag = requestItemsMap[deviceId][address];
if (!varsValue[tag.id] || varsValue[tag.id].value !== vars[index].value) {
changed[tag.id] = { id: tag.id, value: vars[index].value, type: vars[index].type, daq: tag.daq };
varsValue[tag.id] = changed[tag.id];
}
varsValue[tag.id].changed = varsValue[tag.id].rawValue !== vars[index].rawValue;
if (!utils.isNullOrUndefined(vars[index].rawValue)) {
varsValue[tag.id].rawValue = vars[index].rawValue;
varsValue[tag.id].value = await deviceUtils.tagValueCompose(vars[index].rawValue, varsValue[tag.id] ? varsValue[tag.id].value : null, tag, runtime);
vars[index].value = varsValue[tag.id].value;
if (this.addDaq && deviceUtils.tagDaqToSave(varsValue[tag.id], timestamp)) {
changed[tag.id] = { id: tag.id, value: varsValue[tag.id].value, type: vars[index].type, daq: tag.daq, timestamp: timestamp };
varsValue[tag.id] = changed[tag.id];
}
varsValue[tag.id].timestamp = timestamp;
}
varsValue[tag.id].changed = false;
someval = true;
}
}
if (someval) {
return changed;
}
return null;
}
/**
* Clear local Tags value by set all to null
*/
var _clearVarsValue = function () {
for (let id in varsValue) {
varsValue[id].value = null;
}
_emitValues(varsValue);
}
/**
* Return formatted object id
* @param {*} type
* @param {*} instance
*/
var _formatId = function (type, instance) {
return type + '-' + instance;
}
/**
* Return id and type from formatted id
* @param {*} id
*/
var _extractId = function (id) {
let tks = id.split('-');
return { type: tks[0], instance: tks[1] };
}
/**
* To manage a overloading connection
* @param {*} check
*/
var _checkWorking = function (check) {
if (check && working) {
logger.warn(`'${data.name}' working (connection || polling) overload! ${overloading}`);
if (overloading >= 3) {
client.close();
} else {
return false;
}
}
overloading = 0;
working = check;
return true;
}
/**
* Emit Objects (Tags) in application
* @param {*} values
*/
var _emitValues = function (values) {
events.emit('device-value:changed', { id: data.name, values: values });
}
/**
* Emit status in application
* @param {*} status
*/
var _emitStatus = function (status) {
lastStatus = status;
events.emit('device-status:changed', { id: data.name, status: status });
}
var _getDeviceAddress = function (device) {
return device.address || device.sender.address;
}
}
module.exports = {
init: function (settings) {
// deviceCloseTimeout = settings.deviceCloseTimeout || 15000;
},
create: function (data, logger, events, manager, runtime) {
try { bacnet = require('node-bacnet'); } catch { }
if (!bacnet && manager) { try { bacnet = manager.require('node-bacnet'); } catch { } }
if (!bacnet) return null;
return new BACNETclient(data, logger, events, runtime);
}
}