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 thread::sleep(Duration::from_secs(2));
46 let peripheral = find_by_name(¢ral)
47 .await
48 .ok_or(ConnectionError::DeviceNotFound)?;
49
50 peripheral.connect().await?;
51
52 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 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}