Skip to main content

scd41_embedded/
scd41.rs

1use embedded_hal::delay::DelayNs;
2use embedded_hal::i2c::I2c;
3
4use crate::crc::crc8;
5use crate::types::{Measurement, RawMeasurement};
6use crate::Error;
7
8/// Default 7-bit I2C address of the SCD41.
9pub const DEFAULT_ADDRESS: u8 = 0x62;
10
11/// SCD41 command set.
12///
13/// Values and timings are consistent with common community drivers. Usually, users
14/// do not need to interact with this enum directly.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[repr(u16)]
17enum Command {
18    StartPeriodicMeasurement = 0x21B1,
19    ReadMeasurement = 0xEC05,
20    StopPeriodicMeasurement = 0x3F86,
21    GetDataReadyStatus = 0xE4B8,
22    GetSerialNumber = 0x3682,
23
24    // SCD41-specific
25    MeasureSingleShot = 0x219D,
26    MeasureSingleShotRhtOnly = 0x2196,
27    PowerDown = 0x36E0,
28    WakeUp = 0x36F6,
29
30    // Configuration
31    SetTemperatureOffset = 0x241D,
32    GetTemperatureOffset = 0x2318,
33    SetSensorAltitude = 0x2427,
34    GetSensorAltitude = 0x2322,
35    SetAmbientPressure = 0xE000,
36    PersistSettings = 0x3615,
37    Reinit = 0x3646,
38    PerformSelfTest = 0x3639,
39    PerformFactoryReset = 0x3632,
40    PerformForcedRecalibration = 0x362F,
41    SetAutomaticSelfCalibrationEnabled = 0x2416,
42    GetAutomaticSelfCalibrationEnabled = 0x2313,
43}
44
45impl Command {
46    const fn delay_ms(self) -> u32 {
47        match self {
48            Self::StartPeriodicMeasurement => 1,
49            Self::ReadMeasurement => 1,
50            Self::StopPeriodicMeasurement => 500,
51            Self::GetDataReadyStatus => 1,
52            Self::GetSerialNumber => 1,
53            Self::MeasureSingleShot => 5000,
54            Self::MeasureSingleShotRhtOnly => 50,
55            Self::PowerDown => 1,
56            Self::WakeUp => 20,
57            Self::SetTemperatureOffset => 1,
58            Self::GetTemperatureOffset => 1,
59            Self::SetSensorAltitude => 1,
60            Self::GetSensorAltitude => 1,
61            Self::SetAmbientPressure => 1,
62            Self::PersistSettings => 800,
63            Self::Reinit => 20,
64            Self::PerformSelfTest => 10_000,
65            Self::PerformFactoryReset => 1200,
66            Self::PerformForcedRecalibration => 400,
67            Self::SetAutomaticSelfCalibrationEnabled => 1,
68            Self::GetAutomaticSelfCalibrationEnabled => 1,
69        }
70    }
71}
72
73/// Blocking SCD41 driver. This is your main interface to the sensor.
74///
75/// ## Example
76///
77/// ```rust
78///  use scd41_embedded::{Error, Scd41};
79///
80///  fn example<I2C, D, E>(i2c: I2C, delay: D) -> Result<(), Error<E>>
81///  where
82///      I2C: embedded_hal::i2c::I2c<Error = E>,
83///      D: embedded_hal::delay::DelayNs,
84///      E: embedded_hal::i2c::Error,
85///  {
86///      let mut scd = Scd41::new(i2c, delay);
87///      scd.start_periodic_measurement()?;
88///     // ... do some other operations
89///  }
90/// ```
91#[derive(Debug)]
92pub struct Scd41<I2C, D> {
93    i2c: I2C,
94    delay: D,
95    address: u8,
96    is_running: bool,
97}
98
99impl<I2C, D> Scd41<I2C, D> {
100    /// Create a new driver using the default I2C address (0x62).
101    #[must_use]
102    pub fn new(i2c: I2C, delay: D) -> Self {
103        Self::new_with_address(i2c, delay, DEFAULT_ADDRESS)
104    }
105
106    /// Create a new driver using a custom I2C address.
107    #[must_use]
108    pub fn new_with_address(i2c: I2C, delay: D, address: u8) -> Self {
109        Self {
110            i2c,
111            delay,
112            address,
113            is_running: false,
114        }
115    }
116
117    /// Destroy the driver and return the underlying I2C bus.
118    #[must_use]
119    pub fn destroy(self) -> I2C {
120        self.i2c
121    }
122}
123
124impl<I2C, D, E> Scd41<I2C, D>
125where
126    I2C: I2c<Error = E>,
127    D: DelayNs,
128{
129    /// Start periodic measurement (data update ~ every 5 seconds).
130    ///
131    /// ## Use Case
132    /// You use periodic measurement when your application needs continuous, automatically updated CO₂ data with minimal firmware control and good real-time responsiveness. Some examples of this would be:
133    /// - You need fast response to CO₂ changes
134    /// - Power consumption is not critical
135    /// - You want simpler firmware
136    /// - You want automatic self-calibration behavior
137    pub fn start_periodic_measurement(&mut self) -> Result<(), Error<E>> {
138        self.write_command(Command::StartPeriodicMeasurement)?;
139        self.is_running = true;
140        Ok(())
141    }
142
143    /// Stop periodic measurement.
144    pub fn stop_periodic_measurement(&mut self) -> Result<(), Error<E>> {
145        self.write_command(Command::StopPeriodicMeasurement)?;
146        self.is_running = false;
147        Ok(())
148    }
149
150    /// Returns `true` if a new measurement is available.
151    pub fn data_ready(&mut self) -> Result<bool, Error<E>> {
152        let status = self.read_u16(Command::GetDataReadyStatus)?;
153        Ok((status & 0x07FF) != 0)
154    }
155
156    /// Read the latest measurement and returns the processed values. This will fail if the periodic measurement has not been started via [`start_periodic_measurement`](Self::start_periodic_measurement).
157    pub fn measurement(&mut self) -> Result<Measurement, Error<E>> {
158        let raw = self.raw_measurement()?;
159        Ok(Measurement::from_raw(raw))
160    }
161
162    /// Read raw measurement words.
163    ///
164    /// Returns CO₂ in ppm, raw temperature ticks, and raw humidity ticks. A typical user should use the [`measurement`](Self::measurement) method instead.
165    pub fn raw_measurement(&mut self) -> Result<RawMeasurement, Error<E>> {
166        let mut buf = [0u8; 9];
167        self.read_words(Command::ReadMeasurement, &mut buf)?;
168
169        let co2 = u16::from_be_bytes([buf[0], buf[1]]);
170        let t = u16::from_be_bytes([buf[3], buf[4]]);
171        let rh = u16::from_be_bytes([buf[6], buf[7]]);
172
173        Ok(RawMeasurement {
174            co2_ppm: co2,
175            temperature_ticks: t,
176            humidity_ticks: rh,
177        })
178    }
179
180    /// Perform a single-shot measurement (takes ~5 seconds).
181    /// After performing the measurement, read the results with [`measurement`](Self::measurement) or [`raw_measurement`](Self::raw_measurement).
182    pub fn measure_single_shot(&mut self) -> Result<(), Error<E>> {
183        self.write_command(Command::MeasureSingleShot)
184    }
185
186    /// Perform a single-shot temperature+humidity-only measurement.
187    pub fn measure_single_shot_rht_only(&mut self) -> Result<(), Error<E>> {
188        self.write_command(Command::MeasureSingleShotRhtOnly)
189    }
190
191    /// Put the sensor to sleep. This operation takes about 1ms.
192    ///
193    /// Power-cycled mode only makes sense if sampling periods are > ~380 s (~6 min).
194    pub fn power_down(&mut self) -> Result<(), Error<E>> {
195        self.write_command(Command::PowerDown)
196    }
197
198    /// Wake the sensor. This operation takes about 20ms on the sensor. This function will not wait for the full wake-up time but rather simply initialize the wake-up command.
199    /// Note that after calling this function, you should wait at least 20ms and discard the first measurement results.
200    pub fn wake_up_best_effort(&mut self) {
201        let _ = self.write_command(Command::WakeUp);
202    }
203
204    /// Read 48-bit serial number.
205    pub fn serial_number(&mut self) -> Result<u64, Error<E>> {
206        let mut buf = [0u8; 9];
207        self.read_words(Command::GetSerialNumber, &mut buf)?;
208
209        let w0 = u16::from_be_bytes([buf[0], buf[1]]) as u64;
210        let w1 = u16::from_be_bytes([buf[3], buf[4]]) as u64;
211        let w2 = u16::from_be_bytes([buf[6], buf[7]]) as u64;
212
213        Ok((w0 << 32) | (w1 << 16) | w2)
214    }
215
216    /// Get temperature offset in °C.
217    pub fn temperature_offset(&mut self) -> Result<f32, Error<E>> {
218        let raw = self.read_u16(Command::GetTemperatureOffset)?;
219        Ok((raw as f32) * 175.0 / 65536.0)
220    }
221
222    /// Set temperature offset in °C.
223    pub fn set_temperature_offset(&mut self, offset_c: f32) -> Result<(), Error<E>> {
224        if !(0.0..=175.0).contains(&offset_c) {
225            return Err(Error::InvalidInput);
226        }
227        let raw = ((offset_c * 65536.0) / 175.0) as u16;
228        self.write_command_with_u16(Command::SetTemperatureOffset, raw)
229    }
230
231    /// Get configured altitude (m above sea level).
232    pub fn altitude(&mut self) -> Result<u16, Error<E>> {
233        self.read_u16(Command::GetSensorAltitude)
234    }
235
236    /// Set configured altitude (m above sea level).
237    pub fn set_altitude(&mut self, meters: u16) -> Result<(), Error<E>> {
238        self.write_command_with_u16(Command::SetSensorAltitude, meters)
239    }
240
241    /// Set ambient pressure in hPa.
242    pub fn set_ambient_pressure(&mut self, pressure_hpa: u16) -> Result<(), Error<E>> {
243        self.write_command_with_u16(Command::SetAmbientPressure, pressure_hpa)
244    }
245
246    /// Enable/disable ASC.
247    ///
248    /// Note that the ASC will not work when you power-cycle between readings. Long-term accuracy may drift unless you manually recalibrate.
249    pub fn set_automatic_self_calibration(&mut self, enabled: bool) -> Result<(), Error<E>> {
250        self.write_command_with_u16(Command::SetAutomaticSelfCalibrationEnabled, enabled as u16)
251    }
252
253    /// Get ASC enabled/disabled.
254    pub fn automatic_self_calibration(&mut self) -> Result<bool, Error<E>> {
255        Ok(self.read_u16(Command::GetAutomaticSelfCalibrationEnabled)? != 0)
256    }
257
258    /// Persist settings to EEPROM.
259    pub fn persist_settings(&mut self) -> Result<(), Error<E>> {
260        self.write_command(Command::PersistSettings)
261    }
262
263    /// Reinitialize by reloading user settings from EEPROM.
264    pub fn reinit(&mut self) -> Result<(), Error<E>> {
265        self.write_command(Command::Reinit)
266    }
267
268    /// Run the built-in self test. Returns true if OK.
269    pub fn self_test_is_ok(&mut self) -> Result<bool, Error<E>> {
270        Ok(self.read_u16(Command::PerformSelfTest)? == 0)
271    }
272
273    /// Factory reset.
274    pub fn factory_reset(&mut self) -> Result<(), Error<E>> {
275        self.write_command(Command::PerformFactoryReset)
276    }
277
278    /// Perform forced recalibration.
279    ///
280    /// Returns the correction value.
281    pub fn forced_recalibration(&mut self, target_co2_ppm: u16) -> Result<u16, Error<E>> {
282        self.write_command_with_u16(Command::PerformForcedRecalibration, target_co2_ppm)?;
283        self.read_u16_raw_response()
284    }
285
286    fn write_command(&mut self, cmd: Command) -> Result<(), Error<E>> {
287        // Enforce basic state rules similar to reference drivers.
288        let allowed_while_running = matches!(
289            cmd,
290            Command::ReadMeasurement
291                | Command::GetDataReadyStatus
292                | Command::StopPeriodicMeasurement
293        );
294        if self.is_running && !allowed_while_running {
295            // Keep this strict to avoid confusing sensor state.
296            return Err(Error::InvalidInput);
297        }
298
299        let word = (cmd as u16).to_be_bytes();
300        self.i2c.write(self.address, &word).map_err(Error::I2c)?;
301        self.delay.delay_ms(cmd.delay_ms());
302        Ok(())
303    }
304
305    fn write_command_with_u16(&mut self, cmd: Command, data: u16) -> Result<(), Error<E>> {
306        let cmd_bytes = (cmd as u16).to_be_bytes();
307        let data_bytes = data.to_be_bytes();
308        let crc = crc8(&data_bytes);
309        let payload = [
310            cmd_bytes[0],
311            cmd_bytes[1],
312            data_bytes[0],
313            data_bytes[1],
314            crc,
315        ];
316
317        self.i2c.write(self.address, &payload).map_err(Error::I2c)?;
318        self.delay.delay_ms(cmd.delay_ms());
319        Ok(())
320    }
321
322    fn read_u16(&mut self, cmd: Command) -> Result<u16, Error<E>> {
323        self.write_command(cmd)?;
324        self.read_u16_raw_response()
325    }
326
327    fn read_u16_raw_response(&mut self) -> Result<u16, Error<E>> {
328        let mut buf = [0u8; 3];
329        self.i2c.read(self.address, &mut buf).map_err(Error::I2c)?;
330        if crc8(&buf[0..2]) != buf[2] {
331            return Err(Error::Crc);
332        }
333        Ok(u16::from_be_bytes([buf[0], buf[1]]))
334    }
335
336    fn read_words(&mut self, cmd: Command, buf: &mut [u8]) -> Result<(), Error<E>> {
337        self.write_command(cmd)?;
338        self.i2c.read(self.address, buf).map_err(Error::I2c)?;
339
340        // Validate CRC for each word.
341        if !buf.len().is_multiple_of(3) {
342            return Err(Error::UnexpectedResponse);
343        }
344        for chunk in buf.chunks_exact(3) {
345            if crc8(&chunk[0..2]) != chunk[2] {
346                return Err(Error::Crc);
347            }
348        }
349        Ok(())
350    }
351}