scd30_modbus/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3use core::time::Duration;
4use embedded_io_async::{Read, ReadExactError, Write};
5use smol::Timer;
6
7#[derive(Debug)]
8pub enum Error<E> {
9    WrongCrc,
10    Serial(E),
11    GotWrongResponse,
12}
13
14#[cfg(feature = "std")]
15impl<E: std::fmt::Display> std::fmt::Display for Error<E> {
16    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
17        match self {
18            Self::Serial(e) => write!(f, "Serial error: {}", e),
19            Self::GotWrongResponse => write!(f, "Wrong response"),
20            Self::WrongCrc => write!(f, "Wrong checksum"),
21        }
22    }
23}
24
25#[cfg(feature = "std")]
26impl<E: std::error::Error> std::error::Error for Error<E> {}
27
28#[allow(dead_code)]
29enum Command {
30    StartContinuousMeasurement = 0x0036,
31    StopContinuousMeasurement = 0x0037,
32    SetMeasurementInterval = 0x0025,
33    GetDataReadyStatus = 0x0027,
34    ReadMeasurement = 0x0028,
35    SetAutomaticSelfCalibration = 0x003A,
36    ForcedRecalibrationValue = 0x0039,
37    SetTemperatureOffset = 0x003B,
38    SetAltitude = 0x0038,
39    ReadFirmwareVersion = 0x0020,
40    SoftReset = 0x0034,
41}
42
43enum RegFunction {
44    ReadHolding = 0x3,
45    #[allow(dead_code)]
46    ReadInput = 0x4,
47    WriteHolding = 0x6,
48}
49
50impl From<RegFunction> for u8 {
51    fn from(f: RegFunction) -> Self {
52        f as u8
53    }
54}
55
56const RESPONSE_TIME: Duration = Duration::from_millis(5);
57const MODBUS_ADDR: u8 = 0x61;
58
59/// Measurements received by the sensor.
60#[derive(Debug)]
61pub struct Measurement {
62    /// Co2 measuring [ppm].
63    pub co2: f32,
64    /// Humidity measuring [%].
65    pub humidity: f32,
66    /// Temperature measuring [°C].
67    pub temperature: f32,
68}
69
70/// Scd30 driver.
71pub struct Scd30<Serial> {
72    serial: Serial,
73}
74
75/// See the [datasheet] for I²c parameters.
76///
77/// [datasheet]: https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.5_CO2/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
78impl<Serial> Scd30<Serial>
79where
80    Serial: Read + Write,
81{
82    pub const BAUDRATE: u16 = 19200;
83
84    /// Returns an [Scd30] instance with the default address 0x61 shifted one place to the left.
85    /// You may or may not need this bitshift depending on the byte size of
86    /// your [I²c](embedded_hal::blocking::i2c) peripheral.
87    pub fn new(serial: Serial) -> Self {
88        Scd30 { serial }
89    }
90
91    /// Resets the sensor as it was after power on.
92    ///
93    /// Intern saved data persists and is loaded again.
94    pub async fn soft_reset(&mut self) -> Result<(), Error<Serial::Error>> {
95        self.serial.flush().await.map_err(Error::Serial)?;
96        self.write(RegFunction::WriteHolding.into(), Command::SoftReset, 0x0001)
97            .await?;
98        if !self
99            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, 0x0001)
100            .await?
101        {
102            return Err(Error::GotWrongResponse);
103        }
104        Ok(())
105    }
106
107    /// Stops interval measuring.
108    pub async fn stop_measuring(&mut self) -> Result<(), Error<Serial::Error>> {
109        self.write(
110            RegFunction::WriteHolding.into(),
111            Command::StopContinuousMeasurement,
112            0x001,
113        )
114        .await?;
115        if !self
116            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, 0x0001)
117            .await?
118        {
119            return Err(Error::GotWrongResponse);
120        }
121        Ok(())
122    }
123
124    /// Enable or disable automatic self calibration (ASC).
125    ///
126    /// According to the datasheet, the sensor should be active continously for at least
127    /// 7 days to find the initial parameter for ASC. The sensor has to be exposed to
128    /// at least 1 hour of fresh air (~400ppm CO₂) per day.
129    pub async fn set_automatic_calibration(
130        &mut self,
131        enable: bool,
132    ) -> Result<(), Error<Serial::Error>> {
133        let data = match enable {
134            true => 0x0001,
135            _ => 0,
136        };
137        self.write(
138            RegFunction::ReadHolding.into(),
139            Command::SetAutomaticSelfCalibration,
140            data,
141        )
142        .await?;
143        if !self
144            .check_resp_data::<7, 5>(RegFunction::ReadHolding.into(), 3, data)
145            .await?
146        {
147            return Err(Error::GotWrongResponse);
148        }
149        Ok(())
150    }
151
152    /// Set forced recalibration value (FCR).
153    ///
154    /// CO2 reference must be in unit ppm.
155    pub async fn force_recalibrate_with_value(
156        &mut self,
157        reference: u16,
158    ) -> Result<(), Error<Serial::Error>> {
159        self.write(
160            RegFunction::WriteHolding.into(),
161            Command::ForcedRecalibrationValue,
162            reference,
163        )
164        .await?;
165        if !self
166            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, reference)
167            .await?
168        {
169            return Err(Error::GotWrongResponse);
170        }
171        Ok(())
172    }
173
174    /// Get the calibration reference which can be set by [`Scd30::force_recalibrate_with_value()`].
175    pub async fn get_forced_recalibration_value(&mut self) -> Result<u16, Error<Serial::Error>> {
176        self.write(
177            RegFunction::ReadHolding.into(),
178            Command::ForcedRecalibrationValue,
179            0x0001,
180        )
181        .await?;
182        Timer::after(RESPONSE_TIME).await;
183        let resp = self.read_n::<7, 5>(RegFunction::ReadHolding.into()).await?;
184        Ok(u16::from_be_bytes([resp[3], resp[4]]))
185    }
186
187    /// Set a temperature offset [°C] which will be applied to the received measures.
188    ///
189    /// The on-board RH/T sensor is influenced by thermal self-heating of SCD30 and other
190    /// electrical components. Design-in alters the thermal properties of SCD30 such that
191    /// temperature and humidity offsets may occur when operating the sensor in end-customer
192    /// devices. Compensation of those effects is achievable by writing the temperature offset
193    /// found in continuous operation of the device into the sensor.
194    pub async fn set_temperature_offset(
195        &mut self,
196        offset: u16,
197    ) -> Result<(), Error<Serial::Error>> {
198        self.write(
199            RegFunction::WriteHolding.into(),
200            Command::SetTemperatureOffset,
201            offset,
202        )
203        .await?;
204        if !self
205            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, offset)
206            .await?
207        {
208            return Err(Error::GotWrongResponse);
209        }
210        Ok(())
211    }
212
213    /// Start measuring without mbar compensation.
214    pub async fn start_measuring(&mut self) -> Result<(), Error<Serial::Error>> {
215        self.start_measuring_with_mbar(0).await
216    }
217
218    pub async fn set_measurement_interval(
219        &mut self,
220        interval: Duration,
221    ) -> Result<(), Error<Serial::Error>> {
222        let secs = interval.as_secs();
223        debug_assert!((2..=1800).contains(&secs));
224        let secs = u16::try_from(secs).unwrap();
225        self.write(
226            RegFunction::WriteHolding.into(),
227            Command::SetMeasurementInterval,
228            secs,
229        )
230        .await?;
231        if !self
232            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, secs)
233            .await?
234        {
235            return Err(Error::GotWrongResponse);
236        }
237        Ok(())
238    }
239
240    /// Start measuring with mbar (pressure) compensation.
241    ///
242    /// Starts continuous measurement of the SCD30 to measure CO2 concentration, humidity and temperature.
243    /// Measurement data which is not read from the sensor will be overwritten. The measurement interval is
244    /// adjustable via [`Scd30::set_measurement_interval()`], initial measurement rate is 2s.
245    ///
246    /// Continuous measurement status is saved in non-volatile memory. When the sensor is powered down
247    /// while continuous measurement mode is active SCD30 will measure continuously after repowering
248    /// without sending the measurement command.
249    ///
250    /// The CO2 measurement value can be compensated for ambient pressure by feeding the pressure value in
251    /// mBar to the sensor. Setting the ambient pressure will overwrite previous settings of altitude
252    /// compensation. Setting the argument to zero will deactivate the ambient pressure compensation
253    /// (default ambient pressure = 1013.25 mBar). For setting a new ambient pressure when continuous
254    /// measurement is running the whole command has to be written to SCD30.
255    pub async fn start_measuring_with_mbar(
256        &mut self,
257        pressure: u16,
258    ) -> Result<(), Error<Serial::Error>> {
259        debug_assert!(pressure == 0 || (700..=1400).contains(&pressure));
260        self.write(
261            RegFunction::WriteHolding.into(),
262            Command::StartContinuousMeasurement,
263            pressure,
264        )
265        .await?;
266        if !self
267            .check_resp_data::<8, 6>(RegFunction::WriteHolding.into(), 4, pressure)
268            .await?
269        {
270            return Err(Error::GotWrongResponse);
271        }
272        Ok(())
273    }
274
275    /// Returns if measures are ready.
276    ///
277    /// Is used to determine if a measurement can be read from the sensor’s buffer. Whenever
278    /// there is a measurement available from the internal buffer it returns true.
279    pub async fn data_ready(&mut self) -> Result<bool, Error<Serial::Error>> {
280        self.write(
281            RegFunction::ReadHolding.into(),
282            Command::GetDataReadyStatus,
283            0x0001,
284        )
285        .await?;
286        self.check_resp_data::<7, 5>(RegFunction::ReadHolding.into(), 3, 0x0001)
287            .await
288    }
289
290    /// Read measurements from the sensor.
291    ///
292    /// This function tests first if data is available as [`Scd30::data_ready()`] does. If data
293    /// is ready it is returned as a `Some` else it returns `None`.
294    pub async fn read(&mut self) -> Result<Option<Measurement>, Error<Serial::Error>> {
295        match self.data_ready().await {
296            Ok(true) => {
297                self.write(
298                    RegFunction::ReadHolding.into(),
299                    Command::ReadMeasurement,
300                    0x0006,
301                )
302                .await?;
303                let buffer = self
304                    .read_n::<17, 15>(RegFunction::ReadHolding.into())
305                    .await?;
306                Ok(Some(Measurement {
307                    co2: f32::from_bits(u32::from_be_bytes([
308                        buffer[3], buffer[4], buffer[5], buffer[6],
309                    ])),
310                    temperature: f32::from_bits(u32::from_be_bytes([
311                        buffer[7], buffer[8], buffer[9], buffer[10],
312                    ])),
313                    humidity: f32::from_bits(u32::from_be_bytes([
314                        buffer[11], buffer[12], buffer[13], buffer[14],
315                    ])),
316                }))
317            }
318            Ok(false) => Ok(None),
319            Err(e) => Err(e),
320        }
321    }
322
323    async fn check_resp_data<const NB: usize, const N: usize>(
324        &mut self,
325        func: u8,
326        high_byte_index: usize,
327        evaluate_data: u16,
328    ) -> Result<bool, Error<Serial::Error>> {
329        Timer::after(RESPONSE_TIME).await;
330        let resp = self.read_n::<NB, N>(func).await?;
331        Ok(u16::from_be_bytes([resp[high_byte_index], resp[high_byte_index + 1]]) == evaluate_data)
332    }
333
334    async fn read_n<const NB: usize, const N: usize>(
335        &mut self,
336        func: u8,
337    ) -> Result<[u8; N], Error<Serial::Error>> {
338        let mut buffer = [0u8; NB];
339
340        let mut byte_index = 0;
341        loop {
342            match byte_index {
343                0 => {
344                    let _skipped = self.skip_until_byte(MODBUS_ADDR).await?;
345                    buffer[0] = MODBUS_ADDR;
346                }
347                1 => {
348                    let byte = self.read_byte().await?;
349                    if byte == func {
350                        buffer[1] = func;
351                    } else {
352                        byte_index = 0;
353                        continue;
354                    }
355                }
356                i if i == NB => match check_crc(&buffer) {
357                    true => break,
358                    _ => return Err(Error::WrongCrc),
359                },
360                i => buffer[i] = self.read_byte().await?,
361            }
362            byte_index += 1;
363        }
364
365        let mut output = [0u8; N];
366        output.copy_from_slice(&buffer[..N]);
367
368        Ok(output)
369    }
370
371    async fn read_byte(&mut self) -> Result<u8, Error<Serial::Error>> {
372        let mut buf = [0u8; 1];
373        self.serial
374            .read_exact(&mut buf)
375            .await
376            .map_err(|e| match e {
377                ReadExactError::Other(e) => Error::Serial(e),
378                ReadExactError::UnexpectedEof => panic!(),
379            })?;
380
381        Ok(buf[0])
382    }
383
384    async fn skip_until_byte(&mut self, byte: u8) -> Result<usize, Error<Serial::Error>> {
385        let mut skipped = 0;
386        loop {
387            let read_byte = self.read_byte().await?;
388            if read_byte == byte {
389                break;
390            }
391            skipped += 1;
392        }
393        Ok(skipped)
394    }
395
396    async fn write(
397        &mut self,
398        func: u8,
399        cmd: Command,
400        data: u16,
401    ) -> Result<(), Error<Serial::Error>> {
402        let mut buffer = [0u8; 6 + 2];
403
404        buffer[0] = MODBUS_ADDR;
405        buffer[1] = func;
406
407        buffer[2..=3].copy_from_slice(&(cmd as u16).to_be_bytes());
408        buffer[4..=5].copy_from_slice(&data.to_be_bytes());
409
410        let crc = calculate_crc(&buffer[..6]);
411        buffer[6..=7].copy_from_slice(&crc.to_be_bytes());
412        self.write_n(&buffer).await
413    }
414
415    async fn write_n(&mut self, data: &[u8]) -> Result<(), Error<Serial::Error>> {
416        self.serial.write_all(data).await.map_err(Error::Serial)
417    }
418}
419
420fn calculate_crc(data: &[u8]) -> u16 {
421    let mut crc = 0xFFFFu16;
422    for d in data {
423        crc ^= u16::from(*d);
424        for _ in 0..u8::BITS {
425            if (crc & 0x0001) != 0 {
426                crc >>= 1;
427                crc ^= 0xA001;
428            } else {
429                crc >>= 1;
430            }
431        }
432    }
433    (crc & 0x00FF) << 8 | (crc & 0xFF00) >> 8
434}
435
436fn check_crc(data: &[u8]) -> bool {
437    let target_length = data.len() - 2;
438    let calculated_crc = calculate_crc(&data[..target_length]);
439
440    calculated_crc.eq(&u16::from_be_bytes([
441        data[data.len() - 2],
442        data[data.len() - 1],
443    ]))
444}
445
446#[cfg(test)]
447mod tests {
448    use crate::{calculate_crc, check_crc};
449
450    #[test]
451    fn test_crc_check() {
452        const BYTES: [u8; 3] = [0x13, 0x12, 0x14];
453        let crc = calculate_crc(&BYTES);
454        let mut bytes_with_crc = [0u8; BYTES.len() + 2];
455        bytes_with_crc[..BYTES.len()].copy_from_slice(&BYTES);
456        bytes_with_crc[BYTES.len() + 0] = u8::try_from(crc >> u8::BITS).unwrap();
457        bytes_with_crc[BYTES.len() + 1] = u8::try_from(crc & 0xFF).unwrap();
458
459        assert!(check_crc(&bytes_with_crc))
460    }
461
462    #[test]
463    fn test_crc_from_sensor() {
464        const BYTES: [u8; 8] = [97, 6, 0, 52, 0, 1, 0, 100];
465        assert!(check_crc(&BYTES))
466    }
467}