HEART Embedded Systems logoHEART

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.

PlacementC: outside, never inside. The implant and its firmware are not changed. The HEART runs on the home transmitter, which already reads the implant.
Start fromThe simulator's Heart, monitor() and handler() (its Section 7), on the transmitter's embedded Linux. heartd with no capabilities is an alternative (Section 9).
ClaimHEART Basic, Autonomy Level A1. No commands, at any level.
Try it firstThe simulator and the design case study

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#

PartAssumed choiceNotes
ImplantAn existing, approved pacemakerUnchanged. Nothing is added, and its telemetry is not changed
Implant linkThe 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
TransmitterA 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 applicationThe maker's existing, regulated software: reads the implant each night and on the implant's own alerts, and sends transmissions to the clinicChanged in one way only: it writes a read-only summary for the HEART (Section 3.2)
UplinkA cellular modemOutbound only
Light and soundA status light and a speaker on the transmitterThe local route with no internet (guide, Step 7)
Secure storageA secure element or TPM, and an encrypted data partitionThe device ID, claim code and key
ClockNetwork time over the cellular link
ButtonNone for HEARTThere is nothing to stop and nothing to approve (guide, Step 6)
Monitor siteThe 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 systemThe maker's existing remote-monitoring service, used by the clinicReceives the HEART's escalations through the transmitter application's own link

2. Where everything runs#

┌ 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 simulatorWhat you do
Body, PacemakerRemove. Replace with Telemetry (3.2), read only
RULES, EXPLAINKeep 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, --seedRemove: there are no staged faults in a product

3.1 Identity#

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:

{"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.

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 happensFault codeSeverity
Pacing needed but not capturingsystem.main-function-lostcritical
Capture threshold above half the output (margin below 2:1)system.main-function-degradedwarning
Lead impedance outside 200 to 2000 ohmssensors.out-of-rangeerror
Electrical noise sensed as heartbeatssensors.noisywarning
Battery at its elective replacement pointpower.battery-lowerror
Battery at end of lifepower.battery-failingcritical
Transmitter cannot reach the implantconnectivity.link-lostwarning
Transmitter silent for three Pulse intervalssystem.flatlineraised 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.

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#

WhoWhat 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 transmitterAlso internal values: battery months and voltage, lead impedance, capture threshold, R-wave, percentage paced, the link; each alert's explanation
The maker's monitor siteThe status, timing, and each alert's fault code, severity and times. Never a reading, a heart rate or an episode
Nobody, everName, 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)#

[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:

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).
  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 eventThe HEART noticesThe patient and the clinic see
The lead fracturesAt 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 outputsystem.main-function-degraded (warning)Clinic: Degraded
The implant reports pacing that does not capturesystem.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 equipmentsensors.noisy (warning)Clinic: a warning, with the advice to move away from the source
The battery reaches its elective replacement pointpower.battery-low (error)Clinic: "Book a replacement"
The battery reaches end of lifepower.battery-failing (critical)Clinic: Failing, "Replacement is urgent". Patient: the light
The patient sleeps away from home, so the nightly read failsconnectivity.link-lost (warning): "The implant keeps working"Clinic: Degraded until the next good read
The transmitter is unplugged, or has no mobile signalNothing can be sentAfter 90 seconds, Not heard from, and one email
An atrial fibrillation episodeNothing: not a faultNothing on the monitor site. The clinic sees the episode in the transmission, as today
A fast heart rate on a walkNothing: not a faultNothing
  1. 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.

FaultHow to cause it on the benchExpect
Lead fractureOpen the test load, or replay a read with impedance at 3000 ohmssensors.out-of-range
Low safety marginReplay a read with a threshold above half the outputsystem.main-function-degraded
Loss of captureReplay a read with pacing needed and not capturedsystem.main-function-lost, red light and sound, a flag to the transmitter application
NoiseReplay a read with noise detected, or the maker's own noise testsensors.noisy
BatteryA test implant or replayed read at the replacement point, then at end of lifepower.battery-low, then power.battery-failing
Implant out of reachMove the test implant out of range, or shield it, for one scheduled readconnectivity.link-lost
Old readingsStop the summary being rewritten past the read scheduleconnectivity.link-lost
No uplinkRemove the transmitter's SIM, or unplug it, for 2 minutesNot heard from, then back after reports resume
HEART stoppedsystemctl stop heart on a development unitNot heard from. The transmitter application reads and transmits as before
Any changePUT, POST, PATCH or DELETE to any HEART path403 capability-not-allowed. Nothing changes
PrivacyRead every public view; inspect what the local monitor site storedOnly status, timing, fault codes and severities. No reading, rate or episode anywhere outside the owner-key views

Then run the HEART-Rating 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 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). 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.