co2mon/
lib.rs

1#![doc(html_root_url = "https://docs.rs/co2mon/2.0.3")]
2#![deny(missing_docs)]
3
4//! A driver for the Holtek ([ZyAura ZG][ZG]) CO₂ USB monitors.
5//!
6//! The implementation was tested using a
7//! [TFA-Dostmann AIRCO2NTROL MINI][AIRCO2NTROL MINI] sensor.
8//!
9//! [AIRCO2NTROL MINI]: https://www.tfa-dostmann.de/en/produkt/co2-monitor-airco2ntrol-mini/
10//! [ZG]: http://www.zyaura.com/products/ZG_module.asp
11//!
12//! # Example usage
13//!
14//! ```no_run
15//! # use co2mon::{Result, Sensor};
16//! # fn main() -> Result<()> {
17//! #
18//! let sensor = Sensor::open_default()?;
19//! let reading = sensor.read_one()?;
20//! println!("{:?}", reading);
21//! #
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! # Permissions
27//!
28//! On Linux, you need to be able to access the USB HID device. For that, you
29//! can save the following `udev` rule to `/etc/udev/rules.d/60-co2mon.rules`:
30//!
31//! ```text
32//! ACTION=="add|change", SUBSYSTEMS=="usb", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", MODE:="0666"
33//! ```
34//!
35//! Then reload the rules and trigger them:
36//!
37//! ```text
38//! ## udevadm control --reload
39//! ## udevadm trigger
40//! ```
41//!
42//! Note that the `udev` rule above makes the device accessible to every local user.
43//!
44//! # References
45//!
46//! The USB HID protocol is not documented, but was [reverse-engineered][had] [before][revspace].
47//!
48//! [had]: https://hackaday.io/project/5301/
49//! [revspace]: https://revspace.nl/CO2MeterHacking
50
51use hidapi::{HidApi, HidDevice};
52use std::convert::TryFrom;
53use std::ffi::CString;
54use std::result;
55use std::time::{Duration, Instant};
56
57pub use error::Error;
58pub use zg_co2::SingleReading;
59
60mod error;
61
62/// A specialized [`Result`][std::result::Result] type for the fallible functions.
63pub type Result<T> = result::Result<T, Error>;
64
65/// A reading consisting of temperature (in °C) and CO₂ concentration (in ppm) values.
66///
67/// # Example
68///
69/// ```no_run
70/// # use co2mon::{Result, Sensor};
71/// # fn main() -> Result<()> {
72/// #
73/// let sensor = Sensor::open_default()?;
74/// let reading = sensor.read()?;
75/// println!("{} °C, {} ppm CO₂", reading.temperature(), reading.co2());
76/// #
77/// # Ok(())
78/// # }
79#[derive(Debug, Clone, PartialEq, PartialOrd)]
80pub struct Reading {
81    temperature: f32,
82    co2: u16,
83}
84
85impl Reading {
86    /// Returns the measured temperature in °C.
87    ///
88    /// # Example
89    ///
90    /// ```no_run
91    /// # use co2mon::{Result, Sensor};
92    /// # fn main() -> Result<()> {
93    /// #
94    /// let sensor = Sensor::open_default()?;
95    /// let reading = sensor.read()?;
96    /// println!("{} °C", reading.temperature());
97    /// #
98    /// # Ok(())
99    /// # }
100    pub fn temperature(&self) -> f32 {
101        self.temperature
102    }
103
104    /// Returns the CO₂ concentration in ppm (parts per million).
105    ///
106    /// # Example
107    ///
108    /// ```no_run
109    /// # use co2mon::{Result, Sensor};
110    /// # fn main() -> Result<()> {
111    /// #
112    /// let sensor = Sensor::open_default()?;
113    /// let reading = sensor.read()?;
114    /// println!("{} ppm CO₂", reading.co2());
115    /// #
116    /// # Ok(())
117    /// # }
118    pub fn co2(&self) -> u16 {
119        self.co2
120    }
121}
122
123/// Sensor driver struct.
124///
125/// # Example
126///
127/// ```no_run
128/// # use co2mon::{Result, Sensor};
129/// # fn main() -> Result<()> {
130/// #
131/// let sensor = Sensor::open_default()?;
132/// let reading = sensor.read_one()?;
133/// println!("{:?}", reading);
134/// #
135/// # Ok(())
136/// # }
137/// ```
138pub struct Sensor {
139    device: HidDevice,
140    key: [u8; 8],
141    timeout: i32,
142}
143
144impl Sensor {
145    /// Opens the sensor device using the default USB Vendor ID (`0x04d9`) and Product ID (`0xa052`) values.
146    ///
147    /// When multiple devices are connected, the first one will be used.
148    ///
149    /// # Example
150    ///
151    /// ```no_run
152    /// # use co2mon::{Result, Sensor};
153    /// # fn main() -> Result<()> {
154    /// #
155    /// let sensor = Sensor::open_default()?;
156    /// let reading = sensor.read_one()?;
157    /// println!("{:?}", reading);
158    /// #
159    /// # Ok(())
160    /// # }
161    pub fn open_default() -> Result<Self> {
162        OpenOptions::new().open()
163    }
164
165    fn open(options: &OpenOptions) -> Result<Self> {
166        let hidapi = HidApi::new()?;
167
168        const VID: u16 = 0x04d9;
169        const PID: u16 = 0xa052;
170
171        let device = match options.path_type {
172            DevicePathType::Id => hidapi.open(VID, PID),
173            DevicePathType::SerialNumber(ref sn) => hidapi.open_serial(VID, PID, sn),
174            DevicePathType::Path(ref path) => hidapi.open_path(path),
175        }?;
176
177        let key = options.key;
178
179        // fill in the Report Id
180        let frame = {
181            let mut frame = [0; 9];
182            frame[1..9].copy_from_slice(&key);
183            frame
184        };
185        device.send_feature_report(&frame)?;
186
187        let timeout = options
188            .timeout
189            .map(|timeout| timeout.as_millis())
190            .map_or(Ok(-1), i32::try_from)
191            .map_err(|_| Error::InvalidTimeout)?;
192
193        let air_control = Self {
194            device,
195            key,
196            timeout,
197        };
198        Ok(air_control)
199    }
200
201    /// Takes a single reading from the sensor.
202    ///
203    /// # Errors
204    ///
205    /// An error will be returned on an I/O error or if a message could not be
206    /// read or decoded.
207    ///
208    /// # Example
209    ///
210    /// ```no_run
211    /// # use co2mon::{Result, Sensor};
212    /// # fn main() -> Result<()> {
213    /// #
214    /// let sensor = Sensor::open_default()?;
215    /// let reading = sensor.read_one()?;
216    /// println!("{:?}", reading);
217    /// #
218    /// # Ok(())
219    /// # }
220    pub fn read_one(&self) -> Result<SingleReading> {
221        let mut data = [0; 8];
222        if self.device.read_timeout(&mut data, self.timeout)? != 8 {
223            return Err(Error::InvalidMessage);
224        }
225
226        // if the "magic byte" is present no decryption is necessary. This is the case for AIRCO2NTROL COACH
227        // and newer AIRCO2NTROL MINIs in general
228        let data = if data[4] == 0x0d {
229            data
230        } else {
231            decrypt(data, self.key)
232        };
233        let reading = zg_co2::decode([data[0], data[1], data[2], data[3], data[4]])?;
234        Ok(reading)
235    }
236
237    /// Takes a multiple readings from the sensor until the temperature and
238    /// CO₂ concentration are available, and returns both.
239    ///
240    /// # Errors
241    ///
242    /// An error will be returned on an I/O error or if a message could not be
243    /// read or decoded.
244    ///
245    /// # Example
246    ///
247    /// ```no_run
248    /// # use co2mon::{Result, Sensor};
249    /// # fn main() -> Result<()> {
250    /// #
251    /// let sensor = Sensor::open_default()?;
252    /// let reading = sensor.read()?;
253    /// println!("{} °C, {} ppm CO₂", reading.temperature(), reading.co2());
254    /// #
255    /// # Ok(())
256    /// # }
257    pub fn read(&self) -> Result<Reading> {
258        let start = Instant::now();
259        let mut temperature = None;
260        let mut co2 = None;
261        loop {
262            let reading = self.read_one()?;
263            match reading {
264                SingleReading::Temperature(val) => temperature = Some(val),
265                SingleReading::CO2(val) => co2 = Some(val),
266                _ => {}
267            }
268            if let (Some(temperature), Some(co2)) = (temperature, co2) {
269                let reading = Reading { temperature, co2 };
270                return Ok(reading);
271            }
272
273            if self.timeout != -1 {
274                let duration = Instant::now() - start;
275                if duration.as_millis() > self.timeout as u128 {
276                    return Err(Error::Timeout);
277                }
278            }
279        }
280    }
281}
282
283fn decrypt(mut data: [u8; 8], key: [u8; 8]) -> [u8; 8] {
284    data.swap(0, 2);
285    data.swap(1, 4);
286    data.swap(3, 7);
287    data.swap(5, 6);
288
289    for (r, k) in data.iter_mut().zip(key.iter()) {
290        *r ^= k;
291    }
292
293    let tmp = data[7] << 5;
294    data[7] = data[6] << 5 | data[7] >> 3;
295    data[6] = data[5] << 5 | data[6] >> 3;
296    data[5] = data[4] << 5 | data[5] >> 3;
297    data[4] = data[3] << 5 | data[4] >> 3;
298    data[3] = data[2] << 5 | data[3] >> 3;
299    data[2] = data[1] << 5 | data[2] >> 3;
300    data[1] = data[0] << 5 | data[1] >> 3;
301    data[0] = tmp | data[0] >> 3;
302
303    for (r, m) in data.iter_mut().zip(b"Htemp99e".iter()) {
304        *r = r.wrapping_sub(m << 4 | m >> 4);
305    }
306
307    data
308}
309
310#[derive(Debug, Clone)]
311enum DevicePathType {
312    Id,
313    SerialNumber(String),
314    Path(CString),
315}
316
317/// Sensor open options.
318///
319/// Opens the first available device with the USB Vendor ID `0x04d9`
320/// and Product ID `0xa052`, a `0` encryption key and a 5 seconds timeout.
321///
322/// Normally there's no need to change the encryption key.
323///
324/// # Example
325///
326/// ```no_run
327/// # use co2mon::{OpenOptions, Result};
328/// # use std::time::Duration;
329/// # fn main() -> Result<()> {
330/// #
331/// let sensor = OpenOptions::new()
332///     .timeout(Some(Duration::from_secs(10)))
333///     .open()?;
334/// #
335/// # Ok(())
336/// # }
337#[derive(Debug, Clone)]
338pub struct OpenOptions {
339    path_type: DevicePathType,
340    key: [u8; 8],
341    timeout: Option<Duration>,
342}
343
344impl Default for OpenOptions {
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350impl OpenOptions {
351    /// Creates a new set of options to be configured.
352    ///
353    /// The defaults are opening the first connected sensor and a timeout of
354    /// 5 seconds.
355    ///
356    /// # Example
357    ///
358    /// ```no_run
359    /// # use co2mon::{OpenOptions, Result};
360    /// # use std::time::Duration;
361    /// # fn main() -> Result<()> {
362    /// #
363    /// let sensor = OpenOptions::new()
364    ///     .timeout(Some(Duration::from_secs(10)))
365    ///     .open()?;
366    /// #
367    /// # Ok(())
368    /// # }
369    pub fn new() -> Self {
370        Self {
371            path_type: DevicePathType::Id,
372            key: [0; 8],
373            timeout: Some(Duration::from_secs(5)),
374        }
375    }
376
377    /// Sets the serial number of the sensor device to open.
378    ///
379    /// The serial number appears to be the firmware version.
380    ///
381    /// # Example
382    ///
383    /// ```no_run
384    /// # use co2mon::OpenOptions;
385    /// # use std::error::Error;
386    /// # use std::ffi::CString;
387    /// # use std::result::Result;
388    /// # fn main() -> Result<(), Box<Error>> {
389    /// #
390    /// let sensor = OpenOptions::new()
391    ///     .with_serial_number("1.40")
392    ///     .open()?;
393    /// #
394    /// # Ok(())
395    /// # }
396    pub fn with_serial_number<S: Into<String>>(&mut self, sn: S) -> &mut Self {
397        self.path_type = DevicePathType::SerialNumber(sn.into());
398        self
399    }
400
401    /// Sets the path to the sensor device to open.
402    ///
403    /// # Example
404    ///
405    /// ```no_run
406    /// # use co2mon::OpenOptions;
407    /// # use std::error::Error;
408    /// # use std::ffi::CString;
409    /// # use std::result::Result;
410    /// # fn main() -> Result<(), Box<Error>> {
411    /// #
412    /// let sensor = OpenOptions::new()
413    ///     .with_path(CString::new("/dev/bus/usb/001/004")?)
414    ///     .open()?;
415    /// #
416    /// # Ok(())
417    /// # }
418    pub fn with_path(&mut self, path: CString) -> &mut Self {
419        self.path_type = DevicePathType::Path(path);
420        self
421    }
422
423    /// Sets the encryption key.
424    ///
425    /// The key is used to encrypt the communication with the sensor, but
426    /// changing it is probably not very useful.
427    ///
428    /// # Example
429    ///
430    /// ```no_run
431    /// # use co2mon::{OpenOptions, Result};
432    /// # use std::ffi::CString;
433    /// # fn main() -> Result<()> {
434    /// #
435    /// let sensor = OpenOptions::new()
436    ///     .with_key([0x62, 0xea, 0x1d, 0x4f, 0x14, 0xfa, 0xe5, 0x6c])
437    ///     .open()?;
438    /// #
439    /// # Ok(())
440    /// # }
441    pub fn with_key(&mut self, key: [u8; 8]) -> &mut Self {
442        self.key = key;
443        self
444    }
445
446    /// Sets the read timeout.
447    ///
448    /// # Example
449    ///
450    /// ```no_run
451    /// # use co2mon::{OpenOptions, Result};
452    /// # use std::time::Duration;
453    /// # fn main() -> Result<()> {
454    /// #
455    /// let sensor = OpenOptions::new()
456    ///     .timeout(Some(Duration::from_secs(10)))
457    ///     .open()?;
458    /// #
459    /// # Ok(())
460    /// # }
461    pub fn timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
462        self.timeout = timeout;
463        self
464    }
465
466    /// Opens the sensor.
467    ///
468    /// # Example
469    ///
470    /// ```no_run
471    /// # use co2mon::{OpenOptions, Result};
472    /// # use std::time::Duration;
473    /// # fn main() -> Result<()> {
474    /// #
475    /// let sensor = OpenOptions::new()
476    ///     .timeout(Some(Duration::from_secs(10)))
477    ///     .open()?;
478    /// #
479    /// # Ok(())
480    /// # }
481    pub fn open(&self) -> Result<Sensor> {
482        Sensor::open(self)
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    #[test]
489    fn test_decrypt() {
490        let data = [0x6c, 0xa4, 0xa2, 0xb6, 0x5d, 0x9a, 0x9c, 0x08];
491        let key = [0; 8];
492
493        let data = super::decrypt(data, key);
494        assert_eq!(data, [0x50, 0x04, 0x57, 0xab, 0x0d, 0x00, 0x00, 0x00]);
495    }
496
497    #[test]
498    fn test_open_options_send() {
499        fn assert_send<T: Send>() {}
500        assert_send::<super::Sensor>();
501        assert_send::<super::OpenOptions>();
502    }
503
504    #[test]
505    fn test_open_options_sync() {
506        fn assert_sync<T: Sync>() {}
507        assert_sync::<super::OpenOptions>();
508    }
509}