DEV Community

wellallyTech
wellallyTech

Posted on

Hack Your Stress: Real-time HRV Analysis with Web Bluetooth and Polar H10

Have you ever wondered if your "flow state" is actually just high-functioning anxiety? Or why your recovery feels sluggish after a late-night coding session? The answer lies in your Heart Rate Variability (HRV).

In this tutorial, we are going to build a high-performance stress monitor using the Web Bluetooth API, RxJS, and Signal Processing techniques. We'll be pulling raw RR-intervals (the time between heartbeats) directly from a Polar H10 chest strap and performing a Fast Fourier Transform (FFT) in the browser to calculate the LF/HF ratioโ€”the gold standard for assessing Autonomic Nervous System (ANS) balance.

Whether you're into biohacking, real-time data visualization, or just want to see how the Web Bluetooth API can turn a browser into a medical-grade dashboard, youโ€™re in the right place.

Pro Tip: If you're looking for more production-ready patterns on integrating wearables with enterprise cloud architectures, definitely check out the deep dives over at WellAlly Tech Blog.


The Architecture: From Heartbeat to Insight

Before we dive into the code, let's look at how the data flows from your chest strap to a real-time stress graph. We use RxJS to handle the asynchronous stream of Bluetooth packets and D3.js to render the results.

graph TD
    A[Polar H10 Chest Strap] -->|Bluetooth LE| B[Web Bluetooth API]
    B -->|Raw Buffer| C[RxJS Data Stream]
    C -->|Parse RR Intervals| D[Signal Processing Pipe]
    D -->|Resampling & FFT| E[LF/HF Ratio Calculation]
    E -->|Stress Metric| F[D3.js Real-time Dashboard]
    F -->|Visual Feedback| G[User]
Enter fullscreen mode Exit fullscreen mode

Prerequisites

To follow along, you'll need:

  • A Polar H10 or any heart rate monitor that supports the standard Heart Rate GATT service (RR-Intervals).
  • A browser with Web Bluetooth support (Chrome, Edge, or Opera).
  • Basic knowledge of RxJS and TypeScript.

Step 1: Connecting to the Heart ๐Ÿ”Œ

The Web Bluetooth API allows us to connect to GATT (Generic Attribute Profile) devices. The Heart Rate service UUID is standardized as 0x180D.

async function connectToHeartRateMonitor() {
  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['heart_rate'] }],
    optionalServices: ['battery_service']
  });

  const server = await device.gatt.connect();
  const service = await server.getPrimaryService('heart_rate');
  const characteristic = await service.getCharacteristic('heart_rate_measurement');

  return characteristic;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Streaming Data with RxJS ๐ŸŒŠ

The data coming from the sensor is a DataView. We need to parse it to extract the RR-intervals (stored in units of 1/1024 seconds for Polar). RxJS is perfect here because heartbeats are discrete events in time.

import { fromEvent, map, scan } from 'rxjs';

function createHeartRateStream(characteristic) {
  return fromEvent(characteristic, 'characteristicvaluechanged').pipe(
    map((event: any) => {
      const value = event.target.value;
      const flags = value.getUint8(0);
      const rate16Bits = flags & 0x1;
      const hasRR = flags & 0x10;

      let offset = rate16Bits ? 3 : 2;
      const rrIntervals = [];

      if (hasRR) {
        for (; offset + 1 < value.byteLength; offset += 2) {
          // Convert to milliseconds
          rrIntervals.push((value.getUint16(offset, true) / 1024) * 1000);
        }
      }
      return rrIntervals;
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Frequency Domain Analysis (The Math bit) ๐Ÿงฎ

To find the "Stress Ratio," we analyze the frequencies within the HRV.

  • Low Frequency (LF): 0.04 - 0.15 Hz (Sympathetic + Parasympathetic activity)
  • High Frequency (HF): 0.15 - 0.40 Hz (Parasympathetic/Vagal tone activity)
  • LF/HF Ratio: High ratio = High Stress.

Since RR-intervals are unevenly spaced, we must resample the data (usually at 4Hz) before applying a Fast Fourier Transform (FFT).

// A simplified conceptual approach to LF/HF calculation
import { fft } from 'some-signal-processing-lib';

function calculateStressMetrics(rrIntervals: number[]) {
  // 1. Resample RR-intervals to a uniform time grid
  const resampled = cubicSplineInterpolation(rrIntervals);

  // 2. Apply Windowing (e.g., Hann Window)
  const windowed = applyHannWindow(resampled);

  // 3. Run FFT
  const spectrum = fft(windowed);

  // 4. Integrate Power in LF and HF bands
  const lfPower = integrate(spectrum, 0.04, 0.15);
  const hfPower = integrate(spectrum, 0.15, 0.40);

  return lfPower / hfPower; // The Stress Ratio!
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Visualizing with D3.js ๐Ÿ“Š

We want to see our stress levels change in real-time. Using D3.js, we can create a rolling line chart that updates every time a new RR-interval is processed.

const margin = { top: 20, right: 20, bottom: 30, left: 50 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;

const svg = d3.select("#chart")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .transform(`translate(${margin.left},${margin.top})`);

// Update function called within the RxJS subscribe block
function updateChart(data) {
  const x = d3.scaleTime().domain(d3.extent(data, d => d.time)).range([0, width]);
  const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value)]).range([height, 0]);

  svg.selectAll(".line")
    .data([data])
    .join("path")
    .attr("class", "line")
    .attr("d", d3.line()
      .x(d => x(d.time))
      .y(d => y(d.value))
    );
}
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns & Deep Dives ๐Ÿš€

Building a prototype is easy, but making it production-ready involves handling Bluetooth disconnections, signal noise filtering, and power-efficient data processing.

If you are interested in medical-grade signal processing or how to scale this architecture to thousands of concurrent users using WebSockets and Cloud Workers, I highly recommend reading the engineering articles at WellAlly Tech Blog. They have some fantastic resources on the intersection of healthcare and cutting-edge web tech.


Conclusion

The Web Bluetooth API has opened up a world where the browser is no longer just a document viewerโ€”it's a powerful interface for the physical world. By combining it with RxJS for stream management and D3.js for visualization, we can build tools that were previously restricted to expensive proprietary software.

Now it's your turn:

  • Try implementing a "Focus Score" based on the LF/HF ratio.
  • Add a vibration alert via the navigator.vibrate API when stress gets too high.
  • Share your results in the comments below! ๐Ÿฅ‘

Happy hacking! ๐Ÿš€

Top comments (0)