Skip to main content

stcc4/
lib.rs

1//! STCC4 CO2 sensor driver (no-std).
2//!
3//! This crate provides a platform agnostic driver for the Sensirion STCC4 CO2 sensor.
4//! It supports both blocking and async modes via `embedded-hal` and `embedded-hal-async`.
5//!
6//! ## Features
7//! - `async` *(default)*: Enables async support via `embedded-hal-async`.
8//! - `defmt`: Enables `defmt::Format` derives and adds `defmt` logging statements.
9//! - `serde`: Enables `serde` support for public data types (no std features).
10//!
11//! ## Usage (blocking)
12//! ```no_run
13//! use stcc4::blocking::Stcc4;
14//!
15//! # fn example<I2C, D>(i2c: I2C, delay: D)
16//! # where
17//! #     D: embedded_hal::delay::DelayNs,
18//! #     I2C: embedded_hal::i2c::I2c,
19//! # {
20//! let mut stcc4 = Stcc4::new(delay, i2c);
21//! stcc4.start_continuous_measurement().ok();
22//! let measurement = stcc4.read_measurement().ok();
23//! stcc4.stop_continuous_measurement().ok();
24//! # }
25//! ```
26//!
27//! ## Usage (async)
28//! ```no_run
29//! # fn main() {}
30//! # #[cfg(feature = "async")]
31//! # {
32//! use stcc4::asynchronous::Stcc4;
33//!
34//! # async fn example<I2C, D>(i2c: I2C, delay: D)
35//! # where
36//! #     D: embedded_hal_async::delay::DelayNs,
37//! #     I2C: embedded_hal_async::i2c::I2c,
38//! # {
39//! let mut stcc4 = Stcc4::new(delay, i2c);
40//! stcc4.start_continuous_measurement().await.ok();
41//! let measurement = stcc4.read_measurement().await.ok();
42//! stcc4.stop_continuous_measurement().await.ok();
43//! # }
44//! # }
45//! ```
46#![cfg_attr(not(test), no_std)]
47
48use crc_internal::CrcError;
49
50#[cfg(feature = "async")]
51pub mod asynchronous;
52
53pub mod blocking;
54
55mod crc_internal;
56
57/// Default I2C address.
58pub const STCC4_ADDR_DEFAULT: u8 = 0x64;
59
60/// Alternative I2C address.
61pub const STCC4_ADDR_ALT: u8 = 0x65;
62
63/// General call I2C address.
64pub const I2C_GENERAL_CALL_ADDR: u8 = 0x00;
65
66/// Exit sleep payload byte (not acknowledged by the sensor).
67pub const EXIT_SLEEP_PAYLOAD: u8 = 0x00;
68
69/// Soft reset command (general call, not acknowledged by the sensor).
70pub const SOFT_RESET_CMD: u8 = 0x06;
71
72/// The maximum number of bytes that the driver has to read for any command.
73const MAX_RX_BYTES: usize = 18;
74
75/// The maximum number of bytes that the driver has to write for any command.
76const MAX_TX_BYTES: usize = 8;
77
78/// Command ID enum (16-bit commands).
79#[repr(u16)]
80#[derive(Copy, Clone, Debug)]
81enum CommandId {
82    StartContinuousMeasurement = 0x218B,
83    StopContinuousMeasurement = 0x3F86,
84    ReadMeasurement = 0xEC05,
85    SetRhtCompensation = 0xE000,
86    SetPressureCompensation = 0xE016,
87    MeasureSingleShot = 0x219D,
88    EnterSleepMode = 0x3650,
89    PerformConditioning = 0x29BC,
90    PerformFactoryReset = 0x3632,
91    PerformSelfTest = 0x278C,
92    EnableTestingMode = 0x3FBC,
93    DisableTestingMode = 0x3F3D,
94    PerformForcedRecalibration = 0x362F,
95    GetProductId = 0x365B,
96}
97
98/// Get execution time per command id (milliseconds).
99fn get_execution_time(command: CommandId) -> u32 {
100    match command {
101        CommandId::StopContinuousMeasurement => 1_200,
102        CommandId::ReadMeasurement => 1,
103        CommandId::SetRhtCompensation => 1,
104        CommandId::SetPressureCompensation => 1,
105        CommandId::MeasureSingleShot => 500,
106        CommandId::EnterSleepMode => 1,
107        CommandId::PerformConditioning => 22_000,
108        CommandId::PerformFactoryReset => 90,
109        CommandId::PerformSelfTest => 360,
110        CommandId::PerformForcedRecalibration => 90,
111        CommandId::GetProductId => 1,
112        _ => 0,
113    }
114}
115
116/// Representing sensor measurement state.
117#[derive(Copy, Clone, Debug, PartialEq)]
118enum ModuleState {
119    Idle,
120    Measuring,
121    Sleep,
122}
123
124/// Shorthand for all functions returning an error in this module.
125type Result<T, E> = core::result::Result<T, Stcc4Error<E>>;
126
127/// Represents any error that may happen during communication.
128#[derive(Debug, Eq, PartialEq)]
129#[cfg_attr(feature = "defmt", derive(defmt::Format))]
130pub enum Stcc4Error<E> {
131    /// An error occurred while reading from the sensor.
132    ReadI2cError(E),
133    /// An error occurred while writing to the sensor.
134    WriteI2cError(E),
135    /// The sensor is in a state that does not permit this command.
136    InvalidState,
137    /// The sensor returned data which could not be parsed.
138    InvalidData,
139    /// CRC related error.
140    CrcError(CrcError),
141}
142
143impl<E> From<CrcError> for Stcc4Error<E> {
144    fn from(e: CrcError) -> Self {
145        Stcc4Error::CrcError(e)
146    }
147}
148
149/// Represents a measured sample from the sensor.
150#[derive(Clone, Debug)]
151#[cfg_attr(feature = "defmt", derive(defmt::Format))]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct Measurement {
154    /// CO2 concentration in ppm.
155    pub co2_ppm: u16,
156    /// Temperature in degrees Celsius.
157    pub temperature_c: f32,
158    /// Relative humidity in percent.
159    pub humidity_percent: f32,
160    /// Sensor status word.
161    pub status: SensorStatus,
162}
163
164/// Sensor status (16-bit word).
165#[derive(Copy, Clone, Debug, PartialEq, Eq)]
166#[cfg_attr(feature = "defmt", derive(defmt::Format))]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168pub struct SensorStatus {
169    /// Raw status word.
170    pub raw: u16,
171}
172
173impl SensorStatus {
174    /// Whether the sensor is in testing mode.
175    pub fn testing_mode(&self) -> bool {
176        (self.raw & 0x0040) != 0
177    }
178}
179
180/// Self test result (raw word + helpers).
181#[derive(Copy, Clone, Debug, PartialEq, Eq)]
182#[cfg_attr(feature = "defmt", derive(defmt::Format))]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184pub struct SelfTestResult {
185    /// Raw self-test word.
186    pub raw: u16,
187}
188
189impl SelfTestResult {
190    /// Whether the self test is considered a success.
191    pub fn is_success(&self) -> bool {
192        self.raw == 0x0000 || self.raw == 0x0010
193    }
194
195    /// Supply voltage out of range.
196    pub fn supply_voltage_out_of_range(&self) -> bool {
197        (self.raw & 0x0001) != 0
198    }
199
200    /// SHT not connected through STCC4 I2C controller interface pad.
201    pub fn sht_missing(&self) -> bool {
202        (self.raw & 0x0010) != 0
203    }
204
205    /// Memory error (bits 6:5).
206    pub fn memory_error(&self) -> bool {
207        (self.raw & 0x0060) != 0
208    }
209
210    /// Debug bits (3:1) for manufacturer diagnostics.
211    pub fn debug_bits(&self) -> u8 {
212        ((self.raw >> 1) & 0x0007) as u8
213    }
214}
215
216/// Forced recalibration correction (ppm).
217#[derive(Copy, Clone, Debug, PartialEq, Eq)]
218#[cfg_attr(feature = "defmt", derive(defmt::Format))]
219#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
220pub struct FrcCorrection(pub i16);
221
222/// Product ID and serial number.
223#[derive(Copy, Clone, Debug, PartialEq, Eq)]
224#[cfg_attr(feature = "defmt", derive(defmt::Format))]
225#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
226pub struct ProductId {
227    /// 32-bit product ID.
228    pub product_id: u32,
229    /// 64-bit unique serial number.
230    pub serial_number: u64,
231}
232
233#[inline]
234fn raw_to_humidity_percent(raw: u16) -> f32 {
235    125.0 * (raw as f32) / 65_535.0 - 6.0
236}
237
238#[inline]
239fn raw_to_temperature_c(raw: u16) -> f32 {
240    175.0 * (raw as f32) / 65_535.0 - 45.0
241}
242
243#[inline]
244fn clamp_u16(value: f32) -> u16 {
245    if value <= 0.0 {
246        0
247    } else if value >= u16::MAX as f32 {
248        u16::MAX
249    } else {
250        (value + 0.5) as u16
251    }
252}
253
254#[inline]
255fn humidity_percent_to_raw(rh_percent: f32) -> u16 {
256    clamp_u16(((rh_percent + 6.0) * 65_535.0) / 125.0)
257}
258
259#[inline]
260fn temperature_c_to_raw(temperature_c: f32) -> u16 {
261    clamp_u16(((temperature_c + 45.0) * 65_535.0) / 175.0)
262}
263
264#[inline]
265fn pressure_pa_to_raw(pressure_pa: u32) -> u16 {
266    let raw = (pressure_pa / 2) as f32;
267    clamp_u16(raw)
268}
269
270#[inline]
271fn frc_correction_from_raw(raw: u16) -> FrcCorrection {
272    FrcCorrection((raw as i32 - 32_768) as i16)
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    /// Verifies temperature conversion round-trip accuracy.
281    fn conversion_roundtrip_temperature() {
282        let t_c = 25.0_f32;
283        let raw = temperature_c_to_raw(t_c);
284        let out = raw_to_temperature_c(raw);
285        assert!((out - t_c).abs() < 0.5);
286    }
287
288    #[test]
289    /// Verifies humidity conversion round-trip accuracy.
290    fn conversion_roundtrip_humidity() {
291        let rh = 50.0_f32;
292        let raw = humidity_percent_to_raw(rh);
293        let out = raw_to_humidity_percent(raw);
294        assert!((out - rh).abs() < 0.5);
295    }
296
297    #[test]
298    /// Verifies pressure input conversion to raw ticks.
299    fn pressure_conversion() {
300        let pressure_pa = 101_300_u32;
301        let raw = pressure_pa_to_raw(pressure_pa);
302        assert_eq!(raw, 50_650);
303    }
304
305    #[test]
306    /// Verifies forced recalibration correction conversion.
307    fn frc_correction_conversion() {
308        let raw = 32_668_u16;
309        let correction = frc_correction_from_raw(raw);
310        assert_eq!(correction.0, -100);
311    }
312
313    #[test]
314    /// Verifies testing mode bit decoding.
315    fn sensor_status_testing_mode() {
316        let status = SensorStatus { raw: 0x0040 };
317        assert!(status.testing_mode());
318
319        let status = SensorStatus { raw: 0x0000 };
320        assert!(!status.testing_mode());
321    }
322
323    #[test]
324    /// Verifies self-test helper flag decoding.
325    fn self_test_helpers() {
326        let result = SelfTestResult { raw: 0x0001 };
327        assert!(!result.is_success());
328        assert!(result.supply_voltage_out_of_range());
329
330        let result = SelfTestResult { raw: 0x0010 };
331        assert!(result.is_success());
332        assert!(result.sht_missing());
333
334        let result = SelfTestResult { raw: 0x0060 };
335        assert!(result.memory_error());
336
337        let result = SelfTestResult { raw: 0x000E };
338        assert_eq!(result.debug_bits(), 0x07);
339    }
340
341    #[test]
342    /// Verifies clamp bounds at low and high inputs.
343    fn clamp_u16_bounds() {
344        assert_eq!(clamp_u16(-10.0), 0);
345        assert_eq!(clamp_u16(100_000.0), u16::MAX);
346    }
347
348    #[test]
349    /// Verifies selected execution time mappings.
350    fn execution_time_mapping() {
351        assert_eq!(
352            get_execution_time(CommandId::StopContinuousMeasurement),
353            1_200
354        );
355        assert_eq!(get_execution_time(CommandId::PerformConditioning), 22_000);
356        assert_eq!(get_execution_time(CommandId::StartContinuousMeasurement), 0);
357    }
358}