rwaldron / johnny-five

JavaScript Robotics and IoT programming framework, developed at Bocoup.
http://johnny-five.io
Other
13.28k stars 1.76k forks source link

Pulse Sensor Node.js Implementation #788

Closed nakulcr7 closed 8 years ago

nakulcr7 commented 9 years ago

Hey,

I have written javascript code using the the same algorithm so that the Pulse Sensor Amped (https://github.com/WorldFamousElectronics/PulseSensor_Amped_Arduino/tree/master/PulseSensorAmped_Arduino_1dot4) can be used with node.js. However I am getting inaccurate values for BPM. What might the issue be?

var rate = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],    
      sampleCounter = 0, 
      lastBeatTime = 0,  
      P = 512,               
      T = 512,                  
      thresh = 525,          
      amp = 100,              
      firstBeat = true,       
      secondBeat = false,
      IBI = 600,
      Pulse = false,
      BPM,
      Signal,
      QS = false;

var five = require('johnny-five'),
      board, sensor;
board = new five.Board();

board.on('ready', function() {
    sensor = new five.Sensor({
                pin: "A0",
                freq: 2
            });        

            sensor.scale([0,1024]).on('read', function() {
                  Signal = this.scaled;
                  calculate_bpm();

                if(QS === true) {
                    console.log('BPM : ', BPM); 
                    QS = false;
        } 
            });                    
});

function calculate_bpm() {

            sampleCounter += 2;                           
            N = sampleCounter - lastBeatTime;       

            if(Signal < thresh && N > (IBI/5)*3) {       
                  if (Signal < T) {                        
                        T = Signal;                  
                  }
            }

            if(Signal > thresh && Signal > P) {          
                  P = Signal;                             
            }                                       

            if (N > 250) {   

              if ((Signal > thresh) && (Pulse === false) && (N > (IBI/5)*3)) {        
                  Pulse = true;                               
                  IBI = sampleCounter - lastBeatTime;         
                  lastBeatTime = sampleCounter;               

                  if(secondBeat) {                        
                      secondBeat = false;                  
                      for(var i=0; i<=9; i++){             
                          rate[i] = IBI;                      
                      }
                  }

                  if(firstBeat) {                         
                      firstBeat = false;               
                      secondBeat = true;      
                      return;                              
                  }   

                  var runningTotal = 0;                 

                  for(var i=0; i<=8; i++) {              
                      rate[i] = rate[i+1];                  
                      runningTotal += rate[i];         
                  }

                  rate[9] = IBI;                          
                  runningTotal += rate[9];        
                  runningTotal /= 10;               
                  BPM = 60000/runningTotal; 
                  QS = true;                            
              }                       
          }

          if (Signal < thresh && Pulse === true){   
              Pulse = false;                         
              amp = P - T;                           
              thresh = amp/2 + T;               
              P = thresh;                            
              T = thresh;
          }

          if (N > 2500) {                           
              thresh = 512;                        
              P = 512;                               
              T = 512;                               
              lastBeatTime = sampleCounter;         
              firstBeat = true;                      
              secondBeat = false;               
          }
} 
soundanalogous commented 9 years ago

This would sort of calculation would be better handled on the microcontroller itself. There is interest in adding such functionality to the Firmata protocol: https://github.com/firmata/protocol/issues/31.

nakulcr7 commented 9 years ago

There's no other way around it presently?

soundanalogous commented 9 years ago

It won't be as accurate.

mMerlin commented 9 years ago

Something to consider. The current Sensor class does some filtering to reduce noise in analog signals. I do not know if that could be affecting your results or not. see the median function lib/sensor.js@104

Also the "read" event has been deprecated in favour of a "data" event.

rwaldron commented 9 years ago

The current Sensor class does some filtering to reduce noise in analog signals

Yep. I also have a plan to allow this filtering to be customized. It would still default to the present filtering, but would allow a custom filter to override.

mMerlin commented 9 years ago

Care to share the plan? I have some ideas that have been bouncing around in my head as well. Been planning a post to get thoughts before writing any code.

Started looking at this, when I noticed that the samples array was very large with a slow sample rate (large freq). A quick review of the code showed me that for values that were expected to be ramping, the calculated median value is actually outdated by half of the freq time. IE with a 10 minute freq value, the median is about 5 minutes old when the value is changing continuously in a single direction. Does not even mater if the change is linear or not. Limiting the median calculation to the last few values reduces that drastically, without [re]introducing significant noise. 'Few' being application specific.

rwaldron commented 9 years ago

Care to share the plan?

Instead of "I also have a plan to allow this filtering to be customized" I should've said "I long standing desire to allow this filtering to be customized".

IE with a 10 minute freq value, the median is about 5 minutes old when the value is changing continuously in a single direction.

Yeah, I guess I never considered the 10 minute period case.

Limiting the median calculation to the last few values reduces that drastically, without [re]introducing significant noise

Based on your ideas above, here's a rough sketch of what I'd like the exposed API to look like:

var sensor = new Sensor({
  pin: "A0", 
  sampling: {
    // flag to keep the queue full (to specified limit) instead of clearing each time (for rolling average)
    preserve: true|false, // what's the default?

    // flag to limit the number of previous entries to keep in the queue
    size: n, 

    // provide callback hook to allow main program to provide custom processing
    filter: function(samples) {

    }
  }
});

// expose the samples array (read only?) for external processing
sensor.samples; // [...]

// sample: 
{
  value: 0-1023
  timestamp: Date.now()
}
mMerlin commented 9 years ago

Any changes in the v0.9.0 queue for sensor?

I have some ideas to extend that api. I'll flesh it out and post as a new issue before coding any of that. Above looks like a workable base.

Concept: (for johnny-five in general). Should it be valid to modify the sampling configuration dynamically, or only during instance creation?

rwaldron commented 9 years ago

Concept: (for johnny-five in general). Should it be valid to modify the sampling configuration dynamically, or only during instance creation?

I wouldn't want it to be such that sensor.sampling.size were a plain property that could be assigned to, but if it were presented like this:

sensor.sampling({
  ... the same shaped object I described above, but with new values
});

That should update some private state (a simple example of how private state is maintained: Relay). The private state would be accessed by the interval, so once sampling(...) is called, the next interval will have the new configuration to work with.

mMerlin commented 9 years ago

I looked at earlier (not closely) some of the existing private state handling. Private data plus getter function(s), and single setter function for the whole sample block. Works for me.

nakulcr7 commented 9 years ago

Guys, so the bottomline is that the pulse sensor cant be implemented as of now right?

rwaldron commented 9 years ago

This might be a great candidate for our I2C "component backpack" project. cc @ajfisher

soundanalogous commented 9 years ago

I created a Firmata wrapper for the PulseSensorAmp a few years ago... had totally forgotten about it. I could possibly add it to configurable firmata per the new device architecture. This would add pulse sensor support for Arduino compatible boards, but not for boards like raspberry pi. An I2C "component backpack" such as @rwaldron mentioned would enable support for any architecture that has I2C, not just Arduino.

soundanalogous commented 9 years ago

One thing to think about is if both methods were supported (i2c component backpack and Firmata device wrapper), could the 2 versions coexist in johnny-five? I'd assume you'd have a different controller for each version.

ajfisher commented 9 years ago

@soundanalogous With ping this is the approach I've taken, either a dedicated firmata that can handle it (the one that uses pulsein) or an i2c component backpack. Different controllers for each one and you set that at the point where you create the object.

Yep I think pushing this down to the micro is going to give you a better result.

@Senade If you're interested in doing this, hop over to https://github.com/ajfisher/nodebots-interchange and I can walk you through the process and you can give it a crack.

nakulcr7 commented 9 years ago

@ajfisher Hey, I'm designing an advanced wheelchair prototype for my final undergrad project, and real-time health tracking is one of the features. So yes, I am interested. Can you guide me through the process please?

rwaldron commented 9 years ago

could the 2 versions coexist in johnny-five? I

Yep. We just give them different "controller" names and definitions, then they sit side by side :)

reconbot commented 8 years ago

Looks like we have a solution. I'm going to close this issue due to it's age, but if you'd like to continue with it feel free to reopen.