@frangoteam/fuxa
Version: 
Web-based Process Visualization (SCADA/HMI/Dashboard) software
487 lines (452 loc) • 18.4 kB
JavaScript
/**
 * 'ROS client': ROS client to manage subscription and publish
 */
;
var ads;
var utils = require('../../utils');
const deviceUtils = require('../device-utils');
function ADSclient(_data, _logger, _events, _runtime) {
    var data = JSON.parse(JSON.stringify(_data)); // Current Device data { id, name, tags, enabled, ... }
    var logger = _logger;
    var events = _events;               // Events to commit change to runtime
    var runtime = _runtime;
    var working = false;                // Working flag to manage overloading polling and connection
    var connected = false;              // Connected flag
    var lastStatus = '';                // Last connections status
    var client = null;                  // ADS client head
    var varsValue = {};                 // Tags to send to frontend { id, type, value }
    var overloading = 0;                // Overloading counter to mange the break connection
    var lastTimestampValue;             // Last Timestamp of asked values
    var topicsMap = {};                 // Map the topic subscribed, to check by on.message
    /**
     * initialize the device type
     */
    this.init = function (_type) {
        console.error('Not supported!');
    }
    /**
     * Connect to device
     * Emit connection status to clients, clear all Tags values
     */
    this.connect = function () {
        return new Promise(async function (resolve, reject) {
            if (data.property && data.property.address) {
                try {
                    if (_checkWorking(true)) {
                        var ipAddress = data.property.address;
                        if (ipAddress.indexOf(':') !== -1) {
                            data.property.port = parseInt(data.property.address.substring(data.property.address.indexOf(':') + 1));
                            ipAddress = data.property.address.substring(0, data.property.address.indexOf(':'));
                        }
                        var options = {
                            targetAmsNetId: ipAddress,
                            targetAdsPort: data.property.port || 30012,
                            // routerAddress: 'localhost',      //PLC ip address
                            // routerTcpPort: data.property.port || 48898
                        };
                        if (data.property.local) {
                            var ipLocalNetId = data.property.local;
                            var ipLocalPort = 32750;
                            if (ipLocalNetId.indexOf(':') !== -1) {
                                ipLocalPort = parseInt(data.property.local.substring(data.property.local.indexOf(':') + 1));
                                ipLocalNetId = data.property.local.substring(0, data.property.local.indexOf(':'));
                            }
                            options = {
                                ...options,
                                localAmsNetId: ipLocalNetId,       //Can be anything but needs to be in PLC StaticRoutes.xml file
                                localAdsPort: ipLocalPort || 32750,
                            }
                        }
                        if (data.property.router) {
                            var ipRouterNetId = data.property.router;
                            var ipRouterPort = 48898;
                            if (ipRouterNetId.indexOf(':') !== -1) {
                                ipRouterPort = parseInt(data.property.router.substring(data.property.router.indexOf(':') + 1));
                                ipRouterNetId = data.property.router.substring(0, data.property.router.indexOf(':'));
                            }
                            options = {
                                ...options,
                                routerAddress: ipRouterNetId,           //PLC ip address
                                routerTcpPort: ipRouterPort || 48898    //PLC needs to have this port opened. Test disabling all firewalls if problems
                            }
                        }
                        client = new ads.Client(options);
                        _clearVarsValue();
                        client.connect().then((res) => {
                            logger.info(`'${data.name}' connected to ${res.targetAmsNetId}!`);
                            _emitStatus('connect-ok');
							browse();
                            _createSubscription().then(() => {
                                connected = true;
                                resolve();
                                _checkWorking(false);
                            }).catch(function (err) {
                                connected = false;
                                logger.error(err);
                                reject(err);
                                _checkWorking(false);
                            });
                        }).then(() => {
                            connected = false;
                            logger.warn(`'${data.name}' client disconnect, connection ended ${data.property.address}`, true);
                            resolve();
                        }).catch((err) => {
                            connected = false;
                            logger.error(`'${data.name}' try to connect error! ${err}`);
                            _checkWorking(false);
                            _emitStatus('connect-error');
                            reject(err);
                        });
                        client.on("connect", function (connectionInfo) {
                            connected = true;
                            logger.info(`'${data.name}' client connected ${connectionInfo.targetAmsNetId}`, false);
                        });
                        client.on("disconnect", function (connectionLost) {
                            connected = false;
                            logger.warn(`'${data.name}' client disconnect, got signal ${data.property.address}: ${connectionLost}`, true);
                        });
                        client.on("reconnect", function () {
                            connected = true;
                            logger.warn(`'${data.name}' client reconnecting ... ${data.property.address}`, true);
                        });
                        client.on("ads-client-error", function (err) {
                            logger.error(`'${data.name}' error! ${err}`);
                        });
                    } else {
                        reject();
                    }
                } catch (err) {
                    logger.error(`'${data.name}' try to connect error! ${err}`);
                    _checkWorking(false);
                    _emitStatus('connect-error');
                    _clearVarsValue();
                    reject();
                }
            } else {
                logger.error(`'${data.name}' missing connection data!`);
                _emitStatus('connect-failed');
                _clearVarsValue();
                reject();
            }
        });
    }
    function browse() {
        return new Promise(async function (resolve, reject) {
            if (client) {
                try {
                    var symbolObject = await client.getSymbols();
					var symbolList = Object.values(symbolObject);
                    var symbols = [];
                    for (var i = 0; i < symbolList.length; i++) {
                        var tag = new AdsSymbol(symbolList[i].name);
                        tag.type = symbolList[i].type;
                        tag.value = symbolList[i].value;
                        symbols.push(tag);
                    }
					// logger.warn(symbols);
                    resolve(symbols);
                } catch (err) {
                    logger.error(`'${data.name}' try to browse error! ${err}`);
                    _checkWorking(false);
                    _emitStatus('browse-error');
                    _clearVarsValue();
                    reject();
                }
            } else {
                logger.error(`'${data.name}' missing connection data!`);
                _emitStatus('browse-failed');
                _clearVarsValue();
                reject();
            }
        });
    }
    /**
     * Disconnect the device
     * Emit connection status to clients, clear all Tags values
     */
    this.disconnect = function () {
        return new Promise(async function (resolve, reject) {
            if (client) {
                try {
                    await client.unsubscribeAll();
                } catch (err) {
                    logger.error(`'${data.name}' try to unsubscribe error! ${err}`);
                }
                try {
                    client.disconnect();
                } catch (err) {
                    logger.error(`'${data.name}' try to disconnect error! ${err}`);
                    connected = false;
                }
                logger.info(`'${data.name}' disconnected!`, true);
                _checkWorking(false);
                _emitStatus('connect-off');
                _clearVarsValue();
                resolve(true);
            }
            else {
                resolve(true);
            }
        });
    }
    /**
     * Read values in polling mode
     * Update the tags values list, save in DAQ if value changed or in interval and emit values to clients
     */
    this.polling = async function () {
        if (_checkWorking(true)) {
            if (client) {
                try {
                    var varsValueChanged = await _checkVarsChanged();
                    lastTimestampValue = new Date().getTime();
                    _emitValues(varsValue);
                    if (this.addDaq) {
                        this.addDaq(varsValueChanged, data.name);
                    }
                } catch (err) {
                    logger.error(`'${data.name}' polling error: ${err}`);
                }
                _checkWorking(false);
            } else {
                _checkWorking(false);
            }
        }
    }
    /**
     * Load Tags attribute to read with polling
     */
    this.load = function (_data) {
        varsValue = {};
        data = JSON.parse(JSON.stringify(_data));
        try {
            var count = Object.keys(data.tags).length;
            logger.info(`'${data.name}' data loaded (${count})`, true);
        } catch (err) {
            logger.error(`'${data.name}' load error! ${err}`);
        }
    }
    /**
     * Return Tags values array { id: <name>, value: <value> }
     */
    this.getValues = function () {
        return data.tags;
    }
    /**
     * Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> }
     */
    this.getValue = function (tagId) {
        if (varsValue[tagId]) {
            return { id: tagId, value: varsValue[tagId].value, ts: lastTimestampValue };
        }
        return null;
    }
    /**
     * Return connection status 'connect-off', 'connect-ok', 'connect-error', 'connect-busy'
     */
    this.getStatus = function () {
        return lastStatus;
    }
    /**
     * Return Tag property to show in frontend
     */
    this.getTagProperty = function (tagId) {
        if (data.tags[tagId]) {
            return { id: tagId, name: data.tags[tagId].name, type: data.tags[tagId].type, format: data.tags[tagId].format };
        } else {
            return null;
        }
    }
    /**
     * Set the Tag value to device
     */
    this.setValue = async (tagId, value) => {
        if (client && client.connection.connected && data.tags[tagId]) {
            try {
                var valueToSend = await deviceUtils.tagRawCalculator(_toValue(data.tags[tagId].type, value), data.tags[tagId]);
                logger.info(`Writing ${valueToSend} to: '${data.tags[tagId].address}'`)
                const res = await client.writeValue(data.tags[tagId].address, valueToSend)
            } catch (err) {
                logger.error(`'${data.name}' setValue error! ${err}`);
            }
        }
    }
    /**
     * Return if device is connected
     */
    this.isConnected = function () {
        return (client) ? client.connection.connected : false;
    }
    /**
     * Bind the DAQ store function and default daqInterval value in milliseconds
     */
    this.bindAddDaq = function (fnc) {
        this.addDaq = fnc;                         // Add the DAQ value to db history
    }
    this.addDaq = null;
    /**
     * Return the timestamp of last read tag operation on polling
     * @returns
     */
    this.lastReadTimestamp = () => {
        return lastTimestampValue;
    }
    /**
     * Create a subscription to receive Topics value
     */
    var _createSubscription = function () {
        return new Promise(function (resolve, reject) {
            var topics = Object.values(data.tags).map(t => t.address);
            _mapTopicsAddress(Object.values(data.tags));
            if (topics && topics.length) {
              var count = 0;
              topics.forEach(async (topic) => {
                  try {
                      await client.subscribeValue(topic, _onChange, 1000, false);
                      count++;
                  } catch (err) {
                      logger.error(`'${data.name}' subscribe ${topic} error! ${err}`);
                      return
                  }
              });
              resolve();
            } else {
              resolve();
            }
        });
    }
    /**
     * Callback from monitor of changed Tag value
     * And set the changed value to local Tags
     * @param {*} _nodeId
     */
    const _onChange = (receivedData, sub) => {
        if (topicsMap[sub.symbol.name]) {
            for (var i = 0; i < topicsMap[sub.symbol.name].length; i++) {
                var id = topicsMap[sub.symbol.name][i].id;
                var oldvalue = data.tags[id].rawValue;
                data.tags[id].rawValue = receivedData.value;
                data.tags[id].timestamp = receivedData.timestamp.getTime();
                data.tags[id].changed = oldvalue !== receivedData.value;
            }
        }
    }
    /**
     * Map the topics to address (path)
     * @param {*} topics
     */
    var _mapTopicsAddress = function (topics) {
        var tmap = {};
        for (var i = 0; i < topics.length; i++) {
            if (tmap[topics[i].address]) {
                tmap[topics[i].address].push(topics[i]);
            } else {
                tmap[topics[i].address] = [topics[i]];
            }
        }
        topicsMap = tmap;
    }
    /**
     * Clear local Topics value by set all to null
     */
    var _clearVarsValue = function () {
        for (var id in varsValue) {
            varsValue[id].value = null;
        }
        _emitValues(varsValue);
    }
    /**
     * Return the Topics to publish that have value changed and clear value changed flag of all Topics
     */
    var _checkVarsChanged = async () => {
        const timestamp = new Date().getTime();
        var result = {};
        for (var id in data.tags) {
        		// logger.warn(`Tag ${id} has raw value ${data.tags[id].rawValue}`);
            if (!utils.isNullOrUndefined(data.tags[id].rawValue)) {
              data.tags[id].value = await deviceUtils.tagValueCompose(data.tags[id].rawValue, data.tags[id]);
              if (this.addDaq && deviceUtils.tagDaqToSave(data.tags[id], timestamp)) {
                  result[id] = data.tags[id];
              }
            }
            data.tags[id].changed = false;
            varsValue[id] = data.tags[id];
        }
        return result;
    }
    /**
     * Emit the mqtt client connection status
     * @param {*} status
     */
    var _emitStatus = function (status) {
        lastStatus = status;
        events.emit('device-status:changed', { id: data.name, status: status });
    }
    /**
     * Emit the mqtt Topics values array { id: <name>, value: <value>, type: <type> }
     * @param {*} values
     */
    var _emitValues = function (values) {
        events.emit('device-value:changed', { id: data.name, values: values });
    }
    /**
     * Used to manage the async connection and polling automation (that not overloading)
     * @param {*} check
     */
    var _checkWorking = function (check) {
        if (check && working) {
            overloading++;
            logger.warn(`'${data.name}' working (connection || polling) overload! ${overloading}`);
            // !The driver don't give the break connection
            if (overloading >= 3) {
                try {
                    if (client) client.end(true);
                } catch (e) {
                    console.error(e);
                }
            } else {
                return false;
            }
        }
        working = check;
        overloading = 0;
        return true;
    }
    /**
     * Convert value from string depending of type
     * @param {*} type
     * @param {*} value
     */
    var _toValue = function (type, value) {
        switch (type) {
            case Datatypes.boolean:
                if (value && value.toLowerCase() !== 'false') {
                    return 1;
                }
                return 0;
            case Datatypes.number:
                return parseFloat(value);
            default:
                return value;
        }
    }
}
module.exports = {
    init: function (settings) {
    },
    create: function (data, logger, events, manager, runtime) {
        try { ads = require('ads-client'); } catch { }
        if (!ads && manager) { try { snap7 = manager.require('ads-client'); } catch { } }
        if (!ads) return null;
        return new ADSclient(data, logger, events, runtime);
    }
}
const Datatypes = {
    number: 'number',
    boolean: 'boolean',
    string: 'string'
}
function AdsSymbol(name) {
    this.name = name;
    this.type = '';
    this.value = '';
}