embedded_devices/devices/sensirion/sen65/
mod.rs

1//! The SEN65 is a particulate matter (PM), VOC, NOₓ, temperature and relative humidity sensor
2//! sensor from Sensition's SEN6x sensor module family.
3//!
4//! The SEN6x sensor module family is an air quality platform that combines critical parameters
5//! such as particulate matter, relative humidity, temperature, VOC, NOx and either CO2 or
6//! formaldehyde, all in one compact package.
7//!
8//! ## Usage (sync)
9//!
10//! ```rust
11//! # #[cfg(feature = "sync")] mod test {
12//! # fn test<I, D>(mut i2c: I, delay: D) -> Result<(), embedded_devices::devices::sensirion::sen65::TransportError<I::Error>>
13//! # where
14//! #   I: embedded_hal::i2c::I2c + embedded_hal::i2c::ErrorType,
15//! #   D: embedded_hal::delay::DelayNs
16//! # {
17//! use embedded_devices::devices::sensirion::sen65::{SEN65Sync, address::Address};
18//! use embedded_devices::sensor::ContinuousSensorSync;
19//! use uom::si::{
20//!    mass_concentration::microgram_per_cubic_meter,
21//!    ratio::percent,
22//!    thermodynamic_temperature::degree_celsius,
23//! };
24//!
25//! // Create and initialize the device
26//! let mut sen65 = SEN65Sync::new_i2c(delay, i2c, Address::Default);
27//! sen65.init()?;
28//! sen65.start_measuring()?;
29//!
30//! // [...] wait ~1h for PM results to stabilize
31//! // Read measurements
32//! let measurement = sen65.next_measurement()?;
33//! let pm1 = measurement.pm1_concentration.unwrap().get::<microgram_per_cubic_meter>();
34//! let pm2_5 = measurement.pm2_5_concentration.unwrap().get::<microgram_per_cubic_meter>();
35//! let pm4 = measurement.pm4_concentration.unwrap().get::<microgram_per_cubic_meter>();
36//! let pm10 = measurement.pm10_concentration.unwrap().get::<microgram_per_cubic_meter>();
37//! let humidity = measurement.relative_humidity.unwrap().get::<percent>();
38//! let temperature = measurement.temperature.unwrap().get::<degree_celsius>();
39//! let voc_index = measurement.voc_index.unwrap().get::<percent>();
40//! let nox_index = measurement.nox_index.unwrap().get::<percent>();
41//! println!("Current measurement: {:?} µg/m³ PM1, {:?} µg/m³ PM2.5, {:?} µg/m³ PM4, {:?} µg/m³ PM10, {:?}%RH, {:?}°C, {:?} VOC, {:?} NOx",
42//!     pm1, pm2_5, pm4, pm10, humidity, temperature, voc_index, nox_index
43//! );
44//! # Ok(())
45//! # }
46//! # }
47//! ```
48//!
49//! ## Usage (async)
50//!
51//! ```rust
52//! # #[cfg(feature = "async")] mod test {
53//! # async fn test<I, D>(mut i2c: I, delay: D) -> Result<(), embedded_devices::devices::sensirion::sen65::TransportError<I::Error>>
54//! # where
55//! #   I: embedded_hal_async::i2c::I2c + embedded_hal_async::i2c::ErrorType,
56//! #   D: embedded_hal_async::delay::DelayNs
57//! # {
58//! use embedded_devices::devices::sensirion::sen65::{SEN65Async, address::Address};
59//! use embedded_devices::sensor::ContinuousSensorAsync;
60//! use uom::si::{
61//!    mass_concentration::microgram_per_cubic_meter,
62//!    ratio::percent,
63//!    thermodynamic_temperature::degree_celsius,
64//! };
65//!
66//! // Create and initialize the device
67//! let mut sen65 = SEN65Async::new_i2c(delay, i2c, Address::Default);
68//! sen65.init().await?;
69//! sen65.start_measuring().await?;
70//!
71//! // [...] wait ~1h for PM results to stabilize
72//! // Read measurements
73//! let measurement = sen65.next_measurement().await?;
74//! let pm1 = measurement.pm1_concentration.unwrap().get::<microgram_per_cubic_meter>();
75//! let pm2_5 = measurement.pm2_5_concentration.unwrap().get::<microgram_per_cubic_meter>();
76//! let pm4 = measurement.pm4_concentration.unwrap().get::<microgram_per_cubic_meter>();
77//! let pm10 = measurement.pm10_concentration.unwrap().get::<microgram_per_cubic_meter>();
78//! let humidity = measurement.relative_humidity.unwrap().get::<percent>();
79//! let temperature = measurement.temperature.unwrap().get::<degree_celsius>();
80//! let voc_index = measurement.voc_index.unwrap().get::<percent>();
81//! let nox_index = measurement.nox_index.unwrap().get::<percent>();
82//! println!("Current measurement: {:?} µg/m³ PM1, {:?} µg/m³ PM2.5, {:?} µg/m³ PM4, {:?} µg/m³ PM10, {:?}%RH, {:?}°C, {:?} VOC, {:?} NOx",
83//!     pm1, pm2_5, pm4, pm10, humidity, temperature, voc_index, nox_index
84//! );
85//! # Ok(())
86//! # }
87//! # }
88//! ```
89
90use self::commands::{DeviceReset, GetDataReady, ReadMeasuredValues, StartContinuousMeasurement, StopMeasurement};
91use embedded_devices_derive::{forward_command_fns, sensor};
92use uom::si::f64::{MassConcentration, Ratio, ThermodynamicTemperature};
93
94pub use super::sen6x::address;
95use super::{commands::Crc8Error, sen6x::commands::DataReadyStatus};
96pub mod commands;
97
98/// Any CRC or Bus related error
99pub type TransportError<E> = embedded_interfaces::TransportError<Crc8Error, E>;
100
101/// Measurement data
102#[derive(Debug, embedded_devices_derive::Measurement)]
103pub struct Measurement {
104    /// PM1 concentration
105    #[measurement(Pm1Concentration)]
106    pub pm1_concentration: Option<MassConcentration>,
107    /// PM2.5 concentration
108    #[measurement(Pm2_5Concentration)]
109    pub pm2_5_concentration: Option<MassConcentration>,
110    /// PM4 concentration
111    #[measurement(Pm4Concentration)]
112    pub pm4_concentration: Option<MassConcentration>,
113    /// PM10 concentration
114    #[measurement(Pm10Concentration)]
115    pub pm10_concentration: Option<MassConcentration>,
116    /// Ambient relative humidity
117    #[measurement(RelativeHumidity)]
118    pub relative_humidity: Option<Ratio>,
119    /// Ambient temperature
120    #[measurement(Temperature)]
121    pub temperature: Option<ThermodynamicTemperature>,
122    /// Current VOC Index (1-500), moving average over past 24 hours. On the VOC Index scale, this
123    /// offset is always mapped to the value of 100, making the readout as easy as possible: a VOC
124    /// Index above 100 means that there are more VOCs compared to the average (e.g., induced by a
125    /// VOC event from cooking, cleaning, breathing, etc.) while a VOC Index below 100 means that
126    /// there are fewer VOCs compared to the average (e.g., induced by fresh air from an open
127    /// window, using an air purifier, etc.).
128    #[measurement(VocIndex)]
129    pub voc_index: Option<Ratio>,
130    /// Current NOx Index (1-500), moving average over past 24 hours. On the NOx Index scale, this
131    /// offset is always mapped to the value of 1, making the readout as easy as possible: an NOx
132    /// Index above 1 means that there are more NOx compounds compared to the average (e.g.,
133    /// induced by cooking on a gas stove), while an NOx Index close to 1 means that there are
134    /// (nearly) no NOx gases present, which is the case most of the time (or induced by fresh air
135    /// from an open window, using an air purifier, etc.).
136    #[measurement(NoxIndex)]
137    pub nox_index: Option<Ratio>,
138}
139
140/// The SEN65 is a particulate matter (PM), VOC, NOₓ, temperature and relative humidity sensor
141/// sensor from Sensition's SEN6x sensor module family.
142///
143/// For a full description and usage examples, refer to the [module documentation](self).
144#[maybe_async_cfg::maybe(
145    idents(
146        hal(sync = "embedded_hal", async = "embedded_hal_async"),
147        CommandInterface,
148        I2cDevice
149    ),
150    sync(feature = "sync"),
151    async(feature = "async")
152)]
153pub struct SEN65<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> {
154    /// The delay provider
155    delay: D,
156    /// The interface to communicate with the device
157    interface: I,
158}
159
160pub trait SEN65Command {}
161
162#[maybe_async_cfg::maybe(
163    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), I2cDevice),
164    sync(feature = "sync"),
165    async(feature = "async")
166)]
167impl<D, I> SEN65<D, embedded_interfaces::i2c::I2cDevice<I, hal::i2c::SevenBitAddress>>
168where
169    I: hal::i2c::I2c<hal::i2c::SevenBitAddress> + hal::i2c::ErrorType,
170    D: hal::delay::DelayNs,
171{
172    /// Initializes a new device with the given address on the specified bus.
173    /// This consumes the I2C bus `I`.
174    ///
175    /// Before using this device, you should call the [`Self::init`] method which
176    /// initializes the device and ensures that it is working correctly.
177    #[inline]
178    pub fn new_i2c(delay: D, interface: I, address: self::address::Address) -> Self {
179        Self {
180            delay,
181            interface: embedded_interfaces::i2c::I2cDevice::new(interface, address.into()),
182        }
183    }
184}
185
186#[forward_command_fns]
187#[sensor(
188    Pm1Concentration,
189    Pm2_5Concentration,
190    Pm4Concentration,
191    Pm10Concentration,
192    RelativeHumidity,
193    Temperature,
194    VocIndex,
195    NoxIndex
196)]
197#[maybe_async_cfg::maybe(
198    idents(
199        hal(sync = "embedded_hal", async = "embedded_hal_async"),
200        CommandInterface,
201        ResettableDevice
202    ),
203    sync(feature = "sync"),
204    async(feature = "async")
205)]
206impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> SEN65<D, I> {
207    /// Initializes the sensor by stopping any ongoing measurement, and resetting the device.
208    pub async fn init(&mut self) -> Result<(), TransportError<I::BusError>> {
209        use crate::device::ResettableDevice;
210
211        // Datasheet specifies 100ms before I2C communication may be started
212        self.delay.delay_ms(100).await;
213        self.reset().await?;
214
215        Ok(())
216    }
217}
218
219#[maybe_async_cfg::maybe(
220    idents(
221        hal(sync = "embedded_hal", async = "embedded_hal_async"),
222        CommandInterface,
223        ResettableDevice
224    ),
225    sync(feature = "sync"),
226    async(feature = "async")
227)]
228impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::device::ResettableDevice
229    for SEN65<D, I>
230{
231    type Error = TransportError<I::BusError>;
232
233    /// Resets the sensor by stopping any ongoing measurement, and resetting the device.
234    async fn reset(&mut self) -> Result<(), Self::Error> {
235        // Try to stop measurement if it is ongoing, otherwise ignore
236        let _ = self.execute::<StopMeasurement>(()).await;
237        // Reset
238        self.execute::<DeviceReset>(()).await?;
239
240        Ok(())
241    }
242}
243
244#[maybe_async_cfg::maybe(
245    idents(
246        hal(sync = "embedded_hal", async = "embedded_hal_async"),
247        CommandInterface,
248        ContinuousSensor
249    ),
250    sync(feature = "sync"),
251    async(feature = "async")
252)]
253impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::sensor::ContinuousSensor
254    for SEN65<D, I>
255{
256    type Error = TransportError<I::BusError>;
257    type Measurement = Measurement;
258
259    /// Starts continuous measurement.
260    async fn start_measuring(&mut self) -> Result<(), Self::Error> {
261        self.execute::<StartContinuousMeasurement>(()).await?;
262        Ok(())
263    }
264
265    /// Stops continuous measurement.
266    async fn stop_measuring(&mut self) -> Result<(), Self::Error> {
267        self.execute::<StopMeasurement>(()).await?;
268        Ok(())
269    }
270
271    /// Expected amount of time between measurements in microseconds.
272    async fn measurement_interval_us(&mut self) -> Result<u32, Self::Error> {
273        Ok(1_000_000)
274    }
275
276    /// Returns the most recent measurement.
277    async fn current_measurement(&mut self) -> Result<Option<Self::Measurement>, Self::Error> {
278        let measurement = self.execute::<ReadMeasuredValues>(()).await?;
279        Ok(Some(Measurement {
280            pm1_concentration: (measurement.read_raw_mass_concentration_pm1() != u16::MAX)
281                .then(|| measurement.read_mass_concentration_pm1()),
282            pm2_5_concentration: (measurement.read_raw_mass_concentration_pm2_5() != u16::MAX)
283                .then(|| measurement.read_mass_concentration_pm2_5()),
284            pm4_concentration: (measurement.read_raw_mass_concentration_pm4() != u16::MAX)
285                .then(|| measurement.read_mass_concentration_pm4()),
286            pm10_concentration: (measurement.read_raw_mass_concentration_pm10() != u16::MAX)
287                .then(|| measurement.read_mass_concentration_pm10()),
288            relative_humidity: (measurement.read_raw_relative_humidity() != i16::MAX)
289                .then(|| measurement.read_relative_humidity()),
290            temperature: (measurement.read_raw_temperature() != i16::MAX).then(|| measurement.read_temperature()),
291            voc_index: (!matches!(measurement.read_raw_voc_index(), i16::MAX | 0))
292                .then_some(measurement.read_voc_index()),
293            nox_index: (!matches!(measurement.read_raw_nox_index(), i16::MAX | 0))
294                .then_some(measurement.read_nox_index()),
295        }))
296    }
297
298    /// Check if new measurements are available.
299    async fn is_measurement_ready(&mut self) -> Result<bool, Self::Error> {
300        Ok(self.execute::<GetDataReady>(()).await?.read_data_ready() == DataReadyStatus::Ready)
301    }
302
303    /// Wait indefinitely until new measurements are available and return them. Checks whether data
304    /// is ready in intervals of 100ms.
305    async fn next_measurement(&mut self) -> Result<Self::Measurement, Self::Error> {
306        loop {
307            if self.is_measurement_ready().await? {
308                return self.current_measurement().await?.ok_or_else(|| {
309                    TransportError::Unexpected("measurement was not ready even though we expected it to be")
310                });
311            }
312            self.delay.delay_ms(100).await;
313        }
314    }
315}