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.
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. SchematicsConnecting 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. ProgramThe 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 AssemblyWhen 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!
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
Comments