Unofficial Node.JS Library for TwinCAT ADS Communication, designed to simplify connection to a Beckhoff PLC, and automatically generating a Symbol Map ADS Protocol from Beckhoff for ease of read/write and subscribe data operations.
The tc-context
library achieves this by fetching all the type data and initial symbol data at the moment connection and caching the memory locations of each individual symbol, as well as their parent symbol relationship. This grants the possibility to write partial structures directly into TwinCAT Function_Blocks
and Structures
, subscribing to complex objects, and clearing multiple Namespaces, all with single-line instructions at minimal run-time cost.
This library is made possible, thanks to the ads-client library by Jussi Isotalo j.isotalo91@gmail.com. If you are in search for a smaller library, focusing on ADS communication, make sure to check theads-client library.
For detailed information, see the official documentation.
Below is a quick example of creating a TcContext
for a Beckhoff PLC located at localhost, and performing read/write/clear operations. For more complex operations, and behavior definitions, see individual chapters on the subject matter.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL := TRUE;
numericValue : INT := 10;
structuredValue : Foo;
arrayValue : ARRAY [0..9] OF STRING := ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j']
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
realValue : REAL;
stringValue : STRING := 'hello world';
END_VAR
index.js
const { TcContext } = require('tc-context');
TcContext.create().then(async context => {
//Reading the program's 'MAIN' Namespace
let result = await context.$.MAIN.$get
/**
* result : {
* booleanValue : true,
* numericValue : 10,
* structuredValue : {
* realValue : 0,
* stringValue : 'hello world'
* },
* arrayValue : ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j']
* }
*/
//Clearing all members of a Structured Value
await context.$.MAIN.structuredValue.$clear()
result = await context.$.MAIN.structuredValue.$get;
/**
* result : { realValue : 0, stringValue : '' }
*/
//Setting a Numeric value
await context.$.MAIN.numericValue.$set(5);
result = await context.$.MAIN.numericValue.$get;
/**
* result : 5
*/
//Setting an Array
await context.$.MAIN.arrayValue.$set(['1', '2', '3']);
result = await context.$.MAIN.arrayValue.$get;
/**
* result : ['1', '2', '3', 'd', 'e', 'f', 'g', 'h', 'j']
*/
//Setting a program's Namespace values
const set = {
booleanValue : false,
structuredValue : {
realValue : 15,
stringValue : 'i am alive'
}
}
await context.$.MAIN.$set(set)
result = await context.$.MAIN.arrayValue.$get;
/**
* result : {
* booleanValue : false,
* numericValue : 5,
* structuredValue : {
* realValue : 15,
* stringValue : 'i am alive'
* },
* arrayValue : ['1', '2', '3', 'd', 'e', 'f', 'g', 'h', 'j']
* }
*/
//Clearing Namespace
await context.$.MAIN.$clear();
result = await context.$.MAIN.arrayValue.$get;
/**
* result : {
* booleanValue : false,
* numericValue : 0,
* structuredValue : {
* realValue : 0,
* stringValue : ''
* },
* arrayValue : ['', '', '', '', '', '', '', '', '']
* }
*/
//Disconnecting Context and killing bindings
await context.kill()
})
Run the following command to install package in your project:
npm install tc-context
Include the module in js:
const { TcContext } = require('tc-context')
The tc-context
library uses the ads-client library for connecting and communicating with a Beckhoff PLC over the ADS Protocol. Because of that, the connection setup configuration is equal to the connection setup of the ads-client, since the supplied settings are routed directly to the ads-client
.
See ads-client for detailed information on different types of possible connections.
const { TcContext } = require('tc-context')
//Connecting to a Localhost Beckhoff PLC
TcContext.create().then(async context => {
//Result contains the currently active context
//Perform operation with the created context....
//When done, kill the context
await context.kill();
})
In order to create a new TcContext
Object, a call to the TcContext.create()
functions must be made. By default, if no arguments are passed to the TcContext.create()
function, it is assumed to that the connection that will be made is to the localhost located PLC. Explicit settings can be provided as the first argument to the TcContext.create()
function.
NOTE: The TcContext
object must be explicitly killed at the end of its use, through the TcContext.kill()
method call. This will clean up all subscription handles, termination the connection, and clear the generated map, thus ensuring no memory leaks.
const { TcContext } = require('tc-context')
const settings = {
targetAmsNetId: '192.168.1.120.1.1',
targetAdsPort: 851
//And more....
}
//Connecting to a Localhost Beckhoff PLC
TcContext.create(settings).then(async context => {
//Result contains the currently active context for 192.168.1.120.1.1:851
//Perform operation with the created context....
//When done, kill the context
await context.kill();
})
The settings are routed to the ads-client Client.connect()
method directly, without any modification. Hence, for more detailed and up-to-date information, on different connection patters, see the official documentation of ads-client.
The TcContext
Object is composed of 3 Components :
TcSymbolRegistry
- Storage for all the Symbol Maps created based based on the data gathered by TcCom
', passed through the TcTypeRegistry
. This property can be accessed by the TcContext.symbols
property, and the registered symbol maps can be accessed by the TcContext.symbols.namespaces
property. The TcSymbolNamespace
represents the Programs and any Global Variable Lists present in the PLC.TcContext.$
, which corresponds to TcContext.symbols.namespaces
TcTypeRegistry
- Storage for all the processed types, which are gathered by TcCom
Component. This component can be accessed through TcContext.types
property, and the registered types can be queried by TcContext.types.has(<type name>)
.TcCom
- Component responsible for communication and data passing between TcContext
and the PLC. This component can be accessed through TcContext.COM
property.Upon a successful TcContext
creation, a full TcSymbol
map is created, mirroring what is currently loaded in the Beckhoff PLC. The create TcSymbolNamespaces
can be accessed either through TcContext.symbols.namespaces
or TcContext.$
properties.
In order to avoid naming conflicts with symbols declared in the PLC, all public methods provided by the TcSymbol
Object begin with ``$` symbol.
IMPORTANT: Type safety is important and there is no implicit type conversion. This is to ensure, that whatever is written is well defined by both ends of the system. In case of type mismatch, an exception TcBindingInvalidTypeException
is raised and no operation is performed.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
numericValue : INT;
rangeValue : BYTE(50..100);
stringValue : STRING(50);
enumValue : MyEnum;
arrayValue : ARRAY[0..4] OF Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
{attribute 'Default' := '22.3'}
realValue : REAL;
{attribute 'ReadOnly'}
stringValue : STRING;
END_VAR
MyEnum(ENUM)
TYPE MyEnum :
(
member1 := 0,
member2 := 1,
member3 := 2
);
END_TYPE
The example above would produce a TcContext
of the following type:
TcContext.$.MAIN
- the MAIN Program.booleanValue
- symbol of type TcBooleanSymbol : TcSymbol
.numericValue
- symbol of type TcNumericSymbol : TcSymbol
.rangeValue
- symbol of type TcNumericSymbol : TcSymbol
with explicit value borders at [50-100]
, and a write of a value outside these borders raises the TcBindingOutOfRangeException
exception.stringValue
- symbol of type TcStringSymbol : TcSymbol
, with a max length of [50]
characters, and any attempt to write a string of [50+]
length raises the TcBindingOutOfRangeException
exception.enumValue
- symbol of type TcEnumSymbol : TcSymbol
, which only accepts input strings from ['MyEnum.member1', 'MyEnum.member2', 'MyEnum.member3']
list, and any write of value outside that list raises the TcBindingOutOfRangeException
exception.arrayValue
- symbol of type TcArraySymbol : TcSymbol
, which represents an array of size [5]
of type Foo
type Function_Block
[0-4]
- symbols of type TcStructureSymbol : TcSymbol
.realValue
- symbol of type TcNumericSymbol : TcSymbol
, with an explicit default value of 22.3
, which will be set, when ``TcSymbol.$clear()` is called.stringValue
- symbol of type TcStringSymbol : TcSymbol
, which is set to ReadOnly
, thus any call to TcSymbol.$clear()
or TcSymbol.$set()
results in an exception TcBindingReadOnlyException
, as well as a call to .stringValue
parent TcSymbol.$clear()
method will ignore this symbolconst { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Exception TcBindingOutOfRangeException
await context.$.MAIN.rangeValue.$set(10)
//MAIN.enumValue is set to 1
await context.$.MAIN.enumValue.$set('MyEnum.member2')
//Exception TcBindingReadOnlyException
await context.$.MAIN.arrayValue[0].stringValue.$set('hello world')
//For [0-4] all the .realValue members are set to 22.3
//while .stringValue member is left untouched
await context.$.MAIN.arrayValue.$clear()
//Explicit TcContext kill call
await context.kill();
})
The tc-context
library currently supports all of the TwinCAT Data Types, with the exception of:
NOTE: The TcContext
Object filters out unsupported members, when generating Symbol Maps. If the end result of a Structure
, Funtion_Block
or Array
, is a Symbol with no Children, no mapping will be created for that Object as well.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
filteredFB : Foo;
filteredArr : ARRAY[0..9] OF REFERENCE TO Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
ptr: POINTER TO INT;
ref : REFERENCE TO BYTE;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//booleanValue is defined
const booleanValue = context.$.MAIN.booleanValue
//filteredFB is undefined
const filteredFB = context.$.MAIN.filteredFB
//filteredArr is undefined
const filteredArr = context.$.MAIN.filteredArr
//Explicit TcContext kill call
await context.kill();
})
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
numericValue : INT;
stringValue : STRING(50);
enumValue : MyEnum;
END_VAR
MyEnum(ENUM)
TYPE MyEnum :
(
member1 := 0,
member2 := 1,
member3 := 2
);
END_TYPE
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Returns the current value of MAIN.booleanValue
const booleanValue = await context.$.MAIN.booleanValue.$get
//Sets the value of MAIN.booleanValue
await context.$.MAIN.booleanValue.$set(true);
//Sets the value of MAIN.enumValue to MyEnum.member2 (1) and then read it back
const enumValue = await context.$.MAIN.enumValue.$set('MyEnum.member2')
.then(() => context.$.MAIN.enumValue.$get )
if (enumValue === 'MyEnum.member2') {
//Clears the MAIN.enumValue to MyEnum.member1 (0)
await context.$.MAIN.enumValue.$clear()
}
//Explicit TcContext kill call
await context.kill();
})
The TcSymbol
Object supports the following data operations:
$set(val)
- method for writing a value to the Binding of the TcSymbol
Object.
Promise
is fulfilled, it returns the value, which was successfully written to the TcSymbol
. Promise
is rejected, returns a TcException
Type Object, with information regarding the error$get
- property for reading the value of the TcSymbol
Object Binding.
Promise
is fulfilled, it returns the value, which was successfully read from the TcSymbol
. Promise
is rejected, returns a TcException
Type Object, with information regarding the error$clear()
- method for clearing the Binding of the TcSymbol
based on either its default value, or the explicitly specified Default
Attribute value
Promise
is fulfilled, clearing has successfully completedPromise
is rejected, returns a TcException
Type Object, with information regarding the errorMAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
numericValue : INT;
stringValue : STRING(50);
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
booleanValue : BOOL;
arrayValue : ARRAY[0..9] OF INT;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Activate on-change detection
await context.$.MAIN.numericValue.$subscribe()
await context.$.MAIN.foo.$subscribe()
//Attach event handlers
context.$.MAIN.numericValue.$on('changed', (e) => {
console.log(`Boolean Value has changed to ${e.data}`);
})
context.$.MAIN.foo.$on('changed', (e) => {
console.log(`{
booleanValue : ${e.data.booleanValue},
arrayValue : ${e.data.arrayValue.toString()}
}`)
})
//When no longer change detection is needed, disable it
await context.$.MAIN.foo.$unsubscribe();
//Subscribe to an array.
await context.$.MAIN.foo.arrayValue.$subscribe();
context.$.MAIN.foo.arrayValue.$on('changed', (e) => {
console.log(e.data.toString())
})
//Explicit TcContext kill call will also
//automatically unsubscribe all active change subscriptions
await context.kill();
})
TcSymbol
Objects are capable of subscribing to a data changed
event of type TcSymbolChangeEvent
. When subscribed, if the PLC symbol changes its value, any handlers, which were attached, will be invoked. To subscribe TcSymbol.$subscribe()
must be called, and to disable change detection TcSymbol.$unsubscribe()
is called. Handlers are added to the TcSymbol
by way of calling TcSymbol.$on(...)
and TcSymbol.$once(...)
and are removed through TcSymbol.$off(...)
. When unsubscribing, event handlers are not removed, they are simply ignored.
When subscribing to Structures
,Function_Blocks
and Arrays
, any change that happens to the data of that symbol, will also result in the changed
event. This includes change in symbol that are not supported.
MAIN(PRG)
PROGRAM MAIN
VAR
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
booleanValue : BOOL := FALSE;
ptr : POINTER TO STRING;
bar : Bar
END_VAR
Bar(FB)
FUNCTION_BLOCK Bar
VAR
numericValue : INT := 5;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Activate on-change detection
await context.$.MAIN.foo.$subscribe()
//Attach event handlers
context.$.MAIN.foo.$on('changed', (e) => {
console.log(e.data);
}
//Hypothetical scenario of indefinite run-time
//When hypothetical scenario is finished
await context.kill();
})
In the example above, a TcSymbol.$subscribe()
is activated on MAIN.foo
, which would mean, any change happening in MAIN.foo
will invoke the changed
event. When MAIN.foo.booleanValue
changes from FALSE to TRUE, the TcEvent.data
will contain an output:
{
booleanValue : true,
bar : {
numericValue : 5
}
}
Following the previous scenario, if MAIN.foo.bar.numericValue
has a change from 5 to 10, this will also produce the changed
event, with the following output:
{
booleanValue : true,
bar : {
numericValue : 10
}
}
Lastly, even though MAIN.foo.ptr
is not supported, and a binding to it is not created, it is still part of the data layout of MAIN.foo
Symbol. It is because of this a change to the pointer value (not what its pointing to), will also produce a changed
event, with the following output (The 2 previous examples have happened) :
{
booleanValue : true,
bar : {
numericValue : 10
}
}
MAIN(PRG)
PROGRAM MAIN
VAR
numericValue : INT;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Subscribe with a sampling rate of 1s
await context.$.MAIN.numericValue.$subscribe(1000)
//Attach event handlers
context.$.MAIN.numericValue.$on('changed', (e) => {
console.log(e.data);
}
//When hypothetical scenario is finished
await context.kill();
})
For Symbols, the value of which changes rapidly, and detection of the change each time would either be too costly or unneeded, it is possible to explicitly set the sampling rate in milliseconds, as the argument for TcSymbol.$subscribe()
. This will keep any changes of the Symbol from emitting changed
event between the sampling period.
MAIN(PRG)
PROGRAM MAIN
VAR
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
booleanValue : BOOL;
numericValue : INT;
stringValue : STRING;
bar : Bar
END_VAR
Bar(FB)
FUNCTION_BLOCK Bar
VAR
numericValue : INT;
booleanValue : BOOL;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
await context.$.MAIN.foo.$set({ booleanValue : true, numericValue : 10});
let result = await context.$.MAIN.foo.$get;
/**
* result : {
* booleanValue : true
* numericValue : 10
* stringValue : ''
* bar : {
* numericValue : 0
* booleanValue : false
* }
* }
*/
await context.$.MAIN.foo.$set({
stringValue : 'hello world'
bar : {
numericValue : 15,
booleanValue : true
}
})
result = await context.$.MAIN.foo.$get;
/**
* result : {
* booleanValue : true
* numericValue : 10
* stringValue : 'hello world'
* bar : {
* numericValue : 15
* booleanValue : true
* }
* }
*/
//Writing to a non existent child, will result in an exception
await context.$.MAIN.foo.$set({ nonExistent : 56.6 })
.catch(err => console.log(err));
/**
* TcBindingOutOfRangeException : ....
*/
context.$.MAIN.foo.bar.$each(symbol => {
console.log(symbol.$binding.path)
})
/**
* Output :
* MAIN.foo.bar.numericValue
* MAIN.foo.bar.booleanValue
*/
await context.$.MAIN.foo.$clear();
result = await context.$.MAIN.foo.$get;
/**
* result : {
* booleanValue : false
* numericValue : 0
* stringValue : ''
* bar : {
* numericValue : 0
* booleanValue : false
* }
* }
*/
await context.kill();
})
TcSymbol
Objects are capable of operating on Structures
and Function_Blocks
. For TcContext
both Structures
and Function_Blocks
are treated equally as structures of data. It is possible to write only explicit parts of a TcSymbol
symbol, bound to a Structure
or Function_Block
Calling TcSymbol.$clear()
will clear all the members of the Structure
or Function_Block
, with the only exception of ignoring any child marked with {attribute 'ReadOnly'}.
By calling TcSymbol.$get
property, a full data-map of all nested-children is returned, with the exception of symbol marked with {attribute 'Ignore'}
When writing to a child, which is not part of TcSymbol
, the operation will result in an exception of type TcBindingOutOfRangeException
.
MAIN(PRG)
PROGRAM MAIN
VAR
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
booleanValue : BOOL;
END_VAR
{attribute 'TcRpcEnable'}
METHOD basicTask : INT
VAR_INPUT
lhs : INT,
rhs : INT
END_VAR
basicTask := lhs + rhs;
{attribute 'TcRpcEnable'}
METHOD complexTask : BOOL
VAR_INPUT
lhs : INT;
rhs : INT;
END_VAR
VAR_OUTPUT
sum : INT;
diff : INT;
END_VAR
complexTask := TRUE;
sum := lhs + rhs;
diff := lhs - rhs;
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
const basicTask = await context.$.MAIN.foo.basicTask({ lhs : 10, rhs : 15 });
/**
* basicTask : {
* result : 25
* }
*/
const complexTask = await context.$.MAIN.foo.complexTask({ lhs : 15, rhs : 10 });
/**
* basicTask : {
* result : 25
* outputs : {
* sum : 25,
* diff : 5
* }
* }
*/
await context.kill();
})
It is possible to invoke RPC Methods, marked with {attribute 'TcRpcEnable'}
, on the PLC Side through the TcContext
. Upon completion, a Javascript Object is returned with a field result
, which contains the return value of the method call.
If the method has VAR_OUTPUT
variables, the return of the method call will also contain outputs
field, which store the values.
NOTE: As of now, no type checking is performed when passing values to the method call, it is a direct route to the ads-client. See official documentation for the limitations of Rpc Method calls. This is important when dealing with enumerators
. When writing and reading enumerators
directly, their type is included, however when passing them to a Rpc Method, the type name of the enum must be omitted.
If the method call fails, exceptions of type TcComIsInvalidException
or TcComMethodCallException
are raised.
MAIN(PRG)
PROGRAM MAIN
VAR
myUnion : MyUnion;
END_VAR
MyUnion(UNION)
TYPE DUT :
UNION
a : LREAL;
b : LINT;
c : WORD;
END_UNION
END_TYPE
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
let result = await context.$.MAIN.myUnion.$get
/**
* result : {
* a : 0,
* b : 0,
* c : 0
* }
*/
await context.$.MAIN.myUnion.c.$set(10)
let result = await context.$.MAIN.myUnion.$get
/**
* result : {
* a : 4.94065645841247E-323,
* b : 10,
* c : 10
* }
*/
await context.kill();
})
UNIONs
are considered Structured symbols in the tc-context
library, and are accessed and manipulated the same way as Structured TcSymbols
.
MAIN(PRG)
PROGRAM MAIN
VAR
arr : ARRAY[0..4] OF BOOLEAN;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
await context.$.MAIN.arr.$set([true, true, true]);
let result = await context.$.MAIN.arr.$get;
/**
* result : [true, true, true, false, false]
*/
await context.$.MAIN.arr.$clear();
result = await context.$.MAIN.arr.$get;
/**
* result : [false, false, false, false, false]
*/
await context.$.MAIN.arr[3].$set(true);
result = await context.$.MAIN.arr[3].$get;
/**
* result : true
*/
result = await context.$.MAIN.arr.$get;
/**
* result : [false, false, true, false, false]
*/
context.$.MAIN.arr.$each(symbol => {
console.log(symbol.$binding.name)
})
/**
* Output :
* arr[0]
* arr[1]
* arr[2]
* arr[3]
* arr[4]
*/
await context.kill();
})
The TcContext
is capable of generating a TcSymbol
for TwinCAT array types, allowing for reading, writing, clearing and subscribing operations on the array as a whole, as well as accessing individual members through the subscript operator.
NOTE: The indexes of TcSymbol
array are synchronized with the TwinCAT array indexes. This means the starting index of a TcSymbol
of an array
is that of the starting symbol of the TwinCAT array
.
MAIN(PRG)
PROGRAM MAIN
VAR
arr1 : ARRAY[0..4] OF INT;
arr2 : ARRAY[1..5] OF INT;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
console.log({
startIndex : context.$.MAIN.arr1.$startIndex,
length : context.$.MAIN.arr1.$length
})
/**
* Output : { startIndex : 0, length : 5 }
*/
console.log({
startIndex : context.$.MAIN.arr2.$startIndex,
length : context.$.MAIN.arr2.$length
})
/**
* Output : { startIndex : 1, length : 5 }
*/
context.$.MAIN.arr1.$each((symbol, index) => {
console.log({ index, name : symbol : symbol.$binding.key})
})
/**
* Output :
* { index : 0, symbol : arr1[0]}
* { index : 1, symbol : arr1[1]}
* { index : 2, symbol : arr1[2]}
* { index : 3, symbol : arr1[3]}
* { index : 4, symbol : arr1[4]}
*/
context.$.MAIN.arr2.$each((symbol, index) => {
console.log({ index, name : symbol : symbol.$binding.key})
})
/**
* Output :
* { index : 1, symbol : arr1[1]}
* { index : 2, symbol : arr1[2]}
* { index : 3, symbol : arr1[3]}
* { index : 4, symbol : arr1[4]}
* { index : 5, symbol : arr1[5]}
*/
//elem is undefined
const elem = context.$.MAIN.arr2[0];
await context.kill();
})
MAIN(PRG)
PROGRAM MAIN
VAR
arr1 : ARRAY[0..2, 0..2] OF INT;
arr2 : ARRAY[0..2] OF ARRAY[0..2] OF INT;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
let result = await context.$.MAIN.arr1.$get;
/**
* result : [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
*/
result = await context.$.MAIN.arr1[1].$get;
/**
* result : [0, 0, 0]
*/
await context.$.MAIN.arr2[2].$set([1, 2, 3])
result = await context.$.MAIN.arr2.$get;
/**
* result : [[0, 0, 0], [0, 0, 0], [1, 2, 3]]
*/
await context.$.MAIN.arr1[1][1].$set(10);
result = await context.$.MAIN.arr1[1].$get;
/**
* result : [0, 10, 0]
*/
result = await context.$.MAIN.arr2.$clear()
.then(() => context.$.MAIN.arr2.$get)
/**
* result : [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
*/
result = await context.$.MAIN.arr1[1].$clear()
.then(() => context.$.MAIN.arr1.$get)
/**
* result : [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
*/
await context.kill();
})
TwinCAT allows for 2 different ways of declaring Multidimensional Arrays :
ARRAY[dimension1, dimension2, ...., dimensionN] OF <type>
ARRAY[dimension1] OF ARRAY[dimension2] OF ..... ARRAY[dimensionN] of <type>
Regardless of the case, the TcContext
treats both of these multidimensional arrays as multidimensional Javascript Arrays, and will process them accordingly. This allows for TcSymbol.$get
, TcSymbol.$set()
, TcSymbol.$clear()
and TcSymbol.$subscribe()
operations on any of the dimension levels.
MAIN(PRG)
PROGRAM MAIN
VAR
numericValue : INT[10..50];
stringValue : STRING(10);
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
arrayValue : ARRAY[0..4] OF int;
enumValue : MyEnum;
END_VAR
MyEnum(ENUM)
TYPE MyEnum :
(
member1 := 0,
member2 := 1,
member3 := 2
);
END_TYPE
index.js
const { TcContext, TcSymbols } = require('tc-context')
TcContext.create().then(async context => {
if (context.$.MAIN.numericValue instanceof TcSymbols.TcNumericSymbol) {
console.log({
min : context.$.MAIN.numericValue.$lowerBorder
max : context.$.MAIN.numericValue.$upperBorder
})
}
/**
* Output :
* { min : 10, max : 50 }
*/
if (context.$.MAIN.stringValue instanceof TcSymbols.TcStringSymbol) {
console.log({ length : context.$.MAIN.stringValue.$length })
}
/**
* Output :
* { length : 10 }
*/
if (context.$.MAIN.foo instanceof TcSymbols.TcStructureSymbol) {
context.$.MAIN.foo.$each(symbol => {
if (symbol instanceof TcSymbols.TcArraySymbol) {
console.log({ length : symbol.$length })
} else if (symbol instanceof TcSymbols.TcEnumSymbol) {
console.log({ fields : symbol.$fields })
}
})
}
/**
* Output :
* { length : 5 }
* { fields : ['MyEnum.member1', 'MyEnum.member2', 'MyEnum.member3']}
*/
//Explicit TcContext kill call
await context.kill();
})
TcSymbol
provides the base foundation for TcContext
Symbol Map creation. Each of the TcSymbols
, has a concrete type, depending on the PLC Symbol Type. The concrete TcSymbol
performs type checks and range checks, as well as exposes additional, type specific, functionality. The concrete types of TcSymbol
and their unique functionality is described below.
There is no implicit type conversion done. All symbols must be provided with the exact type of value, when performing TcSymbol.$set()
operations. The reason is to ensure there is no ambiguity when operating on the PLC.
TcBooleanSymbol
represents the BOOL
Data Type of the PLC, and is the most similar to the base TcSymbol
. The only difference is, that it performs type checking abd throwing an exception of type TcBindingInvalidTypeException
, if during the TcSymbol.$set()
call, the value passed is not of type boolean
.
TcNumericSymbol
represents all the numeric Data Type Symbols of the PLC, and exposes additional symbol information, in the form of TcNumericSymbol.$upperBorder
and TcNumericSymbol.$lowerBorder
.
When performing a TcSymbol.$set()
, the input value is check to be of type number
, and that the value is within the specified range. Failure to comply results in exceptions of type TcBindingInvalidTypeException
and TcBindingOutOfRangeException
respectfully.
TcStringSymbol
represents STRING
and WSTRING
Data Types of the PLC, and exposes the TcStringSymbol.$length
property, which states the maximum length of a string, that can be written to it.
TcStringSymbol
performs type checking on the the passed argument to TcSymbol.$set()
to make sure it is of type string
and does not exceed the specified length. Failure to comply results in exceptions of type TcBindingInvalidTypeException
and TcBindingOutOfRangeException
respectfully.
TcEnumSymbol
represents the ENUM
Data Types of the PLC and provides the TcEnumSymbol.$fields
property. This property provides a list of strings of all the acceptable inputs to the `TcSymbol.
The TcEnumSymbol
performs type checking on the the passed argument to TcSymbol.$set()
to make sure it is of type string
and is part of the TcEnumSymbol.$fields
list. Failure to comply results in exceptions of type TcBindingInvalidTypeException
and TcBindingOutOfRangeException
respectfully.
IMPORTANT: Alias to ENUM
is treated as its own unique type. Be aware of this.
MAIN(PRG)
PROGRAM MAIN
VAR
enumValue : MyEnum;
END_VAR
MyEnum(ENUM)
TYPE MyEnum :
(
member1 := 0,
member2 := 1,
member3 := 2
);
END_TYPE
index.js
const { TcContext, TcEnumSymbol } = require('tc-context');
TcContext.create().then(async context => {
if (context.$.MAIN.enumValue instanceof TcSymbols.TcEnumSymbol) {
console.log(context.$.MAIN.enumValue.$fields)
}
/**
* Output :
* ['MyEnum.member1', 'MyEnum.member2', 'MyEnum.member3']
*/
await context.$.MAIN.enumValue.$set('MyEnum.member3');
let result = await context.$.MAIN.enumValue.$get;
/**
* result : 'MyEnum.member3'
*/
await context.$.MAIN.enumValue.$set('WrongValue')
.catch(err => console.log(err))
/**
* Output :
* TcBindingOutOfRangeException
*/
await context.kill()
})
TcStructureSymbol
represents Structure
and Function_Block
Data Types of the PLC. As of now, no distinction between the two types is made from the pointer of view of TcContext
.
The TcStructureSymbol
performs a type check, to make sure any argument that is passed to TcSymbol.$set()
is a plain Javascript Object, otherwise a TcBindingInvalidTypeException
exception is raised.
Additionally, TcStructureSymbol
provides TcStructureSymbol.$each()
function, which iterates over all of its children TcSymbols
.
TcArraySymbol
represents ARRAY[...] OF <type>
Data Type of the PLC. Similarly to the TcStructureSymbol
, the TcArraySymbol
also provides a TcArraySymbol.$each()
function, which iterates over all of its children TcSymbols
.
Due to TwinCAT allowing to explicitly specify the starting index of an array, that start index can be read through the TcArraySymbol.$startIndex
property. Lastly, TcArraySymbol.$length
allows to read the length of the TcArraySymbol
array.
When writing to TcSymbol.$set()
, the TcArraySymbol
will perform type checking on the input argument, to make sure it is of type array
, as well as that the written array does not exceed the length of the TcArraySymbol
. Failure to comply results in exceptions of type TcBindingInvalidTypeException
and TcBindingOutOfRangeException
respectfully.
In a situation the where the TcContext
was reinitialized
, any previously stored TcSymbol
becomes invalidated. One way to managed invalidation, is by assigning a callback to TcSymbol.$onInvalidated()
method. This method will be called, when the TcContext
invalidates the TcSymbol
. Invalidation in a Symbol Map happens from the bottom-up, where children are invalidated before their parents are.
Beckhoff PLC Symbol can be marked with special attributes, which are processed by TcContext
. These attributes alter the behavior of some TcSymbols
and impose additional rules.
All Primitive Data Types (Not Structures
, Function_Blocks
or Arrays
) support the Default
Attribute. This attribute informs what value to write upon a call to TcSymbol.$clear()
. If no value is specified, the value that is written, is that of a an initial value (false for boolean
, 0 for numbers
and empty string for string
).
Structures
, Function_Blocks
and Arrays
are not affected by the Default
attribute.
MAIN(PRG)
PROGRAM MAIN
VAR
{attribute 'Default' := 'true'}
booleanValue : BOOL;
{attribute 'Default' := '5'}
numericValue : INT;
{attribute 'Default' := 'hello world'}
stringValue : STRING(50);
{attribute 'Default' := 'member3'}
enumValue : MyEnum;
END_VAR
MyEnum(ENUM)
TYPE MyEnum :
(
member1 := 0,
member2 := 1,
member3 := 2
);
END_TYPE
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//MAIN.booleanValue is set to true
await context.$.MAIN.booleanValue.$clear()
//MAIN.numericValue is set to 5
await context.$.MAIN.numericValue.$clear()
//MAIN.stringValue is set to 'hello world'
await context.$.MAIN.stringValue.$clear()
//MAIN.enumValue is set to 'MyEnum.member3'
await context.$.MAIN.enumValue.$clear()
//Explicit TcContext kill call
await context.kill();
})
All Bindable Types support the ReadOnly
Attribute. The ReadOnly
attributes ensures that no write operation can be made to the TcSymbol
Object, either through the TcSymbol.$set()
method or TcSymbol.$clear()
. If a write operation is made to a ReadOnly
Object, a TcBindingReadOnlyException
Exception is thrown.
If TcStructureSymbol
or TcArraySymbol
is marked as ReadOnly
, then all of their children TcSymbols
are marked as ReadOnly
as well. When calling TcSymbol.$clear()
on TcStructureSymbol
Object that is not ReadOnly
, but has children marked as ReadOnly
, those children will be ignored during the TcSymbol.$clear()
operation.
MAIN(PRG)
PROGRAM MAIN
VAR
{attribute 'ReadOnly'}
booleanValue : BOOL;
{attribute 'ReadOnly'}
numericValue : INT;
{attribute 'ReadOnly'}
readOnlyFoo : Foo;
normalFoo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
{attribute 'ReadOnly'}
stringValue: STRING := 'hello world';
numericValue : BYTE := 100;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//MAIN.booleanValue will throw exception
await context.$.MAIN.booleanValue.$clear().catch(err => {
//MAIN.booleanValue is ReadOnly. This block will execute
})
//MAIN.numericValue will throw exception
await context.$.MAIN.numericValue.$clear().catch(err => {
//MAIN.numericValue is ReadOnly. This block will execute
})
//MAIN.foo will throw exception
await context.$.MAIN.foo.$clear().catch(err => {
//MAIN.foo is ReadOnly. This block will execute
})
//This will not raise an exception, however MAIN.normalFoo.stringValue will be left untouched
const result = await context.$.MAIN.normalFoo.$clear().then(() => context.$.MAIN.normalFoo.$get);
/**
* result : {
* stringValue : 'hello world'
* numericValue : 0
* }
*/
//Explicit TcContext kill call
await context.kill();
})
All bindable types support the Ignore
Attribute. The Ignore
attributes ensures that no TcSymbol
generation takes place by the TcContext
. The Ignore
attribute, when applied to TcStructureSymbol
and TcArraySymbol
, will also be applied to its children. If the end result of a TcStructureSymbol
is an Object with no children, then that TcStructureSymbol
will also be ignored.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
numericValue : INT;
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
{attribute 'Ignore'}
stringValue: STRING := 'hello world';
byteValue : BYTE := 100;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//booleanValue is defined
const booleanValue = context.$.MAIN.booleanValue;
//numericValue is defined
const numericValue = context.$.MAIN.numericValue;
//byteValue is defined
const byteValue = context.$.MAIN.foo.byteValue;
//stringValue is undefined
const stringValue = context.$.MAIN.foo.stringValue;
//Explicit TcContext kill call
await context.kill();
})
It is possible to apply an alias to the events produced by TcSymbols
. This way it is possible to narrow-down event handling, based on the specified alias.
MAIN(PRG)
PROGRAM MAIN
VAR
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
{attribute 'onSet' := 'stringSet'}
stringValue: STRING := 'hello world';
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Listen for the aliased event
context.$.MAIN.foo.stringValue.$on('stringSet', (e) => {
console.log('set was aliased to stringSet : ', e.data);
})
//Explicit TcContext kill call
await context.kill();
})
When aliasing a TcSymbol's
event, the default event name will be replaced by the provided name, based on the Attribute Parameter. These attributes are:
onSet
- will replace the set
eventonGet
- will replace the get
eventonClear
- will replace the cleared
eventonChange
- will replace the changed
event.All events emitted by the components of TcContext
inherit from TcEvent
. The TcEvent
in TcContext
are designed to propagate the emitted event up from the component , all the way to the TcContext
Object.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL;
numericValue : INT;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
context.$.MAIN.booleanValue.$on('set', () => {
console.log('set was called on MAIN.booleanValue')
})
context.on('set', () => {
console.log('event propagated to context, where it was also caught')
})
await context.$.MAIN.booleanValue.$set(true);
/**
* When set completed output :
* 'set was called on MAIN.booleanValue'
* 'context also caught the event'
*/
//Explicit TcContext kill call
await context.kill();
})
If the TcEvent
has been handled, and the propagation of the event is no longer wanted, a call to TcEvent.stopPropagation()
function will stop any propagation up towards TcContext
.
MAIN(PRG)
PROGRAM MAIN
VAR
foo : Foo;
END_VAR
Foo(FB)
FUNCTION_BLOCK Foo
VAR
{attribute 'onChange' := 'booleanChanged'}
booleanValue: BOOL;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
context.$.on('booleanChanged', (e) => {
console.log('caught booleanChanged event')
e.stopPropagation()
})
context.on('booleanChanged', () => {
//This will never happen, because event stopped Propagated
})
//Explicit TcContext kill call
await context.kill();
})
List of all events produced by the tc-context
library.
Component | Event | Event Type | Description |
---|---|---|---|
TcComponent |
killed |
TcContextKilledEvent |
When the current TcContext is killed and no longer is valid |
reinitialized |
TcContextReinitializedEvent |
When the current TcContext was rebuild and is once again valid |
|
TcCom |
connected |
TcComConnectedEvent |
When the current TcContext established the initial connection to the PLC |
disconnected |
TcComDisconnectedEvent |
When the current connection to the PLC has been closed | |
sourceChanged |
TcComSourceChangedEvent |
When the PLC Code base changes during an active connection | |
connectionLost |
TcComConnectionLostEvent |
When the TcCom looses connection to the Target PLC | |
reconnected |
TcComReconnectedEvent |
When the TcCom reestablishes the connection to the Target PLC | |
TcSymbol |
set |
TcSymbolSetEvent |
When a .$set() operation was completed |
get |
TcSymbolGetEvent |
When a .$get operation was completed |
|
cleared |
TcSymbolClearedEvent |
When a .$clear() operation was completed |
|
changed |
TcSymbolChangedEvent |
When the symbol value changed post .$subscribe() operation |
|
TcSymbolRegistry |
created |
TcSymbolRegistryCreatedEvent |
When the Symbol Map has been created |
destroyed |
TcSymbolRegistryDestroyedEvent |
When the Symbol Map has been destroyed | |
TcTypeRegistry |
created |
TcTypeRegistryCreatedEvent |
When the Type Map has been created |
destroyed |
TcTypeRegistryDestroyedEvent |
When the Type Map has been destroyed |
Base | Component | Concrete |
---|---|---|
TcEvent |
TcContextEvent |
TcContextReinitializedEvent |
TcContextKilledEvent |
||
TcSymbolEvent |
TcSymbolGetEvent |
|
TcSymbolSetEvent |
||
TcSymbolClearedEvent |
||
TcSymbolChangedEvent |
||
TcComEvent |
TcComConnectedEvent |
|
TcComDisconnectedEvent |
||
TcComSourceChangedEvent |
||
TcTypeRegistryEvent |
TcTypeRegistryCreatedEvent |
|
TcTypeRegistryDestroyedEvent |
||
TcSymbolRegistryEvent |
TcSymbolRegistryCreatedEvent |
|
TcSymbolRegistryDestroyedEvent |
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Attach listeners for kill event...
context.on('killed', () => console.log('context was killed'));
//TcContext manipulation code.....
//Explicit TcContext kill call
await context.kill();
//TcContext is no longer valid after this point, and
//any symbols produced by it should no longer be used...
})
/**
* Expected output:
*
* context was killed
*/
During normal operation, a TcContext
is valid from the moment it was created through TcContext.create()
call, upon until a call to TcContext.kill()
is made. When TcContext.kill()
completes its task, a killed
event of type TcContextKilledEvent
is emitted, which can be captured.
This holds true if no PLC-side code changes have been made from TcContext's
creation moment. If Activation of new configuration, or any Code Change takes place after TcContext.create()
was called, then that change in TcContext
will be detected. When detected, the TcContext
will invalidate all the created TcSymbols
, remove all subscriptions, clear Type and Symbol maps, kill itself, and then it will build a new map, based on the new symbols types and locations, which are present in the PLC.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL := TRUE;
END_VAR
index.js
const { TcContext } = require('tc-context')
TcContext.create().then(async context => {
//Attach listeners for kill and reinitialized events...
context.on('killed', () => console.log('context was killed'));
context.on('reinitialized', () => console.log('context was reinitialized'));
//Storing the symbol for use.
const booleanValue = context.$.MAIN.booleanValue;
/**
* Hypothetical scenario...
* Some code that uses the context cyclically for undefined period of time
* without yet reaching the .kill() call.
*
* During this hypothetical time - the PLC Configuration was changed
*/
//The line below is reached only after the PLC Configuration was changed
//in the above hypothetical scenario
await booleanValue.$set(true)
.catch(err => console.log(err));
/**
* Expected output:
*
* context was killed
* context was reinitialized
* TcBindingIsInvalidException :
* Attempting to read an Invalidated TcBinding[\<symbol name\>] ( \<symbol path\> )...
*/
})
As seen above, if after TcContext
was created, changes to code of the PLC have been made, the TcContext
will detect it, and perform the TcContext.kill()
operation automatically, upon completion of which, the killed
event of type TcContextKilledEvent
will be raised. Afterwards, the TcContext
will reconnect to the PLC, and a generation and build of new Type and Symbol Maps is executed. When completed the TcContext
will raise the reinitialized
event of type TcContextReinitializedEvent
. Any attempt to perform operations on any previously stored Invalidated TcSymbols
(such as booleanValue
in the example above), will result in an exception of type TcBindingIsInvalidException
These events can be used, as means of updating any dependencies, which utilize the created TcSymbols
by the TcContext
and thus ensuring stability.
NOTE: The reason for this approach, is because upon TcContext
creation, a scan is performed, which caches all the Types and Symbols, as well as their memory locations in the PLC. These memory locations are used as a means of reading, writing, clearing and subscribing to symbols, simple or complex. When an online change is performed, there are no guaranties that the location of the previously cached symbols is same, and even if an update to it can be made, the symbol itself could be of different type. All this would result in undefined behavior.
MAIN(PRG)
PROGRAM MAIN
VAR
booleanValue : BOOL := TRUE;
END_VAR
index.js
const { TcContext } = require('tc-context')
//Example of class which uses a symbol for its operation
class Foo {
//Bind the symbol at construction
constructor(symbol) { this.__symbol = symbol; }
//Means to update the binding of the symbol
bind(symbol) {
console.log('setting new symbol')
this.__symbol = symbol
}
//Method which uses the symbol
async printState() {
if (this.__symbol) {
const val = await this.__symbol.$get;
return (val) ? 'Symbol is on' : 'Symbol is off'
}
}
}
TcContext.create().then(async context => {
//Create the object which is dependent on the TcContext's symbol
const bar = new Foo(context.$.MAIN.booleanValue);
//When invalidated set the binding to null
context.$.MAIN.booleanValue.$onInvalidated(() => {
console.log('symbol was invalidated')
bar.bind(null);
})
context.on('reinitialized', () => {
console.log('context was reinitialized')
bar.bind(context.$.MAIN.booleanValue);
})
/**
* Hypothetical scenario...
* Some code that uses the context cyclically for undefined period of time
* without yet reaching the .kill() call.
*
* During this hypothetical time - the PLC Configuration was changed
*/
//The line below is reached only after the PLC Configuration was
//changed in the above hypothetical scenario
await bar.printState()
await context.kill()
})
/**
* Expected output:
*
* symbol was invalidated
* setting new symbol
* context was reinitialized
* setting new symbol
* Symbol is on
* symbol was invalidated
* setting new symbol
*/
The example above illustrates how any components, which depends on TcSymbol
, can use the emitted events to refresh its bindings, thus ensuring defined behavior.
Base | Component | Concrete | Description |
---|---|---|---|
TcException |
TcBindingException |
TcBindingIsInvalidException |
When operations are made on a TcSymbol of a killed TcContext |
TcBindingInvalidTypeException |
When input type does not match TcSymbol type |
||
TcBindingOutOfRangeException |
When input data length is outside the boundaries of TcSymbol |
||
TcBindingReadOnlyException |
When write commands are called on a Read-only TcSymbol |
||
TcComException |
TcComBusyException |
Connection to a PLC has already been made | |
TcComConnectException |
When an error occurred with establishing connection | ||
TcComIsInvalidException |
When operations are made on a TcCom of a killed TcContext |
||
TcComDisconnectException |
When an error occurred with disconnecting from Target | ||
TcComChangeDetectionException |
When an error occurred with establishing Code Change Monitoring | ||
TcComUnsubscribeException |
When an error occurred with unsubscribing from TwinCAT Symbol | ||
TcComFromRawException |
When an error occurred when transforming Data from Raw | ||
TcComToRawException |
When an error occurred when transforming Data to Raw | ||
TcComSubscribeException |
When an error occurred with subscribing to a TwinCAT Symbol | ||
TcComDataWriteException |
When an error occurred during data writing to TwinCAT | ||
TcComDataReadException |
When an error occurred during data reading from TwinCAT | ||
TcComTypeQueryException |
When an error occurred with querying Type Data from TwinCAT | ||
TcComSymbolQueryException |
When an error occurred with querying Symbol Data from TwinCAT | ||
TcComMethodCallException |
When an error occurred with invoking a RPC Method over ADS |
Detailed documentation of the TcContext
code base itself can be found under the /docs/ folder in this repository.
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.
Generated using TypeDoc