xbloom-ble 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xbloom_ble-0.1.0/LICENSE +21 -0
- xbloom_ble-0.1.0/PKG-INFO +464 -0
- xbloom_ble-0.1.0/README.md +436 -0
- xbloom_ble-0.1.0/pyproject.toml +73 -0
- xbloom_ble-0.1.0/setup.cfg +4 -0
- xbloom_ble-0.1.0/tests/test_cloud.py +131 -0
- xbloom_ble-0.1.0/tests/test_protocol.py +138 -0
- xbloom_ble-0.1.0/tests/test_recipe.py +213 -0
- xbloom_ble-0.1.0/tests/test_telemetry.py +116 -0
- xbloom_ble-0.1.0/xbloom_ble/__init__.py +30 -0
- xbloom_ble-0.1.0/xbloom_ble/cli.py +346 -0
- xbloom_ble-0.1.0/xbloom_ble/client.py +255 -0
- xbloom_ble-0.1.0/xbloom_ble/cloud.py +590 -0
- xbloom_ble-0.1.0/xbloom_ble/protocol.py +212 -0
- xbloom_ble-0.1.0/xbloom_ble/recipe.py +241 -0
- xbloom_ble-0.1.0/xbloom_ble/telemetry.py +149 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/PKG-INFO +464 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/SOURCES.txt +20 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/dependency_links.txt +1 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/entry_points.txt +2 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/requires.txt +10 -0
- xbloom_ble-0.1.0/xbloom_ble.egg-info/top_level.txt +1 -0
xbloom_ble-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janczykkkko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xbloom-ble
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Bluetooth LE control for the xBloom Studio coffee machine (loads recipes only β never auto-starts a brew).
|
|
5
|
+
Author: Janczykkkko
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Janczykkkko/xbloom-ble
|
|
8
|
+
Project-URL: Repository, https://github.com/Janczykkkko/xbloom-ble
|
|
9
|
+
Keywords: xbloom,coffee,bluetooth,ble,bleak,pour-over
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Home Automation
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bleak>=0.21
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Provides-Extra: cloud
|
|
22
|
+
Requires-Dist: cryptography>=41; extra == "cloud"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
26
|
+
Requires-Dist: cryptography>=41; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# xbloom-ble
|
|
30
|
+
|
|
31
|
+
> π€ Built & maintained by [Claude Code](https://claude.com/claude-code).
|
|
32
|
+
|
|
33
|
+
[](https://github.com/Janczykkkko/xbloom-ble/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/xbloom-ble/)
|
|
35
|
+
[](https://pypi.org/project/xbloom-ble/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
**Unofficial Bluetooth LE control for the [xBloom Studio](https://xbloom.com) pour-over coffee machine.**
|
|
40
|
+
|
|
41
|
+
There is no official xBloom API. This package speaks the machine's
|
|
42
|
+
reverse-engineered Bluetooth Low Energy protocol so you can script and version
|
|
43
|
+
your recipes β discover the machine, validate a recipe, **load it onto the
|
|
44
|
+
machine**, and watch live brew telemetry.
|
|
45
|
+
|
|
46
|
+
It can also β optionally β **sync recipes to your xBloom phone-app account** over
|
|
47
|
+
the unofficial cloud REST API (`xbloom cloud`, see below), so a recipe you keep
|
|
48
|
+
in version control shows up in the app too.
|
|
49
|
+
|
|
50
|
+
It is a small, dependency-light Python package (`bleak` + `pyyaml`; the cloud
|
|
51
|
+
feature adds an optional `cryptography` dep) with a clean CLI and a fully
|
|
52
|
+
documented protocol so others can build on it.
|
|
53
|
+
|
|
54
|
+
> π€ **Designed with agentic use in mind.** This was written *by* an AI coding agent
|
|
55
|
+
> ([Claude Code](https://claude.com/claude-code)) and tailored to be driven *by* one β
|
|
56
|
+
> scriptable commands, predictable/parseable output, a fully documented protocol, and a
|
|
57
|
+
> safety model where the tool only ever **loads** a recipe and a **human approves the brew
|
|
58
|
+
> on the machine**. (It's just as pleasant to use by hand.)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## β οΈ Safety β this tool only *loads*, it never auto-starts
|
|
63
|
+
|
|
64
|
+
This is the headline design decision and a hard invariant:
|
|
65
|
+
|
|
66
|
+
> **`xbloom-ble` only ever LOADS a recipe onto the machine. It sends the load
|
|
67
|
+
> sequence, the machine then prompts you, and YOU physically approve the brew on
|
|
68
|
+
> the machine itself. The tool will never start a brew for you.**
|
|
69
|
+
|
|
70
|
+
The xBloom BLE protocol *does* have opcodes that force-start a brew (`0x42`
|
|
71
|
+
commit and `0x46` start). **This package never builds or sends them.** There is
|
|
72
|
+
intentionally no code path that emits `0x42`/`0x46` β `build_load_frames()`
|
|
73
|
+
returns only the four LOAD frames, and there is even a belt-and-braces assertion
|
|
74
|
+
that rejects a forbidden opcode if one ever crept in. So the worst this tool can
|
|
75
|
+
do is arm a recipe you then have to confirm by hand, with the cup and beans in
|
|
76
|
+
front of you.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Tested with
|
|
81
|
+
|
|
82
|
+
This was developed and verified against an **xBloom Studio running firmware
|
|
83
|
+
`V12.0D.500`** β that is the **only unit and firmware it has been tested against**.
|
|
84
|
+
The reverse-engineered protocol may differ on other firmware or hardware revisions,
|
|
85
|
+
and could break with future updates.
|
|
86
|
+
|
|
87
|
+
**Reports for other firmware/hardware are very welcome** β if it works (or doesn't)
|
|
88
|
+
on your machine, please open an issue. A BLE capture from a different firmware
|
|
89
|
+
version is especially useful (see [CONTRIBUTING.md](CONTRIBUTING.md); strip any
|
|
90
|
+
personal data first).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install xbloom-ble
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
From source:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/Janczykkkko/xbloom-ble
|
|
104
|
+
cd xbloom-ble
|
|
105
|
+
pip install -e .
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Linux** needs BlueZ running (`bluetoothd`) β it's the standard system
|
|
109
|
+
Bluetooth stack and is what `bleak` talks to. macOS and Windows use their native
|
|
110
|
+
BLE stacks. Bluetooth must be on, and on Linux you may need to run as a user
|
|
111
|
+
with BLE permissions.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
The CLI is `xbloom`.
|
|
118
|
+
|
|
119
|
+
### Discover your machine
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
xbloom scan
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
Found 1 machine(s):
|
|
127
|
+
AA:BB:CC:DD:EE:FF XBLOOM-1234
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The machine is discovered by its vendor **service UUID**
|
|
131
|
+
(`0000e0ff-3c17-d293-8e48-14fe2e4da212`) or a device name starting with
|
|
132
|
+
`XBLOOM` β no hardcoded address.
|
|
133
|
+
|
|
134
|
+
### Validate a recipe (no hardware needed)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
xbloom validate recipes/example-washed.yaml
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
OK: 'Example Washed' β 16 g, grind 62, 3 pours, 240 ml total water
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Load a recipe and watch the brew
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
xbloom brew recipes/example-washed.yaml --address AA:BB:CC:DD:EE:FF
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
or set the address via the environment (so nothing is hardcoded):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
export XBLOOM_ADDRESS=AA:BB:CC:DD:EE:FF
|
|
154
|
+
xbloom brew recipes/example-washed.yaml
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
It validates, connects, **loads** the recipe, then prints:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
β Recipe loaded. Add beans + cup, then APPROVE ON THE MACHINE to start. (This tool will NOT start it.)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
β¦and streams live status (machine state changes) until the brew completes or the
|
|
164
|
+
timeout (`--timeout`, default 300 s) elapses. A telemetry log is written to
|
|
165
|
+
`./telemetry-<timestamp>.json`.
|
|
166
|
+
|
|
167
|
+
Common flags: `--address`, `--timeout`, `-v/--verbose`, `--version`.
|
|
168
|
+
|
|
169
|
+
### Push recipes to your app account (cloud)
|
|
170
|
+
|
|
171
|
+
Separately from BLE machine control, `xbloom cloud` can push recipes to your
|
|
172
|
+
xBloom **app account** via the *unofficial* xBloom cloud REST API, so a recipe
|
|
173
|
+
you define here shows up in the phone app. Needs the optional dependency:
|
|
174
|
+
`pip install "xbloom-ble[cloud]"`.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
export XBLOOM_EMAIL=you@example.com XBLOOM_PASSWORD=β¦ # or `xbloom cloud login`
|
|
178
|
+
xbloom cloud sync my-recipe.yaml # create-or-update a tool-owned recipe (idempotent)
|
|
179
|
+
xbloom cloud list # list account recipes ('*' = tool-owned)
|
|
180
|
+
xbloom cloud delete <tableId> # only AUTO β¦ recipes can be deleted
|
|
181
|
+
xbloom cloud fetch <share-url> # read a publicly shared recipe (no auth)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> π **Safety: the tool only ever manages recipes it created.** Every recipe
|
|
185
|
+
> pushed via `sync` is named **`AUTO <name>`**, and `sync`/`delete` will **only**
|
|
186
|
+
> update or remove `AUTO β¦` recipes. Recipes you made by hand in the app are
|
|
187
|
+
> never modified or deleted. This is enforced in code (`update_recipe` /
|
|
188
|
+
> `delete_recipe` refuse a non-`AUTO` target) and covered by tests.
|
|
189
|
+
|
|
190
|
+
This uses a community-reverse-engineered, unofficial API (it may break, and it
|
|
191
|
+
touches your real account) β see [`cloud.py`](xbloom_ble/cloud.py) for the
|
|
192
|
+
mechanics (RSA-encrypted bodies, endpoints, field schema).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Recipe format
|
|
197
|
+
|
|
198
|
+
> π **Looking for recipes to start from?** Browse the community **[xBloom recipe ledger](https://xbloom.lodywgumce.tv)**
|
|
199
|
+
> β per-bean pour recipes (grind, temps, pour schedule) you can adapt to the format below.
|
|
200
|
+
|
|
201
|
+
Recipes are plain YAML:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
name: Example Washed
|
|
205
|
+
dose_g: 16 # coffee dose in grams
|
|
206
|
+
grind: 62 # grinder setting (1β80)
|
|
207
|
+
ratio: 15 # optional; if given, Ξ£ pour ml must equal dose_g * ratio
|
|
208
|
+
stage_temps: [110.0, 90.0] # optional; machine stage temps, default 110/90
|
|
209
|
+
pours:
|
|
210
|
+
- {ml: 45, temp_c: 93, pattern: spiral, agitation: true, pause_s: 40, rpm: 100, flow_ml_s: 3.0}
|
|
211
|
+
- {ml: 100, temp_c: 91, pattern: spiral, pause_s: 10, rpm: 100, flow_ml_s: 3.2}
|
|
212
|
+
- {ml: 95, temp_c: 90, pattern: spiral, pause_s: 5, rpm: 100, flow_ml_s: 3.2}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Per-pour fields (ranges are **firm β per xBloom Studio specs**):
|
|
216
|
+
|
|
217
|
+
| Field | Meaning |
|
|
218
|
+
|-------------|----------------------------------------------------------------|
|
|
219
|
+
| `ml` | Water volume for this pour (β₯1 ml). A pour over 127 ml is auto-split by the protocol β not an error. |
|
|
220
|
+
| `temp_c` | Water temperature (40β95 Β°C, 1 Β°C steps). |
|
|
221
|
+
| `pattern` | `spiral`, `ring`, or `center`. |
|
|
222
|
+
| `agitation` | `true` only with `spiral` (an agitated bloom). Default `false`. |
|
|
223
|
+
| `pause_s` | Pause after this pour, seconds (0β255; the on-machine countdown caps near 99 s). |
|
|
224
|
+
| `rpm` | Agitation rotation speed (60β120, 10-RPM steps; `0` for `center`). |
|
|
225
|
+
| `flow_ml_s` | Flow rate in ml/s (3.0β3.5, 0.1 steps). |
|
|
226
|
+
|
|
227
|
+
The app also exposes two **special, non-numeric temperature settings β `RT`
|
|
228
|
+
(room temp) and `BP` (boiling point)** β which are not expressible as a numeric
|
|
229
|
+
`temp_c`; the numeric range is 40β95 Β°C.
|
|
230
|
+
|
|
231
|
+
See **[Recipe limits & valid ranges](#recipe-limits--valid-ranges)** below for the
|
|
232
|
+
full table and the firm bounds enforced.
|
|
233
|
+
|
|
234
|
+
Validation rejects: fewer than two pours (you need at least a bloom and a first
|
|
235
|
+
pour), an unknown `pattern`/`agitation` combo, out-of-range values, and β if a
|
|
236
|
+
`ratio` is given β a pour total that doesn't equal `dose_g * ratio`.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Recipe limits & valid ranges
|
|
241
|
+
|
|
242
|
+
These are the bounds `xbloom validate` enforces. Most are **firm (per the xBloom
|
|
243
|
+
Studio published specifications)** β a real machine/app limit; a couple of
|
|
244
|
+
ceilings (`ml`, `pause_s`) remain practical sanity guards. **If your machine
|
|
245
|
+
behaves differently, please open an issue with a capture β the ranges should
|
|
246
|
+
track real hardware.**
|
|
247
|
+
|
|
248
|
+
| Value | Accepted range | Firmness |
|
|
249
|
+
|---------------|----------------|----------|
|
|
250
|
+
| `dose_g` | 1β18 g | **Firm (per xBloom Studio specs).** 18 g is the maximum the xBloom app lets you set. |
|
|
251
|
+
| `grind` | 1β80 | **Firm (per xBloom Studio specs).** The grinder has 80 micro-steps (~18.75 Β΅m each); a *lower* number is *finer*. |
|
|
252
|
+
| `temp_c` (pour) | 40β95 Β°C | **Firm (per xBloom Studio specs).** Settable in 1 Β°C steps. The app also offers special non-numeric `RT` (room temp) and `BP` (boiling point) settings, outside this numeric range. |
|
|
253
|
+
| `stage_temps` | 40β130 Β°C each | Machine **preheat/stage set-points** (default 110/90 Β°C) β NOT the pour temperature, so they legitimately exceed the 95 Β°C pour cap. Wider allowance around the default. |
|
|
254
|
+
| `rpm` | 0, or 60β120 | **Firm (per xBloom Studio specs).** 60β120 in 10-RPM steps; `0` (no agitation) is allowed only for `center` pours. |
|
|
255
|
+
| `flow_ml_s` | 3.0β3.5 ml/s | **Firm (per xBloom Studio specs).** Settable in 0.1 steps. |
|
|
256
|
+
| `pause_s` | 0β255 | The wire byte is `256 β seconds` (so 0β255 fits), but the **on-machine countdown caps near 99 s** β treat 0β99 as the practical range. |
|
|
257
|
+
| `ml` (pour) | 1β4000 ml | Lower bound (β₯1) is firm; a pour **over 127 ml is auto-split** by the protocol (not an error). The 4000 ceiling is just a sanity guard. |
|
|
258
|
+
| `pattern` | `spiral`, `ring`, `center` | **Firm.** These are the decoded pattern codes; `agitation: true` is only valid with `spiral`. |
|
|
259
|
+
|
|
260
|
+
> **Source:** xBloom Studio published specifications.
|
|
261
|
+
|
|
262
|
+
The pour count must be **β₯2** (at least a bloom and a first pour), and if you give
|
|
263
|
+
an optional `ratio`, Ξ£(pour ml) must equal `dose_g * ratio`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Reverse-engineered protocol
|
|
268
|
+
|
|
269
|
+
The wire protocol was reverse-engineered from an Android Bluetooth HCI capture
|
|
270
|
+
and verified by round-tripping against the original recorded frames. This is
|
|
271
|
+
documented in full so you can build on it.
|
|
272
|
+
|
|
273
|
+
> π **How it was done:** the full capture β parse β differential-decode methodology
|
|
274
|
+
> (reproducible, no special hardware) is written up in
|
|
275
|
+
> **[docs/REVERSE-ENGINEERING.md](docs/REVERSE-ENGINEERING.md)**.
|
|
276
|
+
|
|
277
|
+
### Frame format
|
|
278
|
+
|
|
279
|
+
**Commands** written to `ffe1` (host β machine) are:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
58 01 01 | CMD(u8) | SEQ(u8) | LEN(u16 LE) | 00 00 | PAYLOAD | CRC16(u16 LE)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- `58 01 01` β constant header.
|
|
286
|
+
- `CMD` β command opcode.
|
|
287
|
+
- `SEQ` β sequence byte; the load sequence uses `0x1f` (31).
|
|
288
|
+
- `LEN` β total frame length (header through CRC), little-endian, at offset 5.
|
|
289
|
+
- `00 00` β two constant zero bytes.
|
|
290
|
+
- `PAYLOAD` β command-specific.
|
|
291
|
+
- `CRC16` β **CRC-16/KERMIT** over the whole frame minus the trailing two bytes,
|
|
292
|
+
stored little-endian.
|
|
293
|
+
|
|
294
|
+
**Notifications** on `ffe2` (machine β host) use a **different** shape β see
|
|
295
|
+
[Status notifications](#status-notifications-ffe2) below.
|
|
296
|
+
|
|
297
|
+
**CRC-16/KERMIT:** polynomial `0x1021`, init `0`, reflected input and output, no
|
|
298
|
+
final XOR (check value `0x2189` for `b"123456789"`).
|
|
299
|
+
|
|
300
|
+
### GATT
|
|
301
|
+
|
|
302
|
+
Vendor service `0000e0ff-3c17-d293-8e48-14fe2e4da212`:
|
|
303
|
+
|
|
304
|
+
| Characteristic | Short | Role |
|
|
305
|
+
|----------------|-------|---------------------|
|
|
306
|
+
| command | `ffe1`| write |
|
|
307
|
+
| status | `ffe2`| notify (telemetry) |
|
|
308
|
+
| aux | `ffe3`| auxiliary |
|
|
309
|
+
|
|
310
|
+
> β οΈ **`ffe1` accepts only a *Write Command* (write-without-response, ATT `0x52`).**
|
|
311
|
+
> A *Write Request* (write-with-response, `0x12`) is rejected by the firmware with
|
|
312
|
+
> GATT "Unlikely Error" β verified against the vendor app, which never uses a Write
|
|
313
|
+
> Request on `ffe1`. Command acknowledgements come back as notifications on `ffe2`.
|
|
314
|
+
|
|
315
|
+
### The LOAD sequence
|
|
316
|
+
|
|
317
|
+
Sent frame-by-frame to `ffe1`, waiting for each ACK on `ffe2` (the machine
|
|
318
|
+
echoes the command, e.g. `580207a6β¦`):
|
|
319
|
+
|
|
320
|
+
1. **`0xa4`** β session start. Constant payload `01 b9 00 00 00 01 00 00 00`.
|
|
321
|
+
2. **`0xa6`** β dose. Dose in grams as a `u8` at payload offset 9.
|
|
322
|
+
3. **`0xa8`** β stage temps. `01` + f32 LE temp1 + f32 LE temp2 (default
|
|
323
|
+
`110.0`, `90.0`).
|
|
324
|
+
4. **`0x41`** β pours + grind (see byte map below).
|
|
325
|
+
|
|
326
|
+
After frame 4 the machine reports STATE `0x1f` (armed) and **waits for the human
|
|
327
|
+
to approve on the machine**. The protocol's `0x42` (commit) and `0x46` (start)
|
|
328
|
+
opcodes would force-start the brew β **this package never sends them.**
|
|
329
|
+
|
|
330
|
+
### The `0x41` pours frame payload
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
01 | LEN(u8 = #body bytes) | <pour segmentsβ¦> | grind(u8) | tail(u8 = 0xa0)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each pour becomes an **8-byte segment**:
|
|
337
|
+
|
|
338
|
+
| Offset | Byte | Meaning |
|
|
339
|
+
|--------|------------|------------------------------------------------------|
|
|
340
|
+
| 0 | `ml` | Pour volume for this segment, ml. |
|
|
341
|
+
| 1 | `temp` | Water temperature, Β°C. |
|
|
342
|
+
| 2 | `pat` | Pattern code (see table). |
|
|
343
|
+
| 3 | `agit` | Agitation code (see table). |
|
|
344
|
+
| 4 | `negpause` | `(256 β pause_s) & 0xff` β post-pour pause. |
|
|
345
|
+
| 5 | `00` | Constant zero. |
|
|
346
|
+
| 6 | `rpm` | Agitation rotation speed (0 for center pours). |
|
|
347
|
+
| 7 | `flow10` | Flow rate in ml/s Γ 10 (3.0 β `0x1e`). |
|
|
348
|
+
|
|
349
|
+
**Pattern codes** β `(pattern, agitation) β (pat, agit)`:
|
|
350
|
+
|
|
351
|
+
| Pattern | Agitation | `pat` | `agit` |
|
|
352
|
+
|----------|-----------|-------|--------|
|
|
353
|
+
| spiral | true | 0x02 | 0x02 |
|
|
354
|
+
| spiral | false | 0x02 | 0x00 |
|
|
355
|
+
| ring | false | 0x01 | 0x00 |
|
|
356
|
+
| center | false | 0x00 | 0x01 |
|
|
357
|
+
|
|
358
|
+
**Large pours:** a pour above 127 ml is split into 127-ml **4-byte lead
|
|
359
|
+
segments** (`[ml, temp, pat, agit]`) followed by an 8-byte remainder segment
|
|
360
|
+
carrying the flow/pause/rpm fields.
|
|
361
|
+
|
|
362
|
+
### Status notifications (`ffe2`)
|
|
363
|
+
|
|
364
|
+
Notifications use their **own** frame shape (distinct from the command frames
|
|
365
|
+
above):
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
58 02 07 | TYPE(u8) | SUB(u8) | LEN(u32 LE) | 0xc1 | PAYLOAD | CRC16(u16 LE)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- **`TYPE`** (offset 3) is the frame kind:
|
|
372
|
+
- a **command echo / ACK** β `TYPE` equals the command byte just written
|
|
373
|
+
(`a4/a6/a8/41/β¦`), so an ACK is simply "the notification whose offset-3 byte
|
|
374
|
+
matches my command" (e.g. `5802 07 a6 β¦` acks `0xa6`).
|
|
375
|
+
- **`0x57`** β a **status** frame; the byte right after `0xc1` is the machine
|
|
376
|
+
*state* (table below).
|
|
377
|
+
- **`0x15` / `0x4b`** β idle **heartbeats** (ignored).
|
|
378
|
+
- `0x49` carries a machine-info dump (serial + firmware string); `0x39` etc.
|
|
379
|
+
carry live brew progress (best-effort, not needed for load-only).
|
|
380
|
+
|
|
381
|
+
**State byte** (inside a `0x57` frame, right after `0xc1`):
|
|
382
|
+
|
|
383
|
+
| State | Name | Meaning |
|
|
384
|
+
|-------|--------------------|--------------------------------------|
|
|
385
|
+
| 0x01 | idle | Idle / ready (also at brew end). |
|
|
386
|
+
| 0x1d | loading | Recipe being received. |
|
|
387
|
+
| 0x1f | armed | Recipe loaded, awaiting approval. |
|
|
388
|
+
| 0x1e | awaiting_confirm | Waiting for the human to confirm. |
|
|
389
|
+
| 0x3b | brewing | Brew in progress. |
|
|
390
|
+
| 0x41 | complete | Brew complete. |
|
|
391
|
+
|
|
392
|
+
The load path waits for state **`0x1f` (armed)**, which the machine reports right
|
|
393
|
+
after it ACKs the `0x41` pours frame β that's when it prompts the human to approve.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Library API
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
import asyncio
|
|
401
|
+
from xbloom_ble import Recipe
|
|
402
|
+
from xbloom_ble.client import XBloomClient, scan
|
|
403
|
+
|
|
404
|
+
async def main():
|
|
405
|
+
recipe = Recipe.from_yaml("recipes/example-washed.yaml")
|
|
406
|
+
devices = await scan()
|
|
407
|
+
async with XBloomClient(devices[0].address) as client:
|
|
408
|
+
await client.load_recipe(recipe) # loads only β never starts
|
|
409
|
+
# β now physically approve the brew on the machine
|
|
410
|
+
await client.stream_telemetry(lambda ev: print(ev), duration=300)
|
|
411
|
+
|
|
412
|
+
asyncio.run(main())
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`xbloom_ble.protocol` is pure (no BLE) and is the place to start if you want to
|
|
416
|
+
build a different front-end:
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from xbloom_ble.protocol import build_load_frames
|
|
420
|
+
frames = build_load_frames(recipe.to_protocol_dict()) # [a4, a6, a8, 41]
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The cloud client (`pip install "xbloom-ble[cloud]"`) pushes to the app account;
|
|
424
|
+
`sync_recipe` is idempotent and only ever manages `AUTO β¦` recipes:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from xbloom_ble.cloud import XBloomCloud
|
|
428
|
+
client = XBloomCloud(email="β¦", password="β¦") # or XBLOOM_EMAIL/XBLOOM_PASSWORD
|
|
429
|
+
client.login()
|
|
430
|
+
client.sync_recipe(recipe) # create-or-update "AUTO <name>"
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Development
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
pip install -e ".[dev]"
|
|
439
|
+
pytest -q
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The protocol tests assert this package's frames are **byte-for-byte** identical
|
|
443
|
+
to the reverse-engineering reference, including the 127-ml split, the center and
|
|
444
|
+
ring patterns, and an agitated bloom. Point `XBLOOM_REFERENCE` at the reference
|
|
445
|
+
script if it lives elsewhere (those comparison tests skip if it's absent).
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Disclaimer
|
|
450
|
+
|
|
451
|
+
This is an **unofficial** project. It is **not affiliated with, endorsed by, or
|
|
452
|
+
supported by xBloom** in any way. "xBloom" and "xBloom Studio" are trademarks of
|
|
453
|
+
their respective owner.
|
|
454
|
+
|
|
455
|
+
The protocol here was reverse-engineered and may be incomplete or wrong; it may
|
|
456
|
+
break with firmware updates. **Use at your own risk β you assume full
|
|
457
|
+
responsibility** for anything you do with your machine. By design this tool only
|
|
458
|
+
*loads* recipes and **never auto-starts a brew**: the machine always prompts you
|
|
459
|
+
and you approve the brew physically on the device. Even so, supervise your
|
|
460
|
+
machine. No warranty (see [LICENSE](LICENSE)).
|
|
461
|
+
|
|
462
|
+
## License
|
|
463
|
+
|
|
464
|
+
MIT Β© 2026 Janczykkkko
|