ads-client 1.11.2 → 1.12.1

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,29 @@ 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
+ ## [1.12.1] - 17.01.2022
8
+ ### Changed
9
+ - Updated dependecies to latest versions
10
+ - ([See issue #80](https://github.com/jisotalo/ads-client/issues/80))
11
+ - Updated `toString()` methods from `ads-client-ads.js` (problem with undefined variable)
12
+
13
+ ## [1.12.0] - 28.12.2021
14
+ ### Changed
15
+ - Improving handling of connection losses ([See pull request #78](https://github.com/jisotalo/ads-client/pull/78))
16
+ - Thank you [Hopperpop](https://github.com/Hopperpop) for contribution!
17
+ - Re-design of connection, reconnection and disconnection system
18
+ - Re-design of automatic reconnection
19
+ - Calling `reconnect()` now automatically re-subscribes all existing subscriptions
20
+
21
+ ## [1.11.4] - 17.10.2021
22
+ ### Added
23
+ - Added optional `targetAdsPort` parameter to basic raw read and write methods. ([Pull request #79](https://github.com/jisotalo/ads-client/pull/79))
24
+ - Thank you [Hopperpop](https://github.com/Hopperpop) for contribution!
25
+
26
+ ## [1.11.3] - 08.08.2021
27
+ ### Changed
28
+ - Minor bug fix for unhandled exception ([node-red-contrib-ads-client, issue #12](https://github.com/jisotalo/node-red-contrib-ads-client/issues/12#issuecomment-894711952))
29
+
7
30
  ## [1.11.2] - 01.08.2021
8
31
  ### Changed
9
32
  - Mainly minor bug fixes / adjustments that have been found during Node-RED development
package/README.md CHANGED
@@ -10,6 +10,8 @@ Beckhoff TwinCAT ADS client library for Node.js (unofficial). Connects to Beckho
10
10
 
11
11
  Coded from scratch using [TwinCAT ADS specification](https://infosys.beckhoff.com/content/1033/tc3_ads_intro/116157835.html?id=124964102706356243) and [Beckhoff.TwinCAT.Ads nuget package](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads/5.0.0-preview6). Inspiration from similar projects like [node-ads](https://www.npmjs.com/package/node-ads), [beckhoff-js](https://www.npmjs.com/package/beckhoff-js) and [iecstruct](https://www.npmjs.com/package/iecstruct).
12
12
 
13
+ There is automatically created documentation available at https://jisotalo.github.io/ads-client/
14
+
13
15
  # Project status
14
16
  This project is currently "ready". It's maintained actively and used in projects by the author and others.
15
17
 
@@ -20,60 +22,65 @@ Check out the [node-red-contrib-ads-client](https://www.npmjs.com/package/node-r
20
22
 
21
23
 
22
24
  # Table of contents
23
-
24
25
  - [Installation](#installation)
25
- - [Features](#installation)
26
+ - [Features](#features)
26
27
  - [Supported and tested platforms](#supported-and-tested-platforms)
27
28
  - [Connection setups and possibilities](#connection-setups-and-possibilities)
28
- - [Enabling localhost support](#enabling-localhost-support)
29
+ - [Enabling localhost support on TwinCAT 3](#enabling-localhost-support-on-twincat-3)
29
30
  - [IMPORTANT: Writing STRUCT variables](#important-writing-struct-variables)
30
31
  - [IMPORTANT: Things to know when using with TwinCAT 2](#important-things-to-know-when-using-with-twincat-2)
31
32
  - [Getting started](#getting-started)
32
- - [Data types used in getting started](data-types-used-in-getting-started)
33
- - [Creating a new Client instance](#creating-a-new-client-instance)
34
- - [Connecting and disconnecting](#connecting-and-disconnecting)
35
- - [Reading any type PLC variable](#reading-any-type-plc-variable)
36
- - [Example: Reading INT type variable](#example-reading-int-type-variable)
37
- - [Example: Reading STRING type variable](#example-reading-string-type-variable)
38
- - [Example: Reading ENUM type variable](#example-reading-enum-type-variable)
39
- - [Example: Reading STRUCT type variable](#example-reading-struct-type-variable)
40
- - [Example: Reading ARRAY OF INT type variable](#example-reading-array-of-int-type-variable)
41
- - [Example: Reading ARRAY OF STRUCT type variable](#example-reading-array-of-struct-type-variable)
42
- - [Example: Reading FUNCTION BLOCK type variable](#example-reading-function-block-type-variable)
43
- - [Writing any type PLC variable](#writing-any-type-plc-variable)
44
- - [Example: Writing INT type variable](#example-writing-int-type-variable)
45
- - [Example: Writing STRING type variable](#example-writing-string-type-variable)
46
- - [Example: Writing ENUM type variable](#example-writing-enum-type-variable)
47
- - [Example: Writing STRUCT type variable](#example-writing-struct-type-variable)
48
- - [Example: Writing STRUCT type variable (with autoFill parameter)](#example-writing-struct-type-variable-with-autofill-parameter)
49
- - [Example: Writing ARRAY OF INT type variable](#example-writing-array-of-int-type-variable)
50
- - [Example: Writing ARRAY of STRUCT type variable](#example-writing-array-of-struct-type-variable)
51
- - [Example: Writing FUNCTION BLOCK type variable](#example-writing-function-block-type-variable)
52
- - [Subscribing to PLC variables (device notifications)](#subscribing-to-plc-variables-device-notifications)
53
- - [Subcribe to variable value (on-change)](#subcribe-to-variable-value-on-change)
54
- - [Subcribe to variable value (cyclic)](#subcribe-to-variable-value-cyclic)
55
- - [Reading and writing raw data](#reading-and-writing-raw-data)
56
- - [Getting symbol index group, offset and size](#getting-symbol-index-group-offset-and-size)
57
- - [Reading a single raw value](#reading-a-single-raw-value)
58
- - [Writing a single raw value](#writing-a-single-raw-value)
59
- - [Reading multiple raw values](#reading-multiple-raw-values)
60
- - [Writing multiple raw values](#writing-multiple-raw-values)
61
- - [Creating a variable handle and reading a raw value](#creating-a-variable-handle-and-reading-a-raw-value)
62
- - [Creating a variable handle and writing a raw value](#creating-a-variable-handle-and-writing-a-raw-value)
63
- - [Creating and deleting multiple variable handles](#creating-and-deleting-multiple-variable-handles)
64
- - [Converting a raw value to Javascript object](#converting-a-raw-value-to-javascript-object)
65
- - [Converting a Javascript object to raw value](#converting-a-javascript-object-to-raw-value)
66
- - [Reading and writing POINTER TO and REFERENCE TO variables](#reading-and-writing-pointer-to-and-reference-to-variables)
67
- - [Reading a REFERENCE TO value](#reading-a-reference-to-value)
68
- - [Writing a REFERENCE TO value](#writing-a-reference-to-value)
69
- - [Reading a POINTER TO value](#reading-a-pointer-to-value)
70
- - [Writing a POINTER TO value](#writing-a-pointer-to-value)
71
- - [Calling a function block method with parameters using RPC (remote procedure call)](#calling-a-function-block-method-with-parameters-using-rpc-remote-procedure-call)
72
- - [Starting and stopping the PLC](#starting-and-stopping-the-plc)
73
- - [Starting and stopping the TwinCAT system](#starting-and-stopping-the-twincat-system)
74
- - [Sending custom ADS commands](#sending-custom-ads-commands)
75
- - [Available ads-client events](#available-ads-client-events)
33
+ * [Data types used in getting started](#data-types-used-in-getting-started)
34
+ * [Creating a new Client instance](#creating-a-new-client-instance)
35
+ * [Connecting and disconnecting](#connecting-and-disconnecting)
36
+ * [Reading any type PLC variable](#reading-any-type-plc-variable)
37
+ + [Example: Reading `INT` type variable](#example-reading-int-type-variable)
38
+ + [Example: Reading `STRING` type variable](#example-reading-string-type-variable)
39
+ + [Example: Reading `ENUM` type variable](#example-reading-enum-type-variable)
40
+ + [Example: Reading `STRUCT` type variable](#example-reading-struct-type-variable)
41
+ + [Example: Reading `ARRAY OF INT` type variable](#example-reading-array-of-int-type-variable)
42
+ + [Example: Reading `ARRAY OF STRUCT` type variable](#example-reading-array-of-struct-type-variable)
43
+ + [Example: Reading `FUNCTION BLOCK` type variable](#example-reading-function-block-type-variable)
44
+ * [Writing any type PLC variable](#writing-any-type-plc-variable)
45
+ + [Example: Writing `INT` type variable](#example-writing-int-type-variable)
46
+ + [Example: Writing `STRING` type variable](#example-writing-string-type-variable)
47
+ + [Example: Writing `ENUM` type variable](#example-writing-enum-type-variable)
48
+ + [Example: Writing `STRUCT` type variable](#example-writing-struct-type-variable)
49
+ + [Example: Writing `STRUCT` type variable (with autoFill parameter)](#example-writing-struct-type-variable-with-autofill-parameter-)
50
+ + [Example: Writing `ARRAY OF INT` type variable](#example-writing-array-of-int-type-variable)
51
+ + [Example: Writing `ARRAY of STRUCT` type variable](#example-writing-array-of-struct-type-variable)
52
+ + [Example: Writing `FUNCTION BLOCK` type variable](#example-writing-function-block-type-variable)
53
+ * [Subscribing to PLC variables (device notifications)](#subscribing-to-plc-variables-device-notifications-)
54
+ + [Subcribe to variable value (on-change)](#subcribe-to-variable-value-on-change-)
55
+ + [Subcribe to variable value (cyclic)](#subcribe-to-variable-value-cyclic-)
56
+ * [Reading and writing raw data](#reading-and-writing-raw-data)
57
+ + [Getting symbol index group, offset and size](#getting-symbol-index-group-offset-and-size)
58
+ + [Reading a single raw value](#reading-a-single-raw-value)
59
+ + [Writing a single raw value](#writing-a-single-raw-value)
60
+ + [Reading multiple raw values](#reading-multiple-raw-values)
61
+ + [Writing multiple raw values](#writing-multiple-raw-values)
62
+ + [Creating a variable handle and reading a raw value](#creating-a-variable-handle-and-reading-a-raw-value)
63
+ + [Creating a variable handle and writing a raw value](#creating-a-variable-handle-and-writing-a-raw-value)
64
+ + [Creating and deleting multiple variable handles](#creating-and-deleting-multiple-variable-handles)
65
+ + [Converting a raw value to Javascript object](#converting-a-raw-value-to-javascript-object)
66
+ + [Converting a Javascript object to raw value](#converting-a-javascript-object-to-raw-value)
67
+ * [Reading and writing `POINTER TO` and `REFERENCE TO` variables](#reading-and-writing-pointer-to-and-reference-to-variables)
68
+ + [Reading a `REFERENCE TO` value](#reading-a-reference-to-value)
69
+ + [Writing a `REFERENCE TO` value](#writing-a-reference-to-value)
70
+ + [Reading a `POINTER TO` value](#reading-a-pointer-to-value)
71
+ + [Writing a `POINTER TO` value](#writing-a-pointer-to-value)
72
+ * [Calling a function block method with parameters using RPC (remote procedure call)](#calling-a-function-block-method-with-parameters-using-rpc-remote-procedure-call-)
73
+ + [Calling a simple RPC method](#calling-a-simple-rpc-method)
74
+ + [Using structs with RPC methods](#using-structs-with-rpc-methods)
75
+ * [Starting and stopping the PLC](#starting-and-stopping-the-plc)
76
+ * [Starting and stopping the TwinCAT system](#starting-and-stopping-the-twincat-system)
77
+ * [Sending custom ADS commands](#sending-custom-ads-commands)
78
+ - [Available ads-client events](#available-ads-client-events)
79
+ * [Example: Printing to console when PLC runtime state changes:](#example-printing-to-console-when-plc-runtime-state-changes-)
80
+ * [Example: Catching an error that is not directly from a method call](#example-catching-an-error-that-is-not-directly-from-a-method-call)
76
81
  - [Debugging](#debugging)
82
+ * [Enabling debug from code](#enabling-debug-from-code)
83
+ * [Enabling debugging from terminal](#enabling-debugging-from-terminal)
77
84
  - [FAQ](#faq)
78
85
  - [Documentation](#documentation)
79
86
  - [License](#license)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ads-client",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "description": "Beckhoff TwinCAT ADS client library for Node.js (unofficial). Connects to Beckhoff TwinCAT automation systems using ADS protocol.",
5
5
  "main": "./src/ads-client.js",
6
6
  "scripts": {
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "author": "Jussi Isotalo <j.isotalo91@gmail.com> (https://github.com/jisotalo)",
26
26
  "license": "MIT",
27
- "homepage": "https://jisotalo.github.io/ads-client/",
27
+ "homepage": "https://github.com/jisotalo/ads-client/",
28
28
  "bugs": {
29
29
  "url": "https://github.com/jisotalo/ads-client/issues"
30
30
  },
@@ -33,9 +33,9 @@
33
33
  "url": "https://github.com/jisotalo/ads-client.git"
34
34
  },
35
35
  "dependencies": {
36
- "debug": "^4.3.1",
37
- "iconv-lite": "^0.5.2",
38
- "long": "^4.0.0"
36
+ "debug": "^4.3.3",
37
+ "iconv-lite": "^0.6.3",
38
+ "long": "^5.2.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "docdash": "^1.2.0",
@@ -255,9 +255,9 @@ const ADS_COMMAND = {
255
255
  Notification: 8,
256
256
  //ReadWrite Command
257
257
  ReadWrite: 9,
258
-
258
+
259
259
  toString: function (value) {
260
- return ((ret = Object.keys(this).find(key => this[key] == value)) != null ? ret : 'UNKNOWN')
260
+ return (Object.keys(this).find(key => this[key] == value)) || 'UNKNOWN';
261
261
  }
262
262
  }
263
263
  exports.ADS_COMMAND = ADS_COMMAND
@@ -489,7 +489,7 @@ const ADS_STATE = {
489
489
  Exception: 19,
490
490
 
491
491
  toString: function (value) {
492
- return ((ret = Object.keys(this).find(key => this[key] == value)) != null ? ret : 'UNKNOWN')
492
+ return (Object.keys(this).find(key => this[key] == value)) || 'UNKNOWN';
493
493
  }
494
494
  }
495
495
  exports.ADS_STATE = ADS_STATE
@@ -602,9 +602,8 @@ const ADS_RESERVED_INDEX_GROUPS = {
602
602
  //DeviceData (0xF100,61696)
603
603
  DeviceData: 61696, // 0x0000F100
604
604
 
605
-
606
605
  toString: function (value) {
607
- return ((ret = Object.keys(this).find(key => this[key] == value)) != null ? ret : 'UNKNOWN')
606
+ return (Object.keys(this).find(key => this[key] == value)) || 'UNKNOWN'
608
607
  }
609
608
  }
610
609
  exports.ADS_RESERVED_INDEX_GROUPS = ADS_RESERVED_INDEX_GROUPS
@@ -795,7 +794,7 @@ const ADS_DATA_TYPES = {
795
794
  ADST_BIGTYPE: 65, // 0x00000041
796
795
 
797
796
  toString: function (value) {
798
- return ((ret = Object.keys(this).find(key => this[key] == value)) != null ? ret : 'UNKNOWN')
797
+ return (Object.keys(this).find(key => this[key] == value)) || 'UNKNOWN'
799
798
  }
800
799
  }
801
800
  exports.ADS_DATA_TYPES = ADS_DATA_TYPES
@@ -847,7 +846,7 @@ const AMS_ROUTER_STATE = {
847
846
  REMOVED: 2,
848
847
 
849
848
  toString: function (value) {
850
- return ((ret = Object.keys(this).find(key => this[key] == value)) != null ? ret : 'UNKNOWN')
849
+ return (Object.keys(this).find(key => this[key] == value)) || 'UNKNOWN'
851
850
  }
852
851
  }
853
852
  exports.AMS_ROUTER_STATE = AMS_ROUTER_STATE
package/src/ads-client.js CHANGED
@@ -161,12 +161,12 @@ class Client extends EventEmitter {
161
161
  activeAdsRequests: {}, //Active ADS requests that wait for answer from router
162
162
  activeSubscriptions: {}, //Active device notifications
163
163
  symbolVersionNotification: null, //Notification handle of the symbol version changed subscription (used to unsubscribe)
164
- systemManagerStatePoller: null, //Timer that reads the system manager state (run/config) - This is not available through ADS notifications
164
+ systemManagerStatePoller: { id: 0, timer: null }, //Timer that reads the system manager state (run/config) - This is not available through ADS notifications
165
165
  firstStateReadFaultTime: null, //Date when system manager state read failed for the first time
166
166
  socketConnectionLostHandler: null, //Handler for socket connection lost event
167
167
  socketErrorHandler: null, //Handler for socket error event
168
168
  oldSubscriptions: null, //Old subscriptions that were active before connection was lost
169
- reconnectionTimer: null, //Timer that tries to reconnect intervally
169
+ reconnectionTimer: { id: 0, timer: null }, //Timer that tries to reconnect intervally
170
170
  portRegisterTimeoutTimer: null, //Timeout timer of registerAdsPort()
171
171
  }
172
172
 
@@ -290,205 +290,7 @@ class Client extends EventEmitter {
290
290
  * - If rejected, something went wrong and error info is returned (object)
291
291
  */
292
292
  connect() {
293
-
294
- return new Promise(async (resolve, reject) => {
295
-
296
- if (this._internals.socket !== null) {
297
- debug(`connect(): Socket already assigned`)
298
- return reject(new ClientException(this, 'connect()', 'Connection is already opened. Close the connection first using disconnect()'))
299
- }
300
-
301
- debug(`connect(): Starting to connect ${this.settings.routerAddress}:${this.settings.routerTcpPort}`)
302
-
303
- //Creating a socket and setting it up
304
- let socket = new net.Socket()
305
- socket.setNoDelay(true) //Sends data without delay
306
-
307
-
308
-
309
- //----- Connecting error events -----
310
-
311
- //Listening error event during connection
312
- socket.once('error', err => {
313
- debug('connect(): Socket connect failed: %O', err)
314
-
315
- //Remove all events from socket
316
- socket.removeAllListeners()
317
- socket = null
318
-
319
- //Reset connection flag
320
- this.connection.connected = false
321
-
322
- reject(new ClientException(this, 'connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (socket error ${err.errno})`, err))
323
- })
324
-
325
-
326
-
327
- //Listening close event during connection
328
- socket.once('close', hadError => {
329
- debug(`connect(): Socket closed by remote, connection failed`)
330
-
331
- //Remove all events from socket
332
- socket.removeAllListeners()
333
- socket = null
334
-
335
- //Reset connection flag
336
- this.connection.connected = false
337
-
338
- reject(new ClientException(this, 'connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket closed by remote (hadError = ${hadError})`))
339
- })
340
-
341
-
342
- //Listening end event during connection
343
- socket.once('end', () => {
344
- debug(`connect(): Socket connection ended by remote, connection failed.`)
345
-
346
- //Remove all events from socket
347
- socket.removeAllListeners()
348
- socket = null
349
-
350
- //Reset connection flag
351
- this.connection.connected = false
352
-
353
- if (this.settings.localAdsPort != null)
354
- reject(new ClientException(this, 'connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote (is the given local ADS port ${this.settings.localAdsPort} already in use?)`))
355
- else
356
- reject(new ClientException(this, 'connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote`))
357
- })
358
-
359
- //Listening timeout event during connection
360
- socket.once('timeout', () => {
361
- debug(`connect(): Socket timeout`)
362
-
363
- //No more timeout needed
364
- socket.setTimeout(0);
365
- socket.destroy()
366
-
367
- //Remove all events from socket
368
- socket.removeAllListeners()
369
- socket = null
370
-
371
- //Reset connection flag
372
- this.connection.connected = false
373
-
374
- reject(new ClientException(this, 'connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (timeout) - No response from router in ${this.settings.timeoutDelay} ms`))
375
- })
376
-
377
- //----- Connecting error events end -----
378
-
379
-
380
- //Listening for connect event
381
- socket.once('connect', async () => {
382
- debug(`connect(): Socket connection established to ${this.settings.routerAddress}:${this.settings.routerTcpPort}`)
383
-
384
- //No more timeout needed
385
- socket.setTimeout(0);
386
-
387
- this._internals.socket = socket
388
-
389
- //Try to register an ADS port
390
- try {
391
- let res = await _registerAdsPort.call(this)
392
-
393
- this.connection.connected = true
394
- this.connection.localAmsNetId = res.amsTcp.data.localAmsNetId
395
- this.connection.localAdsPort = res.amsTcp.data.localAdsPort
396
- this.connection.isLocal = this.settings.targetAmsNetId === '127.0.0.1.1.1' || this.settings.targetAmsNetId === this.connection.localAmsNetId
397
-
398
- debug(`connect(): ADS port registered from router. We are ${this.connection.localAmsNetId}:${this.connection.localAdsPort}`)
399
- } catch (err) {
400
-
401
- if (socket) {
402
- socket.destroy()
403
- //Remove all events from socket
404
- socket.removeAllListeners()
405
- }
406
- this.connection.connected = false
407
-
408
- return reject(new ClientException(this, 'connect()', `Registering ADS port from router failed`, err))
409
- }
410
-
411
- //Remove the socket events that were used only during connect()
412
- socket.removeAllListeners('error')
413
- socket.removeAllListeners('close')
414
- socket.removeAllListeners('end')
415
-
416
- //When socket errors from now on, we will close the connection
417
- this._internals.socketErrorHandler = _onSocketError.bind(this)
418
- socket.on('error', this._internals.socketErrorHandler)
419
-
420
- try {
421
- //Try to read system manager state - If it's OK, connection is successful to the target
422
- await this.readSystemManagerState()
423
- _systemManagerStatePoller.call(this)
424
-
425
- } catch (err) {
426
- try {
427
- await this.disconnect()
428
- } catch (err) {
429
- debug(`connect(): Reading target system manager failed -> Connection closed`)
430
- }
431
- this.connection.connected = false
432
-
433
- return reject(new ClientException(this, 'connect()', `Connection failed: ${err.message}`, err))
434
- }
435
-
436
-
437
- try {
438
- await _reInitializeInternals.call(this)
439
-
440
- } catch (err) {
441
- if (this.settings.allowHalfOpen !== true) {
442
- try {
443
- await this.disconnect()
444
- } catch (err) {
445
- debug(`connect(): Connecting to target PLC runtime failed -> Connection closed`)
446
- }
447
- this.connection.connected = false
448
-
449
- return reject(new ClientException(this, 'connect()', `Target and system manager found but couldn't connect to the PLC runtime (see setting allowHalfOpen): ${err.message}`, err))
450
- }
451
-
452
- //Todo: Redesign this
453
- if (this.metaData.systemManagerState.adsState !== ADS.ADS_STATE.Run)
454
- _console.call(this, `WARNING: Target is connected but not in RUN mode (mode: ${this.metaData.systemManagerState.adsStateStr}) - connecting to runtime (ADS port ${this.settings.targetAdsPort}) failed`)
455
- else
456
- _console.call(this, `WARNING: Target is connected but connecting to runtime (ADS port ${this.settings.targetAdsPort}) failed - Check the port number and that the target system state (${this.metaData.systemManagerState.adsStateStr}) is valid.`)
457
- }
458
-
459
- //Listening connection lost events
460
- this._internals.socketConnectionLostHandler = _onConnectionLost.bind(this, true)
461
- socket.on('close', this._internals.socketConnectionLostHandler)
462
-
463
- //We are connected to the target
464
- this.emit('connect', this.connection)
465
-
466
- resolve(this.connection)
467
- })
468
-
469
- //Listening data event
470
- socket.on('data', data => {
471
- _socketReceive.call(this, data)
472
- })
473
-
474
- //Timeout only during connecting, other timeouts are handled elsewhere
475
- socket.setTimeout(this.settings.timeoutDelay);
476
-
477
- //Finally, connect
478
- try {
479
- socket.connect({
480
- port: this.settings.routerTcpPort,
481
- host: this.settings.routerAddress,
482
- localPort: (this.settings.localTcpPort ? this.settings.localTcpPort : null),
483
- localAddress: (this.settings.localAddress ? this.settings.localAddress : null),
484
- })
485
-
486
- } catch (err) {
487
- this.connection.connected = false
488
-
489
- reject(new ClientException(this, 'connect()', `Opening socket connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed`, err))
490
- }
491
- })
293
+ return _connect.call(this)
492
294
  }
493
295
 
494
296
 
@@ -511,96 +313,7 @@ class Client extends EventEmitter {
511
313
  * - If rejected, connection is still closed but something went wrong during disconnecting and error info is returned
512
314
  */
513
315
  disconnect(forceDisconnect = false) {
514
- return new Promise(async (resolve, reject) => {
515
- debug(`disconnect(): Starting to close connection (force: ${forceDisconnect})`)
516
-
517
- try {
518
- if (this._internals.socketConnectionLostHandler) {
519
- this._internals.socket.off('close', this._internals.socketConnectionLostHandler)
520
- }
521
-
522
- } catch (err) {
523
- //We probably have no socket anymore. Just quit.
524
- forceDisconnect = true
525
- }
526
-
527
- //Clear system manager state poller
528
- clearTimeout(this._internals.systemManagerStatePoller)
529
- clearTimeout(this._internals.portRegisterTimeoutTimer)
530
-
531
- //If forced, then just destroy the socket
532
- if (forceDisconnect) {
533
-
534
- if (this._internals.socket) {
535
- this._internals.socket.removeAllListeners()
536
- this._internals.socket.destroy()
537
- }
538
- this.connection.connected = false
539
- this.connection.localAdsPort = null
540
- this._internals.socket = null
541
-
542
- this.emit('disconnect')
543
-
544
- return resolve()
545
- }
546
-
547
-
548
- let error = null
549
-
550
- try {
551
- await this.unsubscribeAll()
552
- } catch (err) {
553
- error = new ClientException(this, 'disconnect()', err)
554
- }
555
-
556
- try {
557
- await _unsubscribeAllInternals.call(this)
558
- } catch (err) {
559
- error = new ClientException(this, 'disconnect()', err)
560
- }
561
-
562
- try {
563
- await _unregisterAdsPort.call(this)
564
-
565
- //Done
566
- this.connection.connected = false
567
- this.connection.localAdsPort = null
568
-
569
- //Socket should be null but check it anyways
570
- if (this._internals.socket != null) {
571
- this._internals.socket.removeAllListeners()
572
- this._internals.socket.destroy() //Just incase
573
- this._internals.socket = null
574
- }
575
-
576
- debug(`disconnect(): Connection closed successfully`)
577
-
578
- } catch (err) {
579
- //Force socket close
580
- if (this._internals.socket) {
581
- this._internals.socket.removeAllListeners()
582
- this._internals.socket.destroy()
583
- }
584
-
585
- this.connection.connected = false
586
- this.connection.localAdsPort = null
587
- this._internals.socket = null
588
-
589
- error = new ClientException(this, 'disconnect()', err)
590
-
591
- debug(`disconnect(): Connection closing failed, connection forced to close`)
592
- }
593
-
594
- if (error !== null) {
595
- error.message = `Disconnected but something failed: ${error.message}`
596
- this.emit('disconnect')
597
-
598
- return reject(error)
599
- }
600
-
601
- this.emit('disconnect')
602
- resolve()
603
- })
316
+ return _disconnect.call(this, forceDisconnect)
604
317
  }
605
318
 
606
319
 
@@ -611,42 +324,18 @@ class Client extends EventEmitter {
611
324
 
612
325
 
613
326
  /**
614
- * Disconnects and reconnects again. At the moment does NOT reinitialize subscriptions, everything is lost
327
+ * Disconnects and reconnects again. Subscribes again to all active subscriptions.
328
+ * To prevent subscribing, call unsubscribeAll() before reconnecting.
615
329
  *
616
330
  * @param {boolean} [forceDisconnect] - If true, the connection is dropped immediately (default = false)
617
331
  *
618
332
  * @returns {Promise} Returns a promise (async function)
619
- * - If resolved, connection to PLC runtime is successful
620
- * - If rejected, connection to PLC runtime failed
333
+ * - If resolved, reconnecting was successful
334
+ * - If rejected, reconnecting failed and error info is returned
621
335
  */
622
336
  reconnect(forceDisconnect = false) {
623
- return new Promise(async (resolve, reject) => {
624
-
625
- if (this._internals.socket != null) {
626
- try {
627
- debug(`reconnect(): Trying to disconnect`)
628
-
629
- await this.disconnect(forceDisconnect)
630
-
631
- } catch (err) {
632
- debug(`reconnect(): Disconnecting failed: %o`, err)
633
- }
634
- }
635
-
636
- debug(`reconnect(): Trying to connect`)
637
-
638
- return this.connect()
639
- .then(res => {
640
- debug(`reconnect(): Connected!`)
641
- this.emit('reconnect')
642
-
643
- resolve(res)
644
- })
645
- .catch(err => {
646
- debug(`reconnect(): Connecting failed`)
647
- reject(err)
648
- })
649
- })
337
+ debug(`reconnect(): Reconnecting...`)
338
+ return _reconnect.call(this, forceDisconnect)
650
339
  }
651
340
 
652
341
 
@@ -1694,6 +1383,7 @@ class Client extends EventEmitter {
1694
1383
  * @param {number} indexGroup - Variable index group in the PLC
1695
1384
  * @param {number} indexOffset - Variable index offset in the PLC
1696
1385
  * @param {number} size - Variable size in the PLC (bytes)
1386
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
1697
1387
  *
1698
1388
  * @returns {Promise<Buffer>} Returns a promise (async function)
1699
1389
  * - If resolved, reading was successful and data is returned (Buffer)
@@ -1701,7 +1391,7 @@ class Client extends EventEmitter {
1701
1391
  *
1702
1392
 
1703
1393
  */
1704
- readRaw(indexGroup, indexOffset, size) {
1394
+ readRaw(indexGroup, indexOffset, size, targetAdsPort = null) {
1705
1395
  return new Promise(async (resolve, reject) => {
1706
1396
 
1707
1397
  if (!this.connection.connected)
@@ -1729,7 +1419,7 @@ class Client extends EventEmitter {
1729
1419
  data.writeUInt32LE(size, pos)
1730
1420
  pos += 4
1731
1421
 
1732
- _sendAdsCommand.call(this, ADS.ADS_COMMAND.Read, data)
1422
+ _sendAdsCommand.call(this, ADS.ADS_COMMAND.Read, data, targetAdsPort)
1733
1423
  .then((res) => {
1734
1424
  debug(`readRaw(): Data read - ${res.ads.data.byteLength} bytes from ${JSON.stringify({ indexGroup, indexOffset })}`)
1735
1425
 
@@ -1791,12 +1481,13 @@ class Client extends EventEmitter {
1791
1481
  * All required parameters can be read for example with getSymbolInfo() method, **see also readRawBySymbol()**
1792
1482
  *
1793
1483
  * @param {ReadRawMultiParam[]} targetArray - Targets to read from
1484
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
1794
1485
  *
1795
1486
  * @returns {Promise<Array.<ReadRawMultiResult>>} Returns a promise (async function)
1796
1487
  * - If resolved, reading was successful and data is returned (object)
1797
1488
  * - If rejected, reading failed and error info is returned (object)
1798
1489
  */
1799
- readRawMulti(targetArray) {
1490
+ readRawMulti(targetArray, targetAdsPort = null) {
1800
1491
  return new Promise(async (resolve, reject) => {
1801
1492
 
1802
1493
  if (!this.connection.connected)
@@ -1851,7 +1542,7 @@ class Client extends EventEmitter {
1851
1542
  pos += 4
1852
1543
  })
1853
1544
 
1854
- _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data)
1545
+ _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data, targetAdsPort)
1855
1546
  .then(res => {
1856
1547
  debug(`readRawMulti(): Data read - ${res.ads.data.byteLength} bytes received`)
1857
1548
 
@@ -1912,13 +1603,14 @@ class Client extends EventEmitter {
1912
1603
  * @param {number} indexOffset - Index offset in the PLC
1913
1604
  * @param {number} readLength - Read data length in the PLC (bytes)
1914
1605
  * @param {Buffer} dataBuffer - Data to write
1915
- *
1606
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
1607
+ *
1916
1608
  * @returns {Promise<Buffer>} Returns a promise (async function)
1917
1609
  * - If resolved, writing and reading was successful and data is returned (Buffer)
1918
1610
  * - If rejected, command failed and error info is returned (object)
1919
1611
  *
1920
1612
  */
1921
- readWriteRaw(indexGroup, indexOffset, readLength, dataBuffer) {
1613
+ readWriteRaw(indexGroup, indexOffset, readLength, dataBuffer, targetAdsPort = null) {
1922
1614
  return new Promise(async (resolve, reject) => {
1923
1615
 
1924
1616
  if (!this.connection.connected)
@@ -1956,7 +1648,7 @@ class Client extends EventEmitter {
1956
1648
  dataBuffer.copy(data, pos)
1957
1649
 
1958
1650
 
1959
- _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data)
1651
+ _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data, targetAdsPort)
1960
1652
  .then(res => {
1961
1653
  debug(`readWriteRaw(): Data written and ${res.ads.data.byteLength} bytes response received`)
1962
1654
 
@@ -2049,12 +1741,13 @@ class Client extends EventEmitter {
2049
1741
  * @param {number} indexGroup - Variable index group in the PLC
2050
1742
  * @param {number} indexOffset - Variable index offset in the PLC
2051
1743
  * @param {Buffer} dataBuffer - Buffer object that contains the data (and byteLength is acceptable)
1744
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
2052
1745
  *
2053
1746
  * @returns {Promise<object>} Returns a promise (async function)
2054
1747
  * - If resolved, writing was successful
2055
1748
  * - If rejected, writing failed and error info is returned (object)
2056
1749
  */
2057
- writeRaw(indexGroup, indexOffset, dataBuffer) {
1750
+ writeRaw(indexGroup, indexOffset, dataBuffer, targetAdsPort = null) {
2058
1751
  return new Promise(async (resolve, reject) => {
2059
1752
 
2060
1753
  if (!this.connection.connected)
@@ -2089,7 +1782,7 @@ class Client extends EventEmitter {
2089
1782
  dataBuffer.copy(data, pos)
2090
1783
 
2091
1784
 
2092
- _sendAdsCommand.call(this, ADS.ADS_COMMAND.Write, data)
1785
+ _sendAdsCommand.call(this, ADS.ADS_COMMAND.Write, data, targetAdsPort)
2093
1786
  .then((res) => {
2094
1787
  debug(`writeRaw(): Data written: ${dataBuffer.byteLength} bytes of data to ${JSON.stringify({ indexGroup, indexOffset })}`)
2095
1788
 
@@ -2131,12 +1824,13 @@ class Client extends EventEmitter {
2131
1824
  * All required parameters can be read for example with getSymbolInfo() method, **see also readRawBySymbol()**
2132
1825
  *
2133
1826
  * @param {WriteRawMultiParam[]} targetArray - Targets to write to
1827
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
2134
1828
  *
2135
1829
  * @returns {Promise<Array.<WriteRawMultiResult>>} Returns a promise (async function)
2136
1830
  * - If resolved, writing was successful and data is returned (Buffer)
2137
1831
  * - If rejected, reading failed and error info is returned (object)
2138
1832
  */
2139
- writeRawMulti(targetArray) {
1833
+ writeRawMulti(targetArray, targetAdsPort = null) {
2140
1834
  return new Promise(async (resolve, reject) => {
2141
1835
 
2142
1836
  if (!this.connection.connected)
@@ -2197,7 +1891,7 @@ class Client extends EventEmitter {
2197
1891
  pos += target.data.byteLength
2198
1892
  })
2199
1893
 
2200
- _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data)
1894
+ _sendAdsCommand.call(this, ADS.ADS_COMMAND.ReadWrite, data, targetAdsPort)
2201
1895
  .then(res => {
2202
1896
  debug(`writeRawMulti(): Data written - ${res.ads.data.byteLength} bytes received`)
2203
1897
 
@@ -3561,6 +3255,408 @@ class ClientException extends Error {
3561
3255
 
3562
3256
 
3563
3257
 
3258
+
3259
+
3260
+
3261
+ /**
3262
+ * Connects to the target system using pre-defined Client::settings (at constructor or manually given)
3263
+ *
3264
+ * @param {boolean} [isReconnecting] - If true, the _connect() call is made during reconnection (-> affects disconnect() calls if connecting fails)
3265
+ *
3266
+ * @returns {Promise<object>} Returns a promise (async function)
3267
+ * - If resolved, client is connected successfully and connection info is returned (object)
3268
+ * - If rejected, something went wrong and error info is returned (object)
3269
+ *
3270
+ * @memberof _LibraryInternals
3271
+ */
3272
+ function _connect(isReconnecting = false) {
3273
+ return new Promise(async (resolve, reject) => {
3274
+
3275
+ if (this._internals.socket !== null) {
3276
+ debug(`_connect(): Socket already assigned`)
3277
+ return reject(new ClientException(this, '_connect()', 'Connection is already opened. Close the connection first using disconnect()'))
3278
+ }
3279
+
3280
+ debug(`_connect(): Starting to connect ${this.settings.routerAddress}:${this.settings.routerTcpPort}`)
3281
+
3282
+ //Creating a socket and setting it up
3283
+ let socket = new net.Socket()
3284
+ socket.setNoDelay(true) //Sends data without delay
3285
+
3286
+
3287
+
3288
+ //----- Connecting error events -----
3289
+
3290
+ //Listening error event during connection
3291
+ socket.once('error', err => {
3292
+ debug('_connect(): Socket connect failed: %O', err)
3293
+
3294
+ //Remove all events from socket
3295
+ socket.removeAllListeners()
3296
+ socket = null
3297
+
3298
+ //Reset connection flag
3299
+ this.connection.connected = false
3300
+
3301
+ reject(new ClientException(this, '_connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (socket error ${err.errno})`, err))
3302
+ })
3303
+
3304
+
3305
+
3306
+ //Listening close event during connection
3307
+ socket.once('close', hadError => {
3308
+ debug(`_connect(): Socket closed by remote, connection failed`)
3309
+
3310
+ //Remove all events from socket
3311
+ socket.removeAllListeners()
3312
+ socket = null
3313
+
3314
+ //Reset connection flag
3315
+ this.connection.connected = false
3316
+
3317
+ reject(new ClientException(this, '_connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket closed by remote (hadError = ${hadError})`))
3318
+ })
3319
+
3320
+
3321
+ //Listening end event during connection
3322
+ socket.once('end', () => {
3323
+ debug(`_connect(): Socket connection ended by remote, connection failed.`)
3324
+
3325
+ //Remove all events from socket
3326
+ socket.removeAllListeners()
3327
+ socket = null
3328
+
3329
+ //Reset connection flag
3330
+ this.connection.connected = false
3331
+
3332
+ if (this.settings.localAdsPort != null)
3333
+ reject(new ClientException(this, '_connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote (is the given local ADS port ${this.settings.localAdsPort} already in use?)`))
3334
+ else
3335
+ reject(new ClientException(this, '_connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed - socket ended by remote`))
3336
+ })
3337
+
3338
+ //Listening timeout event during connection
3339
+ socket.once('timeout', () => {
3340
+ debug(`_connect(): Socket timeout`)
3341
+
3342
+ //No more timeout needed
3343
+ socket.setTimeout(0);
3344
+ socket.destroy()
3345
+
3346
+ //Remove all events from socket
3347
+ socket.removeAllListeners()
3348
+ socket = null
3349
+
3350
+ //Reset connection flag
3351
+ this.connection.connected = false
3352
+
3353
+ reject(new ClientException(this, '_connect()', `Connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed (timeout) - No response from router in ${this.settings.timeoutDelay} ms`))
3354
+ })
3355
+
3356
+ //----- Connecting error events end -----
3357
+
3358
+
3359
+ //Listening for connect event
3360
+ socket.once('connect', async () => {
3361
+ debug(`_connect(): Socket connection established to ${this.settings.routerAddress}:${this.settings.routerTcpPort}`)
3362
+
3363
+ //No more timeout needed
3364
+ socket.setTimeout(0);
3365
+
3366
+ this._internals.socket = socket
3367
+
3368
+ //Try to register an ADS port
3369
+ try {
3370
+ let res = await _registerAdsPort.call(this)
3371
+
3372
+ this.connection.connected = true
3373
+ this.connection.localAmsNetId = res.amsTcp.data.localAmsNetId
3374
+ this.connection.localAdsPort = res.amsTcp.data.localAdsPort
3375
+ this.connection.isLocal = this.settings.targetAmsNetId === '127.0.0.1.1.1' || this.settings.targetAmsNetId === this.connection.localAmsNetId
3376
+
3377
+ debug(`_connect(): ADS port registered from router. We are ${this.connection.localAmsNetId}:${this.connection.localAdsPort}`)
3378
+ } catch (err) {
3379
+
3380
+ if (socket) {
3381
+ socket.destroy()
3382
+ //Remove all events from socket
3383
+ socket.removeAllListeners()
3384
+ }
3385
+ this.connection.connected = false
3386
+
3387
+ return reject(new ClientException(this, '_connect()', `Registering ADS port from router failed`, err))
3388
+ }
3389
+
3390
+ //Remove the socket events that were used only during _connect()
3391
+ socket.removeAllListeners('error')
3392
+ socket.removeAllListeners('close')
3393
+ socket.removeAllListeners('end')
3394
+
3395
+ //When socket errors from now on, we will close the connection
3396
+ this._internals.socketErrorHandler = _onSocketError.bind(this)
3397
+ socket.on('error', this._internals.socketErrorHandler)
3398
+
3399
+ try {
3400
+ //Try to read system manager state - If it's OK, connection is successful to the target
3401
+ await this.readSystemManagerState()
3402
+ _systemManagerStatePoller.call(this)
3403
+
3404
+ } catch (err) {
3405
+ try {
3406
+ await _disconnect.call(this, false, isReconnecting)
3407
+ } catch (err) {
3408
+ debug(`_connect(): Reading target system manager failed -> Connection closed`)
3409
+ }
3410
+ this.connection.connected = false
3411
+
3412
+ return reject(new ClientException(this, '_connect()', `Connection failed: ${err.message}`, err))
3413
+ }
3414
+
3415
+
3416
+ try {
3417
+ await _reInitializeInternals.call(this)
3418
+
3419
+ } catch (err) {
3420
+ if (this.settings.allowHalfOpen !== true) {
3421
+ try {
3422
+ await _disconnect.call(this, false, true)
3423
+ } catch (err) {
3424
+ debug(`_connect(): Connecting to target PLC runtime failed -> Connection closed`)
3425
+ }
3426
+ this.connection.connected = false
3427
+
3428
+ return reject(new ClientException(this, '_connect()', `Target and system manager found but couldn't connect to the PLC runtime (see setting allowHalfOpen): ${err.message}`, err))
3429
+ }
3430
+
3431
+ //Todo: Redesign this
3432
+ if (this.metaData.systemManagerState.adsState !== ADS.ADS_STATE.Run)
3433
+ _console.call(this, `WARNING: Target is connected but not in RUN mode (mode: ${this.metaData.systemManagerState.adsStateStr}) - connecting to runtime (ADS port ${this.settings.targetAdsPort}) failed`)
3434
+ else
3435
+ _console.call(this, `WARNING: Target is connected but connecting to runtime (ADS port ${this.settings.targetAdsPort}) failed - Check the port number and that the target system state (${this.metaData.systemManagerState.adsStateStr}) is valid.`)
3436
+ }
3437
+
3438
+ //Listening connection lost events
3439
+ this._internals.socketConnectionLostHandler = _onConnectionLost.bind(this, true)
3440
+ socket.on('close', this._internals.socketConnectionLostHandler)
3441
+
3442
+ //We are connected to the target
3443
+ this.emit('connect', this.connection)
3444
+
3445
+ resolve(this.connection)
3446
+ })
3447
+
3448
+ //Listening data event
3449
+ socket.on('data', data => {
3450
+ _socketReceive.call(this, data)
3451
+ })
3452
+
3453
+ //Timeout only during connecting, other timeouts are handled elsewhere
3454
+ socket.setTimeout(this.settings.timeoutDelay);
3455
+
3456
+ //Finally, connect
3457
+ try {
3458
+ socket.connect({
3459
+ port: this.settings.routerTcpPort,
3460
+ host: this.settings.routerAddress,
3461
+ localPort: (this.settings.localTcpPort ? this.settings.localTcpPort : null),
3462
+ localAddress: (this.settings.localAddress ? this.settings.localAddress : null),
3463
+ })
3464
+
3465
+ } catch (err) {
3466
+ this.connection.connected = false
3467
+
3468
+ reject(new ClientException(this, '_connect()', `Opening socket connection to ${this.settings.routerAddress}:${this.settings.routerTcpPort} failed`, err))
3469
+ }
3470
+ })
3471
+ }
3472
+
3473
+
3474
+ /**
3475
+ * Unsubscribes all notifications, unregisters ADS port from router (if it was registered)
3476
+ * and disconnects target system and ADS router
3477
+ *
3478
+ * @param {boolean} [forceDisconnect] - If true, the connection is dropped immediately (default = false)
3479
+ * @param {boolean} [isReconnecting] - If true, _disconnect() call is made during reconnecting
3480
+ *
3481
+ * @returns {Promise<object>} Returns a promise (async function)
3482
+ * - If resolved, disconnect was successful
3483
+ * - If rejected, connection is still closed but something went wrong during disconnecting and error info is returned
3484
+ *
3485
+ * @memberof _LibraryInternals
3486
+ */
3487
+ function _disconnect(forceDisconnect = false, isReconnecting = false) {
3488
+ return new Promise(async (resolve, reject) => {
3489
+
3490
+ debug(`_disconnect(): Starting to close connection (force: ${forceDisconnect})`)
3491
+
3492
+ try {
3493
+ if (this._internals.socketConnectionLostHandler) {
3494
+ this._internals.socket.off('close', this._internals.socketConnectionLostHandler)
3495
+ }
3496
+
3497
+ } catch (err) {
3498
+ //We probably have no socket anymore. Just quit.
3499
+ forceDisconnect = true
3500
+ }
3501
+
3502
+ //Clear reconnection timer only when not reconnecting
3503
+ if (!isReconnecting) {
3504
+ _clearTimer(this._internals.reconnectionTimer)
3505
+ }
3506
+
3507
+ //Clear other timers
3508
+ _clearTimer(this._internals.systemManagerStatePoller)
3509
+ clearTimeout(this._internals.portRegisterTimeoutTimer)
3510
+
3511
+ //If forced, then just destroy the socket
3512
+ if (forceDisconnect) {
3513
+
3514
+ if (this._internals.socket) {
3515
+ this._internals.socket.removeAllListeners()
3516
+ this._internals.socket.destroy()
3517
+ }
3518
+ this.connection.connected = false
3519
+ this.connection.localAdsPort = null
3520
+ this._internals.socket = null
3521
+
3522
+ this.emit('disconnect')
3523
+
3524
+ return resolve()
3525
+ }
3526
+
3527
+
3528
+ let error = null
3529
+
3530
+ try {
3531
+ await this.unsubscribeAll()
3532
+ } catch (err) {
3533
+ error = new ClientException(this, 'disconnect()', err)
3534
+ }
3535
+
3536
+ try {
3537
+ await _unsubscribeAllInternals.call(this)
3538
+ } catch (err) {
3539
+ error = new ClientException(this, 'disconnect()', err)
3540
+ }
3541
+
3542
+ try {
3543
+ await _unregisterAdsPort.call(this)
3544
+
3545
+ //Done
3546
+ this.connection.connected = false
3547
+ this.connection.localAdsPort = null
3548
+
3549
+ //Socket should be null but check it anyways
3550
+ if (this._internals.socket != null) {
3551
+ this._internals.socket.removeAllListeners()
3552
+ this._internals.socket.destroy() //Just incase
3553
+ this._internals.socket = null
3554
+ }
3555
+
3556
+ debug(`_disconnect(): Connection closed successfully`)
3557
+
3558
+ } catch (err) {
3559
+ //Force socket close
3560
+ if (this._internals.socket) {
3561
+ this._internals.socket.removeAllListeners()
3562
+ this._internals.socket.destroy()
3563
+ }
3564
+
3565
+ this.connection.connected = false
3566
+ this.connection.localAdsPort = null
3567
+ this._internals.socket = null
3568
+
3569
+ error = new ClientException(this, 'disconnect()', err)
3570
+
3571
+ debug(`_disconnect(): Connection closing failed, connection forced to close`)
3572
+ }
3573
+
3574
+ if (error !== null) {
3575
+ error.message = `Disconnected but something failed: ${error.message}`
3576
+ this.emit('disconnect')
3577
+
3578
+ return reject(error)
3579
+ }
3580
+
3581
+ this.emit('disconnect')
3582
+ resolve()
3583
+ })
3584
+ }
3585
+
3586
+
3587
+
3588
+ /**
3589
+ * Disconnects and reconnects again. Subscribes again to all active subscriptions.
3590
+ * To prevent subscribing, call unsubscribeAll() before reconnecting.
3591
+ *
3592
+ * @param {boolean} [forceDisconnect] - If true, the connection is dropped immediately (default = false)
3593
+ * @param {boolean} [isReconnecting] - If true, _reconnect() call is made during reconnecting
3594
+ *
3595
+ * @returns {Promise<object>} Returns a promise (async function)
3596
+ * - If resolved, reconnecting was successful
3597
+ * - If rejected, reconnecting failed and error info is returned
3598
+ *
3599
+ * @memberof _LibraryInternals
3600
+ */
3601
+ function _reconnect(forceDisconnect = false, isReconnecting = false) {
3602
+ return new Promise(async (resolve, reject) => {
3603
+
3604
+ //Clear all cached symbols and data types (might be incorrect)
3605
+ this.metaData.symbols = {}
3606
+ this.metaData.dataTypes = {}
3607
+
3608
+ //Save active subscriptions to memory and delete olds
3609
+ if (this._internals.oldSubscriptions == null) {
3610
+ this._internals.oldSubscriptions = {}
3611
+ Object.assign(this._internals.oldSubscriptions, this._internals.activeSubscriptions)
3612
+
3613
+ debug(`_reconnect(): Total of ${Object.keys(this._internals.activeSubscriptions).length} subcriptions saved for reinitializing`)
3614
+ }
3615
+
3616
+ this._internals.activeSubscriptions = {}
3617
+
3618
+ if (this._internals.socket != null) {
3619
+ try {
3620
+ debug(`_reconnect(): Trying to disconnect`)
3621
+
3622
+ await _disconnect.call(this, forceDisconnect, isReconnecting)
3623
+
3624
+ } catch (err) {
3625
+ //debug(`_reconnect(): Disconnecting failed: %o`, err)
3626
+ }
3627
+ }
3628
+
3629
+ debug(`_reconnect(): Trying to connect`)
3630
+
3631
+ return _connect.call(this, true)
3632
+ .then(res => {
3633
+ debug(`_reconnect(): Connected!`)
3634
+
3635
+ //Reconnected, try to subscribe again
3636
+ _reInitializeSubscriptions.call(this, this._internals.oldSubscriptions)
3637
+ .then(() => {
3638
+ _console.call(this, `PLC runtime reconnected successfully and all subscriptions were restored!`)
3639
+
3640
+ debug(`_reconnect(): Connection and subscriptions reinitialized. Connection is back.`)
3641
+ })
3642
+ .catch(err => {
3643
+ _console.call(this, `PLC runtime reconnected successfully but not all subscriptions were restored. Error info: ${err}`)
3644
+
3645
+ debug(`_reconnect(): Connection and some subscriptions reinitialized. Connection is back.`)
3646
+ })
3647
+
3648
+ this.emit('reconnect')
3649
+
3650
+ resolve(res)
3651
+ })
3652
+ .catch(err => {
3653
+ debug(`_reconnect(): Connecting failed`)
3654
+ reject(err)
3655
+ })
3656
+ })
3657
+ }
3658
+
3659
+
3564
3660
  /**
3565
3661
  * Registers a new ADS port from used AMS router
3566
3662
  *
@@ -3672,8 +3768,8 @@ function _unregisterAdsPort() {
3672
3768
  return new Promise(async (resolve, reject) => {
3673
3769
  debugD(`_unregisterAdsPort(): Unregister ads port ${this.connection.localAdsPort} from ${this.settings.routerAddress}:${this.settings.routerTcpPort}`)
3674
3770
 
3675
- //If we have manually given ADS port, we only need to close the connection manually
3676
- if (this.settings.localAdsPort) {
3771
+ //If we have manually given AmsNetId and ADS port, we only need to close the connection manually
3772
+ if (this.settings.localAmsNetId && this.settings.localAdsPort) {
3677
3773
  debug(`_unregisterAdsPort(): Local AmsNetId and ADS port manually given so no need to unregister`)
3678
3774
 
3679
3775
 
@@ -3824,8 +3920,7 @@ async function _onSocketError(err) {
3824
3920
  */
3825
3921
  async function _onConnectionLost(socketFailure = false) {
3826
3922
  //Clear timers
3827
- clearTimeout(this._internals.systemManagerStatePoller)
3828
- clearTimeout(this._internals.reconnectionTimer)
3923
+ _clearTimer(this._internals.systemManagerStatePoller)
3829
3924
 
3830
3925
  debug(`_onConnectionLost(): Connection was lost. Socket failure: ${socketFailure}`)
3831
3926
 
@@ -3841,58 +3936,75 @@ async function _onConnectionLost(socketFailure = false) {
3841
3936
  return
3842
3937
  }
3843
3938
 
3844
- if (this._internals.socketConnectionLostHandler)
3939
+ if (this._internals.socket && this._internals.socketConnectionLostHandler)
3845
3940
  this._internals.socket.off('close', this._internals.socketConnectionLostHandler)
3846
3941
 
3847
3942
  _console.call(this, 'WARNING: Connection was lost. Trying to reconnect...')
3848
3943
 
3944
+ const tryToReconnect = async (firstTime, timerId) => {
3849
3945
 
3850
- //Clear all cached symbols and data types (might be incorrect)
3851
- this.metaData.symbols = {}
3852
- this.metaData.dataTypes = {}
3946
+ //If the timer has changed, quit here
3947
+ if (this._internals.reconnectionTimer.id !== timerId) {
3948
+ return
3949
+ }
3853
3950
 
3854
- //Save active subscriptions to memory and delete olds
3855
- if (this._internals.oldSubscriptions == null) {
3856
- this._internals.oldSubscriptions = {}
3857
- Object.assign(this._internals.oldSubscriptions, this._internals.activeSubscriptions)
3951
+ //Try to reconnect
3952
+ _reconnect.call(this, socketFailure, true)
3953
+ .then(res => {
3954
+
3955
+ //Success -> remove timer
3956
+ _clearTimer(this._internals.reconnectionTimer)
3858
3957
 
3859
- debugD(`_onConnectionLost(): Total of ${Object.keys(this._internals.activeSubscriptions).length} subcriptions saved for reinitializing`)
3958
+ })
3959
+ .catch(err => {
3960
+ //Reconnecting failed
3961
+ if (firstTime)
3962
+ _console.call(this, `WARNING: Reconnecting failed. Keeping trying in the background every ${this.settings.reconnectInterval} ms...`)
3963
+
3964
+ //If this is still a valid timer, start over again
3965
+ if (this._internals.reconnectionTimer.id === timerId) {
3966
+ //Creating a new timer with the same id
3967
+ this._internals.reconnectionTimer.timer = setTimeout(
3968
+ () => tryToReconnect(false, timerId),
3969
+ this.settings.reconnectInterval
3970
+ )
3971
+ } else {
3972
+ debugD(`_onConnectionLost(): Timer is no more valid, quiting here`)
3973
+ }
3974
+ })
3860
3975
  }
3861
3976
 
3862
- this._internals.activeSubscriptions = {}
3863
-
3977
+ //Clearing old timer if there is one + increasing timer id
3978
+ _clearTimer(this._internals.reconnectionTimer)
3864
3979
 
3865
- const tryToReconnect = async (firstTime) => {
3866
- clearTimeout(this._internals.reconnectionTimer)
3980
+ //Starting poller timer
3981
+ this._internals.reconnectionTimer.timer = setTimeout(
3982
+ () => tryToReconnect(true, this._internals.reconnectionTimer.id),
3983
+ this.settings.reconnectInterval
3984
+ )
3985
+ }
3867
3986
 
3868
- this.reconnect(socketFailure)
3869
- .then(res => {
3870
3987
 
3871
- _reInitializeSubscriptions.call(this, this._internals.oldSubscriptions)
3872
- .then(() => {
3873
- _console.call(this, `PLC runtime reconnected successfully and all subscriptions were restored!`)
3874
3988
 
3875
- debug(`_onConnectionLost(): Connection and subscriptions reinitialized. Connection is back.`)
3876
- })
3877
- .catch(err => {
3878
- _console.call(this, `PLC runtime reconnected successfully but not all subscriptions were restored. Error info: ${err}`)
3879
3989
 
3880
- debug(`_onConnectionLost(): Connection and some subscriptions reinitialized. Connection is back.`)
3881
- })
3882
- })
3883
- .catch(err => {
3884
- if (firstTime)
3885
- _console.call(this, `WARNING: Reconnecting failed. Keeping trying in the background every ${this.settings.reconnectInterval} ms...`)
3886
3990
 
3887
- //Clear timer again just incase
3888
- clearTimeout(this._internals.reconnectionTimer)
3889
- this._internals.reconnectionTimer = setTimeout(() => tryToReconnect(false), this.settings.reconnectInterval)
3890
- })
3891
- }
3991
+ /**
3992
+ * Clears given object timer if it's available
3993
+ * and increases the id
3994
+ *
3995
+ * @param {object} timerObject Timer object {id, timer}
3996
+ *
3997
+ * @memberof _LibraryInternals
3998
+ */
3999
+ function _clearTimer(timerObject) {
4000
+ //Clearing timer
4001
+ clearTimeout(timerObject.timer)
4002
+ timerObject.timer = null
3892
4003
 
3893
- clearTimeout(this._internals.reconnectionTimer)
3894
- this._internals.reconnectionTimer = setTimeout(() => tryToReconnect(true), this.settings.reconnectInterval)
4004
+ //Increasing timer id
4005
+ timerObject.id = timerObject.id < Number.MAX_SAFE_INTEGER ? timerObject.id + 1 : 0;
3895
4006
  }
4007
+
3896
4008
 
3897
4009
 
3898
4010
 
@@ -4162,10 +4274,15 @@ async function _onSymbolVersionChanged(data) {
4162
4274
  * @memberof _LibraryInternals
4163
4275
  */
4164
4276
  function _systemManagerStatePoller() {
4165
- clearTimeout(this._internals.systemManagerStatePoller)
4166
4277
 
4167
- const poller = async () => {
4168
- let startAgain = true, oldState = this.metaData.systemManagerState
4278
+ const poller = async (timerId) => {
4279
+ let startAgain = true
4280
+ let oldState = this.metaData.systemManagerState
4281
+
4282
+ //If the timer has changed, quit here
4283
+ if (this._internals.systemManagerStatePoller.id !== timerId){
4284
+ return
4285
+ }
4169
4286
 
4170
4287
  try {
4171
4288
  await this.readSystemManagerState()
@@ -4186,6 +4303,7 @@ function _systemManagerStatePoller() {
4186
4303
  _onConnectionLost.call(this)
4187
4304
  }
4188
4305
  }
4306
+
4189
4307
  } catch (err) {
4190
4308
  debug(`_systemManagerStatePoller(): Reading system manager state failed: %o`, err)
4191
4309
 
@@ -4202,11 +4320,26 @@ function _systemManagerStatePoller() {
4202
4320
  }
4203
4321
  }
4204
4322
 
4205
- if (startAgain)
4206
- this._internals.systemManagerStatePoller = setTimeout(poller, this.settings.checkStateInterval)
4323
+ //Try again if required AND this is still the valid timer
4324
+ if (startAgain && this._internals.systemManagerStatePoller.id === timerId) {
4325
+ //Creating a new timer with the same id
4326
+ this._internals.systemManagerStatePoller.timer = setTimeout(
4327
+ () => poller(timerId),
4328
+ this.settings.checkStateInterval
4329
+ )
4330
+ }
4207
4331
  }
4208
4332
 
4209
- this._internals.systemManagerStatePoller = setTimeout(poller, this.settings.checkStateInterval)
4333
+
4334
+ //Clearing old timer if there is one + increasing timer id
4335
+ _clearTimer(this._internals.systemManagerStatePoller)
4336
+
4337
+ //Starting poller timer
4338
+ this._internals.systemManagerStatePoller.timer = setTimeout(
4339
+ () => poller(this._internals.systemManagerStatePoller.id),
4340
+ this.settings.checkStateInterval
4341
+ )
4342
+
4210
4343
  }
4211
4344
 
4212
4345
 
@@ -4281,7 +4414,7 @@ async function _onRouterStateChanged(data) {
4281
4414
  //If we have a local connection, connection needs to be reinitialized
4282
4415
  if (this.connection.isLocal === true) {
4283
4416
  //We should stop polling system manager, it will be reinitialized later
4284
- clearTimeout(this._internals.systemManagerStatePoller)
4417
+ _clearTimer(this._internals.systemManagerStatePoller)
4285
4418
 
4286
4419
  debug(`_onRouterStateChanged(): Local loopback connection active, monitoring router state`)
4287
4420
 
@@ -6310,8 +6443,8 @@ function _parseAdsNotification(data) {
6310
6443
  *
6311
6444
  * @param {number} adsCommand - ADS command to send (see ADS.ADS_COMMAND)
6312
6445
  * @param {Buffer} adsData - Buffer object that contains the data to send
6313
- * @param {number} [targetAdsPort] - Target ADS port - default is this.settings.targetAdsPort
6314
- * @param {string} [targetAmsNetId] - Target AmsNetID - default is this.settings.targetAmsNetId
6446
+ * @param {number} [targetAdsPort] - Target ADS port (optional) - default is this.settings.targetAdsPort
6447
+ * @param {string} [targetAmsNetId] - Target AmsNetID (optional) - default is this.settings.targetAmsNetId
6315
6448
  *
6316
6449
  * @returns {Promise<object>} Returns a promise (async function)
6317
6450
  * - If resolved, command was sent successfully and response was received. The received reponse is parsed and returned (object)