jisotalo / ads-client

Unofficial Node.js ADS library for connecting to Beckhoff TwinCAT automation systems using ADS protocol.
https://jisotalo.fi/ads-client/
MIT License
80 stars 17 forks source link

Can't subscribe to POINTER or REFERENCE variables (solved) #105

Closed freefly42 closed 1 year ago

freefly42 commented 1 year ago

Original title: Can't subscribe to data points that are member fields in an object that's part of an array of objects

I have a FunctionBlock that creates an array of structs to match an I/O process image for a device. I am trying to subscribe to one of fields in the struct, and can't subscribe to any of the fields. The address is: Main.fbScreening.fbAlicatSerial.allStatus[1].status The error that is returned is 1793 'Service is not supported by server'. However, I know it is supported by the server as it works with node-ads. I think the issue has to do with the '[]' in the name, possibly with the cache[symbol-name]. I think it's winding up with the wrong group and offset when it subscribes to the PLC, and that's why the PLC gives the not supported error. It successfully reads the symbol info:

info for Main.fbScreening.fbAlicatSerial.allStatus[1].status: {"indexGroup":61462,"indexOffset":68,"size":4,"adsDataType":19,"adsDataTypeStr":"ADST_UINT32","flags":8,"flagsStr":["TypeGuid"],"arrayDimension":0,"nameLength":51,"typeLength":5,"commentLength":0,"name":"Main.fbScreening.fbAlicatSerial.allStatus[1].status","type":"DWORD","comment":"","arrayData":[],"typeGuid":"95190718000000000000000000000007","attributes":[],"reserved":{"type":"Buffer","data":[]}}

I've tried reading directly with the indexGroup and indexOffset, but it's not allowed. I think the index offset is not correct as what I'm getting for the index offset in node-ads is much larger (in the 44000 range).

ads-client:details _readSymbolInfo(): Reading symbol info for Main.fbScreening.fbAlicatSerial.allStatus[1].status successful +1ms ads-client:details getSymbolInfo(): Symbol info read and cached from PLC for Main.fbScreening.fbAlicatSerial.allStatus[1].status +0ms ads-client:details _getDataTypeInfo(): Data type requested for DWORD +1ms ads-client:details _readDataTypeInfo(): Reading data type info for DWORD +0ms ads-client:details _sendAdsCommand(): Sending an ads command ReadWrite (22 bytes): { amsTcp: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD' }, ams: { targetAmsNetId: '5.91.185.48.1.1', targetAdsPort: 851, sourceAmsNetId: '10.0.0.99.1.1', sourceAdsPort: 32751, adsCommand: 9, adsCommandStr: 'ReadWrite', stateFlags: 4, stateFlagsStr: 'AdsCommand, Tcp, Request', dataLength: 22, errorCode: 0, invokeId: 8 }, ads: { rawData: <Buffer 11 f0 00 00 00 00 00 00 ff ff ff ff 06 00 00 00 44 57 4f 52 44 00> } } +1ms ads-client:details _createAmsHeader(): AMS header created (32 bytes) +0ms ads-client:raw-data _createAmsHeader(): AMS header created: '055bb930010153030a0000630101ef7f09000400160000000000000008000000' +4ms ads-client:details _createAmsTcpHeader(): AMS/TCP header created (6 bytes) +0ms ads-client:raw-data _createAmsTcpHeader(): AMS/TCP header created: '000036000000' +0ms ads-client:details _createAmsTcpRequest(): AMS/TCP request created (60 bytes) +0ms ads-client:raw-data IO out ------> 60 bytes : 000036000000055bb930010153030a0000630101ef7f0900040016000000000000000800000011f0000000000000ffffffff0600000044574f524400 +0ms ads-client:raw-data IO in <------ 166 bytes: 0000a00000000a0000630101ef7f055bb9300101530309000500800000000000000008000000000000007800000078000000010000000000000000000000040000000000000013000000811020000500000000000000000044574f52440000009519071800000000000000000000000702000f01446973706c61794d696e56616c75650030000f0a446973706c61794d617856616c7565002378666666666666666600000000 +77ms ads-client:details _parseAmsTcpHeader(): Starting to parse AMS/TCP header +78ms ads-client:details _parseAmsTcpHeader(): AMS/TCP header parsed: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD', dataLength: 160 } +0ms ads-client:details _parseAmsHeader(): Starting to parse AMS header +1ms ads-client:details _parseAmsHeader(): AMS header parsed: { targetAmsNetId: '10.0.0.99.1.1', targetAdsPort: 32751, sourceAmsNetId: '5.91.185.48.1.1', sourceAdsPort: 851, adsCommand: 9, adsCommandStr: 'ReadWrite', stateFlags: 5, stateFlagsStr: 'Response, AdsCommand, Tcp', dataLength: 128, errorCode: 0, invokeId: 8, error: false, errorStr: '' } +0ms ads-client:details _parseAdsData(): Starting to parse ADS data +0ms ads-client:details _parseAdsData(): ADS data parsed: { rawData: <Buffer 00 00 00 00 78 00 00 00 78 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 13 00 00 00 81 10 20 00 05 00 00 00 00 00 00 00 00 00 ... 78 more bytes>, errorCode: 0, dataLength: 120, data: <Buffer 78 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 13 00 00 00 81 10 20 00 05 00 00 00 00 00 00 00 00 00 44 57 4f 52 44 00 00 00 ... 70 more bytes>, error: false } +0ms ads-client:details _onAmsTcpPacketReceived(): A parsed AMS packet received with command 0 +1ms ads-client:details _onAdsCommandReceived(): A parsed ADS command received with command 9 +0ms ads-client:details _sendAdsCommand(): Response received for command ReadWrite with invokeId 8 +0ms ads-client:details _readDataTypeInfo(): Reading data type info info for DWORD successful +2ms ads-client:details _getDataTypeInfo(): Data type info read and cached from PLC for DWORD +2ms ads-client:details _sendAdsCommand(): Sending an ads command AddNotification (40 bytes): { amsTcp: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD' }, ams: { targetAmsNetId: '5.91.185.48.1.1', targetAdsPort: 851, sourceAmsNetId: '10.0.0.99.1.1', sourceAdsPort: 32751, adsCommand: 6, adsCommandStr: 'AddNotification', stateFlags: 4, stateFlagsStr: 'AdsCommand, Tcp, Request', dataLength: 40, errorCode: 0, invokeId: 9 }, ads: { rawData: <Buffer 16 f0 00 00 44 00 00 00 04 00 00 00 03 00 00 00 00 00 00 00 40 4b 4c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00> } } +0ms ads-client:details _createAmsHeader(): AMS header created (32 bytes) +0ms ads-client:raw-data _createAmsHeader(): AMS header created: '055bb930010153030a0000630101ef7f06000400280000000000000009000000' +7ms ads-client:details _createAmsTcpHeader(): AMS/TCP header created (6 bytes) +0ms ads-client:raw-data _createAmsTcpHeader(): AMS/TCP header created: '000048000000' +0ms ads-client:details _createAmsTcpRequest(): AMS/TCP request created (78 bytes) +0ms ads-client:raw-data IO out ------> 78 bytes : 000048000000055bb930010153030a0000630101ef7f0600040028000000000000000900000016f0000044000000040000000300000000000000404b4c0000000000000000000000000000000000 +0ms ads-client:raw-data IO in <------ 46 bytes: 0000280000000a0000630101ef7f055bb93001015303060005000800000000000000090000000107000000000000 +78ms ads-client:details _parseAmsTcpHeader(): Starting to parse AMS/TCP header +78ms ads-client:details _parseAmsTcpHeader(): AMS/TCP header parsed: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD', dataLength: 40 } +0ms ads-client:details _parseAmsHeader(): Starting to parse AMS header +0ms ads-client:details _parseAmsHeader(): AMS header parsed: { targetAmsNetId: '10.0.0.99.1.1', targetAdsPort: 32751, sourceAmsNetId: '5.91.185.48.1.1', sourceAdsPort: 851, adsCommand: 6, adsCommandStr: 'AddNotification', stateFlags: 5, stateFlagsStr: 'Response, AdsCommand, Tcp', dataLength: 8, errorCode: 0, invokeId: 9, error: false, errorStr: '' } +1ms ads-client:details _parseAdsData(): Starting to parse ADS data +0ms ads-client:details _parseAdsData(): ADS data parsed: { rawData: <Buffer 01 07 00 00 00 00 00 00>, errorCode: 1793, data: { notificationHandle: 0 }, error: true, errorStr: 'Service is not supported by server' } +0ms ads-client:details _onAmsTcpPacketReceived(): A parsed AMS packet received with command 0 +0ms ads-client:details _onAdsCommandReceived(): A parsed ADS command received with command 6 +0ms ads-client:details _sendAdsCommand(): Response received for command AddNotification with invokeId 9 +0ms ads-client _subscribe(): Subscribing to 'Main.fbScreening.fbAlicatSerial.allStatus[1].status' failed: ClientException: Response with ADS error received at Client.callback (/src/ads/node_modules/ads-client/src/ads-client.js:6518:25) at Client._onAdsCommandReceived (/src/ads/node_modules/ads-client/src/ads-client.js:6358:29) at Client._onAmsTcpPacketReceived (/src/ads/node_modules/ads-client/src/ads-client.js:6219:29) at Client._parseAmsTcpPacket (/src/ads/node_modules/ads-client/src/ads-client.js:5874:27) at processImmediate (node:internal/timers:466:21) { sender: '_sendAdsCommand()', adsError: true, adsErrorInfo: { adsErrorType: 'ADS error', adsErrorCode: 1793, adsErrorStr: 'Service is not supported by server' }, metaData: null, errorTrace: [], getInnerException: null } +242ms failed to subscribe Main.fbScreening.fbAlicatSerial.allStatus[1].status ClientException: Subscribing to Main.fbScreening.fbAlicatSerial.allStatus[1].status failed at /src/ads/node_modules/ads-client/src/ads-client.js:1080:30 ClientException: Subscribing to "Main.fbScreening.fbAlicatSerial.allStatus[1].status" failed at /src/ads/node_modules/ads-client/src/ads-client.js:4626:16 ClientException: Response with ADS error received at Client.callback (/src/ads/node_modules/ads-client/src/ads-client.js:6518:25) at Client._onAdsCommandReceived (/src/ads/node_modules/ads-client/src/ads-client.js:6358:29) at Client._onAmsTcpPacketReceived (/src/ads/node_modules/ads-client/src/ads-client.js:6219:29) at Client._parseAmsTcpPacket (/src/ads/node_modules/ads-client/src/ads-client.js:5874:27) at processImmediate (node:internal/timers:466:21) { sender: 'subscribe()', adsError: true, adsErrorInfo: { adsErrorType: 'ADS error', adsErrorCode: 1793, adsErrorStr: 'Service is not supported by server' }, metaData: null, errorTrace: [ '_subscribe(): Subscribing to "Main.fbScreening.fbAlicatSerial.allStatus[1].status" failed', '_sendAdsCommand(): Response with ADS error received' ], getInnerException: [Function (anonymous)] }

freefly42 commented 1 year ago

Interesting. ReadRawByName works. I have packet captures, I may post if they're helpful. The indexGroup and indexOffset from getSymbolInfo() differ from the indexGroup and indexOffset that are used in the successful read in node-ads. From below, ads-client is getting "61462" (0xF016) and "76" (0x4c) where node-ads winds up with 61445 (0xf005) and 2357199484(0x8c80027c) respectively.

info for Main.fbScreening.fbAlicatSerial.allStatus[1].pressure: {"indexGroup":61462,"indexOffset":76,"size":4,"adsDataType":4,"adsDataTypeStr":"ADST_REAL32","flags":8,"flagsStr":["TypeGuid"],"arrayDimension":0,"nameLength":53,"typeLength":4,"commentLength":0,"name":"Main.fbScreening.fbAlicatSerial.allStatus[1].pressure","type":"REAL","comment":"","arrayData":[],"typeGuid":"9519071800000000000000000000000d","attributes":[],"reserved":{"type":"Buffer","data":[]}} ads-client readRawByName(): Reading data from Main.fbScreening.fbAlicatSerial.allStatus[1].pressure using ADS command READ_SYMVAL_BYNAME)} +81ms ads-client:details _sendAdsCommand(): Sending an ads command ReadWrite (70 bytes): { amsTcp: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD' }, ams: { targetAmsNetId: '5.91.185.48.1.1', targetAdsPort: 851, sourceAmsNetId: '10.0.0.99.1.1', sourceAdsPort: 32751, adsCommand: 9, adsCommandStr: 'ReadWrite', stateFlags: 4, stateFlagsStr: 'AdsCommand, Tcp, Request', dataLength: 70, errorCode: 0, invokeId: 8 }, ads: { rawData: <Buffer 04 f0 00 00 00 00 00 00 ff ff ff ff 36 00 00 00 4d 61 69 6e 2e 66 62 53 63 72 65 65 6e 69 6e 67 2e 66 62 41 6c 69 63 61 74 53 65 72 69 61 6c 2e 61 6c ... 20 more bytes> } } +1ms ads-client:details _createAmsHeader(): AMS header created (32 bytes) +0ms ads-client:raw-data _createAmsHeader(): AMS header created: '055bb930010153030a0000630101ef7f09000400460000000000000008000000' +4ms ads-client:details _createAmsTcpHeader(): AMS/TCP header created (6 bytes) +1ms ads-client:raw-data _createAmsTcpHeader(): AMS/TCP header created: '000066000000' +1ms ads-client:details _createAmsTcpRequest(): AMS/TCP request created (108 bytes) +0ms ads-client:raw-data IO out ------> 108 bytes : 000066000000055bb930010153030a0000630101ef7f0900040046000000000000000800000004f0000000000000ffffffff360000004d61696e2e666253637265656e696e672e6662416c6963617453657269616c2e616c6c5374617475735b315d2e707265737375726500 +0ms ads-client:raw-data IO in <------ 50 bytes: 00002c0000000a0000630101ef7f055bb93001015303090005000c000000000000000800000000000000040000009af17442 +78ms ads-client:details _parseAmsTcpHeader(): Starting to parse AMS/TCP header +79ms ads-client:details _parseAmsTcpHeader(): AMS/TCP header parsed: { command: 0, commandStr: 'AMS_TCP_PORT_AMS_CMD', dataLength: 44 } +0ms ads-client:details _parseAmsHeader(): Starting to parse AMS header +0ms ads-client:details _parseAmsHeader(): AMS header parsed: { targetAmsNetId: '10.0.0.99.1.1', targetAdsPort: 32751, sourceAmsNetId: '5.91.185.48.1.1', sourceAdsPort: 851, adsCommand: 9, adsCommandStr: 'ReadWrite', stateFlags: 5, stateFlagsStr: 'Response, AdsCommand, Tcp', dataLength: 12, errorCode: 0, invokeId: 8, error: false, errorStr: '' } +0ms ads-client:details _parseAdsData(): Starting to parse ADS data +1ms ads-client:details _parseAdsData(): ADS data parsed: { rawData: <Buffer 00 00 00 00 04 00 00 00 9a f1 74 42>, errorCode: 0, dataLength: 4, data: <Buffer 9a f1 74 42>, error: false } +0ms ads-client:details _onAmsTcpPacketReceived(): A parsed AMS packet received with command 0 +0ms ads-client:details _onAdsCommandReceived(): A parsed ADS command received with command 9 +0ms ads-client:details _sendAdsCommand(): Response received for command ReadWrite with invokeId 8 +0ms ads-client readRawByName(): Data read - 4 bytes received for Main.fbScreening.fbAlicatSerial.allStatus[1].pressure +82ms value read is: {"type":"Buffer","data":[154,241,116,66]} 61.235939025878906

read from ads-client (unsuccessful): image read from node-ads (successful): image

freefly42 commented 1 year ago

I have figured out that the issue is with a REFERENCE type, so I can actually subscribe to the original object as a workaround.

jisotalo commented 1 year ago

Hi @freefly42

Thanks for reporting. Good that you found the reason.

Indeed reading REFERENCE and POINTER variables are not possible with subscribe(), readSymbol() and writeSymbol() methods. See: https://github.com/jisotalo/ads-client#reading-and-writing-pointer-to-and-reference-to-variables

I haven't tested if subscribeRaw() would work if acquiring the indexGroup and indexOffset from the reference using ^ operator.

freefly42 commented 1 year ago

It would be nice if this just worked, but I can certainly live with the workaround. As I looked through the ADS calls from ads-node it appears as though it is creating a handle and possibly subscribing to the handle? Makes sense whey the ADS calls were different. But the ones I posted weren't from subscribe they were from read/write, which was definitely being done with a handle. Anyway, the ads-client implementation does a much better job of managing the PLC state and being stable. Had I been using it in node-red from be beginning I may not find myself directly writing a node client now! Thanks for your help.

jisotalo commented 1 year ago

@freefly42: Well actually I just tried the following and it works:

  1. Get symbol info (size basically) by getSymbolInfo()
  2. Create variable handle to the reference variable
  3. Subscribe to the handle by subscribeRaw

So here is a solution for you. Maybe I will add to the library too.

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

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

client.connect()  
  .then(async res => {
    console.log(`Connected to the ${res.targetAmsNetId}`);

    /**
     * Subscribes to target variable using handles (works with pointers and references)
     * @param {*} variableName 
     * @param {*} callback 
     * @param {*} cycleTime 
     * @param {*} onChange 
     * @param {*} initialDelay 
     * @returns 
     */
    const subscribeByHandle = async (variableName, callback, cycleTime = 10, onChange = true, initialDelay = 0) => {
      //Getting symbol info
      const info = await client.getSymbolInfo(variableName);

      //Creating handle to variable
      const handle = await client.createVariableHandle(variableName);

      //Finally subscribing
      const sub = await client.subscribeRaw(
        ADS.ADS_RESERVED_INDEX_GROUPS.SymbolValueByHandle,
        handle.handle,
        info.size,
        async (data, sub) => {
          //Converting raw data to object and passing forward
          callback(await client.convertFromRaw(data.value, info.type), sub);
        },
        cycleTime,
        onChange,
        initialDelay
      );

      return {
        unsubscribe: async () => {
          await client.unsubscribe(sub.notificationHandle);
          await client.deleteVariableHandle(handle);
        }
      }; 
    }

    //Testing with REFERENCE
    const subREF = await subscribeByHandle('GVL_Test.TestREFERENCE', (data, sub) => {
      console.log('TestREFERENCE data changed:', data);
    }, 100);

    console.log('TestREFERENCE subscribed');

    setTimeout(async () => {
      await subREF.unsubscribe();
      console.log('TestREFERENCE unsubscribed');
    }, 5000);

    //Testing with POINTER
    const subPTR = await subscribeByHandle('GVL_Test.TestPOINTER^', (data, sub) => {
      console.log('TestPOINTER data changed:', data);
    }, 100);

    console.log('TestPOINTER subscribed');

    setTimeout(async () => {
      await subPTR.unsubscribe();
      console.log('TestPOINTER unsubscribed');
    }, 5000);

  })
  .catch(err => {
    console.log('Something failed:', err)
  })
jisotalo commented 1 year ago

@freefly42 did you test the code above?

BrianVanlerberghe commented 1 year ago

@jisotalo , do you have an example where you use the convertFromRaw method to convert an array of pointers? When I use your example above I get an array of buffers. Thanks!

jisotalo commented 1 year ago

@BrianVanlerberghe, could you provide an example PLC code?

BrianVanlerberghe commented 1 year ago

@BrianVanlerberghe, could you provide an example PLC code?

GVL:

example1: ST_Example; example2: ST_Example; example3: ST_Example;

exampleArray : ARRAY [0..2] OF POINTER TO ST_Example;

PRG: GVL.exampleArray[0] := ADR(GVL.example1); GVL.exampleArray[1] := ADR(GVL.example2); GVL.exampleArray[2] := ADR(GVL.example3);

This is an example of how we create an array of pointers. We have a lot of nested structs of the same type and flatlist them by creating an array of pointers to that type.

Thanks for your reply!

jisotalo commented 1 year ago

The following works fine (see previous example) but you need to read array indexes one-by-one:

  const subPTR = await subscribeByHandle('GVL.exampleArray[0]^', (data, sub) => {
    console.log('exampleArray data changed:', data);
  }, 100);

  console.log('exampleArray subscribed');

  setTimeout(async () => {
    await subPTR.unsubscribe();
    console.log('exampleArray unsubscribed');
  }, 5000);

Or reading instead of subscribing:

const info = await client.getSymbolInfo('GVL.exampleArray[0]^');
const handle = await client.createVariableHandle('GVL.exampleArray[0]^');
const value = await client.convertFromRaw(await client.readRawByHandle(handle, info.size), info.type);
await client.deleteVariableHandle(handle);

console.log('Value is:', value);
freefly42 commented 1 year ago

@freefly42 did you test the code above?

Yes, it works. Thanks!