❤️ ESP32-S3 · Sketch 004

Live ECG Heartbeat
Visualizer

MAX30102 IR sensor drives a scrolling ECG waveform with BPM readout, full-screen beat flash, animated pixel-art heart, and touch-to-freeze.

Live BPM
🩺 MAX30102 IR Sensor
📺 ST7789 + Sprite Rendering
🤖 Claude Sonnet 4.6
Session Transcript
Building the Heartbeat Visualizer
Step 1 · Request
👤
Richard

use the heart rate sensor. i want a live ECG-style scrolling waveform on the screen with my BPM in big numbers. make it flash red when my heart beats

Claude

The MAX30102 is already on the shared I2C bus (SDA=2, SCL=1). I'll use the SparkFun MAX30105 library which covers it. Architecture:

  • Top 56 px — header panel: BPM in 48px digits (colour-coded), pixel-art heart, LIVE blink dot
  • Bottom 264 px — scrolling ECG waveform rendered into a TFT_eSprite (zero flicker)
  • AC coupling — exponential moving-average DC baseline subtracted so the waveform stays centred regardless of ambient IR
  • Auto-scale — running peak tracker normalises amplitude so weak signals still fill the display
Step 2 · ECG Grid & Waveform
Claude

ECG grid drawn into the sprite before each waveform render: major horizontal lines every 66 px, minor lines every 33 px, vertical grid every 60/20 px — all in near-black green to stay readable without overpowering the trace.

The waveform ring buffer stores 240 float samples (one per screen column). On each frame the oldest column becomes the new write position — creating a natural left-scroll. An 8-pixel "cursor sweep" fade at the leading edge gives it the classic medical monitor look.

// AC-couple the raw IR signal
dcBaseline = dcBaseline * 0.98f + (float)ir * 0.02f;
float ac = (float)ir - dcBaseline;

// Auto-scale: track peak, decay slowly
if (fabsf(ac) > acPeak) acPeak = fabsf(ac);
else                     acPeak *= 0.998f;

float norm = ac / acPeak;  // –1 .. +1
✓ Sprite-based zero-flicker rendering ECG grid in dark green Cursor sweep effect
Step 3 · Beat Detection & Flash
Claude

Beat detection uses the library's checkForBeat() on each IR sample. On a detected beat, a 3-frame flash sequence fires:

FrameHeader BGDuration
1Full red 0xF80055 ms
2Mid red 0x900055 ms
3Fade red 0x300055 ms

BPM is averaged over the last 8 beats. Colour coding: blue <55, green 55–84, yellow 85–99, red ≥100.

The pixel-art heart (11×9 bitmap, 2px scale normally, 3px on beat) pulses visually in sync.

Step 4 · Touch to Freeze
👤
Richard

can i freeze the trace so i can look at a beat pattern?

Claude

Added tap-to-freeze via FT6336U touch. A leading-edge tap toggles frozen — the ring buffer stops advancing (waveform stays still) but the sensor keeps reading. A [ FROZEN ] label appears in the header. Tap again to resume scrolling from the current live position.

FT6336U_TouchPointType tp = ts.scan();
bool touched = (tp.touch_count > 0);
if (touched && !prevTouch) {
    frozen = !frozen;
    drawHeader(0);  // immediate FROZEN label update
}
prevTouch = touched;
✓ Touch freeze works Sensor continues reading while frozen
Step 5 · Flash
Claude

Wrote flash-sketch-004-heartbeat-viz.sh. Additional libraries required: SparkFun MAX3010x (heart rate) and FT6336U (touch). Both installed automatically by the script.

./flash-sketch-004-heartbeat-viz.sh

# Installing SparkFun MAX3010x library...
# Sketch uses 41% of flash, 18% of RAM
# Wrote 425984 bytes at 0x00010000 in 5.4 seconds
# Hash of data verified.
# Hard resetting via RTS pin...
✓ Compiled — 41% flash ✓ Flashed successfully Live ECG on screen Beat flash + animated heart
Session Summary
Claude — What We Built
FileWhat it does
sketches/heartbeat_viz/heartbeat_viz.inoECG waveform, BPM display, beat flash, pixel heart, touch freeze
flash-sketch-004-heartbeat-viz.shFull compile + flash with SparkFun MAX3010x lib install