S0-ma's Blog

s0-maのブログです。

CO2-miniのデータをPython (Mac OSX Mojave) で読み出してみた

Abstract

買ったCO2濃度モニターをMac OSX (Mojave) にUSB接続して、データ取得してみました。

Introduction

CO2-miniという、手頃な値段のCO2センサーを購入した。 s0-ma.hatenablog.com

決め手となったのは、非公式ながらもUSB接続で値が読み取れるという点。 以下が、CO2-miniのリバースエンジニアリングをした人のブログ。なかなか面白い。Pythonを使って、最終的にプロトコルの解析に成功している。

hackaday.io

ただ上記記事の内容はLinuxを想定したコードであるため、そのままだと手元のMac OSX (Mojave)では動作しなかった。Mac OSX上のPythonから値を読み取れるか試してみた。

読み出し

このCO2センサの場合、USBの中でもHID(Human Interface Device)クラスというプロトコルで読み出しができる。Linuxだとデバイスファイル経由で読み書きするのが簡単らしく、CO2-mini用の読み出しコードもそういった実装のものが多く見つかった。ただ、Mac OSXだとデバイスファイルが存在しないので、少しやり方を変える必要がある。

まずは、PythonからHIDデバイスを使うためのライブラリをインストール。

pip install hidapi

実行コードは最終的に以下のようになりました。 ほとんどの部分がオリジナルのブログからの流用です。

#!/usr/bin/python
#-*- coding: utf-8 -*-

import hid
import time
import datetime

key = [0x86, 0x41, 0xc9, 0xa8, 0x7f, 0x41, 0x3c, 0xac]

def decrypt(data):
    cstate = [0x48, 0x74, 0x65, 0x6D, 0x70, 0x39, 0x39, 0x65]
    shuffle = [2, 4, 0, 7, 1, 6, 5, 3]

    phase1 = [0] * 8
    for i, j in enumerate(shuffle):
        phase1[j] = 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

if __name__=="__main__":
    h = hid.device()
    h.open(0x4d9, 0xa052)
    h.send_feature_report([0x00]+key)

    flg1 = False
    flg2 = False

    while(True):
        buf = h.read(8)
        decrypted = decrypt(buf)
        if decrypted[4] != 0x0d or (sum(decrypted[:3]) & 0xff) != decrypted[3]:
            print("Checksum error")
            break
        else:
            val = decrypted[1] << 8 | decrypted[2]

            if(decrypted[0]==5*16):
                co2 = val
                #print("co2:", co2)
                flg1=True
            elif(decrypted[0]==4*16+2):
                temp = val/16-273.15
                #print("tmp:%.1f" % temp)
                flg2=True
        if(flg1==True and flg2==True):
            print(datetime.datetime.now(), co2, "%.1f"%temp)
            flg1 = False
            flg2 = False

Concolusion

実行結果はこんな感じ。

2019-06-26 22:25:09.452618 478 27.2
2019-06-26 22:25:14.468803 480 27.2

CO2-miniのデータ更新が秒に一回程度らしく、それに合わせたタイミングで取得できる。 CO2濃度の更新は5秒間隔、温度の更新は30秒間隔みたい。