Getting Digital I/O from a modbus TCP device

I am very new to Node-Red, however I have been a controls engineer working with PLCs and other hardware for almost 10 years now. I am very familiar with the hardware and communication side of things, and less familiar with PC/Java programming.

Anyway, I have these 16 channel digital I/O devices that talk Modbus TCP and I have them working perfectly. I have some code set up so that I can connect, read, and generally see the stat of inputs changing. Now, the problem I have is getting them into a usable state so I can have them as led indicators on my dashboard (boolean).

The devices only support reading 16 bit holding registers (quantity 4 with 4 inputs in the lowest nibble of each). That comes back in the payload as a 4 element array. I want to take each of those elements and get the individual bits into a bool array or discrete variables somehow.

I'll try to post what I have so far below along with the error message I'm getting. I realize this is probably not the best way of going about this but I just don't know any better at this point.

[{"id":"f904094d.37cdb8","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"93f34bf.9bb84b8","type":"debug","z":"f904094d.37cdb8","name":"1","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","x":650,"y":340,"wires":[]},{"id":"79921421.d6ffbc","type":"debug","z":"f904094d.37cdb8","name":"3","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":650,"y":480,"wires":[]},{"id":"59a58f0c.cc643","type":"debug","z":"f904094d.37cdb8","name":"2","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":650,"y":420,"wires":[]},{"id":"b7a493ab.5e48f","type":"switch","z":"f904094d.37cdb8","name":"switch1","property":"parts.index","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"eq","v":"1","vt":"str"},{"t":"eq","v":"2","vt":"str"},{"t":"eq","v":"3","vt":"str"}],"checkall":"false","repair":true,"outputs":4,"x":420,"y":420,"wires":[["93f34bf.9bb84b8","8e0dedb2.6d23f"],["59a58f0c.cc643"],["79921421.d6ffbc"],["30bd0c85.16a694"]]},{"id":"eccea110.b2043","type":"split","z":"f904094d.37cdb8","name":"spliit1","splt":"\\n","spltType":"str","arraySplt":"1","arraySpltType":"len","stream":false,"addname":"key","x":170,"y":420,"wires":[["b7a493ab.5e48f"]]},{"id":"30bd0c85.16a694","type":"debug","z":"f904094d.37cdb8","name":"4","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":650,"y":540,"wires":[]},{"id":"672d253e.35422c","type":"modbus-flex-getter","z":"f904094d.37cdb8","name":"Modbus Flexible Read","showStatusActivities":false,"showErrors":false,"logIOActivities":false,"server":"9d8b2011.7823a","useIOFile":false,"ioFile":"","useIOForPayload":false,"x":480,"y":160,"wires":[["975dce4.abe253","94dd4fc.59d00b"],["4961a7cb.c89358","31ceeb70.931974"]]},{"id":"63a4709a.e831f","type":"inject","z":"f904094d.37cdb8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":110,"y":140,"wires":[["cf4d7b97.b90b58"]]},{"id":"cf4d7b97.b90b58","type":"function","z":"f904094d.37cdb8","name":"FC1","func":"msg.payload = { 'fc': 4, 'unitid': 1, 'address': 0, 'quantity': 4}\nreturn msg;","outputs":1,"noerr":0,"x":310,"y":160,"wires":[["672d253e.35422c"]]},{"id":"4961a7cb.c89358","type":"debug","z":"f904094d.37cdb8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":730,"y":200,"wires":[]},{"id":"31ceeb70.931974","type":"modbus-response","z":"f904094d.37cdb8","name":"","registerShowMax":20,"x":750,"y":240,"wires":[]},{"id":"975dce4.abe253","type":"debug","z":"f904094d.37cdb8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","x":710,"y":120,"wires":[]},{"id":"7d89bc8a.e7a4f4","type":"comment","z":"f904094d.37cdb8","name":"Modbus works perfectly!","info":"","x":150,"y":80,"wires":[]},{"id":"94dd4fc.59d00b","type":"link out","z":"f904094d.37cdb8","name":"out1","links":["ad644526.6f53c8"],"x":695,"y":160,"wires":[]},{"id":"ad644526.6f53c8","type":"link in","z":"f904094d.37cdb8","name":"in1","links":["94dd4fc.59d00b"],"x":55,"y":420,"wires":[["eccea110.b2043"]]},{"id":"8e0dedb2.6d23f","type":"bitunloader","z":"f904094d.37cdb8","name":"bits","mode":"arrayBools","prop":"payload","padding":"16","x":650,"y":380,"wires":[["7fc07b82.574e14"]]},{"id":"90ea9201.a1022","type":"debug","z":"f904094d.37cdb8","name":"bools 0","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":1260,"y":300,"wires":[]},{"id":"19c688f5.5a6187","type":"switch","z":"f904094d.37cdb8","name":"switch 2","property":"parts.index","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"eq","v":"1","vt":"str"},{"t":"eq","v":"2","vt":"str"},{"t":"eq","v":"3","vt":"str"},{"t":"eq","v":"4","vt":"str"}],"checkall":"false","repair":true,"outputs":5,"x":1040,"y":380,"wires":[["90ea9201.a1022"],["212c41fb.577ace"],["413f0a7b.180064","e500b5d2.5c6e68"],["4995484c.6df8e8"],[]]},{"id":"7fc07b82.574e14","type":"split","z":"f904094d.37cdb8","name":"spliit","splt":"\\n","spltType":"str","arraySplt":"1","arraySpltType":"len","stream":false,"addname":"value","x":850,"y":380,"wires":[["19c688f5.5a6187"]]},{"id":"212c41fb.577ace","type":"debug","z":"f904094d.37cdb8","name":"bools 1","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":1260,"y":360,"wires":[]},{"id":"413f0a7b.180064","type":"debug","z":"f904094d.37cdb8","name":"bools 2","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":1260,"y":460,"wires":[]},{"id":"4995484c.6df8e8","type":"debug","z":"f904094d.37cdb8","name":"bools 3","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":1260,"y":520,"wires":[]},{"id":"e500b5d2.5c6e68","type":"ui_led","z":"f904094d.37cdb8","group":"e847eaa5.5b4cd8","order":0,"width":0,"height":0,"label":"led","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"red","value":"false","valueType":"bool"},{"color":"green","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"name":"","x":1250,"y":420,"wires":[]},{"id":"93bd578c.a4e648","type":"inject","z":"f904094d.37cdb8","name":"","topic":"","payload":"[4,0,0,2]","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":80,"y":360,"wires":[["eccea110.b2043"]]},{"id":"1e6355ed.7df09a","type":"comment","z":"f904094d.37cdb8","name":"Split array into 4 messages.","info":"","x":160,"y":320,"wires":[]},{"id":"d59f7ea1.c53c7","type":"comment","z":"f904094d.37cdb8","name":"Send each message out on a different line.","info":"","x":360,"y":480,"wires":[]},{"id":"cb94fe08.1bc21","type":"comment","z":"f904094d.37cdb8","name":"Create and array of bools","info":"","x":690,"y":300,"wires":[]},{"id":"599269e3.8b50e8","type":"comment","z":"f904094d.37cdb8","name":"split the array","info":"","x":810,"y":340,"wires":[]},{"id":"f617b415.ee0888","type":"comment","z":"f904094d.37cdb8","name":"send each bool out","info":"","x":1050,"y":320,"wires":[]},{"id":"9d8b2011.7823a","type":"modbus-client","z":"","name":"test","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"tcpHost":"192.168.1.100","tcpPort":"502","tcpType":"TPC-RTU-BUFFERED","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","unit_id":"1","commandDelay":"1","clientTimeout":"1000","reconnectOnTimeout":true,"reconnectTimeout":"2000","parallelUnitIdsAllowed":true},{"id":"e847eaa5.5b4cd8","type":"ui_group","z":"","name":"Default","tab":"6ebd2b21.aa83d4","disp":true,"width":"20","collapse":false},{"id":"6ebd2b21.aa83d4","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

It would make sense if we could do what us PLC guys normally would do.
myWord.4 // get value of bit 4 from word
myOtherWord:X2 // get value of bit 2 from word

Some googling shows a slick way of doing what you want.
Convert the 16 bit word to base 2 using toString(2).

var myWord = modbusWord.toString(2);

var myBool = myWord[3]; // get value of third bit

A string in JavaScript can be accessed just like an array.

Yeah, give me memory addresses and function blocks and I can do whatever I need all day. This higher level language stuff makes my head hurt.

I did see that method, but I was a bit confused. I have been trying to find the syntax for getting a single element of an array. In Allen Bradley world it's : myarray[0]

All of the array examples I have found have pretty much used strings.

Would it just be:

var myword = modbusarray[0].tostring(2); //take the first element of the modbus array and convert it to a base 2 string
var mybool = myword[0]; //grab the first element of the converted word (originally bit 0 of element zero of the modbus array)

Keep in mind the modbus node returns a 4 element array of 16 bit words.

The next step would be to iterate through and get all the bits into a single bool array. This is made more difficult than it needs to since we only care about 4 bits in each 16 bit word... I have no clue why the manufacturer of this device didn't allow single coil reads.

I have another one that works with function 01 and 02 that returns an array of bool that works great.

Yeah, so it's not exactly pretty or as usable for what I want to do with it as I would like, but it does work.

I'm going to have to dig into how to loop this to get it into a bool array with everything aligned nicely. Using dedicated function nodes like this is going to be awful.

Thank you for your assistance, I am sure I will be back here for more as I explore further!

Next on the list is getting OPC working.

[{"id":"3a6301a4.44064e","type":"tab","label":"Flow 3","disabled":false,"info":""},{"id":"36b85f79.82966","type":"ui_led","z":"3a6301a4.44064e","group":"165d20f1.f18e7f","order":0,"width":0,"height":0,"label":"led","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"red","value":"true","valueType":"bool"},{"color":"green","value":"false","valueType":"bool"}],"allowColorForValueInMessage":false,"name":"","x":930,"y":140,"wires":[]},{"id":"3bf62e6f.1ef772","type":"Invert","z":"3a6301a4.44064e","name":"Invert","x":810,"y":140,"wires":[["36b85f79.82966"]]},{"id":"a34cc0f.eb4294","type":"function","z":"3a6301a4.44064e","name":"Get First Bit","func":"var boolholding = false;\nvar holdingint = msg.payload[0];\nvar myboolword = holdingint.toString(2); //take the first element of the modbus array and convert it to a base 2 string\nboolholding = myboolword[0];\nif (boolholding ==1){msg.payload = true} else {msg.payload = false}\n//msg.payload = boolholding;\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":140,"wires":[["3bf62e6f.1ef772"]]},{"id":"6958ceb1.cb0f9","type":"modbus-flex-getter","z":"3a6301a4.44064e","name":"Modbus Flexible Read","showStatusActivities":false,"showErrors":false,"logIOActivities":false,"server":"9d8b2011.7823a","useIOFile":false,"ioFile":"","useIOForPayload":false,"x":440,"y":160,"wires":[["a34cc0f.eb4294"],["b1b121a6.08744"]]},{"id":"b1b121a6.08744","type":"modbus-response","z":"3a6301a4.44064e","name":"","registerShowMax":20,"x":650,"y":220,"wires":[]},{"id":"a2f812f4.802b6","type":"function","z":"3a6301a4.44064e","name":"FC1","func":"msg.payload = { 'fc': 4, 'unitid': 1, 'address': 0, 'quantity': 4}\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":160,"wires":[["6958ceb1.cb0f9"]]},{"id":"62818ce4.2eeae4","type":"inject","z":"3a6301a4.44064e","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":70,"y":140,"wires":[["a2f812f4.802b6"]]},{"id":"a90524d3.50b098","type":"comment","z":"3a6301a4.44064e","name":"Modbus works perfectly!","info":"","x":110,"y":80,"wires":[]},{"id":"2c7cb3d0.c47f6c","type":"comment","z":"3a6301a4.44064e","name":"Get the first bit from the first word of the modbus array.  ","info":"","x":700,"y":80,"wires":[]},{"id":"165d20f1.f18e7f","type":"ui_group","z":"","name":"other","tab":"59ef559f.36fefc","disp":true,"width":"6","collapse":false},{"id":"9d8b2011.7823a","type":"modbus-client","z":"","name":"test","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"tcpHost":"192.168.1.100","tcpPort":"502","tcpType":"TPC-RTU-BUFFERED","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","unit_id":"1","commandDelay":"1","clientTimeout":"1000","reconnectOnTimeout":true,"reconnectTimeout":"2000","parallelUnitIdsAllowed":true},{"id":"59ef559f.36fefc","type":"ui_tab","z":"","name":"other","icon":"dashboard","disabled":false,"hidden":false}]

@seth350

That actually didn't end up to be too bad.... Can you tell I hate not being able to figure things out?

Here's the function I came up with that throws everything into a bool array:

var i = 0;
var i2 = 0;
var boolholding = false
var holdingint = msg;
var myboolword = 0
var boolarrayindex = 0
var myboolarray = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false];
for (i = 0; i < 4; i++){
boolholding = false;
holdingint = msg.payload[i];
myboolword = holdingint.toString(2); //take the first element of the modbus array and convert it to a base 2 string
for (i2 = 0; i2 < 4; i2++){
boolholding = myboolword[i2];
if (boolholding ==1){myboolarray[boolarrayindex] = true} else {myboolarray[boolarrayindex] = false}
boolarrayindex++
}
}

//msg.payload = boolholding;
msg.payload = myboolarray;
return msg;

Arrays can be accessed the same way. Matter of fact, a string itself is an array.

var s = “Hello”;

// s[0] is H
// s[1] is e
// s[2] is l
// s[3] is l
// s[4] is o

:+1: I did end up figuring that out after my reply before. This works like a charm and the same basic concept should work well for dealing with outputs.

This particular module is actually universal in/out and all 16 I/O can be used as either an input or output. I imagine I will a page where I can set a mask to configure them as required.

Hi,

I'm also a newbie with node-red !
I'm into home automation and was a little bit frustrated because I was not able to merge my "old" system (modbus plc masters and slaves, both TCP and RTU) with my new Hubitat.
So I dig a little bit to learn ways to bind them together.

I installed node-red (on a windows system), added modbustcp add-on and made some tests to reach my masters (Tbox / Servelec). So far, so good: I can now poll multiple integers.
Reading the forum, I can poll float but need to split the 2 consecutive registers to build a float value that will be sent to Hubitat (MQTT/drivers, another story for later).

I really discover node-red and I don't know how to do that (simple) task: explanations and/or links are welcome.

Mike

*Edit: I found an answer that could solve my problem:

The question is : where can I put this additional info in node-red (yeah, a real newbie) ?

Glad you found something that works.
Even in AB world, a string is an array of SINT. :wink:

One thing you might can do to simplify your function is to look up the functions associated with arrays in JavaScript.

You could declare myboolarray = [], that is an empty array.

You could then use the push function to literally push values or objects to the array, even false or true literals.

myboolarray.push(false) pushes a value of false into itself. Since the array is empty, it will push the value into the first element. Any values pushed afterwards will be inserted after the previous.

Hmm, a little bit too optimistic...
I'll need to translate your (I suppose very accurate) reply into something understable for me...
I inserted the function into "function" and this is the result :frowning:

Yes, indeed strings are arrays. I have had to do some serious manipulations for a barcode scanning application in the past.

I am actually working on simplifying the code now and did make a couple of those changes by doing just that.

If my understanding of push is correct though, wouldn't that end up making the 1st input the 16th element as I push into the bool array? **Edit hah, nevermind, just found the array method reverse()

Seth was responding to me, however there is an IEEE-754 node you may be able to use. Import the code below for a quick example.

Here is a link for it: https://flows.nodered.org/flow/359ead34237b7ab6ec0465ee85a34b62

[{"id":"db8c3f1e.b7046","type":"tab","label":"Flow 3","disabled":false,"info":""},{"id":"a5ba1393.f9d44","type":"toFloat","z":"db8c3f1e.b7046","name":"","toFixed":"5","x":630,"y":200,"wires":[["574f3852.0aee28"]]},{"id":"cb29316a.731c7","type":"inject","z":"db8c3f1e.b7046","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":340,"y":200,"wires":[["1b7936ec.b8cd99"]]},{"id":"1b7936ec.b8cd99","type":"function","z":"db8c3f1e.b7046","name":"","func":"var array = 0b01000000010010010000111111010000;\nmsg.payload = array;\nreturn msg;","outputs":1,"noerr":0,"x":480,"y":200,"wires":[["a5ba1393.f9d44"]]},{"id":"574f3852.0aee28","type":"debug","z":"db8c3f1e.b7046","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","x":770,"y":200,"wires":[]}]

Probably (will check soon) but those 6 lines I found seems to solve the problem.
Seems because I need to put manually the result of the float register poll into the function, instead of converting the real message... But the result is OK (in my case an instant Watts used by my home).

I need to change the function to insert the real poll now.

If the data is received with the 16th bit first, then yes. Bit 16 would be placed at array index zero.
Are you seeing that behavior with the first function you posted? You should if you receive the data like you said.

What was confusing me is the age-old little vs big endian. In the modbus response node, inputs 1-4 are shown to the left. My brain is so used to looking at things big-endian that it threw me off.

However, when Iterating through my loop input one is actually at 0,0 of the incoming modbus array. It is odd that for display it is shown littleendian, but in reality it is big-endian.

Or maybe i'm just exhausted and my brain is no longer functioning tonight.

1 Like

tagging @seth350 ,

maybe my plc is different (Tbox/Servelec), but I solved my problem without a lot of conversion, only converting the high byte of the 2 consecutives values polled from my plc.
If you have an explanation, feel free, I'm willing to understand how things are working...
Thx anyway for the help

It looks like this is solved for now, but I thought I'd mention that I run into this a lot in my line of work as well, so I made a node that makes breaking out bits from modbus words easier/cleaner for me. A word value of 2 could be transformed (depending on config) to '00000010' or {0:false,1:true,2:false,3:false} or whatever format is most convenient for you. Check it out, open an issue if you have any suggestions or problems.

1 Like

I did find that, and played with it for a while. I could never get it to do what I wanted. What is the message format on the input that it is expecting? It sounded like it was supposed to do exactly what I needed though.

For instance, if my payload is a 4 element array of 16 bit words and I set the bitunloader to use payload[0], I get an error "Property payload[0] is Undefined". If I just use Payload, it gives me only element 0 of the array.

It's expecting a number only, not an array, but I could see that being a common use case. Using a change node to set msg.payload to msg.payload[0] is what I typically do, but if adding it into the node helps reduce clutter, I could work on adding that. Anyway,
thanks for the feedback

Rodney Cluff

Senior Maintenance Technician | Automation Systems Integrator

KING ESTATE WINERY | 80854 Territorial Road | Eugene, OR 97405

King Estate Main Phone 541.942.9874 x1138 | Cell 541.393.2361

Thank you, it dawned on me after I replied that you were probably just looking for an integer on the input so by trial and error I got it working. It may be a useful feature to get it all in one node for extreme beginners to add in the future. Regardless it does save quite a few extra nodes that I had to use! Another thing that would be handy (if possible), is to allow msg.topic to pass through so if you have multiple bitunloader nodes, you can enter them all into a single function node and use topic to make it easy to manipulate the bool arrays. In my case I need to strip off the first 4 bits from each of the 4 bool arrays and stack them into a single 16 element bool array since only the first 4 of each are used.

Something I just threw together and haven't tested:

context.data = context.data ||{};
switch (msg.topic){
    case 0:
        context.data.task1 = msg.payload;
        msg = null;
        break;
    case 1:
        context.data.task2 = msg.payload;
        msg = null;
        break;
    case 2:
        context.data.task3 = msg.payload;
        msg = null;    
        break;
    case 3:
        context.data.task4 = msg.payload;
        msg = null;    
        break;
    default:
        msg = null;
        break;
}

if (context.data.task1 !== null && context.data.task2 !== null &&context.data.task3 !== null &&context.data.task4 !== null){
    i=0
    for (i=0; i<4;i++){ 
        i2 = i+0;
        msg.payload[i2] = context.data.task1[i];
    }    
    i=0
    for (i=0; i<4;i++){ 
        i2 = i+4;
        msg.payload[i2] = context.data.task2[i];
    }    
    i=0
    for (i=0; i < 4;i++){ 
        i2 = i+8;
        msg.payload[i2] = context.data.task3[i];
    }        
    i=0
    for (i=0; i < 4;i++){ 
        i2 = i+12;
        msg.payload[i2] = context.data.task4[i];
    }
    context.data = null
    return msg;
}else return msg;

I'm starting to absorb this JS and wrapping my head around writing custom function nodes. I made one to split my array and return the 4 elements on 4 outputs to feed 4 of your nodes independently.

I haven't investigated creating my own nodes yet, but I'm going to look through your source and will see about creating some of my own. If I understand your license, I am basically free to do whatever I want with it? If I do eventually create something worthy of public release it will certainly have the same license.

1 Like