wellue_ble_protocol

This is an old revision of the document!


SleepU — live BLE protocol

Wellue / Viatom SleepU (sleep oxygen monitor). Host-side contract for a live SpO₂ + pulse connection. Captured on-device 2026-08-20 (advertised SleepU 0920).

This page is enough to scan, connect, poll, and decode live readings. It does not cover overnight file download, PPG/waveform, or device settings.

Item Value
Product SleepU Sleep Oxygen Monitor (SpO₂ + heartbeat)
BLE name Starts with SleepU (observed SleepU 0920)
Family Lepu / Viatom Oxy (same GATT as O2Ring / KidsO2 / Checkme O2)
Connections One GATT client. Disconnect ViHealth / other apps first.
Encryption Optional OEM path in the vendor SDK. Retail unit captured here is plaintext.
Concurrency Device does not handle overlapping commands. Stop the live poll before any other command.

Scan by local name containing SleepU, or by the service UUID below.

Put the probe on a finger before expecting valid SpO₂. Off-finger packets still arrive; worn / spo2==0 tell you to hide the value.

Custom service (not a SIG pulse-oximeter profile).

Role UUID Properties to use
Service 14839ac4-7d7e-415c-9a42-167340cf2339
Write (host → device) 8b00ace7-eb0b-49b0-bbe9-9aee0a26e1a3 Write without response
Notify (device → host) 0734594a-a8e7-4b1a-a6b1-cd5243059a57 Notify; enable CCCD
  1. Scan until a device named SleepU* appears (or the service UUID is advertised).
  2. Connect as central; discover the service and the two characteristics.
  3. Subscribe to Notify (0734594a-…).
  4. Every 1 second, write the 8-byte live-param request on Write (8b00ace7-…), no write-response.
  5. Reassemble notify chunks into one frame (see MTU).
  6. Validate header, CMD XOR, length, and CRC-8, then decode the 13-byte payload.
  7. Publish SpO₂ / pulse only when worn == 1 and spo2 != 0.
  8. Unsubscribe and disconnect when the live view stops (the device stays awake while connected).

Do not send info, file, or settings commands while the 1 Hz loop is running.

Every request and response uses this layout. Integers are little-endian.

Offset Size Field Notes
0 1 HEADER 0xAA = host → device (request). 0x55 = device → host (response).
1 1 CMD See commands. Live request is 0x17. Live response CMD is 0x00 (not an echo of 0x17).
2 1 CMD XOR Must equal CMD XOR 0xFF (0x170xE8; 0x000xFF).
3 2 BLOCK 0x0000 for live-param.
5 2 LENGTH Payload length in bytes.
7 LENGTH PAYLOAD Command-specific.
7+LENGTH 1 CRC CRC-8 of all bytes before this one (header through payload).

Total frame size = 8 + LENGTH.

def crc8(data: bytes) -> int:
    crc = 0
    for byte in data:
        chk = crc ^ byte
        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

Default ATT payload is 20 bytes. A live response is 21 bytes, so it arrives as 20 + 1.

Buffer notify data. A frame is complete when you have at least 7 bytes and len(buffer) >= 7 + LENGTH + 1. Then slice that many bytes and parse. Do not treat the 20-byte prefix as a full packet.

Writes larger than 20 bytes must be chunked with a short delay. The live request is 8 bytes and fits in one write.

Poll at 1 Hz. Empty payload.

aa 17 e8 00 00 00 00 1b

CRC(aa 17 e8 00 00 00 00) = 0x1b

  • HEADER 0x55
  • CMD 0x00, XOR 0xFF
  • BLOCK 0x0000
  • LENGTH 13 (0x0d 00)
  • 13-byte payload
  • CRC

Offsets below are into the payload. Full-frame index = payload offset + 7.

Payload Frame Type Field Meaning
0 7 u8 spo2 SpO₂ percent. 0 = invalid.
1–2 8–9 u16 LE pr Pulse rate (bpm).
3–6 10–13 u32 LE steps Step counter. Unused for live view. Observed 0.
7 14 u8 battery 0–100 percent (vendor docs).
8 15 u8 batteryState 0 none, 1 charging, 2 complete, 3 low battery.
9 16 u8 vector Motion indicator.
10 17 u8 pi Perfusion index × 10. Display as raw / 10.0. 0 = invalid.
11 bit 0 18 flag worn / state 1 = on finger, 0 = off (lead off). Other state values = error.
11 bits 4–7 18 u4 countDown Observed 0 while measuring.
12 19 packed IV flags bits 0–1 invalid, 2–3 SpO₂, 4–5 HR, 6–7 vector. Observed 0.

PI is perfusion index (pulse strength at the probe), not a second oxygen channel. Use it as signal-quality / confidence.

Hide the SpO₂ reading when worn == 0 or spo2 == 0.

Host wrote aa 17 e8 00 00 00 00 1b. Device notified (reassembled):

55 00 ff 00 00 0d 00 62 41 00 00 00 00 00 07 03 04 08 01 00 21
Field Decode
header / cmd / xor 0x55 / 0x00 / 0xFF
length 13
payload 62 41 00 00 00 00 00 07 03 04 08 01 00
spo2 0x62 = 98 %
pr 0x0041 = 65 bpm
steps 0
battery / batteryState 7 / 3 (low)
vector 4
pi 0x08 / 10 = 0.8
worn bit 0 of 0x01 = true
CRC 0x21 = crc8(frame without last byte)

Same session, five 1 s polls: SpO₂ 98, 98, 98, 98, 99; PR 65, 64, 63, 59, 58; PI 0.8 then 0.7; worn true; CRC valid each time.

These exist on the Oxy family. Not captured on this SleepU for this page. Do not send them during the live poll.

CMD Name Notes
0x14 INFO JSON device info / file list. Vendor SDK also writes time (SetTIME) on connect. Live 0x17 worked here without that.
0x03 FILE_OPEN Overnight *.vld download.
0x04 FILE_READ
0x05 FILE_CLOSE
0x1B (27) RT wave oxyGetRtWave
0x1C (28) PPG oxyGetPpgRt
  • Filter scan: name SleepU* or service 14839ac4-7d7e-415c-9a42-167340cf2339
  • Connect, discover write 8b00ace7-… and notify 0734594a-…
  • Enable notify before the first 0x17 write
  • Write aa 17 e8 00 00 00 00 1b at 1 Hz, no response
  • Reassemble 20+1 notify chunks into 21-byte 0x55 frames
  • Check CMD XOR, length 13, CRC-8
  • Decode spo2 u8, pr u16 LE, pi u8/10, worn = payload[11] & 1
  • Gate display on worn + nonzero spo2
  • Single outstanding command; disconnect when idle
  • wellue_ble_protocol.1787263302.txt.gz
  • Last modified: 2026/08/20 22:01
  • by fabricio