ads-client 2.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.2.0] - 27.12.2025
8
+ ### Added
9
+ - Added caching of built data types to improve performance
10
+ - See [pull request #178](https://github.com/jisotalo/ads-client/pull/178)
11
+
12
+ ### Changed
13
+ - Bug fix: Unhandled Promise rejections during automatic reconnecting
14
+ - See [pull request #177](https://github.com/jisotalo/ads-client/pull/177)
15
+ - Fixed a Typescript warning at `socket.on("data", (data) => {...})` as the data chunk could be a string as well
16
+ - Dependencies updated
17
+ - Documentation updated
18
+
19
+ Thank you [Christian Rishøj](https://github.com/crishoj) for contribution!
20
+
21
+ All tests passing.
22
+
23
+ ## [2.1.0] - 27.01.2025
24
+ ### Added
25
+ - New method `readTcSystemExtendedState()`
26
+ - Reads [extended target TwinCAT system state](https://jisotalo.fi/ads-client/interfaces/AdsTcSystemExtendedState.html) (if available)
27
+ - Improved detection of TwinCAT system service restart
28
+ - Available if remote system supports `readTcSystemExtendedState()`
29
+ - Tested with TwinCAT 3.1.4022, 3.1.4024 and 3.1.4026
30
+ - Client detects a restart of TwinCAT system and reconnects to re-establish subscriptions
31
+ - See issue [issue #159](https://github.com/jisotalo/ads-client/issues/159)
32
+ - Added tests for subscription persistence during TwinCAT system restart
33
+
34
+ ### Changed
35
+ - Changed `activeSubscriptions` property visibility from private to public
36
+ - `disconnect` event is no longer emitted continuously each reconnect attempt (only during the initial disconnect / connection lost event)
37
+ - `disconnect` event parameter `isReconnecting` changed to `connectionLost`
38
+ - TwinCAT system state type (`metaData.tcSystemState`) changed from `AdsState` to `AdsTcSystemState`
39
+ - Not a breaking change - the new type extends `AdsState`
40
+
7
41
  ## [2.0.2] - 14.12.2024
8
42
  **IMPORTANT:** This is a major version update. There are lots of **breaking changes**!
9
43
 
package/README.md CHANGED
@@ -34,6 +34,7 @@ See [`legacy-v1` branch](https://github.com/jisotalo/ads-client/tree/legacy-v1)
34
34
  - Calling function block methods (RPC)
35
35
  - Automatic 32/64 bit variable support (PVOID, XINT, etc.)
36
36
  - Automatic byte alignment support (all pack-modes automatically supported)
37
+ - Handles TwinCAT restarts, configuration changes and PLC software updates automatically
37
38
 
38
39
  # Table of contents
39
40
  - [ads-client](#ads-client)
@@ -457,6 +458,7 @@ Click a method to open it's documentation.
457
458
  | [`readRawMulti()`](https://jisotalo.fi/ads-client/classes/Client.html#readRawMulti) | Sends multiple `readRaw()` commands in one ADS packet (ADS sum command). |
458
459
  | [`readState()`](https://jisotalo.fi/ads-client/classes/Client.html#readState) | Reads target ADS state. |
459
460
  | [`readTcSystemState()`](https://jisotalo.fi/ads-client/classes/Client.html#readTcSystemState) | Reads target TwinCAT system state from ADS port 10000 (usually `Run` or `Config`). |
461
+ | [`readTcSystemExtendedState()`](https://jisotalo.fi/ads-client/classes/Client.html#readTcSystemExtendedState) | Reads extended target TwinCAT system service state from ADS port 10000 if supported by target system. Tested to work with 3.1.4022 and newer. |
460
462
  | [`readValue()`](https://jisotalo.fi/ads-client/classes/Client.html#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. |
461
463
  | [`readValueBySymbol()`](https://jisotalo.fi/ads-client/classes/Client.html#readValueBySymbol) | Reads variable's value from the target system by a symbol object (acquired using `getSymbol()`) and returns the value as a Javascript object. |
462
464
  | [`readWriteRaw()`](https://jisotalo.fi/ads-client/classes/Client.html#readWriteRaw) | Writes raw data to the target system by a raw ADS address (index group, index offset) and reads the result as raw data. |
@@ -1,7 +1,7 @@
1
1
  import EventEmitter from "events";
2
2
  import * as ADS from './ads-commons';
3
- import type { ActiveSubscription, AdsClientConnection, AdsClientSettings, AdsCommandToSend, AdsDataTypeContainer, AdsSymbolContainer, ConnectionMetaData, SubscriptionSettings, ReadValueResult, WriteValueResult, VariableHandle, RpcMethodCallResult, CreateVariableHandleMultiResult, ReadRawMultiResult, ReadRawMultiCommand, WriteRawMultiResult, DeleteVariableHandleMultiResult, ReadWriteRawMultiResult, ReadWriteRawMultiCommand, WriteRawMultiCommand, SubscriptionCallback, DebugLevel, AdsClientEvents, SendAdsCommandWithFallbackResult } from "./types/ads-client-types";
4
- import { AdsAttributeEntry, AdsDataType, AdsDeviceInfo, AdsResponse, AdsState, AdsSymbol, AmsAddress, AmsTcpPacket, AdsUploadInfo } from "./types/ads-protocol-types";
3
+ import type { ActiveSubscription, ActiveSubscriptionContainer, AdsClientConnection, AdsClientSettings, AdsCommandToSend, AdsDataTypeContainer, AdsSymbolContainer, ConnectionMetaData, SubscriptionSettings, ReadValueResult, WriteValueResult, VariableHandle, RpcMethodCallResult, CreateVariableHandleMultiResult, ReadRawMultiResult, ReadRawMultiCommand, WriteRawMultiResult, DeleteVariableHandleMultiResult, ReadWriteRawMultiResult, ReadWriteRawMultiCommand, WriteRawMultiCommand, SubscriptionCallback, DebugLevel, AdsClientEvents, SendAdsCommandWithFallbackResult } from "./types/ads-client-types";
4
+ import { AdsAttributeEntry, AdsDataType, AdsDeviceInfo, AdsResponse, AdsState, AdsSymbol, AmsAddress, AmsTcpPacket, AdsUploadInfo, AdsTcSystemExtendedState } from "./types/ads-protocol-types";
5
5
  export type * from "./types/ads-client-types";
6
6
  export type * from './types/ads-protocol-types';
7
7
  export type * from './client-error';
@@ -94,10 +94,6 @@ export declare class Client extends EventEmitter<AdsClientEvents> {
94
94
  * Timer handle of the timer used for detecting ADS port registeration timeout.
95
95
  */
96
96
  private portRegisterTimeoutTimer?;
97
- /**
98
- * Container for all active subscriptions.
99
- */
100
- private activeSubscriptions;
101
97
  /**
102
98
  * Container for previous subscriptions that were active
103
99
  * before reconnecting or when PLC runtime symbol version changed.
@@ -164,6 +160,12 @@ export declare class Client extends EventEmitter<AdsClientEvents> {
164
160
  * Some properties might not be available in all connection setups and setting combinations.
165
161
  */
166
162
  metaData: ConnectionMetaData;
163
+ /**
164
+ * Container for all active subscriptions.
165
+ *
166
+ * Do not edit this directly.
167
+ */
168
+ activeSubscriptions: ActiveSubscriptionContainer;
167
169
  /**
168
170
  * Creates a new ADS client instance.
169
171
  *
@@ -976,6 +978,8 @@ export declare class Client extends EventEmitter<AdsClientEvents> {
976
978
  /**
977
979
  * Reads target TwinCAT system state from ADS port 10000 (usually `Run` or `Config`).
978
980
  *
981
+ * NOTE: You might want to use the extended {@link readTcSystemExtendedState}() instead.
982
+ *
979
983
  * If `targetOpts` is not used to override target, the state is also
980
984
  * saved to the `metaData.tcSystemState`.
981
985
  *
@@ -996,6 +1000,30 @@ export declare class Client extends EventEmitter<AdsClientEvents> {
996
1000
  * @throws Throws an error if sending the command fails or if the target responds with an error.
997
1001
  */
998
1002
  readTcSystemState(targetOpts?: Partial<AmsAddress>): Promise<AdsState>;
1003
+ /**
1004
+ * Reads extended target TwinCAT system service state from ADS port 10000
1005
+ * if supported by target system. Extended version of the {@link readTcSystemState}().
1006
+ *
1007
+ * If `targetOpts` is not used to override target, the state is also
1008
+ * saved to the `metaData.tcSystemState`.
1009
+ *
1010
+ * NOTE: Might not be supported in older TwinCAT versions. If so, use {@link readTcSystemState}() instead.
1011
+ * Tested to work with 3.1.4022 and newer.
1012
+ *
1013
+ * @example
1014
+ * ```js
1015
+ * try {
1016
+ * const tcSystemState = await client.readTcSystemServiceState();
1017
+ * } catch (err) {
1018
+ * console.log("Error:", err);
1019
+ * }
1020
+ * ```
1021
+ *
1022
+ * @param targetOpts Optional target settings that override values in `settings`
1023
+ *
1024
+ * @throws Throws an error if sending the command fails or if the target responds with an error.
1025
+ */
1026
+ readTcSystemExtendedState(targetOpts?: Partial<AmsAddress>): Promise<AdsTcSystemExtendedState>;
999
1027
  /**
1000
1028
  * Reads target PLC runtime symbol version.
1001
1029
  *
@@ -158,6 +158,7 @@ class Client extends events_1.default {
158
158
  plcSymbols: {},
159
159
  allPlcDataTypesCached: false,
160
160
  plcDataTypes: {},
161
+ builtDataTypes: {},
161
162
  adsSymbolsUseUtf8: false
162
163
  };
163
164
  /**
@@ -182,10 +183,6 @@ class Client extends events_1.default {
182
183
  * Timer handle of the timer used for detecting ADS port registeration timeout.
183
184
  */
184
185
  this.portRegisterTimeoutTimer = undefined;
185
- /**
186
- * Container for all active subscriptions.
187
- */
188
- this.activeSubscriptions = {};
189
186
  /**
190
187
  * Container for previous subscriptions that were active
191
188
  * before reconnecting or when PLC runtime symbol version changed.
@@ -263,6 +260,12 @@ class Client extends events_1.default {
263
260
  * Some properties might not be available in all connection setups and setting combinations.
264
261
  */
265
262
  this.metaData = { ...this.defaultMetaData };
263
+ /**
264
+ * Container for all active subscriptions.
265
+ *
266
+ * Do not edit this directly.
267
+ */
268
+ this.activeSubscriptions = {};
266
269
  //Taking the default settings and then updating the provided ones
267
270
  this.settings = {
268
271
  ...this.settings,
@@ -463,15 +466,21 @@ class Client extends events_1.default {
463
466
  resolve(this.connection);
464
467
  });
465
468
  //Listening data event
466
- socket.on("data", data => {
467
- if (this.debugIO.enabled) {
468
- this.debugIO(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}: ${data.toString("hex")}`);
469
+ socket.on("data", (data) => {
470
+ if (Buffer.isBuffer(data)) {
471
+ if (this.debugIO.enabled) {
472
+ this.debugIO(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}: ${data.toString("hex")}`);
473
+ }
474
+ else if (this.debugD.enabled) {
475
+ this.debugD(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}`);
476
+ }
477
+ this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
478
+ this.handleReceivedData();
469
479
  }
470
- else if (this.debugD.enabled) {
471
- this.debugD(`IO in <------ ${data.byteLength} bytes from ${socket.remoteAddress}`);
480
+ else {
481
+ //This should never happen, as the Socket defaults to Buffer
482
+ this.debug(`Socket callback data type is unknown (${typeof data})`);
472
483
  }
473
- this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
474
- this.handleReceivedData();
475
484
  });
476
485
  //Timeout only during connecting, other timeouts are handled elsewhere
477
486
  socket.setTimeout(this.settings.timeoutDelay);
@@ -528,7 +537,9 @@ class Client extends events_1.default {
528
537
  this.socket?.removeAllListeners();
529
538
  this.socket?.destroy();
530
539
  this.socket = undefined;
531
- this.emit("disconnect", isReconnecting);
540
+ if (!isReconnecting) {
541
+ this.emit("disconnect", isReconnecting);
542
+ }
532
543
  return resolve();
533
544
  }
534
545
  let disconnectError = null;
@@ -550,7 +561,9 @@ class Client extends events_1.default {
550
561
  this.socket?.destroy();
551
562
  this.socket = undefined;
552
563
  this.debug(`disconnectFromTarget(): Connection closed successfully`);
553
- this.emit("disconnect", isReconnecting);
564
+ if (!isReconnecting) {
565
+ this.emit("disconnect", isReconnecting);
566
+ }
554
567
  return resolve();
555
568
  }
556
569
  catch (err) {
@@ -565,7 +578,9 @@ class Client extends events_1.default {
565
578
  this.metaData = { ...this.defaultMetaData };
566
579
  this.activeSubscriptions = {};
567
580
  this.debug(`disconnectFromTarget(): Connection closing failed, connection was forced to close`);
568
- this.emit("disconnect", isReconnecting);
581
+ if (isReconnecting) {
582
+ this.emit("disconnect", isReconnecting);
583
+ }
569
584
  return reject(new client_error_1.default(`disconnect(): Disconnected with errors: ${disconnectError.message}`, err));
570
585
  }
571
586
  });
@@ -585,7 +600,7 @@ class Client extends events_1.default {
585
600
  await this.backupSubscriptions(false);
586
601
  if (this.socket) {
587
602
  this.debug(`reconnectToTarget(): Trying to disconnect`);
588
- await this.disconnectFromTarget(forceDisconnect, isReconnecting).catch();
603
+ await this.disconnectFromTarget(forceDisconnect, isReconnecting).catch(() => { });
589
604
  }
590
605
  this.debug(`reconnectToTarget(): Trying to connect...`);
591
606
  return this.connectToTarget(true)
@@ -775,8 +790,13 @@ class Client extends events_1.default {
775
790
  * This is not called if the `settings.rawClient` is `true`.
776
791
  */
777
792
  async setupPlcConnection() {
778
- //Read system state
779
- await this.readTcSystemState();
793
+ //Read system state - try the extended version first
794
+ try {
795
+ await this.readTcSystemExtendedState();
796
+ }
797
+ catch (err) {
798
+ await this.readTcSystemState();
799
+ }
780
800
  //Start system state poller
781
801
  await this.startTcSystemStatePoller();
782
802
  //Subscribe to runtime state changes (detect PLC run/stop etc.)
@@ -839,10 +859,27 @@ class Client extends events_1.default {
839
859
  }
840
860
  let startTimer = true;
841
861
  try {
842
- let oldState = this.metaData.tcSystemState !== undefined
862
+ const oldState = this.metaData.tcSystemState
843
863
  ? { ...this.metaData.tcSystemState }
844
864
  : undefined;
845
- const state = await this.readTcSystemState();
865
+ let state;
866
+ //If we have extended state, then use it for better restart detection
867
+ if (this.metaData.tcSystemState?.restartIndex !== undefined) {
868
+ state = await this.readTcSystemExtendedState();
869
+ }
870
+ else if (this.metaData.tcSystemState) {
871
+ state = await this.readTcSystemState();
872
+ }
873
+ else {
874
+ //We don't know yet so try the extended first
875
+ try {
876
+ state = await this.readTcSystemExtendedState();
877
+ }
878
+ catch (err) {
879
+ state = await this.readTcSystemState();
880
+ }
881
+ }
882
+ //State read successfully
846
883
  this.connectionDownSince = undefined;
847
884
  if (!oldState || state.adsState !== oldState.adsState) {
848
885
  this.debug(`checkTcSystemState(): TwinCAT system state has changed from ${oldState?.adsStateStr} to ${state.adsStateStr}`);
@@ -861,6 +898,12 @@ class Client extends events_1.default {
861
898
  this.onConnectionLost();
862
899
  }
863
900
  }
901
+ else if (state.restartIndex !== undefined && oldState?.restartIndex !== state.restartIndex) {
902
+ this.debug(`checkTcSystemState(): TwinCAT system service has restarted -> reconnecting`);
903
+ this.emit('tcSystemStateChange', state, oldState);
904
+ startTimer = false;
905
+ this.onConnectionLost();
906
+ }
864
907
  }
865
908
  catch (err) {
866
909
  //Reading state failed.
@@ -949,6 +992,7 @@ class Client extends events_1.default {
949
992
  this.debug(`onPlcSymbolVersionChanged(): PLC runtime symbol version changed from ${this.metaData.plcSymbolVersion === undefined ? 'UNKNOWN' : this.metaData.plcSymbolVersion} to ${symbolVersion} -> Refreshing all cached data and subscriptions`);
950
993
  //Clear all cached symbol and data types etc.
951
994
  this.metaData.plcDataTypes = {};
995
+ this.metaData.builtDataTypes = {};
952
996
  this.metaData.plcSymbols = {};
953
997
  this.metaData.plcUploadInfo = undefined;
954
998
  //Refreshing upload info
@@ -1011,9 +1055,10 @@ class Client extends events_1.default {
1011
1055
  this.debug(`onConnectionLost(): Connection was lost. Socket failure: ${socketFailure}`);
1012
1056
  this.connection.connected = false;
1013
1057
  this.emit('connectionLost', socketFailure);
1058
+ this.emit('disconnect', true);
1014
1059
  if (this.settings.autoReconnect !== true) {
1015
1060
  this.warn("Connection to target was lost and setting autoReconnect was false -> disconnecting");
1016
- await this.disconnectFromTarget(true).catch();
1061
+ await this.disconnectFromTarget(true).catch(() => { });
1017
1062
  return;
1018
1063
  }
1019
1064
  this.socketConnectionLostHandler && this.socket?.off('close', this.socketConnectionLostHandler);
@@ -2407,6 +2452,16 @@ class Client extends events_1.default {
2407
2452
  async buildDataType(name, targetOpts = {}, isRootType = true, knownSize) {
2408
2453
  try {
2409
2454
  this.debug(`buildDataType(): Building data type for ${name}`);
2455
+ //Check cache first (only for root types without custom target options)
2456
+ const cacheKey = name.toLowerCase();
2457
+ if (isRootType
2458
+ && !this.settings.disableCaching
2459
+ && !targetOpts.adsPort
2460
+ && !targetOpts.amsNetId
2461
+ && this.metaData.builtDataTypes[cacheKey]) {
2462
+ this.debug(`buildDataType(): Returning cached built data type for ${name}`);
2463
+ return this.metaData.builtDataTypes[cacheKey];
2464
+ }
2410
2465
  let dataType;
2411
2466
  try {
2412
2467
  dataType = await this.getDataTypeDeclaration(name, targetOpts);
@@ -2551,6 +2606,10 @@ class Client extends events_1.default {
2551
2606
  //The root type has actually the data type in "name" property
2552
2607
  builtType.type = builtType.name;
2553
2608
  builtType.name = '';
2609
+ //Cache the built type (only for root types without custom target options)
2610
+ if (!this.settings.disableCaching && !targetOpts.adsPort && !targetOpts.amsNetId) {
2611
+ this.metaData.builtDataTypes[cacheKey] = builtType;
2612
+ }
2554
2613
  }
2555
2614
  return builtType;
2556
2615
  }
@@ -3771,6 +3830,8 @@ class Client extends events_1.default {
3771
3830
  /**
3772
3831
  * Reads target TwinCAT system state from ADS port 10000 (usually `Run` or `Config`).
3773
3832
  *
3833
+ * NOTE: You might want to use the extended {@link readTcSystemExtendedState}() instead.
3834
+ *
3774
3835
  * If `targetOpts` is not used to override target, the state is also
3775
3836
  * saved to the `metaData.tcSystemState`.
3776
3837
  *
@@ -3813,6 +3874,85 @@ class Client extends events_1.default {
3813
3874
  throw new client_error_1.default(`readTcSystemState(): Reading TwinCAT system state failed`, err);
3814
3875
  }
3815
3876
  }
3877
+ /**
3878
+ * Reads extended target TwinCAT system service state from ADS port 10000
3879
+ * if supported by target system. Extended version of the {@link readTcSystemState}().
3880
+ *
3881
+ * If `targetOpts` is not used to override target, the state is also
3882
+ * saved to the `metaData.tcSystemState`.
3883
+ *
3884
+ * NOTE: Might not be supported in older TwinCAT versions. If so, use {@link readTcSystemState}() instead.
3885
+ * Tested to work with 3.1.4022 and newer.
3886
+ *
3887
+ * @example
3888
+ * ```js
3889
+ * try {
3890
+ * const tcSystemState = await client.readTcSystemServiceState();
3891
+ * } catch (err) {
3892
+ * console.log("Error:", err);
3893
+ * }
3894
+ * ```
3895
+ *
3896
+ * @param targetOpts Optional target settings that override values in `settings`
3897
+ *
3898
+ * @throws Throws an error if sending the command fails or if the target responds with an error.
3899
+ */
3900
+ async readTcSystemExtendedState(targetOpts = {}) {
3901
+ if (!this.connection.connected) {
3902
+ throw new client_error_1.default(`readTcSystemServiceState(): Client is not connected. Use connect() to connect to the target first.`);
3903
+ }
3904
+ this.debug(`readTcSystemServiceState(): Reading TwinCAT system service state`);
3905
+ try {
3906
+ const target = {
3907
+ adsPort: 10000,
3908
+ ...targetOpts
3909
+ };
3910
+ const response = await this.readRaw(240, 0, 16, target);
3911
+ const result = {};
3912
+ let pos = 0;
3913
+ //0..1 ADS state
3914
+ result.adsState = response.readUInt16LE(pos);
3915
+ result.adsStateStr = ADS.ADS_STATE.toString(result.adsState);
3916
+ pos += 2;
3917
+ //2..3 Device state
3918
+ result.deviceState = response.readUInt16LE(pos);
3919
+ pos += 2;
3920
+ //4..5 Restart index
3921
+ result.restartIndex = response.readUInt16LE(pos);
3922
+ pos += 2;
3923
+ //6 Version
3924
+ result.version = response.readUInt8(pos);
3925
+ pos += 1;
3926
+ //7 Revision
3927
+ result.revision = response.readUInt8(pos);
3928
+ pos += 1;
3929
+ //8..9 Build
3930
+ result.build = response.readUInt16LE(pos);
3931
+ pos += 2;
3932
+ //10 Platform
3933
+ result.platform = response.readUInt8(pos);
3934
+ pos += 1;
3935
+ //11 OS type
3936
+ result.osType = response.readUInt8(pos);
3937
+ pos += 1;
3938
+ //12..13 Flags
3939
+ result.flags = response.readUInt16LE(pos);
3940
+ result.flagsStr = ADS.ADS_SYSTEM_SERVICE_STATE_FLAGS.toStringArray(result.flags);
3941
+ pos += 2;
3942
+ //14.. Reserved
3943
+ result.reserved = response.subarray(pos);
3944
+ this.debug(`readTcSystemServiceState(): TwinCAT system service state read successfully`);
3945
+ if (!targetOpts.adsPort && !targetOpts.amsNetId) {
3946
+ //Target is not overridden -> save to metadata
3947
+ this.metaData.tcSystemState = result;
3948
+ }
3949
+ return result;
3950
+ }
3951
+ catch (err) {
3952
+ this.debug(`readTcSystemServiceState(): Reading TwinCAT system service state failed: %o`, err);
3953
+ throw new client_error_1.default(`readTcSystemServiceState(): Reading TwinCAT system service state failed`, err);
3954
+ }
3955
+ }
3816
3956
  /**
3817
3957
  * Reads target PLC runtime symbol version.
3818
3958
  *