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; }
Thanks a lot, in my contry i cant find the CMS50C, only CMS50N or CMS50M, you think i can use this?
I don’t know. There’s no way of telling without having a look inside the device and listening for the signals. Different versions can have entirely different PCBs and firmwares…
Hi,
a very cool idea which I just tried do rebuild on an Arduino Mega. Unfortunately I am a bit confused about the wireing.
Could you tell to how the CMS50C is wired to the Teensy.
thanks
Ralf