Janczykkkko/xbloom-ble

GitHub: Janczykkkko/xbloom-ble

通过逆向工程的 BLE 协议,为 xBloom Studio 手冲咖啡机提供非官方的 Python 命令行配方管理和设备控制能力。

Stars: 0 | Forks: 0

# xbloom-ble [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/Janczykkkko/xbloom-ble/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/xbloom-ble?cacheSeconds=3600)](https://pypi.org/project/xbloom-ble/) [![Python](https://img.shields.io/pypi/pyversions/xbloom-ble?cacheSeconds=3600)](https://pypi.org/project/xbloom-ble/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![Built with Claude Code](https://img.shields.io/badge/built%20with-Claude%20Code-d97757) **对 [xBloom Studio](https://xbloom.com) 手冲咖啡机进行的非官方 Bluetooth LE 控制。** 目前没有官方的 xBloom API。本包解析了机器逆向工程的 Bluetooth Low Energy 协议,以便您可以编写脚本并 对您的配方进行版本控制 —— 发现设备、验证配方、**将其加载到机器上**, 并实时查看冲煮遥测数据。 它还可以 —— 可选地 —— 通过非官方的云 REST API(`xbloom cloud`,见下文) **将配方同步到您的 xBloom 手机 App 账户**,这样您保留在版本控制中的配方也会出现在 App 中。 它是一个体积小巧、依赖轻量的 Python 包(`bleak` + `pyyaml`;云功能额外增加了可选的 `cryptography` 依赖), 拥有简洁的 CLI 和完全公开的协议文档,方便他人在此基础上进行开发。 ## ⚠️ 安全性 —— 本工具仅执行*加载*,绝不自动启动 这是核心设计决策,也是一项严格的不变量: xBloom BLE 协议*确实*包含强制启动冲煮的操作码(`0x42` 提交和 `0x46` 启动)。 **本包绝不构建或发送它们。** 代码中故意没有发射 `0x42`/`0x46` 的路径 —— `build_load_frames()` 仅返回四个 LOAD 帧,甚至还有一个防御性的断言机制,如果意外引入了禁止的操作码,就会直接拒绝。 因此,本工具能做出的最糟糕行为,仅仅是让您准备好一个配方,随后您必须当面在杯子和咖啡豆前手动确认。 ## 测试适配 本工具是基于 **运行固件 `V12.0D.500` 的 xBloom Studio** 开发和验证的 —— 这是它**唯一测试过的设备和固件版本**。 逆向工程的协议在其他固件或硬件版本上可能有所不同,并且可能会在未来的更新中失效。 **非常欢迎您报告其他固件/硬件的情况** —— 如果它在您的机器上可行(或不可行),请提交一个 issue。 来自不同固件版本的 BLE 捕获数据尤其有用(参见 [CONTRIBUTING.md](CONTRIBUTING.md);请先剔除任何个人数据)。 ## 安装 ``` pip install xbloom-ble ``` 从源码安装: ``` git clone https://github.com/Janczykkkko/xbloom-ble cd xbloom-ble pip install -e . ``` **Linux** 需要运行 BlueZ (`bluetoothd`) —— 它是标准的系统蓝牙协议栈,也是 `bleak` 通信的基础。 macOS 和 Windows 使用其原生的 BLE 协议栈。必须开启蓝牙,在 Linux 上您可能需要以具有 BLE 权限的用户身份运行。 ## 快速开始 请按以下顺序操作: 1. **安装**(如上所述)并确保已开启蓝牙。 2. **发现设备** —— `xbloom scan` —— 并记下其地址(或者设置 `export XBLOOM_ADDRESS=…`,这样就不用反复输入了)。 3. **编写配方** —— 一个小型的 YAML 文件(见[配方格式](#recipe-format)),或者通过 URL 指向托管的配方。使用 `xbloom validate ` 进行检查。 4. **在进行任何写入操作之前,请确保手机 App 已与机器断开连接** —— 机器仅允许**单一** BLE 连接,而 App 会占用它。请关闭 App,*并*关闭手机的蓝牙。 5. **选择下方的两种路径之一。** 然后,无论您选择哪条路径: (另外,`xbloom cloud` 用于管理您手机 App 账户中的配方**库** —— 这不是直接驱动机器的方式;见下文。) ## 使用方法 CLI 命令为 `xbloom`。 ### 发现设备 ``` xbloom scan ``` ``` Found 1 machine(s): AA:BB:CC:DD:EE:FF XBLOOM-1234 ``` 机器是通过其厂商 **service UUID** (`0000e0ff-3c17-d293-8e48-14fe2e4da212`) 或以 `XBLOOM` 开头的设备名称来发现的 —— 无需硬编码地址。 ### 验证配方(无需硬件) ``` xbloom validate recipes/example-washed.yaml ``` ``` OK: 'Example Washed' — 16 g, grind 62, 3 pours, 240 ml total water ``` ### 加载配方并查看冲煮 ``` xbloom brew recipes/example-washed.yaml --address AA:BB:CC:DD:EE:FF ``` 或者通过环境变量设置地址(这样任何内容都不会被硬编码): ``` export XBLOOM_ADDRESS=AA:BB:CC:DD:EE:FF xbloom brew recipes/example-washed.yaml ``` 它会验证、连接、**加载**配方,然后打印: ``` ✋ Recipe loaded. Add beans + cup, then APPROVE ON THE MACHINE to start. (This tool will NOT start it.) ``` ……并实时流式传输状态(机器状态变化),直到冲煮完成或超时(`--timeout`,默认 300 秒)结束。遥测日志将写入 `./telemetry-.json`。 `brew` / `validate` / `cloud` 的配方参数也可以是一个 **`http(s)://` URL** —— 这样配方可以直接被服务提供并用于冲煮,无需预先下载: ``` xbloom brew https://xbloom.lodywgumce.tv/r/teso-la-leona.yaml ``` 常用参数:`--address`, `--timeout`, `-v/--verbose`, `--version`。 ### 编程刻度盘预设 (save-slots) xBloom Studio 的 **Auto Mode** 会在机器的刻度盘上存储三个配方(插槽 **A / B / C**),这样无需手机即可从刻度盘开始冲煮。`save-slots` 会通过三个配方一次性对这三个插槽进行编程 —— 这是一个**预设写入操作,它绝不会直接冲煮**: ``` xbloom save-slots light.yaml medium.yaml iced.yaml xbloom save-slots a.yaml b.yaml c.yaml --scale-off C # disable the scale in slot C's preset ``` 在一次调用中必须提供全部三个 —— 机器只有在接收到完整的 A/B/C 集合后才会*存储*预设(它会以原子方式保存批次)。 如果只写入单个插槽,机器会显示 **RETRY**。 ### 将配方推送到您的 App 账户 (cloud) 除了 BLE 机器控制之外,`xbloom cloud` 还可以通过*非官方*的 xBloom 云 REST API 将配方推送到您的 xBloom **App 账户**,这样您在此定义的配方就会出现在手机 App 中。需要可选依赖:`pip install "xbloom-ble[cloud]"`。 ``` export XBLOOM_EMAIL=you@example.com XBLOOM_PASSWORD=… # or `xbloom cloud login` xbloom cloud sync my-recipe.yaml # create-or-update a tool-owned recipe (idempotent) xbloom cloud list # list account recipes ('*' = tool-owned) xbloom cloud delete # only AUTO … recipes can be deleted xbloom cloud fetch # read a publicly shared recipe (no auth) ``` 这使用的是由社区逆向工程的非官方 API(它可能会失效,并且会接触到您的真实账户) —— 有关底层机制(RSA 加密的 body、endpoint、字段 schema),请参阅 [`cloud.py`](xbloom_ble/cloud.py)。 ## 配方格式 配方是普通的 YAML: ``` name: Example Washed dose_g: 16 # coffee dose in grams grind: 62 # grinder setting (1–80) ratio: 15 # optional; if given, Σ pour ml must equal dose_g * ratio stage_temps: [110.0, 90.0] # optional; machine stage temps, default 110/90 pours: - {ml: 45, temp_c: 93, pattern: spiral, agitation: true, pause_s: 40, rpm: 100, flow_ml_s: 3.0} - {ml: 100, temp_c: 91, pattern: spiral, pause_s: 10, rpm: 100, flow_ml_s: 3.2} - {ml: 95, temp_c: 90, pattern: spiral, pause_s: 5, rpm: 100, flow_ml_s: 3.2} ``` 每次注水字段(范围是**严格的 —— 根据官方 xBloom Studio 规格**): | 字段 | 含义 | |-------------|----------------------------------------------------------------| | `ml` | 本次注水的水量 (≥1 ml)。超过 127 ml 的注水会被协议自动拆分 —— 不会报错。 | | `temp_c` | 水温 (40–95 °C,步长为 1 °C)。 | | `pattern` | `spiral`、`ring` 或 `center`。 | | `agitation` | 仅在 `spiral` 下为 `true`(带搅拌的闷蒸)。默认为 `false`。 | | `pause_s` | 本次注水后的暂停时间,秒 (0–255;机器上的倒计时上限接近 99 秒)。 | | `rpm` | 搅拌转速 (60–120,步长为 10-RPM;`center` 下为 `0`)。 | | `flow_ml_s` | 流速,单位为 ml/s (3.0–3.5,步长为 0.1)。 | App 还公开了两个**特殊的非数字温度设置 —— `RT` (室温) 和 `BP` (沸点)** —— 它们无法用数字型的 `temp_c` 表示; 数字范围是 40–95 °C。 有关完整的表格及强制执行的严格边界,请参阅下方的 **[配方限制与有效范围](#recipe-limits--valid-ranges)**。 验证会拒绝以下情况:少于两次注水(您至少需要一次闷蒸和第一次注水)、未知的 `pattern`/`agitation` 组合、超出范围的数值,以及 —— 如果给定了 `ratio` —— 注水总量不等于 `dose_g * ratio`。 ## 配方限制与有效范围 这些是 `xbloom validate` 强制执行的边界。大多数是**严格的(根据 xBloom Studio 公布的规格)** —— 属于真实的机器/App 限制;少数上限(`ml`、`pause_s`)仍作为实际应用中的合理性保护。 **如果您的机器表现不同,请带上捕获数据提交一个 issue —— 这些范围应该与真实的硬件保持一致。** | 数值 | 接受范围 | 严格程度 | |---------------|----------------|----------| | `dose_g` | 1–18 g | **严格(根据 xBloom Studio 规格)。** 18 g 是 xBloom App 允许设置的最大值。 | | `grind` | 1–80 | **严格(根据 xBloom Studio 规格)。** 磨豆机有 80 个微步(每步约 18.75 µm);*数字越小*研磨越*细*。 | | `temp_c` (注水) | 40–95 °C | **严格(根据 xBloom Studio 规格)。** 以 1 °C 步长设置。App 还提供特殊的非数字型 `RT` (室温) 和 `BP` (沸点) 设置,在此数字范围之外。 | | `stage_temps` | 每项 40–130 °C | 机器的**预热/阶段设定值**(默认为 110/90 °C) —— 而非注水温度,因此合理地超过 95 °C 的注水上限是正常的。围绕默认值有更宽的允许误差。 | | `rpm` | 0 或 60–120 | **严格(根据 xBloom Studio 规格)。** 60–120,步长为 10-RPM;`0`(无搅拌)仅在 `center` 注水时允许。 | | `flow_ml_s` | 3.0–3.5 ml/s | **严格(根据 xBloom Studio 规格)。** 步长为 0.1 设置。 | | `pause_s` | 0–255 | 网络传输字节为 `256 − seconds`(因此 0–255 可以放下),但**机器上的倒计时上限接近 99 秒** —— 请将 0–99 视为实际有效范围。 | | `ml` (注水) | 1–4000 ml | 下限 (≥1) 是严格的;超过 127 ml 的注水会被协议**自动拆分**(不报错)。4000 的上限仅为合理性保护。 | | `pattern` | `spiral`, `ring`, `center` | **严格。** 这些是解析出的图案代码;`agitation: true` 仅在 `spiral` 下有效。 | 注水次数必须 **≥2**(至少一次闷蒸和一次注水),并且如果您提供了可选的 `ratio`,Σ(注水 ml) 必须等于 `dose_g * ratio`。 ## 逆向工程协议 该底层协议是从 Android 的蓝牙 HCI 捕捉中逆向工程得出的,并已通过原始录制帧的往返验证。 本文档已作完整记录,方便您在此基础上进行构建。 ### 帧格式 写入 `ffe1`(主机 → 机器)的**命令**格式如下: ``` 58 01 01 | CMD(u8) | SEQ(u8) | LEN(u16 LE) | 00 00 | PAYLOAD | CRC16(u16 LE) ``` - `58 01 01` —— 固定 header。 - `CMD` —— 命令 opcode。 - `SEQ` —— 序列字节;加载序列使用 `0x1f` (31)。 - `LEN` —— 帧总长度(从 header 到 CRC),小端序,位于偏移量 5 处。 - `00 00` —— 两个固定的零字节。 - `PAYLOAD` —— 特定于命令的内容。 - `CRC16` —— 对整个帧减去最后两个字节计算出的 **CRC-16/KERMIT**,以小端序存储。 在 `ffe2`(机器 → 主机)上的**通知**使用**不同**的结构 —— 请参阅下方的[状态通知](#status-notifications-ffe2)。 **CRC-16/KERMIT:** 多项式 `0x1021`,init `0`,反转输入和输出,无最终异或(对于 `b"123456789"`,校验值为 `0x2189`)。 ### GATT 厂商服务 `0000e0ff-3c17-d293-8e48-14fe2e4da212`: | Characteristic | 简称 | 角色 | |----------------|-------|---------------------| | command | `ffe1`| write | | status | `ffe2`| (telemetry) | | aux | `ffe3`| auxiliary | ### LOAD 序列 逐帧发送至 `ffe1`,等待 `ffe2` 上的每个 ACK(机器回显该命令,例如 `580207a6…`): 1. **`0xa4`** —— 会话开始。固定 payload `01 b9 00 00 00 01 00 00 00`。 2. **`0xa6`** —— 咖啡粉量。payload 偏移量 9 处为以 `u8` 表示的克数。 3. **`0xa8`** —— 阶段温度。`01` + f32 LE temp1 + f32 LE temp2 (默认 `110.0`, `90.0`)。 4. **`0x41`** —— 注水 + 研磨度(见下方的字节映射)。 在第 4 帧之后,机器报告 STATE `0x1f`(已部署)并**等待人工在机器上确认**。 协议中的 `0x42`(提交)和 `0x46`(启动)opcode 会强制启动冲煮 —— **本包绝不发送它们。** ### `0x41` 注水帧 payload ``` 01 | LEN(u8 = #body bytes) | | grind(u8) | tail(u8 = 0xa0) ``` 每次注水转换为一个 **8 字节分段**: | Offset | Byte | 含义 | |--------|------------|------------------------------------------------------| | 0 | `ml` | 本分段注水量,ml。 | | 1 | `temp` | 水温,°C。 | | 2 | `pat` | 图案代码(见表)。 | | 3 | `agit` | 搅拌代码(见表)。 | | 4 | `negpause` | `(256 − pause_s) & 0xff` —— 注水后暂停。 | | 5 | `00` | 固定零字节。 | | 6 | `rpm` | 搅拌转速(center 注水为 0)。 | | 7 | `flow10` | 流速 ml/s × 10 (3.0 → `0x1e`)。 | **图案代码** —— `(pattern, agitation) → (pat, agit)`: | Pattern | Agitation | `pat` | `agit` | |----------|-----------|-------|--------| | spiral | true | 0x02 | 0x02 | | spiral | false | 0x02 | 0x00 | | ring | false | 0x01 | 0x00 | | center | false | 0x00 | 0x01 | **大注水量:** 超过 127 ml 的注水会被拆分成 127-ml 的 **4 字节前导分段** (`[ml, temp, pat, agit]`),后跟携带 flow/pause/rpm 字段的 8 字节余数分段。 ### 编程刻度盘预设 (Auto-Mode 插槽) Auto Mode 的三个刻度盘预设 (A/B/C) 是通过**另一条命令 `0x2CF6`** 写入的,并且 —— 与 LOAD 序列不同 —— 是作为 **包含全部三者的批次写入的,没有提交帧**。每个插槽帧: ``` 58 01 02 | f6 2c (=0x2CF6) | LEN(u32 LE) | 01 | SLOT(0/1/2) | FLAGS | <0x41 blob> | CRC16 ``` - **`SLOT`** —— `0`=A,`1`=B,`2`=C。 - **`FLAGS`** —— `0x12` = 开启冲煮 **scale** 存储,`0x02` = 禁用(bit `0x10` 是 scale 标志位)。 - **`<0x41 blob>`** —— 与 LOAD 的 `0x41` 帧相同的 `pours | grind | tail` 主体, 去掉了开头的 `0x01`。 写入序列(通过对两次 App 捕捉的逆向工程 + 在硬件上确认得出): 1. `0xa4` 会话开始;等待机器达到 idle(`0x57` state `0x01`)。 2. 连续写入**三个**插槽帧(A、B、C)。机器对每帧回复 `58 02 07 f6 2c … c2 d204` 通知。 3. 然后机器**以原子方式**存储整个集合 —— 由 `0xf8` 通知和状态进度 `0x43` (saving) → `0x25` (saved) → `0x01` (idle) 发出信号。 **没有提交帧。** 仅写入单个插槽(或添加尾部的“commit”)会使机器卡在 `0x43` 并显示 **RETRY** —— 只有在完成完整的 A/B/C 批次后存储才会完成。 与这里的任何其他写入一样,`0x2CF6` 只是预设写入,**绝不会启动冲煮**。 **Pro 模式与 Auto 模式 (`0x2CF7`)。** 机器仅接受在 **Pro 模式**(状态 `0x01`,idle)下写入插槽。 在 **Auto 模式** —— 即机器上的 A/B/C 配方选择器 —— 下,它会停在状态 `0x41` 并拒绝写入(RETRY)。模式通过命令 **`0x2CF7`** 设置:`58 01 02 | f7 2c | LEN | 01 | <4 bytes> | CRC`,其中 `00 00 00 00` = Pro,`91 32 78 56` = Auto。 因此 `save_slots` 会在批次写入**前**发送 **Pro**,并在之后发送 **Auto**(这样最新的预设就可以直接在刻度盘上选取了)。 这同样只是模式切换 —— 它绝不会直接进行冲煮。 ### 状态通知 (`ffe2`) 通知使用**自身**的帧结构(与上述命令帧不同): ``` 58 02 07 | TYPE(u8) | SUB(u8) | LEN(u32 LE) | 0xc1 | PAYLOAD | CRC16(u16 LE) ``` - **`TYPE`**(偏移量 3)是帧类型: - **命令回显 / ACK** —— `TYPE` 等于刚刚写入的命令字节(`a4/a6/a8/41/…`),因此 ACK 仅仅是“偏移量 3 处的字节与我的命令相匹配的通知”(例如 `5802 07 a6 …` acks `0xa6`)。 - **`0x57`** —— **状态**帧;紧随 `0xc1` 之后的字节是机器*状态*(见下表)。 - **`0x15` / `0x4b`** —— idle **心跳**(被忽略)。 - `0x49` 携带机器信息转储(序列号 + 固件字符串);`0x39` 等携带实时冲煮进度(尽力而为的模式,仅在仅加载时不需要)。 **状态字节**(在 `0x57` 帧内,紧随 `0xc1` 之后): | State | Name | 含义 | |-------|--------------------|--------------------------------------| | 0x01 | idle | Idle / 就绪(在冲煮结束时也会出现)。 | | 0x1d | loading | 正在接收配方。 | | 0x1f | armed | 配方已加载,等待批准。 | | 0x1e | awaiting_confirm | 等待人工确认。 | | 0x3b | brewing | 冲煮进行中。 | | 0x41 | complete | 冲煮完成;也等同于 Auto-mode 选择器。 | | 0x43 | saving_slots | 正在存储 Auto-Mode 插槽批次。 | | 0x25 | slots_saved | Auto-Mode 插槽存储成功 (→ idle)。 | 加载流程会等待状态 **`0x1f` (armed)**,机器在确认(ACK)了 `0x41` 注水帧后会立刻报告此状态 —— 这正是它提示人工批准的时刻。 ## Library API ``` import asyncio from xbloom_ble import Recipe from xbloom_ble.client import XBloomClient, scan async def main(): recipe = Recipe.from_yaml("recipes/example-washed.yaml") devices = await scan() async with XBloomClient(devices[0].address) as client: await client.load_recipe(recipe) # loads only — never starts # → now physically approve the brew on the machine await client.stream_telemetry(lambda ev: print(ev), duration=300) asyncio.run(main()) ``` `xbloom_ble.protocol` 是纯逻辑(无 BLE 依赖),如果您想构建不同的前端,可以从这里开始: ``` from xbloom_ble.protocol import build_load_frames frames = build_load_frames(recipe.to_protocol_dict()) # [a4, a6, a8, 41] ``` 云客户端(`pip install "xbloom-ble[cloud]"`)会推送到 App 账户; `sync_recipe` 是幂等的,并且只管理 `AUTO …` 配方: ``` from xbloom_ble.cloud import XBloomCloud client = XBloomCloud(email="…", password="…") # or XBLOOM_EMAIL/XBLOOM_PASSWORD client.login() client.sync_recipe(recipe) # create-or-update "AUTO " ``` ## 开发 ``` pip install -e ".[dev]" pytest -q ``` 协议测试断言本包生成的帧与逆向工程参考在字节层面**完全一致**, 包括 127-ml 拆分、center 和 ring 图案,以及搅拌闷蒸。如果参考脚本存放在其他位置,请将 `XBLOOM_REFERENCE` 指向它(如果缺少该文件,将跳过这些对比测试)。 ## 免责声明 这是一个**非官方**项目。它**不隶属于 xBloom,也未得到 xBloom 的认可或支持**。 "xBloom" 和 "xBloom Studio" 是其各自所有者的商标。 此处的协议是逆向工程得出的,可能不完整或存在错误;可能会因固件更新而失效。 **使用风险自负 —— 您须对自己对机器进行的任何操作负全责。** 本工具在设计上仅*加载*配方,且**绝不自动启动冲煮**:机器始终会提示您,并且您需要亲自在设备上确认冲煮。 即便如此,也请看管好您的机器。无任何担保(参见 [LICENSE](LICENSE))。 ## License MIT © 2026 Janczykkkko
标签:Python, 云资产清单, 咖啡机控制, 文档结构分析, 无后门, 智能家居, 物联网, 蓝牙低功耗, 逆向工具, 逆向工程