新型コロナウイルス感染拡大防止の観点と結露防止の観点から研究室でCO2と湿度・温度を計測するエッジデバイスを作ってみました。
もともと研究室は裏山の影響で夏期に湿度が高くなってしまうため、除湿機の運用が不可欠です。これを入れ忘れてしまうと最悪、結露による機器の破損が危惧されます。このため、以前より温度・湿度を計測するエッジデバイスを作って設置していました。
今回、更にCO2センサーを追加し、換気状況も確認できるようにしました。
機器構成
使用した機器は以下の通りです。
- マイコン:M5StickC
- 湿度・温度センサー:BME280 (GROVE接続)
- CO2センサー:MH-Z19C(シリアル接続)
CO2センサーはシリアル接続なので、M5StickCのG0/G26をTX/RXとして接続しています。電源は5VなのでこれもM5StickCから供給しています。
温湿度センサーはGROVE接続なので、そのままM5StickCのGROVE端子に接続しています。
赤外線カメラで確認したところ、(確認のため1分ごとに計測・送信)連続動作した状態で最も高温になるのはM5StickCでした。それでも室温24程度の時でM5StickCの表面温度は30度程度なので、特に支障はないと考えました。
ソフトウェア
M5StickC用のソフトウェア例は以下の通りです。このサンプルコードではCO2、湿度・温度を計測し後述するGASへJSON形式データをPOSTしています。おおよそ5分ごとに計測・送信します。このままだとWiFiが一度切断されると復旧しないので、何らかの対処が必要です。
画面表示を止めたり、Deep Sleepさせるなどすれば、もっと消費電力を下げることができるかもしれません。
2021/05/29 ソースコードを一部更新。送信ミスが続いたらRESETするようにしました。
#include <M5StickC.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include "Seeed_BME280.h"
const char* ssid = "!!!SSID!!!";
const char* password = "!!!WiFi PASSWORD!!!";
const char* published_url = "!!!GAS URL!!!";
BME280 bme280;
uint8_t co2Command[9];
uint8_t co2data[9];
int count = 0;
int width, height;
int getCO2() {
int value = -1;
Serial2.readBytes(co2data, 9);
if ((co2data[0] == 0xff) && (co2data[1] == 0x86)) {
value = (int)co2data[2] * 256 + (int)co2data[3];
} else {
;
}
return value;
}
void co2Init() {
co2Command[0] = 0xff;
co2Command[1] = 0x01;
co2Command[2] = 0x86;
co2Command[3] = 0x00;
co2Command[4] = 0x00;
co2Command[5] = 0x00;
co2Command[6] = 0x00;
co2Command[7] = 0x00;
co2Command[8] = 0x79;
// Serial2.begin(9600, SERIAL_8N1, 32, 33);
Serial2.begin(9600, SERIAL_8N1, 0, 26);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.print("CO2 sensor initialize ");
Serial.print("CO2 sensor initialize");
delay(100);
int value;
while ((value = getCO2()) == 500) {
Serial.print(".");
M5.Lcd.print(".");
delay(1000);
}
Serial.println("CO2 sensor start");
}
void wifiInit() {
WiFi.mode(WIFI_STA);
WiFi.disconnect(true);
delay(1000);
Serial.println(ssid);
// esp_wifi_restore(); // どうやってもWiFiが動かなくなったら実行すると良い?
WiFi.begin(ssid, password);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.print("WiFi initialize ");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
M5.Lcd.print(".");
}
}
int sendFailCount = 0;
void sendJson(char *json) {
HTTPClient http;
http.begin(published_url);
int httpCode = http.POST(json);
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK) {
Serial.println("http/post success");
String payload = http.getString();
}
} else {
Serial.printf("http/post error: %s\n", http.errorToString(httpCode).c_str());
M5.Lcd.print("wifi/post fail ");
M5.Lcd.print(sendFailCount);
sendFailCount++;
if (sendFailCount > 2) {
delay(1000);
esp_restart();
}
}
http.end();
}
void setup() {
M5.begin();
M5.Axp.ScreenBreath(8);
M5.Lcd.setRotation(3);
M5.Lcd.setTextSize(1);
M5.Lcd.fillScreen(BLACK);
width = M5.Lcd.width();
height = M5.Lcd.height();
wifiInit();
co2Init();
if(!bme280.init()){
Serial.println("bme280 error");
}
Serial.println("BME280 init finish");
M5.Lcd.fillScreen(BLACK);
}
void loop() {
Serial2.write(co2Command, 9);
delay(100);
float h = bme280.getHumidity();
float t = bme280.getTemperature();
float v = (float)analogRead(33) * 3.3f / 4095.0f;
int co2Value = getCO2();
if (co2Value != -1) {
char json[100];
sprintf(json, "{\"co2\": \"%d\" , \"humi\": \"%f\" , \"temp\": \"%f\" , \"voltage\": \"%f\" }", co2Value, h, t, v);
Serial.println(json);
sendJson(json);
M5.Lcd.setCursor(0, 0);
M5.Lcd.print("CO2 ");
M5.Lcd.print(co2Value);
M5.Lcd.println(" ppm");
// M5.Lcd.setCursor(5, 15);
M5.Lcd.print("H/T : ");
M5.Lcd.print(h);
M5.Lcd.print(" / ");
M5.Lcd.print(t);
int bH = height/2 / (log(50000) - log(400)) * (log(co2Value)-log(400));
M5.Lcd.drawLine(count, height, count, height-2-bH, WHITE);
M5.Lcd.drawLine(count+1, height, count+1, height-2-height/2, BLACK);
} else {
M5.Lcd.setCursor(0, 0);
M5.Lcd.println("CO2 error");
Serial.print("illegal ");
Serial.print(co2data[0]);
Serial.print(",");
Serial.println(co2data[1]);
}
count++;
if (count > width) {
count = 0;
}
// delay(60000LL); // 1分ごと
delay(300000LL); // 5分ごと
// delay(600000LL); // 10分ごと
}
受信ソフトウェア(Gdrive / Google Apps Script)
Gdrive上にSpreadsheetを作成し、ツール→スクリプトエディタから以下のようなコードを作成、Webアプリとしてデプロイします。ここでデプロイしたときに生成されるURLを上のソースコード(published_url)で使用します。
このサンプルコードは受信データをSpreadsheetに追記していくだけです。実際にはタイマー駆動型等の処理で定期的な受信チェック、異常値チェックをするべきです。私は受信チェック(一定期間以上POSTがない)と異常値チェックをしてメール通知するようにしています。
function doPost(ee) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var values = JSON.parse(ee.postData.getDataAsString());
sheet.appendRow([new Date(), values.humi, values.temp, values.co2]);
}