Skip to content

Commit 589a4eb

Browse files
committed
contrib example: TCP drainage simulator with two devices
Simulates two Modbus TCP slave servers: Digital IO (DIO) with 8 discrete inputs and 8 coils. The first two coils each control a simulated pump. Inputs are not used. Water level meter (WLM) returning the current water level in the input register. It increases chronologically and decreases rapidly when one or two pumps are active.
1 parent a7ca62a commit 589a4eb

File tree

1 file changed

+70
-0
lines changed

1 file changed

+70
-0
lines changed

examples/contrib/drainage_sim.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env python3
2+
3+
# Simulates two Modbus TCP slave servers:
4+
#
5+
# Port 5020: Digital IO (DIO) with 8 discrete inputs and 8 coils. The first two coils each control
6+
# a simulated pump. Inputs are not used.
7+
#
8+
# Port 5021: Water level meter (WLM) returning the current water level in the input register. It
9+
# increases chronologically and decreases rapidly when one or two pumps are active.
10+
11+
import asyncio
12+
import logging
13+
from datetime import datetime
14+
15+
from pymodbus import __version__ as pymodbus_version
16+
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
17+
from pymodbus.device import ModbusDeviceIdentification
18+
from pymodbus.server import StartAsyncTcpServer
19+
20+
INITIAL_WATER_LEVEL = 300
21+
WATER_INFLOW = 1
22+
PUMP_OUTFLOW = 8
23+
24+
logging.basicConfig(level = logging.INFO)
25+
26+
dio_di = ModbusSequentialDataBlock(1, [False] * 8)
27+
dio_co = ModbusSequentialDataBlock(1, [False] * 8)
28+
dio_context = ModbusSlaveContext(di = dio_di, co = dio_co)
29+
wlm_ir = ModbusSequentialDataBlock(1, [INITIAL_WATER_LEVEL])
30+
wlm_context = ModbusSlaveContext(ir = wlm_ir)
31+
32+
async def update():
33+
while True:
34+
await asyncio.sleep(1)
35+
36+
# Update water level based on DIO output values (simulating pumps)
37+
water_level = wlm_ir.getValues(1, 1)[0]
38+
dio_outputs = dio_co.getValues(1, 2)
39+
40+
water_level += WATER_INFLOW
41+
water_level -= (int(dio_outputs[0]) + int(dio_outputs[1])) * PUMP_OUTFLOW
42+
water_level = max(0, min(INITIAL_WATER_LEVEL * 10, water_level))
43+
wlm_ir.setValues(1, [water_level])
44+
45+
async def log():
46+
while True:
47+
await asyncio.sleep(10)
48+
49+
dio_outputs = dio_co.getValues(1, 8)
50+
wlm_level = wlm_ir.getValues(1, 1)[0]
51+
52+
logging.info(f"{datetime.now()}: WLM water level: {wlm_level}, DIO outputs: {dio_outputs}")
53+
54+
async def run():
55+
ctx = ModbusServerContext(slaves = dio_context)
56+
dio_server = asyncio.create_task(StartAsyncTcpServer(context = ctx, address = ("0.0.0.0", 5020)))
57+
logging.info("Initialising slave server DIO on port 5020")
58+
59+
ctx = ModbusServerContext(slaves = wlm_context)
60+
wlm_server = asyncio.create_task(StartAsyncTcpServer(context = ctx, address = ("0.0.0.0", 5021)))
61+
logging.info("Initialising slave server WLM on port 5021")
62+
63+
update_task = asyncio.create_task(update())
64+
logging_task = asyncio.create_task(log())
65+
66+
logging.info("Init complete")
67+
await asyncio.gather(dio_server, wlm_server, update_task, logging_task)
68+
69+
if __name__ == "__main__":
70+
asyncio.run(run(), debug=True)

0 commit comments

Comments
 (0)