Modbus RTU slave with simulated sensors
You will find the project Qenex_Serial_ModbusSlave_SimulatedSensors.qproj
in the Examples folder next to QInsight.exe.
What the example shows
- QInsight as a Modbus RTU slave (unit 2) on a serial port — the Serial Port (COM) driver + the Modbus Slave protocol. A master reads the live values of two variables from holding registers 0 and 1.
- Simulated sensors from a script: a periodic Python script (2 s) ramps an oil temperature 0–150 °C and an oil pressure 1000–2500 kPa along a saw-tooth.
- A double write of every value: into the slave variable (for the master) and into its virtual copy (for the controls), because a script write never raises a UI event on a device protocol.
- Reconnection of the Serial Port driver after the converter is unplugged.
What you need
- A Modbus RTU master — e.g. qModMaster: unit 2, holding registers from address 0, count 2 (FC03), RTU, 9600 Bd, 8 bits, no parity, 1 stop bit.
- A serial path to the master: a pair of linked COM ports on one PC (a virtual null-modem pair), or two USB/RS-485 converters — one for QInsight, one for the master.
- What to adapt: in the Serial Port driver replace
port=COM6with your port. Keep the framing andunitId=2, or change them identically on both sides. The scripts need a configured Python.
The project
Drivers and protocols
The Serial Port Driver (SerialPortDriver):
| Parameter | Value | Meaning |
|---|---|---|
port | COM6 | COM port name — replace with the one on your PC |
baudRate | 9600 | communication speed |
dataBits | 8 | number of data bits |
parity | none | no parity |
stopBits | 1 | one stop bit |
handshake | none | no flow control |
reconnectDelayMs | 2000 | pause before the next attempt to open the port after a failure |
reconnectAttempts | -1 | attempts without limit, see Reconnection |
Under it the Modbus Slave (RTU/TCP) protocol (ModbusSlaveProtocol):
| Parameter | Value | Meaning |
|---|---|---|
mode | rtu | RTU framing (CRC16, serial line) |
unitId | 2 | the slave's own address — the master must use it in its requests |
respondToAnyUnit | false | requests for any other unit address are ignored |
The second pair, Virtual Data Driver (VirtualDataDriver, shown as
Virtual Variables Host in the UI) + Virtual Variables
(VirtualDataProtocol), has no settings and carries the two display copies.
The project has no events: the slave needs no eventRef and the virtual
variables get their samples from script writes.
Variables
| Variable | Namespace | Type | Protocol | Register / Comm params | Presentation |
|---|---|---|---|---|---|
OilTemp | /Modbus | ushort | Modbus Slave | holding register 0, wordOrder="big", direction="read" | Temperature |
OilPressure | /Modbus | ushort | Modbus Slave | holding register 1, wordOrder="big", direction="read" | Pressure |
OilTempView | /Virtual | double | Virtual Variables | — (virtual, no parameters) | Temperature |
OilPressureView | /Virtual | double | Virtual Variables | — (virtual, no parameters) | Pressure |
The slave variables are ushort, so each occupies one register, and the
master reads them (direction="read"). The presentations Temperature
(0–150 °C) and Pressure (1000–2500 kPa), both with the format {0:F0}, use
the single linear conversion Raw (multiplier=1, offset=0) — the
engineering value equals the raw one.
Scripts
Both scripts are blocking, without a timeout and for runtime only. init.py
(Startup) sets the initial state of the simulation and publishes it right
away, into the slave registers and into the display copies:
# 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 (Periodic, period=2;unit=s) moves both quantities one
step every 2 s and flips the direction at the ends of the range:
# 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 simulation state lives in plain Python variables — the scripts of a
project share one scope, so the values
survive between runs and init.py resets them on runtime start. A step of
1 °C and 10 kPa every 2 s gives 150 steps per sweep, i.e. 5 minutes up and
5 down.
Why the double write. OilTemp and OilPressure belong to the Modbus
Slave protocol: a script write stores their value and the slave serves it to
the master, but on a device protocol a script write
never raises a UI event
— in the controls these variables would not move. That is why the script
writes the same value into OilTempView and OilPressureView on the
Virtual Variables protocol, which
publishes every write as a timestamped sample. Writing through EngValue
equals raw with the 1:1 conversion; for ushort the value is converted to an
integer, which the steps of 1.0 and 10.0 satisfy.
Workspace
The Simulated sensors workspace has three controls, all bound to the
virtual copies, not to the slave variables: a
Graph "Simulated sensors (virtual copies of
the slave registers)" with a 300 s window (OilTempView in red on the left
axis 0–150, OilPressureView in blue on the right axis 1000–2500) and two
Single-Signal controls (250 ms refresh) with
the current temperature and pressure.
What you will see and what to try
- After Start of the runtime the graph draws the saw-tooth (the 300 s window = one sweep up) and the Single-Signal controls show the current values. The master reads the temperature from register 0 and the pressure from register 1 — the same values, changing by 1 °C and 10 kPa every 2 s.
- A request for any unit address other than 2 gets no reply
(
respondToAnyUnit=false); a read touching register 2 and above is rejected by the slave as a whole. - Unplug and replug the converter. The Serial Port driver goes Faulted,
warnings with the attempt number appear in the log and the port is retried
every 2 s (
reconnectDelayMs=2000) without limit (reconnectAttempts=-1); the script and the graph keep running. Once replugged, the driver reopens the port by itself and the master reads the live values again without any operator action. - Shorten the script period (
period=500;unit=ms) or change the step insimulate.py— neither the master nor the workspace needs any change.
Related chapters
- Serial Port (COM) and Reconnection — driver parameters.
- Modbus Slave (RTU/TCP) — protocol settings and per-variable parameters.
- Virtual Variables — samples born from script writes.
- Accessing variables and Scripts overview.
- Modbus RTU master over a serial line — the opposite role on the same line; Modbus TCP slave on the TCP Server — the same slave over TCP.

