Modbus RTU Sniffer

Hello,

I have an existing Modbus RTU network with a master and a slave. The slave is an energy meter, slave adress is 30:

datasheet

It is not possible to use the known node-red-contrib-modbus because the network is already existing. The two devices are already active and communicating.

I read the serial traffic via node-red-node-serialport. This is ok.

After a silence time of 500ms I split the data. The polling rate of the master should be much longer.

Here is some example of the result in msg.payload:

30,6,2,22,0,0,107,217,255,
30,6,2,22,0,0,107,217,
30,16,2,36,0,5,10,0,0,9,176,9,156,9,186,0,0,28,182,
30,0,2,36,0,5,67,214,
30,6,2,41,0,0,91,213,255,
30,6,2,41,0,0,91,213,
30,6,2,42,0,0,171,213,255,
30,6,2,42,0,0,171,213,
30,6,2,21,0,0,155,217,255,
30,6,2,21,0,0,155,217]


30,6,2,22,0,0,107,217,255
30,6,2,22,0,0,107,217
30,16,2,36,0,5,10,0,0,9,166,9,176,9,206,0,0,186,171,255
30,16,2,36,0,5,67,214
30,6,2,41,0,0,91,213,255
30,6,2,41,0,0,91,213
30,6,2,42,0,0,171,213,255
30,6,2,42,0,0,171,213
30,6,2,21,0,0,155,217,255
30,6,2,21,0,0,155,217

30,6,2,22,0,0,107,217,255
30,6,2,22,0,0,107,217,60,3,0,24,0,1,0,224,124,3,2,0,0,213,129
30,16,2,36,0,5,10,0,0,9,166,9,176,9,206,0,0,186,171,255
30,16,2,36,0,5,67,214,60,3,0,37,0,1,145,44,60,24,2,0,14,84,69
30,6,2,41,0,0,91,213,255
30,6,2,41,0,0,91,213,60,3,0,42,0,1,161,47,255,60,3,2,0,5,21,130
30,6,2,42,0,0,171,213,255
30,6,2,42,0,0,171,213,60,3,0,47,0,1,177,46,124,3,2,0,12,213,132
30,6,2,21,0,0,155,217,255
30,6,2,21,0,0,155,217,60,3,0,27,0,8,48,230,60,3,0,28,0,7,193,35,255,60,3,14,51,67,0,0,35,200,0,0,0,0,0,0,0,0,97,25,60,3,0,0,0,1,128,231,255,60,3,2,0
30,85,137,60,3,0,21,0,1,145,35,255,60,3,2,0,0,213,129,60,3,0,27,0,8,48,230,60,3,16,0,22,51,67,0,0,35,200,0,0,0,0,0,0,0,0,29,110

I tried to split the data via buffer pasrser, but it is not enought. The function code and length is changing.

Maybe somebody can help me? I am not so familar with JS, so I have no idea to start exactly to code an own function.

I think about an array as output. Only if a certain function code is active the data will be writen in an array area.

bests

This is none trivial and you will essentially be duplicating the capabilities of the modbus protocol.

But to get you started, grab the length and FC number using a buffer parser. Then from that, route the message to another buffer parser prepared for the specific FC that knows what bits are data are in what position. TIP: Set the length of an entry to -1 to read until end.

You will likely need to refer to the modbus spec (well documented around the WEB) for understanding what element is what.

PS, It is also possible to build a dynamic buffer parser spec (see the help, readme and built in demos) if you need to.

1 Like

thanks a lot! this pushes me on a good way
I think I will split it from left to right in the modbus protocol
I started with:

but I loss some date between the parsers

what should I write into mask, that it will only look on the first byte with exactly its content (here 06)?

I've written a modbus parser with buffer parser before. It's unfinished but should give you some clues. I'll try to remember to post what I have tomorrow - may be of some use.

now I use the split function instead of the buffer phraser to split the modbus signals.
looks like this:

I had two recognitions doing this:

  • I have more then 2 devices in the modbus network. There is an additional slave with ID30. The master polls also to a device with ID28, but nobody replying. Looks like the master is searching for something

  • There is a offset in the register adresses. For example I want the register 37, but the data is in 36

therefore I have 2 challenges / problems to go:

  • the master is sending the request and immediatly after it the slave is replying to it. but sometimes it does not run well. I guess the slave is replying a bit late at this message. The split at the serial adapter is after a silence of 7ms. is there a better solution for it on modbus?
  • I want to have the sum of all three phases. But the function is not ok.

msg.payload = msg.payload[0] + msg.payload[1] + msg.payload[2]; return msg;

One thing to always remember is that messages NEVER arrive at the same time - so trying to add the values from separate nodes will never work.


If the 3 phase values are in one transmission then using 3 buffer parsers is just creating unnecessary difficulties - instead use 1 buffer parser with 3 entries. That way you'll have all 3 values in one msg.

If the 3 phase values are in separate transmissions, then you can use a join node to put all the values into one msg.

See this article in the cookbook for an example of how to join messages into one object.

the 3 phase values are seperate, delayed with many seconds
so I tried it with join

now I have arrays as input and a string as output. but I want to have a number as output. didnt found the clue for the datatypes in JS world :wink:

sum:

msg.payload = msg.payload[0] + msg.payload[1] + msg.payload[2];
return msg;

You are not accessing the correct elements.

Use the debug "copy path" to get the correct path to items



canned text

There’s a great page in the docs that will explain how to use the debug panel to find the right path to any data item.

Pay particular attention to the part about the buttons that appear under your mouse pointer when you over hover a debug message property in the sidebar.

BX00Cy7yHi

https://nodered.org/docs/user-guide/messages



Once you have the correct path, then your function can just add the values - they are already numbers so no casting or coercion is required.

PS: Remember to prefix the copied path with msg. when using the copied path in a function node.

1 Like

thanks a lot! working fine now
dont know why, but it has to look like this:

msg.payload = msg.payload[0][0] + msg.payload[1][0] + msg.payload[2][0];

I am happy for the solution at the moment, but not happy that I only have the 3 interesting values on the modbus :wink:

Has anybody a good idea to split the serial data correctly?

Sure, however the issue is sniffing the responses of modbus RTU does not tell you what addresses were requested.

The only thing you can do is decode the data.

This is an example of Modbus TCP decoding of various types and sizes...

eDWuVtcg5N

Demo flow...

[{"id":"cf5cd97ad62b91fc","type":"function","z":"efd08af1caf6c97a","name":"inc SID","func":"var sid = context.get(\"sid\") || 0;\nsid++;\nif(sid > 250) sid = 0;\ncontext.set(\"sid\", sid) \nmsg.sid = sid;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":588,"y":160,"wires":[["10b0aca361cf566f"]]},{"id":"3429ea46be924003","type":"inject","z":"efd08af1caf6c97a","name":"FC4: Read 50 from address 100","props":[{"p":"topic","vt":"str"},{"p":"sid","v":"1","vt":"num"},{"p":"pid","v":"0","vt":"num"},{"p":"len","v":"6","vt":"num"},{"p":"uid","v":"1","vt":"num"},{"p":"fc","v":"4","vt":"num"},{"p":"address","v":"100","vt":"num"},{"p":"registerCount","v":"50","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":338,"y":128,"wires":[["cf5cd97ad62b91fc"]]},{"id":"f9d9dc1121cb8994","type":"inject","z":"efd08af1caf6c97a","name":"FC3: Read 20 from address 0","props":[{"p":"topic","vt":"str"},{"p":"sid","v":"1","vt":"num"},{"p":"pid","v":"0","vt":"num"},{"p":"len","v":"6","vt":"num"},{"p":"uid","v":"1","vt":"num"},{"p":"fc","v":"3","vt":"num"},{"p":"address","v":"0","vt":"num"},{"p":"registerCount","v":"20","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":328,"y":80,"wires":[["cf5cd97ad62b91fc"]]},{"id":"7ca47c77dd11af45","type":"inject","z":"efd08af1caf6c97a","name":"FC1: Read 16 DO","props":[{"p":"topic","vt":"str"},{"p":"sid","v":"1","vt":"num"},{"p":"pid","v":"0","vt":"num"},{"p":"len","v":"6","vt":"num"},{"p":"uid","v":"1","vt":"num"},{"p":"fc","v":"1","vt":"num"},{"p":"address","v":"0","vt":"num"},{"p":"registerCount","v":"16","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":298,"y":176,"wires":[["cf5cd97ad62b91fc"]]},{"id":"b22f5fa6ca24eb05","type":"inject","z":"efd08af1caf6c97a","name":"FC2: Read 32 DI","props":[{"p":"topic","vt":"str"},{"p":"sid","v":"1","vt":"num"},{"p":"pid","v":"0","vt":"num"},{"p":"len","v":"6","vt":"num"},{"p":"uid","v":"1","vt":"num"},{"p":"fc","v":"2","vt":"num"},{"p":"address","v":"0","vt":"num"},{"p":"registerCount","v":"32","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":288,"y":224,"wires":[["cf5cd97ad62b91fc"]]},{"id":"10b0aca361cf566f","type":"buffer-maker","z":"efd08af1caf6c97a","name":"build MB Request","specification":"spec","specificationType":"ui","items":[{"name":"sid","type":"uint16be","length":1,"dataType":"msg","data":"sid"},{"name":"pid","type":"uint16be","length":1,"dataType":"msg","data":"pid"},{"name":"len","type":"uint16be","length":1,"dataType":"msg","data":"len"},{"name":"uid","type":"byte","length":1,"dataType":"msg","data":"uid"},{"name":"fc","type":"byte","length":1,"dataType":"msg","data":"fc"},{"name":"address","type":"uint16be","length":1,"dataType":"msg","data":"address"},{"name":"registerCount","type":"uint16be","length":1,"dataType":"msg","data":"registerCount"}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","x":794,"y":160,"wires":[["c3bbb6faf69e775e","a955bd29ac9f2266"]]},{"id":"c3bbb6faf69e775e","type":"debug","z":"efd08af1caf6c97a","name":"modbus request","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1056,"y":160,"wires":[]},{"id":"a955bd29ac9f2266","type":"tcp request","z":"efd08af1caf6c97a","server":"localhost","port":"11502","out":"sit","ret":"buffer","splitc":" ","name":"","x":666,"y":240,"wires":[["395f8760ef281c88"]]},{"id":"395f8760ef281c88","type":"buffer-parser","z":"efd08af1caf6c97a","name":"Parse Response","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"int16be","name":"header=>sid","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"int16be","name":"header=>pid","offset":2,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"int16be","name":"header=>len","offset":4,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint8","name":"header=>uid","offset":6,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint8","name":"header=>fc","offset":7,"length":1,"offsetbit":0,"scale":"1","mask":"0x7F"},{"type":"bool","name":"header=>error","offset":7,"length":1,"offsetbit":7,"scale":"1","mask":""},{"type":"uint8","name":"header=>errorCode","offset":8,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint8","name":"header=>byteCount","offset":8,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"buffer","name":"buffer","offset":9,"length":-1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"output","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":896,"y":240,"wires":[["ab788dd9a35170c0","8bdcbbb6b7f36f28"]]},{"id":"ab788dd9a35170c0","type":"debug","z":"efd08af1caf6c97a","name":"modbus response 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1114,"y":240,"wires":[]},{"id":"8bdcbbb6b7f36f28","type":"switch","z":"efd08af1caf6c97a","name":"bits or words?","property":"payload.header.fc","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"},{"t":"eq","v":"3","vt":"num"},{"t":"eq","v":"4","vt":"num"}],"checkall":"true","repair":false,"outputs":4,"x":672,"y":320,"wires":[["e9ba20391b520469"],["e9ba20391b520469"],["564fce4bc9d55894"],["564fce4bc9d55894"]]},{"id":"e9ba20391b520469","type":"buffer-parser","z":"efd08af1caf6c97a","name":"Parse bits","data":"payload.buffer","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"bool","name":"data","offset":0,"length":-1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"swap16","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"data","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"output","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":876,"y":304,"wires":[["34ebe7247fad8c86","606921269651c652"]]},{"id":"564fce4bc9d55894","type":"buffer-parser","z":"efd08af1caf6c97a","name":"Parse WORDs","data":"payload.buffer","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint16be","name":"data","offset":0,"length":-1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"data","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"output","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":896,"y":336,"wires":[["34ebe7247fad8c86","22551c97d4e7cb30"]]},{"id":"34ebe7247fad8c86","type":"function","z":"efd08af1caf6c97a","name":"check response","func":"var header = msg.payload.header || {};\nvar sidOK = msg.sid == header.sid;\nvar pidOK = msg.pid == header.pid;\nvar uidOK = msg.uid == header.uid;\nvar fcOK = msg.fc == header.fc;\n\nvar statText = \"\";\nif(header.error) {\n    statText = getErrDesc(header.errorCode);\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\n\nmsg.payload.data = msg.data.data;\n//msg.payload.buffer = msg.data.buffer;\n\n\nvar registerCount = msg.registerCount;\nif(msg.fc == 1 || msg.fc == 2) {\n   registerCount = Math.ceil(registerCount / 16);\n} \n\nvar countOK = registerCount == (header.byteCount / 2)\n\nif(!countOK && registerCount > 127) {\n    countOK = registerCount == msg.payload.data.length;\n}\n\n\nif(!sidOK) {\n    statText = \"SID does not match. Sent: ${msg.sid}, Received: ${header.sid}\";\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\nif(!pidOK) {\n    statText = \"PID does not match. Sent: ${msg.pid}, Received: ${header.pid}\";\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\nif(!uidOK) {\n    statText = \"Unit ID does not match. Sent: ${msg.uid}, Received: ${header.uid}\";\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\nif(!fcOK) {\n    statText = \"FC does not match. Sent: ${msg.fc}, Received: ${header.fc}\";\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\nif(!countOK) {\n    statText = \"Response register count does not match requested count\";\n    node.error(statText, msg);\n    node.status({text:statText});\n    return null;\n}\nstatText = `OK. FC: ${msg.fc}, Addr: ${msg.address}[${msg.registerCount}], SID: (${msg.sid}) : Got ${msg.payload.data.length} register(s)` ;\nnode.status({text:statText});\n\nfunction getErrDesc(errCode) {\n    const errDefs = {\n        1 : \"The received function code can not be processed.\",\n        2 : \"The data address specified in the request is not available.\",\n        3 : \"The value contained in the query data field is an invalid value.\",\n        4 : \"An unrecoverable error occurred while the slave attempted to perform the requested action.\",\n        5 : \"The slave has accepted the request and processes it, but it takes a long time. This response prevents the host from generating a timeout error.\",\n        6 : \"The slave is busy processing the command. The master must repeat the message later when the slave is freed.\",\n        7 : \"The slave can not execute the program function specified in the request. This code is returned for an unsuccessful program request using functions with numbers 13 or 14. The master must request diagnostic information or error information from the slave.\",\n        8 : \"The slave detected a parity error when reading the extended memory. The master can repeat the request, but usually in such cases, repairs are required.\",\n    }\n    return errDefs[errCode] || `Unknwon Error ${errCode}`    \n}\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":912,"y":400,"wires":[["9e7cb7da152e30fc"]]},{"id":"606921269651c652","type":"debug","z":"efd08af1caf6c97a","name":"words","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1074,"y":304,"wires":[]},{"id":"22551c97d4e7cb30","type":"debug","z":"efd08af1caf6c97a","name":"bits","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1074,"y":336,"wires":[]},{"id":"9e7cb7da152e30fc","type":"debug","z":"efd08af1caf6c97a","name":"modbus response 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1146,"y":400,"wires":[]}]

You can use the info here: https://ipc2u.com/articles/knowledge-base/modbus-rtu-made-simple-with-detailed-descriptions-and-examples/ to help you understand the RTU protocol and adapt the demo flow.

ok, to do it this way is a good idea also
but I meant to split the raw data from the serial interface...

finding the beginning of the transmission is the challenge, and finding the end of the response
ideally have both together in one message

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.