In my eternal quest for finding possible projects I thought there would be no better way to get into the Lilly-pad (and this is meant to be slightly ironic), than to mess with a slightly finicky hear sensor and even harder to time Adafruit Neopixel rings,
Like the emotionally unstable crazy person that I am, I decided I should find a way to show people what emotion I'm most likely experiencing. I had access to a hear rate sensor and since you can actually mount those as an ear ring, it serves my needs perfectly.
So first of all lets talk about how the pulse sensor works. If we look at the diagram above we an see that we simply have an LED going through a small circuit to a Photo-resistor. The Photo-resistor takes the amount of light present and turns it into an Analog Voltage. When blood is pumped through your finger or ear there is a difference in the amount of light. Through code we can actually get this simple sensor to act as a fairly sensitive heartbeat monitor. Plus, who doesn't want their ears to glow green?!
So next we need to see how we can actually show our emotions from our heart beat. And of course the first thing that comes to mind are individually addressable LED strips, the Adafruit Neopixel rings are perfect for the job!
Important Things to Know About NeoPixels in General
- Not all addressable LEDs are NeoPixels. “NeoPixel” is Adafruit’s brand for individually-addressable RGB color pixels and strips based on the WS2812 and WS2811 LED/drivers, using a single-wire control protocol. Other LED products we carry — WS2801 pixels, LPD8806 and “analog” strips — use different methodologies (and have their own tutorials). When seeking technical support in the forums, a solution can be found more quickly if the correct LED type is mentioned.
- NeoPixels don’t just light up on their own; they require a microcontroller (such as Arduino) and some programming. (Just giving them power will not make them turn on)
- NeoPixels aren’t the answer for every project. The control signal has very strict timing requirements, and some development boards (such as Netduino or Raspberry Pi) can’t reliably achieve this. This is why we continue to offer other LED types; some are more adaptable to certain situations.
The approximate peak power use (all LEDs on at maximum brightness) per meter is:
- 30 LEDs: 9.5 Watts (just under 2 Amps at 5 Volts).
- 60 LEDs: 18 Watts (about 3.6 Amps at 5 Volts).
- 144 LEDs : 35 watts (7 Amps at 5 Volts).
So the plan here is to have the Neopixel Rings light up to the rhythm of your heart. The plan is also to have it change colors if its within certain BPM (beats per minutes) ranges. A possible addition would be to have more and more Neopixels light up as your heart beats faster but this would results in the LEDs
The code will be something similar to this, timing needs to be adjusted however:
/*
>>> Pulse Sensor purple wire goes to Analog Pin 0 by default <<<
Pulse Sensor Power and GND wires go to a Digital Pin set High and to the Ground Plane of the PCB.
Pulse Sensor sample aquisition and processing happens in the background via Timer 2 interrupt. 2mS sample rate.
PWM on pins 3 and 11 will not work when using this code, because we are using Timer 2!
The following variables are automatically updated:
Signal : int that holds the analog signal data straight from the sensor. updated every 2mS.
IBI : int that holds the time interval between beats. 2mS resolution.
BPM : int that holds the heart rate value, derived every beat, from averaging previous 10 IBI values.
QS : boolean that is made true whenever Pulse is found and BPM is updated. User must reset.
Pulse : boolean that is true when a heartbeat is sensed then false in time with pin13 LED going out.
NeoPixel Ring goggles sketch -- Andrei Aldea 2014
Welding goggles using 50mm round lenses can be outfitted with
a pair of Adafruit NeoPixel Rings
By default, pixel #0 (the first LED) on both rings should be at the TOP of
the goggles. Looking at the BACK of the board, pixel #0 is immediately
clockwise from the OUT connection. If a different pixel is at the top,
that's OK, the code can compensate (TOP_LED_FIRST and TOP_LED_SECOND below).
IMPORTANT: To reduce NeoPixel burnout risk, add 1000 uF capacitor across
pixel power leads, add 300 - 500 Ohm resistor on first pixel's data input
and minimize distance between Arduino and first pixel. Avoid connecting
on a live circuit...if you must, connect GND first.
*/
#include <Adafruit_NeoPixel.h>
#ifdef __AVR_ATtiny85__ // Trinket, Gemma or other ATtiny type devices
#include <avr/power.h>
#endif
#define PIN 0
#define TOP_LED_FIRST 0 // Change these if the first pixel is not
#define TOP_LED_SECOND 0 // at the top of the first and/or second ring.
#define EFFECT RAINBOW // Choose a visual effect from the names below
#define RAINBOW 0
#define ECTO 1
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(32, PIN, NEO_GRB + NEO_KHZ800);
const int8_t PROGMEM
yCoord[] = { // Vertical coordinate of each pixel. First pixel is at top.
127,117,90,49,0,-49,-90,-117,-127,-117,-90,-49,0,49,90,117 }
,
sine[] = { // Brightness table for ecto effect
0, 28, 96, 164, 192, 164, 96, 28, 0, 28, 96, 164, 192, 164, 96, 28 };
// Eyelid vertical coordinates. Eyes shut slightly below center.
#define upperLidTop 130
#define upperLidBottom -45
#define lowerLidTop -40
#define lowerLidBottom -130
const uint8_t PROGMEM gamma8[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3,
3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7,
7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12,
13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20,
20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29,
30, 31, 31, 32, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 73, 75,
76, 77, 78, 80, 81, 82, 84, 85, 86, 88, 89, 90, 92, 93, 94, 96,
97, 99,100,102,103,105,106,108,109,111,112,114,115,117,119,120,
122,124,125,127,129,130,132,134,136,137,139,141,143,145,146,148,
150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,
182,184,186,188,191,193,195,197,199,202,204,206,209,211,213,215,
218,220,223,225,227,230,232,235,237,240,242,245,247,250,252,255
};
uint32_t
iColor[16][3]; // Background colors for eyes
int16_t
hue = 0; // Initial hue around perimeter (0-1535)
uint8_t
iBrightness[16], // Brightness map -- eye colors get scaled by these
brightness = 220, // Global brightness (0-255)
blinkFrames = 5, // Speed of current blink
blinkCounter = 30, // Countdown to end of next blink
eyePos = 192, // Current 'resting' eye (pupil) position
newEyePos = 192, // Next eye position when in motion
gazeCounter = 75, // Countdown to next eye movement
gazeFrames = 50; // Duration of eye movement (smaller = faster)
int8_t
eyeMotion = 0; // Distance from prior to new position
// VARIABLES
int pulsePin = 0; // Pulse Sensor purple wire connected to analog pin 0
int blinkPin = 13; // pin to blink led at each beat
int fadePin = 5; // pin to do fancy classy fading blink at each beat
int fadeRate = 0; // used to fade LED on with PWM on fadePin
// these variables are volatile because they are used during the interrupt service routine!
volatile int BPM; // used to hold the pulse rate
volatile int Signal; // holds the incoming raw data
volatile int IBI = 600; // holds the time between beats, must be seeded!
volatile boolean Pulse = false; // true when pulse wave is high, false when it's low
volatile boolean QS = false; // becomes true when Arduoino finds a beat.
//====================================START OF VOID SETUP==============================================
void setup(){
pinMode(blinkPin,OUTPUT); // pin that will blink to your heartbeat!
pinMode(fadePin,OUTPUT); // pin that will fade to your heartbeat!
Serial.begin(115200); // we agree to talk fast!
interruptSetup(); // sets up to read Pulse Sensor signal every 2mS
// UN-COMMENT THE NEXT LINE IF YOU ARE POWERING The Pulse Sensor AT LOW VOLTAGE,
// AND APPLY THAT VOLTAGE TO THE A-REF PIN
//analogReference(EXTERNAL);
#ifdef __AVR_ATtiny85__ // Trinket, Gemma, etc.
if(F_CPU == 16000000) clock_prescale_set(clock_div_1);
// Seed random number generator from an unused analog input:
randomSeed(analogRead(2));
#else
randomSeed(analogRead(A0));
#endif
pixels.begin();
}
//====================================START OF VOID LOOP===============================================
void loop(){
if (QS == true){ // Quantified Self flag is true when arduino finds a heartbeat
fadeRate = 255; // Set 'fadeRate' Variable to 255 to fade LED with pulse
//WRITE YOUR OTHER FUNCTIONS HERE FOR THE GOGGLES! You can use BPM or BPS as a variable here
QS = false; // reset the Quantified Self flag for next time
}
//=================================Start of Neopixel Effects===============================================
uint8_t i, r, g, b, a, c, inner, outer, ep;
int y1, y2, y3, y4, h;
int8_t y;
// Draw eye background colors
#if EFFECT == RAINBOW
// This renders a glotating rainbow...a WAY overdone LED effect but
// does show the color gamut nicely.
for(h=hue, i=0; i<16; i++, h += 96) {
a = h;
switch((h >> 8) % 6) {
case 0: iColor[i][0] = 255; iColor[i][1] = a; iColor[i][2] = 0; break;
case 1: iColor[i][0] = ~a; iColor[i][1] = 255; iColor[i][2] = 0; break;
case 2: iColor[i][0] = 0; iColor[i][1] = 255; iColor[i][2] = a; break;
case 3: iColor[i][0] = 0; iColor[i][1] = ~a; iColor[i][2] = 255; break;
case 4: iColor[i][0] = a; iColor[i][1] = 0; iColor[i][2] = 255; break;
case 5: iColor[i][0] = 255; iColor[i][1] = 0; iColor[i][2] = ~a; break;
}
}
hue += 7;
if(hue >= 1536) hue -= 1536;
#elif EFFECT == ECTO
// A steampunk aesthetic might fare better with this more subdued effect.
// Etherial green glow with just a little animation for visual spice.
a = (hue >> 4) & 15;
c = hue & 15;
for(i=0; i<16; i++) {
b = (a + 1) & 15;
iColor[i][1] = 255; // Predominantly green
iColor[i][0] = (pgm_read_byte(&sine[a]) * (16 - c) +
pgm_read_byte(&sine[b]) * c ) >> 4;
iColor[i][2] = iColor[i][0] >> 1;
a = b;
}
hue -= 3;
#endif
// Render current blink (if any) into brightness map
if(blinkCounter <= blinkFrames * 2) { // In mid-blink?
if(blinkCounter > blinkFrames) { // Eye closing
outer = blinkFrames * 2 - blinkCounter;
inner = outer + 1;
} else { // Eye opening
inner = blinkCounter;
outer = inner - 1;
}
y1 = upperLidTop - (upperLidTop - upperLidBottom) * outer / blinkFrames;
y2 = upperLidTop - (upperLidTop - upperLidBottom) * inner / blinkFrames;
y3 = lowerLidBottom + (lowerLidTop - lowerLidBottom) * inner / blinkFrames;
y4 = lowerLidBottom + (lowerLidTop - lowerLidBottom) * outer / blinkFrames;
for(i=0; i<16; i++) {
y = pgm_read_byte(&yCoord[i]);
if(y > y1) { // Above top lid
iBrightness[i] = 0;
} else if(y > y2) { // Blur edge of top lid in motion
iBrightness[i] = brightness * (y1 - y) / (y1 - y2);
} else if(y > y3) { // In eye
iBrightness[i] = brightness;
} else if(y > y4) { // Blur edge of bottom lid in motion
iBrightness[i] = brightness * (y - y4) / (y3 - y4);
} else { // Below bottom lid
iBrightness[i] = 0;
}
}
} else { // Not in blink -- set all 'on'
memset(iBrightness, brightness, sizeof(iBrightness));
}
if(--blinkCounter == 0) { // Init next blink?
blinkFrames = random(4, 8);
blinkCounter = blinkFrames * 2 + random(5, 180);
}
// Calculate current eye movement, possibly init next one
if(--gazeCounter <= gazeFrames) { // Is pupil in motion?
ep = newEyePos - eyeMotion * gazeCounter / gazeFrames; // Current pos.
if(gazeCounter == 0) { // Last frame?
eyePos = newEyePos; // Current position = new pos
newEyePos = random(16) * 16; // New pos. (always pixel center)
eyeMotion = newEyePos - eyePos; // Distance to move
gazeFrames = random(10, 20); // Duration of movement
gazeCounter = random(gazeFrames, 130); // Count to END of next movement
}
} else ep = eyePos; // Not moving -- fixed gaze
// Draw pupil -- 2 pixels wide, but sup-pixel positioning may span 3.
a = ep >> 4; // First candidate
b = (a + 1) & 0x0F; // 1 pixel CCW of a
c = (a + 2) & 0x0F; // 2 pixels CCW of a
i = ep & 0x0F; // Fraction of 'c' covered (0-15)
iBrightness[a] = (iBrightness[a] * i ) >> 4;
iBrightness[b] = 0;
iBrightness[c] = (iBrightness[c] * (16 - i)) >> 4;
// Merge iColor with iBrightness, issue to NeoPixels
for(i=0; i<16; i++) {
a = iBrightness[i] + 1;
// First eye
r = iColor[i][0]; // Initial background RGB color
g = iColor[i][1];
b = iColor[i][2];
if(a) {
r = (r * a) >> 8; // Scale by brightness map
g = (g * a) >> 8;
b = (b * a) >> 8;
}
pixels.setPixelColor(((i + TOP_LED_FIRST) & 15),
pgm_read_byte(&gamma8[r]), // Gamma correct and set pixel
pgm_read_byte(&gamma8[g]),
pgm_read_byte(&gamma8[b]));
// Second eye uses the same colors, but reflected horizontally.
// The same brightness map is used, but not reflected (same left/right)
r = iColor[15 - i][0];
g = iColor[15 - i][1];
b = iColor[15 - i][2];
if(a) {
r = (r * a) >> 8;
g = (g * a) >> 8;
b = (b * a) >> 8;
}
pixels.setPixelColor(16 + ((i + TOP_LED_SECOND) & 15),
pgm_read_byte(&gamma8[r]),
pgm_read_byte(&gamma8[g]),
pgm_read_byte(&gamma8[b]));
}
pixels.show();
delay(15);
}
//The interrups need to be set different for different AVR Microcontrollers, see code bellow
/*
The register settings above tell Timer2 to go into CTC mode, and to count up to 124 (0x7C) over and over and over again.
A prescaler of 256 is used to get the timing right so that it takes 2 milliseconds to count to 124.
An interrupt flag is set every time Timer2 reaches 124, and a special function called an Interrupt Service Routine (ISR) that we wrote is run at the very next possible moment, no matter what the rest of the program is doing. sei() ensures that global interrupts are enabled.
Timing is important! If you are using a different Arduino or Arduino compatible device, you will need to change this function. (please see different interrupts bellow)
This code works with Arduino UNO or Arduino PRO or Arduino Pro Mini 5V or any Arduino running with an ATmega328 and 16MHz clock.
void interruptSetup(){
TCCR2A = 0x02;
TCCR2B = 0x06;
OCR2A = 0x7C;
TIMSK2 = 0x02;
sei();
}
If you are using a FIO or LillyPad Arduino or Arduino Pro Mini 3V or Arduino SimpleSnap or other Arduino that has ATmega168 or ATmega328 with 8MHz oscillator, change the line TCCR2B = 0x06 to TCCR2B = 0x05.
If you are using Arduino Leonardo or Adafruit's Flora or Arduino Micro or other Arduino that has ATmega32u4 running at 16MHz
void interruptSetup(){
TCCR0A = 0x02;
TCCR0B = 0x04;
OCR0A = 0x7C;
TIMSK0 = 0x02;
sei();
}
The LilyPad Arduino USB runs at 8MHz, likely some other ATmega32u4 based devices out there, so to correct the timing, change TCCR0B = 0x04; to TCCR0B = 0x03; Then change OCR0A = 0x7C; to OCR0A = 0xF9;
The only other thing you will need is the correct ISR vector in the next step. ATmega32u4 devices use ISR(TIMER0_COMPA_vect)
*/
volatile int rate[10]; // array to hold last ten IBI values
volatile unsigned long sampleCounter = 0; // used to determine pulse timing
volatile unsigned long lastBeatTime = 0; // used to find IBI
volatile int P =512; // used to find peak in pulse wave, seeded
volatile int T = 512; // used to find trough in pulse wave, seeded
volatile int thresh = 512; // used to find instant moment of heart beat, seeded
volatile int amp = 100; // used to hold amplitude of pulse waveform, seeded
volatile boolean firstBeat = true; // used to seed rate array so we startup with reasonable BPM
volatile boolean secondBeat = false; // used to seed rate array so we startup with reasonable BPM
void interruptSetup(){
// Initializes Timer2 to throw an interrupt every 2mS.
TCCR2A = 0x02; // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE
TCCR2B = 0x05; // DON'T FORCE COMPARE, 256 PRESCALER
OCR2A = 0X7C; // SET THE TOP OF THE COUNT TO 124 FOR 500Hz SAMPLE RATE
TIMSK2 = 0x02; // ENABLE INTERRUPT ON MATCH BETWEEN TIMER2 AND OCR2A
sei(); // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED
}
// THIS IS THE TIMER 2 INTERRUPT SERVICE ROUTINE.
// Timer 2 makes sure that we take a reading every 2 miliseconds
ISR(TIMER2_COMPA_vect){ // triggered when Timer2 counts to 124
cli(); // disable interrupts while we do this
Signal = analogRead(pulsePin); // read the Pulse Sensor
sampleCounter += 2; // keep track of the time in mS with this variable
int N = sampleCounter - lastBeatTime; // monitor the time since the last beat to avoid noise
// find the peak and trough of the pulse wave
if(Signal < thresh && N > (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI
if (Signal < T){ // T is the trough
T = Signal; // keep track of lowest point in pulse wave
}
}
if(Signal > thresh && Signal > P){ // thresh condition helps avoid noise
P = Signal; // P is the peak
} // keep track of highest point in pulse wave
// NOW IT'S TIME TO LOOK FOR THE HEART BEAT
// signal surges up in value every time there is a pulse
if (N > 250){ // avoid high frequency noise
if ( (Signal > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){
Pulse = true; // set the Pulse flag when we think there is a pulse
digitalWrite(blinkPin,HIGH); // turn on pin 13 LED
IBI = sampleCounter - lastBeatTime; // measure time between beats in mS
lastBeatTime = sampleCounter; // keep track of time for next pulse
if(secondBeat){ // if this is the second beat, if secondBeat == TRUE
secondBeat = false; // clear secondBeat flag
for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup
rate[i] = IBI;
}
}
if(firstBeat){ // if it's the first time we found a beat, if firstBeat == TRUE
firstBeat = false; // clear firstBeat flag
secondBeat = true; // set the second beat flag
sei(); // enable interrupts again
return; // IBI value is unreliable so discard it
}
// keep a running total of the last 10 IBI values
word runningTotal = 0; // clear the runningTotal variable
for(int i=0; i<=8; i++){ // shift data in the rate array
rate[i] = rate[i+1]; // and drop the oldest IBI value
runningTotal += rate[i]; // add up the 9 oldest IBI values
}
rate[9] = IBI; // add the latest IBI to the rate array
runningTotal += rate[9]; // add the latest IBI to runningTotal
runningTotal /= 10; // average the last 10 IBI values
BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM!
QS = true; // set Quantified Self flag
// QS FLAG IS NOT CLEARED INSIDE THIS ISR
}
}
if (Signal < thresh && Pulse == true){ // when the values are going down, the beat is over
digitalWrite(blinkPin,LOW); // turn off pin 13 LED
Pulse = false; // reset the Pulse flag so we can do it again
amp = P - T; // get amplitude of the pulse wave
thresh = amp/2 + T; // set thresh at 50% of the amplitude
P = thresh; // reset these for next time
T = thresh;
}
if (N > 2500){ // if 2.5 seconds go by without a beat
thresh = 512; // set thresh default
P = 512; // set P default
T = 512; // set T default
lastBeatTime = sampleCounter; // bring the lastBeatTime up to date
firstBeat = true; // set these to avoid noise
secondBeat = false; // when we get the heartbeat back
}
sei(); // enable interrupts when you're done!
}// end isr
This project will be continued over the summer, however, due to time and material restraints, I am not able to work on it further at this time. Please stand by for a full tutorial sometime this summer.