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=true the 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:

ParameterValueMeaning in the example
bindAddress0.0.0.0listens on all interfaces — the master connects through any IP address of the PC as well as 127.0.0.1
port502the standard Modbus TCP port
reconnectDelayMs1000pause between attempts to open the listening port
reconnectAttempts-1keeps trying to open the port forever (port in use, address not present)
keepAliveMs5000keepalive 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:

ParameterValueMeaning in the example
modetcpModbus TCP framing (MBAP), matching the TCP Server driver
unitId2the slave's own address
respondToAnyUnittruethe 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

VariableNamespaceTypeProtocolComm paramsRole
OilTemp/ModbusushortModbus SlaveregisterType="holdingRegister";address="0";wordOrder="big";direction="read"holding register 0 read by the master
OilPressure/ModbusushortModbus SlaveregisterType="holdingRegister";address="1";wordOrder="big";direction="read"holding register 1 read by the master
OilTempView/VirtualdoubleVirtual Variables—display copy of OilTemp for the graph and the signal control
OilPressureView/VirtualdoubleVirtual 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 for OilPressureView (blue).
  • Two Signal controls (250 ms refresh) for OilTempView and OilPressureView, read-only.

What you will see and what to try

  1. 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.
  2. 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.
  3. 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 as client … disconnected. The ramp keeps going — neither the script nor the virtual variables depend on the client.
  4. 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.
  5. 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.