use std::error::Error;
use hidapi::{HidApi, DeviceInfo, HidDevice};
#[cfg(feature="serde")]
use serde::{Serialize, Deserialize};
const CO2MON_HID_VENDOR_ID : u16 = 0x04d9;
const CO2MON_HID_PRODUCT_ID : u16 = 0xa052;
const CO2MON_MAGIC_WORD : &str = "Htemp99e";
const CODE_END_MESSAGE : u8 = 0x0D;
const CODE_CO2 : u8 = 0x50;
const CODE_TEMPERATURE : u8 = 0x42;
fn convert_temperature_to_celcius(temp : u16) -> f32 {
temp as f32 * 0.0625 - 273.15
}
fn list_to_u64(x: &[u8]) -> u64 {
x[7] as u64 +
((x[6] as u64) << 8) +
((x[5] as u64) << 16) +
((x[4] as u64) << 24) +
((x[3] as u64) << 32) +
((x[2] as u64) << 40) +
((x[1] as u64) << 48) +
((x[0] as u64) << 56)
}
fn u64_to_list(x:u64)->[u8;8]{
let mut list = [0_u8;8];
list[0] = ((x >> 56) & 0xFF) as u8;
list[1] = ((x >> 48) & 0xFF) as u8;
list[2] = ((x >> 40) & 0xFF) as u8;
list[3] = ((x >> 32) & 0xFF) as u8;
list[4] = ((x >> 24) & 0xFF) as u8;
list[5] = ((x >> 16) & 0xFF) as u8;
list[6] = ((x >> 8) & 0xFF) as u8;
list[7] = (x & 0xFF) as u8;
list
}
fn get_magic_word() -> [u8;8]{
let mut list = [0_u8;8];
let bytes = CO2MON_MAGIC_WORD.bytes();
let mut i = 0;
for byte in bytes {
list[i] = (byte << 4) | (byte >> 4);
i+=1;
}
list
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CO2Reading{
pub co2_ppm: u32,
pub temp_c: f32,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug,Clone)]
pub struct CO2MonitorInfo{
pub vendor_id : u16,
pub product_id : u16,
pub path: String,
pub manufacturer : String,
pub product_name: String,
pub serial_no: String,
}
pub struct CO2Monitor{
bypass_decrypt : bool,
hid : HidApi,
device_info : DeviceInfo,
device : Option<HidDevice>,
magic_table : [u8;8],
}
impl CO2Monitor {
pub fn default() -> Result<CO2Monitor, Box<dyn Error>> {
Self::new(false, None)
}
pub fn new(bypass_decrypt: bool, interface_path: Option<String>) -> Result<CO2Monitor, Box<dyn Error>> {
let hid = HidApi::new()?;
let device_info = Self::find_device(&hid, interface_path).ok_or("Unable to find the hid device")?;
Ok(CO2Monitor {
bypass_decrypt,
hid,
device_info,
device:None,
magic_table : [0_u8;8],
})
}
pub fn info(&self) -> CO2MonitorInfo {
CO2MonitorInfo {
vendor_id: self.device_info.vendor_id(),
product_id: self.device_info.product_id(),
path: String::from(self.device_info.path().to_str().unwrap_or("Error")),
manufacturer: String::from(self.device_info.manufacturer_string().unwrap_or("None provided")),
product_name: String::from(self.device_info.product_string().unwrap_or("None provided")),
serial_no: String::from(self.device_info.serial_number().unwrap_or("None provided"))
}
}
fn find_device(hid: &HidApi, interface_path: Option<String>) -> Option<DeviceInfo>{
for device in hid.device_list(){
if device.vendor_id() == CO2MON_HID_VENDOR_ID &&
device.product_id() == CO2MON_HID_PRODUCT_ID {
if interface_path.is_some() &&
(device.path().to_str().unwrap() != interface_path.as_ref().unwrap().as_str()){
continue;
}
return Some(device.clone());
}
}
None
}
fn hid_open(&mut self, send_magic_tables : bool) -> Result<(), Box<dyn Error>>{
assert!(self.device.is_none());
self.device = Some(self.device_info.open_device(&self.hid)?);
if send_magic_tables{
self.device.as_ref().ok_or("No device found to send tables to. Strange.")?.send_feature_report(&self.magic_table)?;
}
Ok(())
}
fn hid_close(&mut self) -> Result<(), Box<dyn Error>>{
assert!(self.device.is_some());
self.device = None; Ok(())
}
fn hid_read(&mut self) -> Result<[u8;8], Box<dyn Error>>{
let mut data : [u8;8] = [0;8];
self.device.as_ref().ok_or("Device is not opened. Call hid_open before hid_read()")?.read(&mut data)?;
Ok(self.decrypt(data))
}
fn decrypt(&self, data : [u8;8]) -> [u8;8] {
if self.bypass_decrypt{
return data;
}
let rearranged_data : [u8;8] = [
data[2],
data[4],
data[0],
data[7],
data[1],
data[6],
data[5],
data[3]
];
let message = list_to_u64(&rearranged_data);
let mut result = message ^ list_to_u64(&self.magic_table);
result = (result >> 3) | (result << 61);
let result_list = u64_to_list(result);
let magic_word = get_magic_word();
let mut i = 0;
result_list.map(|r| r.wrapping_sub(magic_word[{i+=1;i-1}]))
}
fn decode_message(&self, msg : [u8;8]) -> (Option<u32>,Option<f32>){
if msg[5]!=0 || msg[6]!=0 || msg[7] !=0 || msg[4]!= CODE_END_MESSAGE{
return (None, None);
}
if (msg[0].wrapping_add(msg[1]).wrapping_add(msg[2])) != msg[3]{
return (None,None);
}
let value : u16 = ((msg[1] as u16) << 8) | msg[2] as u16;
match msg[0] {
CODE_CO2 => (Some(value as u32), None),
CODE_TEMPERATURE => (None, Some(convert_temperature_to_celcius(value))),
_ =>(None,None),
}
}
fn read_data_inner(&mut self, max_requests: u32) -> Result<CO2Reading, Box<dyn Error>>{
let mut co2 : Option<u32> = None;
let mut temp : Option<f32> = None;
let mut request_num = 0;
while (request_num < max_requests) ^ (co2.is_some() && temp.is_some()) {
let data = self.hid_read()?;
let message = self.decode_message(data);
match message {
(co2_val,None) => {co2 = co2_val},
(None, temp_val) => {temp = temp_val},
_ => {},
}
request_num += 1;
}
Ok(CO2Reading {
co2_ppm: co2.ok_or("Unable to read the co2 in the allotted number of requests")?,
temp_c : temp.ok_or("Unable to read the temperature in the allotted number of requests")?,
})
}
pub fn read_data(&mut self, max_requests: u32) -> Result<CO2Reading, Box<dyn Error>>{
self.hid_open(true)?;
let result = self.read_data_inner( max_requests);
self.hid_close()?;
result
}
}
#[cfg(test)]
mod tests{
use crate::*;
use serial_test::serial;
#[test]
fn compilation() {
assert_eq!(1+1,2);
}
#[test]
#[serial]
fn find_device() {
let co2 = CO2Monitor::default().unwrap();
assert_eq!(co2.device_info.vendor_id(), CO2MON_HID_VENDOR_ID);
assert_eq!(co2.device_info.product_id(), CO2MON_HID_PRODUCT_ID);
}
#[test]
#[serial]
fn read_message(){
let mut co2 = CO2Monitor::default().unwrap();
let result = co2.read_data( 50);
dbg!(result.unwrap());
}
#[test]
#[serial]
fn get_info_test(){
let co2 = CO2Monitor::default().unwrap();
let info = co2.info();
dbg!(info);
}
}