# Pacemaker: on a real system

**A pacing system with a HEART on its home transmitter, never in the implant: the parts we assume, where everything runs, the code, how to build and install it, manufacturing and clinic setup, and what the patient and the clinic see.**

> **For illustration only.** The transmitter, its software and its data below are assumptions, chosen so the steps can be concrete. A real product will differ. This system has not been built: **no real implant, transmitter or patient was involved.** The code it starts from has been tested on computers with a simulated patient and implant only. **Nothing here is medical advice or a regulatory route.** Transmitter software that reads an implant and alerts a clinic is itself medical device software, regulated as such: it needs the maker's own development, risk and approval process (for example IEC 62304 for software, ISO 14971 for risk, ISO 14708 for active implants, and the regulator's route in each market). HEART conformance says nothing about medical approval.

| | |
|---|---|
| **Placement** | C: outside, never inside. **The implant and its firmware are not changed.** The HEART runs on the home transmitter, which already reads the implant. |
| **Start from** | The [simulator's](/pacemakerSimulator/README.md) `Heart`, `monitor()` and `handler()` (its Section 7), on the transmitter's embedded Linux. `heartd` with no capabilities is an alternative (Section 9). |
| **Claim** | HEART Basic, Autonomy Level A1. **No commands, at any level.** |
| **Try it first** | [The simulator](/pacemakerSimulator/README.md) and the [design case study](/guides/Pacemaker.md) |

---

## Before you start: HEART never touches pacing

Sensing, pacing, every programmed setting, programming itself and the implant's safety modes stay in the implant and the clinician's programmer, exactly as they are. The HEART has **no capabilities**: it cannot change a setting, not even with approval, because a person approving on a phone is not a clinician at a programmer. **No therapy setting ever goes through HEART or HEART Hub.**

And **a patient's condition is not a device fault.** An atrial fibrillation episode or a fast heart rate on a walk is a fact about the patient. It is never a HEART alert. Only problems of the system (lead, capture, battery, noise, link) are.

---

## 1. The system we assume

| Part | Assumed choice | Notes |
|---|---|---|
| Implant | An existing, approved pacemaker | **Unchanged.** Nothing is added, and its telemetry is not changed |
| Implant link | The radio the implant already uses to talk to its transmitter (a medical implant band, or Bluetooth Low Energy, depending on the maker) | HEART has no access to it |
| Transmitter | A bedside unit with an ARM application processor, 512 MB RAM and eMMC flash, running embedded Linux (Yocto) | Class Core-M (32 MB RAM budget for the HEART) |
| Transmitter application | The maker's existing, regulated software: reads the implant each night and on the implant's own alerts, and sends transmissions to the clinic | Changed in one way only: it writes a read-only summary for the HEART (Section 3.2) |
| Uplink | A cellular modem | Outbound only |
| Light and sound | A status light and a speaker on the transmitter | The local route with no internet (guide, Step 7) |
| Secure storage | A secure element or TPM, and an encrypted data partition | The device ID, claim code and key |
| Clock | Network time over the cellular link | |
| Button | None for HEART | There is nothing to stop and nothing to approve (guide, Step 6) |
| Monitor site | **The maker's own** HEART Hub site, run from the open code in `heart-monitor/` | Never `heartcortex.ai` for patients (guide, Step 8). Here it is `https://monitor.example.com` |
| Clinic system | The maker's existing remote-monitoring service, used by the clinic | Receives the HEART's escalations through the transmitter application's own link |

---

## 2. Where everything runs

```text
┌ Implant (unchanged) ┐        ┌──────────── Home transmitter: embedded Linux ────────────┐
│ senses and paces    │ radio  │ Transmitter application (regulated, as before)           │
│ stores measurements ├───────►│  reads the implant each night and on alerts ─────────────────► maker's service ─► clinic
└─────────────────────┘        │  writes /run/transmitter/heart.json (a summary)          │
                               │        │ read only                ▲ fault codes only     │
                               │        ▼                          │                      │
                               │ heart.service (user heart): rules, light and sound,      │
                               │  API on 127.0.0.1:43278 ─────────────────────────────────────► maker's HEART Hub site
                               └──────────────────────────────────────────────────────────┘    (status and fault codes)
```

- **Nothing new in the implant.** The HEART has no path to the radio or to the implant. It reads a file, and the implant is read no more often than before.
- **One way in.** The transmitter application writes the summary; the `heart` user can read it, not write it. The only thing the HEART hands back to the transmitter application is a fault code to flag (Section 3.4), never a setting.
- **HEART is not the alarm path the patient depends on.** The transmitter's own regulated alerts to the clinic stay as they are. HEART adds standard fault codes, plain words and a light. If the HEART stops, pacing and the existing alerts carry on unchanged.

---

## 3. The code

Copy `pacemakerSimulator.py` to `/opt/heart/pacemaker_heart.py`, and change it as below.

| Part of the simulator | What you do |
|---|---|
| `Body`, `Pacemaker` | Remove. Replace with `Telemetry` (3.2), read only |
| `RULES`, `EXPLAIN` | Keep the fault codes. Take every threshold from the implant's own specification (3.3) |
| `Heart.escalate()` | Change to the clinic flag and the light (3.4) |
| `Heart.properties()` | Remove the values that came from `Body` (heart rate, activity, temperature), and go through the privacy table (3.5) |
| `monitor()`, `handler()` | Keep. Every `PUT`, `POST`, `PATCH` or `DELETE` stays refused with `403 capability-not-allowed` |
| `--scenario`, `--speed`, `--seed` | Remove: there are no staged faults in a product |

### 3.1 Identity

```python
SYSTEM_ID = "com.example.cardio.pacing-system"   # CHANGE: names the product, never the patient
NAME = "Example pacing system"                   # CHANGE
VERSION = "1.0.0"                                # CHANGE
INSTANCE = "tx"                                  # CHANGE: must not identify a person
```

The device ID that HEART Hub uses is random. Nothing links it to a person except the clinic's own records.

### 3.2 Reading the implant, through the transmitter

The transmitter application writes a summary after each read of the implant. Its format is ours; it holds only what the HEART needs:

```json
{"readAt": "2026-11-12T03:10:00Z", "implantInRange": true, "pacingNeeded": true, "captureConfirmed": true,
 "outputV": 2.5, "captureThresholdV": 0.75, "leadImpedanceOhm": 560, "rWaveMv": 9.1, "pacedPercent": 12.4,
 "noiseDetected": false, "batteryMonthsLeft": 95.2, "batteryVoltage": 2.78}
```

**Not in it:** the patient's name, date of birth or clinic number, heart rate, movement, temperature and rhythm episodes. The guide's Step 3 allows the physiological values as `private`. We leave them out, because the clinic already receives them in the transmission, and what the HEART does not hold it cannot leak. If you keep them, keep them `private`.

```python
class Telemetry:
    """CHANGE: read only. Filled from the summary the transmitter application writes after each read."""
    PATH = "/run/transmitter/heart.json"        # assumed path; readable by the heart user, never writable

    def refresh(self):
        with open(self.PATH) as f:
            t = json.load(f)
        self.read_at = t["readAt"]
        self.paced, self.captured = t["pacingNeeded"], t["captureConfirmed"]
        self.output_v, self.threshold_now = t["outputV"], t["captureThresholdV"]
        self.impedance_now, self.r_wave = t["leadImpedanceOhm"], t["rWaveMv"]
        self.paced_fraction = t["pacedPercent"] / 100
        self.noise, self.link_down = t["noiseDetected"], not t["implantInRange"]
        self.months, self.voltage = t["batteryMonthsLeft"], t["batteryVoltage"]
```

`outputV` is the programmed output as read from the implant. The HEART reads it to judge the safety margin; it never sets it.

### 3.3 The rules

Keep the simulator's fault codes, from Part 3 of the standard. The thresholds in the simulator are illustrations: a real product takes them from the implant's own specification.

| What happens | Fault code | Severity |
|---|---|---|
| Pacing needed but not capturing | `system.main-function-lost` | critical |
| Capture threshold above half the output (margin below 2:1) | `system.main-function-degraded` | warning |
| Lead impedance outside 200 to 2000 ohms | `sensors.out-of-range` | error |
| Electrical noise sensed as heartbeats | `sensors.noisy` | warning |
| Battery at its elective replacement point | `power.battery-low` | error |
| Battery at end of life | `power.battery-failing` | critical |
| Transmitter cannot reach the implant | `connectivity.link-lost` | warning |
| Transmitter silent for three Pulse intervals | `system.flatline` | raised by HEART Hub |

Two changes from the simulator: compare the threshold with `p.output_v`, read from the implant, instead of the fixed `OUTPUT_V`; and treat a summary whose `readAt` is older than the transmitter's read schedule allows (for example 36 hours, our assumption) as `connectivity.link-lost`, because old readings are not news.

### 3.4 Escalation

Both routes of the guide's Step 7: the clinic's own route, and the transmitter's light and sound with no internet.

```python
FLAGS = "/run/transmitter/heart-flags.sock"   # assumed: the transmitter application listens here for fault codes


def escalate(self, a):
    """CHANGE: flag the next transmission for the clinic, and tell the patient at the bedside. Never a route to the implant."""
    a["status"] = "escalated"
    try:
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
            s.connect(FLAGS)
            s.sendall((json.dumps({"code": a["code"], "severity": a["severity"]}) + "\n").encode())
    except OSError as e:
        log(f"HEART: could not flag the transmission: {e}")
    status_light(a["severity"])     # your board's code: amber for warning and error, red and a sound for critical
```

The transmitter application decides what to do with a flag, under its own regulated design. The message carries a fault code and a severity, nothing else. Add `import socket` at the top.

### 3.5 Who sees what

| Who | What they see |
|---|---|
| Anyone on the transmitter (no key) | The Card, the Pulse, open fault codes, battery status (ok, replace soon, end of life) |
| The owner key, on the transmitter | Also `internal` values: battery months and voltage, lead impedance, capture threshold, R-wave, percentage paced, the link; each alert's explanation |
| The maker's monitor site | The status, timing, and each alert's fault code, severity and times. **Never a reading, a heart rate or an episode** |
| Nobody, ever | Name, date of birth, clinic patient number: not on the transmitter at all |

Even "this person's pacing system has a lead problem" is health information (GDPR Article 9, HIPAA). That is why the monitor site is the maker's own, run under the maker's data-protection duties.

### 3.6 What stays off

- **No commands.** Keep `handler()` refusing every change. Do not add a capability, a lifecycle operation, a Stop, Playbooks or an allowlist.
- **No settings from the monitor site, no approvals, no figures.** The simulator's `monitor()` sends only the public Pulse and public alerts. Do not add more.
- **No AI brain** on this transmitter (see Section 9).
- If you start from the Python template instead, leave `HEART_LIFECYCLE`, `HEART_MONITOR_APPROVALS`, `HEART_MONITOR_APPROVALS_PHYSICAL` and `HEART_BRAIN` unset. Note that the template has no `properties` view or privacy levels above `public`; the simulator's code has both.

---

## 4. Build and install

The HEART is one Python file with only the standard library. Add Python 3.8 or newer to the transmitter's Yocto image, and the file and a systemd unit to the same image, built and signed under the maker's software process. In IEC 62304 terms, Python and the HEART code are software items the maker must account for (for example as SOUP), like any other.

### The systemd unit (in the image: `/etc/systemd/system/heart.service`)

```ini
[Unit]
Description=HEART for the pacing system (monitoring only)
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/heart/pacemaker_heart.py --monitor-url https://monitor.example.com \
    --state-file /var/lib/heart/pacemaker-monitor.json --port 43278
User=heart
Group=heart
SupplementaryGroups=transmitter-ro
StateDirectory=heart
StateDirectoryMode=0700
Restart=on-failure
RestartSec=2
MemoryMax=32M
CPUQuota=5%
Nice=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true

[Install]
WantedBy=multi-user.target
```

- `transmitter-ro` is a group (ours) that may read `/run/transmitter/heart.json` and connect to the flag socket, and nothing else of the transmitter application.
- `PrivateDevices=true` keeps the HEART away from device files, including the radio's.
- The simulator prints the owner key in its start line. **Change that line** so the key goes to the clinic system at setup, never to a log.
- The state file holds the device ID, claim code and key. On a product, keep them in the secure element, or at least on the encrypted partition. The simulator writes the file with mode 0600.
- The memory and CPU limits are our guesses for the Core-M class: measure them.

On a development unit (never a production one, which has no shell), check it:

```sh
journalctl -u heart -b --no-pager
curl -s http://127.0.0.1:43278/heart/v1/pulse
curl -s http://127.0.0.1:43278/heart/v1/arrhythmias            # public: fault codes only
curl -s -X PUT http://127.0.0.1:43278/heart/v1/properties      # 403 capability-not-allowed
```

---

## 5. In manufacturing and clinic setup

### On the production line

For each transmitter:

1. **Flash** the signed release image, with secure boot on.
2. **Start it once** on the test jig. The HEART makes its device ID, claim code and key, and stores them.
3. **Read the device ID and claim code** from the jig, and **print the label** for inside the box: the claim code, and a QR code of `https://monitor.example.com/register?id=<ID>`. Replace the simulator's console lines with your jig's label printer.
4. **Run the bench tests** of Section 7 on a sample from each batch, with an implant simulator, never a patient.

### At the clinic

1. **Pair** the transmitter with the patient's implant, by the transmitter's own procedure, as today. HEART plays no part in it.
2. **Register** it: a clinic user signs in to the maker's monitor site with an emailed code, opens the address on the label, and types the claim code. The site asks for no patient details. Which device ID belongs to which patient stays in the clinic's own records. (Whether the clinic or the patient registers is the maker's choice; we assume the clinic.)
3. **Share, if wanted.** The page is private until shared. A clinic may share it with the patient (see the [owner's manual, Section 10](../HEART-Monitor-Owner-Manual.md#10-share-or-forget-a-device)).
4. **Tell the patient** what the light and the sound mean, and that the transmitter needs power and a mobile signal at the bedside.

---

## 6. What the patient and the clinic see

1. **Every day.** Nothing. The patient sleeps near the transmitter, which reads the implant at night, as before.
2. **The clinic** sees each transmitter's status on the monitor site (**Healthy**, **Degraded**, **Failing** or **Not heard from**) and the fault codes in plain words. The details go to its existing clinic system, through the transmitter application.
3. **When something goes wrong:**

| Real event | The HEART notices | The patient and the clinic see |
|---|---|---|
| The lead fractures | At the next read: impedance above 2000 ohms, `sensors.out-of-range` (error) | Clinic: **Degraded**, a flagged transmission. Patient: the light |
| The capture threshold rises above half the output | `system.main-function-degraded` (warning) | Clinic: **Degraded** |
| The implant reports pacing that does not capture | `system.main-function-lost` (critical): "Pacing is not capturing the heart. Contact the clinic now." | Clinic: **Failing**. Patient: red light and sound |
| Noise from strong magnets, motors or welding equipment | `sensors.noisy` (warning) | Clinic: a warning, with the advice to move away from the source |
| The battery reaches its elective replacement point | `power.battery-low` (error) | Clinic: "Book a replacement" |
| The battery reaches end of life | `power.battery-failing` (critical) | Clinic: **Failing**, "Replacement is urgent". Patient: the light |
| The patient sleeps away from home, so the nightly read fails | `connectivity.link-lost` (warning): "The implant keeps working" | Clinic: **Degraded** until the next good read |
| The transmitter is unplugged, or has no mobile signal | Nothing can be sent | After 90 seconds, **Not heard from**, and one email |
| An atrial fibrillation episode | **Nothing: not a fault** | Nothing on the monitor site. The clinic sees the episode in the transmission, as today |
| A fast heart rate on a walk | **Nothing: not a fault** | Nothing |

4. **What nobody can do through HEART:** change a setting, start a test, or reprogram anything. That needs a clinician at a programmer.

---

## 7. Test it on the bench

**Never with a patient.** Use the maker's implant simulator, or a test implant on the bench connected to a test load in place of the lead, and a local monitor site (`heart-monitor/`; the simulator accepts `http` only for `127.0.0.1`). Cause every fault, and check the right fault code appears after the next read, on the API and on the monitor page.

| Fault | How to cause it on the bench | Expect |
|---|---|---|
| Lead fracture | Open the test load, or replay a read with impedance at 3000 ohms | `sensors.out-of-range` |
| Low safety margin | Replay a read with a threshold above half the output | `system.main-function-degraded` |
| Loss of capture | Replay a read with pacing needed and not captured | `system.main-function-lost`, red light and sound, a flag to the transmitter application |
| Noise | Replay a read with noise detected, or the maker's own noise test | `sensors.noisy` |
| Battery | A test implant or replayed read at the replacement point, then at end of life | `power.battery-low`, then `power.battery-failing` |
| Implant out of reach | Move the test implant out of range, or shield it, for one scheduled read | `connectivity.link-lost` |
| Old readings | Stop the summary being rewritten past the read schedule | `connectivity.link-lost` |
| No uplink | Remove the transmitter's SIM, or unplug it, for 2 minutes | **Not heard from**, then back after reports resume |
| HEART stopped | `systemctl stop heart` on a development unit | **Not heard from**. The transmitter application reads and transmits as before |
| Any change | `PUT`, `POST`, `PATCH` or `DELETE` to any HEART path | `403 capability-not-allowed`. Nothing changes |
| Privacy | Read every public view; inspect what the local monitor site stored | Only status, timing, fault codes and severities. No reading, rate or episode anywhere outside the owner-key views |

Then run the [HEART-Rating](/heart-rating/README.md) client and the hands-on tests of Part 9, publish a Declaration, and claim HEART Basic, Autonomy Level A1. These tests are part of the evidence, never a replacement for the product's own verification and validation.

---

## 8. Timing: what to expect

- **The HEART knows only the last read.** It checks every 30 seconds, but the implant is read each night and when the implant raises its own alert. A lead that fractures in the morning may be seen only at the next read. HEART does not make it faster, and must not: **reading the implant more often costs the implant's battery.**
- **The existing alert path stays first.** Where the implant raises its own alert, the transmitter application sends it to the clinic as before. HEART adds a standard code and plain words to it; it does not replace it.
- **Travel looks like a fault, mildly.** A patient away from home gives `connectivity.link-lost`, and an unplugged transmitter gives **Not heard from** and one email. Clinics should expect both, and read them as "no news", not as a problem with the patient.
- **"Not heard from" says nothing about the heart.** It means the transmitter is silent. The implant keeps pacing.

---

## 9. Going further

- **Explanations (A2).** On a transmitter at HEART Connected, an alert can say why, with a confidence, for example "lead impedance rose from 550 to 3300 ohms in one night: likely fracture, confidence medium" (guide, Step 10). Do it from device data only, never from private health data, and only after the maker's own risk assessment. A3 and above do not apply to therapy.
- **`heartd` instead.** A larger transmitter can run [`heartd`](/heart-core-rs/README.md) monitoring only: mode `observe`, no custom commands, an empty allowlist, no `[presence]` (so nothing can be approved), no `[lifecycle]`, with the summary sent through `host_socket` (see its [configuration guide](/heart-core-rs/docs/CONFIGURATION.md)). In this release `heartd` does not report to HEART Hub.
- **A phone app as the transmitter.** The simulator's Section 7 suggests the Python or JavaScript template inside the app. The same rules apply: read only, no commands, health data private.
- **A smaller transmitter.** A microcontroller unit with Wi-Fi can start from the ESP32 Full starter (guide, Step 1), with no writable signals and no approvals.
- **Your own monitor site.** The code is open, in `heart-monitor/`. For patients, run it under your own data-protection duties; `heartcortex.ai` is for the simulator and for trials only.
