Patient Heart Rate Monitoring System — ECG + EMG (Step-by-step)
Full guide: hardware, wiring, safe electrode placement, firmware, signal processing (ECG QRS detection, EMG RMS), IoT telemetry, calibration, and troubleshooting. Markdown-ready for README or project documentation.
1. Project summary
This project builds a patient heart-rate monitoring system using ECG (electrocardiogram) to measure cardiac electrical activity and EMG (electromyography) to monitor muscle activity. The system uses low-cost front-end modules (e.g., AD8232 for ECG, MyoWare for EMG) connected to an ESP32 for local display, processing, and IoT telemetry (MQTT). The device can provide:
- Real-time ECG waveform visualization
- Heart-rate (BPM) derived from ECG via QRS detection
- EMG activity level (RMS / envelope) for muscle monitoring
- Alerts for arrhythmia-like events, prolonged bradycardia/tachycardia
- Data logging to cloud or local storage
⚠️ Medical & safety disclaimer: This is NOT a medical device. Use only for hobbyist/educational prototyping. Take electrical safety seriously: never connect the device to mains-powered equipment; for patient-proximate use prefer medical-grade isolation and certified designs.
2. Parts & BOM
- ESP32 development board (Wi‑Fi)
- AD8232 ECG module (or AD8233) — simple single-lead ECG front-end
- MyoWare Muscle Sensor (EMG) or similar EMG preamp
- Disposable ECG electrodes and lead wires
- 3.3V supply (LiPo battery recommended) and common ground
- OLED display (SSD1306) — optional
- Breadboard, jumper wires, enclosure
Libraries / tools (Arduino/PlatformIO):
WiFi.h(built-in)PubSubClient(MQTT)Adafruit_GFX+Adafruit_SSD1306(optional)- (Optional plotting library if streaming to local PC)
3. Safety & electrode placement
Safety first:
- Use battery power (LiPo USB) to isolate from mains.
- Do not touch electrodes or patient while device connected to external networks unless isolation verified.
- Add series current-limiting resistors and isolation barrier for any clinical deployment.
Electrode placement (single-lead ECG typical):
- RA (Right Arm) — right chest/shoulder area
- LA (Left Arm) — left chest/shoulder area
- RL (Right Leg, reference) — lower torso or right hip (ground/reference)
For EMG (e.g., forearm):
- Place two active EMG electrodes over the target muscle belly ~2–3 cm apart
- Place reference electrode on a bony area or nearby neutral spot
4. Wiring / Pinout (ESP32)
ECG (AD8232) connections:
AD8232 LO+andLO-to indicate lead-off (optional)AD8232 OUT→ ESP32 analog input (e.g.,A0/GPIO 34)AD8232 3.3V→ 3.3VGND→ GND
EMG (MyoWare) connections:
MyoWare SIG→ ESP32 analog input (e.g.,GPIO 35)5Vor3.3Vdepending on module (use 3.3V for ESP32 compatibility)GND→ GND
Optional OLED (I²C): SDA → 21, SCL → 22, VCC → 3.3V, GND → GND
Use high-quality shielded leads for ECG if possible, and keep signal wiring short.
5. Hardware assembly (step-by-step)
- Mount AD8232 and MyoWare on the breadboard; wire their power rails to 3.3V and GND.
- Attach disposable ECG electrodes to patient and connect lead wires to AD8232 electrode pins (RA, LA, RL).
- Attach EMG electrodes to the muscle and connect to MyoWare.
- Connect AD8232 OUT to an ADC-capable pin on ESP32 (GPIO 34/35/36 etc.).
- Connect MyoWare SIG to another ADC pin.
- Connect OLED (optional) to I²C pins if using a display.
- Power the ESP32 via USB/battery. Verify no mains connection to patient.
6. Firmware (ESP32 sketch) — read ECG & EMG, detect heart beats, send MQTT
The code below samples ECG and EMG via ADC, performs simple filtering and QRS-like detection (Pan-Tompkins simplified), computes BPM, EMG RMS envelope, displays values on OLED, and publishes JSON to MQTT.
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// CONFIG
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASS";
const char* MQTT_SERVER = "test.mosquitto.org";
const char* MQTT_TOPIC = "patient/monitor";
// ADC pins
const int PIN_ECG = 34; // ADC1_CH6
const int PIN_EMG = 35; // ADC1_CH7
// OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
WiFiClient net;
PubSubClient mqtt(net);
// Sampling
const int SAMPLE_RATE = 250; // Hz (ECG typical 200-500Hz)
const int SAMPLE_PERIOD_MS = 1000 / SAMPLE_RATE;
// Buffers for simple filters
const int BUF_SIZE = 512;
int ecgBuf[BUF_SIZE];
int emgBuf[BUF_SIZE];
int bufIdx = 0;
// Heartbeat detection state
unsigned long lastBeat = 0;
int bpm = 0;
void connectWiFi(){
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(200);
}
void reconnectMQTT(){
while (!mqtt.connected()){
mqtt.connect("ESP32Patient");
delay(200);
}
}
float analogToVoltage(int raw){
return (raw / 4095.0) * 3.3; // 12-bit ADC on ESP32
}
// Simple bandpass IIR filter (2nd order) coefficients can be used — here we apply moving average + highpass
float highpass(float in, float alpha=0.95) {
static float prev_in = 0, prev_out = 0;
float out = alpha * (prev_out + in - prev_in);
prev_in = in; prev_out = out;
return out;
}
float lowpass(float in, float alpha=0.1) {
static float s = 0;
s = s + alpha * (in - s);
return s;
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
connectWiFi();
mqtt.setServer(MQTT_SERVER, 1883);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)){
// proceed without OLED
}
display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE);
}
unsigned long lastSampleTime = 0;
void loop() {
// sampling loop
unsigned long now = millis();
if (now - lastSampleTime >= SAMPLE_PERIOD_MS) {
lastSampleTime = now;
int ecgRaw = analogRead(PIN_ECG);
int emgRaw = analogRead(PIN_EMG);
// convert and preprocess
float ecgV = analogToVoltage(ecgRaw);
float emgV = analogToVoltage(emgRaw);
// simple filters
float ecgHP = highpass(ecgV);
float ecgFiltered = lowpass(ecgHP, 0.05);
// store in circular buffer for visualization or further processing
ecgBuf[bufIdx] = (int)(ecgFiltered * 1000);
emgBuf[bufIdx] = (int)(emgV * 1000);
bufIdx = (bufIdx + 1) % BUF_SIZE;
// QRS-like detection: detect large positive slopes
static float prev = 0;
float diff = ecgFiltered - prev;
prev = ecgFiltered;
const float THRESH = 0.4; // tune per sensor/patient (volts scale after filters)
static bool inBeat = false;
if (diff > THRESH && !inBeat) {
unsigned long t = millis();
if (lastBeat != 0) {
bpm = (int)(60000.0 / (t - lastBeat));
}
lastBeat = t;
inBeat = true;
}
if (diff < 0) inBeat = false;
// EMG envelope: moving RMS
static float emgRmsBuf[50];
static int emgIdx = 0;
emgRmsBuf[emgIdx] = emgV*emgV;
emgIdx = (emgIdx+1)%50;
float rmsSum = 0;
for (int i=0;i<50;i++) rmsSum += emgRmsBuf[i];
float emgRms = sqrt(rmsSum/50.0);
// Update OLED
if (display.width() > 0){
display.clearDisplay();
display.setCursor(0,0);
display.setTextSize(2);
display.print("BPM:"); display.println(bpm);
display.setTextSize(1);
display.print("EMG RMS:"); display.println(emgRms,3);
display.display();
}
// Publish every second
static unsigned long lastPub = 0;
if (millis() - lastPub > 1000) {
lastPub = millis();
if (!mqtt.connected()) reconnectMQTT();
String payload = "{\"bpm\": " + String(bpm) + ", \"emg_rms\": " + String(emgRms,3) + "}";
mqtt.publish(MQTT_TOPIC, payload.c_str());
Serial.println(payload);
}
}
}Firmware (ESP32)
Below is the Arduino code with explanation.
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>- These are libraries. WiFi for internet, PubSubClient for MQTT, Adafruit libraries for OLED.
const int PIN_ECG = 34; // ECG sensor pin
const int PIN_EMG = 35; // EMG sensor pin- We tell the ESP32 which pins the sensors are connected to.
const int SAMPLE_RATE = 250; // 250 samples per second
const int SAMPLE_PERIOD_MS = 1000 / SAMPLE_RATE;- ECG signals need fast sampling. 250 Hz is good enough.
unsigned long lastBeat = 0; // store last heartbeat time
int bpm = 0; // beats per minute- Variables to calculate BPM.
Filtering Functions
float highpass(float in, float alpha=0.95) { ... }
float lowpass(float in, float alpha=0.1) { ... }ECG signals have noise. These are simple filters:
- High-pass removes slow drift (baseline wander).
- Low-pass removes high-frequency noise.
Loop
int ecgRaw = analogRead(PIN_ECG);
int emgRaw = analogRead(PIN_EMG);- Read analog values from sensors.
float ecgFiltered = lowpass(highpass(ecgV));- Apply filters to clean the ECG.
float diff = ecgFiltered - prev;
if (diff > THRESH && !inBeat) { ... }- Detect a QRS complex (heartbeat peak) by checking if the slope (difference) is big enough.
- When a beat is found, calculate time since last beat → convert to BPM.
float emgRms = sqrt(rmsSum/50.0);- For EMG, calculate RMS (Root Mean Square) over 50 samples → shows muscle activity strength.
Display
display.print("BPM:"); display.println(bpm);
display.print("EMG RMS:"); display.println(emgRms,3);- Show BPM and EMG values on OLED.
MQTT
String payload = "{\"bpm\": " + String(bpm) + ", \"emg_rms\": " + String(emgRms,3) + "}";
mqtt.publish(MQTT_TOPIC, payload.c_str());- Send JSON data (BPM + EMG RMS) to cloud via MQTT.
7. How the algorithms work
ECG (QRS detection)
- ECG front-end (AD8232) outputs a conditioned signal centered around mid-supply. After AC-coupling / highpass and lowpass smoothing, the algorithm looks for rapid positive slopes (derivative) that correspond to the QRS complex.
- A very simple slope-threshold detector is shown above. For more reliability use Pan–Tompkins algorithm (bandpass -> derivative -> squaring -> moving-window integration -> adaptive threshold).
EMG (RMS envelope)
- Raw EMG is high-frequency. Compute a short-window RMS (e.g., 50 samples at 250Hz → 200ms window) to derive muscle activity level.
- Threshold the RMS to detect contractions or classify activity.
8. Calibration & tuning
- ECG amplitude/thresholds: Adjust
THRESHand filter time constants based on observed ECG amplitude. Test at rest first. - Sampling rate: Use 250–500 Hz for reliable ECG; higher sampling improves QRS timing resolution.
- EMG RMS window: 100–250ms windows are common for muscle activation detection.
9. Testing & validation
- Place electrodes on a healthy volunteer and verify waveform on serial plotter or local display.
- Compare BPM against a reference (pulse oximeter or manual pulse) and tune detection thresholds.
- Introduce muscle contractions and verify EMG RMS rises accordingly.
- Test lead-off detection and add logic to alert when electrodes disconnect.
10. Troubleshooting
- Flatline or noisy ECG: Check electrode contact, lead placement, grounding, and shielding. Ensure AD8232 reference is properly connected.
- Excessive motion artifact: Reduce cable movement, use belt/elastic to secure electrodes, apply digital filters, or discard data during large motion.
- EMG too noisy or low: Check electrode spacing and placement over muscle belly; verify module gain setting.
- MQTT not connecting: Verify Wi‑Fi credentials and broker accessibility. Consider using secure MQTT with authentication for production.
11. Power, isolation & enclosure
- For patient safety, use battery power and avoid any direct connection to mains.
- In a clinical setting, medical isolation amplifiers and certified designs are mandatory.
- Enclose electronics in a patient-safe housing and route only electrode leads through insulated openings.