Creating a Modbus Server: Simulating Industrial Devices
Imagine you’re a chef developing a new recipe. You wouldn’t test it on paying customers first—you’d practice in your kitchen with friends. Similarly, when developing Modbus clients, you need a reliable server to test against. Creating a Modbus server simulation lets you test your client applications without needing physical devices.
In this guide, we’ll walk you through creating Modbus server simulations using two popular approaches:
1. Python with pymodbus (for coders)
2. Node-RED (for visual, low-code users)
We’ll cover data point mapping, handling multiple requests, simulating realistic device behavior, and testing with client tools.
Why Simulate a Modbus Server?
Before we dive in, let’s understand the benefits of creating a Modbus server simulation:
- Parallel Development: Allow client and server development to proceed independently
Prerequisites
For Python Approach
- A Modbus client tool (Modbus Poll demo or Simply Modbus)
For Node-RED Approach
- Modbus nodes for Node-RED (`node-red-contrib-modbus`)
Part 1: Python Modbus Server with pymodbus
Python’s pymodbus library makes it easy to create Modbus servers. We’ll build a server that simulates a temperature controller with the following features:
- Realistic Behavior: Temperature that changes over time
Example 1: Basic Modbus TCP Server
“`python
save as basic_modbus_server.py
from pymodbus.server import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
import time
import threading
Create a datastore for our Modbus server
This will hold all our registers, coils, etc.
store = ModbusSlaveContext(
# Digital Inputs (read-only): [initial values]
di=ModbusSequentialDataBlock(0, [0]*10),
# Coils (read/write): [initial values]
co=ModbusSequentialDataBlock(0, [0]*10),
# Holding Registers (read/write): [initial values]
hr=ModbusSequentialDataBlock(0, [250, 500, 1000, 2000, 3000]), # Temp:25.0°C, Press:50.0kPa, Setpt1:100.0, Setpt2:200.0, Setpt3:300.0
# Input Registers (read-only): [initial values]
ir=ModbusSequentialDataBlock(0, [220, 450, 1023, 0]) # Temp:22.0°C, Press:45.0kPa, Humidity:100%, Status:0
)
Create a server context with our datastore
context = ModbusServerContext(slaves=store, single=True)
Configure server identification
identity = ModbusDeviceIdentification()
identity.VendorName = ‘SimulatedDevices’
identity.ProductCode = ‘SM’
identity.VendorUrl = ‘http://simulateddevices.example.com’
identity.ProductName = ‘Simulated Modbus Server’
identity.ModelName = ‘SMS-1000’
identity.MajorMinorRevision = ‘1.0’
Function to update input registers with simulated values
def update_sensors():
“””Simulate changing sensor values”””
while True:
try:
# Get current temperature (scaled by 10: 220 = 22.0°C)
current_temp = store.getValues(3, 0)[0] # 3 = Input Registers, 0 = starting address
# Simulate temperature fluctuation (±0.1°C)
import random
new_temp = current_temp + random.randint(-1, 1)
# Keep temperature within realistic range (15-35°C)
if 150 <= new_temp <= 350:
# Update input register 0 (temperature)
store.setValues(3, 0, [new_temp])
# Simulate pressure change (±0.5 kPa)
current_press = store.getValues(3, 1)[0] # Input Register 1 = pressure
new_press = current_press + random.randint(-5, 5)
if 300 <= new_press <= 600:
store.setValues(3, 1, [new_press])
time.sleep(1) # Update every second
except Exception as e:
print(f”Error updating sensors: {e}”)
time.sleep(1)
if __name__ == “__main__”:
print(“Starting Modbus TCP Server…”)
print(“Server Address: 0.0.0.0 (all interfaces)”)
print(“Port: 502”)
print(“\nPress Ctrl+C to stop the server”)
# Start sensor update thread
sensor_thread = threading.Thread(target=update_sensors)
sensor_thread.daemon = True # Allow thread to exit when main program exits
sensor_thread.start()
try:
# Start the Modbus server
StartTcpServer(
context=context,
identity=identity,
address=(”, 502) # Listen on all interfaces, port 502
)
except KeyboardInterrupt:
print(“\nStopping Modbus TCP Server…”)
except Exception as e:
print(f”Error starting server: {e}”)
“`
Running the Python Server
1. Save the code as `basic_modbus_server.py`
2. Open a terminal and run:
“`bash
python basic_modbus_server.py
“`
3. The server will start and listen on port 502
4. Sensor values will update every second
Example 2: Multiple Slave IDs and Advanced Features
For more complex simulations, you can create a server with multiple slave IDs:
“`python
save as multi_slave_modbus_server.py
from pymodbus.server import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
import time
import threading
Create datastores for multiple slaves
datastores = {
# Slave ID 1: Temperature Controller
1: ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0]*10),
co=ModbusSequentialDataBlock(0, [0]*10),
hr=ModbusSequentialDataBlock(0, [250, 500, 1000]), # Temp:25.0°C, Setpt:50.0°C, Mode:1000
ir=ModbusSequentialDataBlock(0, [220, 450, 100]) # Input Temp:22.0°C, Press:45.0kPa, Humidity:10.0%
),
# Slave ID 2: Motor Drive
2: ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [1, 0, 1]), # Status bits: Running, Fault, Ready
co=ModbusSequentialDataBlock(0, [1, 0]), # Control bits: Start/Stop, Direction
hr=ModbusSequentialDataBlock(0, [1500, 200, 100]), # Speed:1500 RPM, Torque:200 Nm, Current:100 A
ir=ModbusSequentialDataBlock(0, [1498, 198, 99]) # Feedback:1498 RPM, Torque:198 Nm, Current:99 A
),
# Slave ID 3: Power Meter
3: ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [1, 1]), # Status: Active, Online
co=ModbusSequentialDataBlock(0, [0]), # Reset command
hr=ModbusSequentialDataBlock(0, [2300, 5000, 1000]), # Voltage:230.0V, Current:50.0A, Power:1000.0W
ir=ModbusSequentialDataBlock(0, [2305, 4998, 9995]) # Measured:230.5V, 49.98A, 999.5W
)
}
Create server context with multiple slaves
context = ModbusServerContext(slaves=datastores, single=False)
Configure server identification
identity = ModbusDeviceIdentification()
identity.VendorName = ‘MultiSlaveSimulator’
identity.ProductCode = ‘MSS’
identity.ProductName = ‘Multi-Slave Modbus Simulator’
identity.ModelName = ‘MSS-3000’
identity.MajorMinorRevision = ‘1.0’
Function to update all slave input registers
def update_all_sensors():
“””Update sensor values for all slaves”””
while True:
try:
import random
# Update Slave 1 (Temperature Controller)
current_temp = datastores[1].getValues(3, 0)[0]
new_temp = current_temp + random.randint(-1, 1)
if 150 <= new_temp <= 350:
datastores[1].setValues(3, 0, [new_temp])
# Update Slave 2 (Motor Drive) – simulate speed fluctuation
current_speed = datastores[2].getValues(3, 0)[0]
new_speed = current_speed + random.randint(-5, 5)
if 1000 <= new_speed <= 2000:
datastores[2].setValues(3, 0, [new_speed])
# Update Slave 3 (Power Meter) – simulate voltage variation
current_voltage = datastores[3].getValues(3, 0)[0]
new_voltage = current_voltage + random.randint(-10, 10)
if 2200 <= new_voltage <= 2400:
datastores[3].setValues(3, 0, [new_voltage])
time.sleep(1)
except Exception as e:
print(f”Error updating sensors: {e}”)
time.sleep(1)
if __name__ == “__main__”:
print(“Starting Multi-Slave Modbus TCP Server…”)
print(“Server Address: 0.0.0.0 (all interfaces)”)
print(“Port: 502”)
print(“Available Slave IDs: 1, 2, 3”)
print(“\nPress Ctrl+C to stop the server”)
# Start sensor update thread
sensor_thread = threading.Thread(target=update_all_sensors)
sensor_thread.daemon = True
sensor_thread.start()
try:
StartTcpServer(context=context, identity=identity, address=(”, 502))
except KeyboardInterrupt:
print(“\nStopping Modbus TCP Server…”)
except Exception as e:
print(f”Error starting server: {e}”)
“`
Part 2: Node-RED Modbus Server
Node-RED provides a visual way to create Modbus servers without writing much code. Let’s create a similar temperature controller simulation using Node-RED.
Step 1: Install Node-RED and Modbus Nodes
1. Install Node-RED:
“`bash
npm install -g node-red
“`
2. Start Node-RED:
“`bash
node-red
“`
3. Open a browser and go to `http://localhost:1880`
4. Install Modbus nodes:
– Click the menu (☰) in the top-right corner
– Select “Manage palette”
– Click the “Install” tab
– Search for “node-red-contrib-modbus”
– Click “Install”
Step 2: Create a Modbus Server Flow
1. Drag and drop the following nodes onto the workspace:
– `inject` (from input palette)
– `function` (from function palette)
– `modbus-server` (from modbus palette)
– `debug` (from output palette)
2. Connect them as follows:
– `inject` → `function` → `modbus-server`
– Connect `modbus-server` to `debug`
3. Configure the nodes:
Inject Node:
– Set “Repeat” to “Interval”
– Set “Interval” to “1 second”
– Click “Done”
Function Node:
– Double-click to open configuration
– Name it “Update Sensors”
– Add the following code:
“`javascript
// Initialize context variables if they don’t exist
if (!context.temperature) context.temperature = 220; // 22.0°C
if (!context.pressure) context.pressure = 450; // 45.0 kPa
// Simulate temperature fluctuation (±0.1°C)
context.temperature += Math.floor(Math.random() * 3) – 1;
// Keep within range (15-35°C)
if (context.temperature < 150) context.temperature = 150;
if (context.temperature > 350) context.temperature = 350;
// Simulate pressure fluctuation (±0.5 kPa)
context.pressure += Math.floor(Math.random() * 11) – 5;
// Keep within range (30-60 kPa)
if (context.pressure < 300) context.pressure = 300;
if (context.pressure > 600) context.pressure = 600;
// Output updated values
return {
payload: {
// Update input registers (read-only)
‘3:0’: context.temperature, // Input Register 0 = Temperature
‘3:1’: context.pressure, // Input Register 1 = Pressure
‘3:2’: Math.floor(Math.random() * 100) // Input Register 2 = Humidity
}
};
“`
– Click “Done”
Modbus Server Node:
– Double-click to open configuration
– Name it “Temperature Controller Server”
– Set “Server type” to “TCP”
– Set “Port” to “502”
– Under “Holding Registers”, add:
– Address: 0, Quantity: 4, Value: [250, 500, 1000, 2000]
– Under “Coils”, add:
– Address: 0, Quantity: 4, Value: [false, false, false, false]
– Under “Input Registers”, add:
– Address: 0, Quantity: 3, Value: [220, 450, 50]
– Under “Discrete Inputs”, add:
– Address: 0, Quantity: 4, Value: [true, false, true, false]
– Click “Done”
4. Deploy the flow by clicking the “Deploy” button in the top-right corner
Step 3: Test the Node-RED Server
Your Node-RED Modbus server is now running. The inject node will trigger the function every second, updating the input registers with simulated values.
Testing Your Modbus Simulation
Now that we have our Modbus servers running, let’s test them with client tools.
Option 1: Using Modbus Poll (GUI Tool)
1. Download and install Modbus Poll demo from [Modbus Tools](https://www.modbustools.com/download.html)
2. Open Modbus Poll
3. Go to “Connection” → “Connect…”
4. Set connection type to “TCP/IP”
5. Enter “localhost” as the IP address
6. Leave port as 502
7. Click “OK”
8. Go to “Setup” → “Read/Write Definition…”
9. Set:
– Function: “03: Read Holding Registers”
– Address: 0
– Quantity: 4
– Slave ID: 1
10. Click “OK”
11. You should see the register values updating in real-time
Option 2: Using Simply Modbus (Free Tool)
1. Download Simply Modbus from [Simply Modbus](https://www.simplymodbus.ca/)
2. Open “Simply Modbus TCP Client”
3. Set IP address to “localhost”, port to 502
4. Set Slave ID to 1
5. Under “Read Holding Registers”:
– Start Address: 0
– Quantity: 4
– Poll Rate: 1000 ms
6. Click “Connect” then “Read/Write”
7. You should see the register values updating
Option 3: Using Python Client
“`python
save as test_modbus_server.py
from pymodbus.client import ModbusTcpClient
import time
Create client
client = ModbusTcpClient(host=”localhost”, port=502)
try:
# Connect to server
client.connect()
print(“Connected to Modbus server”)
# Test multiple slaves (if running multi-slave server)
slave_ids = [1, 2, 3]
for _ in range(5): # Run 5 iterations
for slave_id in slave_ids:
print(f”\n— Slave ID: {slave_id} —“)
# Read holding registers
holding_response = client.read_holding_registers(address=0, count=3, slave=slave_id)
if not holding_response.isError():
print(f”Holding Registers: {holding_response.registers}”)
# Read input registers (should be updating)
input_response = client.read_input_registers(address=0, count=3, slave=slave_id)
if not input_response.isError():
print(f”Input Registers: {input_response.registers}”)
# Read coils
coil_response = client.read_coils(address=0, count=3, slave=slave_id)
if not coil_response.isError():
print(f”Coils: {coil_response.bits[:3]}”)
time.sleep(2) # Wait 2 seconds between iterations
except Exception as e:
print(f”Error: {e}”)
finally:
client.close()
print(“\nDisconnected from Modbus server”)
“`
Run the test script:
“`bash
python test_modbus_server.py
“`
Simulating Realistic Device Behavior
To make your simulation more realistic, consider adding these features:
1. Sensor Correlation
- Simulate flow rate changing based on valve position
2. Error Simulation
- Return invalid data for specific addresses
3. Device State Management
- Simulate device startup/shutdown sequences
4. Persistent Data
- Simulate non-volatile memory
Advanced Features
Adding Error Simulation to Python Server
“`python
Add this to your Python server
import random
def error_simulation_hook(request):
“””Hook function to simulate errors”””
# 5% chance of returning an error
if random.random() < 0.05:
# Randomly choose an exception code
exception_code = random.choice([1, 2, 3, 4]) # Illegal function, illegal address, etc.
print(f”Simulating exception code {exception_code} for request: {request}”)
# Return error response
return request.createExceptionResponse(exception_code)
return request
Add this to your StartTcpServer call:
StartTcpServer(
context=context,
identity=identity,
address=(”, 502),
requestHook=error_simulation_hook # Add error simulation hook
)
“`
Conclusion
Creating a Modbus server simulation is a powerful way to test and develop Modbus client applications. Whether you prefer writing Python code or using visual tools like Node-RED, you can create realistic simulations that mimic industrial devices.
Key takeaways:
- Advanced features like error simulation help test edge cases
By creating your own Modbus server simulations, you can accelerate development, improve testing coverage, and gain a deeper understanding of Modbus communication. This skills will serve you well when working with real Modbus devices in industrial environments.
Now it’s your turn to experiment! Try extending these examples with new device types, more complex behavior, or additional slave IDs. The possibilities are endless.