PulseSensor and Webserial

Try it now: Connect your PulseSensor to an Arduino/ESP32, upload the sketch below, then click Connect to see your live heartbeat in the browser. 

No WiFi or software needed—just USB and Chrome/Edge/Brave. Scroll down for setup instructions.

When it's working, you'll see something like this:

PulseSensor WebSerial working

Wiring setup:

PulseSensor wiring to XIAO ESP32-S3

Signal → A0   + → 3.3V   − → GND

Understanding What You're Seeing

If you've never seen a pulse waveform before, here's what each part means—and how to use these values in your own code.

The Waveform

Each peak is one heartbeat. The sensor shines light through your skin and measures how much bounces back—when your heart pumps blood, the amount of light changes. That's what creates the wave pattern. A healthy resting heart beats about once per second, so you should see roughly 60-80 peaks per minute.

The Four Values

Signal (0–4095)

The raw light reading from the sensor, updated 50 times per second. This is what draws the waveform. Higher numbers = more light reflected = less blood in that moment.

int signal = pulseSensor.getLatestSample();
BPM (beats per minute)

Your heart rate, calculated automatically by the library. It averages the last 10 beats for stability. Normal resting: 60-100. Shows 0 until the algorithm detects a clear pattern (takes 5-10 seconds).

int bpm = pulseSensor.getBeatsPerMinute();
IBI (inter-beat interval, milliseconds)

Time between heartbeats in milliseconds. At 60 BPM, IBI ≈ 1000ms. At 80 BPM, IBI ≈ 750ms. Useful for heart rate variability (HRV) projects—the variation in IBI reveals stress levels and nervous system activity.

int ibi = pulseSensor.getInterBeatIntervalMs();
Beat (0 or 1)

Flashes to 1 for one sample when a heartbeat is detected, then back to 0. This is your trigger—use it to flash an LED, play a sound, or sync anything to the heartbeat. The heart icon animates when this fires.

if (pulseSensor.sawStartOfBeat()) { /* do something */ }

How to Wear the Sensor

Fingertip

Pad of index/middle finger. Light touch—don't squeeze. Keep still.

Earlobe

Clip gently. Often the cleanest signal—less muscle interference.

CONCEPT: WEBSERIAL

WebSerial gives your browser direct USB access—no software to install. The Arduino/ESP32 sends signal,bpm,ibi,beat data 50 times per second, and the browser's JavaScript receives it instantly to update the chart. This is why the waveform animates smoothly—there's no page refresh, just a constant data stream.

Setup

Two files work together: Arduino/ESP32 sends CSV over USB, browser parses and displays.

Arduino/ESP32 reads sensor → sends CSV over USB → Browser parses & displays
signal,bpm,ibi,beat → "1847,72,833,1"

Step 1: Upload Arduino Sketch

Install the PulseSensor Playground library (Arduino IDE → Sketch → Include Library → Manage Libraries → search "PulseSensor"). Then upload this:

PulseSensor_WebSerial.ino
/*
 * PulseSensor WebSerial
 * Sends: signal,bpm,ibi,beat over USB @ 115200 baud
 * By: World Famous Electronics | MIT License
 */

#include <PulseSensorPlayground.h>

PulseSensorPlayground pulseSensor;

// CHANGE FOR YOUR BOARD:
// Arduino Uno/Nano/Mega: A0
// XIAO ESP32-S3: 1
// ESP32 DevKit: 34
const int PULSE_PIN = 1;

void setup() {
  Serial.begin(115200);
  delay(1000);
  pulseSensor.analogInput(PULSE_PIN);
  pulseSensor.setThreshold(550);
  pulseSensor.begin();
}

void loop() {
  int signal = pulseSensor.getLatestSample();
  int bpm = pulseSensor.getBeatsPerMinute();
  int ibi = pulseSensor.getInterBeatIntervalMs();
  bool beat = pulseSensor.sawStartOfBeat();
  
  Serial.print(signal); Serial.print(",");
  Serial.print(bpm);    Serial.print(",");
  Serial.print(ibi);    Serial.print(",");
  Serial.println(beat ? 1 : 0);
  
  delay(20);
}

Step 2: Click Connect

Scroll back up to the dashboard and click Connect. Select your board's serial port from the popup. You should see your heartbeat within seconds.

Connect button and connection status
Optional: Run Offline

Want your own copy? Save the HTML file below as PulseSensor_WebSerial.html and open it in Chrome/Edge/Brave.

Show HTML code
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>PulseSensor WebSerial</title>
  <style>
    body { font-family: sans-serif; background: #111; color: white; padding: 20px; }
    .container { max-width: 800px; margin: 0 auto; }
    button { padding: 8px 16px; background: #ff6b6b; color: white; border: none; border-radius: 6px; cursor: pointer; }
    canvas { width: 100%; height: 180px; background: #222; border-radius: 6px; }
    .bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
    .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #444; }
    .dot.on { background: #51cf66; }
    .vars { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 15px; }
    .var { background: #222; padding: 10px; border-radius: 6px; }
    .var .value { font-size: 1.5em; font-weight: bold; }
    .signal .value { color: #ff6b6b; }
    .bpm .value { color: #44ff44; }
    .ibi .value { color: #ffaa44; }
    .beat .value { color: #64c8ff; }
    .heart { color: #ff6b6b; transition: transform 0.1s; }
    .heart.pulse { transform: scale(1.4); }
    .raw { margin-top: 15px; padding: 10px; background: #222; font-family: monospace; border-radius: 6px; }
  </style>
</head>
<body>
  <div class="container">
    <h1>PulseSensor WebSerial</h1>
    <div class="bar">
      <div><span class="dot" id="dot"></span> <span id="status">Disconnected</span></div>
      <button id="btn">Connect</button>
    </div>
    <canvas id="canvas"></canvas>
    <div class="vars">
      <div class="var signal"><div>Signal</div><div class="value" id="sig">—</div></div>
      <div class="var bpm"><div>BPM</div><div class="value" id="bpm">—</div></div>
      <div class="var ibi"><div>IBI (ms)</div><div class="value" id="ibi">—</div></div>
      <div class="var beat"><div>Beat</div><div class="value"><span id="beat">—</span> <span class="heart" id="heart">♥</span></div></div>
    </div>
    <div class="raw"><div style="color:#666;font-size:0.8em">Raw Serial:</div><div id="raw" style="color:#44ff44">waiting...</div></div>
  </div>
<script>
const BAUD = 115200, LEN = 600;
let hist = new Array(LEN).fill(2048), min = 2048, max = 2048;
let port, reader, on = false;
const $ = id => document.getElementById(id);
const canvas = $('canvas'), ctx = canvas.getContext('2d');
const dpr = devicePixelRatio || 1;

function initCanvas() {
  const r = canvas.getBoundingClientRect();
  canvas.width = r.width * dpr; canvas.height = r.height * dpr;
  ctx.setTransform(1,0,0,1,0,0); ctx.scale(dpr, dpr); draw();
}
addEventListener('load', initCanvas);

$('btn').onclick = async () => {
  if (on) {
    on = false; if (reader) reader.cancel(); if (port) await port.close();
    $('btn').textContent = 'Connect'; $('dot').classList.remove('on'); $('status').textContent = 'Disconnected';
    return;
  }
  try {
    port = await navigator.serial.requestPort();
    await port.open({ baudRate: BAUD });
    on = true; $('btn').textContent = 'Disconnect'; $('dot').classList.add('on'); $('status').textContent = 'Connected';
    const dec = new TextDecoderStream(); port.readable.pipeTo(dec.writable); reader = dec.readable.getReader();
    let buf = '';
    while (on) {
      const { value, done } = await reader.read(); if (done) break;
      buf += value; const lines = buf.split('\n'); buf = lines.pop();
      for (const l of lines) parse(l.trim());
    }
  } catch (e) { alert('Error: ' + e.message); }
};

function parse(line) {
  $('raw').textContent = line;
  const p = line.split(','); if (p.length < 4) return;
  const [sig, bpm, ibi, beat] = p.map(Number);
  $('sig').textContent = sig;
  $('bpm').textContent = bpm > 0 ? bpm : '—';
  $('ibi').textContent = bpm > 0 && ibi > 0 ? ibi : '—';
  $('beat').textContent = beat ? '1' : '0';
  if (beat) { $('heart').classList.add('pulse'); setTimeout(() => $('heart').classList.remove('pulse'), 150); }
  hist.shift(); hist.push(sig);
  min = Math.min(...hist); max = Math.max(...hist); draw();
}

function draw() {
  const w = canvas.width/dpr, h = canvas.height/dpr;
  ctx.fillStyle = '#222'; ctx.fillRect(0, 0, w, h);
  const range = Math.max(max - min, 1), scale = h/2/range;
  ctx.strokeStyle = '#ff6b6b'; ctx.lineWidth = 2; ctx.beginPath();
  for (let i = 0; i < LEN; i++) {
    const x = (i/LEN)*w, y = h/2 - ((hist[i]-min)*scale - range/2*scale);
    i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
  }
  ctx.stroke();
}
draw();
</script>
</body>
</html>
SUCCESS LOOKS LIKE
  • Smooth waveform: Clear peaks matching your heartbeat
  • BPM stabilizes: Settles to 60-100 within 5-10 seconds
  • Heart pulses: Icon animates with each detected beat
  • Raw serial updates: Numbers streaming continuously
Important: Close Arduino's Serial Monitor before clicking Connect—only one app can use the serial port at a time.

Troubleshooting

Problem Solution
Can't connect Close Arduino Serial Monitor first. Use Chrome, Edge, or Brave. Try unplugging/replugging USB.
Flat line / no waveform Check wiring: Signal→A0, +→3.3V, −→GND. Verify pin number in code. Confirm 115200 baud.
Noisy / jagged signal Light pressure—don't squeeze. Try earlobe instead. Keep hand still.
BPM shows 0 or — Wait 5-10 seconds to stabilize. Lower setThreshold() for more sensitivity. Signal must swing clearly.
Signal stuck at 0 or 4095 No contact or bad wiring. Check sensor connection. Try different finger.
For Steadier Readings

Try the Gold Stabilizer Ring or Sensor Holder & Light Shield.