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