Send a message from a custom function node without output option

Hi!

I would like to send a message from a custom function node without an output option to "trigger" (like an inject node) another function node. I found a workaround using the node.error("done", msg) function and a catch node that acts like a trigger, but I am not satisfied with it (I don't like using catch error for something that is not an error...) and it doesn't work in some circumstances.
In fact, in the real application, the custom function node raises an error:

Error: ServiceResult is BadInternalError (0x80020000) request was WriteRequest

The same result, in my case, I think can be better achieved by modifying a global variable value and having an event listener that can trigger (inject to) a function node. However, I cannot find a solution without continuous polling the global variable to evaluate the old value until it changes, and I am concerned about the potential performance impact.

Please find below the JSON of the flow with the catch error "solution" to better understand what I mean.

Any help will be appreciated, thank you!

[{"id":"7aa9f6da.a3eeb8","type":"tab","label":"Test","disabled":false,"info":""},{"id":"a01ee6efcd61598c","type":"inject","z":"7aa9f6da.a3eeb8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":410,"y":200,"wires":[["b274d4f1a6a7668b"]]},{"id":"b274d4f1a6a7668b","type":"function","z":"7aa9f6da.a3eeb8","name":"","func":"node.error(\"test\", msg)","outputs":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":200,"wires":[]},{"id":"c3bdcbe7e28f6755","type":"catch","z":"7aa9f6da.a3eeb8","name":"","scope":["b274d4f1a6a7668b"],"uncaught":false,"x":390,"y":320,"wires":[["b03d81d80d8da749"]]},{"id":"b03d81d80d8da749","type":"function","z":"7aa9f6da.a3eeb8","name":"","func":"if (msg.error.message === \"test\") {\n    msg.payload = \"hello\"\n    return msg;\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":320,"wires":[["e85eeaa9aa4f5c7a"]]},{"id":"e85eeaa9aa4f5c7a","type":"debug","z":"7aa9f6da.a3eeb8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":320,"wires":[]}]

I tried using the node.error() function and a catch node, but this is not the right solution. I am trying to trigger a function node from another custom function node that does not have an output option.

The same result can be better achieved by setting up an event listener that monitors changes to the value of a global variable written in the custom function node. However, I am concerned about the performance impact if continuous polling is required.

Your "ask" is pretty much "anti-pattern" and there will likely be a better way (like using link call node or adding your function to flow/global context and just calling the function when needed)

If you explain why you want to call to another function without using wires or traditional means, we might be able to better advise.

1 Like

As Steve suggest a link call would be the simplest route.
e.g

[{"id":"a01ee6efcd61598c","type":"inject","z":"7aa9f6da.a3eeb8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":140,"wires":[["b274d4f1a6a7668b"]]},{"id":"b274d4f1a6a7668b","type":"function","z":"7aa9f6da.a3eeb8","name":"f1","func":"msg.target = \"function2\";\nmsg.payload =\"test\";\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":120,"wires":[["8d674f767f74cb2d"]]},{"id":"8d674f767f74cb2d","type":"link call","z":"7aa9f6da.a3eeb8","name":"","links":[],"linkType":"dynamic","timeout":"5","x":580,"y":120,"wires":[[]]},{"id":"926e7f7865f3f275","type":"link in","z":"7aa9f6da.a3eeb8","name":"function2","links":[],"x":315,"y":260,"wires":[["b03d81d80d8da749","b77b787afca53813"]]},{"id":"b77b787afca53813","type":"link out","z":"7aa9f6da.a3eeb8","name":"link out 80","mode":"return","links":[],"x":605,"y":260,"wires":[]},{"id":"b03d81d80d8da749","type":"function","z":"7aa9f6da.a3eeb8","name":"f2","func":"if (msg.payload === \"test\") {\n    msg.payload = \"hello\"\n    return msg;\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":320,"wires":[["e85eeaa9aa4f5c7a"]]},{"id":"e85eeaa9aa4f5c7a","type":"debug","z":"7aa9f6da.a3eeb8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":320,"wires":[]}]

[edit] Forgot to add return to calling link call.

Thank you for your replies.

I found another solution using the node.status() function, and it works as expected. However, I will share with you the test flow:

[{"id":"7aa9f6da.a3eeb8","type":"tab","label":"Test","disabled":false,"info":""},{"id":"b03d81d80d8da749","type":"function","z":"7aa9f6da.a3eeb8","name":"","func":"if (msg.status.text === \"done\") {\n    msg.payload = \"hello\"\n    return msg;\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":320,"wires":[["e85eeaa9aa4f5c7a"]]},{"id":"e85eeaa9aa4f5c7a","type":"debug","z":"7aa9f6da.a3eeb8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":320,"wires":[]},{"id":"f727751fe91480a8","type":"opcua-compact-server","z":"7aa9f6da.a3eeb8","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://localhost: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  \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  const isoInput2 = \"Boolean\"\n  const nodeIdInput2 = \"Boolean\"\n  const browserNameInput2 = \"I2\"\n \n  const rootFolderName = \"Test\"\n  const childFolderName = \"PLCs_IO\"\n  const inputsFolderName = \"Inputs\"\n  const outputsFolderName = \"Outputs\"\n  const viewInputsName = \"Digital-Ins\"\n  const viewOutputsName = \"Digital-Outs\"\n  \n  \n  var flexServerInternals = this;\n  \n    this.sandboxFlowContext.set(isoInput2, false);\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\": rootFolderName\n    });\n    const gpioFolder = namespace.addFolder(myDevice, { \"browseName\": childFolderName });\n    const isoInputs = namespace.addFolder(gpioFolder, {\n      \"browseName\": inputsFolderName\n    });\n    const isoOutputs = namespace.addFolder(gpioFolder, {\n      \"browseName\": outputsFolderName\n    });\n  \n    const gpioDI2 = namespace.addVariable({\n      \"organizedBy\": isoInputs,\n      \"browseName\": browserNameInput2,\n      \"nodeId\": \"ns=1;s=\" + nodeIdInput2,\n      \"dataType\": \"Boolean\",\n      \"value\": {\n        \"get\": function() {\n          return new Variant({\n            \"dataType\": DataType.Boolean,\n            \"value\": flexServerInternals.sandboxFlowContext.get(isoInput2)\n          });\n        },\n        \"set\": function(variant) {\n          flexServerInternals.sandboxFlowContext.set(\n            isoInput2,\n            variant.value\n          );\n          if (variant.value){\n              node.status({fill:\"green\",shape:\"dot\",text:\"done\"});\n              //node.error(\"done\", msg)\n              \n          }\n          return opcua.StatusCodes.Good;\n        }\n      }\n    });\n  \n    const viewDI = namespace.addView({\n      \"organizedBy\": rootFolder.views,\n      \"browseName\": viewInputsName\n    });\n  \n  \n    viewDI.addReference({\n      \"referenceType\": \"Organizes\",\n      \"nodeId\": gpioDI2.nodeId\n    });\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":840,"y":200,"wires":[]},{"id":"f4c1c90167228418","type":"status","z":"7aa9f6da.a3eeb8","name":"","scope":["f727751fe91480a8"],"x":400,"y":320,"wires":[["b03d81d80d8da749"]]},{"id":"e37f4eb8d480274f","type":"OPCUA-IIoT-Write","z":"7aa9f6da.a3eeb8","connector":"77fdc29e.3c49ec","name":"","justValue":false,"showStatusActivities":false,"showErrors":true,"x":670,"y":440,"wires":[["c7646ab516181d31"]]},{"id":"c7646ab516181d31","type":"OPCUA-IIoT-Response","z":"7aa9f6da.a3eeb8","name":"","compressStructure":false,"showStatusActivities":false,"showErrors":true,"activateUnsetFilter":false,"activateFilters":false,"negateFilter":false,"filters":[],"x":820,"y":440,"wires":[[]]},{"id":"278abff106a2ad9a","type":"function","z":"7aa9f6da.a3eeb8","name":"toWriteMsg","func":"var value = msg.payload\n\nvar newMsg = { payload :{\n        nodetype :'inject',\n        injectType : 'write',\n        valuesToWrite : [value],\n        addressSpaceItems : [{\n            name: \"Boolean\",\n            nodeId: \"ns=1;s=Boolean\",\n            datatypeName: \"Boolean\"\n        }]\n    }\n}\n\nreturn newMsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":440,"wires":[["e37f4eb8d480274f"]]},{"id":"c7952843f94092ab","type":"inject","z":"7aa9f6da.a3eeb8","name":"true","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":290,"y":440,"wires":[["278abff106a2ad9a"]]},{"id":"e1f99b96a6e25eab","type":"inject","z":"7aa9f6da.a3eeb8","name":"false","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":290,"y":500,"wires":[["278abff106a2ad9a"]]},{"id":"77fdc29e.3c49ec","type":"OPCUA-IIoT-Connector","discoveryUrl":"","endpoint":"opc.tcp://localhost:54845","endpointMustExist":false,"keepSessionAlive":true,"loginEnabled":false,"name":"LOCAL FLEX 80","showErrors":true,"securityPolicy":"None","securityMode":"None","individualCerts":false,"publicCertificateFile":"","privateKeyFile":"","defaultSecureTokenLifetime":"","autoSelectRightEndpoint":false,"strategyMaxRetry":"","strategyInitialDelay":"","strategyMaxDelay":"","strategyRandomisationFactor":"","requestedSessionTimeout":"","connectionStartDelay":"","reconnectDelay":"","maxBadSessionRequests":""}]

In this case, if you look at the line number 61 in the address space script of the opcua-compact-server node, you will find the calling of the node.status() function.

I need a trigger when someone writes a certain variable value to the OPC UA server in order to monitor for value changes. I understand that this request violates the main principles of flow-based programming, but the alternative I thought of is to continuously poll the OPC UA server, get the variable's new value, compare it to the old value and act accordingly. I assume this solution, while not an anti-pattern, could be a waste of resources and may affect overall performances.

Any suggestion about how to make it more pattern-friendly, simpler and better (maybe using more appropriate node or a library that I may not be aware of yet...) will be appreciated.

Thank you in advance.

Is the opcua-compact-server a node that you are developing yourself? If so then you could add another output to the node and send whatever information you want on that output.

Moderator here - I have removed several posts from this discussion that were headed off-topic and getting personal. Hopefully it can get back on track... even if the basic request is (as was initially pointed out) a complete anti-pattern.

As OP (@Lex) already noted the server node does seem to update status - which may be the best way to trigger something.

4 Likes

Thank you for your reply.

The library I'm using is the following:

I've already considered your suggestion (adding an output to the node) and then using the node.send() function (I think I can't "return" at that point since the script's execution is not finished yet).

https://nodered.org/docs/user-guide/writing-functions

It appears to be a proper way to achieve what I need. I'm not sure if I can modify a node from a library. If I recall correctly, there are JSON configuration files somewhere that allow you to configure the output options and other settings... Am I right? Could you please provide a link to a guide or some instructions to accomplish this?

Thank you very much.

Correct.

return is synchronous
node.send is asynchronous

EDIT
At least that's how I look at it

Thank you for your assistance, I apologize for any inconvenience.

Do you think using the node.status() function would be acceptable in this case? Do you have any suggestions on how to achieve the same result in a more appropriate manner?

Thank you very much, I appreciated that.

How are you at promise?
You could get really clever and store a promise in context, and resolve it?

Let me summarize for those who're following but haven't installed your example flow & the two opcua node libraries.

  • opcua-compact-server is a node without any input or output terminal.
  • opcua-compact-server yet offers a javascript interface - to setup the "Address Space".
  • @Lex idea / demand is to utilize this interface to inject kind of a monitoring functionality - raise a flag / send a msg when "a (dedicated) value" is coming through.

Some findings:

  • node.send() is available - yet it seems to be an empty function.
  • node.emit() is available as well & operational! Is there a node that is able to listen to internal events?
1 Like

WARNING : Silly idea, Silly, Silly Silly (hey Im creative at the moment :smiley: )

Create a TCP Proxy in Node RED, to "spy" on traffic, act accordingly.
downside, you may need to decipher its protocol a little

1 Like

Good question... ! I don't know enough about that node to comment if the status changes you see are adequate for what you want... normally they should only really be for monitoring the state of the actual server (ie up, down, connected, in error) etc - rather than just reflecting values...

But I think this may be one of those cases where it may be worth contacting the author direct to see if they would be interested in adding extra functionality to provide a trigger output (maybe via a pin if they currently don't even have an output :slight_smile: )

1 Like

Yes, but it's a secret :shushing_face:

The link out node communicates to a link in node using this.

In theory, a link-in node could be targeted by:

const event = "node:xxxyyyzzz"
const msg = {}
msg._event = event
node.emit(event, msg)

Where xxxyyyzzz is the ID of the link node.

3 Likes

Ok. I'll keep that secret! :wink:

1 Like

giphy

3 Likes

You are not the only one, I thought about a similar solution too :joy:

EDIT

Thanks to all for the support. I'm really loving all of this brainstorming!!!
Unfortunately, I have reached the maximum number of messages I can send the first day...

Thanks @Steve-Mcl! I like it, and I will definitely try this approach tomorrow!

1 Like

Its doable - I have done it many times - but much better options before you get to that level of desperation :laughing:

1 Like

If you log out of the forum, and log back in again, hopefully I've removed that restriction for you.

2 Likes

Good morning @Steve-Mcl,

I tried your solution, but unfortunately it doesn't work...

I also modified the link in node directly in the flow.json file (I finally remembered its path!) to add the ID of the opcua-compact-server node to the links option:

{
        "id": "2ad2c0d33f335c48",
        "type": "link in",
        "z": "7aa9f6da.a3eeb8",
        "name": "",
        "links": [
            "f727751fe91480a8"
        ],
        "x": 365,
        "y": 280,
        "wires": [
            [
                "b03d81d80d8da749",
                "e85eeaa9aa4f5c7a"
            ]
        ]
    }

Now, a wire appears in the flow when this node is selected, but I still haven't had any luck.

image

I also tried modifying the opcua-compact-server node options by adding the same configuration as the link out node:

...
"mode": "link",
"links": [
            "2ad2c0d33f335c48"
        ],
...

but that didn't work either.

Next, I tried modifying the opcua-compact-server node by addding the outputs option and adding the ID of the function node I want to reach to the wires option:

 {
        "id": "f727751fe91480a8",
        "type": "opcua-compact-server",
        "outputs": 1,
        "z": "7aa9f6da.a3eeb8",
        "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://localhost: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  \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  const isoInput2 = \"Boolean\"\n  const nodeIdInput2 = \"Boolean\"\n  const browserNameInput2 = \"I2\"\n \n  const rootFolderName = \"Test\"\n  const childFolderName = \"PLCs_IO\"\n  const inputsFolderName = \"Inputs\"\n  const outputsFolderName = \"Outputs\"\n  const viewInputsName = \"Digital-Ins\"\n  const viewOutputsName = \"Digital-Outs\"\n  \n  \n  var flexServerInternals = this;\n  \n    this.sandboxFlowContext.set(isoInput2, false);\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\": rootFolderName\n    });\n    const gpioFolder = namespace.addFolder(myDevice, { \"browseName\": childFolderName });\n    const isoInputs = namespace.addFolder(gpioFolder, {\n      \"browseName\": inputsFolderName\n    });\n    const isoOutputs = namespace.addFolder(gpioFolder, {\n      \"browseName\": outputsFolderName\n    });\n  \n    const gpioDI2 = namespace.addVariable({\n      \"organizedBy\": isoInputs,\n      \"browseName\": browserNameInput2,\n      \"nodeId\": \"ns=1;s=\" + nodeIdInput2,\n      \"dataType\": \"Boolean\",\n      \"value\": {\n        \"get\": function() {\n          return new Variant({\n            \"dataType\": DataType.Boolean,\n            \"value\": flexServerInternals.sandboxFlowContext.get(isoInput2)\n          });\n        },\n        \"set\": function(variant) {\n          flexServerInternals.sandboxFlowContext.set(\n            isoInput2,\n            variant.value\n          );\n          if (variant.value){\n              node.warn(variant.value);\n              var msg  = {};\n              node.send(msg)\n              \n          }\n          return opcua.StatusCodes.Good;\n        }\n      }\n    });\n  \n    const viewDI = namespace.addView({\n      \"organizedBy\": rootFolder.views,\n      \"browseName\": viewInputsName\n    });\n  \n  \n    viewDI.addReference({\n      \"referenceType\": \"Organizes\",\n      \"nodeId\": gpioDI2.nodeId\n    });\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": 420,
        "y": 200,
        "wires": [
            [
                "b03d81d80d8da749"
            ]
        ]
    },

Then, I simply added the following to the address space script of the opcua-compact-server node:

var msg  = {};
node.send(msg)

finally, after restarting Node-RED service and refreshing the browser...

image

IT WORKS!

Now, the "only" problem is that if I deploy some changes from the browser and then refresh it, the manual alterations I made to the opcua-compact-server node in the flow.json file are discarded. Only the following configuration is kept:

"wires": [
            [
                "b03d81d80d8da749"
            ]

Maybe this is because the wires option is standard in the library, but the outputs option is not... Is there a way to make this alteration permanent?

Thank you very much!

EDIT

In the end, I've decided to modify the library to add a field to the configuration settings:

In the address space script:

var msg  = {};
node.send([msg, msg]) // use an array when outputs > 1; each element represents an output

How to add outputs option in the library:

/root/.node-red/node_modules/node-red-contrib-opcua-server/opcuaCompact/server-node.html

line 125:

///////////////////////MOD 26-10-23 
      outputs: {
        value: 0,
        validate: function(v) {
          return (
            v === "" || (RED.validators.number(v) && v >= -1 && v <= 10)
          );
        }
      },
///////////////////////

line 149:
//outputs: 0, ///////////////////////MOD 26-10-23

line 590:

<!-- ///////////////////////MOD 26-10-23  -->
<div class='form-row'> <label for='node-input-outputs'><i class='icon-time'></i> Outputs
    </label> <input type='text' id='node-input-outputs' placeholder='0' style='width:80px'> </div>
<!-- /////////////////////// -->

So... no more anti-pattern workaround is needed!

Still, any comments about this would be greatly appreciated!

Thank you all for the support!