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}