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}