Project tutorial
Heart Rate Monitor (Wearable and Wireless Using ECG)

Heart Rate Monitor (Wearable and Wireless Using ECG) © MIT

A rather convenient device to monitor heart rate while running.

  • 7,471 views
  • 8 comments
  • 29 respects

Components and supplies

uECG device
For actually measuring BPM. It can send data via nRF24-compatible protocol
×1
Ard nano
Arduino Nano R3
×1
nRF24 Module (Generic)
Any module would work here. Required to receive BPM data from uECG
×1
1586 00
Adafruit NeoPixel Ring: WS2812 5050 RGB LED
×1
LiPo battery
Any battery with 200+ mAh capacity
×1

Necessary tools and machines

09507 01
Soldering iron (generic)

About this project

This is a second iteration of my heart monitoring project, previous one was showing heart beats on a chest, and was connected to uECG via wire. That looks cool, but isn't practical at all - you can't see well how many LEDs exactly are currently on, it's out of your normal view field, and wire connecting it to uECG device creates a lot of problems for the sensor itself, so it basically doesn't work when you run.

This version solves all these problems: it is wrist-worn, so you can see it while keeping eyes on the road, and it's wireless, so no distortion of readings, it really works for running and allows you to keep track on heart load.

1. Components

Same as in previous project, all hard work is done by uECG - it measures data and calculates BPM on-board. But also, when it's switched into direct link mode, it sends all this information (together with high resolution ECG data, which we are not using here) via radio protocol that is compatible with generic nRF24 chip. So second critical component is nRF24 module. And Arduino Nano has just the right size to fit underneath a small LED ring, so I'm using it as a controller (but really anything would work just as well here).

2. Schematics

Connecting nRF24 module isn't simple, you have to connect all SPI wires (MISO, MOSI, SCK, CS), also chip enable wire, and power supply. And if you want it in a reasonably small size - all pin headers have to be removed, and wires soldered directly to pads. So connecting nRF alone takes 7 wires, 14 soldering points. The good news is that everything else is simple: LED ring requires 1 data wire and 2 power wires, and another 2 power wires go into battery connector.

Connection list is as follows:
nRF24 pin 1 (GND) - Arduino's GND
nRF24 pin 2 (Vcc) - Arduino's 3.3v
nRF24 pin 3 (Chip Enable) - Arduino's D9
nRF24 pin 4 (SPI:CS) - Arduino's D8
nRF24 pin 5 (SPI:SCK) - Arduino's D13
nRF24 pin 6 (SPI:MOSI) - Arduino's D11
nRF24 pin 7 (SPI:MISO) - Arduino's D12
LED ring Power - Arduino's 5V
LED ring GND - Arduino's GND
LED ring DI - Arduino's D5
Battery positive (red) - Arduino's 5V
Battery negative (black) - Arduino's GND
(note that battery requires connector, so it could be disconnected and charged)

Important note: you can't connect MOSI, MISO, SCK wires to any other Arduino pins. SPI hardware sits on D11, D12, D13 and won't work if not connected there. All other pins can be changed (if you'll make corresponding changes in the program).

3. Program

The only complicated thing about software here is RF channel configuration. I spent quite a while trying to make it work before I realized than uECG and nRF24 use different bit order for pipe address. When I fixed that, everything started to work immediately :) Basically we just read incoming packets, use their 5th byte as BPM, and filter it (RF channel is noisy, so every now and then you get random value instead of correct reading, and hardware CRC is disabled for compatibility reasons). After that, BPM is converted into color and number of active pixels, and that's it.

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
 #include <avr/power.h>
#endif
#include <SPI.h>
#include <RF24.h>
#include <RF24_config.h>
#include <nRF24L01.h>
int rf_cen = 9; //nRF24 chip enable pin
int rf_cs = 8; //nRF24 CS pin
RF24 rf(rf_cen, rf_cs);
//pipe address - hardcoded on uECG side
uint8_t pipe_rx[8] = {0x0E, 0xE6, 0x0D, 0xA7, 0, 0, 0, 0};
// Which pin on the Arduino is connected to the NeoPixels?
#define PIN            5
// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS      16
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
uint8_t  swapbits(uint8_t a){ //uECG pipe address uses swapped bits order
 // reverse the bit order in a single byte
   uint8_t v = 0;
   if(a & 0x80) v |= 0x01;
   if(a & 0x40) v |= 0x02;
   if(a & 0x20) v |= 0x04;
   if(a & 0x10) v |= 0x08;
   if(a & 0x08) v |= 0x10;
   if(a & 0x04) v |= 0x20;
   if(a & 0x02) v |= 0x40;
   if(a & 0x01) v |= 0x80;
   return v;
}
void setup() {
 pixels.begin(); // This initializes the NeoPixel library.
 for(int i=0;i<NUMPIXELS;i++){
    pixels.setPixelColor(i, pixels.Color(1,1,1));
 }
 pixels.show();
 //nRF24 requires relatively slow SPI, probably would work at 2MHz too
 SPI.begin();
 SPI.setBitOrder(MSBFIRST);
 SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
 for(int x = 0; x < 8; x++) //nRF24 and uECG have different bit order for pipe address
   pipe_rx[x] = swapbits(pipe_rx[x]);
 //configure radio parameters
 rf.begin();
 rf.setDataRate(RF24_1MBPS);
 rf.setAddressWidth(4);
 rf.setChannel(22);
 rf.setRetries(0, 0);
 rf.setAutoAck(0);
 rf.disableDynamicPayloads();
 rf.setPayloadSize(32);
 rf.openReadingPipe(0, pipe_rx);
 rf.setCRCLength(RF24_CRC_DISABLED);
 rf.disableCRC();
 rf.startListening(); //listen for uECG data
 //Note that uECG should be switched into raw data mode (via long button press)
 //in order to send compatible packets, by default it sends data in BLE mode
 //which cannot be received by nRF24
}
long last_pix_upd = 0;
byte in_pack[32];
int rf_bpm = 0;
int bpm_hist[5]; //since we disabled CRC, need to filter incoming data
void loop() 
{
 if(rf.available())
 {
   rf.read(in_pack, 32);
   int bb = in_pack[5]; //BPM is located at the 5th byte of the packet
   //detailed packet structure is in uECG docs
   //since we have no CRC for compatibility reasons, we need to filter
   //incoming data, radio channel could be noisy. We compare last 5
   //received BPM values and use one only if all 5 were the same.
   //Since uECG sends about 100 packets per second, it won't cause
   //any noticeable delay in displaying data
   for(int n = 0; n < 5-1; n++) //shift bpm history array by 1
     bpm_hist[n] = bpm_hist[n+1];
   bpm_hist[4] = bb; //add new bpm value
   for(int n = 0; n < 5; n++) //check if all are equal
     if(bpm_hist[n] != bb) bb = -1;
   if(bb > 0) //if yes - store it as new received BPM
     rf_bpm = bb;
 }  
 long ms = millis();
 if(ms - last_pix_upd > 10) //don't update pixels too often
 {
   int r, g, b;
   last_pix_upd = ms;
   int bpm = rf_bpm;
   int max_bright = 160; //value of maximum brightness, max 255. But you don't always want it at max :)
   float dd = 25; //change in BPM between color tones (blue->green->yellow->pink->red)
   float t1 = 90, t2, t3, t4; //t1 - "base" BPM, lower than t1 would be blue
   t2 = t1 + dd;
   t3 = t2 + dd;
   t4 = t3 + dd;
   //code for changing color depending in which t1...t4 range we are now
   if(bpm < t1){ r = 0; g = 0; b = max_bright; }
   else if(bpm < t2) { r = 0; g = max_bright * (bpm-t1)/dd; b = max_bright - g; }
   else if(bpm < t3) { r = max_bright * (bpm-t2)/dd; g = max_bright - r; b = r/4; }
   else if(bpm < t4) { r = max_bright; g = 0; b = max_bright/2 - max_bright * (bpm-t3)/(2*dd); }
   else {r = max_bright; g = 0; b = 0; }
   int on_pixels = (bpm-80)/8; //since it's intended for running, I'm not
   //showing anything less than 80 BPM, this way it's more sensitive in
   //high load area
   for(int i=0;i<NUMPIXELS;i++)
   {
     //pixels are set from last to first for no particular reason, would
     //work just as fine if set from first to last
     if(i < on_pixels) pixels.setPixelColor(NUMPIXELS-i-1, pixels.Color(r,g,b));
     else pixels.setPixelColor(NUMPIXELS-i-1, pixels.Color(0,0,0)); //turn off all other LEDs
   }
   pixels.show();
 }
}

4. Wristband Assembly

When all wires are soldered, program is flashed, and you confirmed that uECG data is received - it's time to get it all together.

I've chosen a very simple way to hold it all together - thermal glue. Since parts themselves are almost fitting already (Nano fits outer ring size, nRF24 module fits internal ring size, and battery, while not fitting any part, somehow doesn't get much in the way - not sure how it works, but I just glued it there and somehow it was really ok :)Then I sewed it to a some random wristband I had at hand (leftover from soldering station pack, a band that is used for grounding while soldering), and that's it!

5. Testing

For testing, I went for a run, and it worked just fine except for one surprise. I've used such settings that at 192 BPM all LEDs were on, since by all recommendations such heart rate is too high for my parameters. The surprise was that I've exceeded it in just a few minutes of running, without even noticing that. I even thought that it could be sensor error, but no - when I stopped, it didn't immediately went down, instead there was a slow relaxation (sensor is 100% reliable when there isn't much motion). So it turns out that for a while I'm training well above my healthy threshold (at least what is supposed to be healthy for a standard adult of my age/weight). It's interesting: I'm quite into (amateur) sports since childhood, but I had problems with heart in my teens and they seemed to go away over time. But I know from experience that any load higher than a fast walk was really hard for me, yet I kept training - and that increased my limit over time, to the point that now I consider myself quite well fit. And now I have a question - is my BPM just higher than normal due to those heart problems in adolescence, or I'm really pushing too hard without realizing that? Anyway I'll have to do something with it - either increase max BPM on the monitor, or train less intensively. :)


P.S. surprisingly, uECG performed very well as an EMG sensor - you can read about it in my Robotic Hand Control project

Code

bpm_watch.inoArduino
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
  #include <avr/power.h>
#endif

#include <SPI.h>
#include <RF24.h>
#include <RF24_config.h>
#include <nRF24L01.h>

int rf_cen = 9; //nRF24 chip enable pin
int rf_cs = 8; //nRF24 CS pin

RF24 rf(rf_cen, rf_cs);
//pipe address - hardcoded on uECG side
uint8_t pipe_rx[8] = {0x0E, 0xE6, 0x0D, 0xA7, 0, 0, 0, 0};

// Which pin on the Arduino is connected to the NeoPixels?
#define PIN            5
// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS      16

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

uint8_t  swapbits(uint8_t a){ //uECG pipe address uses swapped bits order
  // reverse the bit order in a single byte
    uint8_t v = 0;
    if(a & 0x80) v |= 0x01;
    if(a & 0x40) v |= 0x02;
    if(a & 0x20) v |= 0x04;
    if(a & 0x10) v |= 0x08;
    if(a & 0x08) v |= 0x10;
    if(a & 0x04) v |= 0x20;
    if(a & 0x02) v |= 0x40;
    if(a & 0x01) v |= 0x80;
    return v;
}

void setup() {
  pixels.begin(); // This initializes the NeoPixel library.

  for(int i=0;i<NUMPIXELS;i++){
     pixels.setPixelColor(i, pixels.Color(1,1,1));
  }
  pixels.show();

  //nRF24 requires relatively slow SPI, probably would work at 2MHz too
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));

  for(int x = 0; x < 8; x++) //nRF24 and uECG have different bit order for pipe address
    pipe_rx[x] = swapbits(pipe_rx[x]);

  //configure radio parameters
  rf.begin();
  rf.setDataRate(RF24_1MBPS);
  rf.setAddressWidth(4);
  rf.setChannel(22);
  rf.setRetries(0, 0);
  rf.setAutoAck(0);
  rf.disableDynamicPayloads();
  rf.setPayloadSize(32);
  rf.openReadingPipe(0, pipe_rx);
  rf.setCRCLength(RF24_CRC_DISABLED);
  rf.disableCRC();
  rf.startListening(); //listen for uECG data
  //Note that uECG should be switched into raw data mode (via long button press)
  //in order to send compatible packets, by default it sends data in BLE mode
  //which cannot be received by nRF24
}

long last_pix_upd = 0;
byte in_pack[32];
int rf_bpm = 0;
int bpm_hist[5]; //since we disabled CRC, need to filter incoming data

void loop() 
{
  if(rf.available())
  {
    rf.read(in_pack, 32);
    int bb = in_pack[5]; //BPM is located at the 5th byte of the packet
    //detailed packet structure is in uECG docs

    //since we have no CRC for compatibility reasons, we need to filter
    //incoming data, radio channel could be noisy. We compare last 5
    //received BPM values and use one only if all 5 were the same.
    //Since uECG sends about 100 packets per second, it won't cause
    //any noticeable delay in displaying data
    for(int n = 0; n < 5-1; n++) //shift bpm history array by 1
      bpm_hist[n] = bpm_hist[n+1];
    bpm_hist[4] = bb; //add new bpm value
    for(int n = 0; n < 5; n++) //check if all are equal
      if(bpm_hist[n] != bb) bb = -1;
    if(bb > 0) //if yes - store it as new received BPM
      rf_bpm = bb;
  }  
  long ms = millis();
  if(ms - last_pix_upd > 10) //don't update pixels too often
  {
    int r, g, b;
    last_pix_upd = ms;
    int bpm = rf_bpm;
    int max_bright = 160; //value of maximum brightness, max 255. But you don't always want it at max :)
    float dd = 25; //change in BPM between color tones (blue->green->yellow->pink->red)
    float t1 = 90, t2, t3, t4; //t1 - "base" BPM, lower than t1 would be blue
    t2 = t1 + dd;
    t3 = t2 + dd;
    t4 = t3 + dd;
    //code for changing color depending in which t1...t4 range we are now
    if(bpm < t1){ r = 0; g = 0; b = max_bright; }
    else if(bpm < t2) { r = 0; g = max_bright * (bpm-t1)/dd; b = max_bright - g; }
    else if(bpm < t3) { r = max_bright * (bpm-t2)/dd; g = max_bright - r; b = r/4; }
    else if(bpm < t4) { r = max_bright; g = 0; b = max_bright/2 - max_bright * (bpm-t3)/(2*dd); }
    else {r = max_bright; g = 0; b = 0; }
    int on_pixels = (bpm-80)/8; //since it's intended for running, I'm not
    //showing anything less than 80 BPM, this way it's more sensitive in
    //high load area
    for(int i=0;i<NUMPIXELS;i++)
    {
      //pixels are set from last to first for no particular reason, would
      //work just as fine if set from first to last
      if(i < on_pixels) pixels.setPixelColor(NUMPIXELS-i-1, pixels.Color(r,g,b));
      else pixels.setPixelColor(NUMPIXELS-i-1, pixels.Color(0,0,0)); //turn off all other LEDs
    }
    pixels.show();
  }
}

Schematics

Untitled file

No document.

nrf24_led_ring_o2Gij5oigT.fzz
nrf24_led_ring_o2Gij5oigT.fzz

Comments

Similar projects you might like

Heart Beat Indicator Using ECG

Project tutorial by Dmitry Dziuba

  • 2,884 views
  • 2 comments
  • 20 respects

MAX 30102 Heart Rate Monitor on 16x2 LCD

Project tutorial by Mirko Pavleski

  • 2,968 views
  • 4 comments
  • 1 respect

Heart Rate Monitor Using IoT

Project tutorial by Team Technopaths

  • 17,716 views
  • 3 comments
  • 31 respects

Heart Attack with LED

Project tutorial by TheTNR

  • 967 views
  • 0 comments
  • 5 respects

Heart Rate Monitoring System

Project tutorial by Team HRMS

  • 20,136 views
  • 5 comments
  • 34 respects

Amazon Alexa-Powered Wireless Speakers

Project tutorial by WebGeeks

  • 3,117 views
  • 1 comment
  • 14 respects
Add projectSign up / Login