andyguzmaneth/whoop4-ble

GitHub: andyguzmaneth/whoop4-ble

通过逆向 BLE 协议直接访问 Whoop 4.0 腕带,无需订阅即可提取和流式传输心率、RR 间期及加速度计等原始生理数据,并修复了关键的固件 RTC 时钟静默失效问题。

Stars: 1 | Forks: 1

# whoop4-ble 直接 BLE 访问 **Whoop 4.0 腕带** —— 提取你自己的 HR / RR 间期 / 加速度计数据,修复卡死的固件 RTC,并流式传输实时 HR。**无需 Whoop 订阅。** ``` ┌───────────────┐ BLE ┌─────────────┐ JSONL ┌──────────────┐ │ Whoop 4.0 │◄───────►│ This repo │◄─────────►│ Your storage │ │ (your strap) │ │ (Python) │ │ │ └───────────────┘ └─────────────┘ └──────────────┘ ``` ## 一分钟了解这是什么 Whoop 4.0 会持续将 HR、RR 间期、皮肤温度和加速度计数据记录到内部闪存中,并通过低功耗蓝牙 (Bluetooth Low Energy) 广播所有数据。这些数据属于你——只是其协议未被记录。本仓库是对该协议进行逆向工程后整理出的成果,通过它你可以: - 将腕带存储在闪存上的所有 HR / RR / 加速度样本(约 14 天的循环缓冲区,每天约 86,000 条记录)提取到本地 JSONL 文件中 - 通过标准 BLE Heart Rate Service 流式传输实时 HR + RR(无需认证即可工作) - 在腕带记录时查看电池、皮肤温度以及佩戴/摘下事件 - 在闪存写入静默停止工作时修复腕带的内部时钟(这是最常见且最难发现的故障模式) - 使用基于浏览器的监视器实时检查并解码每一个 BLE 数据包 你**无法**获得的是:Whoop 云端的恢复 / 压力 / 睡眠评分。这些是在他们的云端计算的。但是原始数据都在这里,而且 HRV(基于 RR 间期的 RMSSD)只需一行代码即可算出。 ## 为什么会有这个项目 当一些用户的消费者订阅结束后,Whoop 的 API 便被付费墙挡住了。但腕带本身并不知情也不在乎——它会继续在本地记录并通过 BLE 广播。你只需要懂它的协议。 本仓库还记录了一项在**现有社区逆向工程项目**([gowhoop](https://github.com/cs-balazs/gowhoop)、[whoomp](https://github.com/jogolden/whoomp))中**未曾提及的发现**:`SET_CLOCK` 需要 **5 字节的 payload**,而不是 4 字节。如果没有尾部的提交字节,固件会静默接受写入,但**不会更新 RTC**,从而导致闪存写入无形中失败,直到下一次有效的 `SET_CLOCK`。那些仓库中记录的原始 4 字节格式适用于官方 Whoop 应用的频繁重新同步,但在任何不是每次连接都应用 SET_CLOCK 的设置中(例如每天运行一次的自托管同步),RTC 漂移陷阱是真实存在的。完整的发现记录在 [`docs/PROTOCOL.md`](docs/PROTOCOL.md#rtc-management) 中。 ## 展示真实的数据包 ``` [22:38:34.566] HR_SERVICE (4 bytes) raw: 103e1904 flags: 0x10 bit0 = HR size, bit4 = RR present hr_bpm: 62 rr_ms: [1024] 1/1024 sec ticks → milliseconds [22:38:48.060] → SET_CLOCK cmd=0x0A (the 5-byte payload) payload: 1778539127 as LE uint32 + 0x01 commit byte [22:38:48.612] ← COMMAND_RESPONSE cmd=0x0B (GET_CLOCK) hw_rtc: 1778507944 → 2026-05-11T07:59:04+00:00 ✓ valid sub_second: 0.031 (32768 Hz hardware crystal) [22:38:00.662] ← HISTORICAL_DATA (104 bytes, one HR record from flash) timestamp: 1778513464 → 2026-05-11T09:31:04Z hr_bpm: 60 rr_ms: [993] accel xyz: [0.5319, 0.3221, 0.806] (g-force) ``` 完整的注解演练——每一种数据包类型、每一个字节、来自监视器的真实抓包——都在 **[`docs/SAMPLE_SESSION.md`](docs/SAMPLE_SESSION.md)** 中。 ## 文档 根据你想做的事情按顺序阅读以下内容: | 如果你想… | 请阅读 | |------------------|------| | **使用它** —— 提取你自己的数据,设置同步 | 本 README 的 [快速开始](#quickstart) → 运行 [examples/](examples/) | | **了解每个数据包的含义** | [`docs/SAMPLE_SESSION.md`](docs/SAMPLE_SESSION.md) —— 真实抓包注解 | | **查阅协议**(帧格式、命令、字节偏移量) | [`docs/PROTOCOL.md`](docs/PROTOCOL.md) —— 权威参考 | | **了解哪些还是未知的** | [`docs/STATUS.md`](docs/STATUS.md) —— 哪些已解码,哪些部分理解,哪些是黑盒 | | **贡献或扩展逆向工程** | [`CONTRIBUTING.md`](CONTRIBUTING.md) —— 开发循环、探测方法,哪种帮助最有用 | ## 快速开始 ``` # 0. 安装 (Python 3.10+) pip install -r requirements.txt # 1. 首先从官方 Whoop app 取消配对 — 它声称独占 BLE 访问权限。 # 2. 查找你的手环的 BLE 地址 python3 -m whoop4.scan # 扫描 8s... # 发现 1 个疑似 Whoop 手环: # XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX -52 dBm (无名称) # 3. 导出它 export WHOOP_ADDRESS="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" # 4. 修复 RTC (如果 flash 写入一直静默失败) python3 examples/01_fix_rtc.py # 5. 佩戴手环 5–10 分钟,然后提取数据 python3 examples/02_drain_to_jsonl.py # data/hr.jsonl ← 每个 HR 样本对应一条 JSON 记录 # 6. 或者现在实时流式传输 HR python3 examples/03_live_hr.py ``` ## 你能得到什么 | 内容 | 方式 | 备注 | |------|-----|-------| | **实时 HR + RR** | 标准 GATT `0x180D` 服务 | 无需任何自定义协议即可工作。约 1 Hz | | **历史 HR** | 自定义排空 协议 | 每条记录 = 93 字节:ts,HR (bpm),最多 4 个 RR 间期,加速度计 | | **皮肤温度** | 排空期间的 EVENT 数据包 | `TEMPERATURE_LEVEL` (0x11),`BATTERY_LEVEL` (0x03) 中也包含它 | | **电池与佩戴状态** | 排空期间的 EVENT 数据包 | `BATTERY_LEVEL` (0x03),`WRIST_ON`/`WRIST_OFF` (9, 10) | | **Whoop 的恢复 / 压力 / 睡眠评分** | ❌ 不可用 | 这些在他们的云端计算。使用原始的 HR/RR/加速度计数据来计算你自己的(通过 RR 上的 RMSSD 计算 HRV 只需一行代码)。 | ## 仓库布局 ``` whoop4-ble/ ├── README.md This file — start here ├── CONTRIBUTING.md How to extend the RE / probe new commands safely ├── docs/ │ ├── PROTOCOL.md Authoritative protocol reference │ ├── SAMPLE_SESSION.md Real annotated packet captures, end-to-end │ └── STATUS.md What's decoded, what's pending, what's a black box ├── examples/ Minimal single-file scripts to copy-paste │ ├── 01_fix_rtc.py SET_CLOCK with the 5-byte payload │ ├── 02_drain_to_jsonl.py Historical data → ./data/hr.jsonl │ └── 03_live_hr.py Live HR via standard GATT service ├── src/whoop4/ │ ├── __init__.py Public API │ ├── packet.py Frame builder + parser (CRC8/CRC32, all packet types, record decoder) │ ├── scan.py `python3 -m whoop4.scan` │ ├── set_clock.py Standalone SET_CLOCK utility with diagnostics │ ├── drain.py Multi-pass drain with raw capture, dedup, state file │ ├── reader.py Live HR reader (loops, prints, logs) │ └── monitor/ Browser-based live debug monitor │ ├── server.py Python WS + HTTP backend │ └── index.html Dashboard: HR chart, HRV/RHR, hex viewer, command panel, calendar heat map ├── pyproject.toml Installable package metadata ├── requirements.txt └── LICENSE MIT ``` ## 关键 BLE 原语 (备忘录) ``` Service: 61080001-8d6d-82b8-614a-1c8cb0f8dcc6 CMD_TO 61080002-... write send commands CMD_FROM 61080003-... notify command responses DATA_FROM 61080005-... notify historical data + events + console logs Frame: [SOF=0xAA] [Len, 2 LE] [CRC8] [Type] [Seq] [Cmd] [Data...] [CRC32, 4 LE] Length = len(payload) + 4 (payload = Type|Seq|Cmd|Data, CRC32 trails) CRC8 poly = 0x07 over the length field CRC32 = standard (poly 0xEDB88320, init/xor 0xFFFFFFFF) — band accepts this even though its outbound frames use a non-standard variant ``` | Cmd | 名称 | Payload | 备注 | |-----|------|---------|-------| | 0x0A | SET_CLOCK | **5 字节**: `[LE u32 unix_ts][0x01]` | 即发即弃。**4 字节 payload 会被静默忽略——这就是陷阱。** | | 0x0B | GET_CLOCK | 1 字节 `0x00` | 仅在排空 上下文中响应。返回 `[00 01][LE u32 hw_rtc][LE u16 subsec_32768Hz][6× 00]` | | 0x14 | ABORT_HISTORICAL_TRANSMIT | – | 取消活动的排空 | | 0x16 | SEND_HISTORICAL_DATA | `0x00` | 开始排空。同时激活腕带的命令处理器以用于后续命令。 | | 0x17 | HISTORICAL_DATA_RESULT | `[01][LE u32 trim][00 00 00 00]` | 确认 每一个 METADATA HISTORY_END,否则腕带会重发相同的批次 | | 0x21 | SET_READ_POINTER | – | 倒回而不删除数据 | | 0x22 | GET_DATA_RANGE | – | 返回当前的 trim 偏移量和批次范围 | | 0x4C | (GET_DEVICE_NAME) | – | 返回 ASCII "WHOOP AGT" (Advanced Generation Tracker) | 完整表格——包括事件类型、93 字节历史记录布局和历史排空状态机——请参阅 [`docs/PROTOCOL.md`](docs/PROTOCOL.md)。 ## RTC 陷阱(这是重点) 如果即使你一直戴着腕带,排空 仍返回 `PullStats: Data: 0, Events: N`,那么固件 RTC 很可能卡住了。固件在排空期间会在 CONSOLE_LOG 数据包中发出此信息: ``` Flash: RTC timestamp 32154354 is invalid; not saving data to flash. ``` 该值是硬件 RTC 计数器。有效性检查会拒绝任何不是合理 Unix 时间戳(> 大约 2020 年)的值。当 RTC 处于此状态时,闪存写入会静默变成空操作——**你的数据空白期在无形中积累**,直到下一次同步显示零条新记录。 解决方法是使用 5 字节 payload 的 `SET_CLOCK`。[gowhoop](https://github.com/cs-balazs/gowhoop/blob/main/client/commands.go) 和 [whoomp](https://github.com/jogolden/whoomp) 中记录的 4 字节 payload 会被 BLE 协议栈接受,但**从未被固件应用**。我们根据经验确认,位置 4 处的任何字节值都可以——固件似乎只检查 `payload_len >= 5`。我们按惯例使用 `0x01`。 ``` # 错误 (静默忽略) payload = struct.pack("0x60)需自行承担风险——该范围可能包含固件更新 / 恢复出厂设置命令。 ## 许可证 MIT —— 详见 [LICENSE](LICENSE)。 ## 贡献 欢迎提交 PR。请参阅 **[`CONTRIBUTING.md`](CONTRIBUTING.md)** 了解开发循环、探测方法(正是此方法发现了 5 字节 SET_CLOCK 修复方案),以及哪种帮助最有用。 一份高杠杆待解决问题简表: - 解码 93 字节 HISTORICAL_DATA 记录中的第 24-32 字节和第 45-92 字节(可能是更多传感器通道 —— 参见 [STATUS.md](docs/STATUS.md#black-box)) - 解码 EVENT 类型 11、12、16、29、32、101、102、103 - 谨慎探测 cmd 字节 0x60-0x7F(振动触发器似乎位于 0x43-0x4D;固件更新命令可能位于 0x80 以上) - Linux / Windows 测试 —— 大部分开发工作都在 macOS 上完成
标签:BLE, GATT, HRV, JSONL, Python, RR间期, Whoop 4.0, 云资产清单, 健康监控, 加速度计, 协议分析, 可穿戴设备, 命令控制, 固件RTC, 多模态安全, 实时数据流, 开源硬件, 心率, 心率变异性, 心率服务, 数据提取, 数据采集, 无后门, 无订阅, 时序数据库, 智能手环, 本地存储, 权限提升, 物联网, 生物识别, 生理数据, 皮肤温度, 硬件黑客, 绕过订阅, 蓝牙, 逆向工具, 逆向工程