Bluetooth protocol¶
How the app talks to each device family over Bluetooth Low Energy (GATT). This is not official
documentation: it describes what this app does to interoperate with compatible devices, it may be
incomplete, and devices or firmware versions may behave differently. The source of truth is the code in
src/devices/*/protocol.ts and its unit tests. See the legal notice.
Contents¶
Conventions¶
- Multi-byte integers are little-endian.
- Temperatures are in 1/10 °C unless stated otherwise (
1850= 185.0 °C). The app rounds to whole degrees. - UUIDs share a vendor base per family; tables show the full UUID once and the distinguishing part after that.
- "Notify" means the app subscribes to notifications for that characteristic; otherwise it reads on connect or on demand.
Discovery¶
navigator.bluetooth.requestDevice is called with these filters (any one matches):
| Filter | Value |
|---|---|
| name prefix | STORZ&BICKEL, Storz&Bickel, S&B |
| services | 00000001-4c45-4b43-4942-265a524f5453, …0002…, …0003… (Crafty) |
| services | 10100000-5354-4f52-5a26-4249434b454c + 10110000-… (desktop) |
| services | 00000000-5354-4f52-5a26-4249434b454c (Venty / Veazy) |
On iOS Web Bluetooth browsers only the name-prefix filters are passed. optionalServices adds Generic
Access (0x1800).
The device type is derived from the advertised name:
| Name contains | Type |
|---|---|
S&B VOLCANO |
desktop |
S&B VY |
Venty (S&B VY123456: serial number after the space) |
S&B VZ |
Veazy |
| anything else matching the filters | Crafty |
Desktop (Volcano Hybrid)¶
Base xxxxxxxx-5354-4f52-5a26-4249434b454c. Two services:
| Service | UUID |
|---|---|
| State | 10100000-… |
| Control | 10110000-… |
Characteristics¶
| Name | UUID prefix | Service | Access | Format |
|---|---|---|---|---|
| Serial number | 10100008 |
state | read | ASCII, first 8 chars |
| Firmware | 10100003 |
state | read | ASCII, first 8 chars |
| BLE firmware | 10100004 |
state | read | ASCII |
| Register 1 (activity) | 1010000c |
state | read, notify | uint16 bit field |
| Register 2 (display) | 1010000d |
state | read, notify, write | uint16 bit field |
| Register 3 (vibration) | 1010000e |
state | read, write | uint16 bit field |
| History 1 / 2 | 10100015 / 10100016 |
state | read (optional) | raw bytes, used for diagnosis |
| Current temperature | 10110001 |
control | read, notify | uint16, 1/10 °C; values ≥ 6536 °C while starting are ignored |
| Target temperature | 10110003 |
control | read, notify, write | read uint16 / write uint32, 1/10 °C, 40–230 °C |
| Brightness | 10110005 |
control | read, write | uint16, 0–100 |
| Auto-off remaining | 1011000c |
control | read, notify | uint16, seconds |
| Auto-off time | 1011000d |
control | read, write | uint16, seconds |
| Heater on / off | 1011000f / 10110010 |
control | write | one byte 0x00 |
| Pump on / off | 10110013 / 10110014 |
control | write | one byte 0x00 |
| Heating hours / minutes | 10110015 / 10110016 |
control | read, notify | uint16 |
Register bits¶
| Register | Bit | Meaning |
|---|---|---|
| 1 | 0x0020 |
heater on |
| 1 | 0x0200 |
auto-shutdown active |
| 1 | 0x2000 |
pump on |
| 2 | 0x0200 |
Fahrenheit |
| 2 | 0x1000 |
display off while cooling (inverted: set = off) |
| 3 | 0x0400 |
vibration off (inverted: set = off) |
Registers are changed with a uint32 write: the lower 16 bits select the bit, 0x10000 added means
set, otherwise clear. Example: enable Fahrenheit → write 0x00010200 to register 2. Register 3 has
no notifications, so the app reads it back after a write.
Crafty / Crafty+¶
Base xxxxxxxx-4c45-4b43-4942-265a524f5453. Three services:
| Service | UUID | Content |
|---|---|---|
| Control | 00000001-… |
temperatures, battery, LED, auto-off, heater |
| Device info | 00000002-… |
firmware, serial number, BLE firmware |
| Status | 00000003-… |
registers, runtime, security code, factory reset |
Characteristics¶
| Name | UUID prefix | Access | Format |
|---|---|---|---|
| Current temperature | 00000011 |
read, notify | uint16, 1/10 °C |
| Target temperature | 00000021 |
read, write | uint16, 1/10 °C, 40–210 °C. In °F mode the device reports °F, the app converts values > 210 back |
| Boost offset | 00000031 |
read, write | uint16, 1/10 °C, 0–30 |
| Battery | 00000041 |
read, notify | uint16, percent |
| LED brightness | 00000051 |
read, write | uint16, 0–100 |
| Auto-off time | 00000061 |
read, write | uint16, seconds, 30–300; needs the security code first |
| Auto-off remaining | 00000071 |
read, notify | uint16, seconds |
| Heater on / off | 00000081 / 00000091 |
write | uint16 0x0000 |
| Project register | 00000093 |
read, notify | uint16 bit field |
| Usage hours / minutes | 00000023 / 000001e3 |
read | uint16 |
| Status register 2 | 000001c3 |
read, notify, write | uint16 bit field |
| Security code | 000001b3 |
write | uint16 |
| Factory reset | 000001d3 |
write | one byte, after security code |
| System / battery status | 00000083, 00000063, 00000073 |
read | uint16, used for diagnosis |
| Firmware | 00000032 |
read | ASCII, e.g. V02.51 |
| Serial number | 00000052 |
read | ASCII, first 8 chars |
| BLE firmware | 00000072 |
read | 3 bytes: major, minor, patch |
Bits¶
| Register | Bit | Meaning |
|---|---|---|
| project | 1 << 4 |
heater active |
| project | 1 << 5 / 1 << 6 |
boost / superboost mode |
| project | 1 << 15 |
factory reset required |
| status 2 | 1 << 0 |
vibration disabled |
| status 2 | 1 << 1 |
charge LED disabled |
| status 2 | 1 << 2 |
set point reached |
| status 2 | 1 << 3 |
find my device active |
| status 2 | 1 << 12 |
automatic Bluetooth shutdown enabled (cleared = permanent Bluetooth) |
Status register 2 is changed by read-modify-write of the whole uint16, then read back.
Security code¶
Protected values only accept a write right after a code was written to 000001b3:
| Action | Code |
|---|---|
| auto-off time | 815 |
| factory reset | 1000 |
After a factory reset the app waits for the device to settle and re-reads the affected values.
The app sends code and value with writeSequence, so no other queued operation can get in between.
Firmware variants¶
| Firmware | Detection | Effect |
|---|---|---|
| older than 2.51 | version V0x.yy with major ≤ 2 and minor < 51 |
only temperatures, boost, battery, LED, usage hours and the registers exist; no notifications on the project register |
| Crafty+ | major ≥ 3 | find my device available |
Venty / Veazy¶
Base xxxxxxxx-5354-4f52-5a26-4249434b454c. One service 00000000-… with one characteristic
00000001-… (write, notify). Every request is a frame whose byte 0 is the command; the device answers
with a notification that starts with the same command.
Commands¶
| Command | Id | Frame | Response |
|---|---|---|---|
| STATUS | 0x01 |
20 bytes | temperatures, battery, heater mode, settings |
| FIRMWARE | 0x02 |
20 bytes | flags, firmware and bootloader version |
| ANALYSIS | 0x03 |
20 bytes | error code and category |
| EXTENDED_DATA | 0x04 |
20 bytes | heater runtime, battery charging time |
| DEVICE_DATA | 0x05 |
20 bytes | serial number, color index |
| BRIGHTNESS_VIBRATION | 0x06 |
7 bytes | brightness, vibration, boost timeout |
| FIND_MY_DEVICE | 0x0d |
20 bytes, byte 1 = 0x01 |
|
| ADVERTISING_INFO | 0x1d |
20 bytes | find-my-device state |
A read is the command byte followed by zeros. After connecting the app sends FIRMWARE, ADVERTISING_INFO, STATUS, EXTENDED_DATA, DEVICE_DATA and BRIGHTNESS_VIBRATION, then polls STATUS every 500 ms and EXTENDED_DATA every 30th poll.
STATUS frame¶
| Byte | Content |
|---|---|
| 0 | 0x01 |
| 1 | write mask (writes only): 1<<1 target, 1<<2 boost, 1<<3 superboost, 1<<5 heater mode, 1<<7 settings |
| 2–3 | current temperature, uint16, 1/10 °C; in practice always 0x8000 (not measured), so the app ignores it, like the official app |
| 4–5 | target temperature, uint16, 1/10 °C, 40–210 °C |
| 6 | boost offset, °C (1–99) |
| 7 | superboost offset, °C (1–99) |
| 8 | battery, percent |
| 9, 10 | auto-shutdown timer (the two bytes are added, not combined into a uint16) |
| 11 | heater mode: 0 off, 1 normal, 2 boost, 3 superboost |
| 13 | charging when > 0 |
| 14 | settings value: 1<<0 Fahrenheit, 1<<1 set point reached, 1<<2 factory reset, 1<<3 charge current optimization, 1<<4 target changed on device, 1<<5 charge voltage limit, 1<<6 boost visualization (inverted on Veazy) |
| 15 | settings mask (writes only): which bits of byte 14 to apply |
| 16 | settings 2 value: 1<<0 permanent Bluetooth |
| 17 | settings 2 mask (writes only) |
A write sets only the fields selected in byte 1 (and, for settings, the bits selected in bytes 15 / 17).
Example: target 185 °C → 01 02 00 00 3a 07 00 ….
Other responses¶
| Command | Fields |
|---|---|
| FIRMWARE | byte 1 flags (1<<0 application mode, 1<<4 invalid application, 1<<5 invalid bootloader), bytes 2–7 firmware ASCII, bytes 11–16 bootloader ASCII |
| EXTENDED_DATA | bytes 1–3 heater runtime in minutes (uint24), bytes 4–6 battery charging time in minutes (uint24) |
| DEVICE_DATA | serial number = ASCII bytes 15–16 followed by bytes 9–14; byte 18 color index if present |
| BRIGHTNESS_VIBRATION | byte 2 brightness (1–9), byte 5 vibration, byte 6 boost timeout disabled. Writes use byte 1 as mask: 1<<0 brightness, 1<<3 vibration, 1<<4 boost timeout |
| ANALYSIS | byte 1 error code, byte 2 error category (4 = issue detected) |
| ADVERTISING_INFO | byte 1 1<<4: find my device active |
Self-diagnosis¶
Each family has an analyze<Family>() function in its protocol module. It checks error bits in the
registers (desktop, Crafty) or the ANALYSIS response (Venty / Veazy) and returns:
errorReport:null, or a text block with the serial number and the raw values in hex for the manufacturer's support,findings: message keys such asanalysis_finding_coolDown, shown as advice in the UI.
Which error bits mean what is in part undocumented; the app only reports them, it does not interpret them further.
Next: Architecture · Development · Documentation index