embedded_devices/devices/sensirion/sen68/
mod.rs

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