Final Project — Seebscribe My Heartbeat
Wearable · Live Signal · Streaming
Heartbeat concept notes
I started this project thinking about light — atmosphere, mood, modular control. But the more I worked with outputs, the more I wanted the light to mean something. To react to something real.
During Week 11 Networking and Communication I started experimenting with biosensors. A heartbeat turned out to be the most honest signal I could find — constant, invisible, and entirely personal. It's always there. You just can't see it.
Heartbeat Device Research
I conducted research about existing projects, competitors in the market, and potential competitors in the market to understand the direction of my Seebscribe project.
Conclusion: Closest Competitor & Seebscribe Positioning
Among all existing devices surveyed in Section 1, the closest direct competitor to Seebscribe is the Polar H10 Heart Rate Sensor. The H10 is a chest-worn ECG strap that activates automatically when the skin becomes wet during training, streams beat-by-beat heart rate and RR intervals over Bluetooth and ANT+, and retails at approximately $100 USD. It is the gold standard for consumer-grade chest ECG, trusted by athletes, researchers, and sports scientists worldwide. Its open SDK allows developers to receive raw ECG waveform data, which places it in a different category from closed fitness wearables.
Seebscribe
After experimenting with different ways to make invisible body signals visible, I came up with the idea for Seebscribe — a wearable device that captures the heartbeat and shares it as light, rhythm, or pattern. The project began from a simple thought: the heart does not only pump blood, it also produces a signal that can be sensed, translated, and shown. Rather than creating a medical device, I wanted to design a perceptual object that lets someone “subscribe” to a living rhythm — not to content, but to the body itself.
The heart doesn't just pump — it broadcasts. Every beat sends a pressure wave through the entire body, readable by an optical sensor as a flickering shadow in the fingertip. Seebscribe captures that signal and makes it visible: a wearable device that reads your heartbeat and streams it live as light, pattern, or rhythm. Not your content. Your body.
The name collapses three ideas. See — making the invisible visible. Subscribe — the internet act of following someone's output, reimagined as following someone's autonomic state. And Seebeck — Thomas Johann Seebeck, who discovered in 1821 that a temperature difference between two conductors generates voltage. For now, Seebeck is part of the name and a possible future direction for energy experiments, not the current power source.
The goal isn't medical. It's perceptual — take something you feel but never watch, and give it a form worth looking at.
Concept Preview
This is an early concept page for the Seebscribe device and application. It explores how the heartbeat could become a visual output, allowing the user’s internal rhythm to be seen through light, pattern, and movement.
Open separately: seebscribe-memo.html
Seebscribe shares the same core function — capturing and transmitting a live heartbeat signal from the chest — but pursues a different direction on three axes: signal precision, sensing geometry, and openness.
Seebscribe vs. Polar H10 — Summary
The Polar H10 defines the benchmark: a closed, sport-focused ECG chest strap at about $100, with strong signal quality, long battery life, and wide app compatibility. Seebscribe starts from the same idea — reading a live heartbeat signal from the body — but moves it toward open creative use instead of only sport tracking. The goal is not just to measure the heart, but to let the signal become visible, shareable, and usable in light, screen, and media outputs.
Development Versions
I will develop Seebscribe in three versions. The first version proves the heartbeat signal and visualisation. The second version improves comfort and wireless hardware. The third version focuses on making the device easier to wear and closer to a final wearable object.
V1 — First Working Prototype
- Input: AD8232 heartbeat sensor and DFRobot Gravity Heart Rate Monitor Sensor for testing heartbeat capture.
- Output: screen-based heartbeat display.
- GUI: Python visualiser for plotting and checking the live signal.
- Controller: XIAO ESP32-C3 with AD8232, snap connectors, and battery power.
- Case: 3D printed enclosure for quick iteration and wearable tests.
V2 — More Wearable Sensor Version
- Input: easier-to-wear heartbeat sensing, exploring MAX30100 or MAX30102 optical pulse sensors together with the ECG direction.
- Output: screen-based heartbeat display.
- GUI: Python visualiser, updated for cleaner live monitoring.
- Controller: XIAO nRF52840 as the main wireless controller, with AD8232/ESP32-C3 tests kept as reference hardware, plus snap connectors and battery power.
- Case: improved 3D printed enclosure with better comfort and cable/snap placement.
V3 — Refined Wearable Prototype
- Input: heartbeat sensor layout refined for easier wearing and more stable contact with the body.
- Output: screen-based heartbeat display, with the signal ready to connect to other visual outputs later.
- GUI: Python visualiser as the main debugging and demonstration interface.
- Controller: XIAO nRF52840-focused wearable electronics, using the earlier AD8232/ESP32-C3 experiments as the baseline, with snap connectors and battery power.
- Case: polished 3D printed wearable case, designed around comfort, access to connectors, and clean presentation.
System diagram
System Overview
Seebscribe My Heartbeat captures the body's electrical cardiac signal starting from a single chest electrode pair — the same principle as the Polar H10 — with an optional MAX30102 optical sensor on the finger as an alternative PPG input. In a later phase, a second electrode extends to the wrist, forming a full Lead I configuration across the body for a stronger and more precise signal. Both inputs feed into an analog frontend chip (AD8232 for Phase 1, upgrading to the 18-bit MAX30003 in Phase 3) which filters and amplifies the raw heartbeat waveform before passing it to a microcontroller (XIAO ESP32-C3 initially, then XIAO nRF52840, then a custom EBYTE E73 module on the final Yaroboard v2 (v3) PCB). The microcontroller processes the signal and broadcasts live ECG data — raw waveform, RR intervals, and BPM — over BLE 5.0. This open data stream can be received locally by a paired device for direct outputs such as a heartbeat-driven LED sculpture or generative sound, or relayed via phone, computer, or any local device to a cloud-based web app that enables remote display, a shared two-person pulse visualisation, and a personal data journal.
Sensing
The sensing layer is where the body's cardiac signal enters the device. The primary input is a chest electrode pair using Ag/AgCl gel contacts or dry metal pads, capturing the electrical activity of the heart muscle directly from the skin surface — the same fundamental approach used in the Polar H10 and clinical Holter monitors. As an alternative input, a MAX30102 optical sensor on the fingertip reads the pulse through photoplethysmography (PPG), shining infrared light into the skin and measuring how blood flow changes the reflected signal with each heartbeat. In a later phase, a wrist electrode is added to form a true Lead I ECG configuration — one contact on the chest, one on the inner wrist — extending the electrical baseline across the body for a stronger and cleaner signal.
Frontend
The analog frontend is responsible for taking the very small electrical signal from the electrodes (in the millivolt range) and preparing it for the microcontroller. In Phase 1, this is the AD8232 — a dedicated single-lead ECG chip that amplifies the signal, removes baseline drift with a high-pass filter, and cuts muscle noise with a low-pass filter, outputting a clean analog voltage readable by the microcontroller's ADC. In Phase 3, the AD8232 is replaced by the MAX30003, an 18-bit ECG frontend with a 2µV noise floor, configurable bandwidth matching the IEC 60601 clinical standard, hardware R-peak detection, and a digital SPI interface. The upgrade from AD8232 to MAX30003 is designed as a drop-in swap — same electrode connections, same PCB footprint on the Yaroboard, only the chip and firmware interface change.
MCU
The microcontroller reads the processed signal from the frontend, runs peak detection and interval calculation in firmware, and manages the BLE radio. The device follows a three-stage hardware roadmap, each stage using the same XIAO form factor so the surrounding PCB design stays unchanged. Stage 1 uses the XIAO ESP32-C3 — already available, Arduino-compatible, USB-powered, used for firmware development and signal validation. Stage 2 upgrades to the XIAO nRF52840, which uses Nordic silicon — the same chip family as the Polar H10 — with a hardware floating-point unit for real DSP filtering, roughly 10× better BLE power efficiency, and a LiPo or CR2032 battery circuit built in. Stage 3 integrates a bare EBYTE E73 nRF52840 module directly onto the custom Yaroboard v3 PCB, powered by a CR2032 coin cell, bringing the BLE silicon cost down to ¥15 and giving full control over the final form factor.
Wireless
The microcontroller broadcasts all processed cardiac data over BLE 5.0 as an open data stream — raw ECG waveform samples, RR intervals (the precise time between each heartbeat in milliseconds), and calculated BPM. Using the standard Bluetooth Heart Rate Service profile means any BLE-capable device can receive the data without a proprietary app. The open stream is the core design decision that separates Seebscribe from closed fitness wearables: any receiver — a microcontroller, a laptop, a phone, a custom PCB — can subscribe to the data and do something creative with it.
Local output
When received directly by a paired device over BLE without going through the internet, the heartbeat data drives real-time physical outputs. The primary local output is a heartbeat-driven light object — an LED sculpture or lamp that pulses with the live signal. A second local output channel drives generative sound or haptic feedback, turning the wearer's pulse into rhythm, tone, or vibration. Local outputs have the lowest latency (under 10ms) and work without any network connection, making them suitable for live performance and gallery contexts.
Cloud output
When the BLE stream is relayed to the internet via a phone, laptop, or any connected local device, it reaches a web application that enables three remote output modes. A remote display renders the live heartbeat waveform on any browser — a web page that anyone with the link can watch in real time. A shared pulse view connects two people, visualising the relationship between their heartbeats simultaneously. A data journal logs the session and generates a daily portrait — a visual or physical artefact unique to that day's cardiac rhythm, available to export, 3D print, or laser cut.
Phase roadmap
Phase 1 — Proof of signal (now)
The goal is to validate the entire signal chain from skin to BLE using available hardware. This means wiring the AD8232 to the XIAO ESP32-C3, connecting chest gel electrodes, plotting the raw ECG waveform over USB serial, and confirming visible QRS peaks. In parallel, the MAX30102 PPG sensor is tested on the finger over I²C to confirm BPM output. Once both signals are visible and stable, the BLE firmware is written to stream RR intervals and BPM to a receiving device. The Yaroboard v2 PCB is designed in EasyEDA Pro and milled it, integrating AD8232, ESP32-C3, battery connector, and electrode snap connectors on a single board.
Phase 2 — Precision and wearability
The XIAO ESP32-C3 is replaced by the XIAO nRF52840 on the same PCB footprint. The wrist electrode is added, extending the sensing configuration to a full Lead I geometry. Firmware is updated with proper DSP bandpass filtering (0.5–150Hz) using the nRF52840's hardware FPU, and R-peak detection is refined for millisecond-accurate RR interval timing competitive with the Polar H10. A wristband satellite board is designed and milled, housing the wrist electrode contact and connecting back to the main board via a shielded cable. The first output device — a BLE-receiving LED light object — is built and demonstrated.
Phase 3 — Custom Yaroboard v3 and full output
The XIAO module is replaced by a bare EBYTE E73 nRF52840 module soldered directly onto the Yaroboard v3 PCB, with a CR2032 coin cell holder and SWD programming pads. The AD8232 frontend is replaced by the MAX30003 for clinical-comparable signal quality at 512Hz. The web application is built to receive the BLE stream via a phone bridge and render the remote display, shared pulse, and data journal outputs. The complete system — wearable sensor, custom PCB, light object output, and web interface — is integrated and presented as the Fab Academy final project.
List of the Project Tasks
The project is organized as a working schedule so each stage can be checked, tested, and documented before the next hardware version begins. The due times are estimates and will be updated as the fabrication process develops.
Phase 1 — Proof of Signal
| Work area | Tasks | Status | Estimated due time |
|---|---|---|---|
| Electronics & Sensing |
|
Complete | May 3, 2026 |
| Firmware |
|
Planned | May 7, 2026 |
| PCB Design — Yaroboard v2 |
|
Planned | May 10, 2026 |
| First Output |
|
Planned | May 12, 2026 |
| Documentation |
|
Planned | May 13, 2026 |
Phase 2 — Precision & Wearability
| Work area | Tasks | Status | Estimated due time |
|---|---|---|---|
| Sensing Extension |
|
Planned | May 17, 2026 |
| MCU Upgrade |
|
Planned | May 20, 2026 |
| Battery & Wearability |
|
Planned | May 24, 2026 |
| Output Development |
|
Planned | May 28, 2026 |
| Documentation |
|
Planned | May 31, 2026 |
Phase 1 — Proof of Signal
Wiring my custom Yaro board and AD8232
AD8232 Pin Breakdown
Power
Pin |
What it is |
Connect to |
|---|---|---|
3.3V |
Power supply |
3.3V on Yaroboard |
GND |
Ground |
GND on Yaroboard |
Signal output
Pin |
What it is |
Connect to |
|---|---|---|
OUTPUT |
Analog ECG signal |
Any ADC pin on ESP32-C3 (e.g. D0/A0) |
Leads-off detection
Pin |
What it is |
Connect to |
|---|---|---|
LO+ |
Detects if positive electrode lost contact |
Any digital input pin (e.g. D1) |
LO− |
Detects if negative electrode lost contact |
Any digital input pin (e.g. D2) |
SDN |
Shutdown — pull LOW to disable chip |
Leave unconnected or tie to 3.3V to keep active |
Electrode connections — this is the key part
Pin |
What it means |
Where to attach |
|---|---|---|
RA |
Right Arm |
Electrode on right chest / right side |
LA |
Left Arm |
Electrode on left chest / left side |
RL |
Right Leg drive (RLD) |
Third reference electrode — lower chest or abdomen |
These names come from the clinical ECG convention — they don't mean you literally attach to an arm. For your chest configuration:
· RA → right chest electrode
· LA → left chest electrode
· RL → lower chest or lower left abdomen (noise reference)
Wiring to Yaroboard
AD8232 Yaroboard (ESP32-C3)
-------- ---------------------
3.3V -----> 3V3
GND -----> GND
OUTPUT -----> D0 (ADC pin, reads analog ECG)
LO+ -----> D1 (digital in, HIGH = electrode off)
LO- -----> D2 (digital in, HIGH = electrode off)
SDN -----> leave floating or tie to 3V3
RA -----> right chest electrode (snap/wire)
LA -----> left chest electrode (snap/wire)
RL -----> lower abdomen electrode (noise reference)
XIAO ESP32-C3 pinout reference.
For easy connection I solder Pin header to AD8232 board
For the wiring part, this was quite easy because I used jumper wires and I have pin headers.
Also, when I'm connected to the power and I can touch some electrodes, I can see that the AD8232 boards start blinking
Next is positioning electrodes to harvest an accurate heartbeat signal. Here is a diagram of how to make it better.
I also found an article about how to experiment with Arduino and AD8232 board to harvest the heartbeat signal.
Connecting electrodes
I ask AI to generate simple code to check sensor AD8232 and some heartbeat signals.
Generate a simple code for ESP32-C3 and AD8272 sensor to check the electrodes and heart beat signal:
Check code:
// ─────────────────────────────────────────────
// Seebscribe — AD8232 ECG reader
// XIAO ESP32-C3 + AD8232
// View output in: Tools → Serial Plotter
// ─────────────────────────────────────────────
const int ECG_PIN = A0; // AD8232 OUTPUT → A0
const int LO_PLUS = D1; // AD8232 LO+ → D1
const int LO_MINUS = D2; // AD8232 LO- → D2
void setup() {
Serial.begin(115200);
pinMode(LO_PLUS, INPUT);
pinMode(LO_MINUS, INPUT);
delay(500);
Serial.println("Seebscribe ECG ready");
}
void loop() {
// Check if electrodes are connected
if (digitalRead(LO_PLUS) == HIGH ||
digitalRead(LO_MINUS) == HIGH) {
Serial.println(0); // flat line when disconnected
delay(4);
return;
}
// Read and print ECG value
int ecgValue = analogRead(ECG_PIN);
Serial.println(ecgValue);
delay(4); // ~250 Hz sample rate
}
And it's working. I can see some numbers in the serial monitor.
Next step to see visual in serial plotter.
Ai request:
Okay, code is working now. Can you generate a simple code to see the actual heartbeat signal like ECG on the serial plotter?
// ─────────────────────────────────────────────────────────────
// Seebscribe — ECG Serial Plotter
// Hardware: XIAO ESP32-C3 + AD8232 ECG frontend
// Output: Tools → Serial Plotter (115200 baud)
// Wiring: AD8232 OUTPUT → A0
// AD8232 LO+ → D1
// AD8232 LO- → D2
// AD8232 3.3V → 3V3
// AD8232 GND → GND
// ─────────────────────────────────────────────────────────────
// ── Pin definitions ──────────────────────────────────────────
const int ECG_PIN = A0; // Analog ECG signal from AD8232 OUTPUT pin
const int LO_PLUS = D1; // Leads-off detection: HIGH when LA electrode disconnected
const int LO_MINUS = D2; // Leads-off detection: HIGH when RA electrode disconnected
// ── Moving average filter settings ───────────────────────────
// A moving average reduces high-frequency noise from the ADC
// without blurring the sharp QRS spike we want to see clearly.
// FILTER_SIZE = 4 means we average the last 4 raw ADC readings.
// Larger values = smoother signal but slower response to peaks.
const int FILTER_SIZE = 4;
int filterBuffer[FILTER_SIZE]; // Circular buffer storing recent readings
int filterIndex = 0; // Current position in the circular buffer
long filterSum = 0; // Running total of values in the buffer
// ── smoothedRead() ───────────────────────────────────────────
// Reads a new raw value from the ADC, adds it to the circular
// buffer, removes the oldest value, and returns the average.
// Called once per loop iteration instead of analogRead() directly.
int smoothedRead() {
int raw = analogRead(ECG_PIN); // Read raw 12-bit ADC value (0–4095)
filterSum -= filterBuffer[filterIndex]; // Subtract the oldest value from the sum
filterBuffer[filterIndex] = raw; // Overwrite it with the new reading
filterSum += raw; // Add new reading to the running sum
// Advance the buffer index, wrapping back to 0 after reaching FILTER_SIZE
filterIndex = (filterIndex + 1) % FILTER_SIZE;
return filterSum / FILTER_SIZE; // Return the integer average of all buffered values
}
// ── setup() ──────────────────────────────────────────────────
// Runs once at startup. Initialises serial communication,
// configures pin modes, and pre-fills the filter buffer so
// the first readings are not distorted by zeros in the average.
void setup() {
Serial.begin(115200); // Start serial at 115200 baud — must match Serial Plotter
pinMode(LO_PLUS, INPUT); // LO+ is a digital output from AD8232, read as input here
pinMode(LO_MINUS, INPUT); // LO- same — goes HIGH if an electrode is not on the skin
// Pre-fill the filter buffer with real ADC readings
// so smoothedRead() gives a valid average from the very first call
for (int i = 0; i < FILTER_SIZE; i++) {
filterBuffer[i] = analogRead(ECG_PIN);
filterSum += filterBuffer[i];
}
}
// ── loop() ───────────────────────────────────────────────────
// Runs continuously. Each iteration reads one ECG sample and
// sends it to Serial Plotter. The 4ms delay sets the sample
// rate to ~250 Hz — fast enough to capture the sharp QRS spike
// clearly without overwhelming the serial buffer.
void loop() {
// ── Leads-off check ────────────────────────────────────────
// If either electrode is not making skin contact, the AD8232
// pulls LO+ or LO- HIGH to signal the problem.
// We print the ADC midpoint (2048) so the Serial Plotter
// stays centred on screen rather than jumping to zero.
if (digitalRead(LO_PLUS) == HIGH ||
digitalRead(LO_MINUS) == HIGH) {
Serial.println(2048); // Flat line at midpoint while electrodes are off
delayMicroseconds(4000); // Maintain consistent timing even when not reading
return; // Skip the rest of the loop
}
// ── Read and transmit ECG sample ───────────────────────────
// smoothedRead() returns the filtered ADC value (0–4095).
// Serial.println() sends it as a number followed by newline,
// which is exactly the format Serial Plotter expects to draw
// one point on the graph per line.
int ecg = smoothedRead();
Serial.println(ecg);
// ── Timing ─────────────────────────────────────────────────
// delayMicroseconds(4000) = 4 ms per sample = 250 Hz.
// More precise than delay(4) which has millisecond rounding.
// 250 Hz is well above the ~150 Hz bandwidth needed for ECG.
delayMicroseconds(4000);
}
The signal is visible now. Arduino Serial Plotter auto-scales continuously and cannot lock the Y axis, making ECG baselines unstable and the waveform unreadable for documentation and visual.
The real solution for a medical-looking display is to stop using Arduino Serial Plotter and use a Python script instead. Python can draw a proper scrolling ECG trace with a dark background, fixed scale, and grid — exactly like a medical monitor.
The Python visualiser using pyserial and matplotlib fixes the Y range, draws a clinical-style grid, and applies an additional smoothing pass — producing a stable, presentation-quality ECG trace from the same hardware.
Installing packages
pip3 install pyserial matplotlib
Ai request:
Write a Python visualiser for live ECG data from serial port — dark background, green trace, fixed Y axis, ECG paper grid, using pyserial and matplotlib.
# ─────────────────────────────────────────────────────────────
# Seebscribe — ECG Medical Monitor Visualiser
# Hardware: XIAO ESP32-C3 + AD8232
#
# INSTALL DEPENDENCIES (run once in Terminal):
# pip3 install pyserial matplotlib
#
# BEFORE RUNNING:
# 1. Upload the Arduino ECG code to your XIAO ESP32-C3
# 2. Close Arduino Serial Plotter (only one app can use serial at a time)
# 3. Change PORT below to match your board's port
# Mac: /dev/cu.usbmodem1101 (check Arduino IDE → Tools → Port)
# Windows: COM3 (or COM4, COM5 — check Device Manager)
# Linux: /dev/ttyUSB0
#
# RUN:
# python3 ecg_monitor.py
# ─────────────────────────────────────────────────────────────
import serial
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from collections import deque
import sys
# ── Configuration ─────────────────────────────────────────────
# !! CHANGE THIS to your actual port !!
PORT = "/dev/cu.usbmodem1101"
BAUD = 115200 # must match Serial.begin() in your Arduino code
# Number of samples visible on screen at once.
# At 125 Hz (8ms interval in Arduino):
# 500 samples = ~4 seconds visible
# 750 samples = ~6 seconds visible
# 1000 samples = ~8 seconds visible
WINDOW = 500
# Y axis range — based on your screenshots signal sits ~2100-2500.
# Adjust if signal is clipping at top or too small to see.
Y_MIN = 1600
Y_MAX = 2900
# ── Serial connection ─────────────────────────────────────────
try:
ser = serial.Serial(PORT, BAUD, timeout=1)
print(f"Connected: {PORT} at {BAUD} baud")
print("Close Arduino Serial Plotter before running this script.")
print("Press Ctrl+C or close the window to stop.")
except Exception as e:
print(f"\nERROR: Could not open port {PORT}")
print(f"Reason: {e}")
print("\nFix: Change the PORT variable at the top of this file.")
print(" Check Arduino IDE → Tools → Port for the correct port name.")
sys.exit(1)
# ── Data buffer ───────────────────────────────────────────────
# deque automatically drops oldest values as new ones arrive.
# Initialised with midpoint value so graph starts flat.
data = deque([2300] * WINDOW, maxlen=WINDOW)
# ── Plot setup — dark medical monitor style ───────────────────
fig, ax = plt.subplots(figsize=(14, 5))
fig.patch.set_facecolor('#0a0a0a')
ax.set_facecolor('#0a0a0a')
# Classic green ECG trace
line, = ax.plot(list(data), color='#00ff66', linewidth=1.4, antialiased=True)
# Fixed Y axis — prevents auto-scale jumping
ax.set_ylim(Y_MIN, Y_MAX)
ax.set_xlim(0, WINDOW)
# Major grid — large squares like ECG paper (every 50 samples)
ax.grid(True,
which='major',
color='#1f3d1f',
linewidth=0.8,
linestyle='-')
# Minor grid — small squares like ECG paper (every 10 samples)
ax.minorticks_on()
ax.grid(True,
which='minor',
color='#121f12',
linewidth=0.4,
linestyle='-')
ax.set_xticks(range(0, WINDOW + 1, 50))
ax.set_yticks(range(Y_MIN, Y_MAX + 1, 50))
# Title and labels
ax.set_title('Seebscribe My Heartbeat · Live ECG',
color='#00cc44',
fontsize=12,
fontweight='bold',
pad=12)
ax.set_xlabel(f'Samples (window = {WINDOW} samples)',
color='#336633', fontsize=8)
ax.set_ylabel('ADC value (0–4095)',
color='#336633', fontsize=8)
ax.tick_params(colors='#2d5c2d', labelsize=7)
for spine in ax.spines.values():
spine.set_edgecolor('#1f3d1f')
# Status text — shows connection info bottom left
status = ax.text(10, Y_MIN + 20,
f'Port: {PORT} | {BAUD} baud | Seebscribe v.1',
color='#2d5c2d', fontsize=7)
plt.tight_layout()
# ── Animation update function ─────────────────────────────────
# Called by matplotlib every few milliseconds.
# Reads all available serial lines, parses integers,
# adds valid ECG values to the scrolling buffer.
def update(frame):
"""Read new serial data and update the ECG trace."""
# Read all lines currently waiting in the serial buffer.
# in_waiting = number of bytes ready to read right now.
# We loop so we don't fall behind if data arrives in bursts.
while ser.in_waiting > 0:
try:
raw_line = ser.readline().decode('utf-8', errors='ignore').strip()
# Handle both single-value and comma-separated formats:
# "2350" — from the single-value Arduino sketch
# "2100,2350" — from the dual-value Arduino sketch (take second value)
if ',' in raw_line:
parts = raw_line.split(',')
val = int(parts[-1]) # take the ECG value (last column)
else:
val = int(raw_line)
# Only accept values in the valid 12-bit ADC range
if 0 <= val <= 4095:
data.append(val)
except (ValueError, UnicodeDecodeError):
pass # skip malformed or empty lines silently
line.set_ydata(list(data))
return line,
# ── Start animation ───────────────────────────────────────────
# interval=8 means redraw every 8ms — matches 125 Hz Arduino rate.
# blit=True only redraws the changed line, not the whole figure.
# cache_frame_data=False prevents memory growth during long sessions.
ani = animation.FuncAnimation(
fig,
update,
interval=8,
blit=True,
cache_frame_data=False
)
# ── Cleanup on close ─────────────────────────────────────────
def on_close(event):
"""Close serial port cleanly when window is closed."""
try:
ser.close()
print("Serial port closed.")
except:
pass
fig.canvas.mpl_connect('close_event', on_close)
plt.show()
The result looks beautiful.
Next, update the interface.
Upgrade the ECG Python visualiser to add a live dashboard below the trace showing: real-time BPM calculated from R-peak detection, RR interval in milliseconds, HRV using RMSSD, rhythm pattern classification (normal, tachycardia, bradycardia, irregular), signal quality indicator, beat counter, and session timer. Include medical reference ranges on screen. Use matplotlib gridspec for the layout.
If some problem appears with displaying information, I think the recognition is not correct about my actual situation because my hardware is only 13 bits per minute and I have bradycardia. I just need to tune it.
After a series of interactions and adjustments I realized that I need a library which can process a signal from HeartBeat better.
NeuroKit2 — the best single library for current use case. Handles ECG peak detection, HRV analysis, signal quality assessment, and waveform feature extraction all in one package.
pip3 install neurokit2
BioSPPy — older but simpler, good for learning
pip3 install biosppy
HeartPy — specifically designed for wearable/PPG but works for noisy ECG too:
pip3 install heartpy
SciPy — lower level, but gives you the DSP building blocks (bandpass filter, Butterworth filter, wavelet transform) if you want to build your own pipeline:
pip3 install scipy
After using many different libraries, the accuracy became better, but I still realized that it was using different methods of sensing and decided to separate them.
After several hours of experiments with accuracy and troubleshooting with AI, I have this dashboard which can demonstrate different listening methods. Sometimes signal it is the same, but usually it's different.
But still, it's messy and too much information, and now I need to clear the signal. On this stage I decided to switch to Codex and apply more AI in depth on this project. Here are 15 AI requests that I sent to Codex.
Ai requests in CODEX
1. Analyze my existing ECG/heartbeat sensor files, including Python, Arduino, and dashboard files, and explain how the current system works.
2. Review my Fab Academy final project page and connect the ECG heartbeat monitor work to the broader Aura/Seebscribe project concept.
3. List possible improvements for streaming heartbeat data, signal accuracy, interface structure, and project organization before implementation.
4. Create a cleaner browser-first ECG streaming service, improve signal streaming, test the interface, check basic detection accuracy, and create new files without overwriting the old work.
5. Check whether the current XIAO ESP32-C3 Arduino sketch is compatible with the new Python ECG streamer, and provide a new firmware file only if needed.
6. Diagnose why the dashboard shows live ECG signal but does not reliably show BPM, RR, RMSSD, or beat count.
7. Look at the live ECG screenshot and explain whether the signal looks normal, what the dashboard is showing, and how the ECG-to-BPM pipeline works.
8. Update the system to better diagnose AD8232 lead-off pins after correcting LO+ and LO- wiring.
9. Create an Arduino diagnostic sketch for XIAO ESP32-C3 + AD8232 that streams raw ECG plus LO+ and LO- states.
10. Improve beat detection so noisy peaks are rejected, unstable BPM is marked as tentative, and HRV is only shown when enough clean RR intervals exist.
11. Evaluate whether this DIY AD8232 ECG monitor can reach medical-type accuracy, and explain the gap between project-grade monitoring and validated medical ECG systems.
12. Create a Phase 1 snapshot folder containing all related Arduino, Python, dashboard, README, and archive files for documentation and future reference.
13. List the minimum required files for running Phase 1: the Arduino firmware to upload and the Python service to run.
14. Read the Edge Impulse HR/HRV article and translate the useful cardio concepts into practical next steps for this ECG project.
15. Summarize all AI collaboration requests that led to the current Phase 1 ECG heartbeat monitor stage.
Finally I got a very beautiful page with very precise accuracy. I can use my Xiaomi Mi Band 8 to compare it with my real touch on my chest to send the heartbeat and everything is accurate. Everything is the same, which is great news.