embedded_devices/devices/sensirion/scd40/mod.rs
1//! The SCD40 is a photoacoustic NDIR CO2 sensor from Sensirion's SCD4x family which features base
2//! accuracy, measurement range of 400 - 2000 ppm and an inbuilt SHT4x temperature and humidity
3//! 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::scd40::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::scd40::{SCD40Sync, 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 scd40 = SCD40Sync::new_i2c(delay, i2c, Address::Default);
28//! scd40.init()?;
29//! scd40.start_measuring()?;
30//!
31//! // Read measurements
32//! let measurement = scd40.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::scd40::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::scd40::{SCD40Async, 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 scd40 = SCD40Async::new_i2c(delay, i2c, Address::Default);
61//! scd40.init().await?;
62//! scd40.start_measuring().await?;
63//!
64//! // Read measurements
65//! let measurement = scd40.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, PerformForcedRecalibration, ReadMeasurement, Reinit, StartPeriodicMeasurement,
77 StopPeriodicMeasurement,
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 SCD40 is a photoacoustic NDIR CO2 sensor from Sensirion's SCD4x family which features base
119/// accuracy, measurement range of 400 - 2000 ppm and an inbuilt SHT4x temperature and humidity
120/// 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 SCD40<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> SCD40<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 SCD40Command {}
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> SCD40<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#SCD40 => 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 SCD40<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 stop measurement if it is ongoing, otherwise ignore
232 let _ = self.execute::<StopPeriodicMeasurement>(()).await;
233 // Reset
234 self.execute::<Reinit>(()).await?;
235 Ok(())
236 }
237}
238
239#[maybe_async_cfg::maybe(
240 idents(
241 hal(sync = "embedded_hal", async = "embedded_hal_async"),
242 CommandInterface,
243 ContinuousSensor
244 ),
245 sync(feature = "sync"),
246 async(feature = "async")
247)]
248impl<D: hal::delay::DelayNs, I: embedded_interfaces::commands::CommandInterface> crate::sensor::ContinuousSensor
249 for SCD40<D, I>
250{
251 type Error = TransportError<I::BusError>;
252 type Measurement = Measurement;
253
254 /// Starts continuous measurement.
255 async fn start_measuring(&mut self) -> Result<(), Self::Error> {
256 self.execute::<StartPeriodicMeasurement>(()).await?;
257 Ok(())
258 }
259
260 /// Stops continuous measurement.
261 async fn stop_measuring(&mut self) -> Result<(), Self::Error> {
262 self.execute::<StopPeriodicMeasurement>(()).await?;
263 Ok(())
264 }
265
266 /// Expected amount of time between measurements in microseconds.
267 async fn measurement_interval_us(&mut self) -> Result<u32, Self::Error> {
268 Ok(5_000_000)
269 }
270
271 /// Returns the most recent measurement.
272 async fn current_measurement(&mut self) -> Result<Option<Self::Measurement>, Self::Error> {
273 let measurement = self.execute::<ReadMeasurement>(()).await?;
274 Ok(Some(Measurement {
275 relative_humidity: measurement.read_relative_humidity(),
276 temperature: measurement.read_temperature(),
277 co2_concentration: measurement.read_co2_concentration(),
278 }))
279 }
280
281 /// Check if new measurements are available.
282 async fn is_measurement_ready(&mut self) -> Result<bool, Self::Error> {
283 Ok(self.execute::<GetDataReady>(()).await?.read_data_ready() == DataReadyStatus::Ready)
284 }
285
286 /// Wait indefinitely until new measurements are available and return them. Checks whether data
287 /// is ready in intervals of 100ms.
288 async fn next_measurement(&mut self) -> Result<Self::Measurement, Self::Error> {
289 loop {
290 if self.is_measurement_ready().await? {
291 return self.current_measurement().await?.ok_or_else(|| {
292 TransportError::Unexpected("measurement was not ready even though we expected it to be")
293 });
294 }
295 self.delay.delay_ms(100).await;
296 }
297 }
298}