Creating a python module for the Contec CMS50D+ pulse oximeter (Part I)

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):

bytebitcontent
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
51=searching,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…

Creating a Python module for the Beurer BM 65 blood pressure monitor (Part II)

In my last blog post, I reverse engineered most of the USB protocol of the Beurer BM 65 blood pressure monitor. The only thing left was the first byte in each 9-bytes-measurement. I figured it was probably some status bits about detected cardiac arrhythmia. But that just isn’t good enough. I want total knowledge of the protocol, damn it!

We could perhaps reverse engineer the meaning of this byte too by trying all possible values of the first byte against the data downloader in Beurer Health Manager. Then it would be a simple comparison between the byte-value and the reaction. We already know that 0xAC means a normal reading. But what about the other 255 possibilities?

I came up with a few different ways of determining this:

  • Fake/make a measurement with cardiac arrhythmia using the actual device. I just don’t know how easy it is to fool it. And even if we succeed, we would still have incomplete knowledge about the first byte.
  • Use API Monitor to intercept the relevant ReadFile API call and change 0xAC to something else. This is the simplest approach, but very manual and tedious.
  • Program an Arduino to simulate the device. Remember that the Health Manager program spams all COM ports until it gets a response. A fun fact here is that while the program does check for the presence of the Prolific 2303 driver, it’ll happily try any old COM port. And an Arduino would be easy to program in such a way that it reacts like the real device. Besides, this approach would also yield the most Blogosphere points..
  • Implement a fake virtual COM port in Windows. With this, one can simulate the device in software.
  • Use a decompiler to inspect the code within Health Manager itself. All this reverse engineering would have been easier altogether if I’d just done this in the first place. But it feels like cheating. Let’s get back to this one later..

After a bit of consideration, I chose the approach with the virtual COM port. However, instead of creating a virtual COM port driver from scratch, I’m using the com0com project. It creates two virtual COM ports and links them, acting like a null-modem. Then I can code the actual logic in plain Python.

Virtual null-modem between virtual COM ports

Virtual null-modem between virtual COM ports

By default, Windows 7 x64 requires signed kernel-mode drivers, and the com0com driver is only test signed. Fortunately, this security limitation can be easily removed. I then linked the virtual COM ports 5 and 6. Health Manager will try them in ascending order, so by attaching my Python code to COM6, I could fool Health Manager into believing there’s a blood pressure monitor on COM5.

And now for a bit of good news followed by a bit of bad news.. The good news is that my scheme of setting up this virtual null-modem works like a charm! Health Manager is easily fooled into believing that it’s communicating with a real device on COM5. However, the bad news is that after iterating through all combinations for the first byte in the 9-byte measurements, my Python script only triggered a new reaction from Health Manager with the value 0xA9 (169). And this reaction was to reject the entire data transfer! Not a single cardiac arrhythmia-indicating result was reported. Hmm.. That’s unexpected..

Finally, I succumbed to my curiosity and disassembled the Health Manager binary using ILSpy, It’s just regular .NET code. After a bit of digging, I could confirm my new findings. While the first measurement byte (called the header) is recorded, it is never used except for rejecting the transfer if it’s equal to 0xA9. Bummer..

Well.. You can’t win them all..

Hello world!

Hello everybody!

On this blog I’ll document my various weird projects as they progress. I’ll also create pages for my other projects. Oh, and random ramblings! Don’t forget the ramblings.

I don’t assume that I’ll ever get a huge following on this blog. That’s OK. It’s mostly for documenting my own projects. As far as possible, everything I make and post here will be open sourced and put on GitHub or similar repository sites. As we say at my friendly local hackerspace Open Space Aarhus:

BUILD WHAT YOU NEED, SHARE WHAT YOU BUILD, BE AWESOME.

Wise words indeed. Let’s get started!

/atbrask