1use bytes::Buf;
13use serde::{Deserialize, Serialize};
14
15use aranet_types::{DeviceType, Status};
16
17use crate::error::{Error, Result};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AdvertisementData {
22 pub device_type: DeviceType,
24 pub co2: Option<u16>,
26 pub temperature: Option<f32>,
28 pub pressure: Option<f32>,
30 pub humidity: Option<u8>,
32 pub battery: u8,
34 pub status: Status,
36 pub interval: u16,
38 pub age: u16,
40 pub radon: Option<u32>,
42 pub radiation_dose_rate: Option<f32>,
44 pub counter: Option<u8>,
46 pub flags: u8,
48}
49
50pub fn parse_advertisement(data: &[u8]) -> Result<AdvertisementData> {
62 if data.is_empty() {
63 return Err(Error::InvalidData(
64 "Advertisement data is empty".to_string(),
65 ));
66 }
67
68 let device_type = match data[0] {
70 0xF1 => DeviceType::Aranet4,
71 0xF2 => DeviceType::Aranet2,
72 0xF3 => DeviceType::AranetRadon,
73 0xF4 => DeviceType::AranetRadiation,
74 other => {
75 return Err(Error::InvalidData(format!(
76 "Unknown device type: 0x{:02X}",
77 other
78 )));
79 }
80 };
81
82 match device_type {
83 DeviceType::Aranet4 => parse_aranet4_advertisement(data),
84 DeviceType::Aranet2 => parse_aranet2_advertisement(data),
85 DeviceType::AranetRadon => parse_aranet_radon_advertisement(data),
86 DeviceType::AranetRadiation => parse_aranet_radiation_advertisement(data),
87 _ => Err(Error::InvalidData(format!(
89 "Unsupported device type for advertisement parsing: {:?}",
90 device_type
91 ))),
92 }
93}
94
95fn parse_aranet4_advertisement(data: &[u8]) -> Result<AdvertisementData> {
110 if data.len() < 16 {
111 return Err(Error::InvalidData(format!(
112 "Aranet4 advertisement requires 16 bytes, got {}",
113 data.len()
114 )));
115 }
116
117 let mut buf = &data[1..]; let flags = buf.get_u8();
119 let co2 = buf.get_u16_le();
120 let temp_raw = buf.get_u16_le();
121 let pressure_raw = buf.get_u16_le();
122 let humidity = buf.get_u8();
123 let battery = buf.get_u8();
124 let status = Status::from(buf.get_u8());
125 let interval = buf.get_u16_le();
126 let age = buf.get_u16_le();
127 let counter = buf.get_u8();
128
129 Ok(AdvertisementData {
130 device_type: DeviceType::Aranet4,
131 co2: Some(co2),
132 temperature: Some(temp_raw as f32 / 20.0),
133 pressure: Some(pressure_raw as f32 / 10.0),
134 humidity: Some(humidity),
135 battery,
136 status,
137 interval,
138 age,
139 radon: None,
140 radiation_dose_rate: None,
141 counter: Some(counter),
142 flags,
143 })
144}
145
146fn parse_aranet2_advertisement(data: &[u8]) -> Result<AdvertisementData> {
148 if data.len() < 12 {
149 return Err(Error::InvalidData(format!(
150 "Aranet2 advertisement requires at least 12 bytes, got {}",
151 data.len()
152 )));
153 }
154
155 let mut buf = &data[1..];
156 let flags = buf.get_u8();
157 let temp_raw = buf.get_u16_le();
158 let humidity_raw = buf.get_u16_le();
159 let battery = buf.get_u8();
160 let status = Status::from(buf.get_u8());
161 let interval = buf.get_u16_le();
162 let age = buf.get_u16_le();
163
164 Ok(AdvertisementData {
165 device_type: DeviceType::Aranet2,
166 co2: None,
167 temperature: Some(temp_raw as f32 / 20.0),
168 pressure: None,
169 humidity: Some((humidity_raw / 10).min(255) as u8),
170 battery,
171 status,
172 interval,
173 age,
174 radon: None,
175 radiation_dose_rate: None,
176 counter: None,
177 flags,
178 })
179}
180
181fn parse_aranet_radon_advertisement(data: &[u8]) -> Result<AdvertisementData> {
183 if data.len() < 18 {
184 return Err(Error::InvalidData(format!(
185 "Aranet Radon advertisement requires at least 18 bytes, got {}",
186 data.len()
187 )));
188 }
189
190 let mut buf = &data[1..];
191 let flags = buf.get_u8();
192 let temp_raw = buf.get_u16_le();
193 let pressure_raw = buf.get_u16_le();
194 let humidity_raw = buf.get_u16_le();
195 let battery = buf.get_u8();
196 let status = Status::from(buf.get_u8());
197 let interval = buf.get_u16_le();
198 let age = buf.get_u16_le();
199 let radon = buf.get_u32_le();
200
201 Ok(AdvertisementData {
202 device_type: DeviceType::AranetRadon,
203 co2: None,
204 temperature: Some(temp_raw as f32 / 20.0),
205 pressure: Some(pressure_raw as f32 / 10.0),
206 humidity: Some((humidity_raw / 10).min(255) as u8),
207 battery,
208 status,
209 interval,
210 age,
211 radon: Some(radon),
212 radiation_dose_rate: None,
213 counter: None,
214 flags,
215 })
216}
217
218fn parse_aranet_radiation_advertisement(data: &[u8]) -> Result<AdvertisementData> {
220 if data.len() < 16 {
221 return Err(Error::InvalidData(format!(
222 "Aranet Radiation advertisement requires at least 16 bytes, got {}",
223 data.len()
224 )));
225 }
226
227 let mut buf = &data[1..];
228 let flags = buf.get_u8();
229 let battery = buf.get_u8();
230 let status = Status::from(buf.get_u8());
231 let interval = buf.get_u16_le();
232 let age = buf.get_u16_le();
233 let dose_rate_nsv = buf.get_u32_le();
235 let dose_rate_usv = dose_rate_nsv as f32 / 1000.0;
236
237 Ok(AdvertisementData {
238 device_type: DeviceType::AranetRadiation,
239 co2: None,
240 temperature: None,
241 pressure: None,
242 humidity: None,
243 battery,
244 status,
245 interval,
246 age,
247 radon: None,
248 radiation_dose_rate: Some(dose_rate_usv),
249 counter: None,
250 flags,
251 })
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_parse_aranet4_advertisement() {
260 let data: [u8; 16] = [
261 0xF1, 0x00, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
273
274 let result = parse_advertisement(&data).unwrap();
275 assert_eq!(result.device_type, DeviceType::Aranet4);
276 assert_eq!(result.co2, Some(800));
277 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
278 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
279 assert_eq!(result.humidity, Some(45));
280 assert_eq!(result.battery, 85);
281 assert_eq!(result.status, Status::Green);
282 assert_eq!(result.interval, 300);
283 assert_eq!(result.age, 120);
284 assert_eq!(result.counter, Some(5));
285 }
286
287 #[test]
288 fn test_parse_aranet2_advertisement() {
289 let data: [u8; 12] = [
290 0xF2, 0x00, 0xC2, 0x01, 0xC2, 0x01, 85, 1, 0x2C, 0x01, 0x3C, 0x00, ];
299
300 let result = parse_advertisement(&data).unwrap();
301 assert_eq!(result.device_type, DeviceType::Aranet2);
302 assert!(result.co2.is_none());
303 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
304 assert_eq!(result.humidity, Some(45));
305 assert_eq!(result.battery, 85);
306 }
307
308 #[test]
309 fn test_parse_empty_data() {
310 let result = parse_advertisement(&[]);
311 assert!(result.is_err());
312 assert!(result.unwrap_err().to_string().contains("empty"));
313 }
314
315 #[test]
316 fn test_parse_unknown_device_type() {
317 let data: [u8; 16] = [0xFF; 16];
318 let result = parse_advertisement(&data);
319 assert!(result.is_err());
320 assert!(
321 result
322 .unwrap_err()
323 .to_string()
324 .contains("Unknown device type")
325 );
326 }
327
328 #[test]
329 fn test_parse_aranet4_insufficient_bytes() {
330 let data: [u8; 10] = [0xF1; 10];
331 let result = parse_advertisement(&data);
332 assert!(result.is_err());
333 assert!(
334 result
335 .unwrap_err()
336 .to_string()
337 .contains("requires 16 bytes")
338 );
339 }
340
341 #[test]
342 fn test_parse_aranet_radiation_advertisement() {
343 let data: [u8; 16] = [
344 0xF4, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
353
354 let result = parse_advertisement(&data).unwrap();
355 assert_eq!(result.device_type, DeviceType::AranetRadiation);
356 assert!(result.co2.is_none());
357 assert!(result.temperature.is_none());
358 assert!(result.radon.is_none());
359 assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
360 assert_eq!(result.battery, 85);
361 assert_eq!(result.status, Status::Green);
362 assert_eq!(result.interval, 300);
363 assert_eq!(result.age, 60);
364 }
365
366 #[test]
367 fn test_parse_aranet_radiation_insufficient_bytes() {
368 let data: [u8; 10] = [0xF4; 10];
369 let result = parse_advertisement(&data);
370 assert!(result.is_err());
371 assert!(result.unwrap_err().to_string().contains("16 bytes"));
372 }
373}