Reverse Engineering A Bluetooth Low Energy Oximeter

Just as COVID-19 is spreading in Bangalore. I got an Oximeter which can measure SpO2. SpO2 is also called as oxygen saturation, According to Wikipedia

Oxygen saturation is the fraction of oxygen-saturated hemoglobin relative to total hemoglobin (unsaturated + saturated) in the blood. The human body requires and regulates a very precise and specific balance of oxygen in the blood. Normal arterial blood oxygen saturation levels in humans are 95–100 percent. If the level is below 90 percent, it is considered low and called hypoxemia.Arterial blood oxygen levels below 80 percent may compromise organ function, such as the brain and heart, and should be promptly addressed.

I got a Bluetooth one, so I could write some scripts to read the measurements remotely and setup some alerts. Say we have an oximeter attached to an elderly person. Its connected to your phone via Bluetooth. Your connected phone will ring if the oxygen saturation level goes down. That way the care-taker doesn't need to sit next to the patient whole day or night.

Bluetooth Oximeter
Bluetooth Oximeter

BLE - Bluetooth Low Energy has a standard profile called PLXP - Pulse Oximeter Profile for Oximeters. The GATT profile has a characteristic defined called PLX Continuous Measurement Characteristic. It is supposed to send the SpO2 details in continuous mode. You can read more about in this official pdf document. The official name and UUID for this characteristic is

  • Name: PLX Continuous Measurement Characteristic
  • Uniform Type Identifier: org.bluetooth.characteristic.plx_continuous_measurement
  • Assigned Number: 0x2A5F
  • Specification: GSS

There is also a spot check Characteristic, for spot measures.

  • Name: PLX Spot-Check Measurement
  • Uniform Type Identifier: org.bluetooth.characteristic.plx_spot_check_measurement
  • Assigned Number: 0x2A5E
  • Specification: GSS

There is more, you can check the documentation for that. But in the Bluetooth device that I received, I didn't find a characteristic with UUID 0x2A5F ( full UUID 00002a5f-0000-1000-8000-00805f9b34fb). So I had to explore a bit. So I fired up my NRF Connect again and recorded a session with pulse Oximeter. The device was named "Mike" :). There was only one characteristic (49535343-1e4d-4bd9-ba61-23c647249616) for which I could enable notify. As soon as I enabled notify. I started seeing data. So I assumed that's the one sending the continuous measurement Below you can see the screen capture(ignore the background sound) and partial log.

 

 

nRF Connect, 2020-08-04
Mike (00:A0:50:1F:23:70)
I   17:56:18.401    [Server] Server started
V   17:56:18.424    Unknown Service (49535343-fe7d-4ae5-8fa9-9fafd205e455)
- Unknown Characteristic [N] (49535343-1e4d-4bd9-ba61-23c647249616)
   Client Characteristic Configuration (0x2902)
- Unknown Characteristic [W WNR] (49535343-8841-43f4-a8d4-ecbe34729bb3)
Unknown Service (000018f0-0000-1000-8000-00805f9b34fb)
- Unknown Characteristic [I N] (00002af0-0000-1000-8000-00805f9b34fb)
   Client Characteristic Configuration (0x2902)
- Unknown Characteristic [W WNR] (00002af1-0000-1000-8000-00805f9b34fb)
Unknown Service (e7810a71-73ae-499d-8c15-faa9aef0c3f2)
- Unknown Characteristic [I N R W WNR] (bef8d6c9-9c21-4c9e-b632-bd58c1009f9f)
   Client Characteristic Configuration (0x2902)
Device Information (0x180A)
- Serial Number String [R] (0x2A25)
- Software Revision String [R] (0x2A28)
- Hardware Revision String [R] (0x2A27)
- Manufacturer Name String [R] (0x2A29)
- Model Number String [R] (0x2A24)
V   17:56:18.792    Connecting to 00:A0:50:1F:23:70...
D   17:56:18.793    gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, preferred PHY = LE 1M)
D   17:56:19.027    [Server callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I   17:56:19.027    [Server] Device with address 00:A0:50:1F:23:70 connected
D   17:56:19.037    [Callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I   17:56:19.037    Connected to 00:A0:50:1F:23:70
D   17:56:19.070    [Broadcast] Action received: android.bluetooth.device.action.ACL_CONNECTED
V   17:56:19.085    Discovering services...
D   17:56:19.085    gatt.discoverServices()
I   17:56:19.476    Connection parameters updated (interval: 7.5ms, latency: 0, timeout: 5000ms)
D   17:56:19.795    [Callback] Services discovered with status: 0
I   17:56:19.795    Services discovered
V   17:56:19.821    Generic Access (0x1800)
- Device Name [R] (0x2A00)
- Appearance [R] (0x2A01)
- Peripheral Preferred Connection Parameters [R] (0x2A04)
Generic Attribute (0x1801)
- Service Changed [I R] (0x2A05)
   Client Characteristic Configuration (0x2902)
Unknown Service (49535343-fe7d-4ae5-8fa9-9fafd205e455)
- Unknown Characteristic [N] (49535343-1e4d-4bd9-ba61-23c647249616)
   Client Characteristic Configuration (0x2902)
- Unknown Characteristic [W WNR] (49535343-8841-43f4-a8d4-ecbe34729bb3)
- Unknown Characteristic [W WNR] (00005343-0000-1000-8000-00805f9b34fb)
- Unknown Characteristic [R] (00005344-0000-1000-8000-00805f9b34fb)
Device Information (0x180A)
- Manufacturer Name String [R] (0x2A29)
- Model Number String [R] (0x2A24)
- Serial Number String [R] (0x2A25)
- Hardware Revision String [R] (0x2A27)
- Firmware Revision String [R] (0x2A26)
- Software Revision String [R] (0x2A28)
- System ID [R] (0x2A23)
- IEEE 11073-20601 Regulatory Certification Data List [R] (0x2A2A)
- PnP ID [R] (0x2A50)
D   17:56:19.822    gatt.setCharacteristicNotification(00002a05-0000-1000-8000-00805f9b34fb, true)
D   17:56:19.825    gatt.setCharacteristicNotification(49535343-1e4d-4bd9-ba61-23c647249616, true)
I   17:56:19.880    Connection parameters updated (interval: 45.0ms, latency: 0, timeout: 5000ms)
V   17:56:25.078    Enabling notifications for 49535343-1e4d-4bd9-ba61-23c647249616
D   17:56:25.078    gatt.setCharacteristicNotification(49535343-1e4d-4bd9-ba61-23c647249616, true)
D   17:56:25.080    gatt.writeDescriptor(00002902-0000-1000-8000-00805f9b34fb, value=0x0100)
I   17:56:25.144    Data written to descr. 00002902-0000-1000-8000-00805f9b34fb, value: (0x) 01-00
A   17:56:25.144    "Notifications enabled" sent
V   17:56:25.146    Notifications enabled for 49535343-1e4d-4bd9-ba61-23c647249616
I   17:56:25.146    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-09-01-42-60-88-09-01-42-60-88-0A-01-42-60-88-0B-01-42-60
A   17:56:25.146    "(0x) 88-09-01-42-60-88-09-01-42-60-88-0A-01-42-60-88-0B-01-42-60" received
I   17:56:25.190    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-0C-01-42-60-88-0F-02-42-60-88-12-02-42-60-88-16-03-42-60
A   17:56:25.190    "(0x) 88-0C-01-42-60-88-0F-02-42-60-88-12-02-42-60-88-16-03-42-60" received
I   17:56:25.235    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-1B-04-42-60-88-21-05-42-60-88-27-05-42-60-88-2E-06-42-60
A   17:56:25.235    "(0x) 88-1B-04-42-60-88-21-05-42-60-88-27-05-42-60-88-2E-06-42-60" received
I   17:56:25.279    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-34-07-42-60-88-3B-08-42-60-88-42-09-42-60-88-48-0A-42-60
A   17:56:25.279    "(0x) 88-34-07-42-60-88-3B-08-42-60-88-42-09-42-60-88-48-0A-42-60" received
I   17:56:25.324    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-4D-0B-42-60-88-52-0C-42-60-88-56-0D-42-60-88-5A-0D-42-60
A   17:56:25.324    "(0x) 88-4D-0B-42-60-88-52-0C-42-60-88-56-0D-42-60-88-5A-0D-42-60" received
I   17:56:25.370    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-5C-0D-42-60-88-5E-0E-42-60-88-5F-0E-42-60-88-5F-0E-42-60
A   17:56:25.370    "(0x) 88-5C-0D-42-60-88-5E-0E-42-60-88-5F-0E-42-60-88-5F-0E-42-60" received
I   17:56:25.414    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-5E-0E-42-60-C8-5E-0E-42-60-88-5D-0E-42-60-88-5C-0D-42-60
A   17:56:25.414    "(0x) 88-5E-0E-42-60-C8-5E-0E-42-60-88-5D-0E-42-60-88-5C-0D-42-60" received
I   17:56:25.459    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-5A-0D-42-60-88-59-0D-42-60-88-57-0D-42-60-88-56-0C-42-60
A   17:56:25.459    "(0x) 88-5A-0D-42-60-88-59-0D-42-60-88-57-0D-42-60-88-56-0C-42-60" received
I   17:56:25.505    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-54-0C-42-60-88-52-0C-42-60-88-51-0C-42-60-88-4F-0B-42-60
A   17:56:25.505    "(0x) 88-54-0C-42-60-88-52-0C-42-60-88-51-0C-42-60-88-4F-0B-42-60" received
I   17:56:25.509    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-4E-0B-42-60-88-4D-0B-42-60-88-4C-0B-42-60-88-4B-0B-42-60
A   17:56:25.509    "(0x) 88-4E-0B-42-60-88-4D-0B-42-60-88-4C-0B-42-60-88-4B-0B-42-60" received
I   17:56:25.552    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-4B-0B-42-60-88-4A-0B-42-60-88-4A-0B-42-60-88-49-0B-42-60
A   17:56:25.552    "(0x) 88-4B-0B-42-60-88-4A-0B-42-60-88-4A-0B-42-60-88-49-0B-42-60" received
I   17:56:25.595    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-49-0B-42-60-88-49-0A-42-60-88-49-0A-42-60-88-48-0A-42-60
A   17:56:25.595    "(0x) 88-49-0B-42-60-88-49-0A-42-60-88-49-0A-42-60-88-48-0A-42-60" received
I   17:56:25.595    Connection parameters updated (interval: 11.25ms, latency: 0, timeout: 2000ms)
I   17:56:25.628    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-48-0A-42-60-88-48-0A-42-60-88-47-0A-42-60-88-47-0A-42-60
A   17:56:25.628    "(0x) 88-48-0A-42-60-88-48-0A-42-60-88-47-0A-42-60-88-47-0A-42-60" received
I   17:56:25.661    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-46-0A-42-60-88-45-0A-42-60-88-44-0A-42-60-88-43-0A-42-60
A   17:56:25.661    "(0x) 88-46-0A-42-60-88-45-0A-42-60-88-44-0A-42-60-88-43-0A-42-60" received
I   17:56:25.708    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-42-09-42-60-88-41-09-42-60-88-3F-09-42-60-88-3E-09-42-60
A   17:56:25.708    "(0x) 88-42-09-42-60-88-41-09-42-60-88-3F-09-42-60-88-3E-09-42-60" received
I   17:56:25.766    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-3D-09-42-60-88-3B-08-42-60-88-3A-08-42-60-88-38-08-42-60
A   17:56:25.766    "(0x) 88-3D-09-42-60-88-3B-08-42-60-88-3A-08-42-60-88-38-08-42-60" received
I   17:56:25.786    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-37-08-42-60-88-35-08-42-60-88-34-07-42-60-88-33-07-42-60
A   17:56:25.786    "(0x) 88-37-08-42-60-88-35-08-42-60-88-34-07-42-60-88-33-07-42-60" received
I   17:56:25.830    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-32-07-42-60-88-30-07-42-60-88-2F-07-42-60-88-2E-07-42-60
A   17:56:25.830    "(0x) 88-32-07-42-60-88-30-07-42-60-88-2F-07-42-60-88-2E-07-42-60" received
I   17:56:25.863    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-2D-06-42-60-88-2C-06-42-60-88-2B-06-42-60-88-2B-06-42-60
A   17:56:25.863    "(0x) 88-2D-06-42-60-88-2C-06-42-60-88-2B-06-42-60-88-2B-06-42-60" received
I   17:56:25.909    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-2A-06-42-60-88-29-06-42-60-88-28-06-42-60-88-28-06-42-60
A   17:56:25.909    "(0x) 88-2A-06-42-60-88-29-06-42-60-88-28-06-42-60-88-28-06-42-60" received
I   17:56:25.956    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-27-05-42-60-88-26-05-42-60-88-25-05-42-60-88-25-05-42-60
A   17:56:25.956    "(0x) 88-27-05-42-60-88-26-05-42-60-88-25-05-42-60-88-25-05-42-60" received
I   17:56:25.988    Notification received from 49535343-1e4d-4bd9-ba61-23c647249616, value: (0x) 88-24-05-42-60-88-23-05-42-60-88-22-05-42-60-88-21-05-42-60

The most interesting parts are these 20 bytes standard BLE packets

(0x) 86-16-03-41-62-86-15-03-41-62-86-14-03-41-62-86-13-02-41-62
(0x) 86-12-02-41-62-86-11-02-41-62-86-10-02-41-62-86-0F-02-41-62
(0x) 86-13-02-41-62-86-16-03-41-62-86-19-03-41-62-86-1E-04-41-62
(0x) 86-23-05-41-62-86-29-06-41-62-86-2F-07-41-62-86-35-08-41-62
(0x) 86-3B-08-41-62-86-41-09-41-62-86-46-0A-41-62-86-4B-0B-41-62

The device seller recommended an app called oxycare. I just reverse engineered the app to find the part of the code, used to decode the packet.

private static int PACKAGE_LEN = 5;

and

int[] iArr = this.parseBuf;
int i2 = iArr[4];
int i3 = iArr[3] | ((iArr[2] & 64) << 1);
int i4 = iArr[0] & 15;
if (!(i2 == this.mOxiParams.spo2 && i3 == this.mOxiParams.pulseRate && i4 == this.mOxiParams.f41pi)) {
    this.mOxiParams.update(i2, i3, i4);
    this.mOnDataChangeListener.onSpO2ParamsChanged();
}

That made it easy. I just rewrote this into a small function and tried with the above packets. Voila it worked like charm.

class Main {
	public static void main(String[] args) {
	System.out.println("Hello world!");
	byte[]  packet = {(byte)0x86, (byte)0x16, (byte)0x03, (byte)0x41, (byte)0x62};
	int spo2 = packet[4]; // 41 =  65
	int pulseRate = packet[3] | ((packet[2] & 64) << 1);
	int f41pi = packet[0] & 15; 
	System.out.println(spo2);
	System.out.println(pulseRate);
	System.out.println(f41pi);
  }
}

Then I did some more Google search. Apparently Oxycare works with many Berry Med devices. Berry Electronic is a Shanghai based electronic company which makes lots of electronic medical devices. Seems like they are very popular in the electronics world. Even Adafruit sells one.

BCI Protocol
BCI Protocol

Then I found the BCI protocol. It's simple and straight forward. You will easily understand what's happening in code once you understand the protocol. I also found other projects related to this device. Overall I learnt a lot. Next step is to write a script to send alerts.


You can read this blog using RSS Feed. But if you are the person who loves getting emails, then you can join my readers by signing up.

Join 2,231 other subscribers