Reverse-engineered BLE protocol documentation for the Wellue O2Ring pulse oximeter.
The O2Ring communicates via Bluetooth Low Energy (BLE) using a custom GATT service. This document describes the protocol for:
- Retrieving device information
- Reading real-time sensor data
- Downloading stored measurement files
| Type | UUID |
|---|---|
| Service | 14839ac4-7d7e-415c-9a42-167340cf2339 |
| Notify (read responses) | 0734594a-a8e7-4b1a-a6b1-cd5243059a57 |
| Write (send commands) | 8b00ace7-eb0b-49b0-bbe9-9aee0a26e1a3 |
All packets follow this structure:
┌──────┬─────┬──────────┬───────┬────────┬──────────┬─────┐
│ 0xAA │ CMD │ CMD^0xFF │ BLOCK │ LENGTH │ DATA ... │ CRC │
└──────┴─────┴──────────┴───────┴────────┴──────────┴─────┘
1B 1B 1B 2B 2B var 1B
| Field | Size | Description |
|---|---|---|
| Header | 1 byte | Always 0xAA |
| CMD | 1 byte | Command code |
| CMD XOR | 1 byte | CMD ^ 0xFF (validation) |
| Block | 2 bytes | Block number (little-endian), used for file reads |
| Length | 2 bytes | Data length (little-endian) |
| Data | variable | Command-specific payload |
| CRC | 1 byte | CRC-8 checksum |
def crc8(data):
crc = 0
for b in data:
chk = crc ^ b
crc = 0
if chk & 0x01: crc = 0x07
if chk & 0x02: crc ^= 0x0e
if chk & 0x04: crc ^= 0x1c
if chk & 0x08: crc ^= 0x38
if chk & 0x10: crc ^= 0x70
if chk & 0x20: crc ^= 0xe0
if chk & 0x40: crc ^= 0xc7
if chk & 0x80: crc ^= 0x89
return crc| Code | Name | Description |
|---|---|---|
0x14 (20) |
INFO | Get device info (JSON response) |
0x17 (23) |
READ_SENSORS | Real-time SpO2/pulse reading |
0x03 (3) |
FILE_OPEN | Open a stored file for reading |
0x04 (4) |
FILE_READ | Read next block from open file |
0x05 (5) |
FILE_CLOSE | Close the open file |
Request device information including battery status and file list.
Request: Empty data payload
Response: JSON string with device info:
{
"CurBAT": "75%",
"FileList": "20260116233312.vld,20260115221045.vld",
"Model": "O2Ring",
"SN": "XXXX"
}Open a stored measurement file for download.
Request: Filename as ASCII string, must be null-terminated (\x00)
Response:
- Byte 1: Status (0 = success)
- Bytes 7-10: File size (32-bit little-endian)
Important: The null terminator is required, otherwise the device returns error code 9.
Read a block of data from the currently open file.
Request: Block number in the BLOCK field (starts at 0)
Response: File data in the DATA field
Continue incrementing the block number until you've read file_size bytes.
Close the currently open file.
Request: Empty data payload
Response: Status confirmation
The O2Ring stores measurements in .vld files with the following binary format:
| Offset | Size | Type | Description |
|---|---|---|---|
| 0 | 2 | uint16_le | Version (3 for VLD3) |
| 2 | 2 | uint16_le | Year |
| 4 | 1 | uint8 | Month |
| 5 | 1 | uint8 | Day |
| 6 | 1 | uint8 | Hour |
| 7 | 1 | uint8 | Minute |
| 8 | 1 | uint8 | Second |
| 9-17 | 9 | - | Reserved/flags |
| 18 | 2 | uint16_le | Duration in seconds |
| 20-25 | 6 | - | Reserved |
Each record is 5 bytes:
| Offset | Size | Type | Description |
|---|---|---|---|
| 0 | 1 | uint8 | SpO2 percentage (70-100 valid, 0xFF = no finger) |
| 1 | 1 | uint8 | Heart rate BPM (40-200 valid, 0xFF = no finger) |
| 2 | 1 | bool | Invalid flag |
| 3 | 1 | uint8 | Motion indicator |
| 4 | 1 | uint8 | Vibration alert status |
The time interval between records can be calculated as:
interval = duration_seconds / record_count
Typically ~4 seconds per record for overnight recordings.
The O2Ring must be in Standby Mode to accept BLE connections for data sync.
After a recording session:
Remove ring from finger → Saves data → Countdown 10→0 → "END" → Standby
From powered-off state:
Insert finger briefly and remove → Shows time/battery → Standby
Note: The device automatically powers off after ~2 minutes without a BLE connection.
BLE characteristic writes are limited to 20 bytes. Split larger packets:
for i in range(0, len(packet), 20):
await client.write_gatt_char(WRITE_UUID, packet[i:i+20], response=False)
await asyncio.sleep(0.02) # Small delay between chunksResponses may arrive in multiple notifications. Accumulate data until you have:
- At least 7 bytes (header complete)
- Total length matches:
7 + data_length + 1(header + data + CRC)
- MackeyStingray/o2r - Original protocol research
- ecostech/viatom-ble - Viatom/Wellue BLE implementations
This documentation is provided for educational and interoperability purposes.
Contributions welcome! If you discover additional commands or file format details, please submit a PR.