# Guide 7: A HEART-Embedded water pump station

**A case study.** A water utility runs pump stations that lift water from a river intake to a reservoir on the hill, which feeds a town. Each station has two pumps and a PLC that runs them. The utility wants every station to be a HEART-embedded system: it should know when it is unwell, say so in one standard language, explain what is wrong, tell the on-call operator, and never leak anything it should not. This guide goes step by step from day 1 to a station that can claim **HEART Basic, Autonomy Level A1**, and shows how an owner registers it and watches it on HEART Hub.

A working demo comes with it: [`pumpStationSimulator/`](../pumpStationSimulator/README.md), one Python file that simulates a town, a river, a station, its PLC and its HEART, and reports to `https://heartcortex.ai`.

> **Status.** A design case study with a simulator. No real station, PLC or utility was involved. The town, the river, the pumps, the PLC and its register map are all simulated; the numbers are plausible, not engineering data. A real station's controls and safety functions are designed and certified under their own standards.

---

## Before you start: the PLC runs the station, HEART watches it

A pump station's job, keeping the reservoir supplied by starting and stopping pumps, and protecting them when something goes wrong, belongs to the **PLC** and its interlocks. They are in the control loop and in the safety chain. HEART stays out of both.

| Stays in the PLC, its program and any safety relays | Goes to HEART |
|---|---|
| Starting and stopping pumps on reservoir level | Knowing whether the station can actually pump |
| Duty and standby, duty rotation, restart after a power cut | Watching levels, flow, pressure, motors and power |
| Dry-run protection from the low-level float switch | Noticing a sensor that has stopped telling the truth |
| The high discharge pressure trip, with a local reset | Raising alerts with standard fault codes |
| Motor thermal trips | Explaining what is wrong and what to check |
| Every setpoint, which only the utility's engineers change | Telling the on-call operator, even with no internet |

This is Part 6, Section 9 of the standard, and its table "Where HEART sits in a control system", applied to a station:

| Layer | In a pump station | Run by | HEART's role |
|---|---|---|---|
| Safety (trips, interlocks, safety PLC) | Dry-run stop, high-pressure trip, thermal trips, emergency stop | The PLC and hardwired relays | **None.** Read-only at most |
| Real-time control | Start and stop on level, duty and standby, PLC scan | The PLC | **None in the loop.** It may read values |
| Supervision | Start and stop levels, duty rotation period | Operators and engineers | Advises only, in this guide |
| Maintenance and diagnosis | Cavitation, hot motors, stuck sensors, run hours | Engineers | **Its main value**: explains, predicts, gathers evidence |

**This HEART has no commands at all.** Every `PUT`, `POST`, `PATCH` or `DELETE` is refused with `403 capability-not-allowed`, even with the owner key. It cannot start a pump, reset a trip or change a setpoint. If HEART crashes, is removed or is hacked, the PLC goes on running the station exactly as before (Part 6, Section 9, rules 1, 3 and 6).

---

## Step 1: Decide where HEART runs

A PLC is certified, deterministic and busy. It must not carry a HEART, and its program should not change for one. The usual station has room beside it:

```
sensors, pumps ──(wired I/O)──► PLC ──(Modbus TCP, read only)──► edge gateway ──(router)──► control room and HEART Hub
                                 runs the station                 HEART runs here
```

| Part | What it does | HEART's place |
|---|---|---|
| **Sensors, float switch, pumps, motors** | Measure and move water | None |
| **PLC** | Runs the station and its interlocks; holds values in a Modbus register map | None. Read only, through the Modbus map it already has |
| **Edge gateway** (an industrial Linux box in the panel, next to the PLC) | Polls the PLC, serves the HEART API on the station network | **Runs the HEART**: for example `heartd` from `heart-core-rs`, with its Modbus TCP adapter (`heart-core-rs/examples/pump-station` is a complete configuration), class Core-M |
| **Router** | The telemetry link to the control room | Carries HEART's reports and escalations |
| **Control room SCADA** | Operators watch every station | Reads the HEART API; receives escalations |

So the HEART-embedded system is the **station as seen by its gateway**. The gateway adds no new path into the PLC: it uses only the read functions of the Modbus map (input registers and discrete inputs), and the PLC's Modbus server, or a firewall in front of it, should accept nothing else from the gateway's address.

**What a real station needs that the simulator does not have.** The simulator reads the map straight from the simulation. A real gateway needs a Modbus TCP client. `heartd`'s adapter today reads holding registers (function 3) and writes one register (function 6); for a read-only map in input registers and discrete inputs, either have the PLC mirror the values into a block of holding registers, or add functions 2 and 4 to the adapter. Leave out `heartd`'s command definitions (`custom/set-flow.json` in the example) for a monitoring-only station.

---

## Step 2: Give the system an identity

The gateway's HEART serves a Card at `/.well-known/heart-card.json`:

```json
{ "system": { "id": "org.example.water.pump-station", "name": "Hill Road pump station", "version": "1.0.0" },
  "level": "basic", "bindings": ["http-server"], "pulse": { "intervalSec": 30 },
  "modules": ["properties"], "shares": { "pulse": "public", "arrhythmia": "public", "property": "internal" },
  "autonomy": { "level": 1 }, "template": "heart/embeddedstandard/python/v1.2611.2" }
```

The simulator's system ID is `org.heartaispec.demo.pump-station`. The device ID used by HEART Hub (Step 8) is random: it does not reveal the station's name or place.

---

## Step 3: List what the system knows, and how private each item is

Nothing in a pump station is personal, but almost all of it is operational: where the station is weak, when it is down and how long the town can last are things an attacker would like to know. Decide every value's level **before** writing code.

| Value | Source | Privacy | Why |
|---|---|---|---|
| Healthy or not; open fault codes | Derived by HEART | `public` | The only things HEART Hub ever receives (Step 8) |
| Sump and reservoir levels | PLC (input registers 0, 1) | `internal` | For the operators |
| Station flow, outflow to the town | PLC (input registers 2, 16) | `internal` | Reveals the town's demand pattern |
| Discharge pressure | PLC (input register 3) | `internal` | |
| Motor current, winding temperature, vibration, run hours per pump | PLC (input registers 4 to 13) | `internal` | Maintenance |
| Mains voltage, UPS charge and state | PLC (input registers 14, 17; discrete input 3) | `internal` | Reveals when the station is vulnerable |
| Pump running states, float switch, trip and status bits | PLC (discrete inputs 0 to 4; input register 15) | `internal` | |
| The PLC's event log: starts, stops, trips, resets | PLC | `internal` | |
| Operator names, call-out rosters, customer accounts | Utility systems | **not on the gateway** | The HEART does not need them, so it never has them |

Nothing is `private`, because nothing is personal. The full register map, with 0-based addresses as sent on the wire, table by table, is in [the simulator's README, Section 5](../pumpStationSimulator/README.md#5-the-local-heart-api).

---

## Step 4: Say what "doing its main job" means

The main job of a pump station is: **when the town needs water, the station can pump it.** The gateway's HEART checks this from the PLC's values:

```python
def check_main_function(r):
    """CHANGE: return (True, "") if the station can pump, else (False, "what is wrong, in plain words")."""
    if r.available == 0:                       # pumps not tripped, not locked out, with mains
        return False, f"{r.why_lost()}. About {r.hours_left:.1f} h of supply left in the reservoir."
    if r.call and r.n == 0:                    # the PLC calls for water, nothing runs
        return False, "The PLC is calling for water but no pump is running."
    return True, ""
```

A pump is available when the PLC has not tripped it, the station is not locked out, the sump is not at its dry-run level and mains power is on. The main job is **degraded** when a pump runs but delivers less than three quarters of its commissioned flow while vibrating hard: the signature of cavitation.

A condition must hold for a few seconds before it is raised, because one odd reading is not a fault.

---

## Step 5: The faults it recognises

Every alert carries a fault code from Part 3, Section 5.4, so the gateway, the control room and any monitor site read it the same way.

| What happens | Fault code | Severity | Status of the system |
|---|---|---|---|
| No pump available (tripped, locked out, dry-run, no mains), or the PLC calls and nothing runs | `system.main-function-lost` | critical | failing |
| A pump runs with vibration above 7.1 mm/s and flow below 75 % of its commissioned flow (likely cavitation) | `system.main-function-degraded` | warning | degraded |
| The sump level transmitter has not changed for 90 s while water moves | `sensors.stuck` | warning | degraded |
| A motor winding at 100 Cel or more (the PLC trips at 120) | `performance.too-hot` | warning | degraded |
| Mains supply lost: PLC and gateway on the UPS | `power.supply-lost` | error | degraded |
| The telemetry link to the control room is down | `connectivity.link-lost` | warning | degraded |
| The PLC stops answering the gateway | `connectivity.dependency-unreachable` | error | degraded (a real gateway should add this; the simulator cannot lose its PLC) |
| Gateway silent for three Pulse intervals | `system.flatline` | | raised by HEART Hub |

HEART's thresholds sit **below** the PLC's trips on purpose: a person hears that a motor is hot before the PLC stops it, and the PLC stops it whether or not HEART is there. The numbers are illustrations; a real station takes them from its pumps' commissioning curves, the vibration limits for its machines (ISO 10816 or ISO 20816) and the motor maker's data.

**A stuck sensor teaches the split.** When the level transmitter sticks, HEART says "do not trust it", and marks the `sump_level` property `"quality": "bad"`. It does not try to protect the pumps: the PLC's dry-run protection comes from its own hardwired float switch, not from the transmitter HEART watches.

---

## Step 6: No commands, and why there is no Stop button

The HEART has **no capabilities**: the Card lists none, and every change request is refused with `403 capability-not-allowed`. Starting a pump, resetting a trip and changing a setpoint stay with the PLC and the people at the station. A high-pressure trip needs a **local** reset on purpose: someone must go and find the shut valve.

The standard requires a Stop control only from Autonomy Level A3 up, because A1 and A2 take no action and so have nothing to stop. The station's emergency stop is a hardwired safety function, not a HEART control.

---

## Step 7: Tell a person

Autonomy Level A1 needs an escalation route. For a pump station there are two, and both are needed:

1. **The utility's own route**: an alarm in the control room's SCADA, and a page to the on-call operator.
2. **Locally, with no network**: the station's alarm lamp, so a crew on site sees it.

Change `escalate()` to use them. The explanation tells the operator what to do first: for a lost main function it gives the town's draw and the hours of supply left in the reservoir, so the operator knows how urgent the call-out is.

When the telemetry link is down, the lamp is the only route, and alerts wait on the gateway until the link returns. HEART Hub (Step 8) adds one more: an email when the station goes silent.

---

## Step 8: Register and watch it on HEART Hub

HEART Hub lets an owner see a station's health from anywhere, on a private page.

1. **First start.** The gateway makes a random device ID, claim code and key, keeps them, and shows the claim code only to the person holding it: on a label inside the panel door, or on its console.
2. **Sign in.** The owner opens `https://heartcortex.ai/login` and enters an email address. A 6-digit code arrives; there are no passwords.
3. **Register.** At `https://heartcortex.ai/register?id=<device ID>` (a QR code on the label can open it), the owner enters the claim code. Holding the device is the proof of ownership.
4. **Watch.** The station's page, `https://heartcortex.ai/device/<device ID>`, shows its status and open alerts, private to the owner until they share it.
5. **Silence.** If no report arrives for three Pulse intervals, the page shows a Flatline and the owner gets one email. Reports go out through the station's router, so a lost telemetry link shows as a Flatline too.

Only the public Pulse and public alerts are sent, and the site keeps only the status, timing and fault codes: never a level, a flow, a pressure or a PLC event.

**Warning for a real utility.** Even "this station has lost its main function" tells someone when a town's water is vulnerable. A utility should run **its own** monitor site (the code is open, in `heart-monitor/`), inside its own network security rules (for example IEC 62443 zones and conduits), rather than send a station's alerts to a public site. `heartcortex.ai` is right for the simulator and for trials, not for live plant.

---

## Step 9: Try it with the simulator

```sh
python3 pumpStationSimulator/pumpStationSimulator.py --scenario all
```

It prints a device ID and claim code; register them as in Step 8 and watch the page while it stages each fault in turn:

| Scenario | What happens | Code |
|---|---|---|
| `stuck` | The sump level transmitter freezes | `sensors.stuck` |
| `link` | The telemetry router drops out, and so do the reports | `connectivity.link-lost` |
| `cavitation` | The suction strainer blocks: flow down, vibration up | `system.main-function-degraded` |
| `hot` | Pump 1's cooling fan is blocked; HEART warns, then the PLC trips the pump and starts pump 2 | `performance.too-hot` |
| `trip` | A valve is shut by mistake; the PLC trips both pumps on high pressure while the town keeps drawing | `system.main-function-lost` |
| `power` | Mains is lost; the PLC and gateway run on the UPS | `power.supply-lost`, `system.main-function-lost` |

Each fault is "fixed by the maintenance crew" a few minutes later, so you can watch alerts open, escalate and resolve. With the owner key, `/heart/v1/plc-events` shows what the PLC did meanwhile: trips, duty changes, restarts. Details and every option are in [`pumpStationSimulator/README.md`](../pumpStationSimulator/README.md).

---

## Step 10: Test it, rate it and claim only what passed

1. Run the **HEART-Rating client** (`heart-rating/client/heart_rating.py`) against the gateway, and the hands-on tests of Part 9: cause each fault in the table of Step 5 with a PLC simulator or on a test rig, cut the internet, cut the telemetry link, stop the PLC, pull the gateway's power and check the station carries on.
2. Publish a **Declaration** with every result.
3. Claim **HEART Basic, Autonomy Level A1**.

| Level | Fits a pump station? |
|---|---|
| **A1** (notices and tells a person) | Yes: this guide |
| **A2** (says why, with a confidence) | Yes, at HEART **Connected**, with Rhythm events as evidence: "flow fell from 300 to 175 m3/h and vibration rose from 2.2 to 9.4 mm/s within a minute, with the sump level normal: likely a blocked suction strainer, confidence medium" |
| **A3** (suggests a fix and waits for a yes) | Only at HEART **Full**, with approval and Stop, and only for what is in the supervision layer: for example "make pump 2 duty until pump 1's bearing is checked", which a person at the station approves, and which the PLC still accepts or refuses with its own interlocks. Physical changes always need a person present (Part 6). Resetting a trip stays local |
| **A4** (fixes known problems by itself) | Only for the gateway's own housekeeping, with adopted Playbooks and an allowlist entry for `rule` for each: for example "telemetry link lost: send reports through the backup cellular modem", or "PLC not answering: reconnect the Modbus client". Never a pump, a trip or a setpoint |
| **A5** (learns) | Needs the `ai` and `evolving` modules: for example a model that learns each pump's normal flow and vibration and predicts bearing wear. It still only suggests |

HEART conformance says nothing about the safety or security of the station. The PLC program, the safety functions and the network still need their own work (for example IEC 61508 or IEC 61511 for safety functions, IEC 62443 for industrial security, and the utility's own regulator).

---

## What the case study shows

- **A HEART with no commands is still useful.** In a station run by a PLC, the value is noticing, explaining and telling the right person in one standard way: cavitation, a hot motor or a stuck sensor reach an engineer before they become a trip.
- **Where HEART lives is a design decision.** It lives on a gateway beside the PLC, reads the map the PLC already has, and adds no path into it.
- **The interlocks stay where they are.** HEART's thresholds sit below the PLC's trips, and the PLC trips whether or not HEART is there.
- **"Internal" is the default for infrastructure.** Nothing here is personal, yet almost nothing is public: status and fault codes only, and a utility runs its own monitor site.
