This is a Python-based CLI tool for implementing Modbus RTU and TCP communication, acting as both a master (client) and slave (server). It uses the pymodbus and click libraries and supports testing with Node-RED, Python scripts, and socat for serial port simulation. Below is a complete guide to set up and test the tool.
The momodbus tool supports the following Modbus function codes:
-
Read Operations:
- FC 1: Read Coils
- FC 2: Read Discrete Inputs
- FC 3: Read Holding Registers
- FC 4: Read Input Registers
-
Write Operations:
- FC 5: Write Single Coil
- FC 6: Write Single Register
- FC 15: Write Multiple Coils
- FC 16: Write Multiple Registers
- Python 3.13+
- pip (Python package manager)
- Node.js and npm (for Node-RED)
- socat (for serial port simulation)
- pyserial
- pymodbus
- click
- A Unix-like system (e.g., macOS, Linux) or WSL on Windows
A virtual environment isolates project dependencies. Follow these steps:
- Create a virtual environment:
python3 -m venv path/to/venv
- Activate the virtual environment:
- On macOS/Linux:
source path/to/venv/bin/activate - On Windows (WSL or cmd):
venv\Scripts\activate
- On macOS/Linux:
After activating the virtual environment, install the necessary Python libraries:
- Install
clickandpymodbus:pip install click pymodbus pyserial
- Make the script executable:
chmod +x momodbus.py
Node-RED is a flow-based programming tool for integrating Modbus devices.
- Install Node.js and npm (if not already installed):
- On macOS/Linux, use a package manager like
breworapt:orbrew install node
sudo apt update && sudo apt install nodejs npm
- On macOS/Linux, use a package manager like
- Install Node-RED globally:
npm install -g node-red
- Start Node-RED to verify installation:
node-red
- Access it at
http://localhost:1880in your browser.
- Access it at
socat creates virtual serial ports for testing RTU communication.
- Install
socat:- On macOS:
brew install socat
- On Ubuntu/Debian:
sudo apt install socat
- On other systems, check your package manager or download from socat.net.
- On macOS:
Ensure all tools are installed:
- Check Python:
python3 --version - Check
click:pip show click - Check
pymodbus:pip show pymodbus - Check Node-RED:
node-red --version - Check
socat:socat -V
The slave can run as an RTU or TCP server. Use a virtual serial port with socat for RTU testing.
-
Start a TCP Slave (Port 10502):
./momodbus.py slave --protocol tcp --tcp-port 10502 --random-init --random-update --max-register-value 1000
- This starts a TCP server on
0.0.0.0:10502with random initial values and periodic updates. Make sure adjust the host IP Address as needed.
- This starts a TCP server on
-
Start an RTU Slave with
socat:- Create virtual serial ports:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
- This outputs two device paths (e.g.,
/dev/ttys001and/dev/ttys002). Note these paths.
- This outputs two device paths (e.g.,
- Start the slave on one port (e.g.,
/dev/ttys001):./momodbus.py slave --protocol rtu --port /dev/ttys001 --baudrate 9600 --random-init --random-update --max-register-value 1000
socatsimulates a serial connection between the slave and master.
- Create virtual serial ports:
Use the read and write commands to interact with the slave.
-
Read Holding Registers via TCP:
./momodbus.py read --protocol tcp --host localhost --tcp-port 10502 --unit-id 1 --function-code 3 --address 1 --count 5- Reads 5 holding registers starting at address 1.
-
Write Random Registers via RTU:
- Use the second
socatport (e.g.,/dev/ttys002) as the master port:./momodbus.py write --protocol rtu --port /dev/ttys002 --baudrate 9600 --unit-id 1 --function-code 16 --address 1 --count 3 --random-values
- Writes 3 random values to registers starting at address 1.
- Use the second
-
Continuous Reading (2 Hz):
./momodbus.py read --protocol tcp --host localhost --tcp-port 10502 --unit-id 1 --function-code 3 --address 1 --count 3 --sampling-rate 2- Press Ctrl+C to stop.
Node-RED can act as a Modbus client to communicate with the momodbus slave.
-
Install Modbus Nodes:
- Open Node-RED (
http://localhost:1880). - Go to Menu > Manage Palette > Install tab.
- Search for
node-red-contrib-modbusand click Install.
- Open Node-RED (
-
Node-RED JSON Flows for All Scenarios: Import the following JSON configurations into Node-RED (Menu > Import) to test all combinations:
-
TCP Master (Read Holding Registers):
[ { "id": "1", "type": "tab", "label": "TCP Master" }, { "id": "2", "type": "modbus-read", "z": "1", "name": "Read Holding Registers", "topic": "", "showStatusActivities": false, "showErrors": false, "showWarnings": true, "unitid": 1, "dataType": "HoldingRegister", "adr": 1, "quantity": 5, "rate": "5000", "rateUnit": "ms", "delayOnStart": false, "startDelayTime": "", "server": "3", "useIOFile": false, "ioFileName": "", "keepAlive": false, "x": 300, "y": 140, "wires": [["4"]] }, { "id": "3", "type": "modbus-client", "z": "1", "name": "TCP Server", "clienttype": "tcp", "bufferCommands": true, "stateLogEnabled": false, "queueLogEnabled": false, "failureLogEnabled": false, "tcpHost": "localhost", "tcpPort": "10502", "tcpType": "DEFAULT", "serialPort": "/dev/ttys001", "serialBaudrate": "9600", "serialDatabits": "8", "serialStopbits": "1", "serialParity": "none", "serialType": "RTU", "serialTimeOut": "1000", "serialProtocol": "RTU", "x": 120, "y": 140 }, { "id": "4", "type": "debug", "z": "1", "name": "Debug Output", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 480, "y": 140 } ]- Start the TCP slave (
./momodbus.py slave --protocol tcp --tcp-port 10502 ...) and deploy this flow. Make sure both host being used is the same (IP) host.
- Start the TCP slave (
-
RTU Master (Read Holding Registers):
[ { "id": "5", "type": "tab", "label": "RTU Master" }, { "id": "6", "type": "modbus-read", "z": "5", "name": "Read Holding Registers", "topic": "", "showStatusActivities": false, "showErrors": false, "showWarnings": true, "unitid": 1, "dataType": "HoldingRegister", "adr": 1, "quantity": 5, "rate": "5000", "rateUnit": "ms", "delayOnStart": false, "startDelayTime": "", "server": "7", "useIOFile": false, "ioFileName": "", "keepAlive": false, "x": 300, "y": 140, "wires": [["8"]] }, { "id": "7", "type": "modbus-client", "z": "5", "name": "RTU Server", "clienttype": "serial", "bufferCommands": true, "stateLogEnabled": false, "queueLogEnabled": false, "failureLogEnabled": false, "tcpHost": "localhost", "tcpPort": "10502", "tcpType": "DEFAULT", "serialPort": "/dev/ttys002", "serialBaudrate": "9600", "serialDatabits": "8", "serialStopbits": "1", "serialParity": "none", "serialType": "RTU", "serialTimeOut": "1000", "serialProtocol": "RTU", "x": 120, "y": 140 }, { "id": "8", "type": "debug", "z": "5", "name": "Debug Output", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 480, "y": 140 } ]- Start the RTU slave with
socatand deploy this flow (adjust/dev/ttys002to match your socat output).
- Start the RTU slave with
-
TCP Client (Write Registers):
[ { "id": "9", "type": "tab", "label": "TCP Client" }, { "id": "10", "type": "inject", "z": "9", "name": "Trigger Write", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "[10, 20, 30, 40, 50]", "payloadType": "json", "x": 140, "y": 140, "wires": [["11"]] }, { "id": "11", "type": "modbus-write", "z": "9", "name": "Write Registers", "showStatusActivities": false, "showErrors": false, "showWarnings": true, "unitid": 1, "dataType": "HoldingRegister", "adr": 1, "quantity": 5, "server": "12", "emptyMsgOnFail": false, "keepAlive": false, "x": 300, "y": 140, "wires": [["13"]] }, { "id": "12", "type": "modbus-client", "z": "9", "name": "TCP Server", "clienttype": "tcp", "bufferCommands": true, "stateLogEnabled": false, "queueLogEnabled": false, "failureLogEnabled": false, "tcpHost": "0.0.0.0", "tcpPort": "10502", "tcpType": "DEFAULT", "serialPort": "/dev/ttys001", "serialBaudrate": "9600", "serialDatabits": "8", "serialStopbits": "1", "serialParity": "none", "serialType": "RTU", "serialTimeOut": "1000", "serialProtocol": "RTU", "x": 140, "y": 60 }, { "id": "13", "type": "debug", "z": "9", "name": "TCP Client Debug Output", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 460, "y": 140 } ]- Start the TCP slave and deploy this flow to write
[10, 20, 30, 40, 50]to registers.
- Start the TCP slave and deploy this flow to write
-
RTU Client (Write Registers):
[ { "id": "14", "type": "tab", "label": "RTU Client" }, { "id": "15", "type": "inject", "z": "14", "name": "Trigger Write", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "[10, 20, 30, 40, 50]", "payloadType": "json", "x": 140, "y": 140, "wires": [["16"]] }, { "id": "16", "type": "modbus-write", "z": "14", "name": "Write Registers", "showStatusActivities": false, "showErrors": false, "showWarnings": true, "unitid": 1, "dataType": "HoldingRegister", "adr": 1, "quantity": 5, "server": "17", "emptyMsgOnFail": false, "keepAlive": false, "x": 300, "y": 140, "wires": [["18"]] }, { "id": "17", "type": "modbus-client", "z": "14", "name": "RTU Server", "clienttype": "serial", "bufferCommands": true, "stateLogEnabled": false, "queueLogEnabled": false, "failureLogEnabled": false, "tcpHost": "localhost", "tcpPort": "10502", "tcpType": "DEFAULT", "serialPort": "/dev/ttys002", "serialBaudrate": "9600", "serialDatabits": "8", "serialStopbits": "1", "serialParity": "none", "serialType": "RTU", "serialTimeOut": "1000", "serialProtocol": "RTU", "x": 140, "y": 60 }, { "id": "18", "type": "debug", "z": "14", "name": "Debug Output", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 460, "y": 140 } ]- Start the RTU slave with
socatand deploy this flow (adjust/dev/ttys002to match your socat output).
- Start the RTU slave with
-
Write a Python script to act as a master using pymodbus.
- Create
modbus_master.py:from pymodbus.client import ModbusTcpClient client = ModbusTcpClient('localhost', port=10502) client.connect() # Read 5 holding registers result = client.read_holding_registers(address=1, count=5, slave=1) print(f"Read registers: {result.registers}") # Write random values values = [10, 20, 30, 40, 50] client.write_registers(address=1, values=values, slave=1) print(f"Wrote values: {values}") client.close()
- Run it in the virtual environment:
python modbus_master.py
- Serial Port Issues: Ensure
socatports are active and permissions are correct (e.g.,sudo chmod 666 /dev/ttys*). - Connection Errors: Verify the slave is running and ports match.
- Node-RED Errors: Check the Node-RED log (Menu > View Log) for issues.
- The script logs at the INFO level by default.
- Adjust
max-register-valueand other parameters as needed. - For advanced setups, explore Node-RED dashboards or Python threading for concurrent operations.