安価なCO2センサの比較(1)

Sensirion製センサも動くようにしたので、それぞれどんな関係にあるか、見たくなってきました。
ですので、以前作った電源周波数観測用のサーバプログラムを改造して、chart.jsでグラフを描かせるようにしてみました。

本当はリアルタイム更新のほうが見た目には面白いのですが、 asyncio を使いこなせていないせいか、うまく行かず、Javascriptで強制リロードする方式にしました。

各センサからのデータ収集はUSBシリアル経由で行っていますが、今までは数値だけだったり、デバッグ情報が混ざっていたりとバラバラだったので、フォーマットを以下のように規定しました。

$(センサ名) Co2:(CO2濃度値 ppm) ・・・(その他の情報)

各USBシリアルから入ってくるデータを読んで、蓄積、定期的にHTML出力するコードとしました。(とにかく動けばいいや、なのでテキトーです)

# -*- coding:utf-8 -*-
#
import os.path,sys
import serial,time,re

# webserverは
#   python -m http.server 8080
# で別途起動

hist = {}        # データを保持する変数
hlen = 60000       # データを保持する数

devs = (
    '/dev/ttyUSB0',
    '/dev/ttyUSB1',
    '/dev/ttyUSB2',
    '/dev/ttyUSB3',
    '/dev/ttyUSB4',
    '/dev/ttyACM0',
    '/dev/ttyACM1',
    '/dev/ttyACM2',
    '/dev/ttyACM3',
    '/dev/ttyACM4' 
  )

ports = []

header = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>CO2センサ比較</h1>
  時刻はサーバプログラムがデータを受信した時刻です。データは約1分ごとに自動更新しています。30秒で自動リロードしますが、リロードのタイミングとデータの自動更新のタイミングがぶつかると表示の更新が停止する場合があります。<BR>
  センサーのサンプル数は各1台です。入手経路も入手時期も取り付けの構造もまちまちです。また、気分で改造したりします。
  <HR>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"></script>
    <canvas id="Chart1" width="auto" height="auto"></canvas>
    <script>
      var context = document.getElementById('Chart1');
      var Chart1 = new Chart(context, {
        type: 'scatter',
          data: {
            datasets: [
"""
footer = """
              ]},
          options:{
            scales:{
              xAxes: [{
                gridLines: {
                  color: "rgba(255, 0, 0, 0.2)", 
                  zeroLineColor: "black"    },
                type: 'time',
                time: {
                  unit: 'minute',
                  displayFormats: {
                    minute: 'H:mm'
                  },
                },
              }],
              yAxes: [{
                ticks :{
                  userCallback: function(tick) {
                    return tick.toString() + 'ppm'
                  }
                },
                gridLines: {
                  color: "rgba(0, 0, 255, 0.2)",
                  zeroLineColor: "black"
                },
              }]
            },
            animation: false
          }
      });
    </script>
    <script>
    // 30秒に一回リロード
      setTimeout("location.reload(true)",30000);
    </script>
</body>
</html>
"""
prepare = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>準備中です。1分程度で開始します</h1>
  <script>
  // 1分=60秒に一回リロード
    setTimeout("location.reload(true)",60000);
  </script>
</body>
</html>
"""

m1 = re.compile('^\$([A-Z0-9]+)\sCo2:([0-9]+)')

ctbl = (
  'rgba(  0,  0,255,0.8)',
  'rgba(255,  0,  0,0.8)',
  'rgba(  0,128,  0,0.8)',
  'rgba(255,  0,255,0.8)',
  'rgba(  0,128,128,0.8)',
  'rgba(128,128,  0,0.8)',
  'rgba( 64, 64, 64,0.8)'
)

def recieve():
  mkht = time.time()

  for p in ports :
    p.rts = False
    p.reset_input_buffer()
    p.rts = True

  while True:
    for p in ports :
      data = None
      if p.inWaiting() > 0 :
        try :
          data = p.readline().decode().rstrip('\r\n')
        except :
          data = None
          pass
        finally :
          pass
        #print(time.time(),data)
        r1 = m1.match(data)
        if r1 :
          #print(r1.group(1),r1.group(2),time.time())
          sensor = r1.group(1)
          data = float(r1.group(2))
          tstamp = time.time()
        else :
          data = None

      # 配列にデータを追加
      if data is not None :
        if not sensor in hist  :
          hist[sensor] = []
        hist[sensor].append((tstamp,data))
        while len(hist[sensor]) > hlen :
          del(hist[sensor][0])

    # 定期的にHTMLファイルを生成する。
    if time.time() - mkht > 10 :  # 前回ファイル出力から10秒以上経過している場合
      mkht = time.time()
      s = ""
      cidx = 0
      for k in hist.keys():
        s += """
              {
                label: \"""" + k + """\",
                showLine: true ,
                lineTension: 0 ,
                fill: false ,
                borderColor: \"""" + ctbl[cidx] + """\",
                borderWidth: 1,
                pointRadius: 1,
                data :[
"""
        cidx += 1
        for i in hist[k]:
          if mkht - i[0] < 3600*6 :    # 過去20分以内のデータに限り出力する
            s += "{{ x: {:.0f} , y: {} }},".format(i[0]*1000,i[1])
        s = s.rstrip(',')
        s += "]},"
      s = s.rstrip(',')
      with open('index.html',mode='w') as f:
        f.write(header + s + footer)

  for p in ports :
    p.close()

#
if __name__ == "__main__":
  for p in devs :
    if os.path.exists(p) :
      ports.append(serial.Serial(p , baudrate=115200, parity=serial.PARITY_NONE, rtscts=False))
  with open('index.html',mode='w') as f:
    f.write(prepare)
  time.sleep(5)
  recieve()

これをRaspberry Pi 3上で動かして、データ収集しながらHTML生成をさせました。

すべてのセンサを自室内の10cmほど開けてある窓に近いところに置いて比較してみました。

SCD30は変動が大きく、自分がそちらを向くと1mくらいあっても呼気に反応するのか、あるいはちょっとした風向きの変化でCO2濃度が変化する(外気があたる or 室内の空気があたるかが変わる)のか、すぐに数値が上昇します。その他のものは概ね似たような傾向を示しています。

このあと、窓を開けたまま風呂に入って無人の時間がしばらく経過して戻ってきたところです。

やはり、SCD30が極めて敏感に反応しているのですが、400ppmを切ってしまっているので、まだオートキャリブレーションが正常に機能していないのだと思います。(SCD41もまだ稼働時間は24時間以下です)

しばらく様子見するしかなさそうです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)