So now that we have figured out all the puzzle pieces (do we?) we can go over to the last project phase and actually build something that extracts data from the sensor on its own initiative and feeds it into something with a purpose.
Setting: Ubuntu 14.04 LTS, Linux 3.13.0-52-generic, Python 2.7.6. As we recall from one of the earliest logs we can initiate data transmission by issuing a HID SET_REPORT on the USB device. Luckily, Linux offers a very simple API to do just that by way of the hidraw device node we were using at the very beginning. Since we're not interested in any security (which the protocol doesn't offer anyway) we'll just use a static key.
with file("/dev/hidraw0","a+b") as co2:
# Key retrieved from /dev/random, guaranteed to be random ;)
key = [0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96]
co2.write( "\x00" + "".join(chr(e) for e in key) )
while True:
data = co2.read(8)
print " ".join("%02X" % e for e in data)
If we run that code (after fixing permissions, we'll get to the part later) we get … nothing. Bummer.This means it's time to compare exactly what we're doing on the USB with what the Windows software is doing, breaking out the wireshark again:It certainly does look right: there's 8 bytes of payload (note that we wrote 9 bytes into the device file, but the first byte only specifies the report number, 0 in this case; also note that we have seen the same convention earlier in the call to HidD_SetFeature in the Windows code). By carefully comparing this USB packet to the one observed earlier we can figure out what's wrong: Our wValue is 0x0200, theirs was 0x0300. When looking into the USB HID Device Class specification (section 7.2.1 on page 51), or alternatively just looking at the wireshark parse, we see: We sent an "Output" SET_REPORT, they sent a "Feature" SET_REPORT. Returning to the Linux HIDRAW API documentation tells us: In order to send a feature report we can't use a simple write but need to use the HIDIOCSFEATURE ioctl.In Python, ioctls can be sent using the fcntl.ioctl() method, but we need the ioctl number for that. We find HIDIOCSFEATURE defined in hidraw.h as:
#define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len)
which isn't terribly helpful. The pragmatic solution is to compile a short C program and let it print the actual value (note: we're already hard coding the len parameter as 9, which is the only value we will need):#include <linux/hidraw.h>
#include <sys/ioctl.h>
#include <stdio.h>
int main(void)
{
printf("0x%08X\n", HIDIOCSFEATURE(9));
return 0;
}
This tells us that the value of HIDIOCSFEATURE(9) actually is 0xC0094806. So, now for the real, actually working Python code:#!/usr/bin/env python
import sys, fcntl, time
def decrypt(key, data):
cstate = [0x48, 0x74, 0x65, 0x6D, 0x70, 0x39, 0x39, 0x65]
shuffle = [2, 4, 0, 7, 1, 6, 5, 3]
phase1 = [0] * 8
for i, o in enumerate(shuffle):
phase1[o] = data[i]
phase2 = [0] * 8
for i in range(8):
phase2[i] = phase1[i] ^ key[i]
phase3 = [0] * 8
for i in range(8):
phase3[i] = ( (phase2[i] >> 3) | (phase2[ (i-1+8)%8 ] << 5) ) & 0xff
ctmp = [0] * 8
for i in range(8):
ctmp[i] = ( (cstate[i] >> 4) | (cstate[i]<<4) ) & 0xff
out = [0] * 8
for i in range(8):
out[i] = (0x100 + phase3[i] - ctmp[i]) & 0xff
return out
def hd(d):
return " ".join("%02X" % e for e in d)
if __name__ == "__main__":
# Key retrieved from /dev/random, guaranteed to be random ;)
key = [0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96]
fp = open(sys.argv[1], "a+b", 0)
HIDIOCSFEATURE_9 = 0xC0094806
set_report = "\x00" + "".join(chr(e) for e in key)
fcntl.ioctl(fp, HIDIOCSFEATURE_9, set_report)
values = {}
while True:
data = list(ord(e) for e in fp.read(8))
decrypted = decrypt(key, data)
if decrypted[4] != 0x0d or (sum(decrypted[:3]) & 0xff) != decrypted[3]:
print hd(data), " => ", hd(decrypted), "Checksum error"
else:
op = decrypted[0]
val = decrypted[1] << 8 | decrypted[2]
values[op] = val
# Output all data, mark just received value with asterisk
print ", ".join( "%s%02X: %04X %5i" % ([" ", "*"][op==k], k, v, v) for (k, v) in sorted(values.items())), " ",
## From http://co2meters.com/Documentation/AppNotes/AN146-RAD-0401-serial-communication.pdf
if 0x50 in values:
print "CO2: %4i" % values[0x50],
if 0x42 in values:
print "T: %2.2f" % (values[0x42]/16.0-273.15),
if 0x44 in values:
print "RH: %2.2f" % (values[0x44]/100.0),
print
Et voilà! I've skipped one intermediary step where the temperature readings didn't make any sense, since they don't follow the protocol documentation we discovered at first. Thankfully, the American vendor has directory listings active on its AppNotes directory, allowing us to discover another protocol spec that documents the correct formula for the temperature reading. It also describes the relative humidity data point (for which my device has no sensor), so I've added decoding support for all three items: CO₂ (in ppm), temperature (in °C), and relative humidity (in %).
This handy tool also prints all raw data items received, so that we may be able to make some sense of the additional data in the future. For easier reading each line prints the last value received for each item, the item received most recently is marked with an asterisk.
*42: 128F 4751 T: 23.79 42: 128F 4751, *6D: 03E4 996 T: 23.79 42: 128F 4751, 6D: 03E4 996, *6E: 514A 20810 T: 23.79 42: 128F 4751, 6D: 03E4 996, 6E: 514A 20810, *71: 01C9 457 T: 23.79 42: 128F 4751, *50: 01C8 456, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 42: 128F 4751, *4F: 21A3 8611, 50: 01C8 456, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 42: 128F 4751, 4F: 21A3 8611, 50: 01C8 456, *52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 *41: 0000 0, 42: 128F 4751, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, *43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, *42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, *6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, *71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, *50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 *41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, *43: 0CE9 3305, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, *42: 128F 4751, 43: 0CE9 3305, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79
Next thing up: This Python script needs to be called with the hidraw device node as its first argument. In previous snippets I've always assumed /dev/hidraw0, but this may be different between different computers and maybe on each boot (if you have USB HID devices). Also we needed to manually give our user permissions on the device node. Let's fix it with an udev rule (this goes as 90-co2mini.rules into /etc/udev/rules.d):
ACTION=="remove", GOTO="co2mini_end" SUBSYSTEMS=="usb", KERNEL=="hidraw*", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", GROUP="plugdev", MODE="0660", SYMLINK+="co2mini%n", GOTO="co2mini_end" LABEL="co2mini_end"
This will a) allow the group plugdev (change as appropriate, or put your user into that group) access to the device node and b) symlink the hidraw devices of all connected CO₂ monitors (if there's more than one :) in nice numbered files starting with /dev/co2mini0.
Perl code to receive data and FHEM integration are up next.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.
All other co2 monitors in python didn't work for me, so I more or less copies basic algorithm from https://github.com/dmage/co2mon because it uses simpler hid API and it actually works
https://gist.github.com/librarian/306e06c51fe5f53ded6ebc761580b62b
Are you sure? yes | no
Just tiny changes to up-to-date python library `hidapi`:
https://gist.github.com/GennadySpb/5adddfcf5a45a96956ba73854b419b07
Are you sure? yes | no
I run a variant of this code just fine on Ubuntu 16.04. I would like to run it also on Windows and Apple, but I guess this code won't work there, or does it?
Do you know of any Open Source Windows code, preferably in Python?
Are you sure? yes | no
I recently got my hands on a AIRCO2NTROL MINI from TFA that looks quite similar to your device and is also mentioned in some of the projects linked here. However all of them only produced garbage data. After some tweaking I found out that they dropped the "encryption" on my device and send the data in plaintext. Vid and Pid are the same as described here, but "bcdDevice" has gone up to 2.0 from 1.0. Maybe this help someone :)
Are you sure? yes | no
It is now about 5 years later and still very useful. Just acquired the device, made a Python 3 version with simple logging, and made it available here: https://sourceforge.net/projects/minimon/
Are you sure? yes | no
Did anyone succeed in putting this to a fhem module?
Are you sure? yes | no
I wrapped this code into a simple Python class running with 2 and 3.
You can find it at https://github.com/heinemml/CO2Meter
Are you sure? yes | no
Well done! Where can I wire you those two weekends you've just saved me?
Are you sure? yes | no
Very useful write up! I have a CO2mini connected to a Pi and I don't know if anyone noticed but it looks like the CO2 value seems to only update about every four minutes. I was polling the mini every 20 seconds, and that may be kind of pointless. Odd since the display seems to update every time the display cycles between temperature and CO2.
Are you sure? yes | no
C language library to work with device: https://github.com/vshmoylov/libholtekco2
Are you sure? yes | no
Excellent work! Based on your code, I integrated the CO2 monitor in OpenHAB using REST - see
https://github.com/KristofRobot/openhab-config/tree/master/co2mon
Are you sure? yes | no
Great Project! I created a Nodejs Plugin for the CO2 Monitor based on your great work. GitHub: https://github.com/maddindeiss/co2monitor
Are you sure? yes | no
FHEM WOULD BE GREAT. Are you still working on it? Please please please ;)
Are you sure? yes | no