Giter VIP home page Giter VIP logo

Comments (19)

jisotalo avatar jisotalo commented on July 20, 2024

Hmm. I'm not sure if I'm following 100%. So the localAddress 192.168.1.12 is not valid network interface IP at the raspberry? And that causes those problems?

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

Let me try to explain it differently. The rPi has a eth0 & wlan0, 192.168.1.22 & .23.

With above configuration and following static route on the "plc", it works:
image

With above configuration and following not matching static route, it creates havoc. I don't expect it to work, but something isn't handled well. Node-red crashes constantly and is stuck in a reboot cycle.
image

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

Did some testing, and what it looks like is that when autoReconnect option in the config node is true, it's having problems.
Probably because the node has a reconnect mechanisme and the ads-client.js also. Causing a cascade of reconnect retries, where it multiplies like rabbits until something says 'enough is enough'.

I also want to thank you again for all the effort.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

Ok thanks!

I'll try to check during the weekend using my raspberry pi, so the setup is similar too.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

I managed to get the same problem too, however not yet clear what is going on.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

The updates in ads-client 1.11.1 related also to issue #13 should help with this problem too.

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

I tested with ads-client 1.11.1. It didn't crash node-red as before, and at first glance it seemed to be fixed.
Although as seen in the log bellow, it sometimes starts to show warnings in a semi random pattern. It starts slow, but let it run long enough there are more and more warnings at once and cpu usage increases.

I have a hypothesis that it only does that if you deploy only the modified nodes (in this case the connection node). But that still needs more testing to confirm.

On a side note: On node-red-contrib-ads-client it still shows version 1.2.0. Is this intentional or something is going wrong?

14 Jul 22:25:48 - [info] Starting flows
14 Jul 22:25:48 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connecting to 172.28.112.1.1.1:851...
14 Jul 22:25:48 - [info] Started flows
14 Jul 22:25:48 - [info] [aedes broker:a8d39387.25e69] Binding aedes mqtt server on port: 1883
14 Jul 22:25:48 - [info] [mqtt-broker:13590f32.4d4651] Connected to broker: mqtt://localhost:1883
14 Jul 22:25:49 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connected to 172.28.112.1.1.1:851!
14 Jul 22:27:01 - [info] Stopping modified nodes
14 Jul 22:27:01 - [info] [ads-client-connection:d2a2cdf5.d78a9] Disconnecting from 172.28.112.1.1.1:851 and unsubscribing from all...
14 Jul 22:27:01 - [info] [ads-client-connection:d2a2cdf5.d78a9] Disconnected from 172.28.112.1.1.1:851
14 Jul 22:27:01 - [info] Stopped modified nodes
14 Jul 22:27:01 - [info] Starting modified nodes
14 Jul 22:27:01 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connecting to 172.28.112.1.1.1:851...
14 Jul 22:27:01 - [info] Started modified nodes
14 Jul 22:27:03 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connecting to 172.28.112.1.1.1:851 failed, keeping trying..
14 Jul 22:27:03 - [warn] [ads-client-connection:d2a2cdf5.d78a9] Failed to connect 172.28.112.1.1.1:851 at startup: ClientException: Connection failed: Device system manager state read failed - ADS error -1 (Timeout - no response in 2000 ms)
14 Jul 22:27:04 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connecting to 172.28.112.1.1.1:851...
14 Jul 22:27:06 - [info] [ads-client-connection:d2a2cdf5.d78a9] Connecting to 172.28.112.1.1.1:851 failed, keeping trying..
14 Jul 22:27:06 - [error] [ads-client-read-symbol:451f2aca.e8d8c4] ClientException: Connection failed: Device system manager state read failed - ADS error -1 (Timeout - no response in 2000 ms)
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Reconnecting failed. Keeping trying in the background every 2000 ms...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: {"errno":"ECONNRESET","code":"ECONNRESET","syscall":"read"}
ads-client: WARNING: Connection was lost. Trying to reconnect...
ads-client: WARNING: Socket connection had an error, closing connection: 

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

That's exactly the same problem that I had every now and then.

However it's quite hard to debug, haven't yet found a way to re-create it easily, just happens randomly. Probably not a big fix though after you find it. I will keep trying.

The Node-RED page requires manual update so I don't always remember to do that. I updated it now.

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

It's indeed hard to debug. What I found today is that there are indeed multiple tryToReconnect() loops. Still don't know exactly how it starts. A guess would be that _onConnectionLost is fired when tryToReconnect is running, creating a branch and duplicating the tryToReconnect. Basically everytime you see "ads-client: WARNING: Socket connection had an error, closing connection" it has a potential to create a new branch. We should try to build a mechanisme preventing multiple reconnecting loops.

Following logs shows the setTimeout objects. Clearly showing multiple ones starting at the same time and repeating after 4000ms:

ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776113,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776117,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776118,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776121,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776121,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776122,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776125,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2776127,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2778960,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2778965,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2778965,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2779102,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2779113,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780116,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780121,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780121,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780124,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780125,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780125,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780127,"_repeat":null,"_destroyed":false}
ads-client: {"_called":true,"_idleTimeout":2000,"_idlePrev":null,"_idleNext":null,"_idleStart":2780129,"_repeat":null,"_destroyed":false}

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

That's a good finding! I will check it out during the week.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

I just checked the ads-client code and it seems to be possible that indeed the _onConnectionLost() is called quite often which might cause timers to stack.

Could you change the ads-client.js and test if it helps? My raspberry pi has some SD card issues (who would guess) so can't test yet..

I added double (or triple..) checks to clear the reconnect timer handle so that there would be always one timer running. In addition, I changed so that the reconnection is not made immediately but after the reconnectInterval time.

Code before

async function _onConnectionLost(socketFailure = false) {
  debug(`_onConnectionLost(): Connection was lost. Socket failure: ${socketFailure}`)
  
  this.connection.connected = false
  this.emit('connectionLost')
  
  if (this.settings.autoReconnect !== true) {
    _console.call(this, 'WARNING: Connection was lost and setting autoReconnect=false. Quiting.')
    try {
      await this.disconnect(true)
    } catch { }
    
    return
  }

  if (this._internals.socketConnectionLostHandler)
    this._internals.socket.off('close', this._internals.socketConnectionLostHandler)
  
  _console.call(this, 'WARNING: Connection was lost. Trying to reconnect...')
  
  //Clear timers
  clearTimeout(this._internals.systemManagerStatePoller)
  clearTimeout(this._internals.reconnectionTimer)

  //Clear all cached symbols and data types (might be incorrect)
  this.metaData.symbols = {}
  this.metaData.dataTypes = {}

  //Save active subscriptions to memory and delete olds
  if (this._internals.oldSubscriptions == null) {
    this._internals.oldSubscriptions = {}
    Object.assign(this._internals.oldSubscriptions, this._internals.activeSubscriptions)

    debugD(`_onConnectionLost(): Total of ${Object.keys(this._internals.activeSubscriptions).length} subcriptions saved for reinitializing`)
  } 
  
  this._internals.activeSubscriptions = {}


  const tryToReconnect = async (firstTime) => {
    this.reconnect(socketFailure)
      .then(res => {
         
        _reInitializeSubscriptions.call(this, this._internals.oldSubscriptions)
          .then(() => {
            _console.call(this, `PLC runtime reconnected successfully and all subscriptions were restored!`)
            
            debug(`_onConnectionLost(): Connection and subscriptions reinitialized. Connection is back.`)
          })
          .catch(err => {
            _console.call(this, `PLC runtime reconnected successfully but not all subscriptions were restored. Error info:`, JSON.stringify(err))

            debug(`_onConnectionLost(): Connection and some subscriptions reinitialized. Connection is back.`)
          })
      })
      .catch(err => {
        if (firstTime)
          _console.call(this, `WARNING: Reconnecting failed. Keeping trying in the background every ${this.settings.reconnectInterval} ms...`)
        
        this._internals.reconnectionTimer = setTimeout(tryToReconnect, this.settings.reconnectInterval)
    })
  }

  tryToReconnect(true)
}

Code after:

async function _onConnectionLost(socketFailure = false) {
  //Clear timers
  clearTimeout(this._internals.systemManagerStatePoller)
  clearTimeout(this._internals.reconnectionTimer)
  
  debug(`_onConnectionLost(): Connection was lost. Socket failure: ${socketFailure}`)
  
  this.connection.connected = false
  this.emit('connectionLost')
  
  if (this.settings.autoReconnect !== true) {
    _console.call(this, 'WARNING: Connection was lost and setting autoReconnect=false. Quiting.')
    try {
      await this.disconnect(true)
    } catch { }
    
    return
  }

  if (this._internals.socketConnectionLostHandler)
    this._internals.socket.off('close', this._internals.socketConnectionLostHandler)
  
  _console.call(this, 'WARNING: Connection was lost. Trying to reconnect...')
  

  //Clear all cached symbols and data types (might be incorrect)
  this.metaData.symbols = {}
  this.metaData.dataTypes = {}

  //Save active subscriptions to memory and delete olds
  if (this._internals.oldSubscriptions == null) {
    this._internals.oldSubscriptions = {}
    Object.assign(this._internals.oldSubscriptions, this._internals.activeSubscriptions)

    debugD(`_onConnectionLost(): Total of ${Object.keys(this._internals.activeSubscriptions).length} subcriptions saved for reinitializing`)
  } 
  
  this._internals.activeSubscriptions = {}


  const tryToReconnect = async (firstTime) => {
    clearTimeout(this._internals.reconnectionTimer)

    this.reconnect(socketFailure)
      .then(res => {
         
        _reInitializeSubscriptions.call(this, this._internals.oldSubscriptions)
          .then(() => {
            _console.call(this, `PLC runtime reconnected successfully and all subscriptions were restored!`)
            
            debug(`_onConnectionLost(): Connection and subscriptions reinitialized. Connection is back.`)
          })
          .catch(err => {
            _console.call(this, `PLC runtime reconnected successfully but not all subscriptions were restored. Error info: ${err}`)

            debug(`_onConnectionLost(): Connection and some subscriptions reinitialized. Connection is back.`)
          })
      })
      .catch(err => {
        if (firstTime)
          _console.call(this, `WARNING: Reconnecting failed. Keeping trying in the background every ${this.settings.reconnectInterval} ms...`)

        //Clear again just incase
        clearTimeout(this._internals.reconnectionTimer)
        this._internals.reconnectionTimer = setTimeout(() => tryToReconnect(false), this.settings.reconnectInterval)
    })
  }

  clearTimeout(this._internals.reconnectionTimer)
  this._internals.reconnectionTimer = setTimeout(() => tryToReconnect(true), this.settings.reconnectInterval)
}

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

I tried to make some flows to test out the behavior beter. I don't thing I have fully replicated this problem, but it definitely shows others. It all runs locally with some rudimentary mocked ads-servers (sometimes not more then a tcp socket).
For me it mostly looks like not everything get destroyed properly when redeploying the flow, as it's getting muliple socket connections at the same time, where you would normally only expect one.

[{"id":"259a1f0f.ec9aa","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"598b9856.bfe018","type":"group","z":"259a1f0f.ec9aa","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["ff2c8bd5.b27ba8","a98ea1d1.f2788","362f9abd.c96b36","1e6f967d.6661fa","22606490.107ebc"],"x":54,"y":39,"w":482,"h":182},{"id":"808aff94.62281","type":"group","z":"259a1f0f.ec9aa","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["2c0308aa.dc4908","9809a269.67fc9","652199a2.f1b0e8","2d1e0da1.3f73b2","9fe8d5b0.0ee298","81052268.66414","efb2fde2.ce57f","4e666842.549ad8","382e5b34.bd0e74","34bfc6bf.f2ffaa"],"x":54,"y":419,"w":992,"h":182},{"id":"ae2ba7f8.e9ccc8","type":"group","z":"259a1f0f.ec9aa","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["36ee781f.0e7538","1b02b308.733e5d","271bd989.ed8326","22cddf85.c0201","ff053df5.a76cf"],"x":54,"y":619,"w":792,"h":182},{"id":"d49e76e4.3996b8","type":"group","z":"259a1f0f.ec9aa","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["af4f0cd3.3c418","949249de.f18a08","b756662.4507d98","4f1f6fa2.a1006","27fa4ec7.f8cff2","d9d0b162.91784","cb80f66b.896cd8"],"x":54,"y":819,"w":812,"h":182},{"id":"f5aa2cae.56ab8","type":"group","z":"259a1f0f.ec9aa","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e4db79b2.52d418","7a07f5f5.b5c1cc","30d4835c.dfac4c","81eee8e1.dbbe48","7058d752.b73148"],"x":54,"y":239,"w":492,"h":162},{"id":"ff2c8bd5.b27ba8","type":"inject","z":"259a1f0f.ec9aa","g":"598b9856.bfe018","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":120,"wires":[["a98ea1d1.f2788"]]},{"id":"a98ea1d1.f2788","type":"ads-client-write-symbol","z":"259a1f0f.ec9aa","g":"598b9856.bfe018","name":"","connection":"55ba3bb8.8b8cc4","variableName":"MAIN.test","autoFill":false,"x":380,"y":120,"wires":[[]]},{"id":"362f9abd.c96b36","type":"comment","z":"259a1f0f.ec9aa","g":"598b9856.bfe018","name":"","info":"When deploying this flow mulitple times, the tcp node show more and more connetions. Meaning the node reconnecting mechanisme isn't destroyed correctly.","x":160,"y":80,"wires":[]},{"id":"1e6f967d.6661fa","type":"tcp in","z":"259a1f0f.ec9aa","g":"598b9856.bfe018","name":"","server":"server","host":"","port":"48988","datamode":"stream","datatype":"buffer","newline":"","topic":"","base64":false,"x":160,"y":180,"wires":[[]]},{"id":"e4db79b2.52d418","type":"ads-client-write-symbol","z":"259a1f0f.ec9aa","g":"f5aa2cae.56ab8","name":"","connection":"365f0b45.6ad694","variableName":"MAIN.test","autoFill":false,"x":390,"y":320,"wires":[[]]},{"id":"7a07f5f5.b5c1cc","type":"inject","z":"259a1f0f.ec9aa","g":"f5aa2cae.56ab8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":320,"wires":[["e4db79b2.52d418"]]},{"id":"30d4835c.dfac4c","type":"tcp in","z":"259a1f0f.ec9aa","g":"f5aa2cae.56ab8","name":"","server":"server","host":"","port":"48872","datamode":"single","datatype":"buffer","newline":"","topic":"","base64":false,"x":160,"y":360,"wires":[[]]},{"id":"81eee8e1.dbbe48","type":"comment","z":"259a1f0f.ec9aa","g":"f5aa2cae.56ab8","name":"","info":"Also here it creates multiple connections when redeploying. Altough these connections connect and disconnect constantly.","x":160,"y":280,"wires":[]},{"id":"2c0308aa.dc4908","type":"ads-client-write-symbol","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","connection":"8865b45e.f078d8","variableName":"MAIN.test","autoFill":false,"x":390,"y":500,"wires":[[]]},{"id":"9809a269.67fc9","type":"inject","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":500,"wires":[["2c0308aa.dc4908"]]},{"id":"652199a2.f1b0e8","type":"tcp in","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","server":"server","host":"","port":"48873","datamode":"stream","datatype":"buffer","newline":"","topic":"","base64":false,"x":140,"y":560,"wires":[["2d1e0da1.3f73b2"]]},{"id":"2d1e0da1.3f73b2","type":"function","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"_parseAmsTcpPacket","func":"let ADS = adsClient.ADS;\n/**\n * Parses an AMS/TCP packet from given (byte) Buffer and then handles it\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpPacket(data) {\n  const packet = {}\n  let parseResult = null\n\n  //1. Parse AMS/TCP header\n  parseResult = _parseAmsTcpHeader(data)\n  packet.amsTcp = parseResult.amsTcp\n  data = parseResult.data\n\n  //2. Parse AMS header (if exists)\n  parseResult = _parseAmsHeader(data)\n  packet.ams = parseResult.ams\n  data = parseResult.data\n\n  //3. Parse ADS data (if exists)\n  packet.ads = (packet.ams.error ? {} : _parseAdsData(packet, data))\n \n  //4. Handle the parsed packet\n  return packet\n}\n\n\n\n\n\n\n\n\n/**\n * Parses an AMS/TCP header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @returns {object} Object {amsTcp, data}, where amsTcp is the parsed header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpHeader(data) {\n\n  let pos = 0\n  const amsTcp = {}\n\n  //0..1 AMS command (header flag)\n  amsTcp.command = data.readUInt16LE(pos)\n  amsTcp.commandStr = ADS.AMS_HEADER_FLAG.toString(amsTcp.command)\n  pos += 2\n\n  //2..5 Data length\n  amsTcp.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS/TCP header from data  \n  data = data.slice(ADS.AMS_TCP_HEADER_LENGTH)\n\n  //If data length is less than AMS_HEADER_LENGTH,\n  //we know that this packet has no AMS headers -> it's only a AMS/TCP command\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    amsTcp.data = data\n\n    //Remove data (basically creates an empty buffer..)\n    data = data.slice(data.byteLength)\n  }\n\n\n  return { amsTcp, data }\n}\n\n\n\n\n/**\n * Parses an AMS header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single AMS packet (without AMS/TCP header)\n * \n * @returns {object} Object {ams, data}, where ams is the parsed AMS header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsHeader(data) {\n\n  let pos = 0\n  const ams = {}\n\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    return {ams, data}\n  }\n  \n  //0..5 Target AMSNetId\n  ams.targetAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //6..8 Target ads port\n  ams.targetAdsPort = data.readUInt16LE(pos)\n  pos += 2\n\n  //8..13 Source AMSNetId\n  ams.sourceAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  ams.sourceAdsPort = data.readUInt16LE(pos)\n  pos += 2\n    \n  //16..17 ADS command\n  ams.adsCommand = data.readUInt16LE(pos)\n  ams.adsCommandStr = ADS.ADS_COMMAND.toString(ams.adsCommand)\n  pos += 2\n\n  //18..19 State flags\n  ams.stateFlags = data.readUInt16LE(pos)\n  ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(ams.stateFlags)\n  pos += 2\n\n  //20..23 Data length\n  ams.dataLength = data.readUInt32LE(pos)\n  pos += 4\n  \n  //24..27 Error code\n  ams.errorCode = data.readUInt32LE(pos)\n  pos += 4\n\n  //28..31 Invoke ID\n  ams.invokeId = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS header from data  \n  data = data.slice(ADS.AMS_HEADER_LENGTH)\n  \n  //ADS error\n  ams.error = (ams.errorCode !== null ? ams.errorCode > 0 : false)\n  ams.errorStr = ''\n  if (ams.error) {\n    ams.errorStr = ADS.ADS_ERROR[ams.errorCode]\n  }\n  \n\n  return {ams, data}\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses ADS data from given (byte) Buffer. Uses packet.ams to determine the ADS command\n * \n * @param {Buffer} data Buffer that contains data for a single ADS packet (without AMS/TCP header and AMS header)\n * \n * @returns {object} Object that contains the parsed ADS data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsData(packet, data) {\n\n  let pos = 0\n  const ads = {}\n\n  if (data.byteLength === 0) {\n    return ads\n  }\n\n  //Saving the raw buffer data to object too\n  ads.rawData = data\n  \n\n  switch (packet.ams.adsCommand) {\n    //-------------- Read Write ---------------\n    case ADS.ADS_COMMAND.ReadWrite:\n    case ADS.ADS_COMMAND.Read:\n           \n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length (bytes)\n      ads.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      ads.data = Buffer.alloc(ads.dataLength)\n      data.copy(ads.data, 0, pos)\n\n      break\n\n\n    \n    //-------------- Write ---------------\n    case ADS.ADS_COMMAND.Write:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    \n    //-------------- Device info ---------------\n    case ADS.ADS_COMMAND.ReadDeviceInfo:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4 Major version\n      ads.data.majorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //5 Minor version\n      ads.data.minorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //6..7 Version build\n      ads.data.versionBuild = data.readUInt16LE(pos)\n      pos += 2\n\n      //8..24 Device name\n      ads.data.deviceName = _trimPlcString(iconv.decode(data.slice(pos, pos + 16), 'cp1252'))\n\n      break\n\n\n    \n    \n    \n    //-------------- Device status ---------------\n    case ADS.ADS_COMMAND.ReadState:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4..5 ADS state\n      ads.data.adsState = data.readUInt16LE(pos)\n      ads.data.adsStateStr = ADS.ADS_STATE.toString(ads.data.adsState)\n      pos += 2\n\n      //6..7 Device state\n      ads.data.deviceState = data.readUInt16LE(pos)\n      pos += 2 \n\n      break\n    \n\n\n    \n    //-------------- Add notification ---------------\n    case ADS.ADS_COMMAND.AddNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n\n      //4..7 Notification handle\n      ads.data.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n\n    \n    //-------------- Delete notification ---------------\n    case ADS.ADS_COMMAND.DeleteNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n    \n    //-------------- Notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      \n      ads.data = _parseAdsNotification(data)\n\n      break\n    \n    \n    \n    //-------------- WriteControl ---------------\n    case ADS.ADS_COMMAND.WriteControl:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    default:\n      //Unknown command, return a custom error\n      debug(`_parseAdsResponse: Unknown ads command in response: ${packet.ams.adsCommand}`)\n\n      ads.error = true\n      ads.errorStr = `Unknown ADS command for parser: ${packet.ams.adsCommand} (${packet.ams.adsCommandStr})`\n      ads.errorCode = -1\n\n      return ads\n      break\n  }\n\n\n  //Ads error code, if exists\n  ads.error = (ads.errorCode !== null ? ads.errorCode > 0 : false)\n\n  if (ads.error) {\n    ads.errorStr = ADS.ADS_ERROR[ads.errorCode]\n  }\n\n  return ads\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles the parsed AMS/TCP packet and actions/callbacks etc. related to it.\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header and if available, also AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAmsTcpPacketReceived(packet) {\n  \n  switch (packet.amsTcp.command) {\n    //-------------- ADS command ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD:\n      packet.amsTcp.commandStr = 'Ads command'\n      \n      _onAdsCommandReceived(packet)\n      break\n    \n    \n    //-------------- AMS/TCP port unregister ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CLOSE:\n      packet.amsTcp.commandStr = 'Port unregister'\n      //TODO: No action at the moment\n      break\n\n    \n\n    \n    //-------------- AMS/TCP port register ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CONNECT:\n      packet.amsTcp.commandStr = 'Port register'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..5 Own AmsNetId\n        localAmsNetId: _byteArrayToAmsNetIdStr(packet.amsTcp.data.slice(0, ADS.AMS_NET_ID_LENGTH)),\n        //5..6 Own assigned ADS port\n        localAdsPort: packet.amsTcp.data.readUInt16LE(ADS.AMS_NET_ID_LENGTH)\n      }\n\n      if (this._internals.amsTcpCallback !== null) {\n        this._internals.amsTcpCallback(packet)\n      } \n      break\n    \n\n\n    //-------------- AMS router note ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_ROUTER_NOTE:\n      packet.amsTcp.commandStr = 'Port router note'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..3 Router state\n        routerState: packet.amsTcp.data.readUInt32LE(0)\n      }\n\n      _onRouterStateChanged(packet)\n\n      break\n    \n\n\n    \n    \n    \n    //-------------- Get local ams net id response ---------------\n    case ADS.AMS_HEADER_FLAG.GET_LOCAL_NETID:\n      packet.amsTcp.commandStr = 'Get local net id'\n      //TODO: No action at the moment\n      break\n    \n    \n    \n    \n    \n    default:\n      packet.amsTcp.commandStr = `Unknown AMS/TCP command ${packet.amsTcp.command}`\n      debug(`_onAmsTcpPacketReceived(): Unknown AMS/TCP command received: \"${packet.amsTcp.command}\" - Doing nothing`)\n      //TODO: No action at the moment\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles incoming ADS commands\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header, AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAdsCommandReceived(packet) {\n\n  switch (packet.ams.adsCommand) {\n\n    //-------------- ADS notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      //Try to find the callback with received notification handles\n      packet.ads.data.stamps.forEach(async stamp => {\n        stamp.samples.forEach(async sample => {\n          \n          if (this._internals.activeSubscriptions[sample.notificationHandle]) {       \n            const sub = this._internals.activeSubscriptions[sample.notificationHandle]\n            debug(`_onAdsCommandReceived(): Notification received for handle \"${sample.notificationHandle}\" (%o)`, sub.target)\n            \n            //First we parse the data from received byte buffer\n            try {\n              const parsedValue = await sub.dataParser(sample.data)\n\n              parsedValue.timeStamp = stamp.timeStamp\n  \n              //Then lets call the users callback\n              sub.callback(\n                parsedValue,\n                sub\n              )\n            } catch (err) {\n              debug(`_onAdsCommandReceived(): Ads notification received but parsing Javascript object failed: %o`, err)\n              this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but parsing data to Javascript object failed. Subscription: ${JSON.stringify(sub)}`, err, sub))\n            }\n\n          } else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but it has unknown notificationHandle (${sample.notificationHandle}). Use unsubscribe() to save resources.`))\n          }\n        })\n      })\n      break\n    \n    \n      //-------------- All other ADS commands ---------------\n    default:\n      //Try to find the callback with received invoke id and call it\n      if (this._internals.activeAdsRequests[packet.ams.invokeId]) {\n        const adsRequest = this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        //Clear timeout\n        clearTimeout(adsRequest.timeoutTimer)\n\n        //Call callback\n        adsRequest.callback(packet)\n      }\n      else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads command received with unknown invokeId \"${packet.ams.invokeId}\".`, packet))\n      }\n\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses received ADS notification data (stamps) from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains ADS notification stamp data\n * \n * @returns {object} Object that contains the parsed ADS notification\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsNotification(data) {\n  let pos = 0\n  const packet = {}\n\n  //0..3 Data length\n  packet.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //4..7 Stamp count\n  packet.stampCount = data.readUInt32LE(pos)\n  pos += 4\n\n  packet.stamps = []\n\n  //Parse all stamps\n  for (let stamp = 0; stamp < packet.stampCount; stamp++) {\n    const newStamp = {}\n\n    //0..7 Timestamp (Converting Windows FILETIME to Date)\n    newStamp.timeStamp = new Date(new long(data.readUInt32LE(pos), data.readUInt32LE(pos+4)).div(10000).sub(11644473600000).toNumber())\n    pos += 8\n\n    //8..11 Number of samples\n    newStamp.count = data.readUInt32LE(pos)\n    pos += 4\n\n    newStamp.samples = []\n\n    //Parse all samples for this stamp\n    for (let sample = 0; sample < newStamp.count; sample++) {\n      const newSample = {}\n\n      //0..3 Notification handle\n      newSample.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length\n      newSample.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      newSample.data = data.slice(pos, pos + newSample.dataLength)\n      pos += newSample.dataLength\n\n      newStamp.samples.push(newSample)\n    }\n    packet.stamps.push(newStamp)\n  }\n\n  return packet\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Sends an ADS command with given data to the PLC\n * \n * @param {number} adsCommand - ADS command to send (see ADS.ADS_COMMAND)\n * @param {Buffer} adsData - Buffer object that contains the data to send\n * @param {number} [targetAdsPort] - Target ADS port - default is this.settings.targetAdsPort\n * @param {string} [targetAmsNetId] - Target AmsNetID - default is this.settings.targetAmsNetId\n * \n * @returns {Promise<object>} Returns a promise (async function)\n * - If resolved, command was sent successfully and response was received. The received reponse is parsed and returned (object)\n * - If rejected, sending, receiving or parsing failed and error info is returned (object)\n * \n * @memberof _LibraryInternals\n */\nfunction _sendAdsCommand(adsCommand, adsData, targetAdsPort = null, targetAmsNetId = null) {\n  return new Promise(async (resolve, reject) => {\n    \n    //Check that next free invoke ID is below 32 bit integer maximum\n    if (this._internals.nextInvokeId >= ADS.ADS_INVOKE_ID_MAX_VALUE)\n      this._internals.nextInvokeId = 0\n    \n    //Creating the data packet object\n    const packet = {\n      amsTcp: {\n        command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD,\n        commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD)\n      },\n      ams: {\n        targetAmsNetId: (targetAmsNetId == null ? this.connection.targetAmsNetId : targetAmsNetId),\n        targetAdsPort: (targetAdsPort == null ? this.connection.targetAdsPort : targetAdsPort),\n        sourceAmsNetId: this.connection.localAmsNetId,\n        sourceAdsPort: this.connection.localAdsPort,\n        adsCommand: adsCommand,\n        adsCommandStr: ADS.ADS_COMMAND.toString(adsCommand),\n        stateFlags: ADS.ADS_STATE_FLAGS.AdsCommand,\n        stateFlagsStr: '',\n        dataLength: adsData.byteLength,\n        errorCode: 0,\n        invokeId: this._internals.nextInvokeId++\n      },\n      ads: {\n        rawData: adsData\n      }\n    }\n    packet.ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(packet.ams.stateFlags)\n\n    \n\n    //Creating a full AMS/TCP request\n    try {\n      var request = _createAmsTcpRequest(packet)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', err))\n    }\n\n    //Registering callback for response handling\n    this._internals.activeAdsRequests[packet.ams.invokeId] = {\n      callback: function (response) {\n  \n        //Callback is no longer needed, delete it\n        delete this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        if (response.ams.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with AMS error received', response))\n        } else if (response.ads.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with ADS error received', response))\n        }\n\n        return resolve(response)\n      },\n\n      timeoutTimer: setTimeout(function (client) {\n        debug(`_sendAdsCommand(): Timeout for command ${packet.ams.adsCommandStr} with invokeId ${packet.ams.invokeId} - No response`)\n\n        //Callback is no longer needed, delete it\n        delete client._internals.activeAdsRequests[packet.ams.invokeId]\n        \n        //Create a custom \"ads error\" so that the info is passed onwards\n        const adsError = {\n          ads: {\n            error: true,\n            errorCode: -1,\n            errorStr: `Timeout - no response in ${client.settings.timeoutDelay} ms`\n          }\n        }\n        return reject(new ClientException(this, '_sendAdsCommand()', `Timeout - no response in ${client.settings.timeoutDelay} ms`, adsError))\n\n      }, this.settings.timeoutDelay, this)\n    }\n\n    //Write the data \n    try {\n      _socketWrite(request)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', `Error - Socket is not available`, err))\n    }\n  })\n}\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP request from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Full created AMS/TCP request as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpRequest(packet) {\n  //1. Create ADS data\n  const adsData = packet.ads.rawData\n  \n  //2. Create AMS header\n  const amsHeader = _createAmsHeader(packet)\n\n  //3. Create AMS/TCP header\n  const amsTcpHeader = _createAmsTcpHeader(packet, amsHeader)\n\n  //4. Create full AMS/TCP packet\n  const amsTcpRequest = Buffer.concat([amsTcpHeader, amsHeader, adsData])\n\n  \n  return amsTcpRequest\n}\n\n\n\n\n\n\n\n    \n\n\n/**\n * Creates an AMS header from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Created AMS header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsHeader(packet) {\n  //Allocating bytes for AMS header\n  const header = Buffer.alloc(ADS.AMS_HEADER_LENGTH)\n  let pos = 0\n\n  //0..5 Target AMSNetId\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.targetAmsNetId)).copy(header, 0)\n  pos += ADS.AMS_NET_ID_LENGTH\n  \n  //6..8 Target ads port\n  header.writeUInt16LE(packet.ams.targetAdsPort, pos)\n  pos += 2\n  \n  //8..13 Source ads port\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.sourceAmsNetId)).copy(header, pos)\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  header.writeUInt16LE(packet.ams.sourceAdsPort, pos)\n  pos += 2\n\n  //16..17 ADS command\n  header.writeUInt16LE(packet.ams.adsCommand, pos)\n  pos += 2\n  \n  //18..19 State flags\n  header.writeUInt16LE(packet.ams.stateFlags, pos)\n  pos += 2\n  \n  //20..23 Data length\n  header.writeUInt32LE(packet.ams.dataLength, pos)\n  pos += 4\n  \n  //24..27 Error code\n  header.writeUInt32LE(packet.ams.errorCode, pos)\n  pos += 4\n  \n  //28..31 Invoke ID\n  header.writeUInt32LE(packet.ams.invokeId, pos)\n  pos += 4\n\n \n  if (debugIO.enabled) {\n    debugIO(`_createAmsHeader(): AMS header created: %o`, header.toString('hex'))\n  }\n\n  return header\n}\n\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP header from given packet and AMS header\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * @param {Buffer} amsHeader Buffer containing the previously created AMS header\n * \n * @returns {Buffer} Created AMS/TCP header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpHeader(packet, amsHeader) {\n  //Allocating bytes for AMS/TCP header\n  const header = Buffer.alloc(ADS.AMS_TCP_HEADER_LENGTH)\n  let pos = 0\n\n  //0..1 AMS command (header flag)\n  header.writeUInt16LE(packet.amsTcp.command, pos)\n  pos += 2\n\n  //2..5 Data length\n  header.writeUInt32LE(amsHeader.byteLength + packet.ams.dataLength, pos)\n  pos += 4\n\n \n  if (debugIO.enabled) {\n    debugIO(`_createAmsTcpHeader(): AMS/TCP header created: %o`, header.toString('hex'))\n  }\n\n  return header\n}\n\n\n\n\n\n\n\n/**\n * Writes given message to console if settings.hideConsoleWarnings is false\n * \n * @param {string} str Message to console.log() \n * \n * @memberof _LibraryInternals\n */\nfunction _console(str) {\n  if (this.settings.hideConsoleWarnings !== true)\n    console.log(`${PACKAGE_NAME}: ${str}`)\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * **Helper:** Marges objects together recursively. Used for example at writeSymbol() to combine active values and new (uncomplete) object values\n * \n * Based on https://stackoverflow.com/a/34749873/8140625 by Salakar and https://stackoverflow.com/a/49727784/8140625\n * \n * Later modified to work with in both case-sensitive and case-insensitive ways (as the PLC is case-insensitive too). \n * Also fixed isObjectOrArray to return true only when input is object literal {} or array (https://stackoverflow.com/a/16608074/8140625)\n * \n * @param {boolean} isCaseSensitive True = Object keys are merged case-sensitively --> target['key'] !== target['KEY'], false = case-insensitively\n * @param {object} target Target object to copy data to\n * @param {...object} sources Source objects to copy data from\n * \n * @returns {object} Merged object\n * \n * @memberof _LibraryInternals\n */\n\nfunction _deepMergeObjects(isCaseSensitive, target, ...sources) {\n  if (!sources.length) return target\n  \n  const isObjectOrArray = (item) => {\n    return (!!item) && ((item.constructor === Object) || Array.isArray(item))\n  }\n  \n  //Checks if object key exists\n  const keyExists = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return !!Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())\n    } else {\n      return (obj[key] != null)\n    }\n  }\n\n  //Returns object value by key\n  const getValue = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return obj[Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())]\n    } else {\n      return obj[key]\n    }\n  }\n\n  //Sets object value obj[key] to value - If isCaseSensitive == false, obj[KeY] == obj[key]\n  const setValue = (obj, key, value, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      Object.keys(obj).forEach(objKey => {\n        if (objKey.toLowerCase() === key.toLowerCase()) {\n          obj[objKey] = value\n        }\n      });\n    } else {\n      //Case-sensitive is easy\n      obj[key] = value\n    }\n  }\n\n  const source = sources.shift()\n\n  if (isObjectOrArray(target) && isObjectOrArray(source)) {    \n    for (const key in source) {\n\n      if (isObjectOrArray(source[key])) {\n        //If source is object\n        if (keyExists(target, key, isCaseSensitive)) { \n          //Target has this key, copy value\n          setValue(target, key, Object.assign({}, target[key]), isCaseSensitive)\n        } else {       \n          //Target doesn't have this key, add it)\n          Object.assign(target, { [key]: {} })\n        }\n        //As this is an object, go through it recursively\n        _deepMergeObjects(isCaseSensitive, getValue(target, key, false), source[key])\n\n      } else {\n        //Source is not object\n\n        if (keyExists(target, key, isCaseSensitive)) {\n          //Target has this key, copy value\n          setValue(target, key, source[key], isCaseSensitive)\n        } else {\n          //Target doesn't have this key, add it\n          Object.assign(target, { [key]: source[key] })\n        }\n      }\n    }\n  }\n  \n  return _deepMergeObjects(isCaseSensitive, target, ...sources)\n}\n\n\n\n\n\n\n\n/**\n * **Helper:** Trims the given PLC string until en mark (\\0, 0 byte) is found\n * \n * @param {string} plcString String to trim\n * \n * @returns {string} Trimmed string\n * \n * @memberof _LibraryInternals\n */\nfunction _trimPlcString(plcString) {\n  let parsedStr = ''\n\n  for (let i = 0; i < plcString.length; i++) {\n    if (plcString.charCodeAt(i) === 0) break\n\n    parsedStr += plcString[i]\n  }\n\n  return parsedStr\n}\n\n\n\n\n\n\n/**\n * **Helper:** Converts byte array (Buffer) to AmsNetId string\n * \n * @param {Buffer|array} byteArray Buffer/array that contains AmsNetId bytes\n * \n * @returns {string} AmsNetId as string\n * \n * @memberof _LibraryInternals\n */\nfunction _byteArrayToAmsNetIdStr(byteArray) {\n  return byteArray.join('.')\n}\n\n\n\n\n/**\n * **Helper:** Converts AmsNetId string to byte array\n * \n * @param {string} byteArray String that represents an AmsNetId\n * \n * @returns {array} AmsNetId as array\n * \n * @memberof _LibraryInternals\n */\nfunction _amsNetIdStrToByteArray(str) {\n  return str.split('.').map(x => parseInt(x))\n}\n\n\n\n///------------------------------//\nmsg.payload = _parseAmsTcpPacket(msg.payload);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"adsClient","module":"ads-client"}],"x":320,"y":560,"wires":[["9fe8d5b0.0ee298"]]},{"id":"9fe8d5b0.0ee298","type":"switch","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","property":"payload.ams.adsCommandStr","propertyType":"msg","rules":[{"t":"eq","v":"ReadState","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":490,"y":560,"wires":[["81052268.66414"]]},{"id":"81052268.66414","type":"switch","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","property":"payload.ams.targetAdsPort","propertyType":"msg","rules":[{"t":"eq","v":"10000","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":610,"y":560,"wires":[["efb2fde2.ce57f"]]},{"id":"efb2fde2.ce57f","type":"function","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"_createAmsTcpRequest","func":"let ADS = adsClient.ADS;\n/**\n * Parses an AMS/TCP packet from given (byte) Buffer and then handles it\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpPacket(data) {\n  const packet = {}\n  let parseResult = null\n\n  //1. Parse AMS/TCP header\n  parseResult = _parseAmsTcpHeader(data)\n  packet.amsTcp = parseResult.amsTcp\n  data = parseResult.data\n\n  //2. Parse AMS header (if exists)\n  parseResult = _parseAmsHeader(data)\n  packet.ams = parseResult.ams\n  data = parseResult.data\n\n  //3. Parse ADS data (if exists)\n  packet.ads = (packet.ams.error ? {} : _parseAdsData(packet, data))\n \n  //4. Handle the parsed packet\n  return packet\n}\n\n\n\n\n\n\n\n\n/**\n * Parses an AMS/TCP header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @returns {object} Object {amsTcp, data}, where amsTcp is the parsed header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpHeader(data) {\n\n  let pos = 0\n  const amsTcp = {}\n\n  //0..1 AMS command (header flag)\n  amsTcp.command = data.readUInt16LE(pos)\n  amsTcp.commandStr = ADS.AMS_HEADER_FLAG.toString(amsTcp.command)\n  pos += 2\n\n  //2..5 Data length\n  amsTcp.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS/TCP header from data  \n  data = data.slice(ADS.AMS_TCP_HEADER_LENGTH)\n\n  //If data length is less than AMS_HEADER_LENGTH,\n  //we know that this packet has no AMS headers -> it's only a AMS/TCP command\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    amsTcp.data = data\n\n    //Remove data (basically creates an empty buffer..)\n    data = data.slice(data.byteLength)\n  }\n\n\n  return { amsTcp, data }\n}\n\n\n\n\n/**\n * Parses an AMS header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single AMS packet (without AMS/TCP header)\n * \n * @returns {object} Object {ams, data}, where ams is the parsed AMS header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsHeader(data) {\n\n  let pos = 0\n  const ams = {}\n\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    return {ams, data}\n  }\n  \n  //0..5 Target AMSNetId\n  ams.targetAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //6..8 Target ads port\n  ams.targetAdsPort = data.readUInt16LE(pos)\n  pos += 2\n\n  //8..13 Source AMSNetId\n  ams.sourceAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  ams.sourceAdsPort = data.readUInt16LE(pos)\n  pos += 2\n    \n  //16..17 ADS command\n  ams.adsCommand = data.readUInt16LE(pos)\n  ams.adsCommandStr = ADS.ADS_COMMAND.toString(ams.adsCommand)\n  pos += 2\n\n  //18..19 State flags\n  ams.stateFlags = data.readUInt16LE(pos)\n  ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(ams.stateFlags)\n  pos += 2\n\n  //20..23 Data length\n  ams.dataLength = data.readUInt32LE(pos)\n  pos += 4\n  \n  //24..27 Error code\n  ams.errorCode = data.readUInt32LE(pos)\n  pos += 4\n\n  //28..31 Invoke ID\n  ams.invokeId = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS header from data  \n  data = data.slice(ADS.AMS_HEADER_LENGTH)\n  \n  //ADS error\n  ams.error = (ams.errorCode !== null ? ams.errorCode > 0 : false)\n  ams.errorStr = ''\n  if (ams.error) {\n    ams.errorStr = ADS.ADS_ERROR[ams.errorCode]\n  }\n  \n\n  return {ams, data}\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses ADS data from given (byte) Buffer. Uses packet.ams to determine the ADS command\n * \n * @param {Buffer} data Buffer that contains data for a single ADS packet (without AMS/TCP header and AMS header)\n * \n * @returns {object} Object that contains the parsed ADS data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsData(packet, data) {\n\n  let pos = 0\n  const ads = {}\n\n  if (data.byteLength === 0) {\n    return ads\n  }\n\n  //Saving the raw buffer data to object too\n  ads.rawData = data\n  \n\n  switch (packet.ams.adsCommand) {\n    //-------------- Read Write ---------------\n    case ADS.ADS_COMMAND.ReadWrite:\n    case ADS.ADS_COMMAND.Read:\n           \n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length (bytes)\n      ads.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      ads.data = Buffer.alloc(ads.dataLength)\n      data.copy(ads.data, 0, pos)\n\n      break\n\n\n    \n    //-------------- Write ---------------\n    case ADS.ADS_COMMAND.Write:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    \n    //-------------- Device info ---------------\n    case ADS.ADS_COMMAND.ReadDeviceInfo:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4 Major version\n      ads.data.majorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //5 Minor version\n      ads.data.minorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //6..7 Version build\n      ads.data.versionBuild = data.readUInt16LE(pos)\n      pos += 2\n\n      //8..24 Device name\n      ads.data.deviceName = _trimPlcString(iconv.decode(data.slice(pos, pos + 16), 'cp1252'))\n\n      break\n\n\n    \n    \n    \n    //-------------- Device status ---------------\n    case ADS.ADS_COMMAND.ReadState:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4..5 ADS state\n      ads.data.adsState = data.readUInt16LE(pos)\n      ads.data.adsStateStr = ADS.ADS_STATE.toString(ads.data.adsState)\n      pos += 2\n\n      //6..7 Device state\n      ads.data.deviceState = data.readUInt16LE(pos)\n      pos += 2 \n\n      break\n    \n\n\n    \n    //-------------- Add notification ---------------\n    case ADS.ADS_COMMAND.AddNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n\n      //4..7 Notification handle\n      ads.data.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n\n    \n    //-------------- Delete notification ---------------\n    case ADS.ADS_COMMAND.DeleteNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n    \n    //-------------- Notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      \n      ads.data = _parseAdsNotification(data)\n\n      break\n    \n    \n    \n    //-------------- WriteControl ---------------\n    case ADS.ADS_COMMAND.WriteControl:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    default:\n      //Unknown command, return a custom error\n      debug(`_parseAdsResponse: Unknown ads command in response: ${packet.ams.adsCommand}`)\n\n      ads.error = true\n      ads.errorStr = `Unknown ADS command for parser: ${packet.ams.adsCommand} (${packet.ams.adsCommandStr})`\n      ads.errorCode = -1\n\n      return ads\n      break\n  }\n\n\n  //Ads error code, if exists\n  ads.error = (ads.errorCode !== null ? ads.errorCode > 0 : false)\n\n  if (ads.error) {\n    ads.errorStr = ADS.ADS_ERROR[ads.errorCode]\n  }\n\n  return ads\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles the parsed AMS/TCP packet and actions/callbacks etc. related to it.\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header and if available, also AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAmsTcpPacketReceived(packet) {\n  \n  switch (packet.amsTcp.command) {\n    //-------------- ADS command ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD:\n      packet.amsTcp.commandStr = 'Ads command'\n      \n      _onAdsCommandReceived(packet)\n      break\n    \n    \n    //-------------- AMS/TCP port unregister ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CLOSE:\n      packet.amsTcp.commandStr = 'Port unregister'\n      //TODO: No action at the moment\n      break\n\n    \n\n    \n    //-------------- AMS/TCP port register ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CONNECT:\n      packet.amsTcp.commandStr = 'Port register'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..5 Own AmsNetId\n        localAmsNetId: _byteArrayToAmsNetIdStr(packet.amsTcp.data.slice(0, ADS.AMS_NET_ID_LENGTH)),\n        //5..6 Own assigned ADS port\n        localAdsPort: packet.amsTcp.data.readUInt16LE(ADS.AMS_NET_ID_LENGTH)\n      }\n\n      if (this._internals.amsTcpCallback !== null) {\n        this._internals.amsTcpCallback(packet)\n      } \n      break\n    \n\n\n    //-------------- AMS router note ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_ROUTER_NOTE:\n      packet.amsTcp.commandStr = 'Port router note'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..3 Router state\n        routerState: packet.amsTcp.data.readUInt32LE(0)\n      }\n\n      _onRouterStateChanged(packet)\n\n      break\n    \n\n\n    \n    \n    \n    //-------------- Get local ams net id response ---------------\n    case ADS.AMS_HEADER_FLAG.GET_LOCAL_NETID:\n      packet.amsTcp.commandStr = 'Get local net id'\n      //TODO: No action at the moment\n      break\n    \n    \n    \n    \n    \n    default:\n      packet.amsTcp.commandStr = `Unknown AMS/TCP command ${packet.amsTcp.command}`\n      debug(`_onAmsTcpPacketReceived(): Unknown AMS/TCP command received: \"${packet.amsTcp.command}\" - Doing nothing`)\n      //TODO: No action at the moment\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles incoming ADS commands\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header, AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAdsCommandReceived(packet) {\n\n  switch (packet.ams.adsCommand) {\n\n    //-------------- ADS notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      //Try to find the callback with received notification handles\n      packet.ads.data.stamps.forEach(async stamp => {\n        stamp.samples.forEach(async sample => {\n          \n          if (this._internals.activeSubscriptions[sample.notificationHandle]) {       \n            const sub = this._internals.activeSubscriptions[sample.notificationHandle]\n            debug(`_onAdsCommandReceived(): Notification received for handle \"${sample.notificationHandle}\" (%o)`, sub.target)\n            \n            //First we parse the data from received byte buffer\n            try {\n              const parsedValue = await sub.dataParser(sample.data)\n\n              parsedValue.timeStamp = stamp.timeStamp\n  \n              //Then lets call the users callback\n              sub.callback(\n                parsedValue,\n                sub\n              )\n            } catch (err) {\n              debug(`_onAdsCommandReceived(): Ads notification received but parsing Javascript object failed: %o`, err)\n              this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but parsing data to Javascript object failed. Subscription: ${JSON.stringify(sub)}`, err, sub))\n            }\n\n          } else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but it has unknown notificationHandle (${sample.notificationHandle}). Use unsubscribe() to save resources.`))\n          }\n        })\n      })\n      break\n    \n    \n      //-------------- All other ADS commands ---------------\n    default:\n      //Try to find the callback with received invoke id and call it\n      if (this._internals.activeAdsRequests[packet.ams.invokeId]) {\n        const adsRequest = this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        //Clear timeout\n        clearTimeout(adsRequest.timeoutTimer)\n\n        //Call callback\n        adsRequest.callback(packet)\n      }\n      else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads command received with unknown invokeId \"${packet.ams.invokeId}\".`, packet))\n      }\n\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses received ADS notification data (stamps) from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains ADS notification stamp data\n * \n * @returns {object} Object that contains the parsed ADS notification\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsNotification(data) {\n  let pos = 0\n  const packet = {}\n\n  //0..3 Data length\n  packet.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //4..7 Stamp count\n  packet.stampCount = data.readUInt32LE(pos)\n  pos += 4\n\n  packet.stamps = []\n\n  //Parse all stamps\n  for (let stamp = 0; stamp < packet.stampCount; stamp++) {\n    const newStamp = {}\n\n    //0..7 Timestamp (Converting Windows FILETIME to Date)\n    newStamp.timeStamp = new Date(new long(data.readUInt32LE(pos), data.readUInt32LE(pos+4)).div(10000).sub(11644473600000).toNumber())\n    pos += 8\n\n    //8..11 Number of samples\n    newStamp.count = data.readUInt32LE(pos)\n    pos += 4\n\n    newStamp.samples = []\n\n    //Parse all samples for this stamp\n    for (let sample = 0; sample < newStamp.count; sample++) {\n      const newSample = {}\n\n      //0..3 Notification handle\n      newSample.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length\n      newSample.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      newSample.data = data.slice(pos, pos + newSample.dataLength)\n      pos += newSample.dataLength\n\n      newStamp.samples.push(newSample)\n    }\n    packet.stamps.push(newStamp)\n  }\n\n  return packet\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Sends an ADS command with given data to the PLC\n * \n * @param {number} adsCommand - ADS command to send (see ADS.ADS_COMMAND)\n * @param {Buffer} adsData - Buffer object that contains the data to send\n * @param {number} [targetAdsPort] - Target ADS port - default is this.settings.targetAdsPort\n * @param {string} [targetAmsNetId] - Target AmsNetID - default is this.settings.targetAmsNetId\n * \n * @returns {Promise<object>} Returns a promise (async function)\n * - If resolved, command was sent successfully and response was received. The received reponse is parsed and returned (object)\n * - If rejected, sending, receiving or parsing failed and error info is returned (object)\n * \n * @memberof _LibraryInternals\n */\nfunction _sendAdsCommand(adsCommand, adsData, targetAdsPort = null, targetAmsNetId = null) {\n  return new Promise(async (resolve, reject) => {\n    \n    //Check that next free invoke ID is below 32 bit integer maximum\n    if (this._internals.nextInvokeId >= ADS.ADS_INVOKE_ID_MAX_VALUE)\n      this._internals.nextInvokeId = 0\n    \n    //Creating the data packet object\n    const packet = {\n      amsTcp: {\n        command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD,\n        commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD)\n      },\n      ams: {\n        targetAmsNetId: (targetAmsNetId == null ? this.connection.targetAmsNetId : targetAmsNetId),\n        targetAdsPort: (targetAdsPort == null ? this.connection.targetAdsPort : targetAdsPort),\n        sourceAmsNetId: this.connection.localAmsNetId,\n        sourceAdsPort: this.connection.localAdsPort,\n        adsCommand: adsCommand,\n        adsCommandStr: ADS.ADS_COMMAND.toString(adsCommand),\n        stateFlags: ADS.ADS_STATE_FLAGS.AdsCommand,\n        stateFlagsStr: '',\n        dataLength: adsData.byteLength,\n        errorCode: 0,\n        invokeId: this._internals.nextInvokeId++\n      },\n      ads: {\n        rawData: adsData\n      }\n    }\n    packet.ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(packet.ams.stateFlags)\n\n    \n\n    //Creating a full AMS/TCP request\n    try {\n      var request = _createAmsTcpRequest(packet)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', err))\n    }\n\n    //Registering callback for response handling\n    this._internals.activeAdsRequests[packet.ams.invokeId] = {\n      callback: function (response) {\n  \n        //Callback is no longer needed, delete it\n        delete this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        if (response.ams.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with AMS error received', response))\n        } else if (response.ads.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with ADS error received', response))\n        }\n\n        return resolve(response)\n      },\n\n      timeoutTimer: setTimeout(function (client) {\n        debug(`_sendAdsCommand(): Timeout for command ${packet.ams.adsCommandStr} with invokeId ${packet.ams.invokeId} - No response`)\n\n        //Callback is no longer needed, delete it\n        delete client._internals.activeAdsRequests[packet.ams.invokeId]\n        \n        //Create a custom \"ads error\" so that the info is passed onwards\n        const adsError = {\n          ads: {\n            error: true,\n            errorCode: -1,\n            errorStr: `Timeout - no response in ${client.settings.timeoutDelay} ms`\n          }\n        }\n        return reject(new ClientException(this, '_sendAdsCommand()', `Timeout - no response in ${client.settings.timeoutDelay} ms`, adsError))\n\n      }, this.settings.timeoutDelay, this)\n    }\n\n    //Write the data \n    try {\n      _socketWrite(request)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', `Error - Socket is not available`, err))\n    }\n  })\n}\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP request from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Full created AMS/TCP request as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpRequest(packet) {\n  //1. Create ADS data\n  const adsData = packet.ads.rawData\n  \n  //2. Create AMS header\n  const amsHeader = _createAmsHeader(packet)\n\n  //3. Create AMS/TCP header\n  const amsTcpHeader = _createAmsTcpHeader(packet, amsHeader)\n\n  //4. Create full AMS/TCP packet\n  const amsTcpRequest = Buffer.concat([amsTcpHeader, amsHeader, adsData])\n\n  \n  return amsTcpRequest\n}\n\n\n\n\n\n\n\n    \n\n\n/**\n * Creates an AMS header from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Created AMS header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsHeader(packet) {\n  //Allocating bytes for AMS header\n  const header = Buffer.alloc(ADS.AMS_HEADER_LENGTH)\n  let pos = 0\n\n  //0..5 Target AMSNetId\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.targetAmsNetId)).copy(header, 0)\n  pos += ADS.AMS_NET_ID_LENGTH\n  \n  //6..8 Target ads port\n  header.writeUInt16LE(packet.ams.targetAdsPort, pos)\n  pos += 2\n  \n  //8..13 Source ads port\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.sourceAmsNetId)).copy(header, pos)\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  header.writeUInt16LE(packet.ams.sourceAdsPort, pos)\n  pos += 2\n\n  //16..17 ADS command\n  header.writeUInt16LE(packet.ams.adsCommand, pos)\n  pos += 2\n  \n  //18..19 State flags\n  header.writeUInt16LE(packet.ams.stateFlags, pos)\n  pos += 2\n  \n  //20..23 Data length\n  header.writeUInt32LE(packet.ams.dataLength, pos)\n  pos += 4\n  \n  //24..27 Error code\n  header.writeUInt32LE(packet.ams.errorCode, pos)\n  pos += 4\n  \n  //28..31 Invoke ID\n  header.writeUInt32LE(packet.ams.invokeId, pos)\n  pos += 4\n\n\n\n  return header\n}\n\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP header from given packet and AMS header\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * @param {Buffer} amsHeader Buffer containing the previously created AMS header\n * \n * @returns {Buffer} Created AMS/TCP header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpHeader(packet, amsHeader) {\n  //Allocating bytes for AMS/TCP header\n  const header = Buffer.alloc(ADS.AMS_TCP_HEADER_LENGTH)\n  let pos = 0\n\n  //0..1 AMS command (header flag)\n  header.writeUInt16LE(packet.amsTcp.command, pos)\n  pos += 2\n\n  //2..5 Data length\n  header.writeUInt32LE(amsHeader.byteLength + packet.ams.dataLength, pos)\n  pos += 4\n\n\n  return header\n}\n\n\n\n\n\n\n\n/**\n * Writes given message to console if settings.hideConsoleWarnings is false\n * \n * @param {string} str Message to console.log() \n * \n * @memberof _LibraryInternals\n */\nfunction _console(str) {\n  if (this.settings.hideConsoleWarnings !== true)\n    console.log(`${PACKAGE_NAME}: ${str}`)\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * **Helper:** Marges objects together recursively. Used for example at writeSymbol() to combine active values and new (uncomplete) object values\n * \n * Based on https://stackoverflow.com/a/34749873/8140625 by Salakar and https://stackoverflow.com/a/49727784/8140625\n * \n * Later modified to work with in both case-sensitive and case-insensitive ways (as the PLC is case-insensitive too). \n * Also fixed isObjectOrArray to return true only when input is object literal {} or array (https://stackoverflow.com/a/16608074/8140625)\n * \n * @param {boolean} isCaseSensitive True = Object keys are merged case-sensitively --> target['key'] !== target['KEY'], false = case-insensitively\n * @param {object} target Target object to copy data to\n * @param {...object} sources Source objects to copy data from\n * \n * @returns {object} Merged object\n * \n * @memberof _LibraryInternals\n */\n\nfunction _deepMergeObjects(isCaseSensitive, target, ...sources) {\n  if (!sources.length) return target\n  \n  const isObjectOrArray = (item) => {\n    return (!!item) && ((item.constructor === Object) || Array.isArray(item))\n  }\n  \n  //Checks if object key exists\n  const keyExists = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return !!Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())\n    } else {\n      return (obj[key] != null)\n    }\n  }\n\n  //Returns object value by key\n  const getValue = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return obj[Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())]\n    } else {\n      return obj[key]\n    }\n  }\n\n  //Sets object value obj[key] to value - If isCaseSensitive == false, obj[KeY] == obj[key]\n  const setValue = (obj, key, value, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      Object.keys(obj).forEach(objKey => {\n        if (objKey.toLowerCase() === key.toLowerCase()) {\n          obj[objKey] = value\n        }\n      });\n    } else {\n      //Case-sensitive is easy\n      obj[key] = value\n    }\n  }\n\n  const source = sources.shift()\n\n  if (isObjectOrArray(target) && isObjectOrArray(source)) {    \n    for (const key in source) {\n\n      if (isObjectOrArray(source[key])) {\n        //If source is object\n        if (keyExists(target, key, isCaseSensitive)) { \n          //Target has this key, copy value\n          setValue(target, key, Object.assign({}, target[key]), isCaseSensitive)\n        } else {       \n          //Target doesn't have this key, add it)\n          Object.assign(target, { [key]: {} })\n        }\n        //As this is an object, go through it recursively\n        _deepMergeObjects(isCaseSensitive, getValue(target, key, false), source[key])\n\n      } else {\n        //Source is not object\n\n        if (keyExists(target, key, isCaseSensitive)) {\n          //Target has this key, copy value\n          setValue(target, key, source[key], isCaseSensitive)\n        } else {\n          //Target doesn't have this key, add it\n          Object.assign(target, { [key]: source[key] })\n        }\n      }\n    }\n  }\n  \n  return _deepMergeObjects(isCaseSensitive, target, ...sources)\n}\n\n\n\n\n\n\n\n/**\n * **Helper:** Trims the given PLC string until en mark (\\0, 0 byte) is found\n * \n * @param {string} plcString String to trim\n * \n * @returns {string} Trimmed string\n * \n * @memberof _LibraryInternals\n */\nfunction _trimPlcString(plcString) {\n  let parsedStr = ''\n\n  for (let i = 0; i < plcString.length; i++) {\n    if (plcString.charCodeAt(i) === 0) break\n\n    parsedStr += plcString[i]\n  }\n\n  return parsedStr\n}\n\n\n\n\n\n\n/**\n * **Helper:** Converts byte array (Buffer) to AmsNetId string\n * \n * @param {Buffer|array} byteArray Buffer/array that contains AmsNetId bytes\n * \n * @returns {string} AmsNetId as string\n * \n * @memberof _LibraryInternals\n */\nfunction _byteArrayToAmsNetIdStr(byteArray) {\n  return byteArray.join('.')\n}\n\n\n\n\n/**\n * **Helper:** Converts AmsNetId string to byte array\n * \n * @param {string} byteArray String that represents an AmsNetId\n * \n * @returns {array} AmsNetId as array\n * \n * @memberof _LibraryInternals\n */\nfunction _amsNetIdStrToByteArray(str) {\n  return str.split('.').map(x => parseInt(x))\n}\n\n\n\n///------------------------------//\n//Creating the data packet object\n    const packet = {\n      amsTcp: {\n        command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD,\n        commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD)\n      },\n      ams: {\n        targetAmsNetId: msg.payload.ams.sourceAmsNetId,\n        targetAdsPort: msg.payload.ams.sourceAdsPort,\n        sourceAmsNetId: msg.payload.ams.targetAmsNetId,\n        sourceAdsPort: msg.payload.ams.targetAdsPort,\n        adsCommand: 4,\n        adsCommandStr: ADS.ADS_COMMAND.toString(4),\n        stateFlags: 5,\n        stateFlagsStr: '',\n        dataLength: 8,\n        errorCode: 0,\n      },\n      ads: {\n        rawData: new Buffer.from([ 0, 0, 0, 0, 5, 0, 1, 0])\n      }\n    }\n\n\n\nmsg.payload = _createAmsTcpRequest(packet);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"adsClient","module":"ads-client"}],"x":790,"y":560,"wires":[["4e666842.549ad8"]]},{"id":"4e666842.549ad8","type":"tcp out","z":"259a1f0f.ec9aa","g":"808aff94.62281","host":"","port":"","beserver":"reply","base64":false,"end":true,"name":"","x":970,"y":560,"wires":[]},{"id":"382e5b34.bd0e74","type":"comment","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","info":"Attempt to let the ads-client believe it connects to a working system and see how it handles it.\n\nWhy does it initially show connected in the \"connetion status\" node, and then never again?","x":140,"y":460,"wires":[]},{"id":"34bfc6bf.f2ffaa","type":"ads-client-connection-status","z":"259a1f0f.ec9aa","g":"808aff94.62281","name":"","connection":"8865b45e.f078d8","showInput":false,"inputs":0,"x":690,"y":500,"wires":[[]]},{"id":"7058d752.b73148","type":"ads-client-connection-status","z":"259a1f0f.ec9aa","g":"f5aa2cae.56ab8","name":"","connection":"365f0b45.6ad694","showInput":false,"inputs":0,"x":390,"y":360,"wires":[[]]},{"id":"22606490.107ebc","type":"ads-client-connection-status","z":"259a1f0f.ec9aa","g":"598b9856.bfe018","name":"","connection":"55ba3bb8.8b8cc4","showInput":false,"inputs":0,"x":370,"y":180,"wires":[[]]},{"id":"36ee781f.0e7538","type":"ads-client-write-symbol","z":"259a1f0f.ec9aa","g":"ae2ba7f8.e9ccc8","name":"","connection":"c7cffb0d.36f128","variableName":"MAIN.test","autoFill":false,"x":390,"y":700,"wires":[[]]},{"id":"1b02b308.733e5d","type":"inject","z":"259a1f0f.ec9aa","g":"ae2ba7f8.e9ccc8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":700,"wires":[["36ee781f.0e7538"]]},{"id":"271bd989.ed8326","type":"tcp out","z":"259a1f0f.ec9aa","g":"ae2ba7f8.e9ccc8","host":"","port":"48874","beserver":"server","base64":false,"end":true,"name":"","x":620,"y":760,"wires":[]},{"id":"22cddf85.c0201","type":"comment","z":"259a1f0f.ec9aa","g":"ae2ba7f8.e9ccc8","name":"","info":"Why does the sockets increase so fast?","x":140,"y":660,"wires":[]},{"id":"ff053df5.a76cf","type":"ads-client-connection-status","z":"259a1f0f.ec9aa","g":"ae2ba7f8.e9ccc8","name":"","connection":"c7cffb0d.36f128","showInput":false,"inputs":0,"x":690,"y":700,"wires":[[]]},{"id":"af4f0cd3.3c418","type":"ads-client-write-symbol","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"","connection":"5af0d0a7.e6f3d","variableName":"MAIN.test","autoFill":false,"x":410,"y":900,"wires":[[]]},{"id":"949249de.f18a08","type":"inject","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":900,"wires":[["af4f0cd3.3c418"]]},{"id":"b756662.4507d98","type":"function","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"_createAmsTcpRequest","func":"let ADS = adsClient.ADS;\n/**\n * Parses an AMS/TCP packet from given (byte) Buffer and then handles it\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpPacket(data) {\n  const packet = {}\n  let parseResult = null\n\n  //1. Parse AMS/TCP header\n  parseResult = _parseAmsTcpHeader(data)\n  packet.amsTcp = parseResult.amsTcp\n  data = parseResult.data\n\n  //2. Parse AMS header (if exists)\n  parseResult = _parseAmsHeader(data)\n  packet.ams = parseResult.ams\n  data = parseResult.data\n\n  //3. Parse ADS data (if exists)\n  packet.ads = (packet.ams.error ? {} : _parseAdsData(packet, data))\n \n  //4. Handle the parsed packet\n  return packet\n}\n\n\n\n\n\n\n\n\n/**\n * Parses an AMS/TCP header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single full AMS/TCP packet\n * \n * @returns {object} Object {amsTcp, data}, where amsTcp is the parsed header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsTcpHeader(data) {\n\n  let pos = 0\n  const amsTcp = {}\n\n  //0..1 AMS command (header flag)\n  amsTcp.command = data.readUInt16LE(pos)\n  amsTcp.commandStr = ADS.AMS_HEADER_FLAG.toString(amsTcp.command)\n  pos += 2\n\n  //2..5 Data length\n  amsTcp.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS/TCP header from data  \n  data = data.slice(ADS.AMS_TCP_HEADER_LENGTH)\n\n  //If data length is less than AMS_HEADER_LENGTH,\n  //we know that this packet has no AMS headers -> it's only a AMS/TCP command\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    amsTcp.data = data\n\n    //Remove data (basically creates an empty buffer..)\n    data = data.slice(data.byteLength)\n  }\n\n\n  return { amsTcp, data }\n}\n\n\n\n\n/**\n * Parses an AMS header from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains data for a single AMS packet (without AMS/TCP header)\n * \n * @returns {object} Object {ams, data}, where ams is the parsed AMS header and data is rest of the data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAmsHeader(data) {\n\n  let pos = 0\n  const ams = {}\n\n  if (data.byteLength < ADS.AMS_HEADER_LENGTH) {\n    return {ams, data}\n  }\n  \n  //0..5 Target AMSNetId\n  ams.targetAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //6..8 Target ads port\n  ams.targetAdsPort = data.readUInt16LE(pos)\n  pos += 2\n\n  //8..13 Source AMSNetId\n  ams.sourceAmsNetId = _byteArrayToAmsNetIdStr(data.slice(pos, pos + ADS.AMS_NET_ID_LENGTH))\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  ams.sourceAdsPort = data.readUInt16LE(pos)\n  pos += 2\n    \n  //16..17 ADS command\n  ams.adsCommand = data.readUInt16LE(pos)\n  ams.adsCommandStr = ADS.ADS_COMMAND.toString(ams.adsCommand)\n  pos += 2\n\n  //18..19 State flags\n  ams.stateFlags = data.readUInt16LE(pos)\n  ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(ams.stateFlags)\n  pos += 2\n\n  //20..23 Data length\n  ams.dataLength = data.readUInt32LE(pos)\n  pos += 4\n  \n  //24..27 Error code\n  ams.errorCode = data.readUInt32LE(pos)\n  pos += 4\n\n  //28..31 Invoke ID\n  ams.invokeId = data.readUInt32LE(pos)\n  pos += 4\n\n  //Remove AMS header from data  \n  data = data.slice(ADS.AMS_HEADER_LENGTH)\n  \n  //ADS error\n  ams.error = (ams.errorCode !== null ? ams.errorCode > 0 : false)\n  ams.errorStr = ''\n  if (ams.error) {\n    ams.errorStr = ADS.ADS_ERROR[ams.errorCode]\n  }\n  \n\n  return {ams, data}\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses ADS data from given (byte) Buffer. Uses packet.ams to determine the ADS command\n * \n * @param {Buffer} data Buffer that contains data for a single ADS packet (without AMS/TCP header and AMS header)\n * \n * @returns {object} Object that contains the parsed ADS data\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsData(packet, data) {\n\n  let pos = 0\n  const ads = {}\n\n  if (data.byteLength === 0) {\n    return ads\n  }\n\n  //Saving the raw buffer data to object too\n  ads.rawData = data\n  \n\n  switch (packet.ams.adsCommand) {\n    //-------------- Read Write ---------------\n    case ADS.ADS_COMMAND.ReadWrite:\n    case ADS.ADS_COMMAND.Read:\n           \n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length (bytes)\n      ads.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      ads.data = Buffer.alloc(ads.dataLength)\n      data.copy(ads.data, 0, pos)\n\n      break\n\n\n    \n    //-------------- Write ---------------\n    case ADS.ADS_COMMAND.Write:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    \n    //-------------- Device info ---------------\n    case ADS.ADS_COMMAND.ReadDeviceInfo:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4 Major version\n      ads.data.majorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //5 Minor version\n      ads.data.minorVersion = data.readUInt8(pos)\n      pos += 1\n\n      //6..7 Version build\n      ads.data.versionBuild = data.readUInt16LE(pos)\n      pos += 2\n\n      //8..24 Device name\n      ads.data.deviceName = _trimPlcString(iconv.decode(data.slice(pos, pos + 16), 'cp1252'))\n\n      break\n\n\n    \n    \n    \n    //-------------- Device status ---------------\n    case ADS.ADS_COMMAND.ReadState:\n\n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n        \n      //4..5 ADS state\n      ads.data.adsState = data.readUInt16LE(pos)\n      ads.data.adsStateStr = ADS.ADS_STATE.toString(ads.data.adsState)\n      pos += 2\n\n      //6..7 Device state\n      ads.data.deviceState = data.readUInt16LE(pos)\n      pos += 2 \n\n      break\n    \n\n\n    \n    //-------------- Add notification ---------------\n    case ADS.ADS_COMMAND.AddNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n      \n      ads.data = {}\n\n      //4..7 Notification handle\n      ads.data.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n\n    \n    //-------------- Delete notification ---------------\n    case ADS.ADS_COMMAND.DeleteNotification:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n\n    \n    //-------------- Notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      \n      ads.data = _parseAdsNotification(data)\n\n      break\n    \n    \n    \n    //-------------- WriteControl ---------------\n    case ADS.ADS_COMMAND.WriteControl:\n      \n      //0..3 Ads error number\n      ads.errorCode = data.readUInt32LE(pos)\n      pos += 4\n\n      break\n    \n    \n    default:\n      //Unknown command, return a custom error\n      debug(`_parseAdsResponse: Unknown ads command in response: ${packet.ams.adsCommand}`)\n\n      ads.error = true\n      ads.errorStr = `Unknown ADS command for parser: ${packet.ams.adsCommand} (${packet.ams.adsCommandStr})`\n      ads.errorCode = -1\n\n      return ads\n      break\n  }\n\n\n  //Ads error code, if exists\n  ads.error = (ads.errorCode !== null ? ads.errorCode > 0 : false)\n\n  if (ads.error) {\n    ads.errorStr = ADS.ADS_ERROR[ads.errorCode]\n  }\n\n  return ads\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles the parsed AMS/TCP packet and actions/callbacks etc. related to it.\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header and if available, also AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAmsTcpPacketReceived(packet) {\n  \n  switch (packet.amsTcp.command) {\n    //-------------- ADS command ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD:\n      packet.amsTcp.commandStr = 'Ads command'\n      \n      _onAdsCommandReceived(packet)\n      break\n    \n    \n    //-------------- AMS/TCP port unregister ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CLOSE:\n      packet.amsTcp.commandStr = 'Port unregister'\n      //TODO: No action at the moment\n      break\n\n    \n\n    \n    //-------------- AMS/TCP port register ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_CONNECT:\n      packet.amsTcp.commandStr = 'Port register'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..5 Own AmsNetId\n        localAmsNetId: _byteArrayToAmsNetIdStr(packet.amsTcp.data.slice(0, ADS.AMS_NET_ID_LENGTH)),\n        //5..6 Own assigned ADS port\n        localAdsPort: packet.amsTcp.data.readUInt16LE(ADS.AMS_NET_ID_LENGTH)\n      }\n\n      if (this._internals.amsTcpCallback !== null) {\n        this._internals.amsTcpCallback(packet)\n      } \n      break\n    \n\n\n    //-------------- AMS router note ---------------\n    case ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_ROUTER_NOTE:\n      packet.amsTcp.commandStr = 'Port router note'\n\n      //Parse data\n      packet.amsTcp.data = {\n        //0..3 Router state\n        routerState: packet.amsTcp.data.readUInt32LE(0)\n      }\n\n      _onRouterStateChanged(packet)\n\n      break\n    \n\n\n    \n    \n    \n    //-------------- Get local ams net id response ---------------\n    case ADS.AMS_HEADER_FLAG.GET_LOCAL_NETID:\n      packet.amsTcp.commandStr = 'Get local net id'\n      //TODO: No action at the moment\n      break\n    \n    \n    \n    \n    \n    default:\n      packet.amsTcp.commandStr = `Unknown AMS/TCP command ${packet.amsTcp.command}`\n      debug(`_onAmsTcpPacketReceived(): Unknown AMS/TCP command received: \"${packet.amsTcp.command}\" - Doing nothing`)\n      //TODO: No action at the moment\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Handles incoming ADS commands\n * \n * @param {object} packet Fully parsed AMS/TCP packet, includes AMS/TCP header, AMS header and ADS data\n *  * \n * @memberof _LibraryInternals\n */\nasync function _onAdsCommandReceived(packet) {\n\n  switch (packet.ams.adsCommand) {\n\n    //-------------- ADS notification ---------------\n    case ADS.ADS_COMMAND.Notification:\n      //Try to find the callback with received notification handles\n      packet.ads.data.stamps.forEach(async stamp => {\n        stamp.samples.forEach(async sample => {\n          \n          if (this._internals.activeSubscriptions[sample.notificationHandle]) {       \n            const sub = this._internals.activeSubscriptions[sample.notificationHandle]\n            debug(`_onAdsCommandReceived(): Notification received for handle \"${sample.notificationHandle}\" (%o)`, sub.target)\n            \n            //First we parse the data from received byte buffer\n            try {\n              const parsedValue = await sub.dataParser(sample.data)\n\n              parsedValue.timeStamp = stamp.timeStamp\n  \n              //Then lets call the users callback\n              sub.callback(\n                parsedValue,\n                sub\n              )\n            } catch (err) {\n              debug(`_onAdsCommandReceived(): Ads notification received but parsing Javascript object failed: %o`, err)\n              this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but parsing data to Javascript object failed. Subscription: ${JSON.stringify(sub)}`, err, sub))\n            }\n\n          } else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads notification received but it has unknown notificationHandle (${sample.notificationHandle}). Use unsubscribe() to save resources.`))\n          }\n        })\n      })\n      break\n    \n    \n      //-------------- All other ADS commands ---------------\n    default:\n      //Try to find the callback with received invoke id and call it\n      if (this._internals.activeAdsRequests[packet.ams.invokeId]) {\n        const adsRequest = this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        //Clear timeout\n        clearTimeout(adsRequest.timeoutTimer)\n\n        //Call callback\n        adsRequest.callback(packet)\n      }\n      else {\n     this.emit('ads-client-error', new ClientException(this, `_onAdsCommandReceived`, `Ads command received with unknown invokeId \"${packet.ams.invokeId}\".`, packet))\n      }\n\n      break\n  }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Parses received ADS notification data (stamps) from given (byte) Buffer\n * \n * @param {Buffer} data Buffer that contains ADS notification stamp data\n * \n * @returns {object} Object that contains the parsed ADS notification\n * \n * @memberof _LibraryInternals\n */\nfunction _parseAdsNotification(data) {\n  let pos = 0\n  const packet = {}\n\n  //0..3 Data length\n  packet.dataLength = data.readUInt32LE(pos)\n  pos += 4\n\n  //4..7 Stamp count\n  packet.stampCount = data.readUInt32LE(pos)\n  pos += 4\n\n  packet.stamps = []\n\n  //Parse all stamps\n  for (let stamp = 0; stamp < packet.stampCount; stamp++) {\n    const newStamp = {}\n\n    //0..7 Timestamp (Converting Windows FILETIME to Date)\n    newStamp.timeStamp = new Date(new long(data.readUInt32LE(pos), data.readUInt32LE(pos+4)).div(10000).sub(11644473600000).toNumber())\n    pos += 8\n\n    //8..11 Number of samples\n    newStamp.count = data.readUInt32LE(pos)\n    pos += 4\n\n    newStamp.samples = []\n\n    //Parse all samples for this stamp\n    for (let sample = 0; sample < newStamp.count; sample++) {\n      const newSample = {}\n\n      //0..3 Notification handle\n      newSample.notificationHandle = data.readUInt32LE(pos)\n      pos += 4\n\n      //4..7 Data length\n      newSample.dataLength = data.readUInt32LE(pos)\n      pos += 4\n\n      //8..n Data\n      newSample.data = data.slice(pos, pos + newSample.dataLength)\n      pos += newSample.dataLength\n\n      newStamp.samples.push(newSample)\n    }\n    packet.stamps.push(newStamp)\n  }\n\n  return packet\n}\n\n\n\n\n\n\n\n\n\n\n/**\n * Sends an ADS command with given data to the PLC\n * \n * @param {number} adsCommand - ADS command to send (see ADS.ADS_COMMAND)\n * @param {Buffer} adsData - Buffer object that contains the data to send\n * @param {number} [targetAdsPort] - Target ADS port - default is this.settings.targetAdsPort\n * @param {string} [targetAmsNetId] - Target AmsNetID - default is this.settings.targetAmsNetId\n * \n * @returns {Promise<object>} Returns a promise (async function)\n * - If resolved, command was sent successfully and response was received. The received reponse is parsed and returned (object)\n * - If rejected, sending, receiving or parsing failed and error info is returned (object)\n * \n * @memberof _LibraryInternals\n */\nfunction _sendAdsCommand(adsCommand, adsData, targetAdsPort = null, targetAmsNetId = null) {\n  return new Promise(async (resolve, reject) => {\n    \n    //Check that next free invoke ID is below 32 bit integer maximum\n    if (this._internals.nextInvokeId >= ADS.ADS_INVOKE_ID_MAX_VALUE)\n      this._internals.nextInvokeId = 0\n    \n    //Creating the data packet object\n    const packet = {\n      amsTcp: {\n        command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD,\n        commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD)\n      },\n      ams: {\n        targetAmsNetId: (targetAmsNetId == null ? this.connection.targetAmsNetId : targetAmsNetId),\n        targetAdsPort: (targetAdsPort == null ? this.connection.targetAdsPort : targetAdsPort),\n        sourceAmsNetId: this.connection.localAmsNetId,\n        sourceAdsPort: this.connection.localAdsPort,\n        adsCommand: adsCommand,\n        adsCommandStr: ADS.ADS_COMMAND.toString(adsCommand),\n        stateFlags: ADS.ADS_STATE_FLAGS.AdsCommand,\n        stateFlagsStr: '',\n        dataLength: adsData.byteLength,\n        errorCode: 0,\n        invokeId: this._internals.nextInvokeId++\n      },\n      ads: {\n        rawData: adsData\n      }\n    }\n    packet.ams.stateFlagsStr = ADS.ADS_STATE_FLAGS.toString(packet.ams.stateFlags)\n\n    \n\n    //Creating a full AMS/TCP request\n    try {\n      var request = _createAmsTcpRequest(packet)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', err))\n    }\n\n    //Registering callback for response handling\n    this._internals.activeAdsRequests[packet.ams.invokeId] = {\n      callback: function (response) {\n  \n        //Callback is no longer needed, delete it\n        delete this._internals.activeAdsRequests[packet.ams.invokeId]\n\n        if (response.ams.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with AMS error received', response))\n        } else if (response.ads.error) {\n          return reject(new ClientException(this, '_sendAdsCommand()', 'Response with ADS error received', response))\n        }\n\n        return resolve(response)\n      },\n\n      timeoutTimer: setTimeout(function (client) {\n        debug(`_sendAdsCommand(): Timeout for command ${packet.ams.adsCommandStr} with invokeId ${packet.ams.invokeId} - No response`)\n\n        //Callback is no longer needed, delete it\n        delete client._internals.activeAdsRequests[packet.ams.invokeId]\n        \n        //Create a custom \"ads error\" so that the info is passed onwards\n        const adsError = {\n          ads: {\n            error: true,\n            errorCode: -1,\n            errorStr: `Timeout - no response in ${client.settings.timeoutDelay} ms`\n          }\n        }\n        return reject(new ClientException(this, '_sendAdsCommand()', `Timeout - no response in ${client.settings.timeoutDelay} ms`, adsError))\n\n      }, this.settings.timeoutDelay, this)\n    }\n\n    //Write the data \n    try {\n      _socketWrite(request)\n    } catch (err) {\n      return reject(new ClientException(this, '_sendAdsCommand()', `Error - Socket is not available`, err))\n    }\n  })\n}\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP request from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Full created AMS/TCP request as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpRequest(packet) {\n  //1. Create ADS data\n  const adsData = packet.ads.rawData\n  \n  //2. Create AMS header\n  const amsHeader = _createAmsHeader(packet)\n\n  //3. Create AMS/TCP header\n  const amsTcpHeader = _createAmsTcpHeader(packet, amsHeader)\n\n  //4. Create full AMS/TCP packet\n  const amsTcpRequest = Buffer.concat([amsTcpHeader, amsHeader, adsData])\n\n  \n  return amsTcpRequest\n}\n\n\n\n\n\n\n\n    \n\n\n/**\n * Creates an AMS header from given packet\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * \n * @returns {Buffer} Created AMS header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsHeader(packet) {\n  //Allocating bytes for AMS header\n  const header = Buffer.alloc(ADS.AMS_HEADER_LENGTH)\n  let pos = 0\n\n  //0..5 Target AMSNetId\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.targetAmsNetId)).copy(header, 0)\n  pos += ADS.AMS_NET_ID_LENGTH\n  \n  //6..8 Target ads port\n  header.writeUInt16LE(packet.ams.targetAdsPort, pos)\n  pos += 2\n  \n  //8..13 Source ads port\n  Buffer.from(_amsNetIdStrToByteArray(packet.ams.sourceAmsNetId)).copy(header, pos)\n  pos += ADS.AMS_NET_ID_LENGTH\n\n  //14..15 Source ads port\n  header.writeUInt16LE(packet.ams.sourceAdsPort, pos)\n  pos += 2\n\n  //16..17 ADS command\n  header.writeUInt16LE(packet.ams.adsCommand, pos)\n  pos += 2\n  \n  //18..19 State flags\n  header.writeUInt16LE(packet.ams.stateFlags, pos)\n  pos += 2\n  \n  //20..23 Data length\n  header.writeUInt32LE(packet.ams.dataLength, pos)\n  pos += 4\n  \n  //24..27 Error code\n  header.writeUInt32LE(packet.ams.errorCode, pos)\n  pos += 4\n  \n  //28..31 Invoke ID\n  header.writeUInt32LE(packet.ams.invokeId, pos)\n  pos += 4\n\n\n\n  return header\n}\n\n\n\n\n\n\n\n\n\n/**\n * Creates an AMS/TCP header from given packet and AMS header\n * \n * @param {object} packet Object containing the full AMS/TCP packet\n * @param {Buffer} amsHeader Buffer containing the previously created AMS header\n * \n * @returns {Buffer} Created AMS/TCP header as a (byte) Buffer\n * \n * @memberof _LibraryInternals\n */\nfunction _createAmsTcpHeader(packet, amsHeader) {\n  //Allocating bytes for AMS/TCP header\n  const header = Buffer.alloc(ADS.AMS_TCP_HEADER_LENGTH)\n  let pos = 0\n\n  //0..1 AMS command (header flag)\n  header.writeUInt16LE(packet.amsTcp.command, pos)\n  pos += 2\n\n  //2..5 Data length\n  header.writeUInt32LE(amsHeader.byteLength + packet.ams.dataLength, pos)\n  pos += 4\n\n\n  return header\n}\n\n\n\n\n\n\n\n/**\n * Writes given message to console if settings.hideConsoleWarnings is false\n * \n * @param {string} str Message to console.log() \n * \n * @memberof _LibraryInternals\n */\nfunction _console(str) {\n  if (this.settings.hideConsoleWarnings !== true)\n    console.log(`${PACKAGE_NAME}: ${str}`)\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * **Helper:** Marges objects together recursively. Used for example at writeSymbol() to combine active values and new (uncomplete) object values\n * \n * Based on https://stackoverflow.com/a/34749873/8140625 by Salakar and https://stackoverflow.com/a/49727784/8140625\n * \n * Later modified to work with in both case-sensitive and case-insensitive ways (as the PLC is case-insensitive too). \n * Also fixed isObjectOrArray to return true only when input is object literal {} or array (https://stackoverflow.com/a/16608074/8140625)\n * \n * @param {boolean} isCaseSensitive True = Object keys are merged case-sensitively --> target['key'] !== target['KEY'], false = case-insensitively\n * @param {object} target Target object to copy data to\n * @param {...object} sources Source objects to copy data from\n * \n * @returns {object} Merged object\n * \n * @memberof _LibraryInternals\n */\n\nfunction _deepMergeObjects(isCaseSensitive, target, ...sources) {\n  if (!sources.length) return target\n  \n  const isObjectOrArray = (item) => {\n    return (!!item) && ((item.constructor === Object) || Array.isArray(item))\n  }\n  \n  //Checks if object key exists\n  const keyExists = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return !!Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())\n    } else {\n      return (obj[key] != null)\n    }\n  }\n\n  //Returns object value by key\n  const getValue = (obj, key, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      return obj[Object.keys(obj).find(objKey => objKey.toLowerCase() === key.toLowerCase())]\n    } else {\n      return obj[key]\n    }\n  }\n\n  //Sets object value obj[key] to value - If isCaseSensitive == false, obj[KeY] == obj[key]\n  const setValue = (obj, key, value, isCaseSensitive) => {\n    if (isCaseSensitive === false) {\n      Object.keys(obj).forEach(objKey => {\n        if (objKey.toLowerCase() === key.toLowerCase()) {\n          obj[objKey] = value\n        }\n      });\n    } else {\n      //Case-sensitive is easy\n      obj[key] = value\n    }\n  }\n\n  const source = sources.shift()\n\n  if (isObjectOrArray(target) && isObjectOrArray(source)) {    \n    for (const key in source) {\n\n      if (isObjectOrArray(source[key])) {\n        //If source is object\n        if (keyExists(target, key, isCaseSensitive)) { \n          //Target has this key, copy value\n          setValue(target, key, Object.assign({}, target[key]), isCaseSensitive)\n        } else {       \n          //Target doesn't have this key, add it)\n          Object.assign(target, { [key]: {} })\n        }\n        //As this is an object, go through it recursively\n        _deepMergeObjects(isCaseSensitive, getValue(target, key, false), source[key])\n\n      } else {\n        //Source is not object\n\n        if (keyExists(target, key, isCaseSensitive)) {\n          //Target has this key, copy value\n          setValue(target, key, source[key], isCaseSensitive)\n        } else {\n          //Target doesn't have this key, add it\n          Object.assign(target, { [key]: source[key] })\n        }\n      }\n    }\n  }\n  \n  return _deepMergeObjects(isCaseSensitive, target, ...sources)\n}\n\n\n\n\n\n\n\n/**\n * **Helper:** Trims the given PLC string until en mark (\\0, 0 byte) is found\n * \n * @param {string} plcString String to trim\n * \n * @returns {string} Trimmed string\n * \n * @memberof _LibraryInternals\n */\nfunction _trimPlcString(plcString) {\n  let parsedStr = ''\n\n  for (let i = 0; i < plcString.length; i++) {\n    if (plcString.charCodeAt(i) === 0) break\n\n    parsedStr += plcString[i]\n  }\n\n  return parsedStr\n}\n\n\n\n\n\n\n/**\n * **Helper:** Converts byte array (Buffer) to AmsNetId string\n * \n * @param {Buffer|array} byteArray Buffer/array that contains AmsNetId bytes\n * \n * @returns {string} AmsNetId as string\n * \n * @memberof _LibraryInternals\n */\nfunction _byteArrayToAmsNetIdStr(byteArray) {\n  return byteArray.join('.')\n}\n\n\n\n\n/**\n * **Helper:** Converts AmsNetId string to byte array\n * \n * @param {string} byteArray String that represents an AmsNetId\n * \n * @returns {array} AmsNetId as array\n * \n * @memberof _LibraryInternals\n */\nfunction _amsNetIdStrToByteArray(str) {\n  return str.split('.').map(x => parseInt(x))\n}\n\n\n\n///------------------------------//\n//Creating the data packet object\n    const packet = {\n      amsTcp: {\n        command: ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD,\n        commandStr: ADS.AMS_HEADER_FLAG.toString(ADS.AMS_HEADER_FLAG.AMS_TCP_PORT_AMS_CMD)\n      },\n      ams: {\n        targetAmsNetId: \"2.2.2.2.2.2\",\n        targetAdsPort: 222,\n        sourceAmsNetId: \"127.0.0.1.1.1\",\n        sourceAdsPort: 10000,\n        adsCommand: 4,\n        adsCommandStr: ADS.ADS_COMMAND.toString(4),\n        stateFlags: 5,\n        stateFlagsStr: '',\n        dataLength: 8,\n        errorCode: 0,\n      },\n      ads: {\n        rawData: new Buffer.from([ 0, 0, 0, 0, 5, 0, 1, 0])\n      }\n    }\n\n\n\nmsg.payload = _createAmsTcpRequest(packet);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"adsClient","module":"ads-client"}],"x":390,"y":960,"wires":[["4f1f6fa2.a1006"]]},{"id":"4f1f6fa2.a1006","type":"tcp out","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","host":"","port":"48875","beserver":"server","base64":false,"end":true,"name":"","x":640,"y":960,"wires":[]},{"id":"27fa4ec7.f8cff2","type":"comment","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"","info":"Attempt to get adsClient into reconnect state","x":140,"y":860,"wires":[]},{"id":"d9d0b162.91784","type":"ads-client-connection-status","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"","connection":"5af0d0a7.e6f3d","showInput":false,"inputs":0,"x":710,"y":900,"wires":[[]]},{"id":"cb80f66b.896cd8","type":"inject","z":"259a1f0f.ec9aa","g":"d49e76e4.3996b8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"0.32548","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":960,"wires":[["b756662.4507d98"]]},{"id":"55ba3bb8.8b8cc4","type":"ads-client-connection","name":"Local","targetAmsNetId":"localhost","targetAdsPort":"851","objectifyEnumerations":true,"convertDatesToJavascript":true,"readAndCacheSymbols":false,"readAndCacheDataTypes":false,"disableSymbolVersionMonitoring":false,"routerTcpPort":"48988","routerAddress":"127.0.0.1","localAddress":"","localTcpPort":"","localAmsNetId":"","localAdsPort":"","timeoutDelay":"","hideConsoleWarnings":false,"autoReconnect":true,"reconnectInterval":"","checkStateInterval":"","connectionDownDelay":"","allowHalfOpen":false,"disableBigInt":false,"debuggingLevel":""},{"id":"365f0b45.6ad694","type":"ads-client-connection","name":"Local2","targetAmsNetId":"localhost","targetAdsPort":"851","objectifyEnumerations":true,"convertDatesToJavascript":true,"readAndCacheSymbols":false,"readAndCacheDataTypes":false,"disableSymbolVersionMonitoring":false,"routerTcpPort":"48872","routerAddress":"127.0.0.1","localAddress":"","localTcpPort":"","localAmsNetId":"178.25.58.4.2.3","localAdsPort":"255","timeoutDelay":"","hideConsoleWarnings":false,"autoReconnect":true,"reconnectInterval":"","checkStateInterval":"","connectionDownDelay":"","allowHalfOpen":true,"disableBigInt":false,"debuggingLevel":""},{"id":"8865b45e.f078d8","type":"ads-client-connection","name":"Local3","targetAmsNetId":"localhost","targetAdsPort":"851","objectifyEnumerations":true,"convertDatesToJavascript":true,"readAndCacheSymbols":false,"readAndCacheDataTypes":false,"disableSymbolVersionMonitoring":false,"routerTcpPort":"48873","routerAddress":"127.0.0.1","localAddress":"","localTcpPort":"","localAmsNetId":"1.5.58.1.5.6","localAdsPort":"855","timeoutDelay":"","hideConsoleWarnings":false,"autoReconnect":true,"reconnectInterval":"","checkStateInterval":"","connectionDownDelay":"","allowHalfOpen":true,"disableBigInt":false,"debuggingLevel":"1"},{"id":"c7cffb0d.36f128","type":"ads-client-connection","name":"Local4","targetAmsNetId":"localhost","targetAdsPort":"851","objectifyEnumerations":true,"convertDatesToJavascript":true,"readAndCacheSymbols":false,"readAndCacheDataTypes":false,"disableSymbolVersionMonitoring":false,"routerTcpPort":"48874","routerAddress":"127.0.0.1","localAddress":"","localTcpPort":"","localAmsNetId":"2.2.2.2.2.2","localAdsPort":"222","timeoutDelay":"","hideConsoleWarnings":false,"autoReconnect":true,"reconnectInterval":"","checkStateInterval":"","connectionDownDelay":"","allowHalfOpen":true,"disableBigInt":false,"debuggingLevel":""},{"id":"5af0d0a7.e6f3d","type":"ads-client-connection","name":"Local5","targetAmsNetId":"localhost","targetAdsPort":"851","objectifyEnumerations":true,"convertDatesToJavascript":true,"readAndCacheSymbols":false,"readAndCacheDataTypes":false,"disableSymbolVersionMonitoring":false,"routerTcpPort":"48875","routerAddress":"127.0.0.1","localAddress":"","localTcpPort":"","localAmsNetId":"1.1.1.1.1.1","localAdsPort":"111","timeoutDelay":"","hideConsoleWarnings":false,"autoReconnect":true,"reconnectInterval":"","checkStateInterval":"","connectionDownDelay":"","allowHalfOpen":true,"disableBigInt":false,"debuggingLevel":"1"}]

Whit this I haven't seen any difference between the two codes yet. But more testing will be needed.
image

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

I was trying to understand why the connections weren't closed. Finally noticed you have used "tcp out" in the last two groups. When changing that to "tcp in", there are no problems and all ports are closed OK at least for me. Perhaps the node-red tcp out works differently even in listen mode.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

I think I found at least one reason why thing happen when node is restarted.

Will look into it more and see if it helps.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

Just updated both ads-client and this library. Hopefully it fixes at least few of these issues!

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

Hi @Hopperpop! Have you had any time to test if there is any light at the end of a tunnel 😄

from node-red-contrib-ads-client.

Hopperpop avatar Hopperpop commented on July 20, 2024

I'm getting this error sometimes when using my testnodes:

(node:17) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'off' of null
    at Client._onConnectionLost (/data/node_modules/node-red-contrib-ads-client/node_modules/ads-client/src/ads-client.js:3845:28)
    at Timeout.poller (/data/node_modules/node-red-contrib-ads-client/node_modules/ads-client/src/ads-client.js:4201:27)

I also see still multiple connections in test 3/5. I added a unique id to the client and I see that there are still multiple reconnection loops at the same time. The problem is I think that for example the "tryToReconnect" doesn't get destroyed correctly. Clearing the timeout is not enough, because the function could already be triggered and is still processing (reconnecting) at the time of destroying. After it ends it just creates a new timeout although the timeout was cleared. Before creating a new timeout there should be something that checks if it the client still exist and isn't destroyed yet. (Hopefully my wording makes some sense.)

Maybe use "setInterval" instead of "setTimeout", and don't let a function call itself. This way it's easier to destroy.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

Damn. I couldn't get any problems, but thanks! Need to think a solution to that.

I'll also add a check to that ads-client error.

from node-red-contrib-ads-client.

jisotalo avatar jisotalo commented on July 20, 2024

Newest ads-client version 1.12.0 hopefully solves all these problems. Closing this but if there are problems too then we should create new issue for it.

from node-red-contrib-ads-client.

Related Issues (12)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.