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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum OperationalState {
18 Sleeping,
20 Working,
22}
23
24pub struct Sds011<Serial> {
34 serial: Serial,
35 config: Config,
36}
37
38#[derive(Debug, Clone, Copy)]
42pub struct Sds011Data {
43 pub pm2_5: f32,
45 pub pm10: f32,
47}
48
49impl<S> Sds011<S>
50where
51 S: Read + Write,
52{
53 pub fn new(serial: S, config: Config) -> Self {
64 Self { serial, config }
65 }
66
67 pub async fn init(&mut self) -> Result<(), Error> {
75 self.serial.flush().await.map_err(|_| Error::WriteFailure)?;
76
77 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 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 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 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 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 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; self.write(&mut command).await?;
169 self.read().await
170 }
171
172 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; command[3] = 0x01; command[4] = if mode == DeviceMode::Active {
188 0x00 } else {
190 0x01 };
192
193 self.write(&mut command).await?;
194 let buffer = self.read().await?; 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; 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 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; command[3] = 0x00; 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 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; command[3] = 0x01; command[4] = if state == OperationalState::Working {
264 0x01
265 } else {
266 0x00
267 }; 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 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; command[3] = 0x00; 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 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); }
345 debug!("Setting working period to: {} minutes", period);
346 let mut command = self.base_command();
347 command[2] = 0x08; command[3] = 0x01; 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 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; command[3] = 0x00; 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 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(); command[2] = 0x05; command[13] = new_id1;
421 command[14] = new_id2;
422 self.write(&mut command).await?;
425 let buffer = self.read().await?;
426
427 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 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 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 fn base_command(&self) -> [u8; 19] {
477 [
478 HEAD,
479 COMMAND_ID,
480 0x00, 0x00,
482 0x00,
483 0x00,
484 0x00,
485 0x00,
486 0x00,
487 0x00,
488 0x00,
489 0x00,
490 0x00,
491 0x00,
492 0x00, self.config.id.id1, self.config.id.id2, 0x00, TAIL,
497 ]
498 }
499
500 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)?; Ok(())
515 }
516
517 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]; let bytes_read = self.serial.read(&mut read_buffer).await.map_err(|e| {
534 log::debug!("Serial read error during attempt {}: {:?}", attempts, e); 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 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 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; }
559
560 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 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 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 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; }
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::*; use embedded_io_async::{Error, ErrorKind, ErrorType};
634 use embedded_io_async::{Read, Write};
635 use futures_executor::block_on; #[derive(Debug, Default)]
639 struct MockSerial {
640 write_buffer: Vec<u8>, read_buffer: Vec<u8>, read_pos: usize, flush_called: bool, fail_write: bool, fail_read: bool, fail_flush: bool, }
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 fn set_read_data(&mut self, data: Vec<u8>) {
664 self.read_buffer = data;
665 self.read_pos = 0;
666 }
667
668 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); }
679 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 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); }
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); }
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 impl ErrorType for MockSerial {
736 type Error = DummyError;
737 }
738
739 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 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 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 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 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); assert_eq!(cmd1[3], 0x01); assert_eq!(cmd1[4], 0x01); 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 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); assert_eq!(cmd2[3], 0x01); assert_eq!(cmd2[4], 0x00); 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 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 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 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); assert_eq!(cmd1[3], 0x01); assert_eq!(cmd1[4], 0x00); let checksum1 = calculate_checksum(&cmd1[2..=16]);
890 assert_eq!(cmd1[17], checksum1);
891 assert_eq!(cmd1[18], TAIL);
892
893 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); assert_eq!(cmd2[3], 0x01); assert_eq!(cmd2[4], 0x00); 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}