HEART Embedded Systems logoHEART

Water pump station simulator: a HEART-embedded system you can run and watch#

A demonstration only. It simulates a town, a river intake, a pump station, its PLC and a HEART. It is not a control system, models no real station or utility, and must never be connected to real plant.

One Python file, pumpStationSimulator.py, with only the standard library. Run it, register it on HEART Hub with the code it prints, and watch its health on your own private page while it lives a simulated life: the town's demand rising for breakfast and the evening, two pumps taking turns, a level sensor that sticks, a lost telemetry link, cavitation, a motor running hot, a PLC trip that stops both pumps, and a mains failure.

The lesson of this one: HEART changes nothing. The PLC runs the station and holds its interlocks (dry-run protection, the high-pressure trip, the motor thermal trip). HEART reads the PLC's values, notices, explains and tells a person. Every change request is refused with 403 capability-not-allowed.

The case study behind it, step by step for a real station, is guides/Water-Pump-Station.md.

On a real device: Water Pump Station, for real shows the hardware we assume, where the HEART runs, the code to change, how to build and install it, the factory steps and what the owner sees. For illustration only.

FileWhat it is
pumpStationSimulator.pyThe simulator: town and river, station and PLC, HEART, local HEART API, HEART Hub reporting
test_e2e.pyEnd-to-end test against a monitor site on your own computer
README.mdThis file

1. Download and run#

You need Python 3.8 or newer. Nothing to install.

python3 pumpStationSimulator.py --scenario all

It starts at once and prints three things:

  HEART Hub: register this simulated pump station
    1. Sign in at https://heartcortex.ai/login with your email (a 6-digit code is sent to you).
    2. Open https://heartcortex.ai/register?id=5be1c07a93d2f418
    3. Enter the claim code  R4WQ8-KM2TZ-7HXCB-9NPDA
    4. Watch it at https://heartcortex.ai/device/5be1c07a93d2f418  (private to you until you share it)

10:44:22 HEART API on http://127.0.0.1:43305/heart/v1/pulse  (owner key for private views: ...)
10:44:37 sump 3.00 m · reservoir 3.33 m · P1 RUN P2 off (duty P1) · flow  305 m3/h · town  139 m3/h · 4.47 bar · motor   69 Cel · vib  2.5 mm/s · mains on · link up · HEART healthy

Every 15 seconds it prints one line of what the PLC reports. When something goes wrong, its HEART says so:

10:58:12 HEART critical system.main-function-lost: The station cannot pump water. The PLC has tripped the station on
         high discharge pressure and locked both pumps out. The town is drawing 144 m3/h; the reservoir is at 3.39 m,
         about 24.9 h of supply above its low level. Send the on-call operator.
10:59:36 HEART resolved   system.main-function-lost: the station cannot pump water

Stop it with Ctrl-C. The device ID, claim code and key are kept in pump-station-monitor.json (readable only by you), so the same simulated station comes back next time. Delete that file to make a new device.

2. Register it and watch it on HEART Hub#

  1. Sign in. Open https://heartcortex.ai/login and enter your email address. A 6-digit code arrives; type it in. There are no passwords.
  2. Register. Open the register?id=... link the simulator printed, and enter the claim code. The claim code is the proof that you hold the device: it is shown only on the simulator's console, and the site holds only its hash.
  3. Watch. Your device's page, https://heartcortex.ai/device/<ID>, shows its status (healthy, degraded or failing) and its open and recent alerts, each with the plain-language meaning of its fault code. All your devices are at https://heartcortex.ai/devices.
  4. Share, if you want. The page is private until you make a share link or make it public.
  5. Silence. Stop the simulator, or wait for the staged telemetry-link loss: reports go out through the station's router, so while the link is down nothing is sent, and after three missed Pulse intervals (90 seconds) the page shows a Flatline and you get one email. The site checks for silent devices every 5 minutes, so the email can take a few minutes more. When it reports again, you get one more.

Until you register it, the site stores nothing: the simulator's reports are answered "not registered yet" and dropped. After registering, it can take up to five minutes for the first report to arrive, or less as soon as something changes.

What the site sees. Only the public Pulse and public alerts, and it keeps only the status and timing and each alert's fault code, severity and times. Never a level, a flow, a pressure, a motor reading or a PLC event. A real water utility should run its own monitor site rather than send a station's alerts to a public one: see the warning in the case study, Step 8.

3. The staged faults#

With --scenario, each fault starts at a set time, lasts a while, and is then "fixed by the maintenance crew", so you can watch an alert open, escalate and resolve. Times are in simulated seconds: at --speed 1 they are real seconds.

ScenarioStartsLastsWhat happens in the stationWhat HEART raises
stuck1 min3 minThe sump level transmitter freezes at one valuesensors.stuck (warning) after 90 s with not one change. The PLC's dry-run protection is not affected: it uses its own float switch
link4.5 min5 minThe telemetry router to the control room drops outconnectivity.link-lost (warning). No reports while it is down: the page shows a Flatline, then recovers. The pumps carry on
cavitation10 min2.5 minThe suction strainer blocks: flow falls to about 60 %, vibration rises to 8 to 11 mm/ssystem.main-function-degraded (warning)
hot13 min4 minPump 1's cooling fan is blocked. Its winding heats; the PLC trips it at 120 Cel and starts pump 2performance.too-hot (warning) from 100 Cel, before the PLC's trip
trip17.5 min2 minA valve on the rising main is shut by mistake. Pressure rises to shut-off head; the PLC trips the station on high pressure and locks both pumps out until a local reset. The town keeps drawing watersystem.main-function-lost (critical), with the hours of supply left in the reservoir
power20 min2 minMains supply is lost. The PLC and the gateway run on the UPS; the PLC restarts a pump 20 s after mains returnspower.supply-lost (error) and system.main-function-lost (critical)
allAll of the above

Without --scenario, faults still happen now and then on random timers, as in life, but rarely: a telemetry dropout every 3 to 8 hours, a grid fault every 12 to 48 hours.

4. Options#

OptionDefaultMeaning
--scenariononestuck, link, cavitation, hot, trip, power (comma-separated) or all
--speed1Simulated seconds per real second. --speed 5 runs the staged faults in about 4.5 minutes (too fast for Flatline emails, which need real minutes)
--monitor-urlhttps://heartcortex.aiAny HEART Hub site, including your own. http only for 127.0.0.1
--no-monitorReport to no site: local only
--state-filepump-station-monitor.jsonWhere the device ID, claim code and key are kept
--port43305The local HEART API (falls back to 7685; 0 for none)
--seedrandomRepeat the same simulated life
--durationuntil Ctrl-CStop after this many real seconds

5. The local HEART API#

The simulator also serves the HEART API on your own computer, as a real gateway would:

curl http://127.0.0.1:43305/.well-known/heart-card.json         # the Card: what it is
curl http://127.0.0.1:43305/heart/v1/pulse                       # public health
curl http://127.0.0.1:43305/heart/v1/arrhythmias                 # public alerts: codes only
curl http://127.0.0.1:43305/heart/v1/properties                  # public values: none, see the table below
KEY=...   # the owner key it printed
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43305/heart/v1/properties    # every reading from the PLC
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43305/heart/v1/arrhythmias   # alerts with explanations
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:43305/heart/v1/plc-events    # the PLC's event log: starts, stops, trips, resets
curl -X PUT -H "Authorization: Bearer $KEY" http://127.0.0.1:43305/heart/v1/properties   # 403 capability-not-allowed

While the level sensor is stuck, the owner's sump_level property carries "quality": "bad".

What HEART reads, and how private it is. The HEART reads these from the PLC's Modbus map. Addresses are the 0-based protocol addresses within each table, as sent on the wire (Signal Tables, modbus-tcp; never old reference numbers such as 30001). In the simulator they are read from the simulation; on a real gateway, over Modbus TCP.

SignalTableRegisterScale, unitPrivacy
Health status, open fault codes(derived by HEART)public
Sump level (transmitter LT-101)input00.001 minternal
Reservoir levelinput10.001 minternal
Station flowinput20.1 m3/hinternal
Discharge pressureinput30.01 barinternal
Pump 1, pump 2 motor currentinput4, 50.1 Ainternal
Pump 1, pump 2 motor winding temperatureinput6, 70.1 Celinternal
Pump 1, pump 2 vibration (RMS velocity)input8, 90.01 mm/sinternal
Pump 1 run hours (32 bits, high word first)input10, 110.1 hinternal
Pump 2 run hours (32 bits, high word first)input12, 130.1 hinternal
Mains voltageinput140.1 Vinternal
PLC status word: bit 0, 1 pump thermal trip; 2 high-pressure trip; 3 dry-run stop; 4 mains lost; 5 link down; 6 duty is pump 2; 7 PLC calls for waterinput15bitsinternal
Outflow to the towninput160.1 m3/hinternal
UPS chargeinput171 %internal
Pump 1, pump 2 runningdiscrete0, 11 = runninginternal
Low-level float switch (the PLC's dry-run input)discrete21 = lowinternal
UPS on batterydiscrete31 = on batteryinternal
Telemetry link healthydiscrete41 = upinternal
PLC event log(the PLC's own log)internal
Anything about peoplenonenot collected

Every value is internal: the operator's, not the public's. Nothing is private, because nothing is personal: the station knows no customer, no household and no operator name. For critical infrastructure, public stays as small as the standard allows: status and fault codes only.

HEART reads only the two read-only tables (input, function 4; discrete, function 2). It has no write function at all, and the PLC's setpoints (holding registers) are not in its map.

Every message it serves is a valid HEART message: test_e2e.py checks each one against the official schemas.

6. How the code is built#

World  ──►  Station (plant + PLC)  ──(Modbus map, read only)──►  Heart  ──►  local HEART API   (127.0.0.1)
river, town   pumps, interlocks, duty                             watches     └►  HEART Hub  (outbound only)
PartIn the fileIn a real station
WorldThe river head feeding the sump, and the town's daily demand curve, each on its own random timerThe real river and town
StationThe sump, two pumps, the rising main and the reservoir, and the PLC's scan: dry-run stop from the float switch, high-pressure trip with local reset, motor thermal trips, start and stop on reservoir level, duty and standby with rotation, restart after mains returns. Writes its Modbus map each scanThe plant, the PLC and its program, and any safety relays. Never part of HEART
ReadingsOne poll of the PLC's map with read_input_registers() and read_discrete_inputs(), scaled to unitsThe code you add: a Modbus TCP client, read only
HeartRULES turn readings into alerts with fault codes; EXPLAIN says what it means and what to do; escalate() tells a person; serves only what each caller may seeThe code you add, on the edge gateway
monitor()Hello, then reports to HEART Hub; follows the site's nextSec; stops for a wrong key; says hello again if forgotten; sends nothing while the telemetry link is downThe same, unchanged
handler()The HEART API: Card, Pulse, alerts, properties, PLC events; every change refusedThe same, behind TLS on the station network

Nothing in Heart can write to Station: there is no setter, no command, no capability and no Modbus write. That is the design, not an omission. HEART's own thresholds (vibration 7.1 mm/s, motor 100 Cel, the expected flow) sit below the PLC's trips, so a person hears before the PLC acts, and the PLC acts whether or not HEART is there.

7. What code to add to make a real product HEART-embedded#

For a real station, HEART runs on an edge gateway in the station's control panel, next to the PLC, on the station's own network: see Step 1 of the case study. Never in the PLC, never in the safety relays. Pick a starting point, then add four things.

Starting points

Your gatewayStart from
An industrial Linux gateway (most stations)The Rust Core heartd (heart-core-rs) with its Modbus TCP adapter. heart-core-rs/examples/pump-station is a complete configuration with a Modbus pump simulator: for a monitoring-only station, keep its read-only property definitions (custom/tank-level.json is the pattern) and leave out its command (custom/set-flow.json). Draft the property definitions from the PLC's register map with heart-templates/tools/heart_signals.py draft --modbus regs.csv and export
A small Linux box or a SCADA server you already runThis file's Readings, Heart, monitor() and handler(); or the Python template heart-templates/templates/python; or the JavaScript template heart-templates/templates/javascript-node
A microcontroller gateway with Ethernet or Wi-FiThe ESP32 Full starter heart-templates/templates/esp32-full (its signals.json, main-job check and Monitor reporting)

A gap to know about. heartd's Modbus TCP adapter today reads with function 3 (holding registers) and writes with function 6. This map uses the read-only tables (function 4, input registers; function 2, discrete inputs). Either have the PLC mirror these values into a block of holding registers for the gateway, or extend the adapter with functions 2 and 4. Either way, give the gateway no write path: configure the PLC's Modbus server, or a firewall in front of it, to accept only read functions from the gateway's address.

The four things you add

  1. Read the PLC. Replace World and Station with a Modbus TCP client for your PLC's real map. It must be read only: HEART gets values, never a way to write a register or a coil.

``python class Readings: """CHANGE: fill these from your PLC's register map. Read only: functions 2 and 4, nothing else.""" def __init__(self, plc, heart): ir = plc.read_input_registers(0, 18) # your Modbus TCP client, function 4, the PLC's unit ID di = plc.read_discrete_inputs(0, 5) # function 2 self.sump, self.flow, self.pressure = ir[0] / 1000, ir[2] / 10, ir[3] / 100 self.running = [bool(di[0]), bool(di[1])] ... ``

If the PLC stops answering, that is a fault too: raise connectivity.dependency-unreachable, and never guess the values.

  1. Your rules and thresholds. Edit RULES and EXPLAIN with your pumps' commissioning curves, the vibration limits for your machines (ISO 10816 or ISO 20816), and motor temperatures set below the PLC's trips. Keep the standard fault codes (Part 3), so every SCADA system and monitor site reads them the same way.
  2. Your escalation route. Change escalate() to raise an alarm in the control room's SCADA and page the on-call operator, and to light the station's local alarm lamp, which needs no network. When the telemetry link is down, the lamp is the only route: say so to the crew in the commissioning notes.
  3. Your privacy table. Go through properties() and give every value its level (Step 3 of the case study). Operational data is internal; only status and fault codes are public. Keep operator names, call-out rosters and customer data off the gateway.

Then keep monitor() as it is, store the device ID, claim code and key in the gateway's protected storage, and print the claim code and a QR code of https://<your monitor site>/register?id=<ID> on a label inside the panel door. Serve the API over TLS on the station network (or keep it on loopback), run the HEART-Rating client and Part 9's tests, and claim HEART Basic, Autonomy Level A1.

8. Test it#

From the project's top folder, with Node 20+ and Python 3 with pip install --user "jsonschema[format]>=4.18" referencing:

python3 pumpStationSimulator/test_e2e.py

It starts a HEART Hub site on this computer (port 8805), runs the simulator with every staged fault at 5x speed (local API on port 43305), signs in, registers the device through the site's own form, and checks for about five minutes that every message is valid, public views hold only public data, measurements and the PLC event log need the owner key, every change request is refused with 403 capability-not-allowed even with the owner key, every staged fault reaches the owner's page with its fault code, no measurement reaches the site, and the page stays private.


Licence: Apache-2.0. Part of HEART, the open standard at heartcortex.ai.