Differences
This shows you the differences between two versions of the page.
| wellue_ble_protocol [2026/08/20 22:01] – created fabricio | wellue_ble_protocol [2026/08/20 22:43] (current) – fabricio | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== SleepU — live BLE protocol ====== | ====== 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 '' | + | Wellue / Viatom **SleepU** (sleep oxygen monitor). Host-side contract for a **live** SpO₂ + pulse connection. Captured on-device 2026-08-20 (advertised '' |
| - | This page is enough to scan, connect, poll, and decode live readings. It does **not** cover overnight file download, PPG/ | + | This page is enough to scan, connect, poll, decode live readings, and handle protocol errors. It does **not** cover overnight file download, PPG/ |
| ===== Device ===== | ===== Device ===== | ||
| Line 10: | Line 10: | ||
| | Product | SleepU Sleep Oxygen Monitor (SpO₂ + heartbeat) | | | Product | SleepU Sleep Oxygen Monitor (SpO₂ + heartbeat) | | ||
| | BLE name | Starts with '' | | BLE name | Starts with '' | ||
| + | | Identity (INFO) | Model '' | ||
| | Family | Lepu / Viatom **Oxy** (same GATT as O2Ring / KidsO2 / Checkme O2) | | | Family | Lepu / Viatom **Oxy** (same GATT as O2Ring / KidsO2 / Checkme O2) | | ||
| | Connections | **One** GATT client. Disconnect ViHealth / other apps first. | | | Connections | **One** GATT client. Disconnect ViHealth / other apps first. | | ||
| | Encryption | Optional OEM path in the vendor SDK. Retail unit captured here is **plaintext**. | | | 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. | | + | | Concurrency | Do not mix command types while the 1 Hz live poll is running. Two back-to-back valid '' |
| - | Scan by local name containing '' | + | Scan by local name containing '' |
| - | Put the probe on a finger before expecting valid SpO₂. | + | Put the probe on a finger before expecting valid SpO₂. |
| ===== GATT ===== | ===== GATT ===== | ||
| Line 32: | Line 33: | ||
| - Scan until a device named '' | - Scan until a device named '' | ||
| - Connect as central; discover the service and the two characteristics. | - Connect as central; discover the service and the two characteristics. | ||
| - | - Subscribe to **Notify** ('' | + | - Subscribe to **Notify** ('' |
| - Every **1 second**, write the 8-byte live-param request on **Write** ('' | - Every **1 second**, write the 8-byte live-param request on **Write** ('' | ||
| - Reassemble notify chunks into one frame (see MTU). | - Reassemble notify chunks into one frame (see MTU). | ||
| - | - Validate | + | - Check header, CMD XOR, length, and CRC-8. |
| - | - Publish SpO₂ / pulse only when '' | + | |
| + | - If CMD is '' | ||
| - Unsubscribe and disconnect when the live view stops (the device stays awake while connected). | - 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. | + | Do **not** send INFO, file, or settings commands while the 1 Hz loop is running. INFO is optional and is **not** required to start live polling. |
| ===== Packet frame ===== | ===== Packet frame ===== | ||
| Line 47: | Line 49: | ||
| ^ Offset ^ Size ^ Field ^ Notes ^ | ^ Offset ^ Size ^ Field ^ Notes ^ | ||
| | 0 | 1 | HEADER | '' | | 0 | 1 | HEADER | '' | ||
| - | | 1 | 1 | CMD | See commands. Live **request** is '' | + | | 1 | 1 | CMD | **Request** live-param = '' |
| - | | 2 | 1 | CMD XOR | Must equal '' | + | | 2 | 1 | CMD XOR | Must equal '' |
| - | | 3 | 2 | BLOCK | '' | + | | 3 | 2 | BLOCK | '' |
| | 5 | 2 | LENGTH | Payload length in bytes. | | | 5 | 2 | LENGTH | Payload length in bytes. | | ||
| | 7 | LENGTH | PAYLOAD | Command-specific. | | | 7 | LENGTH | PAYLOAD | Command-specific. | | ||
| | 7+LENGTH | 1 | CRC | CRC-8 of all bytes **before** this one (header through payload). | | | 7+LENGTH | 1 | CRC | CRC-8 of all bytes **before** this one (header through payload). | | ||
| - | Total frame size = '' | + | Total frame size = '' |
| + | |||
| + | ==== Response CMD dispatch ==== | ||
| + | |||
| + | ^ Response CMD ^ Meaning ^ Typical LENGTH ^ | ||
| + | | '' | ||
| + | | '' | ||
| ==== CRC-8 ==== | ==== CRC-8 ==== | ||
| Line 77: | Line 85: | ||
| ==== MTU / notify reassembly ==== | ==== MTU / notify reassembly ==== | ||
| - | Default ATT payload is **20 bytes**. A live response is **21 bytes**, so it arrives as **20 + 1**. | + | Default ATT payload is **20 bytes**. |
| - | Buffer notify data. A frame is complete when you have at least 7 bytes **and** '' | + | ^ Frame ^ Size ^ How it arrives ^ |
| + | | Live '' | ||
| + | | Error CMD '' | ||
| + | | INFO '' | ||
| + | |||
| + | Buffer notify data. A frame is complete when you have at least 7 bytes **and** '' | ||
| Writes larger than 20 bytes must be chunked with a short delay. The live request is 8 bytes and fits in one write. | Writes larger than 20 bytes must be chunked with a short delay. The live request is 8 bytes and fits in one write. | ||
| Line 109: | Line 122: | ||
| ^ Payload ^ Frame ^ Type ^ Field ^ Meaning ^ | ^ Payload ^ Frame ^ Type ^ Field ^ Meaning ^ | ||
| - | | 0 | 7 | u8 | spo2 | SpO₂ percent. '' | + | | 0 | 7 | u8 | spo2 | SpO₂ percent. '' |
| - | | 1–2 | 8–9 | u16 LE | pr | Pulse rate (bpm). | | + | | 1–2 | 8–9 | u16 LE | pr | Pulse rate (bpm). Treat '' |
| | 3–6 | 10–13 | u32 LE | steps | Step counter. Unused for live view. Observed 0. | | | 3–6 | 10–13 | u32 LE | steps | Step counter. Unused for live view. Observed 0. | | ||
| - | | 7 | 14 | u8 | battery | 0–100 percent (vendor docs). | | + | | 7 | 14 | u8 | battery | 0–100 percent. Confirmed by INFO '' |
| - | | 8 | 15 | u8 | batteryState | 0 none, 1 charging, 2 complete, 3 low battery. | | + | | 8 | 15 | u8 | batteryState | 0 none, 1 charging, 2 complete, |
| - | | 9 | 16 | u8 | vector | Motion indicator. | | + | | 9 | 16 | u8 | vector | Motion indicator. Observed 0–4 while sitting. | |
| - | | 10 | 17 | u8 | pi | Perfusion index × 10. Display as '' | + | | 10 | 17 | u8 | pi | Perfusion index × 10. Display as '' |
| - | | 11 bit 0 | 18 | flag | worn / state | '' | + | | 11 bit 0 | 18 | flag | worn / state | Vendor: |
| | 11 bits 4–7 | 18 | u4 | countDown | Observed 0 while measuring. | | | 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. | | | 12 | 19 | packed | IV flags | bits 0–1 invalid, 2–3 SpO₂, 4–5 HR, 6–7 vector. Observed 0. | | ||
| Line 122: | Line 135: | ||
| **PI** is perfusion index (pulse strength at the probe), not a second oxygen channel. Use it as signal-quality / confidence. | **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 '' | + | ==== Valid reading (finger on, settled) |
| - | + | ||
| - | ===== Worked example ===== | + | |
| Host wrote '' | Host wrote '' | ||
| Line 142: | Line 153: | ||
| | vector | 4 | | | vector | 4 | | ||
| | pi | '' | | pi | '' | ||
| - | | worn | bit 0 of '' | + | | worn | bit 0 of '' |
| | CRC | '' | | CRC | '' | ||
| - | 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. | + | Same session, five 1 s polls: SpO₂ 98, 98, 98, 98, 99; PR 65, 64, 63, 59, 58; PI 0.8 then 0.7; CRC valid each time. |
| + | |||
| + | ==== Invalid live (still CMD 0x00 — not an error frame) ==== | ||
| + | |||
| + | No usable pulse / not settled. **Same 21-byte live frame**, CRC ok. Hide this in the UI. | ||
| + | |||
| + | < | ||
| + | 55 00 ff 00 00 0d 00 00 00 00 00 00 00 00 06 03 00 c8 01 00 8f | ||
| + | </ | ||
| + | |||
| + | ^ Field ^ Decode ^ | ||
| + | | spo2 / pr | **0 / 0** | | ||
| + | | battery / batteryState | 6 / 3 (low) — matches INFO '' | ||
| + | | pi | '' | ||
| + | | worn / state | bit 0 still **1** | | ||
| + | | CRC | '' | ||
| + | |||
| + | Gate: show only if '' | ||
| + | |||
| + | ===== Error responses (CMD 0x01) ===== | ||
| + | |||
| + | Protocol errors are **not** text. They are a 12-byte '' | ||
| + | |||
| + | ^ Offset ^ Size ^ Field ^ | ||
| + | | 0 | 1 | HEADER '' | ||
| + | | 1 | 1 | CMD '' | ||
| + | | 2 | 1 | XOR '' | ||
| + | | 3–4 | 2 | BLOCK '' | ||
| + | | 5–6 | 2 | LENGTH '' | ||
| + | | 7–10 | 4 | error code, u32 LE | | ||
| + | | 11 | 1 | CRC | | ||
| + | |||
| + | Captured on this SleepU (2026-08-20): | ||
| + | |||
| + | ^ Trigger ^ Frame ^ Code ^ | ||
| + | | Unknown command '' | ||
| + | | Live '' | ||
| + | |||
| + | Family note (not captured here): missing NUL on FILE_OPEN is error **9**. Other codes not exercised. | ||
| + | |||
| + | Two back-to-back **valid** '' | ||
| + | |||
| + | ===== INFO (0x14), optional ===== | ||
| + | |||
| + | Not required for live view. Live '' | ||
| + | |||
| + | Request (empty payload): | ||
| + | |||
| + | < | ||
| + | aa 14 eb 00 00 00 00 c6 | ||
| + | </ | ||
| + | |||
| + | Response: HEADER '' | ||
| + | |||
| + | Observed JSON (this unit, 2026-08-20): | ||
| + | |||
| + | < | ||
| + | {" | ||
| + | </ | ||
| + | |||
| + | ^ Key ^ Meaning here ^ | ||
| + | | CurBAT / CurBatState | '' | ||
| + | | CurMode | '' | ||
| + | | CurState | '' | ||
| + | | FileList | Overnight recordings; ignore for live view | | ||
| ===== Commands not needed for live view ===== | ===== Commands not needed for live view ===== | ||
| - | These exist on the Oxy family. **Not captured on this SleepU** for this page. Do not send them during the live poll. | + | Do not send these during the 1 Hz poll. |
| ^ CMD ^ Name ^ Notes ^ | ^ CMD ^ Name ^ Notes ^ | ||
| - | | '' | + | | '' |
| - | | '' | + | | '' |
| - | | '' | + | | '' |
| - | | '' | + | | '' |
| - | | '' | + | | '' |
| - | | '' | + | | '' |
| ===== Implementation checklist ===== | ===== Implementation checklist ===== | ||
| Line 165: | Line 240: | ||
| * Enable notify **before** the first '' | * Enable notify **before** the first '' | ||
| * Write '' | * Write '' | ||
| - | * Reassemble | + | * Reassemble |
| - | * Check '' | + | * If CMD is '' |
| - | * Decode spo2 u8, pr u16 LE, pi u8/ | + | * Live data is CMD '' |
| - | * Gate display on worn + nonzero | + | * Decode spo2 u8, pr u16 LE, pi u8/ |
| - | * Single outstanding command; disconnect when idle | + | * Show SpO₂ only when '' |
| + | * Do not mix other commands into the 1 Hz loop; disconnect when idle | ||