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
JavaScript
"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,
};