npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ads-client

v2.0.2

Published

Beckhoff TwinCAT ADS client library for Node.js (unofficial). Connect to a Beckhoff TwinCAT automation system using the ADS protocol from a Node.js app.

Downloads

4,818

Readme

ads-client

npm version GitHub License

Beckhoff TwinCAT ADS client library for Node.js (unofficial).

Connect to a Beckhoff TwinCAT automation system using the ADS protocol from a Node.js app.

If you are using Node-RED, check out the node-red-contrib-ads-client.

There is automatically created documentation available at https://jisotalo.fi/ads-client/

Project status

14.12.2024 - version 2 released!

See legacy-v1 branch for previous/legacy version 1.4.4.

Features

  • Supports TwinCAT 2 and 3
  • Supports connecting to the local TwinCAT 3 runtime
  • Supports any kind of target systems with ADS protocol (local runtime, PLC, EtherCAT I/O...)
  • Supports multiple connections from the same host
  • Reading and writing any kind of variables
  • Subscribing to variable value changes (ADS notifications)
  • Automatic conversion between PLC and Javascript objects
  • Calling function block methods (RPC)
  • Automatic 32/64 bit variable support (PVOID, XINT, etc.)
  • Automatic byte alignment support (all pack-modes automatically supported)

Table of contents

Support

If you want to support my work, you can buy me a coffee! Contact for more options.

Donate

If you need help with integrating the ads-client, I'm available for coding work with invoicing. Contact for further details.

Installing

Install the npm package:

npm install ads-client

Include the module in your code:

//Javascript:
const { Client } = require('ads-client');

//Typescript:
import { Client } from 'ads-client';

You can also clone the repository and run npm run build. After that, the library is available at ./dist/

Minimal example (TLDR)

This connects to a local PLC runtime, reads a value, writes a value, reads it again and then disconnects. The value is a string and it's located in GVL_Global.StringValue.

const { Client } = require('ads-client');

const client = new Client({
  targetAmsNetId: 'localhost',
  targetAdsPort: 851
});

client.connect()
  .then(async (res) => {   
    console.log(`Connected to the ${res.targetAmsNetId}`);
    console.log(`Router assigned us AmsNetId ${res.localAmsNetId} and port ${res.localAdsPort}`);

    try {
      //Reading a value
      const read = await client.readValue('GVL_Global.StringValue');
      console.log('Value read (before):', read.value); 

      //Writing a value
      await client.writeValue('GVL_Global.StringValue', 'This is a new value');

      //Reading a value
      const read2 = await client.readValue('GVL_Global.StringValue');
      console.log('Value read (after):', read2.value); 

    } catch (err) {
      console.log('Something failed:', err);
    }
    
    //Disconnecting
    await client.disconnect();
    console.log('Disconnected');

  }).catch(err => {
    console.log('Error:', err);
  });

Connection setup

The ads-client can be used with multiple system configurations.

ads-client-setups

Setup 1 - Connect from Windows

This is the most common scenario. The client is running on a Windows PC that has TwinCAT Router installed (such as development laptop, Beckhoff IPC/PC, Beckhoff PLC).

Requirements:

  • Client has one of the following installed
    • TwinCAT XAE (dev. environment)
    • TwinCAT XAR (runtime)
    • TwinCAT ADS
  • An ADS route is created between the client and the PLC using TwinCAT router

Client settings:

const client = new Client({
  targetAmsNetId: '192.168.1.120.1.1', //AmsNetId of the target PLC
  targetAdsPort: 851,
});

Setup 2 - Connect from Linux/Windows

In this scenario, the client is running on Linux or Windows without TwinCAT Router. The .NET based router can be run separately on the same machine.

Requirements:

Client settings:

const client = new Client({
  targetAmsNetId: '192.168.1.120.1.1', //AmsNetId of the target PLC
  targetAdsPort: 851,
});

Setup 3 - Connect from any Node.js system

In this scenario, the client is running on a machine that has no router running (no TwinCAT router and no 3rd party router). For example, Raspberry Pi without any additional installations.

In this setup, the client directly connects to the PLC and uses its TwinCAT router for communication. Only one simultaneous connection from the client is possible.

Requirements:

  • Target system (PLC) firewall has TCP port 48898 open
    • Windows Firewall might block, make sure Ethernet connection is handled as "private"
  • Local AmsNetId and ADS port are set manually
    • Used localAmsNetId is not already in use
    • Used localAdsPort is not already in use
  • An ADS route is configured to the PLC (see blow)

Setting up the route:

  1. At the PLC, open C:\TwinCAT\3.1\Target\StaticRoutes.xml
  2. Copy paste the following under <RemoteConnections>
<Route>
  <Name>UI</Name>
  <Address>192.168.1.10</Address>
  <NetId>192.168.1.10.1.1</NetId>
  <Type>TCP_IP</Type>
  <Flags>64</Flags>
</Route>
  1. Edit Address to IP address of the client (which runs the Node.js app), such as 192.168.1.10
  2. Edit NetId to any unused AmsNetId address, such as 192.168.1.10.1.1
  3. Restart PLC

Client settings:

const client = new Client({
  localAmsNetId: '192.168.1.10.1.1',  //Same as Address in PLC's StaticRoutes.xml (see above)
  localAdsPort: 32750,                //Can be anything that is not used
  targetAmsNetId: '192.168.1.120.1.1',//AmsNetId of the target PLC
  targetAdsPort: 851,
  routerAddress: '192.168.1.120',     //PLC IP address
  routerTcpPort: 48898                
});

See also:

Setup 4 - Connect from local system

In this scenario, the PLC is running Node.js app locally. For example, the development PC or Beckhoff PLC with a screen for HMI.

Requirements:

Client settings:

const client = new Client({
  targetAmsNetId: '127.0.0.1.1.1', //or 'localhost'
  targetAdsPort: 851,
});

Setup 5 - Docker container

It's also possible to run the client in a docker containers, also with a separate router (Linux systems).

I'm available for coding work if you need help with this. See Support

Important

Enabling localhost support on TwinCAT 3

If connecting to the local TwinCAT runtime (Node.js and PLC on the same machine), the ADS router TCP loopback feature has to be enabled.

TwinCAT 4024.5 and newer already have this enabled as default.

  1. Open registery edition (regedit)
  2. Navigate to
32 bit operating system:
  HKEY_LOCAL_MACHINE\SOFTWARE\Beckhoff\TwinCAT3\System\

64 bit it operating system:
  HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Beckhoff\TwinCAT3\System\
  1. Create new DWORD registery named EnableAmsTcpLoopback with value of 1
  2. Restart

ads-client-tcp-loopback

Now you can connect to the localhost using targetAmsNetId address of 127.0.0.1.1.1 or localhost.

Structured variables

When writing structured variables, the object properties are handled case-insensitively. This is because the TwinCAT is case-insensitive.

In practice, it means that the following objects are equal when passed to writevalue() or convertToRaw():

{
  sometext: 'hello',
  somereal: 3.14
}
{
  SOmeTEXT: 'hello',
  SOMEreal: 3.14
}

If there are multiple properties with the same name (in case-insensitive manner), the client selects probably the first one (the selection is done by Object.find()):

//In this case, probably the first one (sometext) is selected and the SOMEtext is skipped.
{
  sometext: 'hello',
  SOMEtext: 'good day'
}

Connecting to targets without a PLC runtime

Usually the ads-client is used to connect to a PLC runtime. Howevever, it's also possible to connect to any device supporting ADS protocol, such as

  • TwinCAT system service
  • I/O devices (EtherCAT, K-bus etc.)
  • ads-server instances

As default, the client assumes that there should be a PLC runtime. This causes errors with non-PLC systems, as different PLC related things are initialized.

Connection failed - failed to set PLC connection. If target is not PLC runtime, use setting "rawClient". If system is in config mode or there is no PLC software yet, you might want to use setting "allowHalfOpen".

By using the rawClient setting, the client allows raw connections to any ADS supported system.

const client = new Client({
  targetAmsNetId: '192.168.5.131.3.1', 
  targetAdsPort: 1002,
  rawClient: true // <-- NOTE
})

Note that when the rawclient setting is set, the client only connects to the target. All other background features, such as monitoring system state, PLC symbol version or connection issues, are not available.

Differences when using with TwinCAT 2

  • ADS port for first the PLC runtime is 801 instead of 851
const client = new ads.Client({
  targetAmsNetId: '...', 
  targetAdsPort: 801 //<-- NOTE
});
  • All variable and data type names are in UPPERCASE

This might cause problems if your app is used with both TC2 & TC3 systems.

image

  • Global variables are accessed with dot (.) prefix (without the GVL name)
await client.readSymbol('GVL_Test.ExampleSTRUCT') //TwinCAT 3
await client.readSymbol('.ExampleSTRUCT') //TwinCAT 2
  • ENUMs are always numeric values only

  • Empty structs and function blocks (without members) can't be read

Getting started

Documentation

The documentation is available at https://jisotalo.fi/ads-client and ./docs folder.

Examples in the getting started are based on a PLC project from https://github.com/jisotalo/ads-client-test-plc-project.

You can use the test PLC project as reference together with the ads-client.test.js to see and try out all available features.

Available methods

Click a method to open it's documentation.

| Method | Description | | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cacheDataTypes() | Caches all data types from the target PLC runtime. | | cacheSymbols() | Caches all symbols from the target PLC runtime. | | connect() | Connects to the target. | | convertFromRaw() | Converts raw data to a Javascript object by using the provided data type. | | convertToRaw() | Converts a Javascript object to raw data by using the provided data type. | | createVariableHandle() | Creates a handle to a variable at the target system by variable path (such as GVL_Test.ExampleStruct). | | createVariableHandleMulti() | Sends multiple createVariableHandle() commands in one ADS packet (ADS sum command). | | deleteVariableHandle() | Deletes a variable handle that was previously created using createVariableHandle(). | | deleteVariableHandleMulti() | Sends multiple deleteVariableHandle() commands in one ADS packet (ADS sum command). | | disconnect() | Disconnects from the target and closes active connection. | | getDataType() | Returns full data type declaration for requested data type (such as ST_Struct). | | getDataTypes() | Returns all target PLC runtime data types. | | getDefaultPlcObject() | Returns a default (empty) Javascript object representing provided PLC data type. | | getSymbol() | Returns a symbol object for given variable path (such as GVL_Test.ExampleStruct). | | getSymbols() | Returns all symbols from the target PLC runtime. | | invokeRpcMethod() | Invokes a function block RPC method on the target system. | | readDeviceInfo() | Reads target device information. | | readPlcRuntimeState() | Reads target PLC runtime state (Run, Stop etc.) | | readPlcSymbolVersion() | Reads target PLC runtime symbol version. | | readPlcUploadInfo() | Reads target PLC runtime upload information. | | readRaw() | Reads raw data from the target system by a raw ADS address (index group, index offset and data length). | | readRawByHandle() | Reads raw data from the target system by a previously created variable handle (acquired using createVariableHandle()). | | readRawByPath() | Reads raw data from the target system by variable path (such as GVL_Test.ExampleStruct). | | readRawBySymbol() | Reads raw data from the target system by a symbol object (acquired using getSymbol()). | | readRawMulti() | Sends multiple readRaw() commands in one ADS packet (ADS sum command). | | readState() | Reads target ADS state. | | readTcSystemState() | Reads target TwinCAT system state from ADS port 10000 (usually Run or Config). | | readValue() | Reads variable's value from the target system by a variable path (such as GVL_Test.ExampleStruct) and returns the value as a Javascript object. | | readValueBySymbol() | Reads variable's value from the target system by a symbol object (acquired using getSymbol()) and returns the value as a Javascript object. | | readWriteRaw() | Writes raw data to the target system by a raw ADS address (index group, index offset) and reads the result as raw data. | | readWriteRawMulti() | Sends multiple readWriteRaw() commands in one ADS packet (ADS sum command). | | reconnect() | Reconnects to the target (disconnects and then connects again). | | resetPlc() | Resets the target PLC runtime. Same as reset cold in TwinCAT XAE. | | restartPlc() | Restarts the PLC runtime. Same as calling resetPlc() and then startPlc(). | | restartTcSystem() | Restarts the target TwinCAT system. | | sendAdsCommand() | Sends a raw ADS command to the target. | | sendAdsCommandWithFallback() | Sends a raw ADS command to the target. If it fails to specific ADS error codes, sends the fallback ADS command. | | setDebugLevel() | Sets active debug level. | | setTcSystemToConfig() | Sets the target TwinCAT system to config mode. Same as Restart TwinCAT (Config mode) in TwinCAT XAE. | | setTcSystemToRun() | Sets the target TwinCAT system to run mode. Same as Restart TwinCAT system in TwinCAT XAE. | | startPlc() | Starts the target PLC runtime. Same as pressing the green play button in TwinCAT XAE. | | stopPlc() | Stops the target PLC runtime. Same as pressing the red stop button in TwinCAT XAE. | | subscribe() | Subscribes to value change notifications (ADS notifications) by variable path (such as GVL_Test.ExampleStruct) or raw ADS address (index group, index offset and data length). | | subscribeRaw() | Subscribes to raw value change notifications (ADS notifications) by a raw ADS address (index group, index offset and data length). | | subscribeValue() | Subscribes to value change notifications (ADS notifications) by a variable path, such as GVL_Test.ExampleStruct. | | unsubscribe() | Unsubscribes a subscription (deletes ADS notification). | | unsubscribeAll() | Unsubscribes all active subscription (deletes all ADS notifications). | | writeControl() | Sends an ADS WriteControl command to the target. | | writeRaw() | Writes raw data to the target system by a raw ADS address (index group, index offset and data length). | | writeRawByHandle() | Writes raw data to the target system by a previously created variable handle (acquired using createVariableHandle()). | | writeRawByPath() | Writes raw data to the target system by variable path (such as GVL_Test.ExampleStruct). | | writeRawBySymbol() | Writes raw data to the target system by a symbol object (acquired using getSymbol()). | | writeRawMulti() | Sends multiple writeRaw() commands in one ADS packet (ADS sum command). | | writeValue() | Writes variable's value to the target system by a variable path (such as GVL_Test.ExampleStruct). Converts the value from a Javascript object to a raw value. | | writeValueBySymbol() | Writes variable's value to the target system by a symbol object (acquired using getSymbol()). Converts the value from a Javascript object to a raw value. |

Creating a client

Settings are passed via the Client constructor. The following settings are mandatory:

See other settings from the AdsClientSettings documentation.

const client = new Client({
  targetAmsNetId: "localhost",
  targetAdsPort: 851
});

Connecting

It's a good practice to start a connection at startup and keep it open until the app is closed. If there are connection issues or the PLC software is updated, the client will handle everything automatically.

const { Client } = require('ads-client');

const client = new Client({
  targetAmsNetId: 'localhost',
  targetAdsPort: 851
});

client.connect()
  .then(async (res) => {   
    console.log(`Connected to the ${res.targetAmsNetId}`);
    console.log(`Router assigned us AmsNetId ${res.localAmsNetId} and port ${res.localAdsPort}`);
    //Connected

    //...

    //Disconnecting
    await client.disconnect();

  }).catch(err => {
    console.log('Error:', err);
  });

Reading values

Reading any value

Use readValue() to read any PLC value.

The only exception is the dereferenced value of a reference/pointer, see Reading reference/pointer.

Reading INT

const res = await client.readValue('GVL_Read.StandardTypes.INT_');
console.log(res.value); 
// 32767

Reading STRING

const res = await client.readValue('GVL_Read.StandardTypes.STRING_');
console.log(res.value); 
// A test string ääöö!!@@

Reading DT

const res = await client.readValue('GVL_Read.StandardTypes.DT_');
console.log(res.value); 
// 2106-02-06T06:28:15.000Z (Date object)

Reading STRUCT

const res = await client.readValue('GVL_Read.ComplexTypes.STRUCT_');
console.log(res.value); 
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
}
*/

Reading FUNCTION_BLOCK

const res = await client.readValue('GVL_Read.ComplexTypes.BLOCK_2'); //TON
console.log(res.value); 
// { IN: false, PT: 2500, Q: false, ET: 0, M: false, StartTime: 0 }

Reading ARRAY

const res = await client.readValue('GVL_Read.StandardArrays.REAL_3');
console.log(res.value); 
// [ 75483.546875, 0, -75483.546875 ]

Reading ENUM

const res = await client.readValue('GVL_Read.ComplexTypes.ENUM_');
console.log(res.value); 
// { name: 'Running', value: 100 }

Typescript example: Reading INT

const res = await client.readValue<number>('GVL_Read.StandardTypes.INT_');
console.log(res.value); //res.value is typed as number
// 32767

Typescript example: Reading STRUCT

interface ST_ComplexTypes {
  BOOL_: boolean,
  BOOL_2: boolean,
  BYTE_: number,
  WORD_: number,
  //..and so on
}

const res = await client.readValue<ST_ComplexTypes>('GVL_Read.ComplexTypes.STRUCT_');
console.log(res.value); //res.value is typed as ST_ComplexTypes
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Reading raw data

Use readRaw() or readRawByPath() to read any PLC value as raw data. The raw data in this context means received bytes (a Buffer object).

The only exception is the dereferenced value of a reference/pointer, see Reading reference/pointer.

For converting the data between raw data and Javascript object, see Converting data between raw data and Javascript objects.

readRaw()

The indexGroup and indexOffset can be acquired by using getSymbol().

//Read 2 bytes from indexGroup 16448 and indexOffset 414816
const data = await client.readRaw(16448, 414816, 2);
console.log(data); //<Buffer ff 7f>

readRawByPath()

const data = await client.readRawByPath('GVL_Read.StandardTypes.INT_');
console.log(data); //<Buffer ff 7f>

Reading reference/pointer

The dereferenced value of a reference (REFERENCE TO) or a pointer (POINTER TO) can be read with readRawByPath() or by using variable handles.

Reading POINTER (readRawByPath())

//Reading a raw POINTER value (Note the dereference operator ^)
const value = await client.readRawByPath('GVL_Read.ComplexTypes.POINTER_^');

//Converting to a Javascript object
const value = await client.convertFromRaw(rawValue, 'ST_StandardTypes');
console.log(value);
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Reading REFERENCE (readRawByPath())

//Reading a raw REFERENCE value
const rawValue = await client.readRawByPath('GVL_Read.ComplexTypes.REFERENCE_');

//Converting to a Javascript object
const value = await client.convertFromRaw(rawValue, 'ST_StandardTypes');
console.log(value);
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Reading POINTER (variable handle)

//Reading a POINTER value (Note the dereference operator ^)
const handle = await client.createVariableHandle('GVL_Read.ComplexTypes.POINTER_^');
const rawValue = await client.readRawByHandle(handle);
await client.deleteVariableHandle(handle);

//Converting to a Javascript object
const value = await client.convertFromRaw(rawValue, 'ST_StandardTypes');
console.log(value);
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Reading REFERENCE (variable handle)

//Reading a REFERENCE value
const handle = await client.createVariableHandle('GVL_Read.ComplexTypes.REFERENCE_');
const rawValue = await client.readRawByHandle(handle);
await client.deleteVariableHandle(handle);

//Converting to a Javascript object
const value = await client.convertFromRaw(rawValue, 'ST_StandardTypes');
console.log(value);
/* 
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Writing values

Writing any value

Use writeValue() to write any PLC value.

The only exception is the dereferenced value of a reference/pointer, see Writing reference/pointer.

Writing INT

const res = await client.writeValue('GVL_Write.StandardTypes.INT_', 32767);
console.log(res.value); 
// 32767

Writing STRING

await client.writeValue('GVL_Write.StandardTypes.STRING_', 'This is a test');

Writing DT

await client.writeValue('GVL_Write.StandardTypes.DT_', new Date());

Writing STRUCT (all properties)

await client.writeValue('GVL_Write.ComplexTypes.STRUCT_', {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
});

Writing STRUCT (some properties only)

All other properties will keep their values. The client reads the active value first and then makes changes.

await client.writeValue('GVL_Write.ComplexTypes.STRUCT_', {
  WORD_: 65535
}, true); //<-- NOTE: autoFill set

Writing FUNCTION_BLOCK (all properties)

const timerBlock = {
  IN: false, 
  PT: 2500, 
  Q: false, 
  ET: 0, 
  M: false, 
  StartTime: 0
};

await client.writeValue('GVL_Write.ComplexTypes.BLOCK_2', timerBlock);

Writing FUNCTION_BLOCK (some properties only)

All other properties will keep their values. The client reads the active value first and then makes changes.

await client.writeValue('GVL_Write.ComplexTypes.BLOCK_2', {
  IN: true
}, true); //<-- NOTE: autoFill set

Writing ARRAY

const data = [
  75483.546875, 
  0, 
  -75483.546875
];

await client.writeValue('GVL_Write.StandardArrays.REAL_3', data);

Writing ENUM

await client.writeValue('GVL_Write.ComplexTypes.ENUM_', 'Running');
//...or...
await client.writeValue('GVL_Write.ComplexTypes.ENUM_', 100);

Writing raw data

Use writeRaw() or writeRawByPath() to write any PLC value using raw data. The raw data in this context means bytes (a Buffer object).

The only exception is the dereferenced value of a reference/pointer, see Reading reference/pointer.

For converting the data between raw data and Javascript object, see Converting data between raw data and Javascript objects.

writeRaw()

//Creating raw data of an INT
const data = await client.convertToRaw(32767, 'INT');
console.log(data); //<Buffer ff 7f>

//Writing the value to indexGroup 16448 and indexOffset 414816
await client.writeRaw(16448, 414816, data);

writeRawByPath()

//Creating raw data of an INT
const data = await client.convertToRaw(32767, 'INT');
console.log(data); //<Buffer ff 7f>

await client.writeRawByPath('GVL_Write.StandardTypes.INT_', data);

Writing reference/pointer

The dereferenced value of a reference (REFERENCE TO) or a pointer (POINTER TO) can be written with writeRawByPath() or by using variable handles.

Writing POINTER (writeRawByPath())

const value = {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
};
const rawValue = await client.convertToRaw(value, 'ST_StandardTypes');

//Writing a raw POINTER value (Note the dereference operator ^)
await client.writeRawByPath('GVL_Write.ComplexTypes.POINTER_^', rawValue);

Writing REFERENCE (writeRawByPath())

const value = {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
};
const rawValue = await client.convertToRaw(value, 'ST_StandardTypes');

//Writing a raw REFERENCE value
await client.writeRawByPath('GVL_Write.ComplexTypes.REFERENCE_', rawValue);

Writing POINTER (variable handle)

const value = {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
};
const rawValue = await client.convertToRaw(value, 'ST_StandardTypes');

//Writing a raw POINTER value (Note the dereference operator ^)
const handle = await client.createVariableHandle('GVL_Write.ComplexTypes.POINTER_^');
await client.writeRawByHandle(handle, rawValue);
await client.deleteVariableHandle(handle);

Writing REFERENCE (variable handle)

const value = {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
};
const rawValue = await client.convertToRaw(value, 'ST_StandardTypes');

//Writing a raw REFERENCE value
const handle = await client.createVariableHandle('GVL_Write.ComplexTypes.POINTER_');
await client.writeRawByHandle(handle, rawValue);
await client.deleteVariableHandle(handle);

Subscribing to value changes

In ads-client, subscriptions are used to handle ADS notifications. ADS notifications are data sent by PLC automatically without request. For example, the latest value of a variable every second.

By subscribing to a variable value changes, the target system (PLC) will send ADS notifications when the value changes (or every x milliseconds). The client then receives these notifications and calls the user callback function with the latest value.

More information about ADS notifications at Beckhoff Infosys: Use of ADS Notifications.

Any value

Use subscribeValue() or subscribe() to subscribe to PLC variable value changes.

Example

Subscribing to changes of GVL_Subscription.NumericValue_10ms. The callback is called when the PLC value changes (at maximum every 100 milliseconds).

const onValueChanged = (data, subscription) => {
  console.log(`Value of ${subscription.symbol.name} has changed: ${data.value}`);
}

const subscription = await client.subscribeValue(
  'GVL_Subscription.NumericValue_10ms',
  onValueChanged,
  100 
);

Example

Subscribing to value of GVL_Subscription.NumericValue_1000ms. The callback is called with the latest value every 100 milliseconds (doesn't matter if the value has changed or not).

const onValueReceived = (data, subscription) => {
  console.log(`Value of ${subscription.symbol.name} is: ${data.value}`);
}

const subscription = await client.subscribeValue(
  'GVL_Subscription.NumericValue_1000ms',
  onValueReceived,
  100,
  false 
);

Typescript example

Same as previous example, but with subscribe() instead. A type can be provided for subscribeValue<T>() as well.

const subscription = await client.subscribe<number>({
  target: 'GVL_Subscription.NumericValue_1000ms',
  callback: (data, subscription) => {
    //data.value is typed as "number" instead of "any"
    console.log(`Value of ${subscription.symbol.name} is: ${data.value}`);
  },
  cycleTime: 100,
  sendOnChange: false
});

Raw data

Use subscribeRaw() or subscribe() to subscribe to raw value changes.

The indexGroup and indexOffset can be acquired by using getSymbol().

Example

Subscribing to raw address of indexGroup = 16448 and indexOffset = 414816 (2 bytes).

await client.subscribeRaw(16448, 414816, 2, (data, subscription) => {
  console.log(`Value has changed: ${data.value.toString('hex')}`);
}, 100);

Example

Same as previous example, but with subscribe() instead.

const onValueChanged = (data, subscription) => {
  console.log(`Value has changed: ${data.value.toString('hex')}`);
}

await client.subscribe({
  target: {
    indexGroup: 16448,
    indexOffset: 414816,
    size: 2
  },
  callback: onValueChanged,
  cycleTime: 100
});

Unsubscribing

Subscriptions should always be cancelled when no longer needed (to save PLC resources).

To unsubscribe, use unsubscribe() or subscription object's ActiveSubscription.unsubscribe().

const subscription = await client.subscribeValue(...);

//Later when no longer needed
await subscription.unsubscribe();

//Or alternatively
await client.unsubscribe(subscription);

Using variable handles

Using variable handles is an another way to read and write raw data.

First, a handle is created to a specific PLC variable by the variable path. After that, read and write operations are available.

Handles should always be deleted after no longer needed, as the PLC has limited number of handles.

Handles can also be used to read/write reference and pointer values, see Reading reference/pointer.

Reading a value using a variable handle

//Creating a handle
const handle = await client.createVariableHandle('GVL_Read.StandardTypes.INT_');

//Reading a value
const data = await client.readRawByHandle(handle);

//Deleting the handle
await client.deleteVariableHandle(handle);

//Converting to a Javascript value
const converted = await client.convertFromRaw(data, 'INT');
console.log(data); //<Buffer ff 7f>
console.log(converted); //32767

Writing a value using a variable handle

//Creating a raw value
const data = await client.convertToRaw(32767, 'INT');
console.log(data); //<Buffer ff 7f>

//Creating a handle
const handle = await client.createVariableHandle('GVL_Write.StandardTypes.INT_');

//Writing the value
await client.writeRawByHandle(handle, data);

//Deleting the handle
await client.deleteVariableHandle(handle);

Calling function block RPC methods

If a function block method has pragma {attribute 'TcRpcEnable'}, the method can be called from ads-client.

Read more at Beckhoff Infosys: Attribute 'TcRpcEnable'.

Things to note when using RPC Methods

These are my own observations.

  • Do not use online change if you change RPC method parameters or return data types
  • Make sure that parameters and return value have no pack-mode pragmas defined, otherwise data might be corrupted
  • Do not use ARRAY values directly in parameters or return value, encapsulate arrays inside struct and use the struct instead
  • The feature isn't well documented by Beckhoff, so there might be some things that aren't taken into account

About examples

These examples use FB_RPC from the test PLC project at https://github.com/jisotalo/ads-client-test-plc-project.

There is an instance of the function block at GVL_RPC.RpcBlock.

RPC method with standard data types

conn The Calculator() method calculates sum, product and division of the input values. The method returns true, if all calculations were successful.

{attribute 'TcRpcEnable'}
METHOD Calculator : BOOL
VAR_INPUT
	Value1	: REAL;
	Value2	: REAL;
END_VAR
VAR_OUTPUT
	Sum			: REAL;
	Product 	: REAL;
	Division	: REAL;
END_VAR

//--- Code starts ---

//Return TRUE if all success
Calculator := TRUE;

Sum := Value1 + Value2;
Product := Value1 * Value2;

IF Value2 <> 0 THEN
	Division := Value1 / Value2;
ELSE
	Division := 0;
	Calculator := FALSE;
END_IF

Example call:

const res = await client.invokeRpcMethod('GVL_RPC.RpcBlock', 'Calculator', {
  Value1: 1,
  Value2: 123
});

console.log(res);
/*
{
  returnValue: true,
  outputs: { 
    Sum: 124, 
    Product: 123, 
    Division: 0.008130080997943878 
  }
}
*/

RPC method with struct

The Structs() method takes a struct value as input, changes its values and then returns the result.

{attribute 'TcRpcEnable'}
METHOD Structs : ST_Struct
VAR_INPUT
	Input	: ST_Struct;
END_VAR

//--- Code starts ---

Structs.SomeText := CONCAT('Response: ', Input.SomeText);
Structs.SomeReal := Input.SomeReal * 10.0;
Structs.SomeDate := Input.SomeDate + T#24H;

Example call:

const res = await client.invokeRpcMethod('GVL_RPC.RpcBlock', 'Structs', {
  Input: {
    SomeText: 'Hello ads-client',
    SomeReal: 3.14,
    SomeDate: new Date('2024-12-24T00:00:00.000Z') 
  }
});

console.log(res);
/*
{
  returnValue: {
    SomeText: 'Response: Hello ads-client',
    SomeReal: 31.4,
    SomeDate: 2024-12-24T01:00:00.000Z
  },
  outputs: {}
}
*/

Converting data between raw data and Javascript objects

The raw data in this context means sent or received bytes (a Buffer object).

When using methods such as readValue(), writeValue() and subscribeValue(), the client converts data automatically. The conversion can be done manually as well, by using convertFromRaw() and convertToRaw().

See my other library iec-61131-3 for other possibilities to convert data between Javascript and IEC 61131-3 types.

Converting a raw value to a Javascript object

Use convertFromRaw() to convert raw data to Javascript object.

Converting INT

const data = await client.readRaw(16448, 414816, 2);
console.log(data); //<Buffer ff 7f>

const converted = await client.convertFromRaw(data, 'INT');
console.log(converted); //32767

Converting STRUCT

const converted = await client.convertFromRaw(data, 'ST_StandardTypes');
console.log(converted);
/*
{
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //..and so on
}
*/

Converting a Javascript object to a raw value

Use convertToRaw() to convert Javascript object to raw data.

Converting INT

const data = await client.convertToRaw(12345, 'INT');
console.log(data); //<Buffer 39 30>

Converting STRUCT

const value = {
  BOOL_: true,
  BOOL_2: false,
  BYTE_: 255,
  WORD_: 65535,
  //...and so on
};

const data = await client.convertToRaw(value, 'ST_StandardTypes');
console.log(data); //<Buffer ...>

Converting STRUCT (some properties only)

All other (missing) properties are set to default values (zero / empty string).

const value = {
  WORD_: 65535
};

const data = await client.convertToRaw(value, 'ST_StandardTypes', true); //<-- NOTE: autoFill set
console.log(data); //<Buffer ...>

Other features

ADS sum commands

ADS sum commands can be used to have multiple ADS commands in one request. This can be useful for efficiency reasons.

See Beckhoff Information System for more info.

Starting and stopping a PLC

Starting and stopping TwinCAT system

Client events

See AdsClientEvents for all available events, their descriptions and examples.

client.on('connect', (connection) => {
  console.log('Connected:', connection);
});

Debugging

Use setDebugLevel() to change debug level.

client.setDebugLevel(1);
  • 0: no debugging (default)
  • 1: basic debugging ($env:DEBUG='ads-client')
  • 2: detailed debugging ($env:DEBUG='ads-client,ads-client:details')
  • 3: detailed debugging with raw I/O data ($env:DEBUG='ads-client,ads-client:details,ads-client:raw-data')

Debug data is available in the console (See Debug library for more).

Disconnecting

After the client is no more used, always use disconnect() to release all subscription handles and other resources.

await client.disconnect();

Common issues and questions

There are lot's of connection issues and timeouts

Things to try:

  • Remove all TwinCAT routes and create them again (yes, really)
  • Increase value of timeoutDelay setting
  • Cache all data types and symbols straight after connecting using cacheDataTypes and cacheSymbols

Getting TypeError: Do not know how to serialize a BigInt

  • JSON.stringify doesn't understand BigInt values (such as LINT or similar 64 bit PLC values)
  • Check this Github issue for the following patch:
BigInt.prototype.toJSON = function() { return this.toString() }

Can I connect from Raspberry Pi to TwinCAT?

Yes, for example using Setup 3 - Connect from any Node.js system.

  1. Open a TCP port 48898 from your PLC
  2. Edit StaticRoutes.xml file from your PLC
  3. Connect from the Raspberry Pi using the PLC IP address as routerAddress and the AmsNetID written to StaticRoutes.xml as localAmsNetId

Receiving ADS error 1808 Symbol not found even when it should be found

  • Make sure you have updated the latest PLC software using download. Sometimes online change causes this.
  • If you are using TwinCAT 2, see chapter Differences when using with TwinCAT 2
  • Double check variable path for typos

Having timeouts or 'mailbox is full' errors

  • The AMS router is capable of handling only limited number of requests in a certain time.
  • Other possible reason is that operating system TCP window is full because of large number of requests.
  • Solution:
    • Use structs or arrays to send data in larger packets
    • Try raw commands or sum commands to decrease data usage

Having problems to connect from OSX or Raspberry Pi to target PLC

  • The local machine has no AMS router
  • You need to connect to the PLC's AMS router instead
  • See this issue comment

A data type is not found even when it should be

If you use methods like convertFromRaw() and getDataType() but receive an error similar to ClientException: Finding data type *data type* failed, make sure you have really written the data type correctly.

For example, when copying a variable name from TwinCAT online view using CTRL+C, it might not work:

  • Displayed name: ARRAY [0..1, 0..1] OF ST_Example
  • The value copied to clipboard ARRAY [0..1, 0..1] OF ST_Example
  • --> This causes error!
  • The real data type name that needs to be used is ARRAY [0..1,0..1] OF ST_Example (note no whitespace between array dimensions)

If you have problems, try to read the symbol object using getSymbol(). The final solution is to read all data types using getDataTypes() and manually locate the correct type.

Connection failed - failed to set PLC connection

  • The targetAmsNetId didn't contain a system manager service (port 10000)
  • The target is not a PLC and rawClient setting is not set
  • Solution:

Connection failed (error EADDRNOTAVAIL)

This could happen if you have manually provided localAddress or localTcpPort that don't exist. For example, setting localAddress to 192.168.10.1 when the computer has only ethernet interface with IP 192.168.1.1.

See also https://github.com/jisotalo/ads-client/issues/82

Problems running ads-client with docker

  • EADDRNOTAVAIL: See above and https://github.com/jisotalo/ads-client/issues/82

How to connect to a PLC that is in CONFIG mode?

As default, the ads-client checks if the target has PLC runtime at given port. However, when target system manager is at config mode, there is none. The client will throw an error during connecting:

Connection failed - failed to set PLC connection. If target is not PLC runtime, use setting "rawClient". If system is in config mode or there is no PLC software yet, you might want to use setting "allowHalfOpen"

You can disable the check by providing setting allowHalfOpen: true. After that, it's possible to start the PLC by setTcSystemToRun().

Another option is to use setting rawClient: true - see Connecting to targets without a PLC runtime.

Issues with TwinCAT 2 low-end devices (BK9050, BC9050 etc.)

  • You can only use raw commands (such as readRaw(), writeRaw(), subscribeRaw()) as these devices provide no symbols
  • See issue 114 and issue 116

External links

| Description | Link | | -------------------------------------------- | ------------------------------------------------------- | | ADS client for Node-RED | https://github.com/jisotalo/node-red-contrib-ads-client | | ADS server for Node.js | https://github.com/jisotalo/ads-server | | IEC 61131-3 PLC data type helper for Node.js | https://github.com/jisotalo/iec-61131-3/ | | Codesys client for Node.js | https://github.com/jisotalo/codesys-client/ |

Library testing

All features of this library are tested using quite large test suite - see ./test/ directory. This prevents regression, thus updating the ads-client should be always safe.

There are separate tests for TwinCAT 2 and TwinCAT 3.

PLC projects for running test suites