UNPKG

tc-context

Version:

TwinCAT ADS Communication Library for creating an active TwinCAT Context, with automatic symbol and type mapping

428 lines (427 loc) 25.7 kB
"use strict"; // tc-com.ts /** * Module containing the main TcCom Class, responsible for establishing ADS Connection and managing communication * between {@link TcContext} and the PLC * * * Licensed under MIT License. * * Copyright (c) 2020 Dmitrij Trifanov <d.v.trifanov@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * * @packageDocumentation */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ADST = exports.TcCom = void 0; //----IMPORTS... const debug_1 = __importDefault(require("debug")); // @ts-ignore const ads_client_1 = require("ads-client"); const tc_event_1 = require("./tc-event"); const tc_exception_1 = require("./tc-exception"); /** * Class responsible for establishing connection and managing all communication and data transformation * to and from the Target PLC over TwinCAT's ADS layer. * * Is used as a wrapper for the [ads-client](https://github.com/jisotalo/ads-client) library. * */ let TcCom = /** @class */ (() => { class TcCom extends tc_event_1.TcEmitter { /** * Constructor, which stores the {@link TcComSettings} used for establishing communication, as well as * the callback, which is triggered upon Code Change detection * * @param context - Parent {@link TcContext}, of whom `TcCom` is part of, and whom to propagate events to * @param settings - Settings used for communicating over ADS. Definition of connection settings can be found at [ads-client](https://github.com/jisotalo/ads-client) library * @param onChange - Callback, which is called when Code Changes are detected. This callback is called after the `sourceChanged` event is emitted * @param debug - If enabled, will produce debug information */ constructor(context, settings, onChange, debug = false) { super(context); /** * @internal */ this.__log = debug_1.default(`TcContext::TcCom`); this.__context = context; this.__settings = Object.assign(Object.assign({}, TcCom.defaultSettings), settings); this.__callOnChange = onChange; this.__log.enabled = debug; this.__log('Creating TcCom Object...'); } /** * Access to the previously used {@link TcComSettings} for establishing connection to the TwinCAT PLC */ get settings() { return this.__settings; } ; /** * Returns `true` if the current `TcCom` Object is in a valid state, and can be used for communication */ get isValid() { return this.__ads !== undefined; } ; /** * Initializes the `TcCom` Object, by establishing a connection to the TwinCAT PLC, with the previously provided {@link TcComSettings}, as well as * setting up Code Change monitoring, if the Source Code on the PLC Changes, during run-time * * @throws {@link TcComBusyException} - Connection has already been created previously * @throws {@link TcComConnectException} - Failed to establish a connection to the TwinCAT PLC over ADS * @throws {@link TcComChangeDetectionException} - Failed to set up Code Change monitoring * * @return - The initialized `TcCom` Object */ async initialize() { this.__log(`initialize() : Initializing TcCom Object for ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); //Check to see if a connection was already made with this Object. if (this.__ads || this.__changeHndl) throw new tc_exception_1.TcComBusyException(this.__context, this, 'TcCom already has an active ADS Connection. Consider calling .kill() before calling .initialize() for re-initialization'); //Attempt to connect to the TwinCAT Target, and if successful set up the Code Change monitoring const ads = new ads_client_1.Client(this.__settings); await ads.connect() .catch((err) => { throw new tc_exception_1.TcComConnectException(this.__context, this, `TcCom encountered an error when connecting to ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`, err); }); this.__log(`initialize() : Connection established to ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); const changeHndl = await ads.subscribe(TcCom.CHANGE_COUNTER, this.__callback.bind(this)) .catch(async (err) => { //Clean up the connection, if Code Change monitoring has failed before exiting await ads.disconnect(true); throw new tc_exception_1.TcComChangeDetectionException(this.__context, this, `TcCom encountered an error when linking Source Changes Monitoring to ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`, err); }); this.__log(`initialize() : Link to monitor Source Changes established with ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); //Attach listeners for the Connection Lost and Reconnect events ads.on('connectionLost', () => { this.emit('connectionLost', new tc_event_1.TcComConnectionLostEvent(this.__context, this)); }); ads.on('reconnect', () => { this.emit('reconnected', new tc_event_1.TcComReconnectedEvent(this.__context, this)); }); //This point will only be reached if all the previous steps were successful, so //it is safe to store the created ADS Client and Code Change Handle this.__ads = ads; this.__changeHndl = changeHndl; this.__log(`initialize() : TcCom Object connected to ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); this.emit('connected', new tc_event_1.TcComConnectedEvent(this.__context, this, this.__settings)); return this; } /** * Disconnects the previously established connection to the TwinCAT PLC, and cleans up all subscription handles. * The `TcCom` Object is no longer usable after this point, unless `TcCom.initialize()` is once again called, to * reestablish the connection. * * @throws {@link TcComUnsubscribeException} - Failed to unsubscribe the Handles * @throws {@link TcComDisconnectException} - Failed to disconnect from the TwinCAT PLC * */ async disconnect() { this.__log(`disconnect() : Disconnecting TcCom Object for ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); //Check if there is a valid ADS Connection, else skip execution if (this.__ads) { this.__log(`disconnect() : Removing all Subscription handles from TcCom Object at ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); //Unsubscribe all the Change Handles, which were created during the lifetime of //the TcCom Object. Regardless of success or failure of this action, perform a disconnected //when done, and clean up remaining variables await this.__ads.unsubscribeAll() .catch(err => { throw new tc_exception_1.TcComUnsubscribeException(this.__context, this, `TcCom encountered an error when unsubscribing all handles from ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`, err); }) .finally(() => { var _a; this.__changeCounter = undefined; this.__changeHndl = undefined; this.__log(`disconnect() : Disconnecting from TcCom Object at ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); return (_a = this.__ads) === null || _a === void 0 ? void 0 : _a.disconnect(true).catch(err => { throw new tc_exception_1.TcComDisconnectException(this.__context, this, `TcCom encountered an error when disconnecting from ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`, err); }).finally(() => { this.__ads = undefined; this.__log(`disconnect() : TcCom Object killed at ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); this.emit('disconnected', new tc_event_1.TcComDisconnectedEvent(this.__context, this)); }); }); } else { this.__log(`disconnect() : TcCom Object was already disconnected at ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); this.emit('disconnected', new tc_event_1.TcComDisconnectedEvent(this.__context, this)); } } /** * Converts a given Buffer of data to Javascript Data, based on the TwinCAT Type. * **This conversion works for primitive types, and not structured** * * @param type - The TwinCAT Type, whose data is to be converted * @param buffer - The Buffer of Raw Data, that is to be converted * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for conversion * @throws {@link TcComFromRawException} - Failed to convert the Raw Data * * @return - The Javascript equivalent of `buffer` data converted from the TwinCAT `type` * */ async fromRaw(type, buffer) { if (this.__ads) { this.__log(`fromRaw() : Transforming Buffer to type [${type}]`); const data = await this.__ads.convertFromRaw(buffer, type) .catch(err => { throw new tc_exception_1.TcComFromRawException(this.__context, this, `TcCom encountered an error when transforming Buffer to type [${type}]`, err); }); this.__log(`fromRaw() : Transforming Buffer to type [${type}] result ${data}`); return data; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to convert Buffer to Data using a TcCom Object, which was not initialized or was killed previously`); } /** * Converts a primitive non-structured Javascript Value to a Buffer of Data, which can be * passed to a TwinCAT Type, as specified by the `type` argument. * **This conversion works for primitive types, and not structured** * * @param type - The TwinCAT Type, to whom the value is converted * @param value - The Javascript value, which is converted to Raw Data * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for conversion * @throws {@link TcComToRawException} - Failed to convert to Raw Data * * @return - The Data Buffer, which can be passed to a TwinCAT Symbol of Type `type`, representing the passed `value` * */ async toRaw(type, value) { if (this.__ads) { this.__log(`toRaw() : Transforming value [${value}] to Buffer for type [${type}]`); const buffer = await this.__ads.convertToRaw(value, type) .catch(err => { throw new tc_exception_1.TcComToRawException(this.__context, this, `TcCom encountered an error when transforming value [${value}] to Buffer for type [${type}]`, err); }); return buffer; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to convert Data to Buffer using a TcCom Object, which was not initialized or was killed previously`); } /** * Subscribes to a TwinCAT Symbol, with a callback, which is invoked, whenever the Symbol value changes. * The detection of change speed can be set through the `sampling` argument, in case the value changes too fast * and such detection is not needed * * @param sampling - The speed in `ms` of detecting change. Any change in this interval will not trigger change events * @param pointer - The Symbol Pointer, to which to subscribe * @param callback - The callback that is invoked, whenever Symbol change is detected * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComSubscribeException} - Failed to subscribe to the provided pointer * * @return - The Subscription Handle, that can be used to unsubscribe in the future */ async subscribe(sampling, pointer, callback) { if (this.__ads) { this.__log(`subscribe() : Subscribing to memory pointer { indexGroup : ${pointer.indexGroup}, indexOffset : ${pointer.indexOffset}, size : ${pointer.size}`); const hndl = await this.__ads.subscribeRaw(pointer.indexGroup, pointer.indexOffset, pointer.size, callback, sampling) .catch(err => { throw new tc_exception_1.TcComSubscribeException(this.__context, this, `TcCom encountered an error when subscribing to memory pointer { indexGroup : ${pointer.indexGroup}, indexOffset : ${pointer.indexOffset}, size : ${pointer.size}`, err); }); return hndl; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to issue subscription command through a TcCom Object, which was not initialized or was killed previously`); } /** * Unsubscribes the previously created TwinCAT Handle for value change event * * @param hndl - The previously create active subscription handle to a TwinCAT Symbol * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComUnsubscribeException} - Failed to unsubscribe the handle * */ async unsubscribe(hndl) { if (this.__ads) { this.__log(`unsubscribe() : Unsubscribing from handle`); await hndl.unsubscribe() .catch(err => { throw new tc_exception_1.TcComUnsubscribeException(this.__context, this, `TcCom encountered an error when unsubscribing from handle`, err); }); return; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to issue unsubscribe command through a TcCom Object, which was not initialized or was killed previously`); } /** * Performs a write operation over ADS to the TwinCAT PLC of the provided `TcDataPackages`. * When sending more than 500+ packages at once, the packages will be split in groups of 500 due to a limitation of ADS * * @param dataPackages - The packages with symbol location and data to be send to the Target * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComDataWriteException} - Failed to write data packages * */ async write(dataPackages) { if (this.__ads) { this.__log(`write() : Writing to memory pointers[${dataPackages.length}]`); const split = this.__splitData(dataPackages); for (let i = 0; i < split.length; i++) { await this.__ads.writeRawMulti(split[i]) .catch(err => { throw new tc_exception_1.TcComDataWriteException(this.__context, this, `TcCom encountered an error when writing memory packages[${dataPackages.length}]`, err); }); } return; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to write to memory pointers through a TcCom Object, which was not initialized or was killed previously`); } /** * Performs a read operation over ADS of the TwinCAT Symbol Pointers. * When requesting more than 500+ packages at once, the pointers will be split in groups of 500 due to a limitation of ADS * * @param pointer - The symbol pointers, whose data to be queried * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComDataReadException} - Failed to read data pointers * * @return - The data packages which were queried by the Symbol Pointers */ async read(pointer) { if (this.__ads) { this.__log(`read() : Reading memory pointers[${pointer.length}]`); const split = this.__splitData(pointer); const result = []; for (let i = 0; i < split.length; i++) { const response = await this.__ads.readRawMulti(split[i]) .catch(err => { throw new tc_exception_1.TcComDataReadException(this.__context, this, `TcCom encountered an error when reading memory pointers[${pointer.length}]`, err); }); result.push(...response); } return result; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to read memory pointers through a TcCom Object, which was not initialized or was killed previously`); } /** * Performs a call to a method of a specific variable over ADS * * @param variable - The variable name, whose method is called * @param method - The name of the method that is to be called * @param parameters - The parameters, which are passed to the method * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComMethodCallException} - Failed to call the Rpc Method on the PLC Side * * @return - The result of the method call */ async callMethod(variable, method, parameters) { if (this.__ads) { this.__log(`callMethod() : Calling method ${variable}#${method}`); const result = await this.__ads.invokeRpcMethod(variable, method, parameters) .catch(err => { throw new tc_exception_1.TcComMethodCallException(this.__context, this, `TcCom encountered an error when calling method ${variable}#${method}`, err); }); for (let key in result.outputs) { if (result.outputs.hasOwnProperty(key)) { return { result: result.returnValue, outputs: result.outputs }; } } return { result: result.returnValue }; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to call method through a TcCom Object, which was not initialized or was killed previously`); } /** * Queries the raw ADS Type Data from the Target PLC * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComTypeQueryException} - Failed to query Type Data * * @return - The map of all the ADS Types currently present in the TwinCAT PLC */ async types() { if (this.__ads) { this.__log(`types() : Reading types...`); const results = await this.__ads.readAndCacheDataTypes() .catch(err => { throw new tc_exception_1.TcComTypeQueryException(this.__context, this, `TcCom encountered an error when reading types`, err); }); return results; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to read types through a TcCom Object, which was not initialized or was killed previously`); } /** * Queries the raw ADS Symbol Data from the Target PLC * * @throws {@link TcComIsInvalidException} - Attempted to use an Invalid `TcCom` Object for subscription * @throws {@link TcComSymbolQueryException} - Failed to query Symbol Data * * @return - The map of all the ADS Symbols currently present in the TwinCAT PLC */ async symbols() { if (this.__ads) { this.__log(`symbols() : Reading symbols...`); const results = await this.__ads.readAndCacheSymbols() .catch(err => { throw new tc_exception_1.TcComSymbolQueryException(this.__context, this, `TcCom encountered an error when reading symbols`, err); }); return results; } else throw new tc_exception_1.TcComIsInvalidException(this.__context, this, `Attempting to read symbols through a TcCom Object, which was not initialized or was killed previously`); } on(eventName, listener) { super.on(eventName, listener); return this; } /** * Internal function, which is invoked whenever Code Changes are detected by the `TcCom` object. * Will emit the `sourceChanged` event, as well as invoke a callback which was provided * * @param response - The current PLC Last code change stamp which is used to see if changes have happened * */ __callback(response) { if (this.__changeCounter !== undefined && this.__changeCounter !== response.value) { this.__log(`TcCom Object detected Source Change at ${this.__settings.targetAmsNetId}:${this.__settings.targetAdsPort}`); this.emit('sourceChanged', new tc_event_1.TcComSourceChangedEvent(this.__context, this)); if (this.__callOnChange) { this.__callOnChange(); } ; } this.__changeCounter = response.value; } __splitData(data) { const result = []; let startIndex = 0; let endIndex = 0; do { endIndex = endIndex + 500; endIndex = (endIndex > data.length) ? data.length : endIndex; result.push(data.slice(startIndex, endIndex)); startIndex = endIndex; } while (startIndex < data.length); return result; } } /** * The Default settings, used for connecting to a TwinCAT PLC, located at localhost. * These settings are merged in, with whatever custom settings are provided during construction */ TcCom.defaultSettings = Object.assign(Object.assign({}, ads_client_1.Client.defaultSettings()), { targetAmsNetId: '127.0.0.1.1.1', targetAdsPort: 851, readAndCacheSymbols: true, readAndCacheDataTypes: true, disableStructPackModeWarning: true }); /** * Path to the PLC Symbol, used as the Code Change Tracker * @internal */ TcCom.CHANGE_COUNTER = 'TwinCAT_SystemInfoVarList._AppInfo.AppTimestamp'; return TcCom; })(); exports.TcCom = TcCom; /** * List of constants, which provide information on what PLC Type the Type is. * This list of constants can be found here, with more information: [ADSDATATYPEID](https://infosys.beckhoff.com/english.php?content=../content/1033/tcplclib_tc2_utilities/9007199290071051.html&id=) */ exports.ADST = { VOID: 0, INT8: 16, UINT8: 17, INT16: 2, UINT16: 18, INT32: 3, UINT32: 19, INT64: 20, UINT64: 21, REAL32: 4, REAL64: 5, BIGTYPE: 65, STRING: 30, WSTRING: 31, REAL80: 32, BIT: 33, };