【コロナ対策】オフィス換気のため Co2 センサー作り ー NodeMCU, BME280, MH-Z19編

こんにちは、エンジニアです。

最近ニュースで中国製CO2センサーが測れない問題があり、CO2センサーのニーズが再び出てきました。

前回CO2センサーづくりArduino編の評判よかったので、情報を更新しようと思います。

実は前回の記事公開時の2018年から、弊社のCO2センサーはArduinoからNodeMCUに作り変えたのですが、なかなか記事にする時間がなくて…もう3年経ってしまいました。

このセンサーはこの3年間、24時間365日稼働していますが問題ないので紹介したいと思います!

NodeMCU

まずはNodeMCUはこちらです。

本来のCP2102と中国版のCH340 (ドライバーとピンが違います)

CH340もピンが少ないバージョン (私は別のプロジェクトで、プラレールに入れて遠隔操作をしています)

いろんな種類があって、大体パソコンとつなぐUSBドライバーが違うだけです。

自分のパソコンがUSBドライバー対応できる安いモノを買えばよいです。

つなぐ

BME280とMH-Z19つなぐ手順は以下になります

MH-Z19は5V必要なので中国バージョンCH340は「VU」(直接USBにつなぐ)のピンがあるので、とても便利です。

ファームウェア

NodeMCUは、CとLua両方対応ができ、今回はLuaを紹介しようと思います。

まずはメモリが少ないので、ファームウェアをカスタムしないと入らないです。

それが https://nodemcu-build.com/ で簡単にできるようです。

以下の「Select modules to include」を必要なモジュールをチェック入れます。

BME280とI2Cで繋ぎ、MH-Z19はUARTで繋ぎます。

Fileはソースコード読み込み用で、WIFIとNETはインターネット接続用です。

あとtimerはセンサー読み込み間隔用です。

上でメールアドレスを入力するところにアドレスを入力すると、ダウンロードURLがメールで来ます。

準備できたら一番下の「Start your build」を押します。

メールはこんな感じです

私はいつも float: の方を使ってます。

ドライバーをインストール

Googleですね.. 「CH340」

https://sparks.gogo.co.nz/ch340.html

Windows : https://sparks.gogo.co.nz/assets/site/downloads/CH34x_Install_Windows_v3_4.zip

Mac : https://github.com/adrianmihalko/ch340g-ch34g-ch34x-mac-os-x-driver/raw/master/CH34x_Install_V1.5.pkg

ファームウェアをインストール

NodeMCUにメールからダウンロードした.binファイル(ファームウェア)をインストール。

おすすめソフトは「nodemcu-pyflasher」です、ダウンロードURL:

https://github.com/marcelstoer/nodemcu-pyflasher/releases

Windows : https://github.com/marcelstoer/nodemcu-pyflasher/releases/download/v5.0.0/NodeMCU-PyFlasher.dmg

Mac : https://github.com/marcelstoer/nodemcu-pyflasher/releases/download/v5.0.0/NodeMCU-PyFlasher.exe

アプリを実行するとこうに見えます。

Serial Port : usb serial を選択します

NodeMCU firmware : 先ダウンロードしたfloat.binファイルを選択します

Rate : 115200をオススメします、安定しない場合もっと遅いrateでも大丈夫です

Flash mode : Quad I/O (4ビットデータバス)、少し遅いDual I/O (2ビットデータバス) でも大丈夫です

Flash : Yes

プログラムをアップロード

次はプログラムをアップロードするに使うのESPlorerをダウンロード。

https://github.com/4refr0nt/ESPlorer/releases

Java Runtimeが必要ですので、用意してね

*NodeMCUをUSBに繋いてままで起動してください*

アプリ開いだらこんな感じです

メニューからusb serial を選択します、rateを「115200」を選択し、「Open」のボタンをクッリクします

「Open」をクッリク後はこうに見えます

NodeMCUの「Rst」リセットボタンを押します、

そうしたら、出力が見えます。(firmwareのバージョンと選んだモジュールが確認できます)

今度は左側でソースコードをコピペします

-- node.setcpufreq(node.CPU80MHZ)
-- setup wifi
station_cfg={}
station_cfg.ssid="WIFI_SSID"
station_cfg.pwd="WIFI_PASSWORD"
station_cfg.save=false
wifi.setmode(wifi.STATION)
wifi.sta.config(station_cfg)  

sda, scl = 3, 4
i2c.setup(0, sda, scl, i2c.SLOW) -- call i2c.setup() only once
bme280.setup(nil, nil, nil, 0) -- sleep mode, use startreadout to force out

co2=0
airpressure=0
temperature=0
humidity=0

function co2loop()

    uart.alt(1)
    uart.setup(0,9600, 8, uart.PARITY_NONE, uart.STOPBITS_1,1)
    uart.on("data", 9, function(data)
        local a,b = string.byte(data,1,2)
        if (a==tonumber('FF',16)) and (b==tonumber('86',16)) then
            local high,low = string.byte(data,3,4)
            co2 = high * 256 + low
        end
        uart.on("data") -- unregister callback function
    end, 0)

    uart.write(0,0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79)

    bme280.startreadout(0, function ()
        -- temp
        local P, T = bme280.baro()
        airpressure = string.format("%d.%02d", P/1000, P%1000)

        local H, T = bme280.humi()
        local Tsgn = (T < 0 and -1 or 1); T = Tsgn*T
        temperature = string.format("%s%d.%02d", Tsgn<0 and "-" or "", T/100, T%100)
        humidity = string.format("%d.%02d", H/1000, H%1000)
    end)

--    print("airpressure: "..airpressure)
--    print("temperature: "..temperature)
--    print("humidity: "..humidity)
    
    if wifi.sta.getip()==nil then
        mytimer:interval(10000) -- 10s
        wifi.sta.connect()
    else
        mytimer:interval(60000) -- 1min
        -- http
        conn=net.createConnection(net.TCP, 0)
        conn:on("connection", function(conn)
        conn:send("GET /sensor?i=2&t="..temperature.."&h="..humidity.."&p="..airpressure.."&c="..co2.." HTTP/1.1\r\n"..
               "Host: localhost\r\n"..
               "User-Agent: sensor\r\n"..
               "Connection: close\r\n"..
               "\r\n")
        end)
        conn:connect(80,"sensor.mochi.space")
    end
end

print("you have 10sec to rewrite 'init.lu' to get back to normal")
mytimer = tmr.create()
mytimer:register(10000, tmr.ALARM_AUTO, co2loop)
mytimer:start()

こんな風に見えます

フロッピーディスクアイコン「Save」ボタンを押して、「init.lua」って言うファイル名を保存します。

*init.luaは最初に自動で実行するファイル名になります*

保存完了後は「Save to ESP」をクッリクします

右側に「Reload」ボタン押すと

init.luaファイルがNodeMCU内にアップロード成功したがわかります

もう一回「Rst」リセットボタン押すと

「you have 10sec to rewrite ‘init.lu’ to get back to…」が表示されれば、プログラムの動作が確認できます。

ソースコードの説明

まず「–」で始まる行はコメントです、実行しない行です。

-- node.setcpufreq(node.CPU80MHZ)
-- setup wifi
station_cfg={}
station_cfg.ssid="WIFI_SSID"
station_cfg.pwd="WIFI_PASSWORD"
station_cfg.save=false
wifi.setmode(wifi.STATION)
wifi.sta.config(station_cfg)  

最初にnode.setcpufreqはその通りcpuのスピード調整です。節電したくで遅くでも構わない人やオーバークロックしたい人、ご自由に「–」を外して試してください。

NodeMCUはwifiにつながるので、WIFI_SSIDとWIFI_PASSWORDを入力してください。

sda, scl = 3, 4
i2c.setup(0, sda, scl, i2c.SLOW) -- call i2c.setup() only once
bme280.setup(nil, nil, nil, 0) -- sleep mode, use startreadout to force out

単純なI/Oピンの設定です。

co2=0
airpressure=0
temperature=0
humidity=0

取りたい数字を最初0にする

    uart.alt(1)
    uart.setup(0,9600, 8, uart.PARITY_NONE, uart.STOPBITS_1,1)
    uart.on("data", 9, function(data)
        local a,b = string.byte(data,1,2)
        if (a==tonumber('FF',16)) and (b==tonumber('86',16)) then
            local high,low = string.byte(data,3,4)
            co2 = high * 256 + low
        end
        uart.on("data") -- unregister callback function
    end, 0)

    uart.write(0,0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79)

uart.alt(1):0デフォルトピン、1はGPIO13とGPIO15ピンを使います。

MH-Z19はUARTに9600bandで繋ぎます。

簡単に説明すると、9 byteコマンド送って、レスポンスが戻ってきます。
0x86はreadコマンドで、戻るときも確認用のため0x86も一緒に返します。


    bme280.startreadout(0, function ()
        -- temp
        local P, T = bme280.baro()
        airpressure = string.format("%d.%02d", P/1000, P%1000)

        local H, T = bme280.humi()
        local Tsgn = (T < 0 and -1 or 1); T = Tsgn*T
        temperature = string.format("%s%d.%02d", Tsgn<0 and "-" or "", T/100, T%100)
        humidity = string.format("%d.%02d", H/1000, H%1000)
    end)

BME280はモジュールからそのまま使います。

bme280.baro()はpressure P to Temperature T を返します、bme280.humi()はHumidity H とTemperature Tを返します。

    if wifi.sta.getip()==nil then
        mytimer:interval(10000) -- 10s
        wifi.sta.connect()
    else
        mytimer:interval(60000) -- 1min
        -- http
        conn=net.createConnection(net.TCP, 0)
        conn:on("connection", function(conn)
        conn:send("GET /sensor?i=2&t="..temperature.."&h="..humidity.."&p="..airpressure.."&c="..co2.." HTTP/1.1\r\n"..
               "Host: localhost\r\n"..
               "User-Agent: sensor\r\n"..
               "Connection: close\r\n"..
               "\r\n")
        end)
        conn:connect(80,"localhost")
    end

ここはWiFiがつながらない時に、タイマーで10秒間隔でもう一回試して接続する、それ以外は毎60秒間隔でHTTP GETで情報を「Host: localhost」に送ります。(ここはlocalhostを好きなサーバーに設定してください)

最後にconnect(80,”localhost”)にもlocalhostを別のサーバーに設定してください。

print("you have 10sec to rewrite 'init.lu' to get back to normal")
mytimer = tmr.create()
mytimer:register(10000, tmr.ALARM_AUTO, co2loop)
mytimer:start()

先ほどの10秒間隔が60秒間隔の設定をタイマーにco2loopを実行する設定です。

*「mytimer:register(10000,..」は10秒実行延期です! つけた理由は3つがあります。(私の勝手なトリックです)

  1. センサー立ち上がるには2-3秒掛かります
  2. コンソールに出力しているピンがi2cに使われています(出力が確認はできません)
  3. 2番のコンソールに出力がないと、ファームウェアも再アップロードができない

解決方法は10秒実行延期することで、新しいファームウェアの書き換え時間ができました。

ネットで調べたんですが、この問題で解決策を見つけられずに断念した人が結構多いようです。。。

動作の確認

先ほどの2番の問題で出力が見えないので、一つ動作確認方法はbmeとuart部分のソースを別々の.luaファイルに保存して動作テストすること、もう一つはサーバーをつながって確認する方法です。

ここまで来たらもう長すぎますので、別の記事にしましょう。

次回はNodeMCUからサーバー上に以下のグラフを確認する方法を紹介します。

最後に実物はこちらです、現在2台を稼働しています。