aranet_btle/
lib.rs

1use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
2use btleplug::api::{CentralEvent, Characteristic};
3use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
4use byteorder::{LittleEndian, ReadBytesExt};
5use thiserror::Error;
6use tokio_stream::{Stream, StreamExt};
7use uuid::{uuid, Uuid};
8
9use std::collections::HashMap;
10use std::io::Cursor;
11use std::thread;
12use std::time::{Duration, Instant};
13
14const CURRENT_READINGS_CHARACTERISTIC: Uuid = uuid!("f0cd3001-95da-4f4b-9ac8-aa55d312af0c");
15const ADVERTISED_SERVICE_UUID: Uuid = uuid!("0000fce0-0000-1000-8000-00805f9b34fb");
16
17pub struct Aranet4 {
18    peripheral: Peripheral,
19    current_reading_char: Characteristic,
20}
21
22#[derive(Error, Debug)]
23pub enum ConnectionError {
24    #[error("failed to get a bluetooth adapter")]
25    AdapterUnavaliable,
26    #[error("aranet device not found")]
27    DeviceNotFound,
28    #[error("the Characteristic for UUID {0} was not found")]
29    CharacteristicNotFound(String),
30    #[error(transparent)]
31    BTLEError(#[from] btleplug::Error),
32}
33
34pub async fn connect() -> Result<Aranet4, ConnectionError> {
35    let manager = Manager::new().await?;
36
37    let adapters = manager.adapters().await?;
38    let central = adapters
39        .into_iter()
40        .nth(0)
41        .ok_or(ConnectionError::AdapterUnavaliable)?;
42
43    central.start_scan(ScanFilter::default()).await?;
44    // todo: improve this hardcoded sleep
45    thread::sleep(Duration::from_secs(2));
46    let peripheral = find_by_name(&central)
47        .await
48        .ok_or(ConnectionError::DeviceNotFound)?;
49
50    peripheral.connect().await?;
51
52    // Currently doesn't do anything
53    peripheral.discover_services().await?;
54
55    let chars = peripheral.characteristics();
56    let current_reading_char = chars
57        .iter()
58        .find(|c| c.uuid == CURRENT_READINGS_CHARACTERISTIC)
59        .ok_or(ConnectionError::CharacteristicNotFound(
60            CURRENT_READINGS_CHARACTERISTIC.to_string(),
61        ))?;
62
63    Ok(Aranet4 {
64        peripheral,
65        current_reading_char: current_reading_char.clone(),
66    })
67}
68
69pub async fn scan() -> Result<impl Stream<Item = ScanResult>, ConnectionError> {
70    let mut sensor_reading_times: HashMap<PeripheralId, Instant> = HashMap::new();
71
72    let manager = Manager::new().await?;
73    let adapters = manager.adapters().await?;
74    let central = adapters
75        .into_iter()
76        .nth(0)
77        .ok_or(ConnectionError::AdapterUnavaliable)?;
78    let events = central.events().await?;
79
80    let filter = ScanFilter {
81        services: vec![ADVERTISED_SERVICE_UUID],
82    };
83
84    // start scanning for devices
85    central.start_scan(filter).await?;
86
87    Ok(events.filter_map(move |event| match event {
88        CentralEvent::ManufacturerDataAdvertisement {
89            id,
90            manufacturer_data,
91        } => {
92            let mut rdr = Cursor::new(&manufacturer_data[&1794]);
93
94            rdr.set_position(8);
95            match parse_data_in_cursor(rdr) {
96                Ok(data) => {
97                    let read_time = Instant::now();
98
99                    if let Some(last_time) = sensor_reading_times.get(&id) {
100                        if read_time < *last_time {
101                            return None;
102                        }
103                    }
104
105                    sensor_reading_times.insert(
106                        id.clone(),
107                        read_time + Duration::new(data.interval as u64 - data.age as u64 + 1, 0),
108                    );
109
110                    Some(ScanResult {
111                        sensor_data: data,
112                        id,
113                    })
114                }
115                Err(_) => None,
116            }
117        }
118        _ => None,
119    }))
120}
121
122pub struct SensorData {
123    pub co2: u16,
124    pub temperature: f32,
125    pub pressure: u16,
126    pub humidity: u8,
127    pub battery: u8,
128    pub status: u8,
129    pub interval: u16,
130    pub age: u16,
131}
132
133pub struct ScanResult {
134    pub sensor_data: SensorData,
135    pub id: PeripheralId,
136}
137
138#[derive(Error, Debug)]
139pub enum DeviceError {
140    #[error(transparent)]
141    IOError(#[from] std::io::Error),
142    #[error(transparent)]
143    BTLEError(#[from] btleplug::Error),
144}
145
146impl Aranet4 {
147    pub async fn read_data(&self) -> Result<SensorData, DeviceError> {
148        let res = self.peripheral.read(&self.current_reading_char).await?;
149
150        let rdr = Cursor::new(&res);
151        let data = parse_data_in_cursor(rdr)?;
152        Ok(data)
153    }
154
155    pub async fn reconnect(&self) -> Result<(), DeviceError> {
156        self.peripheral.connect().await?;
157
158        Ok(())
159    }
160
161    pub async fn disconnect(&self) -> Result<(), DeviceError> {
162        self.peripheral.disconnect().await?;
163
164        Ok(())
165    }
166}
167
168fn parse_data_in_cursor(mut cursor: Cursor<&Vec<u8>>) -> Result<SensorData, std::io::Error> {
169    let co2 = cursor.read_u16::<LittleEndian>()?;
170    let temperature = cursor.read_u16::<LittleEndian>()? as f32 / 20.0;
171    let pressure = cursor.read_u16::<LittleEndian>()? / 10;
172    let humidity = cursor.read_u8()?;
173    let battery = cursor.read_u8()?;
174    let status = cursor.read_u8()?;
175    let interval = cursor.read_u16::<LittleEndian>()?;
176    let age: u16 = cursor.read_u16::<LittleEndian>()?;
177
178    Ok(SensorData {
179        co2,
180        temperature,
181        pressure,
182        humidity,
183        battery,
184        status,
185        interval,
186        age,
187    })
188}
189
190async fn find_by_name(central: &Adapter) -> Option<Peripheral> {
191    for p in central.peripherals().await.unwrap() {
192        if p.properties()
193            .await
194            .unwrap()
195            .unwrap()
196            .local_name
197            .iter()
198            .any(|name| name.contains("Aranet4"))
199        {
200            return Some(p);
201        }
202    }
203    None
204}