Some time ago I wrote a bit about how to download data from a Beurer BM65 blood pressure monitor to my PC via USB using a homemade Python module. That was good fun, so when I discovered the Contec CMS50D+ pulse oximeter (which also has USB connectivity) on everybody’s favorite online auctioning site for ~$40 including shipping, I had to buy one.

Contec CMS50D+

Contec CMS50D+

So, what’s in the box?

Apart from the device itself, the box contains a special USB cable (we’ll get back to this), a lanyard (because why not), no batteries, a small mysterious CD-ROM, and a surprisingly well-written English instruction manual. The device works as expected and outputs plausible data on it’s little display, which is very crisp by the way. I can’t vouch for the validity of the blood oxidation level, but the pulse stuff seems pretty precise at least. However, this is not the important part of this blog entry. Let’s focus on getting data off the damn thing without having to rely on the official software.

Contec CMS50D+ box content

Contec CMS50D+ box content

Plugging it in

Others have already reverse engineered the protocol of this device, but let’s have a look at the bundled software anyway. After updating all the anti-malware software on my Windows box, I tried putting in the CD-ROM. So far it seems benign enough. It contains a single executable that installs a driver and two applications: one for live data display and one for downloading up to 24 hours of recorded data from the device. The software isn’t actually half bad, but as usual it’s unfortunately Windows-only. It runs in Wine, but sadly fails to connect to the device.

The bundled driver is for the Silicon Labs CP210x series of USB to UART converters, which simplifies things considerably. Now it’s just a matter of sniffing some serial traffic. In Linux, dmesg agrees with this when the device is plugged in:

CP2102 detected!

CP2102 detected!

Fun fact: The CP2102 chip is also detected if I only plug in the bundled USB cable and leave the pulse oximeter itself disconnected. Apparently this cable has a built-in USB to UART converter! That’s a bit weird considering the mini-USB plug at the other end… Even though the device itself sports a mini-USB connector, it’s not actually USB compliant and it won’t work at all with regular USB cables. So don’t throw away the special “USB cable”!

Let’s look at some real-time data

The device has two modes: real-time data and recorded data (up to 24 hours). The former streams data via the USB connection as it’s measured, while the latter is useful for situations where a running PC would be impractical. The real-time mode offers relatively rich 60Hz data while the historical mode only supports 1Hz averaged pulse and Spo2 readings. Coding-wise the real-time mode is the simplest as its protocol is one-way, so it’s a good place to start. I’ll cover the recorded data mode in another post.

Anyway, I tried starting the live data application through API Monitor v2 like I did for the blood pressure monitor. Immediately when the live data application starts, it starts spamming the SetCommState API call in Kernel32.dll trying to open all available COM ports at 4800, 19200, and 115200 baud in quick succession. All three bitrates are configured as 8O1 (oddly enough). Perhaps this is in order to support several slightly different devices?

Another slightly odd thing is that the device is always transmitting data without any handshake. All the computer has to do is to open the right virtual serial port at 19200 baud 8O1. Yet another slightly odd thing is the protocol itself. Each data packet is 5 bytes long and according to some documentation found on the web, 60 packets are sent per second. The first byte always has its MSB set to 1 while the four others always have it set to 0. According to said documentation, the meaning of the 5 bytes are as follows (spelling errors and all):

10~3Signal strength for pulsate(0~8)
41=searching too long,0=OK
51=dropping of SpO2,0=OK
61=beep flag
7Synchronization,always be 1
20~6pulse waveform data
7synchronization,always be 0
30~3bar gragh (stand for pulsate case)
41=probe error,0=OK
6bit 7 for Pulse Rate
7synchronization,always be 0
40~6bit 0~bit 6 for Pulse Rate
7synchronization,always be 0
50~6bit 0~bit 6 for SpO2
7synchronization,always be 0

I think this information is for a slightly different model (hence the 4800 baud setting instead of my device’s 19200 baud), but it seems legit. In any case, I have yet to see data that doesn’t make sense according to this table.

Like last time, I have implemented a small python script that is able to connect to the device. It takes a few command line arguments and outputs a CSV file with the received data.

All code for this project can be found on GitHub.

Next up: Retrieving historical data for the last 24 hours… Stay tuned…


Manuel · 2015-04-04 at 00:16

My name is Manuel, from Spain.
Congratulation for your article!

I would like to work with this oximetre but previously I would like to run your code. In this moment I have not the oximetre. Would be possible get us a example of string (bytes) that the oximetre send by serial?
The idea is create a big file with these strings and send its by “serial emulator”

Regards and wonderful code!!!!

    atbrask · 2015-04-06 at 20:36

    Thanks for your comment. I haven’t got the device by my computer right now. I’ll try to update the post with some example data one of the coming days.

Angus · 2016-02-08 at 02:41

Nice work,

I have got your code working, and it seems to work well.

Do you know if it is possible to control the device via the serial interface? I would like to turn the device on and off, so that the laser is not running all the time (according to the manual, the device should not be worn for extended periods (more than 2 hours))

Cheers, Angus

    atbrask · 2016-02-08 at 13:30

    Thank you! 🙂

    No, I’m afraid not. To my knowledge there is no built-in way to turn the device on or off remotely from the PC. As far as I remember, there is no such feature in the bundled Windows software. Nothing is impossible, of course, but not without making hardware modifications to the device. I suppose you could add a small microcontroller that listens in on the serial line for custom turn on/off commands.


      TJ · 2016-05-28 at 10:34

      Hello atbrask, Can I have your email. I was make similiar logger for raspberry Pi can we talk on email because the explanation is too long in the comment.


        Theo Fountain · 2017-02-25 at 07:51

        I’m working on a similar project. DId you ever make any progress? I’ve only had the pulse ox a couple hours and have not even tried interfacing with the RPi yet so any help at all would be greatly appreciated!

          O · 2018-08-22 at 20:46

          You ever get yours working?

        O · 2018-08-22 at 20:46

        Any luck with either of you guys?

christophe · 2016-06-09 at 09:43

Hello !

Thank you for your article.
I have ordered the same CMS Modell directly from China. (cheaper :-))
The device is working well on the PC with the delivered Driver.

However, I don’t get data with your python script.
Could it be that the Serial communication is different
(tried with 19200 baud 8O1) ?


    atbrask · 2016-06-09 at 20:15


    Not even live data? That is weird. Have you tried changing the script to use one of the other possible baud rates? (4800 baud and 115200 baud)

    If you still don’t get anything, you could try to connect to the device using a serial monitor. The device is supposed to start streaming live data immediately when turned on. There is no handshake or anything.

    If nothing happens, they have probably changed the protocol. In that case you’ll have to sniff the serial communication while the bundled application is running. There are various tools for this (e.g. by using API Monitor as described above).

Siswanto · 2016-11-02 at 05:33

i’m siswanto from Indonesia
have you tried CMS50D+ Ver4.6? it’s different.
i have tried and got data like below
115200 – 8 – none – 1 – XON/XOFF
01 E0 86 94 92 D0 E1 FF FF
01 E0 86 99 93 D0 E1 FF FF
01 E0 86 A0 94 D0 E1 FF FF
01 E0 86 A8 95 D0 E1 FF FF
01 E0 86 B0 96 D0 E1 FF FF
01 E0 86 B9 97 D0 E1 FF FF
01 E0 86 C0 98 D0 E1 FF FF
01 E0 86 C6 98 D0 E1 FF FF
01 E0 86 C9 99 D0 E1 FF FF
01 E0 86 C9 99 D0 E1 FF FF
01 E0 86 C9 99 D0 E1 FF FF
could you help me to analysis the data?


    atbrask · 2016-11-02 at 12:45


    Unfortunately, I don’t own a version 4.6, so it’s a bit difficult for me to give good advice on it. It is indeed quite different from the version I have. But I do have a few observations..
    All bytes (except the first byte in every “packet”) have their most significant bit set. This indicates to me that the protocol uses this bit to tell whenever a new packet starts. And two of the bytes seem to be changing alot. You could try to compare the lower 7 bits of the 4th and 5th byte with whatever the device says on its display while running.

Martijn · 2017-03-03 at 18:10


I have also bought version 4.6 and I am not able to get the live data from it :/
I only need the live data, so i don’t care about recorded data.
Your script is only giving some weird characters.
Can you give me a quick guideline how I can do what you did, but now for v4.6?
I am not familiar with sniffing serial communication :/

Mike · 2017-03-18 at 02:14

I am trying this project with a Pi3. Has anyone tried that yet with a ver 4.6 cms50D+?


Leo · 2017-03-27 at 02:22


Thanks for the code! However, I ran into a problem with the live data feature. When I ran the code to take live data, it said, “Press CTRL-C or disconnect the device to terminate data collection.” Instead of continuing to record, it immediately ended without me doing anything. When I looked at the file, it only showed the headers without any actual data. I followed all of the steps you provided. Is there something I am missing?


    Ewa Nowara · 2017-07-06 at 20:36

    Hi Leo,
    I am having the same issue – the code displays “Press CTRL-C or disconnect the device to terminate data collection.” but no data is saved into the csv file except for the headers.
    Were you able to resolve this problem?
    Thank you.

      atbrask · 2017-07-07 at 07:32

      That is strange. It doesn’t do anything at all? Is it one of the newer “v4.6” devices? If so, I’m afraid I can’t really help..

      Ewa Nowara · 2017-07-21 at 15:32

      Actually, it seems that the new model now requires a handshake. You need to figure out what command to send to the device before it starts streaming the data and the order in which the data is sent is now different from the older model. We had to change the baudrate and other parameters as well. You can figure out what message to send to the device and how it is streaming the data by using a packet sniffer once it starts streaming the data to the provided SpO2 Assistant visualization software.

        Lalit Hissaria · 2018-01-19 at 08:11

        Hi Ewa,

        I am also facing the same issue.
        For sure there is some handshaking going on. If you have resolved this issue request for your help.

        Thanks in advance.

Patrick Samy · 2017-05-12 at 11:35

Hi folks,

It seems that version 4.6 doesn’t stream live measurements anymore. I’m able to download recorded data however.

There is nothing to read the serial port, it should just stream without the need to write anything right?
Has anyone had more luck with this version?


sunshine · 2017-06-06 at 17:19

Sorry for late answer.
I’ve been starting recording with: python ./ LIVE COM8 foo3.csv. You should adjust your COM port (see port with Device manager).

Ben · 2017-06-08 at 12:53

Very useful info on this device; thanks! Much easier than using the Windows software in a VM to get the data.

Simon · 2017-11-15 at 15:51

I would like to display the pulsoximeter data with C. I tried to read from ttyUSB0 port the data but I got only dummy data. I also tried with PUTTY, the result was similar. I used 9600 and 19200 baud rate.

Low · 2018-03-26 at 06:46

Nice work,

I want to know how much the sampling rate of this device can be?

    atbrask · 2018-03-26 at 07:45

    Thanks! I don’t know about the later revisions of this device, but mine samples at 60 Hz in live mode and at 1 Hz when recording.

Shachar Weis · 2019-03-03 at 02:09

Contec Oximeters sold after 2017 have version 4.6, which doesn’t work with this code. They device does not stream live data automatically. They seem to have added some kind of handshake, which I tried to replicated using a port sniffer but it doesn’t work for some reason.

Todd Stiers · 2019-07-16 at 03:10

# 2019-0715 modification to atbrask code to generate live output from CMS50D+ from

def get_raw_data(ser, outfile):
sys.stdout.write(“Connecting to device…”)
f = open(outfile, ‘wb’)
raw = list(
while len(raw) >= 9:
# print ord(raw[5]) & 0x7f, ord(raw[6]) & 0x7f
# print ord(raw[2]), ord(raw[3]) & 0x7f, ord(raw[5]) & 0x7f, ord(raw[6]) & 0x7f
row = “%f %d %d %d” % (time.time(), ord(raw[3]) & 0x7f, ord(raw[5]) & 0x7f, ord(raw[6]) & 0x7f)
print(“%s” % row )
raw =
if len(raw) <= 1:
print("no data received. Is the device on?")
return raw

Carlos Rodriguez · 2020-08-30 at 20:58

I shared my code it works, 100% tested

    P-J · 2022-10-01 at 18:21

    Thanks Carlos! Your code works great for my CMS50D+ device manufactured in august 2022.

    For others: if you also want to capture the ppg signal (waveform) then it is stored in the fourth bit of Carlos’ serial data object.

    Sp02 = str((serial_data[6] & 0x7F))
    PRbmp = str((serial_data[5] & 0x7F))
    wave = str((serial_data[3] & 0x7F))

How to check MSB at the start of a serial data packet in C? – TheInstaPreneur · 2017-04-03 at 22:09

[…] am adapting code from a similar project to record live data from a USB Pulse Oximeter. I have gotten everything working in python, but I […]

Leave a Reply

Avatar placeholder

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