LogosIsLife/open_ring

GitHub: LogosIsLife/open_ring

纯 Python 实现的 Oura Ring 4 BLE 协议只读驱动,通过静态逆向工程实现完整的生物识别数据解码与设备参数控制。

Stars: 1 | Forks: 1

# `open_ring` — 纯 Python 的 Oura Ring 4 BLE 协议只读驱动 对 Oura Ring BLE→生物识别数据管线的纯净室重新实现, 基于 `libringeventparser.so` (arm64-v8a 版本) 的静态逆向工程及 针对设备端数据库行的实证验证。 **无附带二进制文件。无专有代码库。仅依赖标准库 + `bleak` + `cryptography`。** ## *与 Oura Health Oy 无关联* ## 已实现的功能 ### 线路层 - ✅ 外部帧解析(24/24 操作码 — battery、time-sync、handshake、subscribe、history-fetch、parameter RPC 等) - ✅ 内部记录 TLV 遍历器 - ✅ AES-128-ECB-PKCS5 握手(`cryptography` 库 或 `openssl` 子进程回退) - ✅ 从 `assa-store.realm` 提取 `auth_key` - ✅ 针对 **35 种无状态类型 + 1 种有状态类型 (`0x81 CvaRawPpg`) + 31 种 DebugData (`0x61`) 子类型** 的按类型线路格式解码器(其中 3 种为有状态的多记录会话),0% 解码错误。在 70 小时的 `sunday_evening.log` 测试捕获(117,662 条内部记录,34 种不同类型)中: - **99.73% 强解码**(117,345 条记录 — 类型化语义字段,与设备端数据库和/或 proto schema 交叉验证): - `0x60 IBI` — 11 位位打包 IBI + 带移位的 amp;在 77k 个样本中,**平均匹配设备端数据库的误差在 1.3 毫秒内** - `0x46 Temp` — temp1/2/3 °C 范围**与数据库完全匹配** - `0x5d HRV` — HR 平均值与 IBI 导出的平均值相差在 **0.2 BPM 以内** - `0x47 MotionEvent` — `acm_x/y/z` 范围**与数据库逐字节精确匹配** - `0x6f Spo2` — 16,905 个按样本百分比值,在 80–100% 的生理完美范围内 - `0x80 GreenIbiQuality` — 43,859 个按样本的 11 位 IBI-ms + 2 位/3 位质量标志 - `0x81 CvaRawPpgData` — **45,851 个原始 24 位 ADC 样本 = 数据库 `TimeseriesDbPpgSample` 的精确前缀,100% 逐样本对应** - **有状态 DD 子类型** — `ChargerInformation` (sub_sub_type 1 记录一致解码为固件字符串 `'6050378'`),`ChargerDebugInformation` (头部/连续帧平衡为 16/20),`HardwareTestResultValues` (初始/中间/最终三元组平衡为 3/3/3,中间的 u16 = `3934`,一致的 ADC 测试读数) - `0x6a SleepPeriodInfo2` — `average_hr [57.5..102.0] BPM`,`breath [6..20.25] /min`,`sleep_state ∈ {0,1,2}`,以及 mzci/dzci/breath_v/motion_count/cv(通过真值表声明进行验证) - `0x75 SleepTempEvent` — 以 30 秒间隔采样的 N×u16 皮肤温度样本;在 2,182 个样本中的范围为 `[31.84..36.78]°C` - `0x61 DEBUG_DATA` — 子字节分发到 28 个类型化解析器(约 99.7% 的调试记录,包括 1 个位打包、1 个有状态和 **4 个库自身的分发表映射到其默认抛出分支,但携带着应用在下游使用的真实结构的子类型**):`OpenAfePpgSettingsData` (5,861),`SleepStatistics` (2,723),`AfeStatisticsValues` (2,710),`FlashUsageStatistics`/`BleUsageStatistics`/`PeriodInfoStatistics` (各 1,368),`AfePeriodTick` (1,323 — 子类型 0x3b,以 50Hz/25Hz 严格交替,恰好 661/662),`PpgSignalQualityStats` (1,074 — 最高有效位优先比特流),`FuelGaugeStatistics` (511 — 电池/电压与 BatteryLevelChanged 交叉核对),`AcmConfigurationChanged` (329),`EventSyncStatistics` (227),`EventSyncCacheStatistics` (227),`BatteryLevelChanged` (135),`PeriodicCounter` (111),`DebugDataText` (77 — 与 0x43 同族的 ASCII 字符串),`PpgCont` (69),`FingerDetection` (68),外加 11 个低容量子类型 (`SecurityFailure`、`BootLoaderDebugLog`、`FuelGaugeRegisterDump`、`RingHwInformation`、`ChargingEndStatistics`、`FuelGaugeLoggingRegisters`、`HardwareTestStartValues`、`ChargingEndStatisticsContinued`、`FieldTestInformation`、`StackUsageStatistics`、`DailyDropSample`) - `0x42 TimeSync`,`0x43 DebugEventInd`,`0x45 StateChangeInd / 0x53 WearEvent`, `0x76 BedtimePeriod`,`0x4a PpgAmplitudeInd`,`0x41 RingStartInd`,`0x69 TempPeriod`, `0x73 EHR trace`,`0x74 EHR ACM intensity`,`0x6e SpO2-IBI/amp`,`0x7e/0x7f RealSteps`, `0x77 SpO2-DC`,`0x6c FeatureSession`,`0x82 ScanStart` / `0x83 ScanEnd`, `0x50 ActivityInfo`,`0x5b BleConnection`,`0x5e Selftest`, `0x6b MotionPeriod`,`0x72 SleepAcmPeriod`,`0x49 / 0x4c / 0x4f SleepSummary 1-3` - **0% 弱解码/仅类型** — 每条观察到的记录现在都具有结构化字段。有状态的 DD 子类型(`0x36 charger_information`、`0x3d charger_debug_information`、`0x26 hardware_test_result_values`)按记录种类/阶段及结构化字段进行解码;完整的会话聚合(匹配 `Session+0x138 DebugData_State_v1`)留给消费者处理,因为每条记录的解码已经过真值验证(例如 ChargerInfo 文本解码为固件字符串 `'6050378'`,HardwareTestResult 中间 u16 读数始终为 `3934`)。 - **0.3% 无解码器**(317 条记录):2 个未映射的固件扩展标签 `0x33` (240 条) / `0x85` (77 条) ### 驱动 - ✅ **实时 BLE 传输**(基于 `bleak` 的异步 `OuraRingClient`),支持自动重连、握手、时间同步、事件订阅和自主追赶 - ✅ 针对 btsnoop 捕获的**离线重放**模式 (`oura_ring.replay`) — 与实时模式使用相同的信封 schema - ✅ **JSONL 输出信封** — 每行一条记录,对流式处理友好 - ✅ 用于控制平面/生命周期可见性的**合成事件**(见下文“两个平面”) ### 两个平面 驱动程序分离开两个不同的关注点: **1. 自主追赶(数据平面)** — 在每次(重新)连接时,驱动程序请求 history-fetch 以检索断开连接期间缓冲的记录。作为 `_HISTORY_FETCH_REQ` / `_HISTORY_FETCH_RESP` 事件呈现。已验证:在测试捕获中,**167 次重连中发生了 327 次抓取 = 每次重连 1.96 次**。 **2. 按需控制(控制平面)** — 用于主动配置环形设备的高级方法: | 方法 | 功能 | |---|---| | `await client.set_spo2(on)` | 切换 SpO₂ 采样 | | `await client.set_activity_hr(on)` | 切换活动心率检测 | | `await client.set_dhr_mode(mode, sub_mode)` | 设置白天 HR 模式 + 子模式 | | `await client.request_hr_on_demand()` | 触发已记录的突发心率检查 | | `await client.read_param(0x04)` | 读取 4 字节的 SpO₂ 结构 | | `await client.write_param_byte0(p, v)` | 通用字节 0 写入 | | `await client.request_history(sub, cur)` | 手动增量同步抓取 | 作为 `_PARAM_READ` / `_PARAM_READ_RESP` / `_PARAM_WRITE_B0` / `_PARAM_WRITE_B2` / `_PARAM_PUSH` 合成事件呈现。 ### 持久化 大多数状态都是短暂的或可重新推导的;**在跨进程重启时唯一重要的丢失字段 是 `ClientState.last_history_cursor_by_subop`** — 每个子操作的增量同步游标。丢失它们会导致每次重连都变成一次完整重新同步 (`cursor=0`),这会重新抓取环形设备循环缓冲区中的所有内容, 如果缓冲区在会话之间循环覆盖,则有永久丢失数据的风险。 `oura_ring.CursorStore` 将游标映射持久化到一个小的 JSON 文件中(对于 70 小时的会话约为 3 KB, 每个活动子操作约 24 个整数): ``` from oura_ring import CursorStore from oura_ring.transport import OuraRingClient store = CursorStore("~/.local/share/oura_ring/cursors.json") async with OuraRingClient(mac="A0:...", realm_path="assa-store.realm", cursor_store=store) as r: async for rec in r.stream(): ... # 每 64 次前进自动 flush Cursors,以及在断开连接 / 取消 / 关闭时 flush。 ``` CLI 形式:`python -m oura_ring.cli live --mac ... --realm ... [--cursor-file PATH | --no-cursor-file]`。 默认路径:`~/.local/share/oura_ring/cursors.json`。原子写入(tmp + 重命名),对损坏文件具有弹性(加载返回 `{}` 而不是抛出异常),且 仅支持单调更新,因此线路流中偶尔的回归不会使游标回退。 ### 状态模型 两个小型 dataclass,用于消费 JSONL 流并跟踪环形设备 + 驱动状态 — 无 I/O,无传输耦合: | `RingState` | `ClientState` | |---|---| | BLE 链接,身份(固件/序列号) | 连接阶段 (DISCONNECTED → CONNECTING → HANDSHAKING → SUBSCRIBED → STREAMING) | | 统一的 `StateChange` 枚举(当前状态 + 名称 + 文本) | 握手 / 时间同步计数器 | | 子状态机:DHR、CVA、A:SA、EHR(从 `0x43` 调试字符串解析) | 已见记录 + 按类型覆盖率(计数、最后计数器、最后会话) | | 电池(电量 %,电压 mV),充电,方向 | 自主追赶:历史抓取,每个子操作的游标 | | `params[pid]` — 每个 ID 最后一次见到的 4 字节参数结构 | 按需控制:按参数的读/写/推计数 | ``` from oura_ring import replay, RingState, ClientState ring = RingState() client = ClientState() for rec in replay("capture.log"): ring.apply(rec) client.apply(rec) print(client.snapshot()) # {phase: STREAMING, handshake_count: 168, …} print(ring.snapshot()) # {state: 3 STATE_FINGER_USER_ACTIVE, dhr_state: 1, …} ``` ## 端到端验证 `tools/verify_claims.py` 中的完整回归测试套件。最近一次运行: ``` ## 验证 — 测试了 230 个 claims PASS=227 FAIL=3 ``` 3 个失败 (FAIL) 是对先前 markdown 声明的真正证伪 (`_FIELD_NUMBER` 计数差了 3,jzlog 帧不统一,battery 子操作)。 已验证内容的亮点: - 线路真值:ATT MTU=247,484/484 握手 nonce/proof 对根据 `auth_key` 验证通过,166/166 时间同步公式检查通过 - 与设备端数据的往返比对 (Realm 导出 + SQLite): - **IBI**:99.7% 覆盖率,与数据库的平均匹配度在 **1.3 ms 以内** (845.0 对比 846.3) - **IBI 振幅**:最大值精确匹配 (16256),中位数相差在 10% 以内 - **TempEvent.temp1/2/3_c**:范围与数据库 `temperature_1/2/3` *精确匹配* (`[24.10..41.74]`,`[28.00..46.00]`,`[20.28..37.71]`) - **MotionEvent.acm_x/y/z**:范围与数据库 `acm_average_x/y/z` 逐字节精确匹配 (`[-968..1016]`,`[-1024..1016]`,`[-1000..992]`) - **HRV.hr_bpm 平均值 = 70.8** 与 IBI 导出的平均值 (60000/845 = **71.0 BPM 相差在 0.2 BPM 以内 — 跨解码器一致性 - **SpO₂**:16,905 个按样本百分比值在 80–100% 范围内,平均值 93.3% - **GreenIbiQuality**:43,859 个按样本的 `value_11bit` 在 IBI-ms 范围 `[320..2000]` 内 - **CvaRawPpg (原始 24 位 ADC 样本)**:45,851 个驱动样本 = `TimeseriesDbPpgSample` 的精确前缀,按顺序逐样本 100% 匹配(有状态的 `CvaPpgDecoder` 反映了 lib 的 `decode_ppg_event_bytes`) - **DebugData (0x61) 子分发**:17 个子类型(约 19,645 条调试记录的 99.5%)解码出经过交叉验证的值:`EventSyncStatistics.mtu == 247` (匹配 K8),`FuelGaugeStatistics.battery_pct ∈ [49.4..91.1]%`,`voltage ∈ [3533..4142] mV` (锂离子电池),`BatteryLevelChanged.voltage ∈ [3534..4141] mV` **独立地**与 FuelGauge 包络一致,`PpgSignalQualityStats.ibi_quality_percentage ∈ [0..95]%`,`AfePeriodTick.period_us ∈ {20000, 40000}` (50Hz/25Hz,在 1,323 条记录中精确平衡为 661/662),`AcmConfigurationChanged.mode ∈ [0..4]` (完整的 proto enum),以及 **4 个库的映射表指向 default-throw 但包含应用消费结构的子类型**(经验性地解码:ASCII 字符串,周期节拍,周期性计数器,PPG-cont 头部) - 11 天内的 DailyReadiness 分数 ∈ [0..100];4 天内的 DailySleep 分数 [0..100];所有 16 个子分数贡献者均在范围内 - 固件版本 `2.10.4` ✓,引导程序 `1.0.1` ✓,ring_type=6 (Gen 4) ✓ - 管道基数 (Wire → DbRawEvent):**100.3% 完整性**,32/34 种类型以 1:1 持久化;`0x43` 调试字符串 + `0x33` 固件扩展被故意过滤 - 状态机:重现了 DHR `0→1→4→2→0` 主循环(573 个周期,11.0% 的重试率与分析笔记中约 10% 一致);CVA 各状态计数平衡;A:SA 在顶部呈现 `1,1`/`1,2` 乒乓状态 - **因果证明**:每个观察到的 SpO₂ 切换写入都在几秒钟内伴随着 `O2Mode;N` 调试字符串(10 次写入 → 12 个字符串) - 在 `thursday.log` 中**发现了软重置操作码**:手机 `0e 01 ff` → 环形设备 `0f 01 00` → 在 22-35 秒内重启;3/3 次重置命令与 `RingStartInd` 事件相关联 - **端到端管道重建** (`tools/reconstruct_timeseries.py`):驱动程序解码 BLE 线路字节,并以接近 1:1 的基数重建设备端的 `TimeseriesDb*` Realm + SQLite 表。**8 个环形调试表与真实值的偏差验证在 ±5% 以内** (battery_level,fuel_gauge_statistics,ppg_signal_quality_stats,feature_session,state_change,time_sync_indication,以及 100% 精确前缀的 PpgSample)。**有状态多记录重建**:我们的 0x36 charger_information 解码器重建了存储在 `timeseries_charger_firmware_and_psn` 中的精确充电器序列号 `'40260D2606050378'`。**值级别**:不同 `battery_percentage` 值的 100% 重叠;IBI 逐拍平均值与数据库匹配度在 **2 ms 以内**。 ## 快速开始 ``` # 在 stdout 上以 JSONL 格式回放现有的 btsnoop 捕获 python3 -m oura_ring.cli replay sunday_evening.log | head # 来自 ring 的实时流(需要 `pip install bleak` + 已配对的 ring) python3 -m oura_ring.cli live --mac A0:38:F8:A4:09:C9 --realm path/to/assa-store.realm ``` ``` import asyncio from oura_ring import RingState, ClientState from oura_ring.transport import OuraRingClient async def main(): ring = RingState() client_state = ClientState() async with OuraRingClient(mac="A0:...", realm_path="assa-store.realm") as r: # Toggle SpO2 on demand await r.set_spo2(True) async for rec in r.stream(): ring.apply(rec) client_state.apply(rec) if rec.type == "API_IBI_AND_AMPLITUDE_EVENT": print(rec.data["ibi_ms"], rec.data["amp"]) if rec.type == "_PARAM_PUSH": print("ring config changed:", ring.params) asyncio.run(main()) ``` ## JSONL 信封 通用形状,每行一条记录: ``` {"t":1777033068525,"rt":76522,"ctr":11609,"sess":2,"tag":"0x60","type":"API_IBI_AND_AMPLITUDE_EVENT","data":{"ibi_ms":[555,867,843,827,817,764],"amp":[0,896,992,1168,1024,928],"amp_shift":4}} ``` | 字段 | 总是存在 | 含义 | |---|---|---| | `t` | 是 | UTC 毫秒 (Unix 纪元),经时间同步校正 | | `rt` | 仅内部记录 | TLV 头部中的环形设备时间 uint32 | | `ctr` | 仅内部记录 | 按类型的计数器 | | `sess` | 仅内部记录 | 会话 ID | | `tag` | 是 | 线路字节十六进制 (`"0xNN"`) 或带下划线前缀的合成事件 | | `type` | 是 | 规范名称 (`API_*`) 或 `_*` 合成事件 | | `data` | 是 | 特定类型的解码字段(或 `{"hex":"..."}` 回退) | ## 合成事件类型 | 类型 | 何时发出 | 平面 | |---|---|---| | `_HANDSHAKE_NONCE` / `_PROOF` / `_OK` / `_FAIL` | 握手序列 | 生命周期 | | `_TIME_SYNC_REQ` / `_TIME_SYNC_REPLY` | 0x12 / 0x13 | 生命周期 | | `_BATTERY` | 0x0d 电池响应 (voltage_mv) | 生命周期 | | `_DISCONNECT` | BLE 链接断开(仅限实时模式) | 生命周期 | | `_HISTORY_FETCH_REQ` / `_HISTORY_FETCH_RESP` | 带有游标的 0x10 / 0x11 | **自主追赶** | | `_PARAM_READ` / `_PARAM_READ_RESP` | 0x2F sub 0x20 / 0x21 | **按需控制** | | `_PARAM_WRITE_B0` / `_PARAM_WRITE_B2` | 0x2F sub 0x22 / 0x26 | **按需控制** | | `_PARAM_PUSH` | 0x2F sub 0x28 (来自环形设备的主动上报) | **按需控制** | | `_STATE_PULSE` | 0x1f 自主状态机脉冲 | 生命周期 | ## 尚未实现的功能 - 🟡 实时 BLE 传输**无法从该开发机测试**(没有环形设备) — 解码管道以 1:1 的方式与离线重放共享(离线重放已验证),因此实时路径只需最终用户在真实的环形设备上进行冒烟测试。 - 🟡 **两个未映射的固件扩展标签** `0x33`(240 条记录)和 `0x85`(77 条记录) — 库中没有任何解析器符号。如果不逆向工程固件本身,**数据流中这 0.27% 是真正无法恢复的**。 - 🟡 `0x68 API_RAW_PPG_DATA` — 类型在枚举中定义,但在任何捕获的日志中都从未在线路上观察到。库的重载 `decode_ppg_event_bytes(…, OldPPG_State_v1)` 位于 `libringeventparser.so 0x2c1268`,一旦有语料库即可使用。 - 🟡 两个固件扩展标签 `0x33` 和 `0x85`(库中没有解析器符号)。 ## 架构 ``` btsnoop/BLE bytes │ ▼ framing.parse_outer_frames / parse_inner_records ← TLV / opcode walker │ ▼ decoders.decode(type_byte, payload) ← per-type wire decoders (data plane) │ ▼ replay._outer_to_record / transport._outer_to_records envelope.Record ← typed dataclass │ ├──→ consumer (your code) — JSONL stdout, file, network │ └──→ state.RingState.apply(rec) state.ClientState.apply(rec) ← state-tracking models (both planes) ``` ## 来源 该驱动程序直接基于 `oura_truth_table.md` 中经过验证的发现 以及 `wireformat_extract.json` 中自动提取的解析器元数据构建。高频线路格式解码器(IBI 位打包,HRV 对遍历,Temp signed/100,StateChange 模板等)均逐字节派生自 `libringeventparser.so` (arm64-v8a,md5 `9941cfb8214faf55150a0b6082127e90`) 的反汇编。 往返实证验证:纯 Python IBI 解码器从 70 小时的 `sunday_evening.log` 捕获中生成了 77,760 个值;设备端数据库在同一捕获的 76 小时超集窗口内持久化了 77,985 行 IBI。平均 IBI:驱动程序 845.0 ms,数据库 846.3 ms(差值 1.3 ms — 在采样时钟噪声范围内)。 ## 许可信息 本项目基于 GNU 通用公共许可证第 3 版授权。有关详细信息,请参阅 LICENSE 文件。
标签:AES-128-ECB, arm64, BLE, bleak, cryptography, IoT安全, libringeventparser, Oura Ring, PKCS5, Python, 云安全监控, 云资产清单, 健康监测, 健身数据, 加速度计, 医疗物联网, 协议分析, 可穿戴设备, 固件分析, 开源驱动, 心率, 心率变异性, 插件系统, 数据解析, 无后门, 时序数据库, 智能戒指, 权限提升, 洁净室设计, 温度传感器, 生物特征识别, 蓝牙低功耗, 血氧, 计算机取证, 逆向工具, 逆向工程, 静态分析