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
8const 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; const CHARACTERISTIC_HISTORY_READ_ID: u16 = 59; const CHARACTERISTIC_HISTORY_TIME_ID: u16 = 64;
42
43const CMD_HISTORY_READ_INIT: [u8; 3] = [0xa0, 0x00, 0x00];
45const CMD_HISTORY_READ_SUCCESS: [u8; 3] = [0xa2, 0x00, 0x00];
46const 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#[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#[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 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 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}