Pulse Oximeter Hack

The pulse oximeter is a very useful and affordable medical gadget. It incorporates a quick and easy way of measuring your heart rate as well as calculating the blood oxygen saturation in one go! After having a look inside the Contec CMS50C and CMS50D which are both MSP430-based devices with OLED displays, it quickly became apparent, which one is easier to hack to extract the data.




The CMS50C (the one with the color OLED, available for <15$ on the usual platforms) has a nice line of well accessible and labeled pads, among which is also a UART interface that is streaming all of the useful values such as 7bit photoplethismographic curve data, the "bar graph", the SPO2 and heart rate value, a "beat detection bit" and various diagnostic bits at a sampling rate of 60Hz. An overview over the entire communication protocol can be found here. The serial port settings are a bit unusual: 4800 baud, and EVEN parity, so watch out for that, but other than that it’s a quite clever protocol that is storing all this information in only 5 bytes.



















The most essential connections are GND, TX, RST, the right pin of the trigger pushbutton and Batt. After that you should be able to completely control the thing from a microcontroller and harvest the data.
A code sample for Teensy 3.x is found below:




//sketch for reading the CMS50C pulse oximeter data using a Teensy 3.x

#define TRIG_PULSEOX 3 // has to be pulled to 3.3V to start the CMS50C
#define RST_PULSEOX 15
//vars for pulseOx
char incomingPulseOxbyte;
unsigned int byteCntPulse = 0;
char pulseCurve_PulseOx = 0;
char SPO2;
char HR_PulseOx;
boolean lastBitPulseOx = 0; //the MSB of the heart rate byte is hidden in a different byte - the one that holds the bar graph data
unsigned long pulseOxTrigTmr=0;
unsigned long pulseOxTrigInt=600000; // length of the pulse that triggers the CMS50C
unsigned long pulseOxRstTmr=0;
unsigned long pulseOxRstInt=800000;

boolean triggeredOx = false;
boolean resettedOx = false;

//packet management bytes for the Amarino Android app
char startFlag = 18;
char ack = 19;
char delimiter = 59; //';' in case we use more than 1 channel


void setup() {
  // put your setup code here, to run once:
  Serial1.begin(115200); // Bluetooth port
  Serial3.begin(4800, SERIAL_8E1); // Pulse oximeter CONTEC CMS 50C, annoying fact: it has EVEN PARITY!

   // make the pins outputs:
  pinMode(RST_PULSEOX, OUTPUT);
  pinMode(TRIG_PULSEOX, OUTPUT);
  
  digitalWrite(RST_PULSEOX, HIGH);
  
  
  triggerOx(); //turn on the PulseOx

}

void loop() {

  
  // put your main code here, to run repeatedly:
   //waiting to pull TRIG_PULSEOX LOW again 
  if((micros()-pulseOxTrigTmr)>pulseOxTrigInt && triggeredOx == true){
    digitalWrite(TRIG_PULSEOX, LOW);
    triggeredOx=false;
  }
  

   readPulseOx();

}

void readPulseOx() {
 
  //--------------------Pulse Oximeter data analysis----------------
  if (Serial3.available() > 0) {
    //pulseox data
    incomingPulseOxbyte = Serial3.read();
    if (byteCntPulse == 1) {
      //the second byte contains a 7 bit pulse wave value, Fs=60Hz
      pulseCurve_PulseOx = incomingPulseOxbyte;
      byteCntPulse++;
    } else if (byteCntPulse == 2) {
      //the 0-3rd bits of the third byte is the bar graph value .
      //the 6th bit is bit 7 of the heart rate value!
      if ( bitRead(incomingPulseOxbyte, 6)) {
        //if the heart rate is >= 128, this happens
        lastBitPulseOx = 1;
      } else
        lastBitPulseOx = 0;

      byteCntPulse++; //move on
    } else if (byteCntPulse == 3) {
      //
      if (lastBitPulseOx)
        //if the heart rate is >= 128, this happens
        HR_PulseOx = 128 + incomingPulseOxbyte;
      else
        HR_PulseOx = incomingPulseOxbyte;

      byteCntPulse++;
    } else if (byteCntPulse == 4) {
      //the last byte contains the most important SPO2 value
      SPO2 = incomingPulseOxbyte;
      byteCntPulse = 0; //we're done the data packet has 5 bytes in total
    } else {


      Serial1.print(startFlag);
      Serial1.print("P");
      Serial1.print(delimiter);
      Serial1.print(pulseCurve_PulseOx, DEC); //send the pulse curve value
      Serial1.print(delimiter);
      Serial1.print(HR_PulseOx, DEC); //send the heart rate
      Serial1.print(delimiter);
      Serial1.print(SPO2, DEC); //send SPO2 value!
      Serial1.print(ack);

    }

    //analyze the first byte. it has plenty of information about the data
    //the fourth bit of the first packet byte means "OK signal" if zero, otherwise it stands for "searching too long"
    //the seventh bit of the first byte has to be always set!!!!! (the only byte where this is the case) all other bytes have a bit 7 == 0
    if ( bitRead(incomingPulseOxbyte, 7)) {
      byteCntPulse++;
    }
  }

}

void triggerOx(){
//function to trigger the pulse oximeter button
   digitalWrite(TRIG_PULSEOX,HIGH);
   pulseOxTrigTmr=micros();
   triggeredOx=true;
 }


Leave a Reply

Your email address will not be published. Required fields are marked *