zg_co2/
lib.rs

1#![doc(html_root_url = "https://docs.rs/zg-co2/2.0.1")]
2#![deny(missing_docs)]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5//! A `no_std` crate implementing the [ZyAura ZG][ZG] CO₂ sensor protocol.
6//!
7//! This crate decodes the packets, but does not perform the decryption
8//! commonly required for USB devices using this sensor. To read data from one
9//! of the compatible commercially-available USB sensors, use the
10//! [`co2mon`][co2mon] crate.
11//!
12//! The implementation was tested using a [TFA-Dostmann AIRCO2NTROL MINI][AIRCO2NTROL MINI]
13//! sensor.
14//!
15//! [AIRCO2NTROL MINI]: https://www.tfa-dostmann.de/en/produkt/co2-monitor-airco2ntrol-mini/
16//! [ZG]: http://www.zyaura.com/products/ZG_module.asp
17//!
18//! # Example
19//!
20//! ```no_run
21//! # use zg_co2::Result;
22//! # fn main() -> Result<()> {
23//! #
24//! let packet = [0x50, 0x04, 0x57, 0xab, 0x0d];
25//! let reading = zg_co2::decode(packet)?;
26//! println!("{:?}", reading);
27//! #
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! # Features
33//!
34//! The `std` feature, enabled by default, makes [`Error`][Error] implement the
35//! [`Error`][std::error::Error] trait.
36//!
37//! # References
38//!
39//! See [this link][revspace] for more information about the protocol.
40//!
41//! [co2mon]: https://docs.rs/co2mon/
42//! [revspace]: https://revspace.nl/CO2MeterHacking
43
44use core::result;
45
46pub use error::Error;
47
48mod error;
49
50/// A specialized [`Result`][std::result::Result] type for the [`decode`] function.
51pub type Result<T> = result::Result<T, Error>;
52
53/// A single sensor reading.
54///
55/// # Example
56///
57/// ```
58/// # use zg_co2::{SingleReading, Result};
59/// # fn main() -> Result<()> {
60/// #
61/// let decoded = zg_co2::decode([0x50, 0x04, 0x57, 0xab, 0x0d])?;
62/// if let SingleReading::CO2(co2) = decoded {
63///     println!("CO₂: {} ppm", co2);
64/// }
65/// #
66/// # Ok(())
67/// # }
68/// ```
69#[derive(Debug, Clone, PartialEq, PartialOrd)]
70#[non_exhaustive]
71pub enum SingleReading {
72    /// Relative humidity
73    Humidity(f32),
74    /// Temperature in °C
75    Temperature(f32),
76    /// CO₂ concentration, measured in ppm
77    CO2(u16),
78    /// An unknown reading
79    Unknown(u8, u16),
80}
81
82/// Decodes a message from the sensor.
83///
84/// # Example
85///
86/// ```
87/// let decoded = zg_co2::decode([0x50, 0x04, 0x57, 0xab, 0x0d]);
88/// ```
89///
90/// # Errors
91///
92/// An error will be returned if the message could not be decoded.
93pub fn decode(data: [u8; 5]) -> Result<SingleReading> {
94    if data[4] != 0x0d {
95        return Err(Error::InvalidMessage);
96    }
97
98    if data[0].wrapping_add(data[1]).wrapping_add(data[2]) != data[3] {
99        return Err(Error::Checksum);
100    }
101
102    let value = u16::from(data[1]) << 8 | u16::from(data[2]);
103    let reading = match data[0] {
104        b'A' => SingleReading::Humidity(f32::from(value) * 0.01),
105        b'B' => SingleReading::Temperature(f32::from(value) * 0.0625 - 273.15),
106        b'P' => SingleReading::CO2(value),
107        _ => SingleReading::Unknown(data[0], value),
108    };
109    Ok(reading)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::{Error, SingleReading};
115
116    #[test]
117    fn test_decode() {
118        match super::decode([0x50, 0x04, 0x57, 0xab, 0x0d]) {
119            Ok(SingleReading::CO2(val)) => assert_eq!(val, 1111),
120            _ => assert!(false),
121        }
122
123        match super::decode([0x41, 0x00, 0x00, 0x41, 0x0d]) {
124            Ok(SingleReading::Humidity(val)) => assert!(val == 0.0),
125            _ => assert!(false),
126        }
127
128        match super::decode([0x42, 0x12, 0x69, 0xbd, 0x0d]) {
129            Ok(SingleReading::Temperature(val)) => assert!(val == 4713.0 * 0.0625 - 273.15),
130            _ => assert!(false),
131        }
132
133        match super::decode([0x42, 0x12, 0x69, 0xbd, 0x00]) {
134            Err(Error::InvalidMessage) => {}
135            _ => assert!(false),
136        }
137
138        match super::decode([0x42, 0x12, 0x69, 0x00, 0x0d]) {
139            Err(Error::Checksum) => {}
140            _ => assert!(false),
141        }
142    }
143}