Complete guide: Modbus RTU and TCP with Python and Node-RED
To write and read a coil over Modbus TCP from a Raspberry PLC in Node-RED, install the node-red-contrib-modbus nodes and create a Modbus-Client with Type TCP, the IP address of the slave and port 502. Then use a Modbus Write node with FC 5 and a Modbus Read node with FC 1. Give both nodes the same Unit-Id, the one that the device expects (often 1 or 255). The flow of this post was tested with Node-RED 4.1 and node-red-contrib-modbus 5 against a simulated slave.
This is part 2 of the Modbus and Raspberry PLC series. Parts 1 and 2 use Node-RED, and parts 3 and 4 use Python. All the details, such as timeouts, addressing, 32-bit values and troubleshooting, are in the Modbus guide for Raspberry Pi PLCs.
What you need
- A Raspberry PLC with Node-RED. It is the Modbus TCP client (master). If Node-RED is not installed yet, see how to install Node-RED on a Raspberry PLC.
- A Modbus TCP server (slave): a meter, a drive, another PLC or a gateway. To test without a device, run the Python Modbus TCP server of the Modbus guide on the same PLC and use the address
127.0.0.1and port5020. - An Ethernet connection between the PLC and the slave, with both in the same subnet. By default,
eth0of the Raspberry PLC has the address 10.10.10.20. To change it, see how to configure a static IP on a Raspberry PLC. - The IP address, the TCP port and the Unit-Id of the slave. This post uses a slave at 10.10.10.4, port 502, Unit-Id 1.
Start Node-RED and install the Modbus nodes
Start Node-RED in a terminal of the PLC:
node-red-start
Open the editor in a browser. From another computer, use http://PLC_IP_ADDRESS:1880. On the PLC itself, with a screen connected, use http://127.0.0.1:1880.
Open Menu, Manage palette (or press Alt+Shift+P), go to the Install tab, search for node-red-contrib-modbus and install it. Version 5.62 of these nodes needs Node-RED 4 or later and Node.js 22 or later, so check node -v first. If you did part 1, the nodes are already installed.
Configure the Modbus-Client for TCP
Drag a Modbus Write node to the flow, double click it, and click the pencil icon next to Server. This opens the Modbus-Client: the connection that all Modbus nodes of the flow share.
| Field | Value | Note |
|---|---|---|
| Type | TCP | Modbus TCP goes over Ethernet |
| Host | 10.10.10.4 | IP address of the slave |
| Port | 502 | The Modbus TCP port of most devices. Use 5020 for the Python test server of the guide |
| TCP Type | DEFAULT | |
| Unit-Id | 1 | Default Unit-Id for the nodes that leave their own Unit-Id empty |
Give the configuration a name and click Update. For each slave with a different IP address, create one more Modbus-Client.
Unit-Id in Modbus TCP
On Modbus TCP, the IP address selects the device. The Unit-Id still travels in the header of every request, and it selects a device behind a gateway, for example an RTU slave behind a Modbus TCP to RTU gateway. Many devices ignore it, and others answer only to 1 or to 255. Use the value in the manual of the device, and the same value in all the nodes.
The first version of this flow had Unit-Id 1 in Modbus Write and 31 in Modbus Read. Against our simulated slave, which checks the Unit-Id, the write worked, the read got an exception response, and the Debug panel showed nothing. The flow of this post uses Unit-Id 1 in both nodes.
Write a coil with Modbus Write (FC 5)
Configure the Modbus Write node:
- Unit-Id:
1. - FC:
FC 5: Force Single Coil. - Address:
0. Coil 00001 in the manual of the device is address 0. - Quantity:
1. - Server: the Modbus-Client of the previous step.
Add an Inject node, set msg.payload to the boolean true and connect it to Modbus Write. Add a second Inject node with the boolean false. Each click on an Inject node sends one write request over Ethernet: true turns the coil on and false turns it off.

Read the coil with Modbus Read (FC 1)
Add a Modbus Read node with the same Modbus-Client:
- Unit-Id:
1, the same as in Modbus Write. - FC:
FC 1: Read Coil Status. - Address:
0. Quantity:1. - Poll Rate:
1second.
Connect a Debug node to the first output of Modbus Read, which gives the data array, and click Deploy. The Debug panel shows a new message every second. The array has 8 values, because the answer carries the coils in whole bytes. The first value is coil 0.
In our test, after a click on the true Inject node, the next reads showed an array that starts with true, and after a click on false, an array that starts with false.

Import the complete flow
To skip the manual steps, open Menu, Import, paste this flow and click Import. Then change the IP address, the port and the Unit-Id to the values of your slave.
[{"id":"3c784f724b6bb9da","type":"modbus-write","z":"66f823dbe170e15a","name":"","showStatusActivities":true,"showErrors":true,"showWarnings":true,"unitid":"1","dataType":"Coil","adr":"0","quantity":"1","server":"a0cecbb6e2cd25e8","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":560,"y":220,"wires":[[],[]]},{"id":"3459adaa212c8dd3","type":"inject","z":"66f823dbe170e15a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":390,"y":200,"wires":[["3c784f724b6bb9da"]]},{"id":"526661ccfde56f87","type":"inject","z":"66f823dbe170e15a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":390,"y":240,"wires":[["3c784f724b6bb9da"]]},{"id":"f5c9e191b12f95d7","type":"modbus-read","z":"66f823dbe170e15a","name":"","topic":"","showStatusActivities":false,"logIOActivities":false,"showErrors":false,"showWarnings":true,"unitid":"1","dataType":"Coil","adr":"0","quantity":"1","rate":"1","rateUnit":"s","delayOnStart":false,"startDelayTime":"","server":"a0cecbb6e2cd25e8","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"x":430,"y":320,"wires":[["3ff9cb94cb91b7e8"],[]]},{"id":"3ff9cb94cb91b7e8","type":"debug","z":"66f823dbe170e15a","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":580,"y":320,"wires":[]},{"id":"a0cecbb6e2cd25e8","type":"modbus-client","name":"","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"queueLogEnabled":false,"failureLogEnabled":true,"tcpHost":"10.10.10.4","tcpPort":"502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","serialAsciiResponseStartDelimiter":"0x3A","unit_id":"1","commandDelay":"1","clientTimeout":"1000","reconnectOnTimeout":true,"reconnectTimeout":"2000","parallelUnitIdsAllowed":true,"showWarnings":true,"showLogs":true}]
When the slave does not answer
| Symptom | What to check |
|---|---|
| Connection refused | The server does not run, the port is wrong or a firewall blocks it |
| No connection, timeout | IP address of the slave, cable, subnet. Two new Raspberry PLCs on one network both have 10.10.10.20 on eth0: change one of them. Check with ip a and ping |
| The write works but Debug shows nothing | The Unit-Id of Modbus Read. Use the same Unit-Id as in Modbus Write, the one of the device manual |
| Exception code 02 (illegal data address) | The coil address does not exist in the slave. Check the coil table in the manual of the slave |
| Exception code 0B (gateway target device failed to respond) | A Modbus TCP to RTU gateway got no answer from the RTU slave with that Unit-Id |
To see errors in the Debug panel, enable Show Errors in the Modbus Read and Modbus Write nodes.
What we tested. Node-RED 4.1.17 on Node.js 22 (Linux) with node-red-contrib-modbus 5.62.1 and 5.23.3. Version 5.62.1 ran with the modbus-serial 8.0.25 library from npm in place of its own fork of that library. The flow of this post, with only the host and the port changed to 127.0.0.1 and a free port, ran against a simulated Modbus TCP slave (pymodbus 3.16.1, Unit-Id 1). The true and false Inject nodes turned coil 0 on and off, and Modbus Read showed the new value within one second. The first version of the flow, with Unit-Id 31 in Modbus Read, gave no data in Debug against the same slave. We did not run this update on Raspberry PLC hardware: the two screenshots come from the first version of the post.
Find the fault in a Modbus RTU and TCP network. In the simulator, an M-Duino 21 polls four Modbus RTU slaves on RS-485 and serves the data on Modbus TCP. Switch a slave off or change the baud rate, and find the fault in the frames.
Open the Modbus network simulatorOpen the complete Modbus guide
FAQ about Modbus TCP in Node-RED
Do I need the Unit-Id with Modbus TCP?
The IP address selects the device, but the Unit-Id is still in every request. Many devices ignore it, some answer only to 1 or 255, and a gateway uses it to select the RTU slave behind it. Use the value of the device manual in all the nodes.
Which TCP port does Modbus TCP use?
Port 502 is the standard. Some devices let you change it, and test servers on Linux often use 5020, because only root can open ports under 1024.
Can several clients read the same Modbus TCP slave?
Yes, unlike Modbus RTU, where there is one master per line. A SCADA system and the Node-RED flow can read the same server at the same time, if the device accepts several connections.
Why does the Debug panel show 8 values for one coil?
Because the Modbus answer carries the coils in whole bytes, and the node gives all the bits of those bytes. Use only the first value.
How do I do the same in Python?
With ModbusTcpClient of pymodbus. See part 4: Modbus TCP and Python.
More about Modbus on the Raspberry PLC
- Modbus TCP and RTU guide for Raspberry Pi PLCs: Python and Node-RED examples
- Part 1: Modbus RTU and Node-RED
- Part 3: Modbus RTU and Python
- Part 4: Modbus TCP and Python
- TCP socket between Python and Node-RED on a Raspberry PLC
- Case study: Modbus RTU thermometers collected over Modbus TCP
- Choosing an industrial controller: PLC, embedded controller, IPC or gateway
Choose the model and the I/O mix in the Raspberry PLC family.