embedded_devices/devices/sensirion/sen63c/
mod.rs

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