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=COM6 with your port. Keep the framing and unitId=2, or change them identically on both sides. The scripts need a configured Python.

The project

Drivers and protocols

The Serial Port Driver (SerialPortDriver):

ParameterValueMeaning
portCOM6COM port name — replace with the one on your PC
baudRate9600communication speed
dataBits8number of data bits
paritynoneno parity
stopBits1one stop bit
handshakenoneno flow control
reconnectDelayMs2000pause before the next attempt to open the port after a failure
reconnectAttempts-1attempts without limit, see Reconnection

Under it the Modbus Slave (RTU/TCP) protocol (ModbusSlaveProtocol):

ParameterValueMeaning
modertuRTU framing (CRC16, serial line)
unitId2the slave's own address — the master must use it in its requests
respondToAnyUnitfalserequests 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

VariableNamespaceTypeProtocolRegister / Comm paramsPresentation
OilTemp/ModbusushortModbus Slaveholding register 0, wordOrder="big", direction="read"Temperature
OilPressure/ModbusushortModbus Slaveholding register 1, wordOrder="big", direction="read"Pressure
OilTempView/VirtualdoubleVirtual Variables— (virtual, no parameters)Temperature
OilPressureView/VirtualdoubleVirtual 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 in simulate.py — neither the master nor the workspace needs any change.