Modbus TCP slave on the TCP Server
You will find the project in
Examples\Qenex_TcpServer_ModbusSlave_HoldingRegisters.qproj in the
Examples folder next to QInsight.exe.
What the example shows
Here QInsight is not the master but the slave: the TCP Server
driver listens on port 502 and the Modbus Slave (RTU/TCP) protocol in
tcp mode serves two variables as holding registers 0 and 1 under unit
address 2. There is no real device — the register values are produced by
a periodic Python script that every 2 s moves a simulated oil
temperature (0–150 °C) and oil pressure (1000–2500 kPa) one step up
or down. An external Modbus TCP master connecting to QInsight reads the
saw-tooth rising and falling values.
The other half of the example is the display: a script write into a variable of a device protocol does not show in controls (no signal sample is born). The script therefore writes every value twice — into the slave variable for the master and into a virtual variable drawn by the graph and the signal controls. The project also shows how the TCP Server behaves when the client drops out: a pulled cable is detected by the keepalive, the server returns to listening and the master reconnects once the link is back.
What you need
- QInsight with Python set up — the scripts are the core of the example, see Python setup.
- A Modbus TCP master as the counterpart, for example qModMaster.
Point it at the IP address of the QInsight PC, port 502, unit 2,
and let it read holding registers from address 0, count 2.
Thanks to
respondToAnyUnit=truethe slave answers any unit address as well. - The master may run on the same PC — then it connects to
127.0.0.1. - With the master on another computer, the firewall on the QInsight PC must allow incoming connections on port 502 (local side QInsight, remote side the master). After an outage the connection is always re-established by the master (the client); the server only listens.
- Port 502 must not be taken by another program; otherwise the driver keeps retrying to open it (see Reconnection) and comes up as soon as the port is free.
The project
Drivers and protocols
The project has two driver + protocol pairs.
TCP Server (TcpServerDriver) with the settings
bindAddress=0.0.0.0;port=502;reconnectDelayMs=1000;reconnectAttempts=-1;keepAliveMs=5000:
| Parameter | Value | Meaning in the example |
|---|---|---|
bindAddress | 0.0.0.0 | listens on all interfaces — the master connects through any IP address of the PC as well as 127.0.0.1 |
port | 502 | the standard Modbus TCP port |
reconnectDelayMs | 1000 | pause between attempts to open the listening port |
reconnectAttempts | -1 | keeps trying to open the port forever (port in use, address not present) |
keepAliveMs | 5000 | keepalive on the accepted connection: probes after 5 s of silence, a dead client is detected within seconds |
Under it Modbus Slave (RTU/TCP) (ModbusSlaveProtocol) with the
settings mode=tcp;unitId=2;respondToAnyUnit=true:
| Parameter | Value | Meaning in the example |
|---|---|---|
mode | tcp | Modbus TCP framing (MBAP), matching the TCP Server driver |
unitId | 2 | the slave's own address |
respondToAnyUnit | true | the slave answers regardless of the unit address in the request — set explicitly, because the pre-filled template contains false |
The second pair, Virtual Variables Host (VirtualDataDriver) +
Virtual Variables (VirtualDataProtocol), has no settings; it carries
the display copies of both registers.
Variables
| Variable | Namespace | Type | Protocol | Comm params | Role |
|---|---|---|---|---|---|
OilTemp | /Modbus | ushort | Modbus Slave | registerType="holdingRegister";address="0";wordOrder="big";direction="read" | holding register 0 read by the master |
OilPressure | /Modbus | ushort | Modbus Slave | registerType="holdingRegister";address="1";wordOrder="big";direction="read" | holding register 1 read by the master |
OilTempView | /Virtual | double | Virtual Variables | — | display copy of OilTemp for the graph and the signal control |
OilPressureView | /Virtual | double | Virtual Variables | — | display copy of OilPressure for the graph and the signal control |
The slave variables are ushort — a single 16-bit register, into which
both ranges 0–150 and 1000–2500 fit without scaling. The presentations
Temperature (0–150, °C) and Pressure (1000–2500, kPa) both use the
linear conversion Raw with multiplier=1, offset=0 and the format
{0:F0}; the engineering value equals the raw one, so the master reads
degrees and kilopascals directly. The virtual copies share the same
presentations, so the graph and the signal controls show the same units
and ranges.
None of the slave variables is bound to a control — since a script write on a device protocol raises no UI event, it would show nothing there.
Scripts
The project has two scripts, both for runtime only (not for replay), blocking, without a timeout.
init.py in Startup mode sets the initial state of the simulation and
writes the starting values right away. The Python globals oil_temp,
oil_pressure and direction live in the
shared script scope,
so the periodic script finds them:
# Startup script: initial state of the simulated sensors.
# The periodic script (simulate.py) ramps the values from here; the globals persist
# between script runs inside one runtime session.
oil_temp = 0.0 # degC, ramps 0 .. 150
oil_pressure = 1000.0 # kPa, ramps 1000 .. 2500
direction = 1 # +1 ramping up, -1 ramping down
# Publish the initial values right away, both to the slave registers and to the display copies.
OilTemp.EngValue = oil_temp
OilPressure.EngValue = oil_pressure
OilTempView.EngValue = oil_temp
OilPressureView.EngValue = oil_pressure
simulate.py in Periodic mode with the parameters period=2;unit=s
moves the temperature by 1 °C and the pressure by 10 kPa every 2 s; at
the upper or lower bound it clamps both quantities to the end value and
flips the direction. Both ranges have 150 steps, so one sweep up takes
5 minutes:
# Periodic script (period=2;unit=s): saw-tooth ramp of the simulated sensors.
# Temperature moves 1 degC per step between 0 and 150, pressure 10 kPa per step between
# 1000 and 2500, so one sweep up takes 5 minutes; at either end the direction flips.
#
# Every value is written twice on purpose:
# - OilTemp / OilPressure are the Modbus slave variables. The slave serves their current
# value to the master (holding registers 0 and 1). A script write never raises a UI event
# on a device protocol, so these two are not shown in any control.
# - OilTempView / OilPressureView are virtual variables. The Virtual Data protocol publishes
# every script write to the UI, so the graph and signal controls follow them.
oil_temp += direction * 1.0
oil_pressure += direction * 10.0
if oil_temp >= 150.0 or oil_pressure >= 2500.0:
oil_temp, oil_pressure, direction = 150.0, 2500.0, -1
elif oil_temp <= 0.0 or oil_pressure <= 1000.0:
oil_temp, oil_pressure, direction = 0.0, 1000.0, 1
OilTemp.EngValue = oil_temp
OilPressure.EngValue = oil_pressure
OilTempView.EngValue = oil_temp
OilPressureView.EngValue = oil_pressure
The double write is deliberate and is the key to the whole example.
The write into OilTemp and OilPressure only stores the value that the
Modbus Slave hands to the master on its next request — a device protocol
produces no UI sample from such a write. The write into OilTempView and
OilPressureView, on the other hand, is published by the Virtual
Variables protocol as a sample timestamped at the moment of the write,
and that is what the graph and the signal controls see. If the script
wrote only the slave variables, the master would read correct data but
the workspace would stay empty; if it wrote only the virtual ones,
everything would be displayed but the master would read zeros.
Workspace
The Simulated sensors workspace holds three controls, all bound to the virtual copies:
- A Graph "Simulated sensors (virtual copies of the slave registers)"
with a 300 s time window (exactly one ramp sweep) and two vertical axes
with fixed ranges: left 0–150 for
OilTempView(red), right 1000–2500 forOilPressureView(blue). - Two Signal controls (250 ms refresh) for
OilTempViewandOilPressureView, read-only.
What you will see and what to try
- Start the runtime. The graph starts drawing two saw-tooth curves: the temperature from 0 °C upwards, the pressure from 1000 kPa upwards; after 5 minutes both turn around.
- Connect the master to port 502, unit 2, and read holding registers 0 and 1. The master shows the same values as the signal controls and every 2 s they move by 1 and by 10. The slave only serves the values; between requests the master sends nothing and the driver does not treat the silence as a fault.
- Pull the master's cable (or disconnect its network). After 5 s of
silence the keepalive starts probing and a few seconds later the
Logs window shows the warning
client … lost. The server returns to listening and the driver stays running; a clean disconnect of the master would be logged asclient … disconnected. The ramp keeps going — neither the script nor the virtual variables depend on the client. - Plug the cable back in. The master reconnects on its own and reads the values that kept climbing in the meantime; nothing needs to be done on the QInsight side.
- Connect a second master while the first one still holds its connection: the new incoming connection replaces the current one — the TCP Server serves a single client. A dead master is thus replaced by a new one without waiting for the keepalive.
Related chapters
- TCP Server and
Reconnection — driver parameters,
keepalive, the
client … lostandclient … disconnectedmessages. - Modbus Slave (RTU/TCP) — slave settings, register parameters and behaviour on writes from the master.
- Virtual Variables — why the display goes through virtual copies.
- Accessing variables — writing
through
EngValueand the shared script scope. - Scripts overview — the Startup and
Periodic modes, the
periodparameter. - Watching live data — the Logs and Script-Logs windows.
- Graphs and Signal control.

