#![doc(html_root_url = "https://docs.rs/co2mon/2.0.3")]
#![deny(missing_docs)]
use hidapi::{HidApi, HidDevice};
use std::convert::TryFrom;
use std::ffi::CString;
use std::result;
use std::time::{Duration, Instant};
pub use error::Error;
pub use zg_co2::SingleReading;
mod error;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct Reading {
temperature: f32,
co2: u16,
}
impl Reading {
pub fn temperature(&self) -> f32 {
self.temperature
}
pub fn co2(&self) -> u16 {
self.co2
}
}
pub struct Sensor {
device: HidDevice,
key: [u8; 8],
timeout: i32,
}
impl Sensor {
pub fn open_default() -> Result<Self> {
OpenOptions::new().open()
}
fn open(options: &OpenOptions) -> Result<Self> {
let hidapi = HidApi::new()?;
const VID: u16 = 0x04d9;
const PID: u16 = 0xa052;
let device = match options.path_type {
DevicePathType::Id => hidapi.open(VID, PID),
DevicePathType::SerialNumber(ref sn) => hidapi.open_serial(VID, PID, sn),
DevicePathType::Path(ref path) => hidapi.open_path(path),
}?;
let key = options.key;
let frame = {
let mut frame = [0; 9];
frame[1..9].copy_from_slice(&key);
frame
};
device.send_feature_report(&frame)?;
let timeout = options
.timeout
.map(|timeout| timeout.as_millis())
.map_or(Ok(-1), i32::try_from)
.map_err(|_| Error::InvalidTimeout)?;
let air_control = Self {
device,
key,
timeout,
};
Ok(air_control)
}
pub fn read_one(&self) -> Result<SingleReading> {
let mut data = [0; 8];
if self.device.read_timeout(&mut data, self.timeout)? != 8 {
return Err(Error::InvalidMessage);
}
let data = if data[4] == 0x0d {
data
} else {
decrypt(data, self.key)
};
let reading = zg_co2::decode([data[0], data[1], data[2], data[3], data[4]])?;
Ok(reading)
}
pub fn read(&self) -> Result<Reading> {
let start = Instant::now();
let mut temperature = None;
let mut co2 = None;
loop {
let reading = self.read_one()?;
match reading {
SingleReading::Temperature(val) => temperature = Some(val),
SingleReading::CO2(val) => co2 = Some(val),
_ => {}
}
if let (Some(temperature), Some(co2)) = (temperature, co2) {
let reading = Reading { temperature, co2 };
return Ok(reading);
}
if self.timeout != -1 {
let duration = Instant::now() - start;
if duration.as_millis() > self.timeout as u128 {
return Err(Error::Timeout);
}
}
}
}
}
fn decrypt(mut data: [u8; 8], key: [u8; 8]) -> [u8; 8] {
data.swap(0, 2);
data.swap(1, 4);
data.swap(3, 7);
data.swap(5, 6);
for (r, k) in data.iter_mut().zip(key.iter()) {
*r ^= k;
}
let tmp = data[7] << 5;
data[7] = data[6] << 5 | data[7] >> 3;
data[6] = data[5] << 5 | data[6] >> 3;
data[5] = data[4] << 5 | data[5] >> 3;
data[4] = data[3] << 5 | data[4] >> 3;
data[3] = data[2] << 5 | data[3] >> 3;
data[2] = data[1] << 5 | data[2] >> 3;
data[1] = data[0] << 5 | data[1] >> 3;
data[0] = tmp | data[0] >> 3;
for (r, m) in data.iter_mut().zip(b"Htemp99e".iter()) {
*r = r.wrapping_sub(m << 4 | m >> 4);
}
data
}
#[derive(Debug, Clone)]
enum DevicePathType {
Id,
SerialNumber(String),
Path(CString),
}
#[derive(Debug, Clone)]
pub struct OpenOptions {
path_type: DevicePathType,
key: [u8; 8],
timeout: Option<Duration>,
}
impl Default for OpenOptions {
fn default() -> Self {
Self::new()
}
}
impl OpenOptions {
pub fn new() -> Self {
Self {
path_type: DevicePathType::Id,
key: [0; 8],
timeout: Some(Duration::from_secs(5)),
}
}
pub fn with_serial_number<S: Into<String>>(&mut self, sn: S) -> &mut Self {
self.path_type = DevicePathType::SerialNumber(sn.into());
self
}
pub fn with_path(&mut self, path: CString) -> &mut Self {
self.path_type = DevicePathType::Path(path);
self
}
pub fn with_key(&mut self, key: [u8; 8]) -> &mut Self {
self.key = key;
self
}
pub fn timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
self.timeout = timeout;
self
}
pub fn open(&self) -> Result<Sensor> {
Sensor::open(self)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_decrypt() {
let data = [0x6c, 0xa4, 0xa2, 0xb6, 0x5d, 0x9a, 0x9c, 0x08];
let key = [0; 8];
let data = super::decrypt(data, key);
assert_eq!(data, [0x50, 0x04, 0x57, 0xab, 0x0d, 0x00, 0x00, 0x00]);
}
#[test]
fn test_open_options_send() {
fn assert_send<T: Send>() {}
assert_send::<super::Sensor>();
assert_send::<super::OpenOptions>();
}
#[test]
fn test_open_options_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<super::OpenOptions>();
}
}