Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| wellue_ble_protocol [2026/08/20 22:43] – fabricio | wellue_ble_protocol [2026/09/14 12:55] (current) – [Repository sources and maintenance] fabricio | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== 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 '' | + | Investigation: |
| - | This page is enough to scan, connect, poll, decode live readings, and handle | + | **2026-09-08**. Tested retail SleepU: INFO model '' |
| + | '' | ||
| - | ===== Device ===== | + | Consolidated on **2026-09-14** from the August 20 live captures, September 8 |
| + | investigation, | ||
| + | were performed for this consolidation. | ||
| - | ^ Item ^ Value ^ | + | Protocol capabilities and implemented |
| - | | Product | SleepU Sleep Oxygen Monitor (SpO₂ + heartbeat) | | + | Android features are distinguished below. All command |
| - | | BLE name | Starts with '' | + | |
| - | | Identity (INFO) | Model '' | + | |
| - | | 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 | Do not mix command | + | |
| - | Scan by local name containing '' | + | ===== Evidence and limits ===== |
| - | Put the probe on a finger before expecting valid SpO₂. Packets still arrive with no usable pulse; hide the value when '' | + | **Verified** below means exercised on this particular SleepU. **SDK** means found |
| + | in the vendor library/ | ||
| + | **Unknown** means the evidence is insufficient. This is the complete command | ||
| + | inventory recovered from the standard Oxy command builder, not a claim that every | ||
| + | firmware command or product variant has been discovered. | ||
| - | ===== GATT ===== | + | Sources: |
| - | Custom service | + | * [[https:// |
| + | * That checkout contains '' | ||
| + | * [[https:// | ||
| + | * Direct BLE captures and three downloaded files, kept outside Git in '' | ||
| - | ^ Role ^ UUID ^ Properties to use ^ | + | Initial probes were read-only. Subsequent user-authorized write tests temporarily |
| - | | Service | '' | + | changed oxygen threshold 90 → 91 and motor intensity 20 → 21, then restored both |
| - | | Write (host → device) | '' | + | with verified INFO readback. A grouped write of the original values and a clock |
| - | | Notify (device → host) | '' | + | write using the freshly read device |
| + | firmware and stored recordings were not erased or rewritten. File reads open and | ||
| + | close a transfer session. | ||
| - | ===== Connection sequence | + | ===== Capability matrix |
| - | - Scan until a device named '' | + | ^ Capability ^ Result on tested |
| - | | + | | Live SpO₂, pulse, motion, PI, worn/ |
| - | - Subscribe to **Notify** ('' | + | | Live pleth waveform | **Verified**, '' |
| - | - Every **1 second**, write the 8-byte live-param request on **Write** ('' | + | | Raw red + infrared optical samples | **Rejected**, '' |
| - | - Reassemble notify chunks into one frame (see MTU). | + | | List stored recordings | **Verified**, '' |
| - | - Check header, CMD XOR, length, and CRC-8. | + | | Download stored recordings | **Verified**, '' |
| - | - If CMD is '' | + | | Waveform/ |
| - | - If CMD is '' | + | | Interleave numeric + waveform reads | **Verified**, |
| - | - Unsubscribe and disconnect when the live view stops (the device stays awake while connected). | + | | Download older files while measuring | **Verified**, |
| + | | Internal recording alongside live BLE | Measurement remained active; final saved-file continuity still needs a controlled finalize/ | ||
| + | | Long-term phone waveform recording | Technically possible from '' | ||
| + | | Identity/ | ||
| + | | Set clock / alarm settings | **Verified**, | ||
| + | | Factory reset / factory provisioning | **SDK**, | ||
| + | | Box information | **Rejected**, | ||
| + | | Ping | **Verified**, | ||
| + | | Optional OEM encryption negotiation | **SDK**, | ||
| + | | Firmware update | Update-like GATT service present; update procedure/ | ||
| + | | Raw three-axis acceleration / automatic push mode | No verified SleepU command; SDK's S3 auto-stream API belongs to another model | | ||
| + | | Remote start/stop measurement, | ||
| - | Do **not** send INFO, file, or settings commands while the 1 Hz loop is running. INFO is optional and is **not** required | + | The SDK's OxyII family has additional APIs and file types. Those are not evidence |
| + | that this SleepU supports them. Likewise, the shared Oxy README' | ||
| + | description must not be applied | ||
| - | ===== Packet frame ===== | + | ===== Discovery and GATT ===== |
| - | Every request | + | Scan using the advertised '' |
| + | INFO to establish the model/ | ||
| + | Only one central was used; disconnect ViHealth/ | ||
| - | ^ Offset | + | ^ Service / role ^ UUID / observed properties |
| - | | 0 | 1 | HEADER | + | | Oxy service |
| - | | 1 | 1 | CMD | **Request** live-param = '' | + | | Write | '' |
| - | | 2 | 1 | CMD XOR | Must equal '' | + | | Notifications |
| - | | 3 | 2 | BLOCK | '' | + | | DIS | '' |
| - | | 5 | 2 | LENGTH | Payload length in bytes. | | + | | DIS manufacturer |
| - | | 7 | LENGTH | PAYLOAD | Command-specific. | + | | Update service |
| - | | 7+LENGTH | + | | Update characteristic |
| + | | GATT service changed | ||
| - | Total frame size = '' | + | No standard Battery Service, DIS serial/ |
| + | sensor characteristics were exposed in this discovery. Battery comes from the | ||
| + | custom protocol; identity comes from INFO. RSSI is available from the BLE stack, | ||
| + | not from an Oxy payload. Neither RSSI nor a device name authenticates a device. | ||
| - | ==== Response CMD dispatch | + | ===== Transport ===== |
| - | ^ Response | + | All multibyte integers below are little endian. Offsets in subsequent tables |
| - | | '' | + | refer to the payload, excluding the seven-byte frame header. |
| - | | '' | + | |
| + | <code text> | ||
| + | Request: | ||
| + | Response: 55 status | ||
| + | </ | ||
| + | |||
| + | Total length is '' | ||
| + | polynomial '' | ||
| + | Response status '' | ||
| + | The response does **not** echo the request command. | ||
| + | |||
| + | Subscribe before writing. Reassemble across notification boundaries, validate | ||
| + | complement/ | ||
| + | into at most 20 bytes and successfully received fragmented 520-byte INFO/file | ||
| + | frames. Do not limit protocol payloads to the ATT payload size. | ||
| + | |||
| + | Use **one outstanding request across all command types**. Dispatch by that | ||
| + | request, not response length: a waveform, file block, and INFO can have overlapping | ||
| + | lengths. File requests also carry a block counter. A timeout leaves response | ||
| + | ownership ambiguous; stop the transaction and recover the connection instead of | ||
| + | immediately issuing a different command that could consume the late reply. | ||
| + | |||
| + | ==== Complete recovered standard-Oxy command inventory ==== | ||
| + | |||
| + | ^ Command | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | The SDK resets its block counter for non-block commands, increments it for '' | ||
| + | and wraps before 65535. Do not interleave file transfer requests with other | ||
| + | commands when following this implementation. Finish/ | ||
| + | then resume live polling; waveform continuity across that pause is not guaranteed. | ||
| + | |||
| + | Error codes verified: | ||
| + | unknown command '' | ||
| + | request on this firmware, not proof about every variant. Other error code meanings | ||
| + | are unverified; preserve the numeric code. The earlier notes mention family error | ||
| + | 9 for a malformed filename but it was not established in these tests. | ||
| + | |||
| + | ===== CRC and fragmentation examples ===== | ||
| ==== CRC-8 ==== | ==== CRC-8 ==== | ||
| Line 96: | Line 171: | ||
| 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. | ||
| - | ===== Live parameter command (0x17) ===== | ||
| - | Poll at 1 Hz. Empty payload. | + | ===== INFO ('' |
| + | |||
| + | Strip trailing NUL padding, parse JSON, retain strings and unknown keys. These | ||
| + | keys were actually returned: | ||
| + | |||
| + | ^ Keys ^ Interpretation ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | SDK also recognizes '' | ||
| + | '' | ||
| + | '' | ||
| + | of a supported feature being disabled; avoid converting it into a false/ | ||
| + | capability claim. INFO is not required before basic live reads and the clock need | ||
| + | not be synchronized to read data. Re-read identity/ | ||
| + | |||
| + | ===== Numeric live data ('' | ||
| + | |||
| + | ^ Offset ^ Encoding ^ Meaning ^ | ||
| + | | 0 | u8 | SpO₂ %, zero invalid | | ||
| + | | 1–2 | u16 | Pulse bpm, zero invalid | | ||
| + | | 3–6 | u32 | Steps | | ||
| + | | 7 | u8 | Battery percent | | ||
| + | | 8 | u8 | Battery state: 0 not charging, 1 charging, 2 complete, 3 low | | ||
| + | | 9 | u8 | Motion scalar, not three-axis IMU data | | ||
| + | | 10 | u8 / 10 | Perfusion index; unreliable when reading invalid | | ||
| + | | 11 | packed | Bit 0 worn; high nibble countdown; other bits unresolved | | ||
| + | | 12 | packed | Two-bit fields: invalid 0–1, SpO₂ 2–3, pulse 4–5, motion 6–7; meanings of individual field values not verified | | ||
| + | |||
| + | One-second polling is adequate for existing numeric live display. It retrieves | ||
| + | the current values, not a backlog. Timestamp replies on the host; the payload has | ||
| + | no sample clock or sequence. Worn can remain true while SpO₂/ | ||
| + | Valid/ | ||
| + | the complete | ||
| + | in the Somnolinc section below. | ||
| + | |||
| + | ===== Numeric live and error golden fixtures ===== | ||
| ==== Request (8 bytes) ==== | ==== Request (8 bytes) ==== | ||
| Line 116: | Line 234: | ||
| * 13-byte payload | * 13-byte payload | ||
| * CRC | * CRC | ||
| - | |||
| - | ==== Payload (13 bytes, after the 7-byte header) ==== | ||
| - | |||
| - | Offsets below are into the **payload**. Full-frame index = payload offset + 7. | ||
| - | |||
| - | ^ Payload ^ Frame ^ Type ^ Field ^ Meaning ^ | ||
| - | | 0 | 7 | u8 | spo2 | SpO₂ percent. '' | ||
| - | | 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. | | ||
| - | | 7 | 14 | u8 | battery | 0–100 percent. Confirmed by INFO '' | ||
| - | | 8 | 15 | u8 | batteryState | 0 none, 1 charging, 2 complete, **3 low**. INFO '' | ||
| - | | 9 | 16 | u8 | vector | Motion indicator. Observed 0–4 while sitting. | | ||
| - | | 10 | 17 | u8 | pi | Perfusion index × 10. Display as '' | ||
| - | | 11 bit 0 | 18 | flag | worn / state | Vendor: '' | ||
| - | | 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. | ||
| ==== Valid reading (finger on, settled) ==== | ==== Valid reading (finger on, settled) ==== | ||
| Line 173: | Line 273: | ||
| | CRC | '' | | CRC | '' | ||
| - | Gate: show only if '' | + | SpO₂ validity gate: show only if '' |
| - | ===== Error responses (CMD 0x01) ===== | + | ==== Captured error responses (status |
| Protocol errors are **not** text. They are a 12-byte '' | Protocol errors are **not** text. They are a 12-byte '' | ||
| Line 198: | Line 298: | ||
| Two back-to-back **valid** '' | Two back-to-back **valid** '' | ||
| - | ===== INFO (0x14), optional ===== | + | ==== Captured |
| Not required for live view. Live '' | Not required for live view. Live '' | ||
| Line 222: | Line 322: | ||
| | FileList | Overnight recordings; ignore for live view | | | FileList | Overnight recordings; ignore for live view | | ||
| - | ===== Commands not needed for live view ===== | ||
| - | Do not send these during the 1 Hz poll. | + | ===== Pleth waveform ('' |
| + | |||
| + | ^ Offset ^ Encoding ^ Meaning ^ | ||
| + | | 0 | u8 | SpO₂ | | ||
| + | | 1–2 | u16 | Pulse bpm | | ||
| + | | 3 | u8 | Battery percent | | ||
| + | | 4 | u8 | Battery state | | ||
| + | | 5 | u8 / 10 | SDK PI field | | ||
| + | | 6 | u8 | SDK state field | | ||
| + | | 7 | u8 | SDK internal '' | ||
| + | | 8–9 | bytes | Not interpreted by inspected parser | | ||
| + | | 10–11 | u16 | Sample count N | | ||
| + | | 12 onward | N × u8 | Single waveform channel, uncalibrated amplitude | | ||
| + | |||
| + | Require exactly '' | ||
| + | recording implementation should report malformed packets instead. The waveform | ||
| + | PI/state fields were zero even with valid numeric PI/worn values, so continue | ||
| + | using '' | ||
| + | |||
| + | The SDK preserves original '' | ||
| + | with neighbors (or their average) in its display array. Their protocol meaning is | ||
| + | unknown. Keep every original byte in recordings; any interpolation belongs only | ||
| + | in visualization. | ||
| + | The initial reply included suspicious repeated filler-like patterns; mark startup | ||
| + | data as uncertain rather than treating the first batch as settled physiology. | ||
| + | |||
| + | ==== Measured timing and buffering ==== | ||
| + | |||
| + | ^ Host polling experiment ^ Observed result ^ | ||
| + | | 0.25 s sleep after reply | Approximately 38–45 samples per reply, around 0.30–0.33 s between replies | | ||
| + | | 1 s sleep after reply | 125 samples, despite approximately 1.1 s between replies | | ||
| + | | 2 s sleep after reply | Still 125 samples, despite approximately 2.1 s between replies | | ||
| + | | Sustained 0.2 s sleep, numeric read every 20 waveform replies | 120 waveform replies; after first reply, 3,675 samples over 29.342 s = 125.25 samples/s; subsequent batches 29–41 samples | | ||
| + | |||
| + | All 131 replies in the sustained session completed with valid CRCs: 120 waveform, | ||
| + | 6 numeric, 2 INFO, file open/close, and one deliberately rejected unknown command. | ||
| + | INFO remained in measuring state and battery percent was unchanged across this | ||
| + | short session. This is not an overnight endurance test. | ||
| + | |||
| + | **Inference: | ||
| + | second/125 samples. Slow polling cannot retrieve all elapsed data. Which samples | ||
| + | are retained | ||
| + | unknown. Schedule waveform requests approximately every 200–250 ms with a single | ||
| + | command arbiter, accounting for BLE latency. Preserve host monotonic request/ | ||
| + | times and batch boundaries. Flag stalls/ | ||
| + | do not promise lossless reconstruction or invent exact sample timestamps. | ||
| + | |||
| + | ===== Raw PPG ('' | ||
| + | |||
| + | The SDK decoder reads N as u16 at offset 0, followed by N nine-byte records: | ||
| + | four little-endian bytes for infrared, four for red, one motion byte. Expected | ||
| + | length is '' | ||
| + | signedness and physical calibration are not established here. | ||
| + | |||
| + | The Oxy-family README describes 150 Hz for this API. This SleepU rejected it | ||
| + | repeatedly while worn and measuring, including payload '' | ||
| + | of a separate mode that enables it. Do not expose this as an available capability | ||
| + | or retry continuously after rejection on the same firmware. | ||
| + | |||
| + | ===== Stored recordings ('' | ||
| + | |||
| + | - Read INFO and take a snapshot of nonempty '' | ||
| + | - Open an exact listed name plus NUL, block 0. Require a four-byte size response and a bounded, plausible size before allocating. | ||
| + | - Request block 0, then 1, etc. Append verified payloads until the declared size; validate returned block numbers and reject empty/ | ||
| + | - Close the transfer, including on errors where the connection remains usable. | ||
| + | - Validate header size/ | ||
| + | |||
| + | Observed files were **75, 530 and 170 bytes**, with **7, 98 and 26 samples** and | ||
| + | header durations **28, 392 and 104 seconds** respectively. Thus all have exactly | ||
| + | **4 seconds per numeric record**. The 530-byte file arrived as 512 + 18 payload | ||
| + | bytes. This raw binary transfer is not necessarily identical to a ViHealth export | ||
| + | container or a file with a '' | ||
| + | |||
| + | **Filename quirk:** opening nonexistent '' | ||
| + | size 170 instead of an error. That transfer was closed without reading its body; | ||
| + | whether it aliases a previous file or uses a fallback is unresolved. Never trust | ||
| + | the open acknowledgement alone or silently label downloaded data with the request | ||
| + | name. Validate the embedded start date against the requested date. | ||
| + | |||
| + | ==== Version 3 header (40 bytes) ==== | ||
| + | |||
| + | ^ Offset ^ Type ^ SDK field / interpretation ^ | ||
| + | | 0 | u8 | Version; observed 3 | | ||
| + | | 1 | u8 | Operation mode; observed 0 (sleep) | | ||
| + | | 2–3 | u16 | Year | | ||
| + | | 4–8 | five u8 | Month, day, hour, minute, second | | ||
| + | | 9–12 | u32 | Entire file size including header | | ||
| + | | 13–14 | u16 | Recording duration, seconds | | ||
| + | | 15–16 | u16 | Asleep time; SDK says reserved | | ||
| + | | 17 | u8 | Average SpO₂ | | ||
| + | | 18 | u8 | Minimum SpO₂ | | ||
| + | | 19 | u8 | Count of baseline drops by 3 percentage points | | ||
| + | | 20 | u8 | Count of baseline drops by 4 percentage points | | ||
| + | | 21 | u8 | '' | ||
| + | | 22–23 | u16 | Time below 90%; SDK duration field, units not independently checked | | ||
| + | | 24 | u8 | Drops below 90%; SDK says reserved | | ||
| + | | 25 | u8 | O₂ score, SDK range 0–100, divide by 10 for 0–10 display; observed 255, out of range/ | ||
| + | | 26–29 | u32 | Step counter | | ||
| + | | 30–39 | bytes | Reserved/ | ||
| + | |||
| + | Summary-statistic names come from the SDK; their algorithms and alert thresholds | ||
| + | were not independently validated. Do not interpret reserved sleep fields as sleep | ||
| + | staging. Dates have no timezone; retain original wall time and explicit timezone | ||
| + | assumptions separately. The four-second cadence does not establish whether each | ||
| + | timestamp denotes an interval' | ||
| + | |||
| + | ==== Version 3 records (5 bytes each, beginning at offset 40) ==== | ||
| + | |||
| + | ^ Record offset ^ Type ^ Meaning ^ | ||
| + | | 0 | u8 | SpO₂; 255 observed as invalid sentinel | | ||
| + | | 1–2 | u16 | Pulse; 65535 observed as invalid sentinel | | ||
| + | | 3 | u8 | Motion scalar | | ||
| + | | 4 | u8 | Flags, preserve raw | | ||
| + | |||
| + | The invalid sentinels differ from numeric live data's zero values. Do not graph | ||
| + | 255% or 65,535 bpm, and reject out-of-range summary values. There are no waveform | ||
| + | bytes in this layout. | ||
| + | |||
| + | The decompiled SDK labels flag masks '' | ||
| + | motion and invalid warnings, but compares the masked results to **1**, which | ||
| + | would always yield false. It also derives sleep state from '' | ||
| + | overlapping those masks. All captured record flags were zero. This mapping is | ||
| + | unverified and the SDK decoder is suspect; retain the byte without asserting | ||
| + | warning or sleep-state semantics. | ||
| + | |||
| + | ===== Settings and maintenance ===== | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | than providing a general Unicode encoding. Use documented ASCII keys/values only. | ||
| + | The SDK supports one key or a group of settings in the same object. | ||
| + | |||
| + | ^ Key ^ Vendor family documentation ^ SleepU evidence ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | Do not infer writable keys merely by replacing '' | ||
| + | display settings, operating-mode changes and sensor-start/ | ||
| + | established by this investigation. Re-read INFO after a future deliberate settings | ||
| + | write; an acknowledgement alone need not prove the requested value was applied. | ||
| + | |||
| + | All six tested setting requests returned success with payload '' | ||
| + | The grouped request contained original threshold/ | ||
| + | acceptance of that JSON shape, not atomic application of two changed values. | ||
| + | The tests establish configuration read/write behavior, not the physical alert' | ||
| + | trigger timing, vibration strength calibration, | ||
| + | Settings without readable original values were not toggled because their state | ||
| + | could not be reliably restored. | ||
| + | |||
| + | '' | ||
| + | negotiates optional encryption. Payload schemas for provisioning and the complete | ||
| + | OEM security exchange remain unresolved. None is needed for retail live/file | ||
| + | reads. The exposed update service is not sufficient to establish an upgrade | ||
| + | recipe, supported image, rollback behavior or permission to flash firmware. | ||
| + | |||
| + | ===== Consequences for Somnolinc ===== | ||
| + | |||
| + | * Numeric '' | ||
| + | * A separate optional '' | ||
| + | * Use one BLE request scheduler for INFO, numeric data, waveform and downloads. A file transfer can interrupt the short waveform buffer; schedule downloads outside waveform recording or explicitly mark the resulting gap. | ||
| + | * Import version 3 files as four-second SpO₂/ | ||
| + | * Do not advertise raw dual-channel PPG, raw accelerometry or remote recording control for this firmware. Cache rejections by firmware identity, not forever by Bluetooth address. | ||
| + | |||
| + | Remaining device experiments: | ||
| + | streaming; check waveform continuity under movement/ | ||
| + | measure extended Android/ | ||
| + | fallback and nonzero file flags. These are explicit evidence gaps, not implied | ||
| + | capabilities. Further configuration and DFU tests would be separate deliberate operations. | ||
| + | |||
| + | A subsequent roughly three-minute finalization watch continued to receive valid | ||
| + | live data, but INFO stayed in measuring state and its file list did not change. | ||
| + | The controlled internal-recording test therefore remains pending sensor removal | ||
| + | and completion of the device' | ||
| + | |||
| + | ===== Offline validation ===== | ||
| + | |||
| + | The analyzer uses Python' | ||
| + | |||
| + | <code bash> | ||
| + | python3 -m unittest discover -s plans/ | ||
| + | python3 plans/ | ||
| + | python3 plans/ | ||
| + | </ | ||
| + | |||
| + | The capture command expects September probe events with '' | ||
| + | hex bytes and notification times. It checks every request/ | ||
| + | matching, serialization and complete transactions before reporting counts and | ||
| + | waveform timing. The file command validates version, calendar, byte size and | ||
| + | record alignment. It reports aggregate validity counts without printing dates, | ||
| + | serials or physiological sample values. Five regression tests cover a captured | ||
| + | fragmented live frame, corruption/ | ||
| + | waveform count validation/ | ||
| + | |||
| + | ===== Current Somnolinc implementation (2026-09-14) ===== | ||
| + | |||
| + | Connect the SpO₂ finger sensor in the live view and press **Record**. The phone | ||
| + | records its live BLE readings together with any connected headband/ | ||
| + | SleepU can also be the only connected recording device. **Stop record** finalizes | ||
| + | the capture and EDF+ export while leaving the live sensor connection active. | ||
| + | |||
| + | Connecting alone leaves the display frozen. **Start** resumes all connected live | ||
| + | displays, including SleepU; **Freeze** holds their plots without interrupting an | ||
| + | active recording. **Record** also resumes the displays, and **Stop record** freezes | ||
| + | them, matching the headband/ | ||
| + | display history with a gray vertical separator in both SpO₂ and pulse. The traces | ||
| + | do not connect across the omitted pause interval. BLE | ||
| + | polling remains active while connected, independently of display updates. Turning | ||
| + | the screen off or backgrounding the app freezes all live displays and keeps the | ||
| + | connections and any active recording. Press **Start** to resume after returning. | ||
| + | |||
| + | This is phone recording of the existing approximately | ||
| + | not download SleepU' | ||
| + | |||
| + | The source '' | ||
| + | percentage, worn, and countdown. Battery state, packed IV flags, and the step | ||
| + | counter still arrive in the shared BLE packet but are omitted from new captures | ||
| + | and EDF exports. Older recordings retain their original channel descriptors. | ||
| + | Raw numeric invalid sentinels are retained in capture; EDF marks invalid | ||
| + | oxygen/ | ||
| + | EDF uses its existing gap annotations and physical-minimum filler convention; | ||
| + | consumers must honor the annotations rather than treating filler as a valid value. | ||
| + | |||
| + | Timestamps use Android elapsed realtime when the notification completing the | ||
| + | frame arrives. SleepU sends no sample timestamps or sequence numbers. The capture | ||
| + | retains each observation time; EDF uses a nominal 1 Hz grid, with indices rounded | ||
| + | from elapsed time since the first reading. Missed polls and reconnect gaps remain | ||
| + | holes, and competing observations in the same grid cell use the existing EDF | ||
| + | collision handling. This timing is not the sensor' | ||
| + | time. The firmware-segment fields describe one synthetic host-clock segment here. | ||
| + | |||
| + | Recording retains the SleepU connection when the app goes into the background. | ||
| + | Changing a recorded SleepU connection stops and saves the current recording first. | ||
| + | Automatic reconnect keeps the timeline; terminal connection loss follows the same | ||
| + | stop behavior as the other recorded devices. Display visibility does not govern | ||
| + | recording ownership: connected devices are selected when recording starts. | ||
| + | |||
| + | The current client does not read INFO identity. The descriptor' | ||
| + | is a connection address, not a durable unit key or authenticated identity. Recording | ||
| + | rejects a different connection address while attached. Serial/ | ||
| + | be added when INFO support is implemented. | ||
| + | |||
| + | The Android client currently implements numeric polling only. Its frame codec | ||
| + | caps payloads at 64 bytes; INFO/ | ||
| + | limit and adding request-aware dispatch. The device capability matrix above | ||
| + | does not imply those features are exposed by the app. | ||
| + | |||
| + | ==== Copy a recording to Downloads ==== | ||
| - | ^ CMD ^ Name ^ Notes ^ | + | In the live view, open **More → Export recording**, |
| - | | '' | + | and press **Export**. This works for any device combination. The app copies the |
| - | | '' | + | EDF and unchanged |
| - | | '' | + | The EDF is named '' |
| - | | '' | + | by the private recording folder. Each export uses a separate folder; the original |
| - | | '' | + | recording stays in private storage. Active, unfinished, and failed EDF exports |
| - | | '' | + | are not listed. Exporting an older recording does not stop a current recording. |
| - | ===== Implementation checklist ===== | + | Android 10+ uses [[https:// |
| + | without a storage permission. Android 8–9 requests storage permission when exporting. | ||
| + | The UI reports success only after both files have been copied and published. | ||
| - | * Filter scan: name '' | ||
| - | * Connect, discover write '' | ||
| - | * Enable notify **before** the first '' | ||
| - | * Write '' | ||
| - | * Reassemble until '' | ||
| - | * If CMD is '' | ||
| - | * Live data is CMD '' | ||
| - | * Decode spo2 u8, pr u16 LE, pi u8/10, battery u8 | ||
| - | * Show SpO₂ only when '' | ||
| - | * Do not mix other commands into the 1 Hz loop; disconnect when idle | ||