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.

  1. Concept 1 — Aura (Space) → click to enter the page A modular lighting controller for atmosphere, mood modes, and scalable light outputs.

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.

  1. Market Research — Heartbeat as Media → click to enter the page Existing heartbeat devices, biometric art projects, output directions, customer groups, and Seebscribe positioning.

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.

Second sketch idea: wearable device for reading and transmitting heartbeat signals
Early heartbeat sensing and transmission concept sketch.

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.

Hand-drawn Seebscribe concept preview sketch
Early Seebscribe concept preview: wearable heartbeat sender, receivers, display, and outputs.

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

V2 — More Wearable Sensor Version

V3 — Refined Wearable Prototype

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.

Seebscribe My Heartbeat system diagram
System diagram for Seebscribe My Heartbeat.
Gemini-generated Seebscribe visual based on the system diagram
Visual generated with Gemini based on the Seebscribe system diagram.

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
  • Wire AD8232 to XIAO ESP32-C3 on breadboard (3 pins: OUT, LO+, LO−)
  • Connect Ag/AgCl gel electrodes to AD8232 via snap connectors
  • Plot raw ADC output over USB serial — confirm visible QRS peaks
  • Wire MAX30102 to ESP32-C3 via I²C (SDA/SCL)
  • Confirm BPM output from MAX30102 using SparkFun library
  • Compare ECG and PPG signals side by side on serial plotter
Complete May 3, 2026
Firmware
  • Write R-peak detection algorithm on ESP32-C3
  • Calculate RR intervals in milliseconds from peak timing
  • Implement BLE Heart Rate Service profile (standard GATT)
  • Stream BPM + RR intervals over BLE to phone/laptop
  • Test BLE reception stability over 10+ minutes
Planned May 7, 2026
PCB Design — Yaroboard v2
  • Design schematic in EasyEDA Pro: AD8232 + ESP32-C3 + snap connectors + battery
  • Route PCB — single layer, FR1, suitable for Lunyee 3018 Pro milling
  • Export Gerber + drill files, check trace widths (min 0.4mm)
  • Mill Yaroboard v2
  • Solder components and test
Planned May 10, 2026
First Output
  • Build a simple BLE receiver (second ESP32-C3 or laptop)
  • Drive a single LED or LED strip that pulses with each received heartbeat
  • Demonstrate end-to-end: chest electrodes → BLE → light pulse
Planned May 12, 2026
Documentation
  • Document signal validation with photos and waveform screenshots
  • Document Yaroboard v2 PCB design files and mill process
  • Update final project page with Phase 1 results
Planned May 13, 2026

Phase 2 — Precision & Wearability

Work area Tasks Status Estimated due time
Sensing Extension
  • Design wrist satellite PCB — single snap connector + 3.5mm audio jack
  • Mill wrist satellite PCB
  • Source shielded cable (audio cable, ~80cm) for chest-to-wrist connection
  • Test Lead I configuration: chest electrode + wrist electrode
  • Compare Lead I signal quality vs chest-only — document difference
Planned May 17, 2026
MCU Upgrade
  • Purchase XIAO nRF52840
  • Confirm XIAO nRF52840 fits same PCB footprint as ESP32-C3 on Yaroboard v2
  • Port firmware from ESP32-C3 to nRF52840 (Arduino IDE + Seeed BSP)
  • Implement DSP bandpass filter (0.5–150 Hz) using nRF52840 hardware FPU
  • Measure and document RR interval accuracy vs Phase 1
Planned May 20, 2026
Battery & Wearability
  • Connect LiPo battery to XIAO nRF52840 onboard charger
  • Measure active current draw — calculate expected battery life
  • Design and fabricate a wearable enclosure (chest strap or clip)
  • Test comfort and signal stability during movement
Planned May 24, 2026
Output Development
  • Build heartbeat-driven LED light object (lamp / sculpture form)
  • Program BLE receiver firmware for the light object
  • Test generative sound output (second MCU + speaker or laptop)
  • Document both outputs with video demonstration
Planned May 28, 2026
Documentation
  • Update final project page with Phase 2 hardware and signal results
  • Photograph Yaroboard v3 and wrist satellite PCB
  • Record video of full system: wear → BLE → light pulse
Planned May 31, 2026

Phase 1 — Proof of Signal

Wiring my custom Yaro board and AD8232

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 diagram

XIAO ESP32-C3 pinout reference.

For easy connection I solder Pin header to AD8232 board

Phase 1 documentation image 4
Phase 1 documentation image 5

For the wiring part, this was quite easy because I used jumper wires and I have pin headers.

Phase 1 documentation image 6

Also, when I'm connected to the power and I can touch some electrodes, I can see that the AD8232 boards start blinking

Phase 1 documentation image 7

Next is positioning electrodes to harvest an accurate heartbeat signal. Here is a diagram of how to make it better.

Understating ECG Sensors and How to Program AD8232 ECG Sensor with ...

I also found an article about how to experiment with Arduino and AD8232 board to harvest the heartbeat signal.

https://circuitdigest.com/microcontroller-projects/understanding-ecg-sensor-and-program-ad8232-ecg-sensor-with-arduino-to-diagnose-various-medical-conditions

Connecting electrodes

Phase 1 documentation image 9

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.

Phase 1 documentation image 10

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);
}
Phase 1 documentation image 11

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.

Phase 1 documentation image 12
Raw ECG signal visible in the serial plotter.

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.

Phase 1 documentation image 13
Phase 1 documentation image 14
Phase 1 documentation image 15

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.

Phase 1 documentation image 16

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.

Phase 1 documentation image 17
Dashboard comparison of ECG processing methods.

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.

Phase 1 documentation image 18
Final Phase 1 dashboard test with stable heartbeat reading.

Phase 1 Files