M5StackでCO2濃度の監視モニタを作ってみた

緊急事態宣言がいよいよ解除になりました。 第二波も怖いですし、引き続き新しい生活様式での生活が求められる昨今であります。

自分は全面的にリモートワークになっているのですが、現場でしかできない作業もあるので ソーシャルディスタンスを厳守した上で、定期的に研究所へ出勤することになりそう。


リモートワークでの仕事は、自宅にもデュアルディスプレイやら
ゲーミングチェア、HHKBといった作業環境は整えてあるので
デスクワークの作業効率自体はそこまで落ちていないのですが
時折集中力が続かなくなります。

原因は気圧のせいだったり色々あるのですが、部屋のCO2濃度も集中力を乱す1つの原因となります。

てことで、昔買って積んであったCO2センサーを使って、CO2濃度の監視モニターを作ってみました。


欲しい機能

・測定したCO2濃度を画面に表示したい
・できたらリアルタイムでグラフもプロット
・ambientといったクラウドサービスは使いたくない
・一定数値以上になったら、LINEに換気を促す通知を送りたい

ということで、これらの機能を満たすようなCO2濃度監視モニタを作ってみました。

使用したCO2センサーはAliexperessで購入

ja.aliexpress.com

MH-Z19Bという、この界隈ではよく使われる格安センサーです。
送料込みで20ドル程度で購入しました。


数値のリアルタイムプロットは、こちらのサイトを参考にさせていただきました。

magazine.halake.com

LINEへの通知は、過去に作ったこれを流用しています。

tmegane.hatenablog.com


ソースコード

コードはこんな感じ。
測定したCO2濃度が1000ppmを超えたら表示を黄色時に 1500ppmを超えたら、赤字にするかつLINEへ通知を送ります。 一度LINEに通知を送った後は、
濃度が1500ppmを下回ってから、再度上昇した時に再び通知を送るような仕様にしています。 そうしないと、1500ppmを超えたら無限に通知が来ることになっちゃいますからね。

1000ppmは厚生労働省の建築物環境衛生管理基準値
1500ppmは文部科学省の学校環境衛生基準値をそれぞれ採用しています。

#include "MHZ19.h"

#include <ssl_client.h>
#include <WiFiClientSecure.h>
#include <M5Stack.h>


// 表示に関係する定義
#define WIDTH 320
#define HEIGHT 240
#define GRAPH_X 25
#define GRAPH_Y 50
#define GRAPH_SPACE 2
#define GRAPH_W WIDTH - GRAPH_X - GRAPH_SPACE
#define GRAPH_H HEIGHT - GRAPH_Y - GRAPH_SPACE
#define GRAPH_MAX 3500
#define GRAPH_MIN 200


TFT_eSPI tft = TFT_eSPI();

MHZ19 myMHZ19;
HardwareSerial mySerial(2);

int flag=0;



uint16_t GraphBuff[int(GRAPH_W)] = {0};
uint16_t graphStartPos[2] = {
  GRAPH_X - 1,
  GRAPH_Y - 1 + int(GRAPH_H)
};


void wifi_connect() {
  const char* ssid = "SSID"; //Wifi SSID
  const char* passwd = "password"; //Wifi passwd
  WiFi.begin(ssid, passwd);

  while (WiFi.status() != WL_CONNECTED) { //Wifi 接続待ち
    delay(500);
  }

  Serial.println(WiFi.localIP());
}



void send_bot(int CO2_value) {

  const char* host = "notify-api.line.me"; // line_notifyのURL
  const char* token = "LINE API トークンキー"; //トークンキー

  const char* message1 = " ppm";
  const char* message2 = "換気しよう!";

  
  WiFiClientSecure client;
  if (!client.connect(host, 443)) {
    Serial.print("error1");
    return;
  }
  String query = String("message=") + String(CO2_value) + message1 + "\n" + message2;
  String request = String("") +
                   "POST /api/notify HTTP/1.1\r\n" +
                   "Host: " + host + "\r\n" +
                   "Authorization: Bearer " + token + "\r\n" +
                   "Content-Length: " + String(query.length()) +  "\r\n" +
                   "Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
                   query + "\r\n";
  client.print(request);
  while (client.connected()) { //受信待機
    String line = client.readStringUntil('\n');
    if (line == "\r") {
      break;
    }
  }

  Serial.print(request);


}


// -------  グラフに表示 -------

void slideBuff(uint16_t buff[], uint16_t size){
  for(int i = size - 1; i >= 0; i--) buff[i] = buff[i - 1];
}



void drawText(uint32_t x, uint32_t y, String text, uint32_t color = -1, uint8_t size = 1){
  M5.Lcd.setTextColor(color);
  M5.Lcd.setTextSize(size);
  M5.Lcd.setCursor(x, y);
  M5.Lcd.print(text);
}



void updateGraph(){
  M5.Lcd.fillRect(GRAPH_X + 1, GRAPH_Y + 1, GRAPH_W - GRAPH_SPACE, GRAPH_H - GRAPH_SPACE, 0);
  for(int i = 0; i < sizeof(GraphBuff); i++){
    M5.Lcd.drawPixel(graphStartPos[0] + i, graphStartPos[1] - GraphBuff[i], -1);
  }
}



void setupGraph(){
  M5.Lcd.drawRect(GRAPH_X, GRAPH_Y, GRAPH_W, GRAPH_H, -1);
  drawText(0, HEIGHT - 10, String(GRAPH_MIN), -1, 1);
  drawText(0, HEIGHT - GRAPH_SPACE - (int(GRAPH_H) / 2), String(((GRAPH_MAX - GRAPH_MIN) / 2) + GRAPH_MIN), -1, 1);
  drawText(0, HEIGHT - GRAPH_SPACE - int(GRAPH_H), String(GRAPH_MAX), -1, 1);
}

// -------  グラフに表示ここまで -------

void setup() {
 
  M5.begin();
  wifi_connect();
  
  Serial.begin(115200);
  mySerial.begin(9600, SERIAL_8N1, 16, 17);
  myMHZ19.begin(mySerial);
  setupGraph();



}


void loop() {
  
  M5.update();
 // tft.fillScreen(BLACK);

  
  
  int CO2 = myMHZ19.getCO2();
  int8_t Temp = myMHZ19.getTemperature();
  Serial.println("CO2 (ppm): " + String(CO2) + "\tTemperature (C): " + String(Temp));

  if(CO2 >= 1000 && CO2<1500){
    tft.setTextColor(YELLOW,BLACK);
    tft.setTextSize(3);
    tft.setCursor(3, 20);
    tft.println("CO2: " + String(CO2) + " ppm ");
  }else if(CO2 >= 1500) {
    tft.setTextColor(RED,BLACK);
    tft.setTextSize(3);
    tft.setCursor(3, 20);
    tft.println("CO2: " + String(CO2) + " ppm ");

    if(flag==0){
      send_bot(CO2);      
      flag=1;
    }
  
  } else {
    tft.setTextColor(WHITE,BLACK);
    tft.setTextSize(3);
    tft.setCursor(3, 20);
    tft.println("CO2: " + String(CO2) + " ppm ");
    flag=0;
  }

  slideBuff(GraphBuff, sizeof(GraphBuff) / 2); 
  GraphBuff[0] = map(CO2, GRAPH_MIN, GRAPH_MAX, 0, GRAPH_H - 2);
  updateGraph();

  
  delay(500);

  


}


M5StackとMH-Z19Bとの接続は簡単で、ジャンパ線で線を4本つなぐだけ。

M5Stack MH-Z19B
GND GND
5V Vin
TXD RX
RXD TX

動作させてみるとこんな感じ。

f:id:Tmegane:20200525144000j:plain:w200 f:id:Tmegane:20200525144022j:plain:w200f:id:Tmegane:20200525144010j:plain:w200

1500ppmを超えるとLINEに通知が来ます。

f:id:Tmegane:20200525144306p:plain

ちなみに、このセンサーは電源をいれてからヒーターが安定するまで3分程度かかります。
安定するまでは、突然濃度が高い値になり、LINEに通知を送ってしまうことがあるので
気になる人は電源をいれてから3分間は通知を送らないようなロジックを入れ込んでも良いと思います。


基板にしてみた

PCBWayのポイントが余っていたので、プリント基板にしました。

f:id:Tmegane:20200525144626j:plain:w300

M5Stackの横に挿して使うだけの簡単な基板です。

この様に、机の上に立てて使うことができます。
ジャンパ線もなくなり、すっきりしました。
f:id:Tmegane:20200525144713j:plain:w300

基板は例によって10枚作って余っているので、 気になる人がいたらTwitterかメールで連絡ください。
送料+100円分くらいのamazonギフト券 or Paypayで送ります。