sds011/
lib.rs

1//! This crate implements a driver for the SDS011 particle sensor based on
2//! [`embedded-hal`](https://github.com/rust-embedded/embedded-hal).
3//! Thanks to this abstraction layer, it can be used on full-fledged operating
4//! systems as well as embedded devices.
5//!
6//! # Features
7//! * `sync`: To use the synchronous interface, enable this feature.
8//!   By default, this library exposes an async API.
9//!
10//! # Examples
11//! The crate ships with two small CLI examples that utilize the library:
12//! * [`cli.rs`](examples/cli.rs) uses the synchronous interface (embedded-io),
13//! * [`cli_async.rs`](examples/cli_async.rs) uses the asynchronous interface
14//!   (embedded-io-async).
15//!
16//! The example below demonstrates how to use the sensor with an ESP32,
17//! showcasing the strength of the embedded-hal abstractions.
18//!
19//! ```ignore
20//! #![no_std]
21//! #![no_main]
22//!
23//! use embassy_executor::Spawner;
24//! use embassy_time::{Duration, Timer, Delay};
25//! use esp_backtrace as _;
26//! use esp_hal::{
27//!     clock::ClockControl,
28//!     gpio::Io,
29//!     peripherals::Peripherals,
30//!     prelude::*,
31//!     system::SystemControl,
32//!     timer::timg::TimerGroup,
33//!     uart::{config::Config, TxRxPins, Uart},
34//! };
35//! use esp_println::println;
36//! use sds011::SDS011;
37//!
38//! #[main]
39//! async fn main(_s: Spawner) -> ! {
40//!     let peripherals = Peripherals::take();
41//!     let system = SystemControl::new(peripherals.SYSTEM);
42//!     let clocks = ClockControl::max(system.clock_control).freeze();
43//!
44//!     let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks);
45//!     esp_hal_embassy::init(&clocks, timg0.timer0);
46//!
47//!     let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
48//!     let (tx_pin, rx_pin) = (io.pins.gpio3, io.pins.gpio2);
49//!     let config = Config::default()
50//!         .baudrate(9600)
51//!         .rx_fifo_full_threshold(10);
52//!
53//!     let mut uart1 =
54//!         Uart::new_async_with_config(peripherals.UART1, config, &clocks, tx_pin, rx_pin).unwrap();
55//!
56//!     let sds011 = SDS011::new(&mut uart1, sds011::Config::default());
57//!     let mut sds011 = sds011.init(&mut Delay).await.unwrap();
58//!
59//!     println!("SDS011 version {}, ID {}", sds011.version(), sds011.id());
60//!     loop {
61//!         let dust = sds011.measure(&mut Delay).await.unwrap();
62//!         println!("{}", dust);
63//!
64//!         Timer::after(Duration::from_millis(30_000)).await;
65//!     }
66//! }
67//! ```
68//!
69//! # Technical Overview
70//! The sensor has two operating modes:
71//! * "query mode": The sensor does nothing until it is actively instructed to
72//!   perform a measurement (we call this polling).
73//! * "active mode": The sensor continuously produces data in a configurable
74//!   interval (we call this periodic).
75//!
76//! We abstract this into the following interface:
77//! * A sensor created using `new()` is in `Uninitialized` state.
78//!   No serial communication is performed during creation.
79//! * You call `init()`. This will return a sensor in `Polling` state.
80//!   The sensor is instructed via serial commands to switch to query mode and
81//!   goes to sleep (fan off).
82//! * The sensor can now be queried via the `measure()` function.
83//!   This will wake the sensor, spin the fan for a configurable duration
84//!   (which is necessary to get a correct measurement), read the sensor and
85//!   put it back to sleep.
86//! * Optionally (not recommended!), the sensor can be put into `Periodic` state
87//!   by calling `make_periodic()` on a sensor in `Polling` state.
88//!   This puts the sensor in charge of sleeping and waking up.
89//!   Since it will continuously produce data, make sure to call `measure()`
90//!   in time so the serial output buffer does not overflow.
91//!
92//! # Limitations
93//! This abstraction does not yet support sending commands only to a specific
94//! sensor id (it effectively uses broadcast mode all the time).
95//! This feature seemed irrelevant, but the backend code for it is completely
96//! implemented, so this may change in a future version if there is demand.
97//! Also, putting sensors into periodic mode can have the side effect of missing
98//! package boundaries. The current version cannot recover from this; it will
99//! return an error. Close the serial port and retry, or probably better,
100//! just don't use periodic mode.
101//!
102//! # Acknowledgements
103//! Thank you to Tim Orme, who implemented sds011lib in Python
104//! and wrote [documentation](https://timorme.github.io/sds011lib/resource/)
105//! that pointed me in the right direction, especially to:
106//! * [The Data Sheet](https://cdn-reichelt.de/documents/datenblatt/X200/SDS011-DATASHEET.pdf)
107//! * [The Control Protocol](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf)
108//!
109//! for the SDS011 sensor.
110
111#![no_std]
112#![warn(clippy::pedantic)]
113#![warn(clippy::cargo)]
114
115use core::error::Error;
116use core::fmt::{Debug, Display, Formatter};
117use core::marker::PhantomData;
118#[cfg(feature = "sync")]
119use embedded_hal::delay::DelayNs;
120#[cfg(not(feature = "sync"))]
121use embedded_hal_async::delay::DelayNs;
122#[cfg(feature = "sync")]
123use embedded_io::{Read, ReadExactError, Write};
124#[cfg(not(feature = "sync"))]
125use embedded_io_async::{Read, ReadExactError, Write};
126use maybe_async::maybe_async;
127pub use message::{FirmwareVersion, Measurement};
128use message::{
129    Kind, Message, ParseError, Reporting, ReportingMode, Sleep, SleepMode, WorkingPeriod,
130    RECV_BUF_SIZE,
131};
132
133mod message;
134
135/// Sensor configuration, specifically delay times.
136///
137/// Delays are necessary between waking up the sensor
138/// and reading its value to stabilize the measurement.
139#[derive(Debug, Clone)]
140pub struct Config {
141    sleep_delay: u32,
142    measure_delay: u32,
143}
144
145impl Default for Config {
146    fn default() -> Self {
147        Self {
148            sleep_delay: 500,
149            measure_delay: 30_000,
150        }
151    }
152}
153
154impl Config {
155    /// Configure the time between waking the sensor (spinning up the fan)
156    /// and reading the measurement, in milliseconds.
157    /// The sensor manual recommends 30 seconds, which is the default.
158    #[must_use]
159    pub fn set_measure_delay(mut self, measure_delay: u32) -> Self {
160        self.measure_delay = measure_delay;
161        self
162    }
163
164    /// How many milliseconds to wait before waking the sensor; defaults to 500.
165    /// Setting this too low can result in the sensor not coming up (boot time?)
166    #[must_use]
167    pub fn set_sleep_delay(mut self, sleep_delay: u32) -> Self {
168        self.sleep_delay = sleep_delay;
169        self
170    }
171}
172
173/// Error type for operations on the SDS011 sensor.
174pub enum SDS011Error<E> {
175    /// A received message could not be decoded.
176    ParseError(ParseError),
177    /// The serial interface returned an error while reading.
178    ReadError(E),
179    /// The serial interface returned an error while writing.
180    WriteError(E),
181    /// Encountered an EOF while reading.
182    UnexpectedEof,
183    /// The received message was not expected in the current sensor state.
184    UnexpectedType,
185    /// The requested operation failed.
186    OperationFailed,
187    /// The given parameters were invalid.
188    Invalid,
189}
190
191impl<E> Display for SDS011Error<E> {
192    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
193        match self {
194            SDS011Error::ParseError(e) => {
195                f.write_fmt(format_args!("message could not be decoded: {e}"))
196            }
197            SDS011Error::ReadError(_) => f.write_str("serial read error"),
198            SDS011Error::WriteError(_) => f.write_str("serial write error"),
199            SDS011Error::UnexpectedEof => f.write_str("unexpected EOF"),
200            SDS011Error::UnexpectedType => f.write_str("unexpected message type"),
201            SDS011Error::OperationFailed => f.write_str("requested operation failed"),
202            SDS011Error::Invalid => f.write_str("given parameters were invalid"),
203        }
204    }
205}
206
207impl<E> Debug for SDS011Error<E> {
208    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
209        Display::fmt(self, f)
210    }
211}
212
213impl<E> Error for SDS011Error<E> {}
214
215pub mod sensor_state {
216    mod private {
217        pub trait Sealed {}
218    }
219
220    /// Encodes state for the [SDS011](crate::SDS011) struct,
221    /// as explained in the [technical overview](crate#technical-overview).
222    ///
223    /// This trait is sealed to prevent external implementations.
224    pub trait SensorState: private::Sealed {}
225
226    /// Sensor reports periodically
227    pub struct Periodic;
228    impl private::Sealed for Periodic {}
229    impl SensorState for Periodic {}
230
231    /// Sensor sleeps until polled
232    pub struct Polling;
233    impl private::Sealed for Polling {}
234    impl SensorState for Polling {}
235
236    /// Sensor not yet initialized
237    pub struct Uninitialized;
238    impl private::Sealed for Uninitialized {}
239    impl SensorState for Uninitialized {}
240}
241
242pub use sensor_state::SensorState;
243use sensor_state::{Periodic, Polling, Uninitialized};
244
245/// The main struct.
246/// Wraps around a serial interface that implements embedded-io(-async).
247///
248/// Calling `new()` will give you an uninitialized struct.
249/// You need to call `init()` on it to get a sensor that can be polled.
250pub struct SDS011<RW, S: SensorState> {
251    serial: RW,
252    config: Config,
253    sensor_id: Option<u16>,
254    firmware: Option<FirmwareVersion>,
255    _state: PhantomData<S>,
256}
257
258impl<RW, S> SDS011<RW, S>
259where
260    RW: Read + Write,
261    S: SensorState,
262{
263    #[maybe_async]
264    async fn get_reply(&mut self) -> Result<Message, SDS011Error<RW::Error>> {
265        let mut buf = [0u8; RECV_BUF_SIZE];
266
267        match self.serial.read_exact(&mut buf).await {
268            Ok(()) => Message::parse_reply(&buf).map_err(SDS011Error::ParseError),
269            Err(ReadExactError::UnexpectedEof) => Err(SDS011Error::UnexpectedEof),
270            Err(ReadExactError::Other(e)) => Err(SDS011Error::ReadError(e)),
271        }
272    }
273
274    #[maybe_async]
275    async fn send_message(&mut self, kind: Kind) -> Result<(), SDS011Error<RW::Error>> {
276        let msg = Message::new(kind, self.sensor_id);
277        let out_buf = msg.create_query();
278
279        self.serial
280            .write_all(&out_buf)
281            .await
282            .map_err(SDS011Error::WriteError)
283    }
284
285    #[maybe_async]
286    async fn read_sensor(&mut self, query: bool) -> Result<Measurement, SDS011Error<RW::Error>> {
287        if query {
288            self.send_message(Kind::Query(None)).await?;
289        }
290
291        match self.get_reply().await?.kind {
292            Kind::Query(data) => Ok(data.expect("replies always contain data")),
293            _ => Err(SDS011Error::UnexpectedType),
294        }
295    }
296
297    #[maybe_async]
298    async fn get_firmware(&mut self) -> Result<(u16, FirmwareVersion), SDS011Error<RW::Error>> {
299        self.send_message(Kind::FWVersion(None)).await?;
300
301        let reply = self.get_reply().await?;
302        let id = reply.sensor_id.expect("replies always contain data");
303        match reply.kind {
304            Kind::FWVersion(data) => Ok((id, data.expect("replies always contain data"))),
305            _ => Err(SDS011Error::UnexpectedType),
306        }
307    }
308
309    #[maybe_async]
310    async fn _get_runmode(&mut self) -> Result<ReportingMode, SDS011Error<RW::Error>> {
311        let r = Reporting::new_query();
312        self.send_message(Kind::ReportingMode(r)).await?;
313
314        match self.get_reply().await?.kind {
315            Kind::ReportingMode(data) => Ok(data.mode()),
316            _ => Err(SDS011Error::UnexpectedType),
317        }
318    }
319
320    #[maybe_async]
321    async fn set_runmode_query(&mut self) -> Result<(), SDS011Error<RW::Error>> {
322        let r = Reporting::new_set(ReportingMode::Query);
323        self.send_message(Kind::ReportingMode(r)).await?;
324
325        match self.get_reply().await?.kind {
326            Kind::ReportingMode(r) => match r.mode() {
327                ReportingMode::Query => Ok(()),
328                ReportingMode::Active => Err(SDS011Error::OperationFailed),
329            },
330            _ => Err(SDS011Error::UnexpectedType),
331        }
332    }
333
334    #[maybe_async]
335    async fn set_runmode_active(&mut self) -> Result<(), SDS011Error<RW::Error>> {
336        let r = Reporting::new_set(ReportingMode::Active);
337        self.send_message(Kind::ReportingMode(r)).await?;
338
339        match self.get_reply().await?.kind {
340            Kind::ReportingMode(r) => match r.mode() {
341                ReportingMode::Active => Ok(()),
342                ReportingMode::Query => Err(SDS011Error::OperationFailed),
343            },
344            _ => Err(SDS011Error::UnexpectedType),
345        }
346    }
347
348    #[maybe_async]
349    async fn _get_period(&mut self) -> Result<u8, SDS011Error<RW::Error>> {
350        let w = WorkingPeriod::new_query();
351        self.send_message(Kind::WorkingPeriod(w)).await?;
352
353        match self.get_reply().await?.kind {
354            Kind::WorkingPeriod(data) => Ok(data.period()),
355            _ => Err(SDS011Error::UnexpectedType),
356        }
357    }
358
359    #[maybe_async]
360    async fn set_period(&mut self, minutes: u8) -> Result<(), SDS011Error<RW::Error>> {
361        let w = WorkingPeriod::new_set(minutes);
362        self.send_message(Kind::WorkingPeriod(w)).await?;
363
364        match self.get_reply().await?.kind {
365            Kind::WorkingPeriod(data) if data.period() == minutes => Ok(()),
366            Kind::WorkingPeriod(_) => Err(SDS011Error::OperationFailed),
367            _ => Err(SDS011Error::UnexpectedType),
368        }
369    }
370
371    #[maybe_async]
372    async fn _get_sleep(&mut self) -> Result<SleepMode, SDS011Error<RW::Error>> {
373        let s = Sleep::new_query();
374        self.send_message(Kind::Sleep(s)).await?;
375
376        match self.get_reply().await?.kind {
377            Kind::Sleep(data) => Ok(data.sleep_mode()),
378            _ => Err(SDS011Error::UnexpectedType),
379        }
380    }
381
382    #[maybe_async]
383    async fn sleep(&mut self) -> Result<(), SDS011Error<RW::Error>> {
384        let s = Sleep::new_set(SleepMode::Sleep);
385        self.send_message(Kind::Sleep(s)).await?;
386
387        match self.get_reply().await?.kind {
388            Kind::Sleep(s) => match s.sleep_mode() {
389                SleepMode::Sleep => Ok(()),
390                SleepMode::Work => Err(SDS011Error::OperationFailed),
391            },
392            _ => Err(SDS011Error::UnexpectedType),
393        }
394    }
395
396    #[maybe_async]
397    async fn wake(&mut self) -> Result<(), SDS011Error<RW::Error>> {
398        let s = Sleep::new_set(SleepMode::Work);
399        self.send_message(Kind::Sleep(s)).await?;
400
401        match self.get_reply().await?.kind {
402            Kind::Sleep(s) => match s.sleep_mode() {
403                SleepMode::Work => Ok(()),
404                SleepMode::Sleep => Err(SDS011Error::OperationFailed),
405            },
406            _ => Err(SDS011Error::UnexpectedType),
407        }
408    }
409}
410
411impl<RW> SDS011<RW, Uninitialized>
412where
413    RW: Read + Write,
414{
415    /// Create a new sensor instance, consuming the serial interface.
416    /// The returned instance needs to be initialized before use.
417    pub fn new(serial: RW, config: Config) -> Self {
418        SDS011::<RW, Uninitialized> {
419            serial,
420            config,
421            sensor_id: None,
422            firmware: None,
423            _state: PhantomData,
424        }
425    }
426
427    /// Put the sensor in a well-defined state (sleeping in polling mode).
428    ///
429    /// # Errors
430    /// This communicates with the sensor over serial and may fail with any
431    /// [SDS011Error].
432    #[maybe_async]
433    pub async fn init<D: DelayNs>(
434        mut self,
435        delay: &mut D,
436    ) -> Result<SDS011<RW, Polling>, SDS011Error<RW::Error>> {
437        // sleep a short moment to make sure the sensor is ready
438        delay.delay_ms(self.config.sleep_delay).await;
439        self.wake().await?;
440
441        self.set_runmode_query().await?;
442
443        // while we're at it, read the firmware version once
444        let (id, firmware) = self.get_firmware().await?;
445        self.sleep().await?;
446
447        Ok(SDS011::<RW, Polling> {
448            serial: self.serial,
449            config: self.config,
450            sensor_id: Some(id),
451            firmware: Some(firmware),
452            _state: PhantomData,
453        })
454    }
455}
456
457impl<RW> SDS011<RW, Periodic>
458where
459    RW: Read + Write,
460{
461    /// In this state, the sensor will wake up periodically (as configured),
462    /// wait 30 seconds, send a measurement over serial, and go back to sleep.
463    /// This method waits until data is available before returning.
464    ///
465    /// # Errors
466    /// This communicates with the sensor over serial and may fail with any
467    /// [SDS011Error].
468    #[maybe_async]
469    pub async fn measure(&mut self) -> Result<Measurement, SDS011Error<RW::Error>> {
470        self.read_sensor(false).await
471    }
472
473    /// Get the sensor's ID.
474    #[allow(clippy::missing_panics_doc)] // should never panic
475    pub fn id(&self) -> u16 {
476        self.sensor_id.expect("sensor is initialized")
477    }
478
479    /// Get the sensor's firmware version.
480    #[allow(clippy::missing_panics_doc)] // should never panic
481    pub fn version(&self) -> FirmwareVersion {
482        self.firmware.clone().expect("sensor is initialized")
483    }
484}
485
486impl<RW> SDS011<RW, Polling>
487where
488    RW: Read + Write,
489{
490    /// In this state, measurements are triggered by calling this function.
491    /// The sensor is woken up and the fan spins for the configured delay time,
492    /// after which we send the measurement query and put it back to sleep.
493    ///
494    /// # Errors
495    /// This communicates with the sensor over serial and may fail with any
496    /// [SDS011Error].
497    #[maybe_async]
498    pub async fn measure<D: DelayNs>(
499        &mut self,
500        delay: &mut D,
501    ) -> Result<Measurement, SDS011Error<RW::Error>> {
502        // sleep a short moment to make sure the sensor is ready
503        delay.delay_ms(self.config.sleep_delay).await;
504        self.wake().await?;
505
506        // do a dummy measurement, spin for a few secs, then do real measurement
507        _ = self.read_sensor(true).await?;
508        delay.delay_ms(self.config.measure_delay).await;
509        let res = self.read_sensor(true).await?;
510        self.sleep().await?;
511
512        Ok(res)
513    }
514
515    /// Set the sensor into periodic measurement mode, in which it performs
516    /// a measurement every 0-30 `minutes`.
517    /// If > 0, the sensor will go to sleep between measurements.
518    ///
519    /// # Errors
520    /// This communicates with the sensor over serial and may fail with any
521    /// [SDS011Error].
522    #[maybe_async]
523    pub async fn make_periodic<D: DelayNs>(
524        mut self,
525        delay: &mut D,
526        minutes: u8,
527    ) -> Result<SDS011<RW, Periodic>, SDS011Error<RW::Error>> {
528        if minutes > 30 {
529            return Err(SDS011Error::Invalid);
530        }
531
532        // sleep a short moment to make sure the sensor is ready
533        delay.delay_ms(self.config.sleep_delay).await;
534        self.wake().await?;
535
536        self.set_period(minutes).await?;
537        self.set_runmode_active().await?;
538
539        Ok(SDS011::<RW, Periodic> {
540            serial: self.serial,
541            config: self.config,
542            sensor_id: self.sensor_id,
543            firmware: self.firmware,
544            _state: PhantomData,
545        })
546    }
547
548    /// Get the sensor's ID.
549    #[allow(clippy::missing_panics_doc)] // should never panic
550    pub fn id(&self) -> u16 {
551        self.sensor_id.expect("sensor is initialized")
552    }
553
554    /// Get the sensor's firmware version.
555    #[allow(clippy::missing_panics_doc)] // should never panic
556    pub fn version(&self) -> FirmwareVersion {
557        self.firmware.clone().expect("sensor is initialized")
558    }
559}