Open-source Python client for the WatchPAT ONE sleep diagnostics device by Itamar Medical / ZOLL. Connects over Bluetooth Low Energy, captures raw sensor data, decodes the binary protocol in real time, and visualizes waveforms in a live dashboard.
The BLE protocol and binary data format were reverse-engineered from the WatchPAT Android app (v4.2.0) and raw device captures.
- BLE connection via Nordic UART Service (NUS) with full packet reassembly and CRC validation
- Device control — session start, acquisition start/stop, BIT (Built-In Test), finger detection, LED control, tech status
- Real-time data decoding of all known sensor channels:
- Oximetry A & B (red/IR, 100 Hz) — zigzag byte-delta compressed
- PAT signal (100 Hz) — zigzag byte-delta compressed
- Chest / respiratory effort (100 Hz) — nibble-delta compressed
- Body position & accelerometer (5 Hz) — SBP chest sensor with CRC-verified subframes
- Derived metric (1 Hz)
- Event records
- Live GUI dashboard with rolling waveform plots, body position indicator, accelerometer view, and session stats
- Raw capture to length-prefixed
.datfiles for offline analysis - Offline replay and CSV export of all decoded channels
- Estimated sleep-event metrics including ODI-style
AHI, PAT-basedpAHI,pRDI, and central-event counts during replay/analysis - Heuristic sleep staging with per-recording
Awake/Light/Deep/REMpercentages and a unified full-night stage graph in the GUI - Capture comparison tool for diffing two
.datfiles and reporting metric deltas such asAHI/pAHI - MQTT summary publishing for final analysis results, with Home Assistant MQTT Discovery support
- Python 3.10+
kaitaistruct— runtime needed by the generated packet parser- bleak — BLE client library
- matplotlib — dashboard GUI
- numpy — numerical arrays for plotting
Install everything with:
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
If you already have the repo virtualenv, run commands with .venv/bin/python.
Minimal manual install:
pip install bleak kaitaistruct matplotlib numpy pytest paho-mqtt
python watchpat_ble.py --scan-only
# Built-In Test
python watchpat_ble.py --serial XXXXXXXXX --bit
# Technical status (battery, LEDs)
python watchpat_ble.py --serial XXXXXXXXX --tech-status
# Finger detection
python watchpat_ble.py --serial XXXXXXXXX --finger
# Record with decoded status line
python watchpat_ble.py --serial XXXXXXXXX --monitor
# Record for 60 seconds to a specific file
python watchpat_ble.py --serial XXXXXXXXX --monitor --duration 60 -o capture.dat
python watchpat_gui.py # live auto-detect
python watchpat_gui.py --serial XXXXXXXXX
# Replay a capture file (text summary)
python watchpat_ble.py --replay capture.dat
# Export all channels to CSV
python watchpat_ble.py --replay capture.dat --csv output_prefix
# Replay with GUI dashboard
python watchpat_gui.py --replay capture.dat
python watchpat_gui.py --replay capture.dat --speed 10 # 10x speed
# Compare two captures
python watchpat_diff.py baseline.dat followup.dat
python watchpat_diff.py baseline.dat followup.dat --json
# Publish a retained MQTT summary from a capture and verify broker round-trip
python watchpat_mqtt_test.py --input-dat tests/testdata2.dat --server 192.168.1.150 --username watchpat --password watchpat1 --retain
During replay and offline analysis the project derives a few sleep-event estimates from the raw WatchPAT signals:
AHIis an ODI-style estimate based on SpO2 desaturation events (drop >= 3% for >= 10 s).pAHIis a PAT-based estimate that countsAPNEAandHYPOPNEAevents per hour.pRDIextendspAHIby also countingRERAevents per hour.Centralevents are desaturations with no nearby PAT attenuation and are tracked separately.- Sleep stages are estimated heuristically from the available motion, heart-rate, PAT-amplitude, and respiratory-effort signals and labeled as
Awake,Light,Deep, orREM.
These values appear in:
- the GUI replay dashboard session stats plus the unified full-night sleep-stage graph
watchpat_diff.py, which reports stage percentages and other metrics side by side with deltas- MQTT summaries, which publish
% of total recordingfor each estimated sleep stage
The AHI-related values and sleep-stage labels here are heuristic estimates derived from reverse-engineered signal processing in this repo. They are useful for comparing captures and validating pipeline behavior, but they are not a substitute for the vendor software, polysomnography staging, or a clinical scoring workflow.
The project can publish final analysis summaries over MQTT as a single JSON payload containing values such as AHI, pAHI, pRDI, mean/min SpO2, heart-rate stats, event counts, and sleep-stage percentages.
watchpat_mqtt_test.pyis a Python integration check which analyzes a.datfile, publishes the final summary to MQTT, and verifies the broker echoes the retained payload back.- By default the test script publishes state to
watchpat/analysis/testand publishes Home Assistant MQTT Discovery config under thehomeassistant/prefix. - The Android app publishes retained summary state to
watchpat/analysisand also publishes Home Assistant MQTT Discovery config automatically.
If Home Assistant MQTT Discovery is enabled and connected to the same broker, the published discovery config should create entities automatically for summary metrics like AHI, pAHI, pRDI, mean SpO2, mean heart rate, and the Awake / Light / Deep / REM percentages.
python tests/test_protocol.py
python tests/test_gui.py
python tests/test_mqtt_payload.py
The GUI tests are headless and validate dashboard update/close behavior without opening a real desktop window.
The device uses the Nordic UART Service (NUS). Packets have a 24-byte header:
| Offset | Size | Field | Encoding |
|---|---|---|---|
| 0 | 2 | Signature | 0xBBBB big-endian |
| 2 | 2 | Opcode | big-endian |
| 4 | 8 | Timestamp | little-endian |
| 12 | 4 | Packet ID | little-endian |
| 16 | 2 | Total length | little-endian |
| 18 | 2 | Opcode-dependent | little-endian |
| 20 | 2 | Reserved | |
| 22 | 2 | CRC-16 | little-endian |
CRC-16 uses polynomial 0x1021 with init 0xFFFF (CCITT variant).
Each DATA_PACKET (opcode 0x0800) payload contains multiple logical records, each prefixed with a 12-byte header:
| Offset | Size | Field |
|---|---|---|
| 0 | 2 | Sync word 0xAAAA |
| 2 | 1 | Record ID |
| 3 | 1 | Record type |
| 4 | 2 | Payload length (LE) |
| 6 | 2 | Sample rate (LE) |
| 8 | 4 | Flags (LE) |
| Record ID/Type | Rate | Description | Codec |
|---|---|---|---|
01/11 |
100 Hz | Oximetry channel A | 2-byte seed + zigzag8 deltas |
02/11 |
100 Hz | Oximetry channel B | 2-byte seed + zigzag8 deltas |
03/11 |
100 Hz | PAT waveform | 2-byte seed + zigzag8 deltas |
04/01 |
100 Hz | Chest / respiratory effort | 2-byte seed + nibble deltas |
05/10 |
1 Hz | Derived metric | 4-byte signed LE int |
06/00 |
5 Hz | SBP motion/orientation | 5x 16-byte CRC'd subframes |
0C/00 |
rare | Event code | 2-byte LE value |
0D/00 |
rare | Event payload | raw bytes |
Each of the five 16-byte subframes:
dd dd a3 57 — fixed marker
field_a:u16le — chest motion summary
field_b:u16le — snore summary candidate
x:i16le — accelerometer X
y:i16le — accelerometer Y
z:i16le — accelerometer Z
crc16:u16le — CRC-16 over first 14 bytes
Body position is derived from dominant accelerometer axis (supine = z+, prone = z-, left = y+, right = y-, upright = x+).
Capture .dat files use a simple length-prefixed format for easy replay:
[4-byte LE length][payload bytes][4-byte LE length][payload bytes]...
Each stored payload is the DATA_PACKET body only, meaning the bytes after the 24-byte BLE packet header. Each payload contains about 1 second of sensor data across all channels.
A standalone Android recorder app is included in the android/ directory. It connects to the device over BLE, records raw .dat files to the phone, and can share them via standard Android share intents (email, Drive, etc.).
- Android 5.0+ (API 21)
- Bluetooth LE
- USB debugging enabled for sideloading
python build_apk.py
python install_apk.py
Both scripts require:
- Android SDK with
platform-tools(foradb) - JDK 17 or 21.
build_apk.pywill useJAVA_HOMEif set and otherwise tries common system locations.
The Gradle wrapper (android/gradlew) is included in the repo.
- Tap Scan & Connect — the app scans for
ITAMAR_*devices and connects automatically - Wait for "Session confirmed" (serial number appears)
- Tap Start Recording
- Wait ~40 seconds — the device has a firmware warmup period before it begins streaming data packets
- Record as long as needed; packet count increments every 10 packets
- Tap Stop Recording
- The app runs its Python-based analysis on the finished recording and can publish the final summary to MQTT
- Tap Share File to export the
.datfile via email, Drive, or any other app
Recordings are saved to app-specific external storage and are not accessible via the phone's file manager — use the Share button or USB transfer.
The Android app includes an MQTT Config screen where you can set:
- MQTT server URI or host
- Optional MQTT username
- Optional MQTT password
When a recording analysis completes, the app publishes one retained JSON summary to:
watchpat/analysis
It also publishes retained Home Assistant MQTT Discovery config topics under:
homeassistant/sensor/watchpat_android_summary/.../config
This allows Home Assistant to create MQTT entities automatically if MQTT Discovery is enabled.
The Android app implements the same NUS protocol as the Python client. Key implementation details:
- BLE writes use
WRITE_WITHOUT_RESPONSEto match the NUS RX characteristic properties - Every packet received from the device (including data packets) is ACKed — the device will stall and retransmit if ACKs are not sent
- On unexpected disconnection the app attempts reconnection for up to 5 minutes and resumes the recording seamlessly into the same file
| File | Description |
|---|---|
watchpat_ble.py |
BLE client, protocol implementation, data decoding, CLI |
watchpat_gui.py |
Real-time matplotlib dashboard |
watchpat_diff.py |
Offline comparator for two .dat captures, including AHI/pAHI/pRDI deltas |
watchpat_mqtt_test.py |
MQTT integration verifier for retained analysis summary publishing |
watchpat_to_resmed_sd.py |
Export tool for ResMed SD card format |
build_apk.py |
Build the Android debug APK |
install_apk.py |
Install the APK via USB (adb) |
tests/ |
Protocol and headless GUI test coverage |
android/ |
Android Studio project (Java, API 21+) |
This project is an independent reverse engineering effort for personal and educational use. It is not affiliated with, endorsed by, or supported by Itamar Medical, ZOLL, or any related entity. The WatchPAT ONE is a medical device — this software is not intended for clinical use. Use at your own risk.
