CO2モニタリングでテレワーク環境の空気を可視化する

COVID-19によりテレワーク中心の生活になった方が多くいらっしゃるかと思います。
こういう私も、ここ1年で出社したのは10回程度と、ほぼテレワークで仕事をしています。

さて皆さんのテレワーク環境は、仕事をするにあたり快適な環境を維持しているでしょうか?
特に二酸化炭素(CO2)の濃度が上昇すると集中力が低下し眠気を催すことが解っています。
会議脱出ボタンの話題に絡むかもしれませんね)

note ご指定のページが見つかりません

国内の法律において、労働安全衛生法では労働環境は5,000ppm以下に保つ必要があると定められていますし、建築物における衛生的環境の確保に関する法律(ビル管理法)において3,000m2を超えるオフィスビル等においては 1,000ppm以下に保つ必要があると定められています。
(なお、大気中の二酸化炭素濃度は概ね400ppm程度です)

そこで業務環境を快適に保つため、CO2濃度を測定し一定値を超過した場合には通知するようにしたいと思います。

デバイス

今回は以下の組み合わせを準備しました。

なお、先に発売になったTVOC/eCO2 ガスセンサユニット(SGP30)も試しましたが、こちらは「CO2濃度はH2濃度に基づいて計算される」との記載の通り、異常値を示すことが多かったため、SCD30を採用した次第です。

アーキテクチャ

図に表すほどではありませんが、ひとまずこんな感じにしました。

Node-RED経由でローカル環境のZabbixに値を投入し可視化します。また、当方のZabbixはアラート発生時にBacklogに課題を自動起票するように設定されているので、アラート発生時は課題が追加されメールが届くようになります。

自宅内にNode-REDでMQTTブローカーを立ち上げて、M5Stackからのパブリッシュを受け付け、フォーマットを書き換えてZabbixに値を投入する流れです。

手順

デバイス側コード

M5Stack側のコードを記載します。
5秒おきにCO2濃度・温度・湿度の測定を測定し、30秒おきに平均値をMQTTブローカー宛にパブリッシュするよう設計しました。Node-REDのIPやWiFi関連パラメータは読み替えて下さい。

#include "M5Stack.h"
#include "SCD30.h"
#include "PubSubClient.h"
#include "WiFi.h"

#define SEND_INTERVAL     (30000)
#define SENSOR_INTERVAL   (5000)
#define MQTT_SERVER_HOST   "Node-REDのホスト名orIP"
#define MQTT_SERVER_PORT   (1883)
#define MQTT_OUTTOPICBASE  "co2sensor"

const char *wifissid = "WiFi-SSID";
const char *wifipass = "WiFi-Password";
const char *thingname = "M5Stack_Gray";

#if defined(ARDUINO_ARCH_AVR)
    #pragma message("Defined architecture for ARDUINO_ARCH_AVR.")
    #define SERIAL Serial
#elif defined(ARDUINO_ARCH_SAM)
    #pragma message("Defined architecture for ARDUINO_ARCH_SAM.")
    #define SERIAL SerialUSB
#elif defined(ARDUINO_ARCH_SAMD)
    #pragma message("Defined architecture for ARDUINO_ARCH_SAMD.")
    #define SERIAL SerialUSB
#elif defined(ARDUINO_ARCH_STM32F4)
    #pragma message("Defined architecture for ARDUINO_ARCH_STM32F4.")
    #define SERIAL SerialUSB
#else
    #pragma message("Not found any architecture.")
    #define SERIAL Serial
#endif

WiFiClient NetClient;
PubSubClient MqttClient;

void setup() {
  M5.begin();
  M5.Power.begin();
  // Initialize Serial Port
  Serial.begin(9600);  

  // Initialize Display
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(40, 10);
  M5.Lcd.print("Startup...");

  // Connect to WiFi
  connect_wifi();

  // Connect to MQTT Broker
  connect_mqtt();
    
  // Set LCD TextSize
  M5.Lcd.setTextSize(3);
}

void connect_wifi() {
    Serial.print("Connecting to ");
    Serial.println(wifissid);
    WiFi.begin(wifissid, wifipass);
   
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");    
    Serial.println("WiFi Connected.");
}

void connect_mqtt() {
    Serial.print("Connecting to MQTT Broker : ");
    Serial.print(MQTT_SERVER_HOST);
    Serial.print(":");
    Serial.println(MQTT_SERVER_PORT);
    MqttClient.setServer(MQTT_SERVER_HOST, MQTT_SERVER_PORT);
    MqttClient.setClient(NetClient);
    if (!MqttClient.connect(thingname)) {
      Serial.println(MqttClient.state());
      return;
    }
    Serial.println("MQTT Server Connected.");
}

void loop() {
  unsigned long now = millis();
  int n = 0, i;
  char payload[64];
  char content_length_hdr[32];
  char topicname[32];
  float measure[3] = {0};
  double sum[3] = {0}, ave[3];

  while (millis() < now + SEND_INTERVAL)
  {
    // delay to next measurement
    delay(SENSOR_INTERVAL);

    // Measurement CO2
    if (scd30.isAvailable()) {
        scd30.getCarbonDioxideConcentration(measure);
        for (i=0; i<=3; i++)
          sum[i] = sum[i] + (double)measure[i];
        n++;
    } 

    // Output to Serial
    SERIAL.print("CO2(ppm) : ");
    SERIAL.println(measure[0]);
    SERIAL.print("Temp(C)  : ");
    SERIAL.println(measure[1]);
    SERIAL.print("Humi(%)  : ");
    SERIAL.println(measure[2]);

    // Output to Display
    M5.Lcd.clear(BLACK);
    M5.Lcd.setCursor(40, 10);
    M5.Lcd.print("CO2");
    M5.Lcd.setCursor(40, 50);
    M5.Lcd.print(measure[0]);
    M5.Lcd.println(" ppm");

    M5.Lcd.setCursor(40, 90);
    M5.Lcd.print("Temp");
    M5.Lcd.setCursor(40, 130);
    M5.Lcd.print(measure[1]);
    M5.Lcd.println(" C");

    M5.Lcd.setCursor(40, 170);
    M5.Lcd.print("Humi");
    M5.Lcd.setCursor(40, 210);
    M5.Lcd.print(measure[2]);
    M5.Lcd.print(" %");
  }

  // Calculate&Print Average
  for (i=0; i<=3; i++)
    ave[i] = sum[i] / n;

  // Create Payload & Header
  sprintf(payload, "{\"co2ppm\": %5.2f,\"temp\": %3.2f,\"humi\": %3.2f}", ave[0], ave[1], ave[2]);
  sprintf(content_length_hdr, "Content-Length: %d", strlen(payload));

  // Reconnect WiFi if disconnected
  if (WiFi.status() != WL_CONNECTED) {
    connect_wifi();
  }

  // Reconnect MQTT Server if disconnected
  if (!MqttClient.connected()) {
    connect_mqtt();
  }

  // Publish
  sprintf(topicname, "%s/%s",MQTT_OUTTOPICBASE,thingname ); 
  SERIAL.print("topicname : ");
  SERIAL.println(topicname);  
  SERIAL.print("payload : ");
  SERIAL.println(payload);  

  MqttClient.publish(topicname, payload);
  M5.Lcd.println("  Sent.");
}

Node-RED

自宅Linuxサーバ上でコンテナにて起動しました。
当方自宅環境のDockerは特殊な環境で動作しているので参考になるかわかりませんが、以下のコマンドで起動しました。

# docker run -d --name nodered --hostname nodered --net shared_nw --ip 192.168.X.X --restart=always -it nodered/node-red
一般的なDocker環境では、HTTP及びMQTTのポート(1883/tcp)のマッピングが必要です。

起動後、以下のノードを追加します。

フローは単純ですが以下のようにしました。

MQTTブローカーにて co2sensor/# トピックでサブスクライブしています。
またJSON形式でパブリッシュされたデータをZabbix Senderに変換するロジックは関数で定義してしまいましたが、他の機能で代替できるかもしれません。

const orgjson = JSON.parse(msg.payload);
var dev_name   = msg.topic.split("/")[1];

var data = [
        [dev_name,"co2.ppm",orgjson.co2ppm],
        [dev_name,"temp",orgjson.temp],
        [dev_name,"humi",orgjson.humi]
    ];
msg.payload = data;

return msg;

Zabbix側設定

M5Stack側コードで指定したデバイス名(*thingname)の値でホストを作成します。
また、Node-REDのコードに合わせ、以下のアイテムを作成します。

  • co2.ppm : CO2濃度
  • temp : 温度
  • humi : 湿度

また、閾値によって通知するようトリガーの設定を行います。

結果

監視状況

以上により簡便にCO2濃度の測定ができるようになりました。

現在はモニターベゼルの空きスペースに設置して常時監視してます。
今は過ごしやすいシーズンで窓開放のため濃度は低めですが、今後冷房シーズンになると上昇しそうです。
監視して適度に換気を心がけたいと思います。

今後

監視通知がメールというのも能が無いですよね。
また、データを活かしてクラウドへのアップロードして可視化、分析に繋げたいですね。

今後もゆるりと開発して行ければと思います。

コメント