Description
Create a physically interactive system of your choice that relies on a multimedia computer for some sort of processing or data analysis. The Final should use BOTH Processing AND Arduino. Your focus should be on careful and timely sensing of the relevant actions of the person or people that you’re designing this for, and on clear, prompt, and effective responses. Any interactive system is going to involve systems of listening, thinking, and speaking from both parties. Whether it involves one cycle or many, the exchange should be engaging. You may work alone or in pairs.
Description of the game
So, I used a pulse sensor to make a heartbeat monitor which can be used for different purposes. Maybe in the health care industry or sports industry.
I used two LEDs that turn on and off according to the pulse rate. I added a scaling component in the code to adjust the size of the rate. I also checked the timings between heartbeat by subtracting the last-current. I also made the heart pump by increasing the stroke weight. You can also use “S” or “s” to save the heartbeat in the folder where the code. it saved and later you can compare the heartbeat.
I did the user testing as well after that. The test person came back from a run, thus his heart was beating faster.
He gave some feedback. Thank you.
Certain challenges:-
- It took me some time to figure out how the sensor worked because I have never worked with a pulse sensor before.
- it was hard to display the pulse wave first, then I found some resources to make me understand the concept.
- it was hard to add further things to the project to make it more useful.
The code is following:-
Arduino
// 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 // Volatile Variables, used in the interrupt service routine! volatile int BPM; // int that holds raw Analog in 0. updated every 2mS volatile int Signal; // holds the incoming raw data volatile int TBH = 600; // int that holds the time interval between beats! Must be seeded! volatile boolean Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". volatile boolean QS = false; // becomes true when Arduoino finds a beat. // Regards Serial OutPut -- Set This Up to your needs static boolean serialVisual = false; // Set to 'false' by Default. Re-set to 'true' to see Arduino Serial Monitor ASCII Visual Pulse 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 // IF YOU ARE POWERING The Pulse Sensor AT VOLTAGE LESS THAN THE BOARD VOLTAGE, // UN-COMMENT THE NEXT LINE AND APPLY THAT VOLTAGE TO THE A-REF PIN // analogReference(EXTERNAL); } // Where the Magic Happens void loop(){ serialOutput() ; if (QS == true){ // A Heartbeat Was Found // BPM and TBH have been Determined // Quantified Self "QS" true when arduino finds a heartbeat fadeRate = 255; // Makes the LED Fade Effect Happen // Set 'fadeRate' Variable to 255 to fade LED with pulse serialOutputWhenBeatHappens(); // A Beat Happened, Output that to serial. QS = false; // reset the Quantified Self flag for next time } ledFadeToBeat(); // Makes the LED Fade Effect Happen delay(20); // take a break } void ledFadeToBeat(){ fadeRate -= 15; // set LED fade value fadeRate = constrain(fadeRate,0,255); // keep LED fade value from going into negative numbers! analogWrite(fadePin,fadeRate); // fade LED } void serialOutput(){ // Decide How To Output Serial. if (serialVisual == true){ arduinoSerialMonitorVisual('-', Signal); // goes to function that makes Serial Monitor Visualizer } else{ sendDataToSerial('S', Signal); // goes to sendDataToSerial function } } // Decides How To OutPut BPM and TBH Data void serialOutputWhenBeatHappens(){ if (serialVisual == true){ // Code to Make the Serial Monitor Visualizer Work Serial.print("*** Heart-Beat Happened *** "); //ASCII Art Madness Serial.print("BPM: "); Serial.print(BPM); Serial.print(" "); } else{ sendDataToSerial('B',BPM); // send heart rate with a 'B' prefix sendDataToSerial('Q',TBH); // send time between beats with a 'Q' prefix } } // Sends Data to Pulse Sensor Processing App, Native Mac App, or Third-party Serial Readers. void sendDataToSerial(char symbol, int data ){ Serial.print(symbol); Serial.println(data); } // Code to Make the Serial Monitor Visualizer Work void arduinoSerialMonitorVisual(char symbol, int data ){ const int sensorMin = 0; // sensor minimum, discovered through experiment const int sensorMax = 1024; // sensor maximum, discovered through experiment int sensorReading = data; // map the sensor range to a range of 12 options: int range = map(sensorReading, sensorMin, sensorMax, 0, 11); // do something different depending on the // range value: switch (range) { case 0: Serial.println(""); /////ASCII Art Madness break; case 1: Serial.println("---"); break; case 2: Serial.println("------"); break; case 3: Serial.println("---------"); break; case 4: Serial.println("------------"); break; case 5: Serial.println("--------------|-"); break; case 6: Serial.println("--------------|---"); break; case 7: Serial.println("--------------|-------"); break; case 8: Serial.println("--------------|----------"); break; case 9: Serial.println("--------------|----------------"); break; case 10: Serial.println("--------------|-------------------"); break; case 11: Serial.println("--------------|-----------------------"); break; } } volatile int rate[10]; // array to hold last ten TBH values volatile unsigned long sampleCounter = 0; // used to determine pulse timing volatile unsigned long lastBeatTime = 0; // used to find TBH 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 = 525; // 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 = 0x06; // 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 > (TBH/5)*3){ // avoid dichrotic noise by waiting 3/5 of last TBH 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 > (TBH/5)*3) ){ Pulse = true; // set the Pulse flag when we think there is a pulse digitalWrite(blinkPin,HIGH); // turn on pin 13 LED TBH = 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] = TBH; } } 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; // TBH value is unreliable so discard it } // keep a running total of the last 10 TBH 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 TBH value runningTotal += rate[i]; // add up the 9 oldest TBH values } rate[9] = TBH; // add the latest TBH to the rate array runningTotal += rate[9]; // add the latest TBH to runningTotal runningTotal /= 10; // average the last 10 TBH 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 youre done! }// end isr
Processing:-
import processing.sound.*; SoundFile file; import processing.serial.*; PFont font; PFont portsFont; Scrollbar scaleBar; Serial port; int Sensor; // holds pusle sensor data from the arduino int TBH; // HOLDS TIME BETWEN HEARTBEATS FROM ARDUINO int BPM; // HOLDS HEART RATE VALUE FROM ARDUINO int[] RawY; // HOLDS HEARTBEAT WAVEFORM DATA BEFORE SCALING int[] ScaledY; // USED TO POSITION SCALED HEARTBEAT WAVEFORM int[] rate; // USED TO POSITION BPM DATA WAVEFORM float zoom; // USED WHEN SCALING PULSE WAVEFORM TO PULSE WINDOW float offset; // USED WHEN SCALING PULSE WAVEFORM TO PULSE WINDOW color eggshell = color(171,219,227); int heart = 0; // This variable times the heart image 'pulse' on screen // THESE VARIABLES DETERMINE THE SIZE OF THE DATA WINDOWS int PulseWindowWidth = 490; int PulseWindowHeight = 512; int BPMWindowWidth = 180; int BPMWindowHeight = 340; boolean beat = false; // set when a heart beat is detected, then cleared when the BPM graph is advanced // SERIAL PORT STUFF TO HELP YOU FIND THE CORRECT SERIAL PORT String serialPort; String[] serialPorts = new String[Serial.list().length]; boolean serialPortFound = false; Radio[] button = new Radio[Serial.list().length]; void setup() { size(700, 600); // Stage size file = new SoundFile(this, "heart.mp3"); frameRate(100); font = loadFont("Arial-BoldMT-24.vlw"); textFont(font); textAlign(CENTER); rectMode(CENTER); ellipseMode(CENTER); // Scrollbar constructor inputs: x,y,width,height,minVal,maxVal scaleBar = new Scrollbar (400, 575, 180, 12, 0.5, 1.0); // set parameters for the scale bar RawY = new int[PulseWindowWidth]; // initialize raw pulse waveform array ScaledY = new int[PulseWindowWidth]; // initialize scaled pulse waveform array rate = new int [BPMWindowWidth]; // initialize BPM waveform array zoom = 0.75; // initialize scale of heartbeat window // set the visualizer lines to 0 for (int i=0; i<rate.length; i++){ rate[i] = 555; // Place BPM graph line at bottom of BPM Window } for (int i=0; i<RawY.length; i++){ RawY[i] = height/2; // initialize the pulse window data line to V/2 } background(0); noStroke(); // DRAW OUT THE PULSE WINDOW AND BPM WINDOW RECTANGLES drawDataWindows(); drawHeart(); // GO FIND THE ARDUINO fill(eggshell); text("Select Your Serial Port",245,30); listAvailablePorts(); } void draw() { if(serialPortFound){ // ONLY RUN THE VISUALIZER AFTER THE PORT IS CONNECTED background(0); noStroke(); drawDataWindows(); drawPulseWaveform(); drawBPMwaveform(); drawHeart(); // PRINT THE DATA AND VARIABLE VALUES fill(eggshell); // get ready to print text text("Check your Heart Beat and Pulse",245,30); // tell them what you are text("TBH " + TBH + "mS",600,585); // print the time between heartbeats in mS text(BPM + "BPM",600,200); // print the Beats Per Minute text("Scale the Pulse Rate " + nf(zoom,1,2), 150, 585); // show the current scale of Pulse Window // DO THE SCROLLBAR THINGS scaleBar.update (mouseX, mouseY); scaleBar.display(); } else { // SCAN BUTTONS TO FIND THE SERIAL PORT for(int i=0; i<button.length; i++){ button[i].overRadio(mouseX,mouseY); button[i].displayRadio(); } } } //end of draw loop void drawDataWindows(){ // DRAW OUT THE PULSE WINDOW AND BPM WINDOW RECTANGLES fill(eggshell); // color for the window background rect(255,height/2,PulseWindowWidth,PulseWindowHeight); rect(600,385,BPMWindowWidth,BPMWindowHeight); } void drawPulseWaveform(){ // DRAW THE PULSE WAVEFORM // prepare pulse data points RawY[RawY.length-1] = (1023 - Sensor) - 212; // place the new raw datapoint at the end of the array zoom = scaleBar.getPos(); // get current waveform scale value offset = map(zoom,0.5,1,150,0); // calculate the offset needed at this scale for (int i = 0; i < RawY.length-1; i++) { // move the pulse waveform by RawY[i] = RawY[i+1]; // shifting all raw datapoints one pixel left float dummy = RawY[i] * zoom + offset; // adjust the raw data to the selected scale ScaledY[i] = constrain(int(dummy),44,556); // transfer the raw data array to the scaled array } stroke(250,0,0); // red is a good color for the pulse waveform noFill(); beginShape(); // using beginShape() renders fast for (int x = 1; x < ScaledY.length-1; x++) { vertex(x+10, ScaledY[x]); //draw a line connecting the data points } endShape(); } void drawBPMwaveform(){ // DRAW THE BPM WAVE FORM // first, shift the BPM waveform over to fit then next data point only when a beat is found if (beat == true){ // move the heart rate line over one pixel every time the heart beats file.play(); beat = false; // clear beat flag (beat flag waset in serialEvent tab) for (int i=0; i<rate.length-1; i++){ rate[i] = rate[i+1]; // shift the bpm Y coordinates over one pixel to the left } // then limit and scale the BPM value BPM = min(BPM,200); // limit the highest BPM value to 200 float dummy = map(BPM,0,200,555,215); // map it to the heart rate window Y rate[rate.length-1] = int(dummy); // set the rightmost pixel to the new data point value } // GRAPH THE HEART RATE WAVEFORM stroke(250,0,0); // color of heart rate graph strokeWeight(2); // thicker line is easier to read noFill(); beginShape(); for (int i=0; i < rate.length-1; i++){ // variable 'i' will take the place of pixel x position vertex(i+510, rate[i]); // display history of heart rate datapoints } endShape(); } void drawHeart(){ // DRAW THE HEART AND MAYBE MAKE IT BEAT fill(250,0,0); stroke(250,0,0); // the 'heart' variable is set in serialEvent when arduino sees a beat happen heart--; // heart is used to time how long the heart graphic swells when your heart beats heart = max(heart,0); // don't let the heart variable go into negative numbers if (heart > 0){ // if a beat happened recently, strokeWeight(8); // make the heart big } smooth(); // draw the heart with two bezier curves bezier(width-100,50, width-20,-20, width,140, width-100,150); bezier(width-100,50, width-190,-20, width-200,140, width-100,150); strokeWeight(1); // reset the strokeWeight for next time } void listAvailablePorts(){ serialPorts = Serial.list(); fill(0); textFont(font,16); textAlign(LEFT); // set a counter to list the ports backwards int yPos = 0; for(int i=serialPorts.length-1; i>=0; i--){ button[i] = new Radio(35, 95+(yPos*20),12,color(180),color(80),color(255),i,button); text(serialPorts[i],50, 100+(yPos*20)); yPos++; } textFont(font); textAlign(CENTER); }
void mousePressed(){ scaleBar.press(mouseX, mouseY); if(!serialPortFound){ for(int i=0; i<button.length; i++){ if(button[i].pressRadio(mouseX,mouseY)){ try{ port = new Serial(this, Serial.list()[i], 115200); // make sure Arduino is talking serial at this baud rate delay(1000); println(port.read()); port.clear(); // flush buffer port.bufferUntil('\n'); // set buffer full flag on receipt of carriage return serialPortFound = true; } catch(Exception e){ println("Couldn't open port " + Serial.list()[i]); } } } } } void mouseReleased(){ scaleBar.release(); } void keyPressed(){ switch(key){ case 's': // pressing 's' or 'S' will take a jpg of the processing window case 'S': saveFrame("heartLight-####.jpg"); // take a shot of that! break; default: break; } }
class Radio { int _x,_y; int size, dotSize; color baseColor, overColor, pressedColor; boolean over, pressed; int me; Radio[] radios; Radio(int xp, int yp, int s, color b, color o, color p, int m, Radio[] r) { _x = xp; _y = yp; size = s; dotSize = size - size/3; baseColor = b; overColor = o; pressedColor = p; radios = r; me = m; } boolean pressRadio(float mx, float my){ if (dist(_x, _y, mx, my) < size/2){ pressed = true; for(int i=0; i<radios.length; i++){ if(i != me){ radios[i].pressed = false; } } return true; } else { return false; } } boolean overRadio(float mx, float my){ if (dist(_x, _y, mx, my) < size/2){ over = true; for(int i=0; i<radios.length; i++){ if(i != me){ radios[i].over = false; } } return true; } else { return false; } } void displayRadio(){ noStroke(); fill(baseColor); ellipse(_x,_y,size,size); if(over){ fill(overColor); ellipse(_x,_y,dotSize,dotSize); } if(pressed){ fill(pressedColor); ellipse(_x,_y,dotSize,dotSize); } } }
/* from the book "Processing" by Reas and Fry */ class Scrollbar{ int x,y; // the x and y coordinates float sw, sh; // width and height of scrollbar float pos; // position of thumb float posMin, posMax; // max and min values of thumb boolean rollover; // true when the mouse is over boolean locked; // true when it's the active scrollbar float minVal, maxVal; // min and max values for the thumb Scrollbar (int xp, int yp, int w, int h, float miv, float mav){ // values passed from the constructor x = xp; y = yp; sw = w; sh = h; minVal = miv; maxVal = mav; pos = x - sh/2; posMin = x-sw/2; posMax = x + sw/2; // - sh; } // updates the 'over' boolean and position of thumb void update(int mx, int my) { if (over(mx, my) == true){ rollover = true; // when the mouse is over the scrollbar, rollover is true } else { rollover = false; } if (locked == true){ pos = constrain (mx, posMin, posMax); } } // locks the thumb so the mouse can move off and still update void press(int mx, int my){ if (rollover == true){ locked = true; // when rollover is true, pressing the mouse button will lock the scrollbar on }else{ locked = false; } } // resets the scrollbar to neutral void release(){ locked = false; } // returns true if the cursor is over the scrollbar boolean over(int mx, int my){ if ((mx > x-sw/2) && (mx < x+sw/2) && (my > y-sh/2) && (my < y+sh/2)){ return true; }else{ return false; } } // draws the scrollbar on the screen void display (){ noStroke(); fill(255); rect(x, y, sw, sh); // create the scrollbar fill (250,0,0); if ((rollover == true) || (locked == true)){ stroke(250,0,0); strokeWeight(8); // make the scale dot bigger if you're on it } ellipse(pos, y, sh, sh); // create the scaling dot strokeWeight(1); // reset strokeWeight } // returns the current value of the thumb float getPos() { float scalar = sw / sw; // (sw - sh/2); float ratio = (pos-(x-sw/2)) * scalar; float p = minVal + (ratio/sw * (maxVal - minVal)); return p; } }
void serialEvent(Serial port){ try{ String inData = port.readStringUntil('\n'); inData = trim(inData); // cut off white space (carriage return) if (inData.charAt(0) == 'S'){ // leading 'S' for sensor data inData = inData.substring(1); // cut off the leading 'S' Sensor = int(inData); // convert the string to usable int } if (inData.charAt(0) == 'B'){ // leading 'B' for BPM data inData = inData.substring(1); // cut off the leading 'B' BPM = int(inData); // convert the string to usable int beat = true; // set beat flag to advance heart rate graph heart = 20; // begin heart image 'swell' timer } if (inData.charAt(0) == 'Q'){ // leading 'Q' means IBI data inData = inData.substring(1); // cut off the leading 'Q' TBH = int(inData); // convert the string to usable int } } catch(Exception e) { // println(e.toString()); } }