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:
Wiring setup:
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
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();
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();
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();
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
Pad of index/middle finger. Light touch—don't squeeze. Keep still.
Clip gently. Often the cleanest signal—less muscle interference.
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.
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:
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.
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>
- 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
Troubleshooting
Try the Gold Stabilizer Ring or Sensor Holder & Light Shield.
