(Very) fast way to read digital inputs via UDP streaming using ONLY Node-RED

Thanks to a post in an earlier thread by @philip (link) I have been testing UDP streaming of digital channels, but focusing on using just Node-RED, rather than groov Manage and Python.
This approach is entirely based around the OptoMMP endpoints for streaming configuration that’s fully explained under “Streaming Configuration–Read/Write” in “Appendix A” of the Opto22 - 1465 OptoMMP Protocol Guide

The specific address is 0xF03FFFC0, and uses the 10 32-bit integers that follow to configure different settings for the stream. (Index 0 is reserved and unused in this application.)

  1. 0xF03FFFC4 = Enable or disable I/O mirroring.
  2. 0xF03FFFC8 = Beginning address of data to stream (leave at 0 to stream everything).
  3. 0xF03FFFCC = Size of data to stream.
  4. 0xF03FFFD0 = Streaming On/Off.
  5. 0xF03FFFD4 = Streaming Interval, in milliseconds (unsigned integer).
  6. 0xF03FFFD8 = UDP port number to stream to.
  7. 0xF03FFFDC = Reserved
  8. 0xF03FFFE0 = Stream target IP address #1
  9. 0xF03FFFE40xF03FFFFC = Additional target IP addresses #2 - #8

I’ll focus on only the ones I needed to set to stream a single digital point, and why I chose the values I did.


I will leave the streaming toggle at 0xF03FFFD0 until the end, since I want all the other things configured correctly first, and start at 0xF03FFFC8 with the beginning address at index 2.
First, you’ll need the address of the point you want to read. My digital input is on module 0, channel 1, so I just put that into my MMP Calculator in groov manage and get the hex address.

In groov Manage you can use generic MMP to enter a hexadecimal value directly, but since I’m using the REST API it must be a 32-bit signed integer.
However my address 0xF01E0040 converted directly to an integer is 4,030,726,088 – which is overflows the 32-bits I have to use, so it needs to be rolled over to a negative. You can use whatever tool you’d like, but I used this site: Signed integer (32-bit) Converter which gave me the result -266469312, so that’s what I’ll be putting into index 2, MMP address 0xF03FFFC8.


The next few are much more straightforward.
The size of data to stream is easy for this example since I’m just getting one point, so I set MMP address 0xF03FFFCC to 4, since four 8-bit bytes = 32 bits: the length of one point.

Then, the stream interval. 100ms is more than enough for most applications, but just to push it, I set mine to 10ms for 0xF03FFFD4.

The UDP address defaults to 5001, which is fine as long as you’re not using it for anything else, so I won’t change this, but if you want a different port just write that integer to 0xF03FFFE4.

Next is the IP address. IP addresses are already 32-bit for IPv4, we just need to convert it from dot notation to a solid number. I just need to set each byte to the value for the IP so if I’m streaming to an external device, for example 192.168.1.22 I would use the following formula:
(192 * 2^24) + (168 * 2^16) + (1 * 2^8) + (22) = 3232235798
In this example I’m running Node-RED directly on the groov EPIC / RIO I’ll be streaming form , so that’s localhost at 127.0.0.1, which looks like this:
(127 * 2^24) + 0 + 0 + 1 = 2130706433
So that’s the number I’ll put into 0xF03FFFE0.
I can put in additional addresses in the following 32-bit integer indexes, but I just need this one localhost endpoint.

Now that I’ve set starting address, data length, stream interval, UDP port (optional), and target IP address, the configuration is complete.


This will send a firehose of messages into Node-RED, so before I enable any debug nodes, I’m going to do a little trimming and filtering so I don’t just get a status message every 10ms.
Firstly, I want to trim off the header that comes with the UDP packet, since not only do I not need it, but it also changes a little bit with every message, so it’s hard to filter by.
You can learn the exact details from the protocol guide under “Traditional Stream Packet Format” in “Chapter 2: Overview of Programming”.

For just one channel I’m going to grab only the byte that changes using a simple function:

return { payload : msg.payload.slice(11, 12)[0] }

This grabs only the 12th byte out of the buffer and outputs it on msg.payload.
Now I can use an RBE “filter” node to check to see if msg.payload changes and the only messages I get out of that are when the value from the stream changes. How cool is that?!


Now all I have to do is write a non-0 value to 0xF03FFFD0 to enable streaming, and a 0 to turn it off again, and I’ll see the data coming into my debug pane every time it changes.

Here’s the example flow I put together to test this on my groov EPIC and RIO:
image

And here’s the import JSON:

[{"id":"45c76d95c34ea450","type":"inject","z":"2f30d40e39eb0deb","name":"","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","x":190,"y":1000,"wires":[["0700d01695aca07b"]]},{"id":"d07506ab7afabd8e","type":"udp in","z":"2f30d40e39eb0deb","name":"","iface":"","port":"5001","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":140,"y":1300,"wires":[["0494c9820ba4953e","f25ec8682c024ba2"]]},{"id":"ad8d2fc97de6679a","type":"debug","z":"2f30d40e39eb0deb","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":610,"y":1300,"wires":[]},{"id":"44c3872ef114e7f6","type":"inject","z":"2f30d40e39eb0deb","name":"show config","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":1240,"wires":[["4b5b740d8c327d57"]]},{"id":"4b5b740d8c327d57","type":"groov-io-read","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC0","mmpType":"int32","mmpLength":"10","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"check configuration","x":370,"y":1240,"wires":[["395aa9b8f9ecf40c"]]},{"id":"e7b7a5f8f38977e0","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"1","valueType":"value","name":"enable streaming","x":370,"y":1180,"wires":[["4b5b740d8c327d57"]]},{"id":"395aa9b8f9ecf40c","type":"debug","z":"2f30d40e39eb0deb","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":550,"y":1240,"wires":[]},{"id":"47cd51ad35c31c4b","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"0","valueType":"value","name":"0xF03FFFD0 = 0","x":430,"y":1420,"wires":[[]]},{"id":"d47539594853ebb2","type":"inject","z":"2f30d40e39eb0deb","name":"disable streaming","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":180,"y":1420,"wires":[["47cd51ad35c31c4b"]]},{"id":"0700d01695aca07b","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC8","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"-266469312","valueType":"value","name":"set starting address","x":370,"y":1000,"wires":[["a52fd550be24d5f9"]]},{"id":"a52fd550be24d5f9","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFCC","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"4","valueType":"value","name":"set read length","x":360,"y":1040,"wires":[["54480238cf6814a1"]]},{"id":"0494c9820ba4953e","type":"function","z":"2f30d40e39eb0deb","name":"grab one byte","func":"return { payload : msg.payload.slice(11, 12)[0] }","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":1300,"wires":[["b3a22b36ea84077d","ce3291e311e6e745"]]},{"id":"b3a22b36ea84077d","type":"rbe","z":"2f30d40e39eb0deb","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":490,"y":1300,"wires":[["ad8d2fc97de6679a"]]},{"id":"f25ec8682c024ba2","type":"debug","z":"2f30d40e39eb0deb","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":310,"y":1340,"wires":[]},{"id":"54480238cf6814a1","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD4","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"10","valueType":"value","name":"set stream interval","x":370,"y":1080,"wires":[["58e87c1faf646a31"]]},{"id":"58e87c1faf646a31","type":"groov-io-write","z":"2f30d40e39eb0deb","device":"","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFE0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"2130706433","valueType":"value","name":"set IP address","x":360,"y":1120,"wires":[["e7b7a5f8f38977e0"]]},{"id":"ce3291e311e6e745","type":"debug","z":"2f30d40e39eb0deb","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":490,"y":1340,"wires":[]}]

If you have any questions, or end up using this in your own application, just leave a line in the thread below. And as always, happy flowing!

This is actually a very useful method, I modified it slightly so that it reads the float table from the scratchpad, then converts the 32bit floats into values, and stores them on the global memory, to use them on other nodes, a few points to note: IP address needs to be sent using UINT, this was incorrect on the original data, and also the starting point for the requested data starts on array position 8, where the original reads from position 11, and creates confusion. This code adds a second IP Address 192.168.4.100, which can be removed or adjusted as needed.

[{"id":"5ca075183bfa0a75","type":"tab","label":"UDP Transmission","disabled":false,"info":"","env":[]},{"id":"45c76d95c34ea450","type":"inject","z":"5ca075183bfa0a75","name":"","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","x":430,"y":120,"wires":[["0700d01695aca07b"]]},{"id":"d07506ab7afabd8e","type":"udp in","z":"5ca075183bfa0a75","name":"","iface":"","port":"5001","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":380,"y":440,"wires":[["f25ec8682c024ba2","cd4e0a10de367857"]]},{"id":"44c3872ef114e7f6","type":"inject","z":"5ca075183bfa0a75","name":"show config","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":410,"y":380,"wires":[["4b5b740d8c327d57"]]},{"id":"4b5b740d8c327d57","type":"groov-io-read","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC0","mmpType":"int32","mmpLength":"10","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"check configuration","x":610,"y":380,"wires":[["395aa9b8f9ecf40c"]]},{"id":"e7b7a5f8f38977e0","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"1","valueType":"value","name":"enable streaming","x":610,"y":320,"wires":[["4b5b740d8c327d57"]]},{"id":"395aa9b8f9ecf40c","type":"debug","z":"5ca075183bfa0a75","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":790,"y":380,"wires":[]},{"id":"47cd51ad35c31c4b","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"0","valueType":"value","name":"0xF03FFFD0 = 0","x":670,"y":560,"wires":[[]]},{"id":"d47539594853ebb2","type":"inject","z":"5ca075183bfa0a75","name":"disable streaming","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":420,"y":560,"wires":[["47cd51ad35c31c4b"]]},{"id":"0700d01695aca07b","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC8","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"-254271488","valueType":"value","name":"set starting address","x":610,"y":120,"wires":[["a52fd550be24d5f9"]]},{"id":"a52fd550be24d5f9","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFCC","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"40","valueType":"value","name":"set read length","x":600,"y":160,"wires":[["54480238cf6814a1"]]},{"id":"f25ec8682c024ba2","type":"debug","z":"5ca075183bfa0a75","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":550,"y":480,"wires":[]},{"id":"54480238cf6814a1","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD4","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"500","valueType":"value","name":"set stream interval","x":610,"y":200,"wires":[["58e87c1faf646a31"]]},{"id":"58e87c1faf646a31","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFE0","mmpType":"uint32","mmpLength":"1","mmpEncoding":"ascii","value":"2130706433","valueType":"value","name":"set IP address","x":600,"y":240,"wires":[["e68faded3283fd21"]]},{"id":"ce3291e311e6e745","type":"debug","z":"5ca075183bfa0a75","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":730,"y":480,"wires":[]},{"id":"cd4e0a10de367857","type":"function","z":"5ca075183bfa0a75","name":"","func":"function bytesToFloat(bytes) {\n  var buf = Buffer.from(bytes);\n  return buf.readFloatBE(0);\n}\n\n// Usage:\nvar payload = msg.payload;\nvar payloadSize = payload.length;\nvar numValues = Math.floor((payloadSize - 8) / 4); // Calculate numValues based on payload size\nvar floatArray = [];\n\nfor (var i = 0; i < numValues; i++) {\n  var startIdx = i * 4 + 8;\n  var endIdx = startIdx + 4;\n  var floatValue = bytesToFloat(payload.slice(startIdx, endIdx));\n  floatArray.push(floatValue);\n}\n\nmsg.float = floatArray;\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":440,"wires":[["ce3291e311e6e745","62d2cebc0fb56f94"]]},{"id":"62d2cebc0fb56f94","type":"change","z":"5ca075183bfa0a75","name":"","rules":[{"t":"set","p":"telemetry.float","pt":"global","to":"float","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":440,"wires":[[]]},{"id":"e68faded3283fd21","type":"groov-io-write","z":"5ca075183bfa0a75","device":"2d72cc137285b422","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFE4","mmpType":"uint32","mmpLength":"1","mmpEncoding":"ascii","value":"3232236644","valueType":"value","name":"set Additional IP address ","x":630,"y":280,"wires":[["e7b7a5f8f38977e0"]]},{"id":"2d72cc137285b422","type":"groov-io-device","address":"localhost","msgQueueFullBehavior":"DROP_OLD"}]
1 Like