@opcua/for-node-red
Version:
The Node-RED node to communicate via OPC UA, powered NodeOPCUA and developed by Sterfive's team
112 lines (111 loc) • 11.6 kB
JSON
[
{
"id": "0ca41155c5f07e18",
"type": "tab",
"label": "OPCUA Server - TemperatureSensor",
"disabled": false,
"info": "",
"env": []
},
{
"id": "44b39231a044d020",
"type": "function",
"z": "0ca41155c5f07e18",
"name": "TemperatureSensor",
"func": "const sterfive = global.get(\"sterfive\");\nif (!sterfive) {\n node.error(\"global.get('sterfive') is not set — is opcua-for-nodered loaded?\");\n} else {\n\n node.status({fill:\"blue\",shape:\"ring\",text:\"configuring ...\"});\n const { opcua, bootstrap } = sterfive;\n const { bootstrapServer } = bootstrap;\n\n const cfg = msg.config || {};\n\n const handle = await bootstrapServer({\n port: cfg.port ?? 4840,\n endpoint: cfg.endpoint || \"node-red-server\",\n forceRebuild: true,\n nodesets: [\n \"standard\",\n \"di\",\n \"ia\",\n \"machinery\"\n ],\n onPopulate: (addressSpace, exposed) => {\n createTemperatorSensorType(opcua,addressSpace);\n const { setRawValue } = instantiateTemperatureSensor(opcua, addressSpace);\n exposed.setRawValue = setRawValue;\n node.status({fill:\"orange\",shape:\"ring\",text:\"configured ...\"});\n },\n });\n \n \n flow.set(\"sterfive$onRawTemperatureChange\", handle.exposed.setRawValue);\n const a = flow.get(\"sterfive$onRawTemperatureChange\");\n node.send({ \n payload: \n `OPC UA Server (globals path) running at ${handle.server.getEndpointUrl()}`\n });\n \n if (handle.isRunning) {\n node.status({fill:\"green\",shape:\"ring\",text:\"listening ...\"});\n }\n\n}\n\nfunction createTemperatorSensorType(opcua, addressSpace) {\n const namespace = addressSpace.getOwnNamespace();\n const nsDi = addressSpace.getNamespaceIndex(\"http://opcfoundation.org/UA/DI/\");\n if (nsDi === -1) {\n throw new Error(\"DI namespace not found in address space\");\n }\n const componentType = addressSpace.findObjectType(\"DeviceType\", nsDi);\n if (!componentType) {\n throw new Error(\"ComponentType not found in DI namespace\");\n }\n const temperatureSensorType = namespace.addObjectType({\n browseName: \"TemperatureSensorType\",\n subtypeOf: componentType,\n eventNotifier: 0x01, // This object can generate events\n });\n const parameterSet = namespace.addObject({\n browseName: { name: \"ParameterSet\", namespaceIndex: nsDi },\n componentOf: temperatureSensorType,\n modellingRule: \"Mandatory\",\n });\n const _uaTemperature = namespace.addAnalogDataItem({\n browseName: \"Temperature\",\n componentOf: parameterSet,\n dataType: \"Double\",\n minimumSamplingInterval: 500,\n engineeringUnitsRange: { low: -50, high: 100 },\n engineeringUnits: opcua.standardUnits.degree_celsius,\n modellingRule: \"Mandatory\",\n });\n namespace.addVariable({\n browseName: { name: \"CalibrationDate\", namespaceIndex: namespace.index },\n dataType: \"DateTime\",\n componentOf: parameterSet,\n modellingRule: \"Mandatory\",\n });\n namespace.addVariable({\n browseName: { name: \"SensorStatus\", namespaceIndex: namespace.index },\n dataType: \"Int32\",\n componentOf: parameterSet,\n modellingRule: \"Optional\",\n });\n const methodSet = namespace.addObject({\n browseName: { name: \"MethodSet\", namespaceIndex: nsDi },\n componentOf: temperatureSensorType,\n modellingRule: \"Mandatory\",\n });\n namespace.addMethod(methodSet, {\n browseName: { name: \"Calibrate\", namespaceIndex: namespace.index },\n modellingRule: \"Mandatory\",\n inputArguments: [\n {\n name: \"CoefficientA\",\n dataType: opcua.DataType.Double,\n },\n {\n name: \"CoefficientB\",\n dataType: opcua.DataType.Double,\n },\n // T = a * raw + b\n ],\n outputArguments: [\n {\n name: \"Success\",\n dataType: opcua.DataType.Boolean,\n },\n ],\n });\n}\nconst HealthStatus = {\n Normal: 0,\n Failure: 1,\n CheckFunction: 2,\n OffSpec: 3,\n MaintenanceRequired: 4,\n};\nfunction instantiateTemperatureSensor(opcua, addressSpace) {\n\n const namespace = addressSpace.getOwnNamespace();\n const ownNamespaceIndex = namespace.index;\n const temperationSensorType = addressSpace.findObjectType(\"TemperatureSensorType\", ownNamespaceIndex);\n if (!temperationSensorType) {\n throw new Error(\"TemperatureSensorType not found in address space\");\n }\n const nsDi = addressSpace.getNamespaceIndex(\"http://opcfoundation.org/UA/DI/\");\n if (nsDi === -1) {\n throw new Error(\"DI namespace not found in address space\");\n }\n const deviceSetFolder = addressSpace.findNode(`ns=${nsDi};i=5001`); // DeviceSet folder in DI namespace\n if (!deviceSetFolder) {\n // DeviceSet folder in DI namespace\n throw new Error(\"DeviceSet folder not found in DI namespace\");\n }\n const uaTemperatureSensor = temperationSensorType.instantiate({\n browseName: \"MyTemperatureSensor\",\n organizedBy: deviceSetFolder, // Assuming deviceSetFolder is defined in the address space\n eventSourceOf: addressSpace.rootFolder.objects.server, // Make the sensor an event source\n \n });\n let coefA = 1;\n let coefB = 0;\n let rawValue = 0;\n const methodSet = uaTemperatureSensor.getComponentByName(\"MethodSet\", nsDi);\n if (!methodSet) {\n throw new Error(\"MethodSet not found in TemperatureSensorType\");\n }\n const calibrateMethod = methodSet.getChildByName(\"Calibrate\");\n if (!calibrateMethod) {\n throw new Error(\"Calibrate method not found in MethodSet\");\n }\n const deviceHealth = uaTemperatureSensor.getComponentByName(\"DeviceHealth\", nsDi);\n if (deviceHealth) {\n deviceHealth.setValueFromSource({ dataType: opcua.DataType.Int32, value: 0 }); // 0 = Good\n }\n // need calibration\n deviceHealth?.setValueFromSource({ dataType: opcua.DataType.Int32, value: HealthStatus.MaintenanceRequired });\n const parameterSet = uaTemperatureSensor.getComponentByName(\"ParameterSet\", nsDi);\n if (!parameterSet) {\n throw new Error(\"ParameterSet not found in TemperatureSensorType\");\n }\n const calibrationDate = parameterSet.getChildByName(\"CalibrationDate\", 1);\n calibrationDate?.setValueFromSource({ dataType: opcua.DataType.DateTime, value: new Date() });\n calibrateMethod.bindMethod(async (inputArguments, context) => {\n // ensure that the current user has the necessary permissions to execute this method\n if (!context?.session?.getSessionId()) {\n return { statusCode: opcua.StatusCodes.BadUserAccessDenied };\n }\n const coefficientA = inputArguments[0]?.value || 1.0;\n const coefficientB = inputArguments[1]?.value || 0.0;\n // Here you would implement the actual calibration logic using the coefficients\n console.log(`Calibrating sensor with CoefficientA: ${coefficientA}, CoefficientB: ${coefficientB}`);\n // For demonstration, we'll just return success without doing anything\n coefA = coefficientA;\n coefB = coefficientB;\n setRawValue(rawValue); // Recalculate the temperature with the new coefficients\n const success = true;\n deviceHealth?.setValueFromSource({ dataType: opcua.DataType.Int32, value: HealthStatus.Normal });\n calibrationDate?.setValueFromSource({ dataType: opcua.DataType.DateTime, value: new Date() });\n return {\n statusCode: opcua.StatusCodes.Good,\n outputArguments: [{ dataType: opcua.DataType.Boolean, value: success }],\n };\n });\n const uaTemperatureVariable = parameterSet\n .getChildByName(\"Temperature\", 1);\n if (!uaTemperatureVariable) {\n throw new Error(\"Temperature variable not found in ParameterSet\");\n }\n const setRawValue = (newRawValue) => {\n rawValue = newRawValue;\n const calibratedValue = coefA * rawValue + coefB;\n uaTemperatureVariable?.setValueFromSource({ dataType: opcua.DataType.Double, value: calibratedValue });\n };\n const exclusiveLimitAlarmType = addressSpace.findEventType(\"ExclusiveLimitAlarmType\");\n if (!exclusiveLimitAlarmType) {\n throw new Error(\"cannot find ExclusiveLimitAlarmType in namespace 0\");\n }\n namespace.instantiateExclusiveLimitAlarm(exclusiveLimitAlarmType, {\n browseName: \"TemperatureConddition\",\n componentOf: uaTemperatureSensor,\n conditionName: \"TemperatureCondition\",\n conditionSource: uaTemperatureSensor,\n highHighLimit: 50,\n highLimit: 40,\n inputNode: uaTemperatureVariable, // the variable that will be monitored for change\n lowLimit: 10,\n setpointNode: null,\n });\n return { setRawValue };\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 430,
"y": 140,
"wires": [
[
"f713e3745e933393"
]
]
},
{
"id": "d57386a79225e964",
"type": "inject",
"z": "0ca41155c5f07e18",
"name": "Start Server",
"props": [
{
"p": "config",
"v": "{ \"port\": 26550 }",
"vt": "json"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 210,
"y": 140,
"wires": [
[
"44b39231a044d020"
]
]
},
{
"id": "f713e3745e933393",
"type": "debug",
"z": "0ca41155c5f07e18",
"name": "debug output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 770,
"y": 180,
"wires": []
},
{
"id": "035721b7e2d4bef5",
"type": "function",
"z": "0ca41155c5f07e18",
"name": "Update Raw Temperature",
"func": "const setRawValue = flow.get(\"sterfive$onRawTemperatureChange\");\nif (!setRawValue) return;\n\ntry {\n const rawValue = Math.sin(Date.now() / 10000) * 30 + 40;\n setRawValue(rawValue);\n} catch(err) {\n return { ...msg, payload: \"Errro=\" + err.message};\n}\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 450,
"y": 240,
"wires": [
[
"f713e3745e933393"
]
]
},
{
"id": "cbde8d32bfca9fc0",
"type": "inject",
"z": "0ca41155c5f07e18",
"name": "Simulate",
"props": [],
"repeat": "0.5",
"crontab": "",
"once": false,
"onceDelay": "2",
"topic": "",
"x": 200,
"y": 240,
"wires": [
[
"035721b7e2d4bef5"
]
]
}
]