BLE client and CSV parser for the EMAY SleepO2 pulse oximeter
Project description
EMAY SleepO2 BLE SDK
A multi-language SDK for the EMAY SleepO2 pulse oximeter's real-time Bluetooth streaming protocol. Read SpO₂ and pulse rate at 1 Hz from a $30 consumer device.
Every package exposes the same simple API. Pick your language — the
EMAYClient interface is identical across Swift, Python, Node.js, Rust,
Go, and Kotlin.
Quick Start
Swift
import EMAYSleepO2
let emay = EMAYClient()
emay.onReading = { reading in
print("SpO₂: \(reading.spo2 ?? 0)% HR: \(reading.pulse ?? 0)")
}
try await emay.start()
Python
import asyncio
from emay_sleepo2 import EMAYClient
async def main():
emay = EMAYClient()
emay.on_reading = lambda r: print(f"SpO₂: {r.spo2}% HR: {r.pulse}")
await emay.start()
await asyncio.sleep(30) # stream for 30 seconds
await emay.stop()
asyncio.run(main())
Node.js
import { EMAYClient } from 'emay-sleepo2';
const emay = new EMAYClient();
emay.on('reading', (r) => {
console.log(`SpO₂: ${r.spo2}% HR: ${r.pulse}`);
});
await emay.start();
// ... stream ...
await emay.stop();
Rust
use emay_sleepo2::EMAYClient;
#[tokio::main]
async fn main() {
let mut emay = EMAYClient::new().await?;
emay.on_reading(|r| println!("SpO₂: {}% HR: {}", r.spo2, r.pulse));
emay.start().await?;
tokio::time::sleep(Duration::from_secs(30)).await;
emay.stop().await?;
}
Go
package main
import (
"fmt"
"time"
emay "github.com/chenders/emay-sleepo2"
)
func main() {
client, _ := emay.NewClient(emay.DefaultAdapter())
client.OnReading = func(r emay.Reading) {
fmt.Printf("SpO₂: %d%% HR: %d\n", r.SpO2, r.Pulse)
}
client.Start()
time.Sleep(30 * time.Second)
client.Stop()
}
Kotlin (Android)
val emay = EMAYClient(context)
emay.onReading = { reading ->
println("SpO₂: ${reading.spo2}% HR: ${reading.pulse}")
}
emay.start(scope = lifecycleScope)
API Reference
Every binding exposes the same minimal interface:
class EMAYClient {
// Lifecycle
start() — connect and begin streaming
stop() — stop streaming and disconnect
isStreaming: bool — whether currently streaming
// Data
onReading: (Reading) -> void — called at ~1 Hz with new readings
// Status
status: Status — Idle | Scanning | Connecting | Streaming | Failed
onStatusChange: (Status) -> void
// Advanced
start(address) — connect to a specific device by address
batteryLevel: Int? — battery percentage (after connection)
heartbeatInterval: Duration — default 1.5s
// CSV (parse only, no BLE)
static parseCSV(data) -> [Reading]
static parseCSVFile(path) -> [Reading]
}
Reading Type
Reading {
spo2: Int? // SpO₂ percent (0–100), nil when finger off
pulse: Int? // Pulse rate in bpm, nil when no reading
timestamp: Instant // When this reading was captured
}
The Protocol (tl;dr)
- BLE service
FF12, writeFF01, notifyFF02 - Commands:
payload + sum(payload) & 0x7F - Start sequence:
hello → deviceState → startRealtime → getBattery - Sustain with a heartbeat command every ~1.5 s
- Data frames: 8 bytes —
EB 01 05 [PR] [SpO2] 7F 00 [cks] - Full specification:
spec.md
Packages
| Language | Source | Platform | BLE Library |
|---|---|---|---|
| Swift | swift/ |
iOS/macOS | CoreBluetooth |
| Python | python/ |
macOS/Linux/Windows/RPi | bleak |
| Node.js | node/ |
macOS/Linux/Windows/RPi | noble |
| Rust | rust/ |
macOS/Linux/Windows | btleplug |
| Go | go/ |
macOS/Linux/Windows/embedded | TinyGo BLE |
| Kotlin | kotlin/ |
Android 8.0+ | Android BLE |
About
The EMAY SleepO2 is a $30 continuous pulse oximeter capable of overnight streaming. No public documentation existed for its protocol. We reverse-engineered it and open-source the result so health-app developers, researchers, and tinkerers can stream oxygen data without cracking the spec themselves.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file emay_sleepo2-1.0.2.tar.gz.
File metadata
- Download URL: emay_sleepo2-1.0.2.tar.gz
- Upload date:
- Size: 14.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54be2de1aab762236c5d223348957f56ecc25d577ad42c2e7e5927353eb5657b
|
|
| MD5 |
6710be219f9adb10a218898637872134
|
|
| BLAKE2b-256 |
9b6cad88e78da4b3927ffab30de6e7f39ad10abf20623be6c42f38f077a005a3
|
File details
Details for the file emay_sleepo2-1.0.2-py3-none-any.whl.
File metadata
- Download URL: emay_sleepo2-1.0.2-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b09ad9b1aa861217c8c10c0f9c5268cca2be94a377830f712ff47d1d00d2a6c2
|
|
| MD5 |
c4f73697d07648dec12531ad6de886df
|
|
| BLAKE2b-256 |
83e2f4283875f3b7e20b4c1ca1aff7c298eea684bffcae7c7bf9acd7a2de78d2
|