node-opcua

Call OPCUA method with struct input argument using node-opcua


I am trying to interface with an RFID reader which implements an OPC-UA server according to this specification.

I am trying to call the method ScanStart which takes the ScanSettings struct as an input argument (an AutoID datatype) but despite reading through the examples and documentation I can't figure out a way to do this.

Using UAExpert I can call the method and enter the values for the struct using the GUI which produces the following dump in wireshark:

    ArraySize: 1
    [0]: Variant
        Variant Type: ExtensionObject (0x16)
        Value: ExtensionObject
            TypeId: ExpandedNodeId
                EncodingMask: 0x01, EncodingMask: Four byte encoded Numeric
                    .... 0001 = EncodingMask: Four byte encoded Numeric (0x1)
                    .0.. .... = has server index: False
                    0... .... = has namespace uri: False
                Namespace Index: 3
                Identifier Numeric: 5015
            EncodingMask: 0x01, has binary body
                .... ...1 = has binary body: True
                .... ..0. = has xml body: False
            ByteString: 0000000000000000000000000000000000

Has anyone successfully managed to register an ExtensionObject for passing to a method call using node-opcua? At this point I am happy to just send the ByteString above without needing to encode/decode the struct as it is always static.

Apparently there is a constructExtensionObject method. The client code I have for this is:

(async () => {

    const client = OPCUAClient.create({ endpoint_must_exist: false});
    client.on("backoff", () => console.log("Backoff: trying to connect to ", endpointUri));

    await client.withSessionAsync(endpointUri, async (session) => {
        let scanSettings = {
            Duration: 0,
            Cyles: 0,
            DataAvailble: false
        };
        const nodeID = new NodeId(NodeIdType.STRING, "rfr310.ScanStart.InputArguments", 4);
        const extObj = session.constructExtensionObject(nodeID, scanSettings);

        const methodsToCall = [
            {
                objectId: "ns=4;s=rfr310",
                methodId: "ns=4;s=rfr310.ScanStart",
                inputArguments: [extObj]
            }
        ];

        extObj.then(() => {
            session.call(methodsToCall,(err,results) => {
                if (err) {
                    console.log(err);
                } else {
                    console.log(results);
                }
            });
        }).catch(() => {
        })
    });
})();

produces the error "dispose when pendingTransactions is not empty", which is caught by the extObj.catch()

What am I doing wrong? I'm fairly certain this is a promise handling issue on my part...

Any help is appreciated!


Solution

  • OK so I finally got there. Here is the method to call an OPC-UA method with a struct input argument using node-opcua:

    const { OPCUAClient, NodeId, NodeIdType, DataType} = require("node-opcua");
    
    const endpointUri = "opc.tcp://<your-endpoint>:<your-port>";
    
    (async () => {
    
        const client = OPCUAClient.create({ endpoint_must_exist: false});
        client.on("backoff", () => console.log("Backoff: trying to connect to ", endpointUri));
    
        await client.withSessionAsync(endpointUri, async (session) => {
            // Scan settings value input
            const scanSettingsParams = {
                duration: 0,
                cycles : 0,
                dataAvailable : false,
                locationType: 0
            };
    
            try {
                // NodeID for InputArguments struct type (inherits from ScanSettings)
                const nodeID = new NodeId(NodeIdType.NUMERIC, 3010, 3);
                // Create ExtensionObject for InputArguments
                const scanSettingsObj = await session.constructExtensionObject(nodeID, scanSettingsParams);
    
                // Populate Method call with ExtensionObject as InputArgument
                const methodToCall = {
                        objectId: "ns=4;s=rfr310",
                        methodId: "ns=4;s=rfr310.ScanStart",
                        inputArguments: [
                            {
                                dataType: DataType.ExtensionObject,
                                value: scanSettingsObj
                            }
                        ]
                    };
    
                    // Call method, passing ScanSettings as input argument
                    session.call(methodToCall,(err,results) => {
                        if (err) {
                            console.log(err);
                        } else {
                            console.log(results);
                        }
                    });
    
            } catch (err) {
                console.log(err);
            }
        });
    })();