1#![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
89pub trait BaseApi<E> {
91 fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>>;
93
94 fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>>;
96
97 fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>>;
102}
103
104#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct Co2AndTemperature {
107 pub co2_ppm: u16,
109 pub temp_celsius: f32,
111}
112
113pub trait Firmware5Api<E>: BaseApi<E> {
115 fn read_co2_and_temp(&mut self) -> nb::Result<Co2AndTemperature, Error<E>>;
117}
118
119#[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 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 pub fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
171 BaseApi::read_co2_ppm(self)
172 }
173
174 pub fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
176 BaseApi::get_firmware_version(self)
177 }
178
179 pub fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
184 BaseApi::set_self_calibrate(self, enabled)
185 }
186
187 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 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
338pub 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 pub fn read_co2_ppm(&mut self) -> nb::Result<u16, Error<E>> {
352 BaseApi::read_co2_ppm(self)
353 }
354
355 pub fn get_firmware_version(&mut self) -> nb::Result<[u8; 4], Error<E>> {
357 BaseApi::get_firmware_version(self)
358 }
359
360 pub fn set_self_calibrate(&mut self, enabled: bool) -> nb::Result<(), Error<E>> {
365 BaseApi::set_self_calibrate(self, enabled)
366 }
367
368 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 ValidateFrameError(ValidateFrameError),
437 NotAResponse,
439 OpCodeMismatch { expected: u8, got: u8 },
441 UartError(T),
443 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(); }
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}