Talking to a Bluetooth (BLE) Pulse Oximeter - Sun, Jul 4, 2021
I was playing around with a Viatom SleepU Pulse Oximeter the past few days. It’s a device with a ring-shaped sensor that you wear on your finger and it monitors and continuously records your blood oxygen percentage and heart rate.
To communicate with this device over BLE, you need to look for the service with UUID 14839ac4-7d7e-415c-9a42-167340cf2339
. This service has two characteristics that interest us:
- For Reading:
0734594a-a8e7-4b1a-a6b1-cd5243059a57
- For Writing:
8b00ace7-eb0b-49b0-bbe9-9aee0a26e1a3
We write to the second characteristic and monitor the value update of the first characteristic for reading.
In this note I explain how to read device information, sensor values and files from the device.
But first, let’s discuss packet formats.
Write Packets
To send a message to the device, you have to create a packet with the following structure (byte array):
00 byte 0xAA
01 byte requested_command
02 byte requested_command XOR 0xFF
03 ushort (2B) file_block_id [when reading a file. set to 0 otherwise.]
05 ushort (2B) payload_length [when sending data. set to 0 otherwise.]
07 byte_array payload [optional]. This can be the name of a file to download for example. Use UTF-8 to convert text to byte array.
07+payload_length byte CRC of bytes [00 to 07)
To calculate the CRC, use the following algorithm:
static byte CRC(IEnumerable<byte> bytes)
{
byte crc = 0;
foreach (var b in bytes)
{
var chk = (byte)(crc ^ b);
crc = 0;
if ((chk & 0x01) != 0) crc = 0x07;
if ((chk & 0x02) != 0) crc ^= 0x0e;
if ((chk & 0x04) != 0) crc ^= 0x1c;
if ((chk & 0x08) != 0) crc ^= 0x38;
if ((chk & 0x10) != 0) crc ^= 0x70;
if ((chk & 0x20) != 0) crc ^= 0xe0;
if ((chk & 0x40) != 0) crc ^= 0xc7;
if ((chk & 0x80) != 0) crc ^= 0x89;
}
return crc;
}
There is a maximum message length limit of, I guess, 20 bytes. You can send your message in chunks if it’s larger than that:
async Task WriteAsync(byte[] bytes)
{
if (bytes == null) return;
while (bytes.Length > 0)
{
var chunk = bytes.Take(20).ToArray();
await WriteAsyncFunc(chunk);
bytes = bytes.Skip(20).ToArray();
}
}
If you are using later versions of C#, you can optimize this code and not duplicate the array in every loop.
Read Packets
Incoming packets come in the following format. Note that a message can come in multiple packets and therefore we have to keep track of how many bytes we expect, so we can join subsequent packets that carry the same message together.
00 byte device ID, must be 0x55
01 byte
02 byte. [01] must be equal to 0xFF XOR [02]
05 ushort (2B) + 8 = total bytes to expect
Keep reading until you have all the bytes you expect. Once you have enough bytes, take the last byte, that should be the CRC of everything except the last byte.
Calculate the CRC of everything except the last byte and make sure it is equal to this value.
Commands
0x14: Device Information
To request device information, send a packet with the command 0x14
. Your bytes in hex should look like this: aa14eb00000000c6
.
The response will be something like this:
5500ff000000027b22526567696f6e223a224345222c224d6f6
4656c223a2231363534222c224861726477617265566572223a224
141222c22536f667477617265566572223a22312e322e30222c224
26f6f746c6f61646572566572223a22302e312e302e30222c22466
96c65566572223a2233222c2253504350566572223a22312e34222
c22534e223a2232303131324232363237222c2243757254494d452
23a22323032312d30372d30332c31363a33373a3133222c2243757
2424154223a22363325222c224375724261745374617465223a223
0222c224375724f7869546872223a223837222c224375724d6f746
f72223a22313030222c22437572506564746172223a22393939393
9222c224375724d6f6465223a2230222c224375725374617465223
a2232222c224272616e6368436f6465223a2232313037303030302
22c2246696c654c697374223a22323032313037303331343035333
62c32303231303730333134343832312c323032313037303330313
13233352c227d00000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000
0000000000000007c
Looking closely at the hex, you can see the 7c
in the end is the CRC. Ignoring that, you have an almost 512 bytes long message that ends at some point and is padded with 0
s. The first few bytes in the beginning (5500ff00000002
) are packet details.
The rest of the bytes (7b
to 7d
) contain a JSON string that has the device information.
0x17: Sensors
Send a packet with command 0x17
(aa17e8000000001b
) and you will receive something like this:
5500ff00000d00615400000000004b000000000058
As explained above, the actual payload is this part:
615400000000004b0000000000
Data structure:
00: O2 level
01: Heart Rate (bpm)
07: Battery %
0x03: Open File
To download a file, you have to send a request to open it, a few requests to read it and a final request to close.
Send a request of:
- Command:
0x03
- Block:
0
- File Name:
filename\0
To open a file. The response’s first four bytes (uint) indicate the total size of the requested file in bytes. Remember this number.
0x04: Read File
You download the file in blocks (chunks). Send a request of:
- Command:
0x04
- Block:
0..i..n
To download the i-th block. Every time you receive a message, subtract the size of the message (not counting the header and CRC bytes) from the total expected bytes (obtained from the Open phase). Increase the block
number by one every time and repeat the read request until you have all the bytes you need.
Save them to a file.
0x05: Close File
Send a request with command 0x05
to close the file.