Reverse-Engineering a low-cost USB CO₂ monitor

I'm trying to get data out of a relatively low-cost (80€) CO₂ monitor that appears to have a USB connection for data as well as for power

Similar projects worth following

Contrasted with, f.e. CO, monitoring CO₂ seems to be tricky. Most devices in a reasonable price range will only give you a qualitative output: "good"-"bad"-"worse". And even getting this rather restricted kind of data into a computer for logging (and eventually: actions of the home automation system) leads to device prices at ~120€ and well above.

Recently a friend tipped me off to a device (also on that is almost cheap (80€) and actually gives you a CO₂ readout in ppm! Now all I need is to whack at it to get the data into a computer.

  • All your base are belong to us

    Henryk Plötz05/14/2015 at 15:27 13 comments

    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 =
    		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.

    Read more »

  • How are you, gentlemen

    Henryk Plötz04/26/2015 at 18:40 0 comments

    Now that I've had my breakthrough I can leisurely stroll around the rest of the code for completeness sake. This should show me how and what data is being sent to the device to initiate data reporting.

    Read more »

  • It's you

    Henryk Plötz04/25/2015 at 01:01 0 comments

    Loading a new file into IDA for the second time was easier. Again, I had to prepare it with

    upx -d HIDApi.dll

    and this time all functions were already nicely labeled for me. And again I spent a lot of time being stupid, starting here:

    Read more »

  • Main screen turn on

    Henryk Plötz04/25/2015 at 00:00 0 comments

    So, looking at the transmitted data wasn't that successful in figuring out the protocol. My normal approach now would be to 'spoof' the other side of the communication channel and see how the device under test changes behaviour. In this case I would've written a short piece of Python to send out this initial SET_REPORT packet and then vary contents and look at how the device responds differently. But that's some boring work I've done several times before with other devices, so I'd wanted to try something new and this would be the perfect chance. I've never reverse-engineered anything by looking at a disassembly, so let's dive in!

    Read more »

  • What

    Henryk Plötz04/24/2015 at 22:13 0 comments

    In past projects I've had raging successes by just looking at the raw data, XORing stuff together, and maybe squinting my eyes a little. Spoiler: Not so this time. I'll walk you through the steps anyway.

    Read more »

  • Somebody set up us the bomb

    Henryk Plötz04/17/2015 at 02:05 1 comment

    Luckily reverse engineering funky binary protocols is one of my specialities, so, let's see …

    Data seems to arrive in 8-byte packets always. Indeed, this appears to be a property of what they did with the HID spec (more on that in a moment). The timing between 8-byte packets is irregular. In each packet there seem to be two groups of 4 byte each. The second byte of each group is always the same.

    Getting more data by re-starting the Windows software makes things more confusing: The second byte of each group is still constant, but different now. It's numerically close though, so, time-based? This sucks.

    Read more »

  • We get signal

    Henryk Plötz04/17/2015 at 01:27 0 comments

    Indeed, immediately after running the Windows software in a VM with the device connected I was getting a live data log of the CO₂ concentration! This screenshot shows me breathing into the vicinity of the sensor:

    Ok, time to disconnect the device from the virtual machine and look at the data on my Linux box. Since it's enumerated as a HID, there's /dev/hidrawX device node that would receive the raw data sent by the device.
    # hd < /dev/hidraw0
    00000000  be 9b 28 69 d0 a5 34 ba  cb 9b 90 68 4c a5 34 aa  |..(i..4....hL.4.|
    00000010  5f 9b 8e 68 de a5 34 c2  f3 9b b0 68 5c a5 34 82  |_..h..4....h\.4.|
    00000020  f2 9b b8 69 5c a5 34 8a  e2 9b c1 69 55 a5 34 3a  |...i\.4....iU.4:|
    00000030  52 9b c9 69 75 a5 34 d2  aa 9b 30 69 27 a5 34 42  |R..iu.4...0i'.4B|
    00000040  38 9b 20 69 89 a5 34 6a  8e 9b 28 69 d0 a5 34 ca  |8. i..4j..(i..4.|
    00000050  cb 9b 90 68 4c a5 34 aa  57 9b 8e 68 de a5 34 ba  |...hL.4.W..h..4.|
    00000060  fb 9b b0 68 5c a5 34 8a  fa 9b b8 69 5c a5 34 92  |...h\.4....i\.4.|
    00000070  35 9b 81 69 5d a5 34 52  d5 9b a9 69 72 a5 34 72  |5..i]|
    00000080  aa 9b 30 69 27 a5 34 42  00 9b 20 69 89 a5 34 72  |..0i'.4B.. i..4r|
    00000090  8e 9b 28 69 d0 a5 34 ca  c3 9b 90 68 4c a5 34 a2  |..(i..4....hL.4.|
    000000a0  57 9b 8e 68 de a5 34 ba  fb 9b b0 68 5c a5 34 8a  |W..h..4....h\.4.|
    000000b0  fa 9b b8 69 5c a5 34 92  12 9b c1 69 54 a5 34 2a  |...i\.4....iT.4*|

    Yeah \o/, there's data! Bummer, it's doesn't look at all like the protocol I was promised. But there does seem to be a structure to it, so there's hope, I guess.

  • War was beginning

    Henryk Plötz04/17/2015 at 01:11 0 comments

    First thing I noticed after unpacking: While the included manual only talks about the USB connection in terms of providing power, the thing actually enumerates on the bus as a HID with vendor/product ID 04d9:a052.

    When researching this VID/PID combination, I found that the device appears to be sold in other markets, with different brand markings: I don't speak any Russian, and Google Translate is rather weak here, but from the pictures it seems that someone was using an identically looking device to monitor some brewing process. And that there's software that reads the data from USB.

    Doing a Google image search for co2 meters and selecting a product photo that matches the physical appearance (with yet another different branding) lead me to this American company which had not only the Windows software to connect to the meter for download, but also a PDF documenting the protocol. Well, this should be a breeze …

View all 8 project logs

Enjoy this project?



Gennady wrote 02/17/2023 at 22:29 point

Tiny python app that allow get metrics in console:

  Are you sure? yes | no

hberg539 wrote 10/08/2021 at 20:59 point

Thanks Henryk for the project, it was one of the reason i bought this device. Sadly, they have changed the firmware on the last version, as it doesn't work with the ZG app you linked in the post (v1.0.0). When using the latest version (v2.4.4.7), it does work:

On my device, it says it was manufactured 03/21.

When doing "hd < /dev/hidraw0", i get no output.

This is the lsusb output:

Bus 001 Device 003: ID 04d9:a052 Holtek Semiconductor, Inc. USB-zyTemp
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0x04d9 Holtek Semiconductor, Inc.
  idProduct          0xa052 USB-zyTemp
  bcdDevice            2.00
  iManufacturer           1 Holtek
  iProduct                2 USB-zyTemp
  iSerial                 3 2.00
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x0022
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              100mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0
      bInterfaceProtocol      0
      iInterface              0
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.10
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength      53
         Report Descriptors:
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0008  1x 8 bytes
        bInterval              10
can't get debug descriptor: Resource temporarily unavailable
Device Status:     0x0000
  (Bus Powered)

I noticed the bcdDevice changed from 1.0 to 2.0.

Maybe they changed it because of your findings :-)

  Are you sure? yes | no

Vitold S. wrote 06/08/2021 at 11:15 point

I rewrite a application to use this sensor on Rust. The idea is to keep it simple and use it together with other sensors.

You can find it at

Also create project here

  Are you sure? yes | no

ian.kirker wrote 03/02/2020 at 16:03 point

Thank you for putting this together!

I've just been porting the code to Golang, and I was wondering: did you ever figure out what any of the other data types might be? Or whether they're anything relevant at all? I figured they might be either voltage/electrical data or air pressure, but if so, none of them seem to correspond directly to any obvious unit and I don't have enough data to trendline with other sources yet.

  Are you sure? yes | no

heine wrote 01/08/2017 at 20:21 point

I created a small python class to use this sensor. It's Python2 and 3 compatible. The idea is to keep it simple and use it together with other sensors.

You can find it at

  Are you sure? yes | no

Yann Büchau wrote 08/04/2016 at 15:29 point

I am working on a project for convenient plug'n'play co2 data logging and visualization:

It includes a python module to communicate with the device based on your amazing code.

The device is detected automatically by udev and a logging service is then started (systemd integration present but not mandatory).

There are easy-to-install debian packages available.

Thank you very much for your work on reverse-engineering the protocol!!

  Are you sure? yes | no

Victor Shmoylov wrote 04/20/2016 at 18:33 point

Thanks for your work and your project!
It helped me not to give up when I was trying to figure out how this device works.
As a result, I've developed a C language library (also reverse-engineered how they generate encoding key):

  Are you sure? yes | no

martin wrote 11/06/2015 at 09:36 point

Impressive Work! I created a Nodejs Plugin for the CO2 Monitor based on your project.

  Are you sure? yes | no wrote 05/03/2015 at 12:15 point

Impressive work done!
I have the same sensor and I am using this software to log it under linux:

  Are you sure? yes | no

zakqwy wrote 04/19/2015 at 03:52 point

Cool project! How does the sensor work? What sort of CO2 detection tech can they cram into an 80€ product?

  Are you sure? yes | no

Henryk Plötz wrote 04/25/2015 at 01:06 point

According to the data sheet it's NDIR: Non-dispersive infrared:

The Russian post I linked to in my first project log seems to discuss this further and has close-up photos of the sensor.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates