OPC-UA Server in RIO using Node-RED

I understand there is a way of using a Node-RED flow to allow the RIO to become an OPC-UA Server and respond to external OPC-UA Clients.
Can anyone provide an example of how to achieve this, please?

Hi Darran. Welcome to the forums.

Did you try and install it? Its helpful sometimes to let us know what you have tried, what failed, what errors you got and such.

We have it working here, I will write it up in a moment, but I was wondering what you got on your end.

Here is how we got RIO to be an OPCUA server through Node-RED.

First up, I want to say this is a bit inelegant. It works, but eh, it just does not seem like it’s the right approach. There is a bit of code wrangling to be done and in fair warning, both myself and a colleague testing this had the node crash hard, to the point where we had to delete our projects and start over… Sure, we got it working, but you have been warned… Not only, but also, I did not configure security on the node. I wanted anonymous access so groov View (my test client) could connect.

Open Node-RED and install the node-red-contrib-opcua-server (node) - Node-RED node.
Big note here… This node takes around 15 minutes to install. (It ends up installing around 370+ packages which in itself should be a bit of a warning).
Its worth opening the log so you can watch it install everything.
It will look like nothing is happening for a good 3-5 minutes, just be patient… It will install.

Once you have the node installed we next need to configure it.
Here is a sample flow that will get you up and running with your RIO.

We have the context inputs and outputs, the OPCUA server node and a small flow that shows how to read a RIO input (temperature) and send it to the context input.

Here is the flow code.

[{"id":"936a5d0c.9f6818","type":"tab","label":"OPC-UA Server","disabled":false,"info":""},{"id":"2b151ee0.48eb52","type":"opcua-compact-server","z":"936a5d0c.9f6818","port":54845,"endpoint":"","productUri":"","acceptExternalCommands":true,"maxAllowedSessionNumber":"10","maxConnectionsPerEndpoint":"10","maxAllowedSubscriptionNumber":"100","alternateHostname":"","name":"","showStatusActivities":false,"showErrors":true,"allowAnonymous":true,"individualCerts":false,"isAuditing":false,"serverDiscovery":true,"users":[],"xmlsetsOPCUA":[],"publicCertificateFile":"","privateCertificateFile":"","registerServerMethod":"1","discoveryServerEndpointUrl":"opc.tcp://rio-ben:54845","capabilitiesForMDNS":"","maxNodesPerRead":1000,"maxNodesPerWrite":1000,"maxNodesPerHistoryReadData":100,"maxNodesPerBrowse":3000,"maxBrowseContinuationPoints":"10","maxHistoryContinuationPoints":"10","delayToInit":"1000","delayToClose":"200","serverShutdownTimeout":"100","addressSpaceScript":"function constructAlarmAddressSpace(server, addressSpace, eventObjects, done) {\n  // server = the created node-opcua server\n  // addressSpace = address space of the node-opcua server\n  // eventObjects = add event variables here to hold them in memory from this script\n\n  // internal sandbox objects are:\n  // node = the compact server node,\n  // coreServer = core compact server object for debug and access to NodeOPCUA\n  // this.sandboxNodeContext = node context node-red\n  // this.sandboxFlowContext = flow context node-red\n  // this.sandboxGlobalContext = global context node-red\n  // this.sandboxEnv = env variables\n  // timeout and interval functions as expected from nodejs\n\n  const opcua = coreServer.choreCompact.opcua;\n  const LocalizedText = opcua.LocalizedText;\n  const namespace = addressSpace.getOwnNamespace();\n\n  const Variant = opcua.Variant;\n  const DataType = opcua.DataType;\n  const DataValue = opcua.DataValue;\n\n  var flexServerInternals = this;\n\n  this.sandboxFlowContext.set(\"isoInput1\", 0);\n  this.setInterval(() => {\n    flexServerInternals.sandboxFlowContext.set(\n      \"isoInput1\",\n      Math.random() + 50.0\n    );\n  }, 500);\n  this.sandboxFlowContext.set(\"isoInput2\", 0);\n  this.sandboxFlowContext.set(\"isoInput3\", 0);\n  this.sandboxFlowContext.set(\"isoInput4\", 0);\n  this.sandboxFlowContext.set(\"isoInput5\", 0);\n  this.sandboxFlowContext.set(\"isoInput6\", 0);\n  this.sandboxFlowContext.set(\"isoInput7\", 0);\n  this.sandboxFlowContext.set(\"isoInput8\", 0);\n\n  this.sandboxFlowContext.set(\"isoOutput1\", 0);\n  this.setInterval(() => {\n    flexServerInternals.sandboxFlowContext.set(\n      \"isoOutput1\",\n      Math.random() + 10.0\n    );\n  }, 500);\n\n  this.sandboxFlowContext.set(\"isoOutput2\", 0);\n  this.sandboxFlowContext.set(\"isoOutput3\", 0);\n  this.sandboxFlowContext.set(\"isoOutput4\", 0);\n  this.sandboxFlowContext.set(\"isoOutput5\", 0);\n  this.sandboxFlowContext.set(\"isoOutput6\", 0);\n  this.sandboxFlowContext.set(\"isoOutput7\", 0);\n  this.sandboxFlowContext.set(\"isoOutput8\", 0);\n\n  coreServer.debugLog(\"init dynamic address space\");\n  const rootFolder = addressSpace.findNode(\"RootFolder\");\n\n  node.warn(\"construct new address space for OPC UA\");\n\n  const myDevice = namespace.addFolder(rootFolder.objects, {\n    \"browseName\": \"groov_rio_hostname\"\n  });\n  const gpioFolder = namespace.addFolder(myDevice, { \"browseName\": \"rio_tags\" });\n  const isoInputs = namespace.addFolder(gpioFolder, {\n    \"browseName\": \"Inputs\"\n  });\n  const isoOutputs = namespace.addFolder(gpioFolder, {\n    \"browseName\": \"Outputs\"\n  });\n\n  const gpioDI1 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I1\",\n    \"nodeId\": \"ns=1;s=Isolated_Input1\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput1\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput1\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI2 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I2\",\n    \"nodeId\": \"ns=1;s=Isolated_Input2\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput2\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput2\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI3 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I3\",\n    \"nodeId\": \"ns=1;s=Isolated_Input3\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput3\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput3\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI4 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I4\",\n    \"nodeId\": \"ns=1;s=Isolated_Input4\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput4\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput4\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI5 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I5\",\n    \"nodeId\": \"ns=1;s=Isolated_Input5\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput5\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput5\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI6 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I6\",\n    \"nodeId\": \"ns=1;s=Isolated_Input6\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput6\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput6\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI7 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I7\",\n    \"nodeId\": \"ns=1;s=Isolated_Input7\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput7\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput7\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDI8 = namespace.addVariable({\n    \"organizedBy\": isoInputs,\n    \"browseName\": \"I8\",\n    \"nodeId\": \"ns=1;s=Isolated_Input8\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoInput8\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoInput8\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO1 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O1\",\n    \"nodeId\": \"ns=1;s=Isolated_Output1\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput1\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput1\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO2 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O2\",\n    \"nodeId\": \"ns=1;s=Isolated_Output2\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput2\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput2\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO3 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O3\",\n    \"nodeId\": \"ns=1;s=Isolated_Output3\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput3\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput3\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO4 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O4\",\n    \"nodeId\": \"ns=1;s=Isolated_Output4\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput4\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput4\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO5 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O5\",\n    \"nodeId\": \"ns=1;s=Isolated_Output5\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput5\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput5\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO6 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O6\",\n    \"nodeId\": \"ns=1;s=Isolated_Output6\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput6\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput6\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO7 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O7\",\n    \"nodeId\": \"ns=1;s=Isolated_Output7\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput7\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput7\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  const gpioDO8 = namespace.addVariable({\n    \"organizedBy\": isoOutputs,\n    \"browseName\": \"O8\",\n    \"nodeId\": \"ns=1;s=Isolated_Output8\",\n    \"dataType\": \"Double\",\n    \"value\": {\n      \"get\": function() {\n        return new Variant({\n          \"dataType\": DataType.Double,\n          \"value\": flexServerInternals.sandboxFlowContext.get(\"isoOutput8\")\n        });\n      },\n      \"set\": function(variant) {\n        flexServerInternals.sandboxFlowContext.set(\n          \"isoOutput8\",\n          parseFloat(variant.value)\n        );\n        return opcua.StatusCodes.Good;\n      }\n    }\n  });\n\n  //------------------------------------------------------------------------------\n  // Add a view\n  //------------------------------------------------------------------------------\n  const viewDI = namespace.addView({\n    \"organizedBy\": rootFolder.views,\n    \"browseName\": \"RPIW0-Digital-Ins\"\n  });\n\n  const viewDO = namespace.addView({\n    \"organizedBy\": rootFolder.views,\n    \"browseName\": \"RPIW0-Digital-Outs\"\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI1.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI2.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI3.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI4.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI5.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI6.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI7.nodeId\n  });\n\n  viewDI.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDI8.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO1.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO2.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO3.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO4.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO5.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO6.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO7.nodeId\n  });\n\n  viewDO.addReference({\n    \"referenceType\": \"Organizes\",\n    \"nodeId\": gpioDO8.nodeId\n  });\n\n  coreServer.debugLog(\"create dynamic address space done\");\n  node.warn(\"construction of new address space for OPC UA done\");\n\n  done();\n}\n","x":950,"y":180,"wires":[]},{"id":"200feeb9.bfacaa","type":"inject","z":"936a5d0c.9f6818","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":"0.5","x":220,"y":120,"wires":[["476a9040.11b04"]]},{"id":"476a9040.11b04","type":"function","z":"936a5d0c.9f6818","name":"set flow context Inputs","func":"// flow.set('isoInput1', Math.random() + 11.0) interval comes from server\n//flow.set('isoInput2', Math.random() + 12.0) value comes from other flow/RIO analog input\nflow.set('isoInput3', Math.random() + 13.0)\nflow.set('isoInput4', Math.random() + 14.0)\nflow.set('isoInput5', Math.random() + 15.0)\nflow.set('isoInput6', Math.random() + 16.0)\nflow.set('isoInput7', Math.random() + 17.0)\nflow.set('isoInput8', Math.random() + 18.0)\n\nmsg.payload = [\n    flow.get('isoInput1'),\n    flow.get('isoInput2'),\n    flow.get('isoInput3'),\n    flow.get('isoInput4'),\n    flow.get('isoInput5'),\n    flow.get('isoInput6'),\n    flow.get('isoInput7'),\n    flow.get('isoInput8'),\n]\nreturn msg;","outputs":1,"noerr":0,"x":470,"y":120,"wires":[["4e41f021.087598"]]},{"id":"4e41f021.087598","type":"debug","z":"936a5d0c.9f6818","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":720,"y":120,"wires":[]},{"id":"e5129784.896498","type":"function","z":"936a5d0c.9f6818","name":"set flow context Outputs","func":"// flow.set('isoOutput1', Math.random() + 1.0) interval comes from server\nflow.set('isoOutput2', Math.random() + 2.0)\nflow.set('isoOutput3', Math.random() + 3.0)\nflow.set('isoOutput4', Math.random() + 4.0)\nflow.set('isoOutput5', Math.random() + 5.0)\nflow.set('isoOutput6', Math.random() + 6.0)\nflow.set('isoOutput7', Math.random() + 7.0)\nflow.set('isoOutput8', Math.random() + 8.0)\n\nmsg.payload = [\n    flow.get('isoOutput1'),\n    flow.get('isoOutput2'),\n    flow.get('isoOutput3'),\n    flow.get('isoOutput4'),\n    flow.get('isoOutput5'),\n    flow.get('isoOutput6'),\n    flow.get('isoOutput7'),\n    flow.get('isoOutput8'),\n]\nreturn msg;","outputs":1,"noerr":0,"x":480,"y":180,"wires":[["6d45a39e.02195c"]]},{"id":"86a861c5.0f0718","type":"inject","z":"936a5d0c.9f6818","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":true,"onceDelay":"0.5","x":230,"y":180,"wires":[["e5129784.896498"]]},{"id":"6d45a39e.02195c","type":"debug","z":"936a5d0c.9f6818","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":720,"y":180,"wires":[]},{"id":"a0f4e532.2b9a5","type":"debug","z":"936a5d0c.9f6818","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":870,"y":300,"wires":[]},{"id":"2f76f11a.9d371e","type":"function","z":"936a5d0c.9f6818","name":"set flow context Inputs","func":"// flow.set('isoInput1', Math.random() + 11.0) interval comes from server\n//flow.set('isoInput2', Math.random() + 12.0) value comes from other flow/RIO analog input\nflow.set('isoInput2', msg.temperature)\nflow.set('isoInput3', Math.random() + 13.0)\nflow.set('isoInput4', Math.random() + 14.0)\nflow.set('isoInput5', Math.random() + 15.0)\nflow.set('isoInput6', Math.random() + 16.0)\nflow.set('isoInput7', Math.random() + 17.0)\nflow.set('isoInput8', Math.random() + 18.0)\n\nmsg.payload = [\n    flow.get('isoInput1'),\n    flow.get('isoInput2'),\n    flow.get('isoInput3'),\n    flow.get('isoInput4'),\n    flow.get('isoInput5'),\n    flow.get('isoInput6'),\n    flow.get('isoInput7'),\n    flow.get('isoInput8'),\n]\nreturn msg;","outputs":1,"noerr":0,"x":680,"y":300,"wires":[["a0f4e532.2b9a5"]]},{"id":"3233df67.9e73c","type":"groov-io-read","z":"936a5d0c.9f6818","device":"8107b30e.59a1a","dataType":"channel-analog","moduleIndex":"0","channelIndex":"0","mmpAddress":"0xF0D81000","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"temperature","valueType":"msg","itemName":"","name":"","x":460,"y":300,"wires":[["2f76f11a.9d371e"]]},{"id":"54962131.69d78","type":"inject","z":"936a5d0c.9f6818","name":"5 seconds","topic":"","payload":"","payloadType":"date","repeat":"5","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":300,"wires":[["3233df67.9e73c"]]},{"id":"8107b30e.59a1a","type":"groov-io-device","z":"","address":"localhost","msgQueueFullBehavior":"DROP_OLD"}]

The function node code works hand in hand with the server configuration. I will leave it to you to explore that relationship. The example flow gives 10 inputs and 10 outputs, so you can just copy/paste the examples and expand or subtract as needed.

When it comes the RIO, there are a few settings I’d like to point out.
First, double click on the OPCUA node and look at the first tab;

I accepted the default port number, you can change it, but it must be higher than 1024.

The Linux OS on the groovs will not allow you to use lower port numbers.
Once you set the port number, open up the RIO firewall to match.

Next, you want to click on the ‘Discovery’ tab and setup your Endpoint URL.

Its a bit counter-intutive, but I left the server method as ‘Hidden’ and it works as expected. I was able to discover the server and get the tag data without issue. There is not a lot of documentation on the node, so I just went with what worked.
The endpoint I used was the RIO hostname and set the port to match.

To test the OPCUA server I used groov View Server running on a Windows PC.

Of course you can edit the name of the server and such in the OPCUA node.

Lastly, after you deploy, it seems to take a solid minute or so for the server to fully spin up.
You are looking for the following two messages to show up in your debug tab;

It seems that once those messages show up, you are Ok to connect.

And with that, hopfully you can get up and running.

@Beno - Ben, thanks for replying so quickly. We are expecting delivery of our first RIO soon and I will try out your suggestions once I have it in my hands and let you know how I get on.

Hi Ben, thanks for the flow and detailed description of setting this up - It worked great!!
Only 8-mins to install :timer_clock:
I have groov View installed on my phone, but unfortunately, I couldn’t use it to test the server as it doesn’t appear to support opc.tcp://… type URL. So, I downloaded Matrikon FLEX and it connected no problem.

When installing the OPC nodes, I noticed another that’s been around about a week, so I might have a play with this too!!!

Thanks for the great support!

1 Like

Glad you got it working, thanks for reporting back, always helpful to know the instructions worked!

I used it fine on groov View, I think you have the device configured incorrectly.
Here is the URL you need;

We also tested the nodes you linked to, got them working, but there was more involved.
That said, they did provide a way to make your own tag paths, so that may be something you find helpful.
I strongly advise you don’t install both, we did and it did not end well. I think the two servers beat each other up inside my RIO to be THE chosen server!

Just got your update as the 2nd OPC node install completed - Now being removed!!!

Are the groov View settings you are showing from Windows version? I am using the mobile App, which gives a Connection Failed error, reason: Unsupported URL.
In the settings on the App, I set the Hostname to opc.tcp://rio ip address and Port to 54845.

groov View is the same on EPIC as groov Server for Windows. So the URL to the OPCUA server is the same for both.

I think what you are talking about is the groov View app for iOS or Android.
The app provides a smooth experience for connecting to groov View running on either EPIC or Windows.
In the case of the app you configure the URL of the EPIC itself… something like https://EPICIPorHostname.
Here is a screen shot of my Android app configured for my EPIC Learning Center.

On the EPIC in groov Build, you then configure the RIOs OPCUA URL like my last post.
With that, you should be up and running.

If not, just drop back here and show me a screen shot or two and we will get it sorted.

Yes, that’s what I meant, I am trying to use the groov View App on iOS phone to connect to the RIO. I don’t want to go via groov Server for Windows nor an EPIC.
I wrongly assumed that the groov View App supported OPC-UA Client connection.

The groov View server is not part of RIO, there just was not the need since it has a limited I/O count.

The only only GUI that you can get working on RIO are the Dashboard Nodes.
But even then, you wont be able to use the groov View app on your phone to connect to those nodes either since the app has been built just for groov View… Just use your phones web browser to view the mini web server that the dashboard nodes create.

It’s OK Ben, our client only needs OPC-UA connection for their ERP software, so that’s what I was testing with a phone App. Matrikon FLEX proved that the RIO was successfully serving the data using OPC-UA thanks to your help!
Next step is to achieve this remotely as they have quite a number of sites with small IO counts, which currently use G1 IO and are looking to upgrade.
I suggested RIO, so now to put it on the Internet securely to prove concept :slightly_smiling_face:

Sounds like a perfect use case of the RIOs built in VPN client.
Should be up and running in no time with that.
Best of luck and thanks for letting us know how its all going. Appreciate the feedback.

Thinking about it a little more… Is it worth taking a step back and looking at the bigger picture with the new tools that the RIO has built in…?

Sounds like this application is perfect for MQTT. With it built in to RIO, you can quickly and securely publish each remote sites I/O data to a central broker without even using Node-RED. No installing nodes, no configureing nodes, just publish the encrypted data and your done.
You would not even have to spin up a VPN server. Install the server/broker certificate on the RIO, configure its URL, turn on the points and you are done.

Seems so much faster, cleaner, more secure and so much simpler to maintain… I really think its worth considering…

Do you mean VPN client here, or did I miss something?

Sorry, will go back and fix the typo, so this thread will be a little odd…

Thought about using MQTT in the beginning but clients ERP needs a OPC-UA to get the data.
Unless there is a cloud service that can provide MQTT to OPC-UA, then I think the way forward is the server and VPN on the RIO.
Also keeps middleware to a minimum!!!