embedded_devices/devices/sensirion/scd41/
mod.rs

1//! The SCD41 is a photoacoustic NDIR CO2 sensor from Sensirion's SCD4x family which features
2//! improved accuracy, large measurement range of 400 - 5000 ppm, single shot operation feature and
3//! an inbuilt SHT4x temperature and humidity sensor for measurement compensation.
4//!
5//! The SCD4x is Sensirion's second generation series of optical CO2 sensors. The sensor series
6//! builds on the photoacoustic NDIR sensing principle and Sensirion's patented PASens® and CMOSens®
7//! technology to offer high accuracy at an attractive price and small form factor.
8//!
9//! ## Usage (sync)
10//!
11//! ```rust
12//! # #[cfg(feature = "sync")] mod test {
13//! # fn test<I, D>(mut i2c: I, delay: D) -> Result<(), embedded_devices::devices::sensirion::scd41::InitError<I::Error>>
14//! # where
15//! #   I: embedded_hal::i2c::I2c + embedded_hal::i2c::ErrorType,
16//! #   D: embedded_hal::delay::DelayNs
17//! # {
18//! use embedded_devices::devices::sensirion::scd41::{SCD41Sync, address::Address};
19//! use embedded_devices::sensor::ContinuousSensorSync;
20//! use uom::si::{
21//!    mass_concentration::microgram_per_cubic_meter,
22//!    ratio::{part_per_million, percent},
23//!    thermodynamic_temperature::degree_celsius,
24//! };
25//!
26//! // Create and initialize the device
27//! let mut scd41 = SCD41Sync::new_i2c(delay, i2c, Address::Default);
28//! scd41.init()?;
29//! scd41.start_measuring()?;
30//!
31//! // Read measurements
32//! let measurement = scd41.next_measurement()?;
33//! let humidity = measurement.relative_humidity.get::<percent>();
34//! let temperature = measurement.temperature.get::<degree_celsius>();
35//! let co2 = measurement.co2_concentration.get::<part_per_million>();
36//! println!("Current measurement: {:?}%RH, {:?}°C, {:?}ppm CO₂", humidity, temperature, co2);
37//! # Ok(())
38//! # }
39//! # }
40//! ```
41//!
42//! ## Usage (async)
43//!
44//! ```rust
45//! # #[cfg(feature = "async")] mod test {
46//! # async fn test<I, D>(mut i2c: I, delay: D) -> Result<(), embedded_devices::devices::sensirion::scd41::InitError<I::Error>>
47//! # where
48//! #   I: embedded_hal_async::i2c::I2c + embedded_hal_async::i2c::ErrorType,
49//! #   D: embedded_hal_async::delay::DelayNs
50//! # {
51//! use embedded_devices::devices::sensirion::scd41::{SCD41Async, address::Address};
52//! use embedded_devices::sensor::ContinuousSensorAsync;
53//! use uom::si::{
54//!    mass_concentration::microgram_per_cubic_meter,
55//!    ratio::{part_per_million, percent},
56//!    thermodynamic_temperature::degree_celsius,
57//! };
58//!
59//! // Create and initialize the device
60//! let mut scd41 = SCD41Async::new_i2c(delay, i2c, Address::Default);
61//! scd41.init().await?;
62//! scd41.start_measuring().await?;
63//!
64//! // Read measurements
65//! let measurement = scd41.next_measurement().await?;
66//! let humidity = measurement.relative_humidity.get::<percent>();
67//! let temperature = measurement.temperature.get::<degree_celsius>();
68//! let co2 = measurement.co2_concentration.get::<part_per_million>();
69//! println!("Current measurement: {:?}%RH, {:?}°C, {:?}ppm CO₂", humidity, temperature, co2);
70//! # Ok(())
71//! # }
72//! # }
73//! ```
74
75use self::commands::{
76    GetDataReady, GetSensorVariant, MeasureSingleShot, PerformForcedRecalibration, ReadMeasurement, Reinit,
77    StartPeriodicMeasurement, StopPeriodicMeasurement, WakeUp,
78};
79use embedded_devices_derive::{forward_command_fns, sensor};
80use uom::si::f64::{Ratio, ThermodynamicTemperature};
81
82use super::{
83    commands::Crc8Error,
84    scd4x::commands::{DataReadyStatus, SensorVariant, TargetCo2Concentration},
85};
86
87pub use super::scd4x::address;
88pub mod commands;
89
90/// Any CRC or Bus related error
91pub type TransportError<E> = embedded_interfaces::TransportError<Crc8Error, E>;
92
93#[cfg_attr(feature = "defmt", derive(defmt::Format))]
94#[derive(Debug, thiserror::Error)]
95pub enum InitError<BusError> {
96    /// Transport error
97    #[error("transport error")]
98    Transport(#[from] TransportError<BusError>),
99    /// Invalid sensor variant was encountered
100    #[error("invalid sensor variant {0:?}")]
101    InvalidSensorVariant(SensorVariant),
102}
103
104/// Measurement data
105#[derive(Debug, embedded_devices_derive::Measurement)]
106pub struct Measurement {
107    /// Ambient relative humidity
108    #[measurement(RelativeHumidity)]
109    pub relative_humidity: Ratio,
110    /// Ambient temperature
111    #[measurement(Temperature)]
112    pub temperature: ThermodynamicTemperature,
113    /// Current CO₂ concentration
114    #[measurement(Co2Concentration)]
115    pub co2_concentration: Ratio,
116}
117
118/// The SCD41 is a photoacoustic NDIR CO2 sensor from Sensirion's SCD4x family which features
119/// improved accuracy, large measurement range of 400 - 5000 ppm, single shot operation feature and
120/// an inbuilt SHT4x temperature and humidity sensor for measurement compensation.
121///
122/// For a full description and usage examples, refer to the [module documentation](self).
123#[maybe_async_cfg::maybe(
124    idents(
125        hal(sync = "embedded_hal", async = "embedded_hal_async"),
126        CommandInterface,
127        I2cDevice
128    ),
129    sync(feature = "sync"),
130    async(feature = "async")
131)]
132pub struct SCD41<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> {
133    /// The delay provider
134    delay: D,
135    /// The interface to communicate with the device
136    interface: I,
137}
138
139#[maybe_async_cfg::maybe(
140    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), I2cDevice),
141    sync(feature = "sync"),
142    async(feature = "async")
143)]
144impl<D, I> SCD41<D, embedded_interfaces::i2c::I2cDevice<I, hal::i2c::SevenBitAddress>>
145where
146    I: hal::i2c::I2c<hal::i2c::SevenBitAddress> + hal::i2c::ErrorType,
147    D: hal::delay::DelayNs,
148{
149    /// Initializes a new device with the given address on the specified bus.
150    /// This consumes the I2C bus `I`.
151    ///
152    /// Before using this device, you should call the [`Self::init`] method which
153    /// initializes the device and ensures that it is working correctly.
154    #[inline]
155    pub fn new_i2c(delay: D, interface: I, address: self::address::Address) -> Self {
156        Self {
157            delay,
158            interface: embedded_interfaces::i2c::I2cDevice::new(interface, address.into()),
159        }
160    }
161}
162
163pub trait SCD41Command {}
164
165#[forward_command_fns]
166#[sensor(RelativeHumidity, Temperature, Co2Concentration)]
167#[maybe_async_cfg::maybe(
168    idents(
169        hal(sync = "embedded_hal", async = "embedded_hal_async"),
170        CommandInterface,
171        ResettableDevice
172    ),
173    sync(feature = "sync"),
174    async(feature = "async")
175)]
176impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> SCD41<D, I> {
177    /// Initializes the sensor by stopping any ongoing measurement, resetting the device and
178    /// verifying the sensor variant.
179    pub async fn init(&mut self) -> Result<(), InitError<I::BusError>> {
180        use crate::device::ResettableDevice;
181
182        // Datasheet specifies 30ms before sensor has reached idle state after power-up.
183        self.delay.delay_ms(30).await;
184        self.reset().await?;
185
186        // Verify sensor variant
187        match self.execute::<GetSensorVariant>(()).await?.read_variant() {
188            SensorVariant::r#SCD41 => Ok(()),
189            variant => Err(InitError::InvalidSensorVariant(variant)),
190        }
191    }
192
193    /// Performs forced recalibration (FRC) of the CO2 signal. See the datasheet of the SCD4x
194    /// sensor (which is used in this sensor) for details how the forced recalibration shall be
195    /// used.
196    ///
197    /// After power-on wait at least 1000 ms and after stopping a measurement 600 ms before sending
198    /// this command. This function will take about 500 ms to complete.
199    ///
200    /// Returns None if the recalibration failed, otherwise the correction in PPM.
201    pub async fn perform_forced_recalibration(
202        &mut self,
203        target_co2_concentration: Ratio,
204    ) -> Result<Option<Ratio>, TransportError<I::BusError>> {
205        let frc_correction = self
206            .execute::<PerformForcedRecalibration>(
207                TargetCo2Concentration::default().with_target_co2_concentration(target_co2_concentration),
208            )
209            .await?;
210
211        Ok((frc_correction.read_raw_correction() != u16::MAX).then(|| frc_correction.read_correction()))
212    }
213}
214
215#[maybe_async_cfg::maybe(
216    idents(
217        hal(sync = "embedded_hal", async = "embedded_hal_async"),
218        CommandInterface,
219        ResettableDevice
220    ),
221    sync(feature = "sync"),
222    async(feature = "async")
223)]
224impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::device::ResettableDevice
225    for SCD41<D, I>
226{
227    type Error = TransportError<I::BusError>;
228
229    /// Resets the sensor by stopping any ongoing measurement, and resetting the device.
230    async fn reset(&mut self) -> Result<(), Self::Error> {
231        // Try to wake the sensor up
232        let _ = self.execute::<WakeUp>(()).await;
233        // Try to stop measurement if it is ongoing, otherwise ignore
234        let _ = self.execute::<StopPeriodicMeasurement>(()).await;
235        // Reset
236        self.execute::<Reinit>(()).await?;
237        Ok(())
238    }
239}
240
241#[maybe_async_cfg::maybe(
242    idents(
243        hal(sync = "embedded_hal", async = "embedded_hal_async"),
244        CommandInterface,
245        ContinuousSensor
246    ),
247    sync(feature = "sync"),
248    async(feature = "async")
249)]
250impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::sensor::ContinuousSensor
251    for SCD41<D, I>
252{
253    type Error = TransportError<I::BusError>;
254    type Measurement = Measurement;
255
256    /// Starts continuous measurement.
257    async fn start_measuring(&mut self) -> Result<(), Self::Error> {
258        self.execute::<StartPeriodicMeasurement>(()).await?;
259        Ok(())
260    }
261
262    /// Stops continuous measurement.
263    async fn stop_measuring(&mut self) -> Result<(), Self::Error> {
264        self.execute::<StopPeriodicMeasurement>(()).await?;
265        Ok(())
266    }
267
268    /// Expected amount of time between measurements in microseconds.
269    async fn measurement_interval_us(&mut self) -> Result<u32, Self::Error> {
270        Ok(5_000_000)
271    }
272
273    /// Returns the most recent measurement.
274    async fn current_measurement(&mut self) -> Result<Option<Self::Measurement>, Self::Error> {
275        let measurement = self.execute::<ReadMeasurement>(()).await?;
276        Ok(Some(Measurement {
277            relative_humidity: measurement.read_relative_humidity(),
278            temperature: measurement.read_temperature(),
279            co2_concentration: measurement.read_co2_concentration(),
280        }))
281    }
282
283    /// Check if new measurements are available.
284    async fn is_measurement_ready(&mut self) -> Result<bool, Self::Error> {
285        Ok(self.execute::<GetDataReady>(()).await?.read_data_ready() == DataReadyStatus::Ready)
286    }
287
288    /// Wait indefinitely until new measurements are available and return them. Checks whether data
289    /// is ready in intervals of 100ms.
290    async fn next_measurement(&mut self) -> Result<Self::Measurement, Self::Error> {
291        loop {
292            if self.is_measurement_ready().await? {
293                return self.current_measurement().await?.ok_or_else(|| {
294                    TransportError::Unexpected("measurement was not ready even though we expected it to be")
295                });
296            }
297            self.delay.delay_ms(100).await;
298        }
299    }
300}
301
302#[maybe_async_cfg::maybe(
303    idents(
304        hal(sync = "embedded_hal", async = "embedded_hal_async"),
305        CommandInterface,
306        ContinuousSensor,
307        OneshotSensor
308    ),
309    sync(feature = "sync"),
310    async(feature = "async")
311)]
312impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::sensor::OneshotSensor
313    for SCD41<D, I>
314{
315    type Error = TransportError<I::BusError>;
316    type Measurement = Measurement;
317
318    /// Performs a one-shot measurement.
319    async fn measure(&mut self) -> Result<Self::Measurement, Self::Error> {
320        use crate::sensor::ContinuousSensor;
321        self.execute::<MeasureSingleShot>(()).await?;
322        self.next_measurement().await
323    }
324}