Skip to main content

aranet_core/
advertisement.rs

1//! BLE advertisement data parsing for passive monitoring.
2//!
3//! This module provides functionality to parse sensor data directly from
4//! Bluetooth advertisements without requiring a connection. This enables
5//! monitoring multiple devices simultaneously with lower power consumption.
6//!
7//! # Requirements
8//!
9//! For advertisement data to be available, Smart Home integration must be
10//! enabled on the Aranet device (see [`Device::set_smart_home`](crate::device::Device::set_smart_home)).
11
12use bytes::Buf;
13use serde::{Deserialize, Serialize};
14
15use aranet_types::{DeviceType, Status};
16
17use crate::error::{Error, Result};
18
19/// Parsed sensor data from a BLE advertisement.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AdvertisementData {
22    /// Device type detected from advertisement.
23    pub device_type: DeviceType,
24    /// CO2 concentration in ppm (Aranet4 only).
25    pub co2: Option<u16>,
26    /// Temperature in degrees Celsius.
27    pub temperature: Option<f32>,
28    /// Atmospheric pressure in hPa.
29    pub pressure: Option<f32>,
30    /// Relative humidity percentage (0-100).
31    pub humidity: Option<u8>,
32    /// Battery level percentage (0-100).
33    pub battery: u8,
34    /// CO2 status indicator.
35    pub status: Status,
36    /// Measurement interval in seconds.
37    pub interval: u16,
38    /// Age of reading in seconds since last measurement.
39    pub age: u16,
40    /// Radon concentration in Bq/m³ (Aranet Radon only).
41    pub radon: Option<u32>,
42    /// Radiation dose rate in µSv/h (Aranet Radiation only).
43    pub radiation_dose_rate: Option<f32>,
44    /// Advertisement counter (increments with each new reading).
45    pub counter: Option<u8>,
46    /// Raw manufacturer data flags.
47    pub flags: u8,
48}
49
50/// Parse advertisement data from raw manufacturer data bytes.
51///
52/// The manufacturer data should be from manufacturer ID 0x0702 (SAF Tehnika).
53///
54/// # Arguments
55///
56/// * `data` - Raw manufacturer data bytes (excluding the manufacturer ID)
57///
58/// # Returns
59///
60/// Parsed advertisement data or an error if the data is invalid.
61pub fn parse_advertisement(data: &[u8]) -> Result<AdvertisementData> {
62    parse_advertisement_with_name(data, None)
63}
64
65/// Parse advertisement data with optional device name for better detection.
66///
67/// The device name helps distinguish Aranet4 from other device types since
68/// Aranet4 advertisements don't include a device type prefix byte.
69pub fn parse_advertisement_with_name(data: &[u8], name: Option<&str>) -> Result<AdvertisementData> {
70    if data.is_empty() {
71        return Err(Error::InvalidData(
72            "Advertisement data is empty".to_string(),
73        ));
74    }
75
76    // Aranet advertisement format detection:
77    // - Aranet4: NO device type byte prefix, detect by name or length (7 or 22 bytes)
78    // - Aranet2: First byte = 0x01
79    // - Aranet Radiation: First byte = 0x02
80    // - Aranet Radon: First byte = 0x03
81    //
82    // The data structure is:
83    // - Bytes 0-3: Basic info (flags, version)
84    // - Bit 5 of flags (byte 0): Smart Home integrations enabled
85    // - Remaining bytes: Sensor measurements (if integrations enabled)
86
87    let is_aranet4_by_name = name.map(|n| n.starts_with("Aranet4")).unwrap_or(false);
88    let is_aranet4_by_len = data.len() == 7 || data.len() == 22;
89
90    let (device_type, sensor_data) = if is_aranet4_by_name || is_aranet4_by_len {
91        // Aranet4: prepend virtual 0x00 device type byte
92        (DeviceType::Aranet4, data)
93    } else {
94        // Other devices have the device type as first byte
95        let device_type = match data[0] {
96            0x01 => DeviceType::Aranet2,
97            0x02 => DeviceType::AranetRadiation,
98            0x03 => DeviceType::AranetRadon,
99            other => {
100                return Err(Error::InvalidData(format!(
101                    "Unknown device type byte: 0x{:02X}. Expected 0x01 (Aranet2), \
102                     0x02 (Radiation), or 0x03 (Radon). Data length: {} bytes.",
103                    other,
104                    data.len()
105                )));
106            }
107        };
108        (device_type, &data[1..])
109    };
110
111    // Check if Smart Home integrations are enabled (bit 5 of flags byte)
112    if sensor_data.is_empty() {
113        return Err(Error::InvalidData(
114            "Advertisement data too short for basic info".to_string(),
115        ));
116    }
117
118    let flags = sensor_data[0];
119    let integrations_enabled = (flags & (1 << 5)) != 0;
120
121    if !integrations_enabled {
122        return Err(Error::InvalidData(
123            "Smart Home integration is not enabled on this device. \
124             To enable: go to device Settings > Smart Home > Enable."
125                .to_string(),
126        ));
127    }
128
129    match device_type {
130        DeviceType::Aranet4 => parse_aranet4_advertisement_v2(sensor_data),
131        DeviceType::Aranet2 => parse_aranet2_advertisement_v2(sensor_data),
132        DeviceType::AranetRadon => parse_aranet_radon_advertisement_v2(sensor_data),
133        DeviceType::AranetRadiation => parse_aranet_radiation_advertisement_v2(sensor_data),
134        _ => Err(Error::InvalidData(format!(
135            "Unsupported device type for advertisement parsing: {:?}",
136            device_type
137        ))),
138    }
139}
140
141/// Parse Aranet4 advertisement data (v2 format - actual device format).
142///
143/// Format (22 bytes, no device type prefix):
144/// - bytes 0-7: Basic info (flags, version, etc.)
145/// - bytes 8-9: CO2 (u16 LE)
146/// - bytes 10-11: Temperature (u16 LE, *0.05 for °C)
147/// - bytes 12-13: Pressure (u16 LE, *0.1 for hPa)
148/// - byte 14: Humidity (u8)
149/// - byte 15: Battery (u8)
150/// - byte 16: Status (u8)
151/// - bytes 17-18: Interval (u16 LE, seconds)
152/// - bytes 19-20: Age (u16 LE, seconds)
153/// - byte 21: Counter (u8)
154fn parse_aranet4_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
155    // Minimum 22 bytes for full Aranet4 advertisement
156    if data.len() < 22 {
157        return Err(Error::InvalidData(format!(
158            "Aranet4 advertisement requires 22 bytes, got {}",
159            data.len()
160        )));
161    }
162
163    let flags = data[0];
164    // Skip to sensor data at offset 8
165    let mut buf = &data[8..];
166    let co2 = buf.get_u16_le();
167    let temp_raw = buf.get_i16_le();
168    let pressure_raw = buf.get_u16_le();
169    let humidity = buf.get_u8();
170    let battery = buf.get_u8();
171    let status = Status::from(buf.get_u8());
172    let interval = buf.get_u16_le();
173    let age = buf.get_u16_le();
174    let counter = if !buf.is_empty() {
175        Some(buf.get_u8())
176    } else {
177        None
178    };
179
180    Ok(AdvertisementData {
181        device_type: DeviceType::Aranet4,
182        co2: Some(co2),
183        temperature: Some(temp_raw as f32 * 0.05),
184        pressure: Some(pressure_raw as f32 * 0.1),
185        humidity: Some(humidity),
186        battery,
187        status,
188        interval,
189        age,
190        radon: None,
191        radiation_dose_rate: None,
192        counter,
193        flags,
194    })
195}
196
197/// Parse Aranet2 advertisement data (v2 format - actual device format).
198///
199/// Format (after device type byte removed, 19+ bytes):
200/// - bytes 0-7: Basic info (flags, version, etc.)
201/// - bytes 8-9: Temperature (u16 LE, *0.05 for °C)
202/// - bytes 10-11: unused
203/// - bytes 12-13: Humidity (u16 LE, *0.1 for %)
204/// - byte 14: Battery (u8)
205/// - byte 15: Status (u8)
206/// - bytes 16-17: Interval (u16 LE, seconds)
207/// - bytes 18-19: Age (u16 LE, seconds)
208/// - byte 20: Counter (u8)
209fn parse_aranet2_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
210    if data.len() < 19 {
211        return Err(Error::InvalidData(format!(
212            "Aranet2 advertisement requires at least 19 bytes, got {}",
213            data.len()
214        )));
215    }
216
217    let flags = data[0];
218    // Skip to sensor data at offset 7
219    let mut buf = &data[7..];
220    let temp_raw = buf.get_i16_le();
221    let _unused = buf.get_u16_le();
222    let humidity_raw = buf.get_u16_le();
223    let battery = buf.get_u8();
224    let status_raw = buf.get_u8();
225    // Status for Aranet2: bits[0:1] = humidity, bits[2:3] = temperature
226    let status = Status::from((status_raw >> 2) & 0x03);
227    let interval = buf.get_u16_le();
228    let age = buf.get_u16_le();
229    let counter = if !buf.is_empty() {
230        Some(buf.get_u8())
231    } else {
232        None
233    };
234
235    Ok(AdvertisementData {
236        device_type: DeviceType::Aranet2,
237        co2: None,
238        temperature: Some(temp_raw as f32 * 0.05),
239        pressure: None,
240        humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
241        battery,
242        status,
243        interval,
244        age,
245        radon: None,
246        radiation_dose_rate: None,
247        counter,
248        flags,
249    })
250}
251
252/// Parse Aranet Radon advertisement data (v2 format - actual device format).
253///
254/// Format (after device type byte removed, 23 bytes):
255/// Based on Python: `<xxxxxxxHHHHBBBHHB` (7 skip bytes, not 8)
256/// - bytes 0-6: Basic info (flags, version, etc.) - 7 bytes
257/// - bytes 7-8: Radon concentration (u16 LE, Bq/m³)
258/// - bytes 9-10: Temperature (u16 LE, *0.05 for °C)
259/// - bytes 11-12: Pressure (u16 LE, *0.1 for hPa)
260/// - bytes 13-14: Humidity (u16 LE, *0.1 for %)
261/// - byte 15: Unknown/reserved (u8) - skipped in Python decode
262/// - byte 16: Battery (u8)
263/// - byte 17: Status (u8)
264/// - bytes 18-19: Interval (u16 LE, seconds)
265/// - bytes 20-21: Age (u16 LE, seconds)
266/// - byte 22: Counter (u8)
267fn parse_aranet_radon_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
268    if data.len() < 22 {
269        return Err(Error::InvalidData(format!(
270            "Aranet Radon advertisement requires at least 22 bytes, got {}",
271            data.len()
272        )));
273    }
274
275    let flags = data[0];
276    // Skip to sensor data at offset 7 (7 bytes of basic info)
277    let mut buf = &data[7..];
278    let radon = buf.get_u16_le() as u32;
279    let temp_raw = buf.get_i16_le();
280    let pressure_raw = buf.get_u16_le();
281    let humidity_raw = buf.get_u16_le();
282    let _reserved = buf.get_u8(); // Unknown/reserved byte (skipped in Python)
283    let battery = buf.get_u8();
284    let status = Status::from(buf.get_u8());
285    let interval = buf.get_u16_le();
286    let age = buf.get_u16_le();
287    let counter = if !buf.is_empty() {
288        Some(buf.get_u8())
289    } else {
290        None
291    };
292
293    Ok(AdvertisementData {
294        device_type: DeviceType::AranetRadon,
295        co2: None,
296        temperature: Some(temp_raw as f32 * 0.05),
297        pressure: Some(pressure_raw as f32 * 0.1),
298        humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
299        battery,
300        status,
301        interval,
302        age,
303        radon: Some(radon),
304        radiation_dose_rate: None,
305        counter,
306        flags,
307    })
308}
309
310/// Parse Aranet Radiation advertisement data (v2 format - actual device format).
311///
312/// Format (after device type byte removed, 19+ bytes):
313/// - bytes 0-5: Basic info (flags, version, etc.)
314/// - bytes 6-9: Radiation total (u32 LE, nSv)
315/// - bytes 10-13: Radiation duration (u32 LE, seconds)
316/// - bytes 14-15: Radiation rate (u16 LE, *10 for nSv/h)
317/// - byte 16: Battery (u8)
318/// - byte 17: Status (u8)
319/// - bytes 18-19: Interval (u16 LE, seconds)
320/// - bytes 20-21: Age (u16 LE, seconds)
321/// - byte 22: Counter (u8)
322fn parse_aranet_radiation_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
323    // Need at least 21 bytes: 5 header + 4 total + 4 duration + 2 rate + 1 battery + 1 status + 2 interval + 2 age
324    if data.len() < 21 {
325        return Err(Error::InvalidData(format!(
326            "Aranet Radiation advertisement requires at least 21 bytes, got {}",
327            data.len()
328        )));
329    }
330
331    let flags = data[0];
332    // Skip to sensor data at offset 5
333    let mut buf = &data[5..];
334    let _radiation_total = buf.get_u32_le(); // nSv total dose
335    let _radiation_duration = buf.get_u32_le(); // seconds
336    let radiation_rate_raw = buf.get_u16_le(); // *10 for nSv/h
337    let battery = buf.get_u8();
338    let status = Status::from(buf.get_u8());
339    let interval = buf.get_u16_le();
340    let age = buf.get_u16_le();
341    let counter = if !buf.is_empty() {
342        Some(buf.get_u8())
343    } else {
344        None
345    };
346
347    // Convert from nSv/h * 10 to µSv/h
348    let dose_rate_usv = (radiation_rate_raw as f32 * 10.0) / 1000.0;
349
350    Ok(AdvertisementData {
351        device_type: DeviceType::AranetRadiation,
352        co2: None,
353        temperature: None,
354        pressure: None,
355        humidity: None,
356        battery,
357        status,
358        interval,
359        age,
360        radon: None,
361        radiation_dose_rate: Some(dose_rate_usv),
362        counter,
363        flags,
364    })
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_parse_aranet4_advertisement() {
373        // Aranet4 v2 format: 22 bytes, no device type prefix
374        // Flags byte has bit 5 set (0x20) for Smart Home integration
375        let data: [u8; 22] = [
376            0x22, // flags (bit 5 = integrations enabled)
377            0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, // basic info (7 bytes)
378            0x20, 0x03, // CO2 = 800
379            0xC2, 0x01, // temp_raw = 450 (450 * 0.05 = 22.5°C)
380            0x94, 0x27, // pressure_raw = 10132 (10132 * 0.1 = 1013.2 hPa)
381            45,   // humidity
382            85,   // battery
383            1,    // status = Green
384            0x2C, 0x01, // interval = 300
385            0x78, 0x00, // age = 120
386            5,    // counter
387        ];
388
389        let result = parse_advertisement(&data).unwrap();
390        assert_eq!(result.device_type, DeviceType::Aranet4);
391        assert_eq!(result.co2, Some(800));
392        assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
393        assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
394        assert_eq!(result.humidity, Some(45));
395        assert_eq!(result.battery, 85);
396        assert_eq!(result.status, Status::Green);
397        assert_eq!(result.interval, 300);
398        assert_eq!(result.age, 120);
399    }
400
401    #[test]
402    fn test_parse_aranet2_advertisement() {
403        // Aranet2 v2 format: device type 0x01, then 19+ bytes
404        // Flags byte has bit 5 set (0x20) for Smart Home integration
405        let data: [u8; 20] = [
406            0x01, // device type = Aranet2
407            0x20, // flags (bit 5 = integrations enabled)
408            0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, // basic info (6 bytes)
409            0xC2, 0x01, // temp_raw = 450 (450 * 0.05 = 22.5°C)
410            0x00, 0x00, // unused
411            0xC2, 0x01, // humidity_raw = 450 (450 * 0.1 = 45%)
412            85,   // battery
413            0x04, // status flags: bits[2:3] = 01 = Green (temperature status)
414            0x2C, 0x01, // interval = 300
415            0x3C, 0x00, // age = 60
416        ];
417
418        let result = parse_advertisement(&data).unwrap();
419        assert_eq!(result.device_type, DeviceType::Aranet2);
420        assert!(result.co2.is_none());
421        assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
422        assert_eq!(result.humidity, Some(45));
423        assert_eq!(result.battery, 85);
424        assert_eq!(result.status, Status::Green);
425    }
426
427    #[test]
428    fn test_parse_aranet_radon_advertisement() {
429        // Aranet Radon v2 format: device type 0x03, then 23 bytes
430        // Format: <xxxxxxxHHHHBBBHHB (7 skip, 4xH, 3xB, 2xH, 1xB)
431        // Flags byte has bit 5 set (0x20) for Smart Home integration
432        let data: [u8; 24] = [
433            0x03, // device type = Aranet Radon
434            0x21, // flags (bit 5 = integrations enabled)
435            0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, // basic info (6 bytes, total 7 with flags)
436            0x51, 0x00, // radon = 81 Bq/m³
437            0xC2, 0x01, // temp_raw = 450 (450 * 0.05 = 22.5°C)
438            0x94, 0x27, // pressure_raw = 10132 (10132 * 0.1 = 1013.2 hPa)
439            0xC2, 0x01, // humidity_raw = 450 (450 * 0.1 = 45%)
440            0x00, // reserved byte (skipped in Python decode)
441            85,   // battery
442            1,    // status = Green
443            0x2C, 0x01, // interval = 300
444            0x3C, 0x00, // age = 60
445            5,    // counter
446        ];
447
448        let result = parse_advertisement(&data).unwrap();
449        assert_eq!(result.device_type, DeviceType::AranetRadon);
450        assert!(result.co2.is_none());
451        assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
452        assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
453        assert_eq!(result.humidity, Some(45));
454        assert_eq!(result.radon, Some(81));
455        assert_eq!(result.battery, 85);
456        assert_eq!(result.status, Status::Green);
457    }
458
459    #[test]
460    fn test_parse_empty_data() {
461        let result = parse_advertisement(&[]);
462        assert!(result.is_err());
463        assert!(result.unwrap_err().to_string().contains("empty"));
464    }
465
466    #[test]
467    fn test_parse_unknown_device_type() {
468        // Unknown device type byte (not 0x01, 0x02, or 0x03)
469        // and not Aranet4 length (7 or 22 bytes)
470        let data: [u8; 16] = [0xFF; 16];
471        let result = parse_advertisement(&data);
472        assert!(result.is_err());
473        let err_msg = result.unwrap_err().to_string();
474        assert!(
475            err_msg.contains("Unknown device type byte"),
476            "Expected unknown device type error, got: {}",
477            err_msg
478        );
479    }
480
481    #[test]
482    fn test_parse_aranet4_insufficient_bytes() {
483        // Aranet4 is detected by length (7 or 22 bytes)
484        // 10 bytes is not a valid Aranet4 length, so it will try to parse as other device
485        // But 0x22 is not a valid device type, so it will fail
486        let data: [u8; 10] = [0x22; 10];
487        let result = parse_advertisement(&data);
488        assert!(result.is_err());
489        let err_msg = result.unwrap_err().to_string();
490        assert!(
491            err_msg.contains("Unknown device type byte"),
492            "Expected unknown device type error, got: {}",
493            err_msg
494        );
495    }
496
497    #[test]
498    fn test_parse_aranet_radiation_advertisement() {
499        // Aranet Radiation v2 format: device type 0x02, then 19+ bytes
500        // Flags byte has bit 5 set (0x20) for Smart Home integration
501        // Note: Using 23 bytes to avoid triggering Aranet4 detection (which uses 7 or 22 bytes)
502        let data: [u8; 23] = [
503            0x02, // device type = Radiation
504            0x20, // flags (bit 5 = integrations enabled)
505            0x13, 0x04, 0x01, 0x00, // basic info (4 bytes)
506            0x00, 0x00, 0x00, 0x00, // radiation total (u32)
507            0x00, 0x00, 0x00, 0x00, // radiation duration (u32)
508            0x64, 0x00, // radiation rate = 100 (*10 = 1000 nSv/h = 1.0 µSv/h)
509            85,   // battery
510            1,    // status = Green
511            0x2C, 0x01, // interval = 300
512            0x3C, 0x00, // age = 60
513            5,    // counter
514        ];
515
516        let result = parse_advertisement(&data).unwrap();
517        assert_eq!(result.device_type, DeviceType::AranetRadiation);
518        assert!(result.co2.is_none());
519        assert!(result.temperature.is_none());
520        assert!(result.radon.is_none());
521        assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
522        assert_eq!(result.battery, 85);
523        assert_eq!(result.status, Status::Green);
524        assert_eq!(result.interval, 300);
525        assert_eq!(result.age, 60);
526    }
527
528    #[test]
529    fn test_parse_aranet_radiation_insufficient_bytes() {
530        // Device type 0x02 but not enough bytes
531        let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
532        let result = parse_advertisement(&data);
533        assert!(result.is_err());
534        let err_msg = result.unwrap_err().to_string();
535        assert!(
536            err_msg.contains("requires at least 21 bytes"),
537            "Expected insufficient bytes error, got: {}",
538            err_msg
539        );
540    }
541
542    #[test]
543    fn test_parse_smart_home_not_enabled() {
544        // Aranet4 format (22 bytes) but bit 5 not set in flags
545        let data: [u8; 22] = [
546            0x00, // flags (bit 5 NOT set - integrations disabled)
547            0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, // basic info
548            0x20, 0x03, // CO2
549            0xC2, 0x01, // temp
550            0x94, 0x27, // pressure
551            45, 85, 1, // humidity, battery, status
552            0x2C, 0x01, // interval
553            0x78, 0x00, // age
554            5,    // counter
555        ];
556
557        let result = parse_advertisement(&data);
558        assert!(result.is_err());
559        let err_msg = result.unwrap_err().to_string();
560        assert!(
561            err_msg.contains("Smart Home integration is not enabled"),
562            "Expected Smart Home error, got: {}",
563            err_msg
564        );
565    }
566}
567
568/// Property-based tests for BLE advertisement parsing.
569///
570/// These tests verify that advertisement parsing is safe with any input,
571/// including malformed or random data that might be received from BLE scans.
572///
573/// # Test Categories
574///
575/// ## Panic Safety Tests
576/// - `parse_advertisement_never_panics`: Any random bytes
577/// - `parse_aranet4_advertisement_never_panics`: 22-byte sequences
578/// - `parse_aranet2_advertisement_never_panics`: Aranet2 device type
579/// - `parse_aranet_radon_advertisement_never_panics`: Radon device type
580/// - `parse_aranet_radiation_advertisement_never_panics`: Radiation device type
581///
582/// # Running Tests
583///
584/// ```bash
585/// cargo test -p aranet-core advertisement::proptests
586/// ```
587#[cfg(test)]
588mod proptests {
589    use super::*;
590    use proptest::prelude::*;
591
592    proptest! {
593        /// Parsing random advertisement bytes should never panic.
594        /// It may return an error, but should always be safe.
595        #[test]
596        fn parse_advertisement_never_panics(data: Vec<u8>) {
597            let _ = parse_advertisement(&data);
598        }
599
600        /// Parsing with valid Aranet4 length (22 bytes) should not panic.
601        #[test]
602        fn parse_aranet4_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 22)) {
603            let _ = parse_advertisement(&data);
604        }
605
606        /// Parsing with Aranet2 format (device type 0x01) should not panic.
607        #[test]
608        fn parse_aranet2_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
609            let mut modified = data.clone();
610            if !modified.is_empty() {
611                modified[0] = 0x01; // Set device type to Aranet2
612            }
613            let _ = parse_advertisement(&modified);
614        }
615
616        /// Parsing with Aranet Radon format should not panic.
617        #[test]
618        fn parse_aranet_radon_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 23..=30)) {
619            let mut modified = data.clone();
620            if !modified.is_empty() {
621                modified[0] = 0x03; // Set device type to Radon
622            }
623            let _ = parse_advertisement(&modified);
624        }
625
626        /// Parsing with Aranet Radiation format should not panic.
627        #[test]
628        fn parse_aranet_radiation_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
629            let mut modified = data.clone();
630            if !modified.is_empty() {
631                modified[0] = 0x02; // Set device type to Radiation
632            }
633            let _ = parse_advertisement(&modified);
634        }
635    }
636}