ember_mug/
mug.rs

1//! Hosts [`EmberMug`] and related functions
2use std::io::Cursor;
3
4use binrw::{BinRead, BinWrite};
5use btleplug::api::{Characteristic, Peripheral as _};
6
7use crate::*;
8
9mod battery;
10mod current_temp;
11mod dsk;
12mod last_location;
13mod liquid_level;
14mod liquid_state;
15mod mug_color;
16mod mug_meta;
17mod name;
18mod ota;
19mod push_events;
20mod target_temp;
21mod temperature_unit;
22mod time_date_zone;
23
24pub use battery::Battery;
25pub use last_location::LastLocation;
26pub use liquid_level::LiquidLevel;
27pub use liquid_state::LiquidState;
28pub use mug_color::Color;
29pub use mug_meta::MugMeta;
30pub use ota::Ota;
31pub use push_events::PushEvent;
32pub use temperature_unit::TemperatureUnit;
33pub use time_date_zone::TimeDateZone;
34
35pub(crate) type Peripheral = <btleplug::platform::Adapter as btleplug::api::Central>::Peripheral;
36
37/// An Ember Mug device
38///
39/// Create an instance with [`EmberMug::find_and_connect`] or [`EmberMug::connect_mug`]
40#[derive(Clone)]
41pub struct EmberMug {
42    /// The underlying [`Peripheral`] representing this device
43    peripheral: std::sync::Arc<EmberMugInner>,
44    /// The set of [`Characteristic`]s for this device
45    characteristics: std::collections::BTreeSet<Characteristic>,
46    /// the adapter the mug is connected to
47    adapter: btleplug::platform::Adapter,
48}
49
50#[derive(Clone)]
51struct EmberMugInner(Peripheral);
52
53impl std::ops::Deref for EmberMugInner {
54    type Target = Peripheral;
55    fn deref(&self) -> &Self::Target {
56        &self.0
57    }
58}
59
60impl Drop for EmberMugInner {
61    fn drop(&mut self) {
62        let peripheral = self.0.clone();
63        futures::executor::block_on(async move {
64            let _ = peripheral.disconnect().await;
65        });
66    }
67}
68
69impl EmberMug {
70    /// Find and connect to the first available Ember Mug
71    pub async fn find_and_connect() -> Result<Self, ConnectError> {
72        use futures::TryStreamExt;
73        // FIXME: pin on stack with `Pin::new_unchecked` or `pin-utils`
74        let mut stream = Box::pin(crate::btle::get_mugs().await?);
75        let Some((adapter, mug)) = stream.try_next().await? else {
76            return Err(ConnectError::NoDevice);
77        };
78        Self::connect_mug(adapter, mug).await
79    }
80
81    /// Connect to specific Ember Mug
82    pub async fn connect_mug(
83        adapter: btleplug::platform::Adapter,
84        peripheral: Peripheral,
85    ) -> Result<Self, ConnectError> {
86        tracing::debug!(peripheral.address = ?peripheral.address(), peripheral.id = ?peripheral.id(), "connecting to mug");
87        peripheral.connect().await?;
88        peripheral.discover_services().await?;
89        Ok(Self {
90            characteristics: peripheral.characteristics(),
91            peripheral: std::sync::Arc::new(EmberMugInner(peripheral)),
92            adapter,
93        })
94    }
95
96    /// Returns true if the device is connected, the device might be considered disconnected if it doesn't respond in 1 second
97    pub async fn is_connected(&self) -> Result<bool, btleplug::Error> {
98        match tokio::time::timeout(
99            std::time::Duration::from_secs(1),
100            self.peripheral.is_connected(),
101        )
102        .await
103        {
104            Ok(r) => r,
105            Err(_e) => Ok(false),
106        }
107    }
108
109    /// Returns when the device is disconnected.
110    pub async fn disconnected(&self) -> Result<(), btleplug::Error> {
111        use btleplug::api::Central as _;
112        use futures::StreamExt;
113        let peripheral_id = std::sync::Arc::new(self.peripheral.id());
114        let mut stream = Box::pin(self.adapter.events().await?.filter_map(move |e| {
115            let peripheral_id = peripheral_id.clone();
116            async move {
117                match e {
118                    btleplug::api::CentralEvent::DeviceDisconnected(id)
119                        if &id == peripheral_id.as_ref() =>
120                    {
121                        Some(())
122                    }
123                    _ => None,
124                }
125            }
126        }));
127        stream.next().await;
128        Ok(())
129    }
130}
131
132impl EmberMug {
133    /// Get characteristic on [`EMBER_MUG_SERVICE_UUID`] with given UUID
134    pub fn get_characteristic(&self, uuid: &uuid::Uuid) -> Option<&Characteristic> {
135        self.get_characteristic_on_service(uuid, &crate::EMBER_MUG_SERVICE_UUID)
136    }
137
138    /// Get all characteristics
139    pub fn get_characteristics(&self) -> impl Iterator<Item = &Characteristic> {
140        self.characteristics.iter()
141    }
142
143    /// Get characteristic on given service UUID with given UUID
144    pub fn get_characteristic_on_service(
145        &self,
146        uuid: &uuid::Uuid,
147        service_uuid: &uuid::Uuid,
148    ) -> Option<&Characteristic> {
149        self.characteristics
150            .iter()
151            .find(|&c| &c.uuid == uuid && &c.service_uuid == service_uuid)
152    }
153}
154
155impl EmberMug {
156    /// Read data from given characteristic with `uuid`
157    pub async fn read_deserialize<T: BinRead + binrw::meta::ReadEndian>(
158        &self,
159        uuid: &KnownCharacteristic,
160    ) -> Result<T, ReadError>
161    where
162        for<'a> T::Args<'a>: Default,
163    {
164        T::read(&mut Cursor::new(self.read(uuid).await?)).map_err(Into::into)
165    }
166
167    /// Deserialize data on given characteristic with `uuid`
168    pub async fn read(&self, uuid: &KnownCharacteristic) -> Result<Vec<u8>, ReadError> {
169        let characteristic = self
170            .get_characteristic(&uuid.get())
171            .ok_or_else(|| ReadError::NoSuchCharacteristic(*uuid))?;
172        self.peripheral
173            .read(characteristic)
174            .await
175            .map_err(|e| ReadError::ReadOperation(e, characteristic.uuid, Some(*uuid)))
176    }
177
178    /// Write data to given characteristic on `uuid`
179    pub async fn write<D>(
180        &self,
181        write: btleplug::api::WriteType,
182        uuid: &KnownCharacteristic,
183        data: &D,
184    ) -> Result<(), WriteError>
185    where
186        D: BinWrite + binrw::meta::WriteEndian + Send + Sync,
187        for<'a> <D as BinWrite>::Args<'a>: Default,
188    {
189        let mut buf = Cursor::new(vec![]);
190        data.write(&mut buf)?;
191        let characteristic = self
192            .get_characteristic(&uuid.get())
193            .ok_or_else(|| WriteError::NoSuchCharacteristic(*uuid))?;
194        self.peripheral
195            .write(characteristic, buf.get_ref(), write)
196            .await
197            .map_err(|e| WriteError::WriteOperation(e, characteristic.uuid, Some(*uuid)))
198    }
199
200    /// Send command to given characteristic on `uuid`
201    pub async fn command<D>(&self, uuid: &KnownCharacteristic, data: &D) -> Result<(), WriteError>
202    where
203        D: BinWrite + binrw::meta::WriteEndian + Send + Sync,
204        for<'a> <D as BinWrite>::Args<'a>: Default,
205    {
206        self.write(btleplug::api::WriteType::WithoutResponse, uuid, data)
207            .await
208    }
209
210    /// Send request to given characteristic on `uuid`
211    pub async fn request<D>(&self, uuid: &KnownCharacteristic, data: &D) -> Result<(), WriteError>
212    where
213        D: BinWrite + binrw::meta::WriteEndian + Send + Sync,
214        for<'a> <D as BinWrite>::Args<'a>: Default,
215    {
216        self.write(btleplug::api::WriteType::WithResponse, uuid, data)
217            .await
218    }
219}
220
221#[derive(BinRead, BinWrite, Debug)]
222#[cfg_attr(
223    feature = "serde",
224    derive(serde::Deserialize, serde::Serialize),
225    serde(transparent)
226)]
227#[br(little)]
228#[bw(little)]
229/// Temperature in a certain unit
230pub struct Temperature {
231    /// The temperature in integer value, use [`Temperature::to_degree`] for a value in degrees
232    pub temperature: u16,
233}
234
235impl Temperature {
236    /// Convert value to degree
237    pub fn to_degree(&self) -> f32 {
238        f32::from(self.temperature) * 0.01
239    }
240
241    /// Convert given degree to a temperature
242    pub fn from_degree(deg: f32) -> Self {
243        Self {
244            temperature: (deg * 100.0) as u16,
245        }
246    }
247}
248
249impl std::fmt::Display for Temperature {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        write!(f, "{:.1}", self.to_degree())
252    }
253}