UDP Streaming an entire RIO module's data

Building off of a project from earlier this year, (Very) fast way to read digital inputs via UDP streaming using ONLY Node-RED, I’ve written some extra code to process floating point values for analog AND digital I/O at the same time. The cool thing is that with all that data you can easily combine the results based on your channel configuration so that an entire groov RIO module can be streamed – regardless of how your RIO is set up!

For this example I am streaming all my RIO Learning Center I/O at 250ms and displaying it in a Node-RED dashboard table:

image

This is a combination of 2 digital inputs, 4 digital outputs, 2 analog inputs, and 2 analog outputs, each of which update extremely quickly with minimal strain on the system.

The main part of this is exactly the same as my previous post, using several OptoMMP addresses to configure and enable streaming, but instead of specifying the address for a single digital point I read “everything”. The key is to get enough bytes to get both the high-density channel data and digital channel states:

By leaving the “starting address” at zero we get both of these, then it’s just a matter of converting the raw response into 4-byte floats for analog, 1-bit true/false Booleans for digital, and then constructing a single object with 10 values based on the I/O configuration.

I’ll have the full flow import at the end of this post, but here’s the important bit of JavaScript I added:

var config = [1,1,0,0,1,0,1,0,1,1];
var analog = [];
var digital = [];
// Convert 32 bytes into eight 4-byte floats:
var buf = new ArrayBuffer(4);
var view = new DataView(buf);
for (var i = 4; i < 36; i+=4) {
    var myFloat = msg.payload.slice(i,i+4);
    myFloat.forEach(function (b,i) {
        view.setUint8(i, b);
    });
    var num = view.getFloat32(0);
    analog.push(num);
}
// Convert two bytes into ten 1/0 bits:
var bits = "";
bits += (msg.payload[2054].toString(2).padStart(8, '0'));
bits += (msg.payload[2055].toString(2).padStart(8, '0'));
bits = bits.substring(6,16).split('').reverse().join('');
// Convert 1/0 characters into booleans:
for (var b in bits)
    digital.push((bits[b]) == "1" ? true : false);
// Create a clean object based on IO configuration, 1 = digital, 0 = analog:
var output = [];
for (var c = 0; c < 10; c++)
    output.push( { "channels" : (config[c] == 1 ? digital[c] : analog[c])});
return { payload : output, analog : analog, digital : digital};

With the Learning Centre configuration my I/O has digital channels set for 0, 1, 4, 6, 8, and 9, then analog for 2, 3, 5, and 7. To make a simple map of that I have the first line var config = set as a list of 1’s for digital and 0’s for analog. By looking at this list the program knows whether to reference the analog results or the digital, since they’re in very different parts of memory.

To process those two parts of memory I have one loop that goes through 32-bytes of 4-byte floats, converting each chunk of bytes into a 32-bit floating point number. Then I have another loop that uses toString(2) to convert two bytes from the digital area into all ten 1/0 bits from the hexadecimal stream. I make sure to pad the results with zeros since we want to know if any given channel is in the off / false state, which comes through as a zero (so we want 0010 not just 10). Depending on the 1/0 character I set a digital value to be the Boolean true / false and finally create a clean output object based on the config map created on line 1.


This is a somewhat complicated process, but the end result is a nice list of data that can be displayed, logged, compared, or whatever else you want to do with it. Even better, you can reconfigure your RIO however you want and all you have to do is change that config map and you’re good to go!
Feel free to import this flow using the JSON below, and as always modify it as you see fit. If you do end up using or modifying this please drop a line in the thread and let us know!

[{"id":"45c76d95c34ea450","type":"inject","z":"471d0107c5bc0d62","name":"","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","x":230,"y":100,"wires":[["0700d01695aca07b"]]},{"id":"d07506ab7afabd8e","type":"udp in","z":"471d0107c5bc0d62","name":"","iface":"","port":"5001","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":180,"y":400,"wires":[["f25ec8682c024ba2","0494c9820ba4953e"]]},{"id":"44c3872ef114e7f6","type":"inject","z":"471d0107c5bc0d62","name":"show config","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":210,"y":340,"wires":[["4b5b740d8c327d57"]]},{"id":"4b5b740d8c327d57","type":"groov-io-read","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC0","mmpType":"int32","mmpLength":"10","mmpEncoding":"ascii","value":"","valueType":"msg.payload","itemName":"","name":"check configuration","x":410,"y":340,"wires":[["395aa9b8f9ecf40c"]]},{"id":"e7b7a5f8f38977e0","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"1","valueType":"value","name":"enable streaming","x":410,"y":280,"wires":[["4b5b740d8c327d57"]]},{"id":"395aa9b8f9ecf40c","type":"debug","z":"471d0107c5bc0d62","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":590,"y":340,"wires":[]},{"id":"47cd51ad35c31c4b","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"0","valueType":"value","name":"0xF03FFFD0 = 0","x":470,"y":520,"wires":[[]]},{"id":"d47539594853ebb2","type":"inject","z":"471d0107c5bc0d62","name":"disable streaming","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":220,"y":520,"wires":[["47cd51ad35c31c4b"]]},{"id":"0700d01695aca07b","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFC8","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"0","valueType":"value","name":"set starting address","x":410,"y":100,"wires":[["a52fd550be24d5f9"]]},{"id":"a52fd550be24d5f9","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFCC","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"2200","valueType":"value","name":"set read length","x":400,"y":140,"wires":[["54480238cf6814a1"]]},{"id":"0494c9820ba4953e","type":"function","z":"471d0107c5bc0d62","name":"convert","func":"var analog = [];\nvar digital = [];\n// Convert 32 bytes into eight 4-byte floats:\nvar buf = new ArrayBuffer(4);\nvar view = new DataView(buf);\nfor (var i = 4; i < 36; i+=4) {\n    var myFloat = msg.payload.slice(i,i+4);\n    myFloat.forEach(function (b,i) {\n        view.setUint8(i, b);\n    });\n    var num = view.getFloat32(0);\n    analog.push(num);\n}\n// Convert two bytes into ten 1/0 bits:\nvar bits = \"\";\nbits += (msg.payload[2054].toString(2).padStart(8, '0'));\nbits += (msg.payload[2055].toString(2).padStart(8, '0'));\nbits = bits.substring(6,16).split('').reverse().join('');\n// Convert 1/0 characters into booleans:\nfor (var b in bits)\n    digital.push((bits[b]) == \"1\" ? true : false);\n// Create a clean object based on IO configuration, 1 = digital, 0 = analog:\nvar output = [];\nvar config = [1,1,0,0,1,0,1,0,1,1];\nfor (var c = 0; c < 10; c++)\n    output.push( { \"channels\" : (config[c] == 1 ? digital[c] : analog[c])});\nreturn { payload : output, analog : analog, digital : digital};","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":400,"wires":[["ce3291e311e6e745","9c6b9e25112fea52"]]},{"id":"f25ec8682c024ba2","type":"debug","z":"471d0107c5bc0d62","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":350,"y":440,"wires":[]},{"id":"54480238cf6814a1","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFD4","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"250","valueType":"value","name":"set stream interval","x":410,"y":180,"wires":[["58e87c1faf646a31"]]},{"id":"58e87c1faf646a31","type":"groov-io-write","z":"471d0107c5bc0d62","device":"c2bc781e80860462","dataType":"mmp-address","moduleIndex":"","channelIndex":"","mmpAddress":"0xF03FFFE0","mmpType":"int32","mmpLength":"1","mmpEncoding":"ascii","value":"2130706433","valueType":"value","name":"set IP address","x":400,"y":220,"wires":[["e7b7a5f8f38977e0"]]},{"id":"ce3291e311e6e745","type":"debug","z":"471d0107c5bc0d62","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":530,"y":440,"wires":[]},{"id":"9c6b9e25112fea52","type":"ui_table","z":"471d0107c5bc0d62","group":"5ae11c9c7c6b19b4","name":"","order":0,"width":"6","height":"9","columns":[],"outputs":0,"cts":false,"x":530,"y":400,"wires":[]},{"id":"c2bc781e80860462","type":"groov-io-device","address":"localhost","msgQueueFullBehavior":"DROP_OLD"},{"id":"5ae11c9c7c6b19b4","type":"ui_group","name":"Realtime Data","tab":"da5706ae.6caf58","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"da5706ae.6caf58","type":"ui_tab","name":"groov RIO Dashboard","icon":"dashboard","disabled":false,"hidden":false}]

And as always, happing flowing!

2 Likes