mh_z19c/
lib.rs

1//! Crate to read out the Winsen MH-Z19C CO2 sensor.
2//!
3//! This crate provides an API to read-out the nondispersive infrared (NDIR)
4//! CO₂ sensor MH-Z19C by Winsen via the serial (UART) interface.
5//!
6//! The provided API supports non-blocking usage and is `no_std`.
7//!
8//!
9//! # Examples
10//!
11//! ```
12//! use mh_z19c::MhZ19C;
13//! use nb::block;
14//!
15//! # use test_support::{create_serial_mock_returning, READ_CO2_RESPONSE};
16//! # fn main() -> Result<(), mh_z19c::Error<String>> {
17//! # let uart = create_serial_mock_returning(&READ_CO2_RESPONSE);
18//! let mut co2sensor = MhZ19C::new(uart);
19//! let co2 = block!(co2sensor.read_co2_ppm())?;
20//! println!("CO₂ concentration: {}ppm", co2);
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! To activate features of a sensor with a firmware of version 5:
26//!
27//! ```
28//! # use mh_z19c::MhZ19C;
29//! # use nb::block;
30//! #
31//! # use test_support::{
32//! #     serial_mock::SerialMock,
33//! #     FIRMWARE_0515_RESPONSE,
34//! #     READ_CO2_AND_TEMPERATURE_RESPONSE
35//! # };
36//! # fn main() -> Result<(), mh_z19c::Error<String>> {
37//! # let mut responses: Vec<nb::Result<u8, String>> = FIRMWARE_0515_RESPONSE
38//! #     .iter()
39//! #     .chain(READ_CO2_AND_TEMPERATURE_RESPONSE.iter())
40//! #     .copied()
41//! #     .map(Ok)
42//! #     .collect();
43//! # let uart = SerialMock::new(responses, vec![Ok(()); 27]);
44//! # let mut co2sensor = MhZ19C::new(uart);
45//! let mut co2sensor = block!(co2sensor.upgrade_to_v5())?;
46//! let co2_temp = block!(co2sensor.read_co2_and_temp())?;
47//! println!("Temperature: {}°C", co2_temp.temp_celsius);
48//! # Ok(())
49//! # }
50//! ```
51//!
52//!
53//! # no_std
54//!
55//! This crate is `no_std` by default, unless the `std` feature is activated.
56//! Currently, the `std` feature will only add [`std::error::Error`] trait
57//! implementations to the error types.
58//!
59//!
60//! # Versioning
61//!
62//! This crate uses [Semantic Versioning](https://semver.org/).
63
64#![cfg_attr(not(feature = "std"), no_std)]
65
66#[cfg(doc)]
67extern crate std;
68
69#[macro_use]
70extern crate lazy_static;
71
72use crate::command::Command;
73use crate::frame::{Frame, ValidateFrameError};
74use crate::nb_comm::{NbFuture, WriteAll, WriteAndReadResponse};
75use core::convert::TryInto;
76use core::fmt::{self, Display};
77use embedded_hal::serial::{Read, Write};
78
79pub mod command;
80pub mod frame;
81mod nb_comm;
82
83lazy_static! {
84    static ref READ_CO2_AND_TEMPERATURE: Frame = Command::ReadCo2AndTemperature.into();
85    static ref READ_CO2: Frame = Command::ReadCo2.into();
86    static ref GET_FIRMWARE_VERSION: Frame = Command::GetFirmwareVersion.into();
87}
88
89/// Methods supported by all MH-Z19C sensors.
90pub trait BaseApi<E> {
91    /// Reads and returns the CO₂ concentration in parts-per-million (ppm).
92    fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>>;
93
94    /// Retrieves the firmware version of the sensor.
95    fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>>;
96
97    /// Activates or deactivates the sensor's self-calibration mode.
98    ///
99    /// See the sensor's data sheet for more information on self-calibration
100    /// and hand-operated mode.
101    fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>>;
102}
103
104/// Data-transfer object for combined measurement of CO₂ and temperature.
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct Co2AndTemperature {
107    /// CO₂ concentration in parts per million (ppm).
108    pub co2_ppm: u16,
109    /// Temperature in degrees Celsius (°C).
110    pub temp_celsius: f32,
111}
112
113/// Methods supported by all MH-Z19C sensors with firmware 5.
114pub trait Firmware5Api<E>: BaseApi<E> {
115    /// Reads the CO₂ concentration and temperature.
116    fn read_co2_and_temp(&mut self) -> nb::Result<Co2AndTemperature, Error<E>>;
117}
118
119/// Driver for the MH-Z19C sensor.
120#[derive(Debug)]
121pub struct MhZ19C<'a, U, E>
122where
123    U: Read<u8, Error = E> + Write<u8, Error = E>,
124{
125    state: MhZ19CState<'a, U, E>,
126    uart: Option<U>,
127}
128
129#[derive(Debug)]
130enum MhZ19CState<'a, U, E>
131where
132    U: Read<u8, Error = E> + Write<u8, Error = E>,
133{
134    Idle,
135    ReadCo2AndTemperature(WriteAndReadResponse<U, E, &'a [u8], [u8; 9]>),
136    ReadCo2(WriteAndReadResponse<U, E, &'a [u8], [u8; 9]>),
137    GetFirmwareVersion(WriteAndReadResponse<U, E, &'a [u8], [u8; 9]>),
138    SetSelfCalibrate(WriteAll<U, E, Frame>),
139}
140
141impl<'a, U, E> Default for MhZ19CState<'a, U, E>
142where
143    U: Read<u8, Error = E> + Write<u8, Error = E>,
144{
145    fn default() -> Self {
146        Self::Idle
147    }
148}
149
150impl<'a, U, E> MhZ19C<'a, U, E>
151where
152    U: Read<u8, Error = E> + Write<u8, Error = E>,
153{
154    /// Create a new instance.
155    ///
156    /// * `uart`: Serial (UART) interface for communication with the sensor.
157    pub fn new(uart: U) -> Self {
158        Self {
159            state: MhZ19CState::default(),
160            uart: Some(uart),
161        }
162    }
163}
164
165impl<'a, U, E> MhZ19C<'a, U, E>
166where
167    U: Read<u8, Error = E> + Write<u8, Error = E>,
168{
169    /// Reads and returns the CO₂ concentration in parts-per-million (ppm).
170    pub fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
171        BaseApi::read_co2_ppm(self)
172    }
173
174    /// Retrieves the firmware version of the sensor.
175    pub fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
176        BaseApi::get_firmware_version(self)
177    }
178
179    /// Activates or deactivates the sensor's self-calibration mode.
180    ///
181    /// See the sensor's data sheet for more information on self-calibration
182    /// and hand-operated mode.
183    pub fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
184        BaseApi::set_self_calibrate(self, enabled)
185    }
186
187    /// Returns the owned UART interface.vec!
188    ///
189    /// Note that this might leave the interface with partially written or read
190    /// bytes on the UART interface if not all MH-Z19C commands have been polled
191    /// to completion (i.e. the last command call did not return
192    /// [`nb::Error::WouldBlock`]).
193    pub fn into_inner(mut self) -> U {
194        use MhZ19CState::*;
195        match self.state {
196            Idle => self.uart.take().unwrap(),
197            ReadCo2AndTemperature(future) => future.into_return_value().0,
198            ReadCo2(future) => future.into_return_value().0,
199            GetFirmwareVersion(future) => future.into_return_value().0,
200            SetSelfCalibrate(future) => future.into_return_value(),
201        }
202    }
203
204    /// Will make the [`Firmware5Api`] capabilities available.
205    ///
206    /// If the sensor firmware is not at least of version 5, an error will be
207    /// returned.
208    pub fn upgrade_to_v5<'b>(&'b mut self) -> nb::Result<MhZ19CFw5<'a, 'b, U, E>, Error<E>> {
209        let fw_version = self.get_firmware_version()?;
210
211        if fw_version[1] >= b'5' {
212            Ok(MhZ19CFw5 { mh_z19c: self })
213        } else {
214            Err(nb::Error::Other(Error::NotSupportedByFirmware(fw_version)))
215        }
216    }
217
218    fn poll(&mut self) -> nb::Result<(), Error<E>> {
219        use MhZ19CState::*;
220        match &mut self.state {
221            Idle => Ok(()),
222            ReadCo2AndTemperature(future) => future.poll(),
223            ReadCo2(future) => future.poll(),
224            GetFirmwareVersion(future) => future.poll(),
225            SetSelfCalibrate(future) => future.poll(),
226        }
227        .map_err(|err| err.map(Error::UartError))
228    }
229
230    fn recover_uart(&mut self, state: MhZ19CState<U, E>) {
231        use MhZ19CState::*;
232        match state {
233            Idle => (),
234            ReadCo2AndTemperature(future) => self.uart = Some(future.into_return_value().0),
235            ReadCo2(future) => self.uart = Some(future.into_return_value().0),
236            GetFirmwareVersion(future) => self.uart = Some(future.into_return_value().0),
237            SetSelfCalibrate(future) => self.uart = Some(future.into_return_value()),
238        }
239    }
240
241    fn unpack_return_frame(command: Command, frame: &Frame) -> Result<&[u8], Error<E>> {
242        frame.validate().map_err(Error::ValidateFrameError)?;
243        if !frame.is_response() {
244            Err(Error::NotAResponse)
245        } else if frame.op_code() != command.op_code() {
246            Err(Error::OpCodeMismatch {
247                expected: command.op_code(),
248                got: frame.op_code(),
249            })
250        } else {
251            Ok(frame.data())
252        }
253    }
254}
255
256impl<'a, U, E> BaseApi<E> for MhZ19C<'a, U, E>
257where
258    U: Read<u8, Error = E> + Write<u8, Error = E>,
259{
260    fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
261        loop {
262            if let MhZ19CState::Idle = &mut self.state {
263                let uart = self.uart.take().unwrap();
264                self.state = MhZ19CState::ReadCo2(WriteAndReadResponse::new(
265                    uart,
266                    READ_CO2.as_ref(),
267                    [0u8; 9],
268                    9,
269                ));
270            }
271
272            self.poll()?;
273
274            let state = core::mem::take(&mut self.state);
275            if let MhZ19CState::ReadCo2(future) = state {
276                let (uart, buf) = future.into_return_value();
277                self.uart = Some(uart);
278                let frame = Frame::new(buf);
279                let data = Self::unpack_return_frame(Command::ReadCo2, &frame)
280                    .map_err(nb::Error::Other)?;
281                return Ok(u16::from_be_bytes(data[..2].try_into().unwrap()));
282            } else {
283                self.recover_uart(state);
284            }
285        }
286    }
287
288    fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
289        loop {
290            if let MhZ19CState::Idle = &mut self.state {
291                let uart = self.uart.take().unwrap();
292                self.state = MhZ19CState::GetFirmwareVersion(WriteAndReadResponse::new(
293                    uart,
294                    GET_FIRMWARE_VERSION.as_ref(),
295                    [0u8; 9],
296                    9,
297                ));
298            }
299
300            self.poll()?;
301
302            let state = core::mem::take(&mut self.state);
303            if let MhZ19CState::GetFirmwareVersion(future) = state {
304                let (uart, buf) = future.into_return_value();
305                self.uart = Some(uart);
306                let frame = Frame::new(buf);
307                let data = Self::unpack_return_frame(Command::GetFirmwareVersion, &frame)
308                    .map_err(nb::Error::Other)?;
309                let ret_data = [data[0], data[1], data[2], data[3]];
310                return Ok(ret_data);
311            } else {
312                self.recover_uart(state);
313            }
314        }
315    }
316
317    fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
318        loop {
319            if let MhZ19CState::Idle = &mut self.state {
320                let uart = self.uart.take().unwrap();
321                let frame: Frame = Command::SetSelfCalibrate(enabled).into();
322                self.state = MhZ19CState::SetSelfCalibrate(WriteAll::new(uart, frame));
323            }
324
325            self.poll()?;
326
327            let state = core::mem::take(&mut self.state);
328            if let MhZ19CState::SetSelfCalibrate(future) = state {
329                self.uart = Some(future.into_return_value());
330                return Ok(());
331            } else {
332                self.recover_uart(state);
333            }
334        }
335    }
336}
337
338/// Driver for the MH-Z19C sensor with firmware 5 capabilities.
339pub struct MhZ19CFw5<'a, 'b, U, E>
340where
341    U: Read<u8, Error = E> + Write<u8, Error = E>,
342{
343    mh_z19c: &'b mut MhZ19C<'a, U, E>,
344}
345
346impl<'a, 'b, U, E> MhZ19CFw5<'a, 'b, U, E>
347where
348    U: Read<u8, Error = E> + Write<u8, Error = E>,
349{
350    /// Reads and returns the CO₂ concentration in parts-per-million (ppm).
351    pub fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
352        BaseApi::read_co2_ppm(self)
353    }
354
355    /// Retrieves the firmware version of the sensor.
356    pub fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
357        BaseApi::get_firmware_version(self)
358    }
359
360    /// Activates or deactivates the sensor's self-calibration mode.
361    ///
362    /// See the sensor's data sheet for more information on self-calibration
363    /// and hand-operated mode.
364    pub fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
365        BaseApi::set_self_calibrate(self, enabled)
366    }
367
368    /// Reads the CO₂ concentration and temperature.
369    pub fn read_co2_and_temp(&mut self) -> nb::Result<Co2AndTemperature, Error<E>> {
370        Firmware5Api::read_co2_and_temp(self)
371    }
372}
373
374impl<'a, 'b, U, E> BaseApi<E> for MhZ19CFw5<'a, 'b, U, E>
375where
376    U: Read<u8, Error = E> + Write<u8, Error = E>,
377{
378    fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
379        self.mh_z19c.read_co2_ppm()
380    }
381
382    fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
383        self.mh_z19c.get_firmware_version()
384    }
385
386    fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
387        self.mh_z19c.set_self_calibrate(enabled)
388    }
389}
390
391impl<'a, 'b, U, E> Firmware5Api<E> for MhZ19CFw5<'a, 'b, U, E>
392where
393    U: Read<u8, Error = E> + Write<u8, Error = E>,
394{
395    fn read_co2_and_temp(&mut self) -> nb::Result<Co2AndTemperature, Error<E>> {
396        loop {
397            if let MhZ19CState::Idle = &mut self.mh_z19c.state {
398                let uart = self.mh_z19c.uart.take().unwrap();
399                self.mh_z19c.state = MhZ19CState::ReadCo2AndTemperature(WriteAndReadResponse::new(
400                    uart,
401                    READ_CO2_AND_TEMPERATURE.as_ref(),
402                    [0u8; 9],
403                    9,
404                ));
405            }
406
407            self.mh_z19c.poll()?;
408
409            let state = core::mem::take(&mut self.mh_z19c.state);
410            if let MhZ19CState::ReadCo2AndTemperature(future) = state {
411                let (uart, buf) = future.into_return_value();
412                self.mh_z19c.uart = Some(uart);
413                let frame = Frame::new(buf);
414                let data =
415                    MhZ19C::<'a, U, E>::unpack_return_frame(Command::ReadCo2AndTemperature, &frame)
416                        .map_err(nb::Error::Other)?;
417
418                let co2_ppm = u16::from_be_bytes(data[2..4].try_into().unwrap());
419                let temp_celsius =
420                    f32::from(u16::from_be_bytes(data[..2].try_into().unwrap())) / 100.0;
421
422                return Ok(Co2AndTemperature {
423                    co2_ppm,
424                    temp_celsius,
425                });
426            } else {
427                self.mh_z19c.recover_uart(state);
428            }
429        }
430    }
431}
432
433#[derive(Debug, PartialEq, Eq)]
434pub enum Error<T> {
435    /// The frame of a command or return value was invalid.
436    ValidateFrameError(ValidateFrameError),
437    /// The received data is not response.
438    NotAResponse,
439    /// Received a response for a different op code than expected.
440    OpCodeMismatch { expected: u8, got: u8 },
441    /// Communication error caused by the UART/serial interface.
442    UartError(T),
443    /// Cannot upgrade to requested firmware version.
444    /// Firmware version reported by the sensor will be included.
445    NotSupportedByFirmware([u8; 4]),
446}
447
448impl<T: Display> Display for Error<T> {
449    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result {
450        match self {
451            Self::ValidateFrameError(err) => write!(f, "frame error: {err}"),
452            Self::NotAResponse => write!(f, "expected response, but got command"),
453            Self::OpCodeMismatch { expected, got } => write!(
454                f,
455                "expected response for op code 0x{expected:x}, but got op code 0x{got:x}"
456            ),
457            Self::UartError(err) => write!(f, "UART communication error: {err}"),
458            Self::NotSupportedByFirmware(version) => {
459                write!(
460                    f,
461                    "not supported by firmware version {}",
462                    core::str::from_utf8(version).unwrap_or("<invalid version string>")
463                )
464            }
465        }
466    }
467}
468
469#[cfg(std)]
470impl std::error::Error for Error {}
471
472#[cfg(test)]
473#[macro_use]
474extern crate std;
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    use nb::block;
481    use std::string::String;
482    use std::vec::Vec;
483    use test_support::serial_mock::SerialMock;
484    use test_support::{
485        create_serial_mock_returning, FIRMWARE_0400_RESPONSE, FIRMWARE_0515_RESPONSE,
486        READ_CO2_AND_TEMPERATURE_RESPONSE, READ_CO2_RESPONSE, SELF_CALIBRATE_ON_COMMAND,
487    };
488
489    #[test]
490    fn test_read_co2() {
491        let uart = create_serial_mock_returning(&READ_CO2_RESPONSE);
492        let mut co2sensor = MhZ19C::new(uart);
493        let co2 = block!(co2sensor.read_co2_ppm());
494        let uart = co2sensor.into_inner();
495        assert_eq!(uart.write_buf, &*READ_CO2.as_ref());
496        assert_eq!(co2, Ok(800));
497    }
498
499    #[test]
500    fn test_read_co2_uart_error() {
501        let uart = SerialMock::new(
502            vec![Err(nb::Error::Other("No more data.".into()))],
503            vec![Ok(()); 9],
504        );
505        let mut co2sensor = MhZ19C::new(uart);
506        assert_eq!(
507            block!(co2sensor.read_co2_ppm()),
508            Err(Error::UartError("No more data.".into()))
509        );
510    }
511
512    #[test]
513    fn test_read_co2_invalid_start_byte() {
514        let mut response = READ_CO2_RESPONSE.clone();
515        response[0] = 0x00;
516        let uart = create_serial_mock_returning(&response);
517        let mut co2sensor = MhZ19C::new(uart);
518        assert_eq!(
519            block!(co2sensor.read_co2_ppm()),
520            Err(Error::ValidateFrameError(
521                ValidateFrameError::InvalidStartByte(0x00)
522            ))
523        );
524    }
525
526    #[test]
527    fn test_read_co2_invalid_checksum() {
528        let mut response = READ_CO2_RESPONSE.clone();
529        response[8] = 0x00;
530        let uart = create_serial_mock_returning(&response);
531        let mut co2sensor = MhZ19C::new(uart);
532        assert_eq!(
533            block!(co2sensor.read_co2_ppm()),
534            Err(Error::ValidateFrameError(
535                ValidateFrameError::InvalidChecksum {
536                    expected: READ_CO2_RESPONSE[8],
537                    actual: 0x00
538                }
539            ))
540        );
541    }
542
543    #[test]
544    fn test_set_self_calibrate() -> Result<(), Error<String>> {
545        let uart = create_serial_mock_returning(&[]);
546        let mut co2sensor = MhZ19C::new(uart);
547        block!(co2sensor.set_self_calibrate(true))?;
548        let uart = co2sensor.into_inner();
549        assert_eq!(uart.write_buf, &*SELF_CALIBRATE_ON_COMMAND.as_ref());
550        Ok(())
551    }
552
553    #[test]
554    fn test_set_self_calibrate_uart_error() {
555        let uart = SerialMock::new(vec![], vec![Err(nb::Error::Other("No more data.".into()))]);
556        let mut co2sensor = MhZ19C::new(uart);
557        assert_eq!(
558            block!(co2sensor.set_self_calibrate(true)),
559            Err(Error::UartError("No more data.".into()))
560        );
561    }
562
563    #[test]
564    fn test_into_inner_during_read() {
565        let uart = SerialMock::new(vec![], vec![Err(nb::Error::WouldBlock)]);
566        let mut co2sensor = MhZ19C::new(uart);
567        assert_eq!(co2sensor.read_co2_ppm(), Err(nb::Error::WouldBlock));
568        let _ = co2sensor.into_inner(); // Must not panic
569    }
570
571    #[test]
572    fn test_ignore_read_co2_result_by_polling_set_self_calibrate() {
573        let mut read_data = Vec::with_capacity(10);
574        read_data.push(Err(nb::Error::WouldBlock));
575        read_data.extend(READ_CO2_RESPONSE.iter().copied().map(Ok));
576        let uart = SerialMock::new(read_data, vec![Ok(()); 2 * 9]);
577        let mut co2sensor = MhZ19C::new(uart);
578        assert_eq!(co2sensor.read_co2_ppm(), Err(nb::Error::WouldBlock));
579        assert_eq!(block!(co2sensor.set_self_calibrate(true)), Ok(()));
580    }
581
582    #[test]
583    fn test_read_co2_without_waiting_for_set_self_calibrate() {
584        let mut write_return_values = vec![Ok(()); 2 * 9 + 1];
585        write_return_values[0] = Err(nb::Error::WouldBlock);
586        let uart = SerialMock::new(
587            READ_CO2_RESPONSE.iter().copied().map(Ok).collect(),
588            write_return_values,
589        );
590        let mut co2sensor = MhZ19C::new(uart);
591        assert_eq!(
592            co2sensor.set_self_calibrate(true),
593            Err(nb::Error::WouldBlock)
594        );
595        assert_eq!(block!(co2sensor.read_co2_ppm()), Ok(800));
596    }
597
598    #[test]
599    fn test_get_firmware_version() {
600        let uart = create_serial_mock_returning(&FIRMWARE_0515_RESPONSE);
601        let mut co2sensor = MhZ19C::new(uart);
602        let firmware = block!(co2sensor.get_firmware_version());
603        let uart = co2sensor.into_inner();
604        assert_eq!(uart.write_buf, &*GET_FIRMWARE_VERSION.as_ref());
605        assert_eq!(firmware, Ok(*b"0515"));
606    }
607
608    #[test]
609    fn test_get_firmware_version_error() {
610        let uart = SerialMock::new(
611            vec![Err(nb::Error::Other("No more data.".into()))],
612            vec![Ok(()); 9],
613        );
614        let mut co2sensor = MhZ19C::new(uart);
615        assert_eq!(
616            block!(co2sensor.get_firmware_version()),
617            Err(Error::UartError("No more data.".into()))
618        );
619    }
620
621    #[test]
622    fn test_upgrade_to_v5() {
623        let uart = create_serial_mock_returning(&FIRMWARE_0515_RESPONSE);
624        let mut co2sensor = MhZ19C::new(uart);
625        block!(co2sensor.upgrade_to_v5()).unwrap();
626    }
627
628    #[test]
629    fn test_upgrade_to_v5_error() {
630        let uart = create_serial_mock_returning(&FIRMWARE_0400_RESPONSE);
631        let mut co2sensor = MhZ19C::new(uart);
632        assert_eq!(
633            block!(co2sensor.upgrade_to_v5()).err(),
634            Some(Error::NotSupportedByFirmware(*b"0400"))
635        );
636    }
637
638    #[test]
639    fn test_read_co2_and_temperature() {
640        let mut responses: Vec<nb::Result<u8, String>> = FIRMWARE_0515_RESPONSE
641            .iter()
642            .chain(READ_CO2_AND_TEMPERATURE_RESPONSE.iter())
643            .copied()
644            .map(Ok)
645            .collect();
646        responses.push(Err(nb::Error::Other("No more data.".into())));
647        let uart = SerialMock::new(responses, vec![Ok(()); 27]);
648        let mut co2sensor = MhZ19C::new(uart);
649        let mut co2sensor = block!(co2sensor.upgrade_to_v5()).unwrap();
650
651        assert_eq!(
652            block!(co2sensor.read_co2_and_temp()),
653            Ok(Co2AndTemperature {
654                co2_ppm: 800,
655                temp_celsius: 24.
656            })
657        );
658        assert_eq!(
659            block!(co2sensor.read_co2_and_temp()),
660            Err(Error::UartError("No more data.".into()))
661        );
662    }
663}