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