Wellue / Viatom SleepU — protocol, capabilities and Somnolinc integration

Investigation:

2026-09-08. Tested retail SleepU: INFO model 1654, hardware AA, firmware 1.3.0, protocol 1.4, file version 3, branch 21070000.

Consolidated on 2026-09-14 from the August 20 live captures, September 8 investigation, and current repository documentation. No new device experiments were performed for this consolidation.

Protocol capabilities and implemented Android features are distinguished below. All command numbers are hexadecimal.

Verified below means exercised on this particular SleepU. SDK means found in the vendor library/demo, without establishing support on this firmware. 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.

Sources:

  • Vendor LepuDemo, pinned revision, including its Oxy-family README and OxyActivity.kt.
  • That checkout contains app/libs/lepu-blepro-1.3.9.aar (the README's dependency version is older). SHA-256: a578425905179cf5433aa6c9323b77fe8b809e7ecd5acf91a13c81711f7cf966. Inspected with CFR 0.152. Relevant classes: doad.ifgm (commands), doah.doab (OxyCommonBleInterface), doad.goto (wave), doad.try and doad.case (PPG), doae.dofd (file), doad.for (INFO). Obfuscated names apply only to this artifact. No vendor binary or decompiled source is included here.
  • SleepU specifications and manufacturer manual describe standalone measurement, vibration alerts, internal storage, and live readings.
  • Direct BLE captures and three downloaded files, kept outside Git in local/sleepu-protocol-2026-09-08/. These contain personal device/health data. plans/ble/05-sleepu-protocol/analyze.py checks captured frames and file structure.

Initial probes were read-only. Subsequent user-authorized write tests temporarily changed oxygen threshold 90 → 91 and motor intensity 20 → 21, then restored both with verified INFO readback. A grouped write of the original values and a clock write using the freshly read device time were also acknowledged. Factory data, firmware and stored recordings were not erased or rewritten. File reads open and close a transfer session.

Capability Result on tested SleepU
Live SpO₂, pulse, motion, PI, worn/countdown, battery Verified, 0x17, 13-byte numeric payload
Live pleth waveform Verified, 0x1B, one unsigned 8-bit sample channel, approximately 125 samples/s
Raw red + infrared optical samples Rejected, 0x1C returns error 11, including the SDK's exact request payload
List stored recordings Verified, 0x14 INFO FileList
Download stored recordings Verified, 0x03 / 0x04 / 0x05; version 3 numeric files
Waveform/PPG in stored files Absent in all three downloaded files; no separate PPG file method recovered for this model
Interleave numeric + waveform reads Verified, serialized requests over one connection
Download older files while measuring Verified, files downloaded with INFO state 2 and valid live readings
Internal recording alongside live BLE Measurement remained active; final saved-file continuity still needs a controlled finalize/download test
Long-term phone waveform recording Technically possible from 0x1B; overnight reliability, battery cost, and loss rate remain untested
Identity/version/clock/current settings Verified, INFO JSON; DIS itself only exposes a generic manufacturer string
Set clock / alarm settings Verified, JSON 0x16: threshold/motor changes and restoration; same-time clock write acknowledged
Factory reset / factory provisioning SDK, 0x18 / 0x1A; not executed
Box information Rejected, 0x1D error 11
Ping Verified, 0x15; four-zero acknowledgement, not payload echo
Optional OEM encryption negotiation SDK, 0xFF; unnecessary for tested plaintext retail unit
Firmware update Update-like GATT service present; update procedure/image compatibility not verified
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, individual file deletion No dedicated command recovered in this SleepU command builder

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's 150 Hz raw-PPG description must not be applied to the observed 125 Hz 8-bit waveform.

Scan using the advertised SleepU name and/or the custom Oxy service, then read INFO to establish the model/version. The service is shared across Oxy products. Only one central was used; disconnect ViHealth/Somnolinc before a host probe.

Service / role UUID / observed properties
Oxy service 14839ac4-7d7e-415c-9a42-167340cf2339
Write 8b00ace7-eb0b-49b0-bbe9-9aee0a26e1a3, write without response
Notifications 0734594a-a8e7-4b1a-a6b1-cd5243059a57, notify
DIS 0000180a-0000-1000-8000-00805f9b34fb
DIS manufacturer 00002a29-0000-1000-8000-00805f9b34fb, read; returned Co.,Ltd.
Update service 0000fe59-0000-1000-8000-00805f9b34fb
Update characteristic 8ec90003-f315-4f60-9fb8-838830daea50, write/indicate; not invoked
GATT service changed 0x1801 / 0x2A05, indicate

No standard Battery Service, DIS serial/model/version characteristics, or extra 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.

All multibyte integers below are little endian. Offsets in subsequent tables refer to the payload, excluding the seven-byte frame header.

Request:  AA command (command XOR FF) block:u16 length:u16 payload CRC8
Response: 55 status  (status  XOR FF) block:u16 length:u16 payload CRC8

Total length is 8 + payload length. CRC covers everything except its own byte: polynomial 0x07, initial value 0, MSB first, no reflection or final XOR. Response status 0 is success; 1 is an error with a four-byte error code. The response does not echo the request command.

Subscribe before writing. Reassemble across notification boundaries, validate complement/length/CRC, and retain extra complete frames. The probe split writes 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.

Command Request payload Success payload / status
03 file open ASCII filename followed by one NUL Verified: total file byte size, u32
04 file block Empty; block counter starts 0 Verified: file bytes, up to 512 observed
05 file close Empty Verified: four zero bytes
14 INFO Empty Verified: JSON with NUL padding to 512 bytes
15 ping SDK accepts arbitrary bytes Verified: four zero bytes even for a nonempty request
16 settings JSON object, string values, no trailing NUL Verified: four zero bytes; threshold/motor readback confirmed
17 numeric live Empty in SDK Verified: 13 bytes; one-zero-byte request also accepted
18 factory reset Empty SDK only; destructive
1A factory information programming Opaque factory bytes SDK only; provisioning layout unresolved
1B waveform One byte 00 in SDK Verified: 12-byte header + N waveform bytes; empty request also accepted
1C raw PPG One byte 00 in SDK Error 11 on this unit, with both SDK payload and empty payload
1D box information Empty Error 11 on this unit
FF encryption negotiation SDK-generated negotiation bytes Optional OEM path; not exercised, not required here

The SDK resets its block counter for non-block commands, increments it for 04, and wraps before 65535. Do not interleave file transfer requests with other commands when following this implementation. Finish/close the file transaction, then resume live polling; waveform continuity across that pause is not guaranteed.

Error codes verified: 1 after a bad request CRC (August capture), 11 after unknown command 99, raw PPG, and box-info requests. Treat 11 as rejection of the 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.

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.

Frame Size How it arrives
Live 0x17 data 21 B 20 + 1
Error CMD 0x01 12 B One notify
INFO 0x14 520 B Many 20-byte notifies (LENGTH 512 + 8-byte header/CRC)

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 a 20-byte prefix as a full live packet. Do not cap LENGTH at 20 — INFO is 512.

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

Strip trailing NUL padding, parse JSON, retain strings and unknown keys. These keys were actually returned:

Keys Interpretation
Region, Model, HardwareVer, SoftwareVer, BootloaderVer Product/firmware identity; tested model 1654, firmware 1.3.0
FileVer, SPCPVer, BranchCode File version 3, protocol 1.4, firmware branch
SN Unit serial; preserve text and leading zeros
CurTIME yyyy-MM-dd,HH:mm:ss, device-local wall time, no timezone
CurBAT, CurBatState Percent string and numeric-state string; matched live readings
CurOxiThr, CurMotor Current oxygen alert threshold and motor setting
CurPedtar SDK step-target field; meaning/use on SleepU not verified
CurMode SDK: 0 sleep, 1 monitor; observed 0
CurState SDK: 0 preparing, 1 ready, 2 measuring; observed 2 during tests
FileList Comma-separated recording names with trailing comma; drop empty entries

SDK also recognizes OxiSwitch, HRSwitch, HRLowThr, HRHighThr, LightingMode, LightStr, SpO2SW, CurBuzzer, MtSW, MtThr, IvSW, IvThr, and SleepTime. They were absent here. An absent key is not evidence of a supported feature being disabled; avoid converting it into a false/zero capability claim. INFO is not required before basic live reads and the clock need not be synchronized to read data. Re-read identity/capabilities after reconnect.

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₂/pulse are invalid. Valid/invalid golden frames are included below. The wire fields above describe the complete payload; the current Android recording stores a subset, documented in the Somnolinc section below.

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

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 = 1
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; CRC valid each time.

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 CurBAT 6%
pi 0xC8 / 10 = 20.0 (filler, not a real PI)
worn / state bit 0 still 1
CRC 0x8F ok

SpO₂ validity gate: show only if spo2 != 0. The worn bit alone does not establish validity. Pulse zero is independently invalid; PI is unreliable while readings are invalid. Do not treat this packet as a protocol error.

Protocol errors are not text. They are a 12-byte 0x55 frame with CMD 0x01 (XOR 0xFE), LENGTH 4, payload = u32 little-endian error code. Fits in one notify.

Offset Size Field
0 1 HEADER 0x55
1 1 CMD 0x01
2 1 XOR 0xFE
3–4 2 BLOCK 0
5–6 2 LENGTH 4
7–10 4 error code, u32 LE
11 1 CRC

Captured on this SleepU (2026-08-20):

Trigger Frame Code
Unknown command 0x99 (aa 99 66 00 00 00 00 8b) 55 01 fe 00 00 04 00 0b 00 00 00 71 11 (0x0B)
Live 0x17 with bad CRC (aa 17 e8 00 00 00 00 e4) 55 01 fe 00 00 04 00 01 00 00 00 ed 1 (0x01)

Family note (not captured here): missing NUL on FILE_OPEN is error 9. Other codes not exercised.

Two back-to-back valid 0x17 writes both got live replies (no NACK). Bad CRC / unknown CMD do return 0x01. Still treat mixed command types as single-outstanding.

Not required for live view. Live 0x17 worked with no prior INFO or SetTIME.

Request (empty payload):

aa 14 eb 00 00 00 00 c6

Response: HEADER 0x55, CMD 0x00, LENGTH 512, JSON then NUL padding. Total 520 bytes, many 20-byte notifies. Reassemble using LENGTH; do not drop frames larger than 21 bytes.

Observed JSON (this unit, 2026-08-20):

{"Region":"CE","Model":"1654","HardwareVer":"AA","SoftwareVer":"1.3.0","BootloaderVer":"0.1.0.0","FileVer":"3","SPCPVer":"1.4","SN":"25062B0920","CurTIME":"2026-08-21,00:17:15","CurBAT":"6%","CurBatState":"3","CurOxiThr":"90","CurMotor":"20","CurPedtar":"99999","CurMode":"0","CurState":"1","BranchCode":"21070000","FileList":"20260816015317,20260819023106,20260820155126,20260820235052,"}
Key Meaning here
CurBAT / CurBatState 6% / 3 — matches live battery 6 / state 3 (low)
CurMode 0 = sleep mode
CurState 1 = ready (INFO field; not the live worn bit)
FileList Overnight recordings; ignore for live view
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 sleepState; semantics not verified, not surfaced by public RtWave DTO
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 12 + N bytes. The SDK silently clamps malformed lengths; a recording implementation should report malformed packets instead. The waveform PI/state fields were zero even with valid numeric PI/worn values, so continue using 17 for those measures. There is no per-sample timestamp or sequence number.

The SDK preserves original waveByte but replaces values 156 and 246 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. Do not describe this 8-bit stream as raw red/infrared ADC data. The initial reply included suspicious repeated filler-like patterns; mark startup data as uncertain rather than treating the first batch as settled physiology.

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: the device drains a short waveform buffer capped at roughly one second/125 samples. Slow polling cannot retrieve all elapsed data. Which samples are retained during overflow, exact latency, and precise oscillator rate are unknown. Schedule waveform requests approximately every 200–250 ms with a single command arbiter, accounting for BLE latency. Preserve host monotonic request/reply times and batch boundaries. Flag stalls/125-sample saturation as possible loss; do not promise lossless reconstruction or invent exact sample timestamps.

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 2 + 9*N. The SDK stores optical words in Java ints; ADC bit depth, 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 00. There is no evidence of a separate mode that enables it. Do not expose this as an available capability or retry continuously after rejection on the same firmware.

  1. Read INFO and take a snapshot of nonempty FileList entries.
  2. Open an exact listed name plus NUL, block 0. Require a four-byte size response and a bounded, plausible size before allocating.
  3. Request block 0, then 1, etc. Append verified payloads until the declared size; validate returned block numbers and reject empty/oversized/missing blocks.
  4. Close the transfer, including on errors where the connection remains usable.
  5. Validate header size/version/date, record divisibility and requested identity before accepting or deduplicating the recording. Preserve the original bytes.

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 .vld extension.

Filename quirk: opening nonexistent 20000101000000 returned success with 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.

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 asleepTimePercent, SDK describes T90 percent
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/unavailable
26–29 u32 Step counter
30–39 bytes Reserved/uninterpreted

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's beginning, end, or aggregation time.

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 80, 40, 20, 10 as oxygen, pulse, motion and invalid warnings, but compares the masked results to 1, which would always yield false. It also derives sleep state from (flags & 30) » 4, 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.

16 sends an ASCII-compatible JSON object with string values, e.g. {“SetTIME”:“2026-09-08,22:00:00”}. The builder casts characters to bytes rather 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
SetTIME Device-local yyyy-MM-dd,HH:mm:ss Same-time write acknowledged; immediate readback matched; a deliberate clock offset/persistence test was not performed
SetOxiThr 80–95 90 → 91 → 90 verified through INFO
SetMotor Model-dependent ranges, including 0–35 and 0–100 20 → 21 → 20 verified; full SleepU range not established
SetOxiSwitch 0/1 for single alert mechanism; 0–3 for combined sound/vibration Corresponding key absent; support unknown
SetHRSwitch Same family switch encodings Absent; support unknown
SetHRLowThr, SetHRHighThr 30–250 Absent; support unknown
SetBuzzer CheckO2Plus-specific 0–100 Not established for SleepU
SetLightingMode, SetLightStr SDK Oxy base-interface keys; INFO documents values 0–2 Corresponding INFO fields absent; not tested
SetSpO2SW, SetMtSW, SetMtThr, SetIvSW, SetIvThr Keys present in SDK Oxy base interface (doab.doen); some variant branches have no-op handling Semantics/ranges and SleepU support not established

Do not infer writable keys merely by replacing Cur with Set. In particular, display settings, operating-mode changes and sensor-start/stop commands were not 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 00 00 00 00. The grouped request contained original threshold/motor values; it verifies acceptance of that JSON shape, not atomic application of two changed values. The tests establish configuration read/write behavior, not the physical alert's trigger timing, vibration strength calibration, or persistence through power-off. Settings without readable original values were not toggled because their state could not be reliably restored.

18 is factory reset, not 1A. 1A programs factory information and FF 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.

  • Numeric 17 phone recording is implemented. Its timing, channel selection, validity handling and identity limits are documented below.
  • A separate optional 1B track can preserve the live 8-bit pleth waveform. Give it its own format/sample-rate provenance, batch timestamps and gap markers; keep visual interpolation out of the saved bytes.
  • 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₂/pulse/motion history. They cannot backfill lost waveform, PI or battery samples.
  • 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: finalize and retrieve a recording covering BLE streaming; check waveform continuity under movement/contact loss/reconnect; measure extended Android/background operation and power use; resolve filename 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's countdown; no new file was available to validate.

The analyzer uses Python's standard library and does not connect to a device:

python3 -m unittest discover -s plans/ble/05-sleepu-protocol -p 'test_*.py'
python3 plans/ble/05-sleepu-protocol/analyze.py capture local/sleepu-protocol-2026-09-08/sustained-wave-capture.json
python3 plans/ble/05-sleepu-protocol/analyze.py file local/sleepu-protocol-2026-09-08/*.bin

The capture command expects September probe events with kind=request/notify, hex bytes and notification times. It checks every request/response CRC, block 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/truncation, overlapping/unsolicited replies, waveform count validation/raw marker preservation, and file integrity/sentinels.

Connect the SpO₂ finger sensor in the live view and press Record. The phone records its live BLE readings together with any connected headband/chestbelt. 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/chestbelt controls. Resuming appends to the retained 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 1 Hz numeric feed. It does not download SleepU's internal files or request pleth/raw PPG waveform data.

The source sleepu-1 / stream live records SpO₂, pulse, PI, motion, battery 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/pulse/PI as missing, preserving other status channels. 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's exact physiological sampling 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's device identifier is a connection address, not a durable unit key or authenticated identity. Recording rejects a different connection address while attached. Serial/version metadata can be added when INFO support is implemented.

The Android client currently implements numeric polling only. Its frame codec caps payloads at 64 bytes; INFO/file/waveform support requires extending that limit and adding request-aware dispatch. The device capability matrix above does not imply those features are exposed by the app.

In the live view, open More → Export recording, select a completed recording, and press Export. This works for any device combination. The app copies the EDF and unchanged manifest.json into Downloads/Somnolinc/<recording-id>_export_<suffix>/. The EDF is named <recording-id>.edf, retaining the device/date identifier used 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.

Android 10+ uses MediaStore Downloads 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.

  • wellue_ble_protocol.txt
  • Last modified: 2026/09/14 12:55
  • by fabricio