bluer_miflora/
lib.rs

1use std::borrow::Cow;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use bluer::gatt::remote::{Characteristic, CharacteristicWriteRequest};
5use bluer::gatt::WriteOp;
6use bluer::{Adapter, Address, Device};
7
8// These are the services/characteristics available on a miflora
9// service=58 characteristic=64
10// service=58 characteristic=59
11// service=58 characteristic=61
12// service=49 characteristic=55
13// service=49 characteristic=52
14// service=49 characteristic=50
15// service=12 characteristic=13
16// service=35 characteristic=38
17// service=35 characteristic=42
18// service=35 characteristic=40
19// service=35 characteristic=36
20// service=35 characteristic=44
21// service=35 characteristic=46
22// service=16 characteristic=28
23// service=16 characteristic=20
24// service=16 characteristic=26
25// service=16 characteristic=17
26// service=16 characteristic=32
27// service=16 characteristic=24
28// service=16 characteristic=22
29// service=16 characteristic=30
30
31/// Device UUID prefix of miflora service
32const DEVICE_UUID_PREFIX: u32 = 0xfe95;
33const SERVICE_DATA_ID: u16 = 49;
34const CHARACTERISTIC_MODE_ID: u16 = 50;
35const CHARACTERISTIC_DATA_ID: u16 = 52;
36const CHARACTERISTIC_FIRMWARE_ID: u16 = 0x37;
37
38const SERVICE_HISTORY_ID: u16 = 58;
39const CHARACTERISTIC_HISTORY_CTRL_ID: u16 = 61; // 0x3d; // 0x3e
40const CHARACTERISTIC_HISTORY_READ_ID: u16 = 59; // 0x3b; // 0x3c
41const CHARACTERISTIC_HISTORY_TIME_ID: u16 = 64;
42
43// const CMD_BLINK_LED: [u8; 2] = [0xfd, 0xff];
44const CMD_HISTORY_READ_INIT: [u8; 3] = [0xa0, 0x00, 0x00];
45const CMD_HISTORY_READ_SUCCESS: [u8; 3] = [0xa2, 0x00, 0x00];
46// const CMD_HISTORY_READ_FAILED: [u8; 3] = [0xa3, 0x00, 0x00];
47const CMD_REALTIME_DISABLE: [u8; 2] = [0xc0, 0x1f];
48const CMD_REALTIME_ENABLE: [u8; 2] = [0xa0, 0x1f];
49
50const WRITE_OPTS: CharacteristicWriteRequest = CharacteristicWriteRequest {
51    offset: 0,
52    op_type: WriteOp::Request,
53    prepare_authorize: false,
54    _non_exhaustive: (),
55};
56
57fn now() -> f64 {
58    SystemTime::now()
59        .duration_since(UNIX_EPOCH)
60        .expect("went back in time")
61        .as_secs_f64()
62}
63
64#[derive(thiserror::Error, Debug)]
65pub enum Error {
66    #[error("unable to find device with address {address}")]
67    DeviceNotFound {
68        address: Address,
69        #[source]
70        cause: bluer::Error,
71    },
72    #[error("unable to find service {service_id}")]
73    ServiceNotFound {
74        service_id: u16,
75        #[source]
76        cause: bluer::Error,
77    },
78    #[error("unable to find characteristic {characteristic_id} for service {service_id}")]
79    CharacteristicNotFound {
80        characteristic_id: u16,
81        service_id: u16,
82        #[source]
83        cause: bluer::Error,
84    },
85    #[error("unable to read from service {service_id} and characteristic {characteristic_id}")]
86    UnableToRead {
87        characteristic_id: u16,
88        service_id: u16,
89        #[source]
90        cause: bluer::Error,
91    },
92    #[error("unable to write to service {service_id} and characteristic {characteristic_id}")]
93    UnableToWrite {
94        characteristic_id: u16,
95        service_id: u16,
96        #[source]
97        cause: bluer::Error,
98    },
99    #[error("the payload was not correctly written")]
100    InvalidWrittenValue {
101        characteristic_id: u16,
102        service_id: u16,
103    },
104    #[error("unable to execute command with bluer")]
105    CommandFailed {
106        #[source]
107        cause: bluer::Error,
108    },
109    #[error("too many retries")]
110    TooManyRetries {
111        retries: u8,
112        #[source]
113        cause: bluer::Error,
114    },
115    #[error("no service data provided")]
116    NoServiceData,
117    #[error("the provided device is not supported")]
118    DeviceNotSupported,
119}
120
121#[derive(Clone)]
122pub struct System {
123    inner: Vec<u8>,
124}
125
126impl From<Vec<u8>> for System {
127    fn from(inner: Vec<u8>) -> Self {
128        Self { inner }
129    }
130}
131
132impl System {
133    pub fn battery(&self) -> u8 {
134        self.inner[0]
135    }
136
137    pub fn firmware(&self) -> Cow<'_, str> {
138        String::from_utf8_lossy(&self.inner[2..])
139    }
140}
141
142impl std::fmt::Debug for System {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct(stringify!(System))
145            .field("battery", &self.battery())
146            .field("firmware", &self.firmware())
147            .finish()
148    }
149}
150
151/// Represents a real time entry of sensor values by parsing the byte array returned by the device.
152///
153/// The sensor returns 16 bytes in total.
154/// It's unclear what the meaning of these bytes is beyond what is decoded in this method.
155///
156/// Semantics of the data (in little endian encoding):
157/// bytes   0-1: temperature in 0.1 °C
158/// byte      2: unknown
159/// bytes   3-6: brightness in lux
160/// byte      7: moisture in %
161/// byted   8-9: conductivity in µS/cm
162/// bytes 10-15: unknown
163///
164/// (source https://github.com/vrachieru/xiaomi-flower-care-api/blob/master/flowercare/reader.py#L138)
165#[derive(Clone)]
166pub struct RealtimeEntry {
167    inner: Vec<u8>,
168}
169
170impl From<Vec<u8>> for RealtimeEntry {
171    fn from(inner: Vec<u8>) -> Self {
172        Self { inner }
173    }
174}
175
176impl RealtimeEntry {
177    pub fn temperature(&self) -> u16 {
178        u16::from_le_bytes([self.inner[0], self.inner[1]])
179    }
180
181    pub fn brightness(&self) -> u32 {
182        u32::from_le_bytes([self.inner[3], self.inner[4], self.inner[5], self.inner[6]])
183    }
184
185    pub fn moisture(&self) -> u8 {
186        self.inner[7]
187    }
188
189    pub fn conductivity(&self) -> u16 {
190        u16::from_le_bytes([self.inner[8], self.inner[9]])
191    }
192}
193
194impl std::fmt::Debug for RealtimeEntry {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.debug_struct(stringify!(RealTimeEntry))
197            .field("temperature", &self.temperature())
198            .field("brightness", &self.brightness())
199            .field("moisture", &self.moisture())
200            .field("conductivity", &self.conductivity())
201            .finish()
202    }
203}
204
205/// Represents a historical entry of sensor values by parsing the byte array returned by the device.
206///
207/// The sensor returns 16 bytes in total.
208/// It's unclear what the meaning of these bytes is beyond what is decoded in this method.
209///
210/// Semantics of the data (in little endian encoding):
211/// bytes   0-3: timestamp, seconds since boot
212/// bytes   4-5: temperature in 0.1 °C
213/// byte      6: unknown
214/// bytes   7-9: brightness in lux
215/// byte     10: unknown
216/// byte     11: moisture in %
217/// bytes 12-13: conductivity in µS/cm
218/// bytes 14-15: unknown
219///
220/// (source https://github.com/vrachieru/xiaomi-flower-care-api/blob/master/flowercare/reader.py#L160)
221#[derive(Clone)]
222pub struct HistoricalEntry {
223    epoch_time: u64,
224    inner: Vec<u8>,
225}
226
227impl HistoricalEntry {
228    fn new(inner: Vec<u8>, epoch_time: u64) -> Self {
229        Self { epoch_time, inner }
230    }
231
232    pub fn timestamp(&self) -> u64 {
233        let offset =
234            u32::from_le_bytes([self.inner[0], self.inner[1], self.inner[2], self.inner[3]]);
235        self.epoch_time + offset as u64
236    }
237
238    pub fn temperature(&self) -> u16 {
239        u16::from_le_bytes([self.inner[4], self.inner[5]])
240    }
241
242    pub fn brightness(&self) -> u32 {
243        u32::from_le_bytes([self.inner[7], self.inner[8], self.inner[9], 0])
244    }
245
246    pub fn moisture(&self) -> u8 {
247        self.inner[11]
248    }
249
250    pub fn conductivity(&self) -> u16 {
251        u16::from_le_bytes([self.inner[12], self.inner[13]])
252    }
253}
254
255impl std::fmt::Debug for HistoricalEntry {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        f.debug_struct(stringify!(HistoricalEntry))
258            .field("timestamp", &self.timestamp())
259            .field("temperature", &self.temperature())
260            .field("brightness", &self.brightness())
261            .field("moisture", &self.moisture())
262            .field("conductivity", &self.conductivity())
263            .finish()
264    }
265}
266
267#[derive(Clone, Debug)]
268pub struct Miflora {
269    device: Device,
270}
271
272impl From<Device> for Miflora {
273    fn from(device: Device) -> Self {
274        Self { device }
275    }
276}
277
278pub async fn is_miflora_device(device: &Device) -> Result<bool, Error> {
279    let service_data = device
280        .service_data()
281        .await
282        .map_err(|err| Error::CommandFailed { cause: err })?;
283    let service_data = service_data.ok_or(Error::NoServiceData)?;
284    Ok(service_data.iter().any(|(uuid, _data)| {
285        let (id, _, _, _) = uuid.as_fields();
286        id == DEVICE_UUID_PREFIX
287    }))
288}
289
290impl Miflora {
291    pub async fn try_from_adapter(adapter: &Adapter, address: Address) -> Result<Self, Error> {
292        let device = adapter
293            .device(address)
294            .map_err(|err| Error::DeviceNotFound {
295                address,
296                cause: err,
297            })?;
298        Self::try_from_device(device).await
299    }
300
301    pub async fn try_from_device(device: Device) -> Result<Self, Error> {
302        if is_miflora_device(&device).await? {
303            Ok(Self { device })
304        } else {
305            Err(Error::DeviceNotSupported)
306        }
307    }
308
309    async fn characteristic(&self, service_id: u16, char_id: u16) -> Result<Characteristic, Error> {
310        let services = self
311            .device
312            .services()
313            .await
314            .map_err(|err| Error::CommandFailed { cause: err })?;
315        let service = services
316            .into_iter()
317            .find(|s| s.id() == service_id)
318            .ok_or_else(|| Error::ServiceNotFound {
319                service_id,
320                cause: bluer::Error {
321                    kind: bluer::ErrorKind::NotFound,
322                    message: "service not found".into(),
323                },
324            })?;
325        let characteristics = service
326            .characteristics()
327            .await
328            .map_err(|err| Error::CommandFailed { cause: err })?;
329        characteristics
330            .into_iter()
331            .find(|c| c.id() == char_id)
332            .ok_or_else(|| Error::CharacteristicNotFound {
333                characteristic_id: char_id,
334                service_id,
335                cause: bluer::Error {
336                    kind: bluer::ErrorKind::NotFound,
337                    message: "characteristic not found".into(),
338                },
339            })
340    }
341
342    async fn read(&self, service_id: u16, char_id: u16) -> Result<Vec<u8>, Error> {
343        let char = self.characteristic(service_id, char_id).await?;
344        tracing::trace!(
345            message = "reading",
346            service = service_id,
347            characteristic = char_id
348        );
349        char.read().await.map_err(|err| Error::UnableToRead {
350            characteristic_id: char_id,
351            service_id,
352            cause: err,
353        })
354    }
355
356    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
357    pub async fn is_connected(&self) -> Result<bool, Error> {
358        self.device
359            .is_connected()
360            .await
361            .map_err(|err| Error::CommandFailed { cause: err })
362    }
363
364    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
365    pub async fn connect(&self) -> Result<(), Error> {
366        self.device
367            .connect()
368            .await
369            .map_err(|err| Error::CommandFailed { cause: err })
370    }
371
372    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
373    pub async fn try_connect(&self, retry: u8) -> Result<(), Error> {
374        let mut count = 0;
375        loop {
376            if self.is_connected().await? {
377                tracing::debug!("already connected");
378                return Ok(());
379            }
380            match self.device.connect().await {
381                Ok(_) => {
382                    tracing::info!("device connected");
383                    return Ok(());
384                }
385                Err(err) => {
386                    count += 1;
387                    tracing::warn!(message = "unable to connect", tries = count, cause = %err);
388                    if count > retry {
389                        return Err(Error::TooManyRetries {
390                            retries: count,
391                            cause: err,
392                        });
393                    }
394                }
395            }
396        }
397    }
398
399    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
400    pub async fn disconnect(&self) -> Result<(), Error> {
401        self.device
402            .disconnect()
403            .await
404            .map_err(|err| Error::CommandFailed { cause: err })
405    }
406
407    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
408    pub async fn try_disconnect(&self, retry: u8) -> Result<(), Error> {
409        let mut count = 0;
410        loop {
411            if !self.is_connected().await? {
412                tracing::debug!("already disconnected");
413                return Ok(());
414            }
415            match self.device.disconnect().await {
416                Ok(_) => {
417                    tracing::info!("device disconnected");
418                    return Ok(());
419                }
420                Err(err) => {
421                    count += 1;
422                    tracing::warn!(message = "unable to disconnect", tries = count, cause = %err);
423                    if count > retry {
424                        return Err(Error::TooManyRetries {
425                            retries: count,
426                            cause: err,
427                        });
428                    }
429                }
430            }
431        }
432    }
433
434    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
435    pub async fn read_system(&self) -> Result<System, Error> {
436        let data = self
437            .read(SERVICE_DATA_ID, CHARACTERISTIC_FIRMWARE_ID)
438            .await?;
439        Ok(System::from(data))
440    }
441
442    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
443    pub async fn read_realtime_values(&self) -> Result<RealtimeEntry, Error> {
444        self.set_realtime_data_mode(true).await?;
445
446        let data = self.read(SERVICE_DATA_ID, CHARACTERISTIC_DATA_ID).await?;
447        Ok(RealtimeEntry::from(data))
448    }
449
450    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
451    pub async fn read_epoch_time(&self) -> Result<u64, Error> {
452        let start = now();
453        let char = self
454            .characteristic(SERVICE_HISTORY_ID, CHARACTERISTIC_HISTORY_TIME_ID)
455            .await?;
456        tracing::trace!(
457            message = "reading",
458            service = SERVICE_HISTORY_ID,
459            characteristic = CHARACTERISTIC_HISTORY_TIME_ID
460        );
461        let data = char.read().await.map_err(|err| Error::UnableToWrite {
462            characteristic_id: CHARACTERISTIC_HISTORY_TIME_ID,
463            service_id: SERVICE_HISTORY_ID,
464            cause: err,
465        })?;
466        let wall_time = (now() + start) / 2.0;
467        let epoch_offset = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
468        let epoch_time = (wall_time as u64) - (epoch_offset as u64);
469        Ok(epoch_time)
470    }
471
472    fn historical_entry_address(&self, index: u16) -> [u8; 3] {
473        let bytes = u16::to_le_bytes(index);
474        [0xa1, bytes[0], bytes[1]]
475    }
476
477    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
478    pub async fn read_historical_values(&self) -> Result<Vec<HistoricalEntry>, Error> {
479        let ctrl_char = self
480            .characteristic(SERVICE_HISTORY_ID, CHARACTERISTIC_HISTORY_CTRL_ID)
481            .await?;
482        tracing::trace!(
483            message = "writing",
484            service = SERVICE_HISTORY_ID,
485            characteristic = CHARACTERISTIC_HISTORY_CTRL_ID
486        );
487        ctrl_char
488            .write_ext(&CMD_HISTORY_READ_INIT, &WRITE_OPTS)
489            .await
490            .map_err(|err| Error::UnableToWrite {
491                characteristic_id: CHARACTERISTIC_HISTORY_CTRL_ID,
492                service_id: SERVICE_HISTORY_ID,
493                cause: err,
494            })?;
495        //
496        let char = self
497            .characteristic(SERVICE_HISTORY_ID, CHARACTERISTIC_HISTORY_READ_ID)
498            .await?;
499        tracing::trace!(
500            message = "reading",
501            service = SERVICE_HISTORY_ID,
502            characteristic = CHARACTERISTIC_HISTORY_READ_ID
503        );
504        let raw_history_data = char.read().await.map_err(|err| Error::UnableToRead {
505            characteristic_id: CHARACTERISTIC_HISTORY_READ_ID,
506            service_id: SERVICE_HISTORY_ID,
507            cause: err,
508        })?;
509        let history_length = u16::from_le_bytes([raw_history_data[0], raw_history_data[1]]);
510        //
511        let mut result = Vec::with_capacity(history_length as usize);
512        if history_length > 0 {
513            let epoch_time = self.read_epoch_time().await?;
514            let read_char = self
515                .characteristic(SERVICE_HISTORY_ID, CHARACTERISTIC_HISTORY_READ_ID)
516                .await?;
517            for i in 0..history_length {
518                tracing::debug!("loading entry {i}");
519                let payload = self.historical_entry_address(i);
520                tracing::trace!(
521                    message = "writing",
522                    service = SERVICE_HISTORY_ID,
523                    characteristic = CHARACTERISTIC_HISTORY_CTRL_ID
524                );
525                ctrl_char
526                    .write_ext(&payload, &WRITE_OPTS)
527                    .await
528                    .map_err(|err| Error::UnableToWrite {
529                        characteristic_id: CHARACTERISTIC_HISTORY_CTRL_ID,
530                        service_id: SERVICE_HISTORY_ID,
531                        cause: err,
532                    })?;
533                tracing::trace!(
534                    message = "reading",
535                    service = SERVICE_HISTORY_ID,
536                    characteristic = CHARACTERISTIC_HISTORY_READ_ID
537                );
538                let data = read_char.read().await.map_err(|err| Error::UnableToRead {
539                    characteristic_id: CHARACTERISTIC_HISTORY_READ_ID,
540                    service_id: SERVICE_HISTORY_ID,
541                    cause: err,
542                })?;
543                result.push(HistoricalEntry::new(data, epoch_time));
544            }
545        }
546        Ok(result)
547    }
548
549    #[tracing::instrument(skip(self), fields(address = %self.device.address()))]
550    pub async fn clear_historical_entries(&self) -> Result<(), Error> {
551        let ctrl_char = self
552            .characteristic(SERVICE_HISTORY_ID, CHARACTERISTIC_HISTORY_CTRL_ID)
553            .await?;
554        tracing::trace!(
555            message = "writing",
556            service = SERVICE_HISTORY_ID,
557            characteristic = CHARACTERISTIC_HISTORY_CTRL_ID
558        );
559        ctrl_char
560            .write_ext(&CMD_HISTORY_READ_SUCCESS, &WRITE_OPTS)
561            .await
562            .map_err(|err| Error::UnableToRead {
563                characteristic_id: CHARACTERISTIC_HISTORY_CTRL_ID,
564                service_id: SERVICE_HISTORY_ID,
565                cause: err,
566            })?;
567        Ok(())
568    }
569
570    async fn set_realtime_data_mode(&self, enabled: bool) -> Result<(), Error> {
571        self.set_device_mode(if enabled {
572            &CMD_REALTIME_ENABLE
573        } else {
574            &CMD_REALTIME_DISABLE
575        })
576        .await
577    }
578
579    async fn set_device_mode(&self, payload: &[u8]) -> Result<(), Error> {
580        let char = self
581            .characteristic(SERVICE_DATA_ID, CHARACTERISTIC_MODE_ID)
582            .await?;
583        tracing::trace!(
584            message = "writing",
585            service = SERVICE_DATA_ID,
586            characteristic = CHARACTERISTIC_MODE_ID
587        );
588        char.write_ext(payload, &WRITE_OPTS)
589            .await
590            .map_err(|err| Error::UnableToWrite {
591                service_id: SERVICE_DATA_ID,
592                characteristic_id: CHARACTERISTIC_MODE_ID,
593                cause: err,
594            })?;
595        let data = char.read().await.map_err(|err| Error::UnableToRead {
596            characteristic_id: CHARACTERISTIC_MODE_ID,
597            service_id: SERVICE_DATA_ID,
598            cause: err,
599        })?;
600        if !data.eq(payload) {
601            return Err(Error::InvalidWrittenValue {
602                characteristic_id: CHARACTERISTIC_MODE_ID,
603                service_id: SERVICE_DATA_ID,
604            });
605        }
606        Ok(())
607    }
608}