Skip to main content

sen6x/
lib.rs

1//! This library provides an embedded `no_std` driver for the [Sensirion SEN6x series](https://sensirion.com/media/documents/FAFC548D/693FBB15/PS_DS_SEN6x.pdf).
2//! This driver is compatible with `embedded-hal` v1.0.
3//!
4//! # Errors
5//!
6//! Every command method returns [`Result`]`<_, `[`Error`]`<E>>`, where `E` is the
7//! I²C bus error type of the underlying `embedded-hal` implementation. The
8//! [`Error`] variants are:
9//!
10//! - [`Error::I2c`] — the underlying I²C transfer failed; the bus error is wrapped.
11//! - [`Error::Crc`] — a CRC-8 checksum in the sensor's response did not match, indicating a corrupted read.
12//! - [`Error::NotAllowed`] — the command is not permitted in the sensor's current
13//!   state (for example, reading measured values while idle, or applying a
14//!   configuration that is only accepted during measurement).
15//! - [`Error::InvalidValue`] — the sensor returned a value outside its defined range.
16//!
17//! Commands that read a response may return any of the four variants. Commands
18//! that only send (with no response) return [`Error::NotAllowed`] or [`Error::I2c`].
19//! Each command method also documents its own `# Errors` section.
20#![cfg_attr(not(test), no_std)]
21
22pub mod commands;
23
24mod errors;
25
26use core::cell::RefCell;
27pub use errors::Error;
28
29mod connection;
30#[cfg(feature = "embedded-hal-async")]
31mod connection_async;
32#[cfg(feature = "embedded-hal")]
33mod connection_sync;
34mod io;
35pub mod types;
36
37use crate::connection::State;
38#[cfg(any(feature = "embedded-hal", feature = "embedded-hal-async"))]
39use crate::types::Milliseconds;
40#[cfg(feature = "embassy")]
41use embassy_sync::mutex::Mutex;
42
43/// Driver for a Sensirion SEN6x air-quality sensor over I²C.
44///
45/// Construct one with [`Sen6x::new`], then drive it through the model-specific
46/// command trait for your sensor (e.g. [`Sen66Commands`] / [`Sen66CommandsAsync`]).
47/// The driver tracks whether the sensor is idle or measuring and rejects commands
48/// that are not valid in the current state (see the crate-level `# Errors` docs).
49///
50/// # Thread safety
51/// `Sen6x` uses [`core::cell::RefCell`] for interior mutability, so it is
52/// [`Send`] but not [`Sync`]. The driver is intended to be owned by a single
53/// task; sharing one instance across threads is not supported. For shared-bus
54/// setups, use the `embassy` feature, which guards the bus with a `Mutex`.
55#[derive(Debug)]
56pub struct Sen6x<'a, I2C, D> {
57    i2c: I2C,
58    delay: RefCell<&'a mut D>,
59    state: State,
60}
61
62#[cfg(feature = "embedded-hal")]
63trait SensorConnectionSync {
64    type I2c: embedded_hal::i2c::I2c<Error = Self::Error>;
65    type Delay: embedded_hal::delay::DelayNs;
66    type Error;
67    fn transaction<R>(&self, f: impl FnOnce(&mut Self::I2c) -> R) -> R;
68    fn delay(&self, delay: Milliseconds);
69}
70
71#[cfg(feature = "embedded-hal-async")]
72trait SensorConnectionAsync {
73    type I2c: embedded_hal_async::i2c::I2c<Error = Self::Error>;
74    type Delay: embedded_hal_async::delay::DelayNs;
75    type Error;
76    async fn transaction<R>(&self, f: impl AsyncFnOnce(&mut Self::I2c) -> R) -> R;
77    async fn delay(&self, delay: Milliseconds);
78}
79
80trait SensorState<E> {
81    fn check_state(&self, valid_in: &[State]) -> Result<(), crate::Error<E>>;
82
83    fn state(&mut self) -> &mut State;
84}
85
86impl<'a, I2C, D, E> SensorState<E> for Sen6x<'a, I2C, D> {
87    fn check_state(&self, valid_in: &[State]) -> Result<(), crate::Error<E>> {
88        if !valid_in.contains(&self.state) {
89            return Err(crate::Error::NotAllowed);
90        }
91        Ok(())
92    }
93
94    fn state(&mut self) -> &mut State {
95        &mut self.state
96    }
97}
98
99#[cfg(feature = "embassy")]
100impl<'a, M, I2C, D, E> SensorConnectionAsync
101    for Sen6x<'a, &'a embassy_sync::mutex::Mutex<M, I2C>, D>
102where
103    I2C: embedded_hal_async::i2c::I2c<Error = E>,
104    M: embassy_sync::blocking_mutex::raw::RawMutex,
105    D: embedded_hal_async::delay::DelayNs,
106{
107    type I2c = I2C;
108    type Error = E;
109    type Delay = D;
110
111    async fn transaction<R>(&self, f: impl AsyncFnOnce(&mut I2C) -> R) -> R {
112        let mut i2c = self.i2c.lock().await;
113        f(&mut *i2c).await
114    }
115
116    // The delay future borrows the timer across the await. Sound because
117    // `Sen6x` is a single-owner, `!Sync` driver (see "Thread safety" above),
118    // so no concurrent borrow of this cell can exist.
119    #[allow(clippy::await_holding_refcell_ref)]
120    async fn delay(&self, delay: Milliseconds) {
121        let mut d = self.delay.borrow_mut();
122        d.delay_ms(delay as u32).await;
123    }
124}
125
126mod sealed {
127    pub trait Sealed {}
128    #[cfg(any(feature = "embedded-hal", feature = "embedded-hal-async"))]
129    impl<I2C> Sealed for &mut I2C {}
130    #[cfg(feature = "embassy")]
131    impl<M, I2C> Sealed for &embassy_sync::mutex::Mutex<M, I2C> where
132        M: embassy_sync::blocking_mutex::raw::RawMutex
133    {
134    }
135}
136
137/// Conversion of an I²C bus handle into the connection type stored by [`Sen6x`].
138/// This is an implementation detail of [`Sen6x::new`]
139#[doc(hidden)]
140pub trait IntoI2cConnection<'a>: sealed::Sealed {
141    type Connection;
142    fn into_i2c_connection(self) -> Self::Connection;
143}
144
145#[cfg(any(feature = "embedded-hal", feature = "embedded-hal-async"))]
146impl<'a, I2C: 'a> IntoI2cConnection<'a> for &'a mut I2C {
147    type Connection = RefCell<&'a mut I2C>;
148    fn into_i2c_connection(self) -> Self::Connection {
149        RefCell::new(self)
150    }
151}
152
153#[cfg(feature = "embassy")]
154impl<'a, M, I2C> IntoI2cConnection<'a> for &'a Mutex<M, I2C>
155where
156    M: embassy_sync::blocking_mutex::raw::RawMutex,
157{
158    type Connection = &'a Mutex<M, I2C>;
159    fn into_i2c_connection(self) -> Self::Connection {
160        self
161    }
162}
163
164impl<'a, C, D> Sen6x<'a, C, D> {
165    /// Creates a driver from an I²C bus and a delay provider.
166    ///
167    /// `i2c` is either an exclusive `&mut` to an [`embedded_hal::i2c::I2c`] /
168    /// [`embedded_hal_async::i2c::I2c`] implementation, or — with the `embassy`
169    /// feature — a shared `&embassy_sync::mutex::Mutex<_, I2C>` for buses shared
170    /// with other drivers. `delay` provides the post-command wait each operation
171    /// needs. The sensor starts in the idle state.
172    ///
173    /// # Example
174    ///
175    #[cfg_attr(feature = "embedded-hal", doc = "```")]
176    #[cfg_attr(
177        feature = "embedded-hal",
178        doc = "use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction};"
179    )]
180    #[cfg_attr(
181        feature = "embedded-hal",
182        doc = "use embedded_hal_mock::eh1::delay::NoopDelay;"
183    )]
184    #[cfg_attr(feature = "embedded-hal", doc = "use sen6x::{Sen6x, Sen66Commands};")]
185    #[cfg_attr(feature = "embedded-hal", doc = "")]
186    #[cfg_attr(
187        feature = "embedded-hal",
188        doc = "// The SEN6x lives at I²C address 0x6B; starting a measurement writes command 0x0021."
189    )]
190    #[cfg_attr(
191        feature = "embedded-hal",
192        doc = "let mut i2c = I2cMock::new(&[Transaction::write(0x6B, vec![0x00, 0x21])]);"
193    )]
194    #[cfg_attr(feature = "embedded-hal", doc = "let mut delay = NoopDelay::new();")]
195    #[cfg_attr(feature = "embedded-hal", doc = "")]
196    #[cfg_attr(
197        feature = "embedded-hal",
198        doc = "let mut sensor = Sen6x::new(&mut i2c, &mut delay);"
199    )]
200    #[cfg_attr(
201        feature = "embedded-hal",
202        doc = "sensor.start_continuous_measurement()?;"
203    )]
204    #[cfg_attr(feature = "embedded-hal", doc = "")]
205    #[cfg_attr(
206        feature = "embedded-hal",
207        doc = "i2c.done(); // all expected I²C traffic happened"
208    )]
209    #[cfg_attr(
210        feature = "embedded-hal",
211        doc = "# Ok::<(), sen6x::Error<embedded_hal::i2c::ErrorKind>>(())"
212    )]
213    #[cfg_attr(feature = "embedded-hal", doc = "```")]
214    pub fn new<I2C>(i2c: I2C, delay: &'a mut D) -> Self
215    where
216        I2C: IntoI2cConnection<'a, Connection = C>,
217    {
218        Self {
219            i2c: i2c.into_i2c_connection(),
220            delay: RefCell::new(delay),
221            state: State::Idle,
222        }
223    }
224}
225
226#[cfg(feature = "embedded-hal")]
227pub use commands::{
228    Sen62Commands, Sen63cCommands, Sen65Commands, Sen66Commands, Sen68Commands, Sen69cCommands,
229};
230#[cfg(feature = "embedded-hal-async")]
231pub use commands::{
232    Sen62CommandsAsync, Sen63cCommandsAsync, Sen65CommandsAsync, Sen66CommandsAsync,
233    Sen68CommandsAsync, Sen69cCommandsAsync,
234};
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    #[test]
240    fn sen6x_is_send() {
241        fn assert_send<T: Send>() {}
242        assert_send::<Sen6x<'static, RefCell<&'static mut u8>, u8>>();
243    }
244}