Pulse Oximeter Data Capture With Raspberry Pi

Introduction: 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 f…

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.

2 People Made This Project!

Recommendations

  • DIY Summer Camp Contest

    DIY Summer Camp Contest
  • Fandom Contest

    Fandom Contest
  • Fruit and Veggies Speed Challenge

    Fruit and Veggies Speed Challenge

49 Comments

0
BarrettK
BarrettK

3 years 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
bonsett
bonsett

Reply 8 months ago

I'm having the same issue but I know why but don't know how to fix. The pulseox sends a null record every other one so when the gpulse.php is loaded it is reading a null record. If I delete the null records the gpulse.php works. Also need help on how to insert (Now) instead of using the time stamp from the pulseox. My son is on his pulsox 24/7 and this is a great solution for remote viewing sats and hr.

0
bschae1
bschae1

Reply 7 months ago

I added a check to see if I got empty data that seemed to resolve it for me:

$din = explode(" ", $buffer);
#echo "Exploded: date $din[0] time $din[1] sp02 $din[3] bpm $din[4] \n";
if (empty($din[1]))
{
continue;
}
$query->bindParam(':param0', $din[0]);
$query->bindParam(':param1', $din[1]);
$query->bindParam(':param2', $din[3]);
$query->bindParam(':param3', $din[4]);
$query->execute();
}

0
BarrettK
BarrettK

Reply 7 months ago

Sadly I can't find any of my code. It has been awhile since i use it or needed it!!!! What I ended up doing was just writing the data to a mysql database and then using grafana to display it. Grafana does queries of the database so you can do "where not null" and then graph it or display it "live". You can set up alerts base on thresholds. I have grafana running on a debian machine, but it can be run in a docker image, linux, mac, windows or ARM. It was awesome. https://grafana.com/get

Pulseox.jpg
0
janczol
janczol

5 years ago

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

0
bschae1
bschae1

Reply 7 months ago

top - 00:13:28 up 1 day, 6:35, 2 users, load average: 0.04, 0.14, 0.15
Tasks: 145 total, 1 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.9 sy, 0.0 ni, 98.6 id, 0.4 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7875.9 total, 4261.5 free, 97.5 used, 3516.9 buff/cache
MiB Swap: 100.0 total, 100.0 free, 0.0 used. 7504.7 avail Mem

0
RogerC54
RogerC54

5 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
bschae1
bschae1

Reply 7 months ago

I borrowed this idea from you. Thanks!

Screenshot_20201211-235647_Chrome.jpg
0
JonahP6
JonahP6

Question 2 years ago on Step 8

Hey Tim!! This is so awesome, thank you for posting!!! I bought the gear and am jumping in.

Our son has the same pulse ox. He’s our miracle dude :).

I am really wanting to know: did you (or anyone here) ever find out how to SEND a silence alarm signal to the unit? Would be really helpful sometimes when we are remotely monitoring from other room (we don’t have a nurse).

Thank you!

0
bschae1
bschae1

Answer 7 months ago

From what I see, the serial port is output only (according to the manual). It may be possible with the other port for nurses, but I haven't gotten that far yet.

0
archangel5884
archangel5884

Question 1 year ago

Tim, do you have any tips for getting the gpulse.php script working? When I run it in the PHP program from command line, it just shows me whats in the script on the CLI. It also mentions some type of error in creating the gauges from the google gauges you used. Any help would be greatly appreciated. (Almost everything went smoothly, except for that, and that I am not getting any data from the pulse Ox, which means I need to change the output settings on the device.) I can post images next time I boot my Rpi (3 B+, if that matters)

0
bschae1
bschae1

Answer 7 months ago

As someone else wrote, I had to change the output to ASCII1 to get data.
A few things to note:
-You need to make sure the poxs.php is always running in the background using your favorite method (init script, cron, etc.)
-the gpulse.php gets run from the browser. If you used nginx and placed the file in /var/www/html for example and your Pi IP is 192.168.1.2, then you would launch http://192.168.1.2/gpulse.php from the browser. Note that you may need to tweak some settings to ensure it doesn't just download the file.

0
yahnatan
yahnatan

5 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.)

0
bschae1
bschae1

Reply 7 months ago

Why didn't I read this a long time ago...it took me forever (and 2 serial cables) to try changing the output type, but finally got it. No idea why ASCII2 isn't working...

0
sterlingwhitten
sterlingwhitten

Reply 2 years 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
yahnatan
yahnatan

Reply 5 years ago

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

0
timbarnes
timbarnes

Reply 5 years ago

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

0
yahnatan
yahnatan

Reply 5 years ago

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

0
sterlingwhitten
sterlingwhitten

2 years 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.