Skip to main content

storz_rs/
lib.rs

1//! # storz-rs
2//!
3//! Rust library for controlling Storz & Bickel vaporizers via Bluetooth Low Energy.
4//!
5//! Supports **Volcano Hybrid**, **Venty**, **Veazy**, and **Crafty+**.
6//!
7//! ## Quick Start
8//!
9//! ```no_run
10//! use std::time::Duration;
11//! use storz_rs::{discover_vaporizers, get_adapter, connect, VaporizerControl};
12//!
13//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
14//! // 1. Get a Bluetooth adapter
15//! let adapter = get_adapter().await?;
16//!
17//! // 2. Scan for vaporizers
18//! let peripherals = discover_vaporizers(&adapter, Duration::from_secs(10)).await?;
19//! let peripheral = peripherals.into_iter().next().expect("No devices found");
20//!
21//! // 3. Connect and get a controller
22//! let device = connect(peripheral).await?;
23//!
24//! // 4. Read temperature
25//! let temp = device.get_current_temperature().await?;
26//! println!("Current temperature: {temp}°C");
27//!
28//! // 5. Set target temperature
29//! device.set_target_temperature(185.0).await?;
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Supported Features
35//!
36//! | Feature | Volcano | Venty/Veazy | Crafty |
37//! |---|---|---|---|
38//! | Temperature control | ✓ | ✓ | ✓ |
39//! | Heater on/off | ✓ | ✓ | ✓ |
40//! | Pump on/off | ✓ | — | — |
41//! | Brightness | ✓ | ✓ | ✓ |
42//! | Vibration | ✓ | ✓ | — |
43//! | Boost temperature | — | ✓ | ✓ |
44//! | Auto-shutdown timer | ✓ | ✓ | ✓ |
45//! | Factory reset | — | ✓ | ✓ |
46//! | Device info (serial, firmware) | ✓ | ✓ | ✓ |
47//! | Workflow automation | ✓ | — | — |
48//!
49
50pub mod device;
51pub mod discovery;
52pub mod error;
53pub mod protocol;
54pub mod utils;
55pub mod uuids;
56pub mod workflow;
57
58pub use device::{DeviceInfo, DeviceModel, DeviceState, HeaterMode};
59pub use discovery::{discover_vaporizers, get_adapter, select_peripheral};
60pub use error::StorzError;
61pub use protocol::{Crafty, VaporizerControl, Venty, VolcanoHybrid};
62pub use workflow::{Workflow, WorkflowRunner, WorkflowState, WorkflowStep};
63
64use btleplug::api::Peripheral as _;
65use btleplug::platform::Peripheral;
66use std::time::Duration;
67use tracing::{debug, info};
68
69/// Auto-detect the device model from its advertised BLE services.
70///
71/// Call this after `discover_services()` has completed.
72pub async fn detect_model(peripheral: &Peripheral) -> Option<DeviceModel> {
73    // Try name-based detection first
74    if let Ok(Some(props)) = peripheral.properties().await {
75        if let Some(name) = props.local_name.as_deref() {
76            if name.contains("VOLCANO") {
77                return Some(DeviceModel::VolcanoHybrid);
78            }
79            if name.contains("VY") || name.to_lowercase().contains("venty") {
80                return Some(DeviceModel::Venty);
81            }
82            if name.contains("VZ") || name.to_lowercase().contains("veazy") {
83                return Some(DeviceModel::Veazy);
84            }
85            if name.to_lowercase().contains("crafty") {
86                return Some(DeviceModel::Crafty);
87            }
88        }
89    }
90
91    // Fall back to service UUID inspection
92    let services = peripheral.services();
93    let service_uuids: Vec<_> = services.iter().map(|s| s.uuid).collect();
94
95    if service_uuids.contains(&uuids::VOLCANO_SERVICE_STATE)
96        || service_uuids.contains(&uuids::VOLCANO_SERVICE_CONTROL)
97    {
98        return Some(DeviceModel::VolcanoHybrid);
99    }
100    if service_uuids.contains(&uuids::VENTY_SERVICE_PRIMARY) {
101        return Some(DeviceModel::Venty);
102    }
103    if service_uuids.contains(&uuids::CRAFTY_SERVICE_1)
104        || service_uuids.contains(&uuids::CRAFTY_SERVICE_2)
105        || service_uuids.contains(&uuids::CRAFTY_SERVICE_3)
106    {
107        return Some(DeviceModel::Crafty);
108    }
109
110    None
111}
112
113/// Connect to a discovered peripheral and return a trait-object controller.
114///
115/// This function:
116/// 1. Establishes a BLE connection
117/// 2. Discovers GATT services
118/// 3. Auto-detects the device model
119/// 4. Returns the appropriate protocol implementation
120/// 5. For Venty/Veazy: runs the init sequence automatically
121pub async fn connect(peripheral: Peripheral) -> Result<Box<dyn VaporizerControl>, StorzError> {
122    info!("Connecting to peripheral…");
123    peripheral.connect().await?;
124    peripheral.discover_services().await?;
125    debug!("Services discovered");
126
127    let model = detect_model(&peripheral).await.unwrap_or_else(|| {
128        debug!("Could not auto-detect model, defaulting to Venty");
129        DeviceModel::Venty
130    });
131    info!("Detected device model: {model}");
132
133    let controller: Box<dyn VaporizerControl> = match model {
134        DeviceModel::VolcanoHybrid => Box::new(VolcanoHybrid::new(peripheral).await?),
135        DeviceModel::Venty | DeviceModel::Veazy => Box::new(Venty::new(peripheral, model).await?),
136        DeviceModel::Crafty => Box::new(Crafty::new(peripheral).await?),
137    };
138
139    Ok(controller)
140}
141
142/// Connect to a peripheral with a timeout.
143///
144/// Same as [`connect`] but fails with [`StorzError::Timeout`] if the connection
145/// or initialization takes longer than the specified duration.
146///
147/// # Example
148///
149/// ```no_run
150/// use std::time::Duration;
151/// use storz_rs::{discover_vaporizers, get_adapter, connect_with_timeout};
152///
153/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
154/// let adapter = get_adapter().await?;
155/// let peripherals = discover_vaporizers(&adapter, Duration::from_secs(10)).await?;
156/// let peripheral = peripherals.into_iter().next().unwrap();
157///
158/// let device = connect_with_timeout(peripheral, Duration::from_secs(15)).await?;
159/// # Ok(())
160/// # }
161/// ```
162pub async fn connect_with_timeout(
163    peripheral: Peripheral,
164    timeout: Duration,
165) -> Result<Box<dyn VaporizerControl>, StorzError> {
166    tokio::time::timeout(timeout, connect(peripheral))
167        .await
168        .map_err(|_| StorzError::Timeout)?
169}