sds011_nostd_rs/
lib.rs

1#![cfg_attr(not(test), no_std)]
2
3use embedded_io_async::{Read, Write};
4use log::debug;
5
6mod constants;
7use constants::*;
8
9mod error;
10pub use error::*;
11
12mod config;
13pub use config::*;
14
15// Represents the operational state of the sensor
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum OperationalState {
18    /// Sensor is in low-power sleep mode.
19    Sleeping,
20    /// Sensor is actively taking measurements.
21    Working,
22}
23
24/// Represents an SDS011 air quality sensor.
25///
26/// This struct provides methods to interact with the sensor,
27/// such as initializing it, reading data, and configuring its settings.
28///
29/// # Type Parameters
30///
31/// * `Serial`: The type of the serial interface used to communicate with the sensor.
32///   It must implement `embedded_io_async::Read` and `embedded_io_async::Write`.
33pub struct Sds011<Serial> {
34    serial: Serial,
35    config: Config,
36}
37
38/// Represents a single data sample read from the SDS011 sensor.
39///
40/// Contains PM2.5 and PM10 particulate matter concentration values.
41#[derive(Debug, Clone, Copy)]
42pub struct Sds011Data {
43    /// PM2.5 concentration in µg/m³.
44    pub pm2_5: f32,
45    /// PM10 concentration in µg/m³.
46    pub pm10: f32,
47}
48
49impl<S> Sds011<S>
50where
51    S: Read + Write,
52{
53    /// Creates a new `Sds011` sensor instance.
54    ///
55    /// # Arguments
56    ///
57    /// * `serial`: The serial interface for communication with the sensor.
58    /// * `config`: The initial configuration for the sensor.
59    ///
60    /// # Returns
61    ///
62    /// A new `Sds011` instance.
63    pub fn new(serial: S, config: Config) -> Self {
64        Self { serial, config }
65    }
66
67    /// Initializes the SDS011 sensor according to the provided configuration.
68    ///
69    /// This involves:
70    /// - Flushing the serial buffer.
71    /// - Setting the reporting mode (Active or Passive).
72    /// - If Passive mode, putting the sensor to sleep initially.
73    /// - If Active mode, setting the working period to continuous.
74    pub async fn init(&mut self) -> Result<(), Error> {
75        self.serial.flush().await.map_err(|_| Error::WriteFailure)?;
76
77        // Set the desired reporting mode (active or passive)
78        self.set_reporting_mode_cmd(self.config.mode)
79            .await
80            .map_err(|e| {
81                log::error!(
82                    "Failed to set reporting mode to {:?} during init: {:?}",
83                    self.config.mode,
84                    e
85                );
86                e
87            })?;
88
89        if self.config.mode == DeviceMode::Passive {
90            // In passive mode, put the sensor to sleep initially
91            self.set_operational_state(OperationalState::Sleeping)
92                .await
93                .map_err(|e| {
94                    log::error!(
95                        "Failed to set state to sleep during init (Passive Mode): {:?}",
96                        e
97                    );
98                    e
99                })?;
100        } else {
101            // In active mode, set the working period to continuous (0)
102            self.set_working_period_value(0x00).await.map_err(|e| {
103                log::error!(
104                    "Failed to set working period to continuous during init (Active Mode): {:?}",
105                    e
106                );
107                e
108            })?;
109        }
110
111        debug!("SDS011 init sequence complete.");
112        Ok(())
113    }
114
115    /// Reads a single data sample from the SDS011 sensor.
116    ///
117    /// In Passive mode, this involves:
118    /// 1. Waking the sensor up.
119    /// 2. Waiting for a short period for the sensor to stabilize (implicitly handled by the sensor's response time).
120    /// 3. Querying the data.
121    /// 4. Putting the sensor back to sleep.
122    ///
123    /// In Active mode, this involves:
124    /// 1. Querying the data (as the sensor is already working).
125    ///
126    /// Returns an `Sds011Data` struct containing PM2.5 and PM10 values, or an `Error` if the operation fails.
127    pub async fn read_sample(&mut self) -> Result<Sds011Data, Error> {
128        if self.config.mode == DeviceMode::Passive {
129            debug!("Waking up sensor (Passive Mode)");
130            self.set_operational_state(OperationalState::Working)
131                .await
132                .map_err(|e| {
133                    log::error!("Failed to wake up sensor: {:?}", e);
134                    e
135                })?;
136
137            debug!("Waiting for sensor to stabilize after wakeup...");
138        }
139
140        // For both active and passive (after wakeup), query data
141        let buffer = self.query_data_cmd().await.map_err(|e| {
142            log::error!("Failed to query sensor data: {:?}", e);
143            e
144        })?;
145
146        let data = self.process_frame(&buffer).ok_or_else(|| {
147            log::error!("Failed to process queried frame. Buffer: {:02X?}", buffer);
148            Error::InvalidFrame
149        })?;
150
151        if self.config.mode == DeviceMode::Passive {
152            debug!("Putting sensor back to sleep (Passive Mode)");
153            self.set_operational_state(OperationalState::Sleeping)
154                .await
155                .map_err(|e| {
156                    log::error!("Failed to put sensor to sleep: {:?}", e);
157                    e
158                })?;
159        }
160        Ok(data)
161    }
162
163    // Sends the query data command and returns the raw 10-byte reply.
164    async fn query_data_cmd(&mut self) -> Result<[u8; 10], Error> {
165        debug!("Querying sensor data (CMD 0x04)");
166        let mut command = self.base_command();
167        command[2] = 0x04; // Query data command
168        self.write(&mut command).await?;
169        self.read().await
170    }
171
172    /// Sets the sensor's reporting mode (Active or Passive).
173    ///
174    /// # Arguments
175    ///
176    /// * `mode`: The `DeviceMode` to set (Active or Passive).
177    ///
178    /// # Returns
179    ///
180    /// * `Ok(())` if the command was successful and the sensor acknowledged the change.
181    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
182    pub async fn set_reporting_mode_cmd(&mut self, mode: DeviceMode) -> Result<(), Error> {
183        debug!("Setting reporting mode to: {:?}", mode);
184        let mut command = self.base_command();
185        command[2] = 0x02; // Reporting mode command
186        command[3] = 0x01; // Set
187        command[4] = if mode == DeviceMode::Active {
188            0x00 // 0x00 for Active mode
189        } else {
190            0x01 // 0x01 for Passive mode
191        };
192
193        self.write(&mut command).await?;
194        let buffer = self.read().await?; // Read the reply
195
196        // Verify reply. The SDS011 sensor might reply with a standard command reply (0xC5) or, in
197        // some cases, immediately send a data report (0xC0) if switching to active mode and data
198        // is ready.
199        if (buffer[1] == REPLY_ID
200            && buffer[2] == 0x02
201            && buffer[3] == 0x01
202            && buffer[4] == command[4])
203            || buffer[1] == DATA_REPORT_ID
204        {
205            self.config.mode = mode; // Update internal config on successful set
206            debug!("Reporting mode set to {:?}, reply: {:02X?}", mode, buffer);
207            Ok(())
208        } else {
209            log::error!(
210                "Failed to set reporting mode, unexpected reply: {:02X?}",
211                buffer
212            );
213            Err(Error::CommandFailed)
214        }
215    }
216
217    /// Queries the sensor's current reporting mode.
218    ///
219    /// # Returns
220    ///
221    /// * `Ok(DeviceMode)` containing the current mode (Active or Passive).
222    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
223    pub async fn get_reporting_mode(&mut self) -> Result<DeviceMode, Error> {
224        debug!("Querying reporting mode (CMD 0x02, Query)");
225        let mut command = self.base_command();
226        command[2] = 0x02; // Reporting mode command
227        command[3] = 0x00; // Query
228        self.write(&mut command).await?;
229        let buffer = self.read().await?;
230
231        if buffer[1] == REPLY_ID && buffer[2] == 0x02 && buffer[3] == 0x00 {
232            let mode = if buffer[4] == 0x00 {
233                DeviceMode::Active
234            } else {
235                DeviceMode::Passive
236            };
237            debug!("Queried reporting mode: {:?}", mode);
238            Ok(mode)
239        } else {
240            log::warn!(
241                "get_reporting_mode: Unexpected reply structure: {:02X?}",
242                buffer
243            );
244            Err(Error::UnexpectedReply)
245        }
246    }
247
248    /// Sets the sensor's operational state (Sleeping or Working).
249    ///
250    /// # Arguments
251    ///
252    /// * `state`: The `OperationalState` to set (Working or Sleeping).
253    ///
254    /// # Returns
255    ///
256    /// * `Ok(())` if the command was successful and the sensor acknowledged the change.
257    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
258    pub async fn set_operational_state(&mut self, state: OperationalState) -> Result<(), Error> {
259        debug!("Setting operational state to: {:?}", state);
260        let mut command = self.base_command();
261        command[2] = 0x06; // Set state command
262        command[3] = 0x01; // Set
263        command[4] = if state == OperationalState::Working {
264            0x01
265        } else {
266            0x00
267        }; // 1=Work, 0=Sleep
268
269        self.write(&mut command).await?;
270        let buffer = self.read().await?;
271
272        if (buffer[1] == REPLY_ID
273            && buffer[2] == 0x06
274            && buffer[3] == 0x01
275            && buffer[4] == command[4])
276            || buffer[1] == DATA_REPORT_ID
277        {
278            debug!(
279                "Operational state set to {:?}, reply: {:02X?}",
280                state, buffer
281            );
282            Ok(())
283        } else {
284            log::error!(
285                "Failed to set operational state, unexpected reply: {:02X?}",
286                buffer
287            );
288            Err(Error::CommandFailed)
289        }
290    }
291
292    /// Queries the sensor's current operational state.
293    ///
294    /// # Returns
295    ///
296    /// * `Ok(OperationalState)` containing the current state (Working or Sleeping).
297    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
298    pub async fn get_operational_state(&mut self) -> Result<OperationalState, Error> {
299        debug!("Querying operational state (CMD 0x06, Query)");
300        let mut command = self.base_command();
301        command[2] = 0x06; // Set state command
302        command[3] = 0x00; // Query
303        self.write(&mut command).await?;
304        let buffer = self.read().await?;
305
306        if buffer[1] == REPLY_ID && buffer[2] == 0x06 && buffer[3] == 0x00 {
307            let state = if buffer[4] == 0x01 {
308                OperationalState::Working
309            } else {
310                OperationalState::Sleeping
311            };
312            debug!("Queried operational state: {:?}", state);
313            Ok(state)
314        } else {
315            log::warn!(
316                "get_operational_state: Unexpected reply structure: {:02X?}",
317                buffer
318            );
319            Err(Error::UnexpectedReply)
320        }
321    }
322
323    /// Sets the sensor's working period.
324    ///
325    /// The working period determines how often the sensor takes measurements when in Active mode.
326    /// - A value of `0` sets the sensor to continuous working mode (reports data as soon as it's available).
327    /// - Values from `1` to `30` set the sensor to work for 30 seconds, then sleep for `(period - 1) * 60 + 30` seconds,
328    ///   reporting data once per `period` minutes.
329    ///
330    /// # Arguments
331    ///
332    /// * `period`: The working period in minutes. Must be between 0 and 30 (inclusive).
333    ///
334    /// # Returns
335    ///
336    /// * `Ok(())` if the command was successful and the sensor acknowledged the change.
337    /// * `Err(Error::InvalidArg)` if `period` is greater than 30.
338    /// * `Err(Error::CommandFailed)` if the command failed or the reply was unexpected.
339    /// * `Err(Error::WriteFailure)` or `Err(Error::ReadFailure)` for serial communication issues.
340    pub async fn set_working_period_value(&mut self, period: u8) -> Result<(), Error> {
341        if period > 30 {
342            log::error!("Working period {} out of range (0-30)", period);
343            return Err(Error::InvalidArg); // Or a specific error for invalid period
344        }
345        debug!("Setting working period to: {} minutes", period);
346        let mut command = self.base_command();
347        command[2] = 0x08; // Set working period command
348        command[3] = 0x01; // Set
349        command[4] = period;
350
351        self.write(&mut command).await?;
352        let buffer = self.read().await?;
353
354        if (buffer[1] == REPLY_ID && buffer[2] == 0x08 && buffer[3] == 0x01 && buffer[4] == period)
355            || buffer[1] == DATA_REPORT_ID
356        {
357            debug!("Working period set to {}, reply: {:02X?}", period, buffer);
358            Ok(())
359        } else {
360            log::error!(
361                "Failed to set working period, unexpected reply: {:02X?}",
362                buffer
363            );
364            Err(Error::CommandFailed)
365        }
366    }
367
368    /// Queries the sensor's current working period.
369    ///
370    /// The working period defines how often the sensor takes measurements and reports data
371    /// when in Active mode. A value of `0` means continuous mode. Values `1-30` correspond
372    /// to reporting data once every `N` minutes.
373    ///
374    /// # Returns
375    ///
376    /// * `Ok(u8)` containing the current working period in minutes (0-30).
377    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
378    pub async fn get_working_period(&mut self) -> Result<u8, Error> {
379        debug!("Querying working period (CMD 0x08, Query)");
380        let mut command = self.base_command();
381        command[2] = 0x08; // Set working period command
382        command[3] = 0x00; // Query
383        self.write(&mut command).await?;
384        let buffer = self.read().await?;
385
386        if buffer[1] == REPLY_ID && buffer[2] == 0x08 && buffer[3] == 0x00 {
387            let period = buffer[4];
388            debug!("Queried working period: {} minutes", period);
389            Ok(period)
390        } else {
391            log::warn!(
392                "get_working_period: Unexpected reply structure: {:02X?}",
393                buffer
394            );
395            Err(Error::UnexpectedReply)
396        }
397    }
398
399    /// Sets the device ID of the sensor.
400    ///
401    /// The device ID is used to address the sensor when multiple sensors might be on the same bus,
402    /// though typically only one SDS011 is used per serial interface.
403    /// The default device ID is `0xFFFF`.
404    ///
405    /// # Arguments
406    ///
407    /// * `new_id1`: The new LSB (Least Significant Byte) of the device ID.
408    /// * `new_id2`: The new MSB (Most Significant Byte) of the device ID.
409    ///
410    /// # Returns
411    ///
412    /// * `Ok(())` if the command was successful, the sensor acknowledged the change,
413    ///   and the internal configuration was updated.
414    /// * `Err(Error::CommandFailed)` if the command failed or the reply was unexpected.
415    /// * `Err(Error::WriteFailure)` or `Err(Error::ReadFailure)` for serial communication issues.
416    pub async fn set_device_id(&mut self, new_id1: u8, new_id2: u8) -> Result<(), Error> {
417        debug!("Setting device ID to: {:02X}{:02X}", new_id1, new_id2);
418        let mut command = self.base_command(); // Uses current self.config.id for addressing
419        command[2] = 0x05; // Set Device ID command
420        command[13] = new_id1;
421        command[14] = new_id2;
422        // Bytes 15 & 16 (current device ID for addressing) are already set by base_command()
423
424        self.write(&mut command).await?;
425        let buffer = self.read().await?;
426
427        // Expected reply: AA C5 05 00 00 00 NEW_ID1 NEW_ID2 CS AB
428        if buffer[1] == REPLY_ID
429            && buffer[2] == 0x05
430            && buffer[6] == new_id1
431            && buffer[7] == new_id2
432        {
433            self.config.id.id1 = new_id1;
434            self.config.id.id2 = new_id2;
435            debug!(
436                "Device ID updated locally to {:02X}{:02X}. Reply: {:02X?}",
437                new_id1, new_id2, buffer
438            );
439            Ok(())
440        } else {
441            log::error!("Failed to set device ID, unexpected reply: {:02X?}", buffer);
442            Err(Error::CommandFailed)
443        }
444    }
445
446    /// Retrieves the firmware version of the sensor.
447    ///
448    /// The firmware version is returned as a tuple `(year, month, day)`.
449    /// For example, a firmware version of "15-10-21" (YY-MM-DD) would be returned as `(15, 10, 21)`.
450    ///
451    /// # Returns
452    ///
453    /// * `Ok((u8, u8, u8))` containing the year, month, and day of the firmware version.
454    /// * `Err(Error)` if the command failed, the reply was unexpected, or a write/read error occurred.
455    pub async fn get_firmware(&mut self) -> Result<(u8, u8, u8), Error> {
456        debug!("Getting firmware version (CMD 0x07)");
457        let mut command = self.base_command();
458        command[2] = 0x07;
459        self.write(&mut command).await?;
460        let buffer = self.read().await?;
461
462        // Expected reply: AA C5 07 YEAR MONTH DAY ID1 ID2 CS AB
463        if buffer[1] == REPLY_ID && buffer[2] == 0x07 {
464            let year = buffer[3];
465            let month = buffer[4];
466            let day = buffer[5];
467            debug!("Firmware version: 20{}-{}-{}", year, month, day);
468            Ok((year, month, day))
469        } else {
470            log::warn!("get_firmware: Unexpected reply structure: {:02X?}", buffer);
471            Err(Error::UnexpectedReply)
472        }
473    }
474
475    // Constructs a base 19-byte command frame.
476    fn base_command(&self) -> [u8; 19] {
477        [
478            HEAD,
479            COMMAND_ID,
480            0x00, // Placeholder for specific command type
481            0x00,
482            0x00,
483            0x00,
484            0x00,
485            0x00,
486            0x00,
487            0x00,
488            0x00,
489            0x00,
490            0x00,
491            0x00,
492            0x00,               // Data bytes
493            self.config.id.id1, // Device ID LSB for addressing
494            self.config.id.id2, // Device ID MSB for addressing
495            0x00,               // Placeholder for checksum
496            TAIL,
497        ]
498    }
499
500    // Writes a 19-byte command to the serial port, calculating and inserting the checksum.
501    async fn write(&mut self, command: &mut [u8; 19]) -> Result<(), Error> {
502        let checksum: u8 = command[2..=16]
503            .iter()
504            .fold(0u8, |sum, &b| sum.wrapping_add(b));
505        command[17] = checksum;
506
507        debug!("Executing command: {:02X?}", command);
508        self.serial.flush().await.map_err(|_| Error::WriteFailure)?;
509        self.serial
510            .write_all(command)
511            .await
512            .map_err(|_| Error::WriteFailure)?;
513        self.serial.flush().await.map_err(|_| Error::WriteFailure)?; // Ensure data is sent
514        Ok(())
515    }
516
517    // Reads a 10-byte frame from the serial port, attempting to synchronize and validate.
518    async fn read(&mut self) -> Result<[u8; 10], Error> {
519        let mut attempts = 0;
520        const MAX_ATTEMPTS: usize = 5;
521
522        loop {
523            attempts += 1;
524            if attempts > MAX_ATTEMPTS {
525                log::error!(
526                    "Failed to read a valid frame after {} attempts",
527                    MAX_ATTEMPTS
528                );
529                return Err(Error::ReadFailure);
530            }
531
532            let mut read_buffer = [0u8; 20]; // Read more to increase chance of catching a frame
533            let bytes_read = self.serial.read(&mut read_buffer).await.map_err(|e| {
534                log::debug!("Serial read error during attempt {}: {:?}", attempts, e); // Assuming e is Debug
535                Error::ReadFailure
536            })?;
537
538            if bytes_read < 10 {
539                log::debug!("Read less than 10 bytes ({}), retrying.", bytes_read);
540                continue;
541            }
542
543            // Search for HEAD...TAIL pattern
544            if let Some(head_idx) = read_buffer[..bytes_read]
545                .windows(10)
546                .position(|window| window[0] == HEAD && window[9] == TAIL)
547            {
548                let mut frame = [0u8; 10];
549                frame.copy_from_slice(&read_buffer[head_idx..head_idx + 10]);
550
551                debug!("Potential frame found: {:02X?}", frame);
552
553                // Validate Checksum (critical)
554                let checksum_calc: u8 = frame[2..8].iter().copied().sum::<u8>();
555                if checksum_calc != frame[8] {
556                    log::error!("Bad checksum: Calculated {:02X}, Received {:02X}. Frame: {:02X?}. Retrying.", checksum_calc, frame[8], &frame);
557                    continue; // Checksum failed, try to read again
558                }
559
560                // Optional: Log if command ID is unexpected, but don't fail read() for it here.
561                // Specific command handlers will interpret frame[1] and frame[2].
562                if frame[1] != REPLY_ID && frame[1] != DATA_REPORT_ID {
563                    log::warn!(
564                        "Frame has unexpected command ID: {:02X} (Expected {:02X} or {:02X})",
565                        frame[1],
566                        REPLY_ID,
567                        DATA_REPORT_ID
568                    );
569                }
570
571                log::debug!("Successfully read and validated frame: {:02X?}", frame);
572                return Ok(frame);
573            } else {
574                log::debug!(
575                    "No HEAD...TAIL pattern in {:02X?}. Retrying.",
576                    &read_buffer[..bytes_read]
577                );
578            }
579        }
580    }
581
582    // Processes a validated 10-byte frame to extract PM2.5 and PM10 data.
583    fn process_frame(&self, data: &[u8; 10]) -> Option<Sds011Data> {
584        let pm2_5_lsb_idx;
585        let pm2_5_msb_idx;
586        let pm10_lsb_idx;
587        let pm10_msb_idx;
588
589        match data[1] {
590            DATA_REPORT_ID => {
591                // Active mode report (0xC0) or data reply for some sensors
592                // Frame: AA C0 PM25_L PM25_H PM10_L PM10_H ID0 ID1 CS AB
593                pm2_5_lsb_idx = 2;
594                pm2_5_msb_idx = 3;
595                pm10_lsb_idx = 4;
596                pm10_msb_idx = 5;
597            }
598            REPLY_ID if data[2] == 0x04 => {
599                // Reply to Query Data command (0xC5, sub-cmd 0x04)
600                // Frame: AA C5 04 PM25_L PM25_H PM10_L PM10_H ID0 ID1 CS AB
601                pm2_5_lsb_idx = 3;
602                pm2_5_msb_idx = 4;
603                pm10_lsb_idx = 5;
604                pm10_msb_idx = 6;
605            }
606            REPLY_ID => {
607                log::debug!("process_frame: Received REPLY_ID frame, but not for a data query (cmd_id: {:02X}). Frame: {:02X?}", data[2], data);
608                return None; // Not a data frame we can parse for PM values
609            }
610            _ => {
611                log::error!(
612                    "process_frame: Unexpected frame command ID {:02X} for PM data. Frame: {:02X?}",
613                    data[1],
614                    data
615                );
616                return None;
617            }
618        }
619
620        let pm2_5 =
621            (u16::from(data[pm2_5_lsb_idx]) | (u16::from(data[pm2_5_msb_idx]) << 8)) as f32 / 10.0;
622        let pm10 =
623            (u16::from(data[pm10_lsb_idx]) | (u16::from(data[pm10_msb_idx]) << 8)) as f32 / 10.0;
624
625        debug!("Processed frame - PM2.5: {}, PM10: {}", pm2_5, pm10);
626        Some(Sds011Data { pm2_5, pm10 })
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*; // Make Sds011, Config, etc. available
633    use embedded_io_async::{Error, ErrorKind, ErrorType};
634    use embedded_io_async::{Read, Write};
635    use futures_executor::block_on; // For running async tests
636
637    // A mock serial port for testing
638    #[derive(Debug, Default)]
639    struct MockSerial {
640        write_buffer: Vec<u8>, // Stores bytes written to the mock serial
641        read_buffer: Vec<u8>,  // Bytes to be returned by read operations
642        read_pos: usize,       // Current position in the read_buffer
643        flush_called: bool,    // Whether flush was called
644        fail_write: bool,      // Simulate write failure
645        fail_read: bool,       // Simulate read failure
646        fail_flush: bool,      // Simulate flush failure
647    }
648
649    impl MockSerial {
650        fn new(read_data: Vec<u8>) -> Self {
651            MockSerial {
652                write_buffer: Vec::new(),
653                read_buffer: read_data,
654                read_pos: 0,
655                flush_called: false,
656                fail_write: false,
657                fail_read: false,
658                fail_flush: false,
659            }
660        }
661
662        // Helper to set expected data to be read by the SUT
663        fn set_read_data(&mut self, data: Vec<u8>) {
664            self.read_buffer = data;
665            self.read_pos = 0;
666        }
667
668        // Helper to get bytes written by the SUT
669        fn get_written_data(&self) -> &[u8] {
670            &self.write_buffer
671        }
672    }
673
674    impl Read for MockSerial {
675        async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
676            if self.fail_read {
677                return Err(DummyError); // Simulate read error
678            }
679            // Simulate reading one frame (10 bytes) or less at a time,
680            // or whatever is smaller: buf.len(), 10 bytes, or remaining data.
681            let max_bytes_for_this_call = core::cmp::min(buf.len(), 10);
682            let bytes_available_in_mock = self.read_buffer.len() - self.read_pos;
683            let bytes_to_read = core::cmp::min(max_bytes_for_this_call, bytes_available_in_mock);
684
685            if bytes_to_read > 0 {
686                buf[..bytes_to_read].copy_from_slice(
687                    &self.read_buffer[self.read_pos..self.read_pos + bytes_to_read],
688                );
689                self.read_pos += bytes_to_read;
690                Ok(bytes_to_read)
691            } else {
692                // If read_pos is at the end of read_buffer, it means no more data.
693                // If buf.len() was 0, this is also Ok(0).
694                Ok(0)
695            }
696        }
697    }
698
699    impl Write for MockSerial {
700        async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
701            if self.fail_write {
702                return Err(DummyError); // Simulate write error
703            }
704            self.write_buffer.extend_from_slice(buf);
705            Ok(buf.len())
706        }
707
708        async fn flush(&mut self) -> Result<(), Self::Error> {
709            if self.fail_flush {
710                return Err(DummyError); // Simulate flush error
711            }
712            self.flush_called = true;
713            Ok(())
714        }
715    }
716
717    #[derive(Debug, Clone, Copy)]
718    pub struct DummyError;
719
720    impl core::fmt::Display for DummyError {
721        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
722            write!(f, "DummyError")
723        }
724    }
725
726    impl core::error::Error for DummyError {}
727
728    impl Error for DummyError {
729        fn kind(&self) -> ErrorKind {
730            ErrorKind::Other
731        }
732    }
733
734    // implement ErrorType
735    impl ErrorType for MockSerial {
736        type Error = DummyError;
737    }
738
739    // Helper to create a default config for tests
740    fn default_config() -> Config {
741        Config::new(DeviceID::default(), DeviceMode::Passive)
742    }
743
744    #[test]
745    fn test_sds011_new() {
746        let mock_serial = MockSerial::new(vec![]);
747        let config = default_config();
748        let sensor = Sds011::new(mock_serial, config);
749        assert_eq!(sensor.config, config);
750    }
751
752    // Helper to calculate checksum for a command slice (excluding head, tail, and checksum byte itself)
753    fn calculate_checksum(command_payload: &[u8]) -> u8 {
754        command_payload
755            .iter()
756            .fold(0u8, |sum, &b| sum.wrapping_add(b))
757    }
758
759    #[test]
760    fn test_init_passive_mode() {
761        let mut mock_serial = MockSerial::new(vec![]);
762        // Expected reply for set_reporting_mode_cmd (passive)
763        // HEAD, REPLY_ID, CMD_SET_REPORTING_MODE, 0x01 (Set), 0x01 (Passive), 0,0,0, CHECKSUM, TAIL
764        let reply_set_mode = vec![
765            HEAD,
766            REPLY_ID,
767            0x02,
768            0x01,
769            0x01,
770            0,
771            0,
772            0,
773            (0x02 + 0x01 + 0x01),
774            TAIL,
775        ];
776        // Expected reply for set_operational_state (sleep)
777        // HEAD, REPLY_ID, CMD_SET_STATE, 0x01 (Set), 0x00 (Sleep), 0,0,0, CHECKSUM, TAIL
778        let reply_set_sleep = vec![
779            HEAD,
780            REPLY_ID,
781            0x06,
782            0x01,
783            0x00,
784            0,
785            0,
786            0,
787            #[allow(clippy::identity_op)]
788            (0x06 + 0x01 + 0x00),
789            TAIL,
790        ];
791
792        mock_serial.set_read_data([reply_set_mode, reply_set_sleep].concat());
793
794        let config = Config::new(DeviceID::default(), DeviceMode::Passive);
795        let mut sensor = Sds011::new(mock_serial, config);
796
797        let result = block_on(sensor.init());
798        assert!(result.is_ok(), "init failed: {:?}", result.err());
799
800        let written_data = sensor.serial.get_written_data();
801        assert!(
802            written_data.len() >= 38,
803            "Expected at least two 19-byte commands"
804        );
805
806        // Command 1: Set Reporting Mode to Passive
807        // HEAD, CMD_ID, 0x02, 0x01, 0x01 (Passive), ..., ID1, ID2, CHECKSUM, TAIL
808        let cmd1 = &written_data[0..19];
809        assert_eq!(cmd1[0], HEAD);
810        assert_eq!(cmd1[1], COMMAND_ID);
811        assert_eq!(cmd1[2], 0x02); // Reporting mode command
812        assert_eq!(cmd1[3], 0x01); // Set
813        assert_eq!(cmd1[4], 0x01); // Passive
814        assert_eq!(cmd1[15], DeviceID::default().id1);
815        assert_eq!(cmd1[16], DeviceID::default().id2);
816        let checksum1 = calculate_checksum(&cmd1[2..=16]);
817        assert_eq!(cmd1[17], checksum1);
818        assert_eq!(cmd1[18], TAIL);
819
820        // Command 2: Set Operational State to Sleeping
821        // HEAD, CMD_ID, 0x06, 0x01, 0x00 (Sleep), ..., ID1, ID2, CHECKSUM, TAIL
822        let cmd2 = &written_data[19..38];
823        assert_eq!(cmd2[0], HEAD);
824        assert_eq!(cmd2[1], COMMAND_ID);
825        assert_eq!(cmd2[2], 0x06); // Set state command
826        assert_eq!(cmd2[3], 0x01); // Set
827        assert_eq!(cmd2[4], 0x00); // Sleep
828        assert_eq!(cmd2[15], DeviceID::default().id1);
829        assert_eq!(cmd2[16], DeviceID::default().id2);
830        let checksum2 = calculate_checksum(&cmd2[2..=16]);
831        assert_eq!(cmd2[17], checksum2);
832        assert_eq!(cmd2[18], TAIL);
833
834        assert!(sensor.serial.flush_called);
835    }
836
837    #[test]
838    fn test_init_active_mode() {
839        let mut mock_serial = MockSerial::new(vec![]);
840        // Expected reply for set_reporting_mode_cmd (active)
841        let reply_set_mode = vec![
842            HEAD,
843            REPLY_ID,
844            0x02,
845            0x01,
846            0x00,
847            0,
848            0,
849            0,
850            #[allow(clippy::identity_op)]
851            (0x02 + 0x01 + 0x00),
852            TAIL,
853        ];
854        // Expected reply for set_working_period (continuous)
855        let reply_set_period = vec![
856            HEAD,
857            REPLY_ID,
858            0x08,
859            0x01,
860            0x00,
861            0,
862            0,
863            0,
864            #[allow(clippy::identity_op)]
865            (0x08 + 0x01 + 0x00),
866            TAIL,
867        ];
868        mock_serial.set_read_data([reply_set_mode, reply_set_period].concat());
869
870        let config = Config::new(DeviceID::default(), DeviceMode::Active);
871        let mut sensor = Sds011::new(mock_serial, config);
872
873        let result = block_on(sensor.init());
874        assert!(result.is_ok(), "init failed: {:?}", result.err());
875
876        let written_data = sensor.serial.get_written_data();
877        assert!(
878            written_data.len() >= 38,
879            "Expected at least two 19-byte commands"
880        );
881
882        // Command 1: Set Reporting Mode to Active
883        let cmd1 = &written_data[0..19];
884        assert_eq!(cmd1[0], HEAD);
885        assert_eq!(cmd1[1], COMMAND_ID);
886        assert_eq!(cmd1[2], 0x02); // Reporting mode command
887        assert_eq!(cmd1[3], 0x01); // Set
888        assert_eq!(cmd1[4], 0x00); // Active
889        let checksum1 = calculate_checksum(&cmd1[2..=16]);
890        assert_eq!(cmd1[17], checksum1);
891        assert_eq!(cmd1[18], TAIL);
892
893        // Command 2: Set Working Period to Continuous (0)
894        let cmd2 = &written_data[19..38];
895        assert_eq!(cmd2[0], HEAD);
896        assert_eq!(cmd2[1], COMMAND_ID);
897        assert_eq!(cmd2[2], 0x08); // Set working period command
898        assert_eq!(cmd2[3], 0x01); // Set
899        assert_eq!(cmd2[4], 0x00); // Continuous
900        let checksum2 = calculate_checksum(&cmd2[2..=16]);
901        assert_eq!(cmd2[17], checksum2);
902        assert_eq!(cmd2[18], TAIL);
903
904        assert!(sensor.serial.flush_called);
905    }
906}