Pulse Oximeter Data Capture With Raspberry Pi

About: I am interested in how everything is made and want to be able to design/make/fix anything. Most of my experience is with woodworking, but I love learning to work with other materials. My projects are usually...

Intro: Pulse Oximeter Data Capture With Raspberry Pi

My daughter has some health issues that requires her to be hooked up to a pulse oximeter at night that monitors her oxygen level and heart rate. We have night nurses so we can sleep, but sometimes we wake up at night and want to "check her numbers" without going into her bedroom and possibly waking her up (our daughter is usually a very light sleeper). The pulseox does have (very loud) alarms that go off according to limits we set, but there are subtle trends that we may watch for according to what type of day she had. Again, the standard way to check these in the middle of the night would be to walk into her room, look at the current numbers and listen to how she is breathing, and then ask the nurse about any "non-alarm" trends she may have noticed. I knew there had to be a better way to do this.

The simplest approach seemed to just put a baby monitor with video in front of the pulseox device and then we could bring up the camera on our phone to see the numbers. Besides the cost and quality issues of most nightime baby monitors, there did not seem to be a location in the room to put the baby monitor that would not either block the nurse's view of the numbers or be too far away from our daughter for the audio to pick up well. We also already own an audio baby monitor that works very well, so it seems a waste to buy another one just to add the remote video to see the current numbers and not even tell us average/trend information.

The device that the home health company provides us is a Masimo RAD-8. It has a serial/RS232 port on the back of it. I was happy to find that the data coming off the port is not protected, so getting the data from the pulseox device to a remote display seemed possible (similar to how they do it at a hospital's nurse station). I chose to use a Raspberry Pi to capture the data, process it, and present it on a web server where we could then load a web page with our phone. The main reasons for choosing this device were low cost, small size, and it has an established community in case I ran into any problems.

Step 1: Hardware List

  1. Masimo RAD-8 Pulse Oximeter
  2. Raspberry Pi Model B+ with power adapter
  3. Wifi dongle (optional)
  4. RPi case(optional)
  5. 8GB MicroSD (you can find these with Raspbian pre-installed)
  6. USB->Serial/RS232 Adapter Cable (I had one already from an old exercise bike, but here is a similar one on Amazon for ~$20. Note that the chipset in mine is in the FTDI D2XX family. This seems to be a popular chipset and it does work on RPi, but other cables/chipsets may not)
  7. RS232 extension (optional)

You can buy kits online that package #2-5 above for $50-$60. If you don't have the #6 like I did and want the #7 extension cable, this entire project from scratch will cost about $90. I am not including the cost of the actual pulse oximeter.

Step 2: Software List

On the Raspberry Pi:

Optional software used:

All of this software is free. I won't go into detail on how to configure all the layers as there are install guides from each link above. Most were installed simply using the "sudo apt-get install " command, as the default respositories in Raspbian were able to find them.

The zip file attached to this step contains 2 files

  • poxs.php is the script for the background process that collect data and inserts into the database
  • gpulse.php is the web page code that retrieves from the database and displays output

Step 3: The Raw Data

First I need to see the format of raw data coming out of the machine. I hooked up the usb->serial cable from the RPi to the back of the pulseox machine, put the sensor on my finger and turned on it.

I needed to know the name of the usb->serial adapter device on the RPi so I could watch it for incoming data. I found this with the following commands:

pi@raspberrypi ~ $ lsusb
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
Bus 001 Device 005: ID 0403:6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC

The last line was obviously my device and now I know the type of chip in it and who made it. There was a device listed in the /dev directory called ttyUSB0, so this was probably the right one. To confirm, I checked the output from dmesg just after connecting the cable:

pi@raspberrypi ~ $ dmesg | grep FTD
[    3.711017] usb 1-1.5: Manufacturer: FTDI
[    3.718342] usb 1-1.5: SerialNumber: FTDDCQ7W
[    9.429993] usbserial: USB Serial support registered for FTDI USB Serial Device
[    9.929322] ftdi_sio 1-1.5:1.0: FTDI USB Serial Device converter detected
[   10.722481] usb 1-1.5: FTDI USB Serial Device converter now attached to ttyUSB0

Every terminal device has specific settings it expects, such as connect speed, bits per character, etc. I found an Operator's Manual for this pulseox through a Google search that gave me the info I needed to setup using the stty command:

  • Baud Rate: 9600
  • Bits per character: 8
  • Parity: None
  • Bits: 1 start, 1 stop
  • Handshaking: None

Even if I didn't find these settings from a web search, they are fairly common (8-N-1) settings for a device like this, so some settings can be left default or guessed in lieu of a spec, without impact to the simple code for this project. The command needed to setup according to above:

stty -F /dev/ttyUSB0 9600 cs8 -parenb -cstopb -crtscts

The last thing to do is use the cat command to check for output:

cat /dev/ttyUSB0

12/30/14 22:40:35 SN=0000057681 SPO2=096% BPM=106 PI=01.68% SPCO=--.-% SPMET=--.-% DESAT=-- PIDELTA=+-- ALARM=0000 EXC=000800

Great, so the data looks pretty simple to parse, feed into a database, and present on a web page. Besides the date/time, the only pieces that I want right now are percent oxygen saturation (spo2) and heart rate (bpm). If anyone knows the purpose of the other fields, I have some academic curiousity but don't need them for this project.

Step 4: Create Database

SQLite is a very simple to use database. From the command line I executed the following:

sqlite3 pulseox.db "CREATE TABLE pulseox(stampdate text, stamptime text, spo2 text, bpm text, id integer primary key)"

This will create a file called pulseox.db in the current directory that contains a single table as defined above. You will notice that I am being pretty lazy using text datatypes. I could parse the text as it comes off the serial port and convert it to a specific type, but my php and javascript code for selecting and displaying the data doesn't care much about datatypes, so I decided to leave the table definition as simple as possible. I may change it later if it causes a problem.

To test the database and the id column, I issued the following insert/select command:

sqlite3 pulseoxdb "INSERT into pulseox(stampdate, stamptime, spo2, bpm) values('12/30/14','08:00:00','SPO2=100%','BPM=100');SELECT * from pulseox"

12/30/14|08:00:00|SPO2=100%|BPM=100|1

Now that the database is setup, I can move on to parsing realtime data and inserting it into this table.

Step 5: Script to Capture and Insert Data

I am running a shell script as a background process that opens the serial device and continually checks for data. When it reads a line, it will chop it up (from the raw data, a single space is the delimiter) and insert a row into the database. I will be using PHP as the scripting language and PDO as the api extension to connect to the database. Below are the just the key parts of code - the entire file is called poxs.php and is in the zip file attached to the "Software" step above.

$ser = fopen("/dev/ttyUSB0","r");

$dbh=new PDO('sqlite:/var/www/pulseox.db');
$query=$dbh->prepare("INSERT into pulseox (stampdate, stamptime, spo2, bpm) VALUES ( :param0, :param1, :param2, :param3);");

while (!feof($ser)) {
  $buffer = fgets($ser);
  $din = explode(" ", $buffer);
  $query->bindParam(':param0', $din[0]); # stampdate - 12/30/14
  $query->bindParam(':param1', $din[1]); # stamptime - 08:00:00
  $query->bindParam(':param2', $din[3]); # spo2 - SPO2=100%
  $query->bindParam(':param3', $din[4]); # bpm - BPM=123
  $query->execute();
}

For brevity, I have removed error detection from the code above, such as checking if the serial device could not be opened, or checking for various database errors.

As each line is read, the "explode" command chops the line into each piece of data. Then each piece of data is bound to the correct location in the database SQL statement before executing the SQL.

Step 6: Web Page to Display the Data

I want to display the last reported oxygen level and heart rate on a web page. First I put some PHP code into a simple web page to retrieve the data. In the next step I will improve the presentation:

$dbh = new PDO('sqlite:/var/www/pulseox.db');
$query=$dbh->prepare("SELECT stampdate, stamptime, spo2, bpm from pulseox order by id desc limit 1");
$query->execute();
$result=$query->fetch();

echo "$result[0] $result[1] $result[2] $result[3]";

So far, we only get the following result:

01/03/15 07:33:18 SPO2=094% BPM=090

I also want to show on the web page the average oxygen level and heart rate over the last hour. The following code is how I get those 2 values:

$query=$dbh->prepare("SELECT round(avg(substr(bpm,5,3)),1), round(avg(substr(spo2,6,3)),1) from pulseox where bpm != 'BPM=---' and spo2 != 'SPO2=---%' and id > ((select max(id) from pulseox) - 3600)");
$query->execute();
$result=$query->fetch();

$avg_bpm = $result[0];
$avg_spo2 = $result[1];

A few notes on the above code:

  • The "substr" functions are used to strip down the data in the table ("SPO2=094%") down to usable numbers ("94")
  • The "max(id)... minus 3600" find the latest entry and backs up 1 hour / 3600 rows, as there is usually 1 reading per second.
  • The "round" function makes sure we calculate to 1 decimal place to right, i.e. 94.5
  • The filters "where bpm/spo2 !=" gets rid of rows where the pulseox machine was collecting data but had no data. This can happen when the sensor is not on correctly or needs to be replaced from wear.

Step 7: Adding Gauges With Google Visualization

Now that I have our 4 pieces of data (bpm, spo2, avg bpm, avg spo2), I can use gauges to visualize the results in relation to value ranges that our daughter usually has. After looking around at different libraries, I chose to use Google Visualization for this. It seemed pretty straightforward Javascript to use, had the features I wanted, and did not require any local install on the web server.

The php code in the previous step that generated the data will be used to set the values on these gauges. Below is the code that converts the php results to a format that Google Visualization can understand. This code also set the text label for each gauge.

var bpm_data = google.visualization.arrayToDataTable([['Label', 'Value'],['BPM', <?php echo $chart_bpm_data; ?>]]);
var spo2_data = google.visualization.arrayToDataTable([['Label', 'Value'],['SPO2', <?php echo $chart_spo2_data; ?>]]);
var avg_bpm_data = google.visualization.arrayToDataTable([['Label', 'Value'],['AVG BPM', <?php echo $chart_avg_bpm_data; ?>]]);
var avg_spo2_data = google.visualization.arrayToDataTable([['Label', 'Value'],['AVG SPO2', <?php echo $chart_avg_spo2_data; ?>]]);

Next up is setting the color ranges on the gauges - remember that higher is better for oxygen levels, but lower is (generally) better for heart rate:

var BPM_Chart_Options = {
  min: 0, max: 200,
  greenFrom: 60, greenTo: 130,
  redFrom: 160, redTo: 200,
  yellowFrom:130, yellowTo: 160,
  minorTicks: 5,
};
        		    
var SPO2_Chart_Options = {
  min: 0, max: 100,
  greenFrom: 90, greenTo: 100,
  redFrom: 50, redTo: 80,
  yellowFrom:80, yellowTo: 90,
  minorTicks: 5,
};

Finally I use the following Javascript to create the actual gauges, and then assign the data/options from above to each:

var bpm_chart = new google.visualization.Gauge(document.getElementById('chart1'));
bpm_chart.draw(bpm_data, BPM_Chart_Options);

var spo2_chart = new google.visualization.Gauge(document.getElementById('chart2'));
spo2_chart.draw(spo2_data, SPO2_Chart_Options);

var avg_bpm_chart = new google.visualization.Gauge(document.getElementById('chart3'));
avg_bpm_chart.draw(avg_bpm_data, BPM_Chart_Options);

var avg_spo2_chart = new google.visualization.Gauge(document.getElementById('chart4'));
avg_spo2_chart.draw(avg_spo2_data, SPO2_Chart_Options);

As in previous steps, the full contents of this file (gpulse.php) can be found in the zip attached to the "Software" step above. I put that file in my web server home directory and then opened the page from my phone. The screenshot can be found above.

Step 8: Final Thoughts

I enjoyed this project, and it makes a big difference to us. We still get worried and wake up in the middle of the night, but the combination of a baby monitor and this pulseox setup and our great night nurses reduce the number of trips (and possible disruptions) to our daughter's room. We have also had doctors ask us to record and share data with them, instead of having her come in for a short stay to record data. Now I can just go to this database and pull out exactly what I need.

I had to include a picture of me and my little princess decked out in her favorite color purple this past Halloween - she's awesome and a lot of my projects are for her!

I hope you have learned something from this Instructable to apply to your data capture project - let me know if you have any questions or comments.

Share

Recommendations

  • Audio Contest 2018

    Audio Contest 2018
  • Fix It! Contest

    Fix It! Contest
  • Furniture Contest 2018

    Furniture Contest 2018

38 Discussions

0
None
yahnatan

2 years ago

This is a great howto. For anyone who follows these instructions and, after hooking up the serial port of your Rad8 to your input device, doesn't see any input: try changing the settings on your Rad8 serial port output from ASCII 2 to ASCII 1. (Search for a Massimo Rad8 manual for details on how to do that.)

4 replies
0
None
sterlingwhittenyahnatan

Reply 12 days ago

I know this is old but thank you very much for this comment. Trying to get to the setup menu level 3 on that old Massimo Rad8 was probably the most difficult part of this!

0
None
yahnatanyahnatan

Reply 2 years ago

I chose Elasticsearch as my database and Grafana for visualization. Here's the end result: https://youtu.be/t2B6XVP6vvs

0
None
timbarnesyahnatan

Reply 2 years ago

Wow - just watched the video and that is absolutely awesome - nice job!

0
None
yahnatantimbarnes

Reply 2 years ago

Thanks! Just posted the scripts (not pretty, but they work) to github: https://github.com/yahnatan/pulseox

0
None
sterlingwhitten

12 days ago

Great idea, I know this is old but thanks! I'm doing basically the same thing but using mostly Python. My one year old daughter might need this for quite a while.

0
None
VolkmarH1

5 months ago

Now, that is one great project with a really great benefit for you and your daughter. Well done!

0
None
BarrettK

10 months ago

I know it has been awhile since this was created, but I hope someone can help me. I have everything working except for the webpage. I have gpulse.php served, but all the webpage says is "Updated: @". Data is going into the database. I have confirmed this on the command line. I can even create a table on a test webpage and see all the data on webpage. pulseox.db is located at /var/www/pulseox.db . Can anyone help?

0
None
LissethM

1 year ago

Hi Tim! I sent you a message, I hope you repl! It's pretty important to me

0
None
RogerC54

2 years ago

Here's what I ended up with. The hour averages didn't really matter much to me, so I swapped out those gauges for a line chart that shows the last 10 minutes of data. Works great! I also wrote another webpage so that I can query any time period to look back.

IMG_2148.PNG
0
None
RogerC54

2 years ago

This is awesome - we have the same machine. My son is also on a pulse-ox at night and during naps. I am trying to get this set up and running, but when I try reading the serial port via cat /dev/ttyUSB0 all I get is a bunch of garbage. I tried changing the output on the pulse-ox, but no go. The serial-USB is using a CH341. Any suggestions?

6 replies
0
None
timbarnesRogerC54

Reply 2 years ago

Roger - what are your TTY settings? I have found certain settings can cause garbage. See above where I talk about settings I have used.

0
None
RogerC54timbarnes

Reply 2 years ago

Tim - thanks for responding! I used the same settings that you outlined (we also have a Masimo RAD-8) using the following command:

stty -F /dev/ttyUSB0 9600 cs8 -parenb -cstopb -crtscts

I also tried setting a few different baud rates with no success. Are there any other settings that you recommend trying? If it matters, I am running Debian version 4.1.17. Hopefully I can make some progress this weekend.

0
None
timbarnesRogerC54

Reply 2 years ago

Roger - before I settled on those settings, I think the following command at least gave my output that was not mangled - maybe try this as well:

stty -F /dev/ttyUSB0 raw

0
None
RogerC54timbarnes

Reply 2 years ago

Thanks for the tips. Turns out the cheap serial to USB converter that I bought was not quite up to snuff. After a long time scouring forums, I was finally able to get it working by modifying the driver, recompiling, and then reloading. It was WAY more than I wanted to do, but I learned a lot. I am now getting the reading from the pulse-ox. Now to get the rest of it up and running...

Again, thanks for the tutorial. I have been wanting to get something up and running for our son so that my wife can watch his numbers from anywhere in the house instead of constantly checking in on him when he is taking a nap or has gone to bed early. This will make a big difference!!

0
None
RogerC54RogerC54

Reply 2 years ago

Got it up and running over the weekend. Had to make a few tweaks to get everything working as expected, but this tutorial was a great guide. Thanks again.

0
None
timbarnesRogerC54

Reply 2 years ago

Roger - glad you got the issue worked out!

0
None
janczol

2 years ago

Very nice! Maybe I'll try this out! And how about RPi CPU&memory load? Is it high?

Great project, and very nice!

Pulse oximeters are useful if you suffer from asthma.

<a href="https://pulsioximetro.wordpress.com/pediatrico/">https://pulsioximetro.wordpress.com/pediatrico/</a>

0
None
RyanB16

3 years ago on Introduction

Thank you so much for posting this!!! I definitely want to build this myself!!

By the way, about your academic curiosity of the interpretation of the data, i can make some decent guesses here (i'm a nurse).. in reference to the following line:

12/30/14 22:40:35 SN=0000057681 SPO2=096% BPM=106 PI=01.68% SPCO=--.-% SPMET=--.-% DESAT=-- PIDELTA=+-- ALARM=0000 EXC=000800

Now i'm only making educated guesses here so take it as a grain of salt, but on pulse-ox's they all should have de-sat (desaturation) limits where you can set at what SpO2 (which is oxygen saturation in the blood vs PaO2 which is the partial pressures of the gas itself in the blood), in this case, the SaO2 should have a de-sat limit where it dings, and then there is also usually a critical threshold where it will really ding (blink red and ding louder and faster); so for example, i want it above 92% so alarm if it drops below 92, but if it hits 85% or below then i want to get in there and fix it so it doesn't go into the 70s and become a serious problem, so i set a critical of 85%. So, the desat is written clearly there im sure another value is for a critical level. The SPCO field is probably a carbon monoxide level (some more expensive machines have this built-in), where i work we had a couple of those in the ER where the pulse-ox had a CO (carb monoxide) reading. I'm not sure what the PIDELTA is for, but i know that when i watch the monitors we look at waveform on the device (shitty waveforms indicate interference where the infrared light either isn't penetrating to the other side of the sensor or their hands are cold and it isnt capturing the blood flow through the fingers--Pulse-oxs work by shining infrared light through and measuring absorbance as Hemaglobin in the blood will absorb taht spectrum, and hemaglobin is a 4-part iron molecule that binds to oxygen--its what carries it on the blood cells and delivers it to your body)--so, if there is interference with either the light shining through or the vascular flow, it isn't able to capture properly and will show-up with a poor wave-form and that tells you the reading is not accurate. Thats my best guess on that one. Lastly, the SPMET, again i have no idea but my best guess would be the only other thing i can think of on pulse-ox's which is measuring End-tidal CO2 (carbon dioxide).. now this wont be on regular finger-stickers/probes, but some (again more expensive ones) come equipped with a connection to attach to a nasal cannula where it can detect the amount of CO2 you exhale during breathing, when you breathe, taking in oxygen is just as important as exhaling CO2, the explanation is better left to wikipedia on that one but basically more CO2 makes the blood acidotic and it can mess up heart rhythms among other things--too much and you die.

Anyways, if you figure it out PM me for my academic curiosity, thanks again for doing this write-up this is super cool!!